From 03d3ae1cb9a0209233dca68b1005c10afd0dc6a2 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Tue, 6 Apr 2021 01:01:05 -0600 Subject: [PATCH] Faucet: repurpose cap and slice args to apply to single IPs (#16381) * Single use stmt * Log request IP * Switch cap and slice to apply per IP * Use SOL in logs, error msgs * Use thiserror instead of overloading io::Error * Return memo transaction for requests that exceed per-request-cap * Handle faucet memos in cli * Add some docs, esp about memo transaction * Use SOL symbol & standardize memo Co-authored-by: Michael Vines * Differentiate faucet tx-length errors * Populate signature in cli airdrop memo case Co-authored-by: Michael Vines --- Cargo.lock | 3 + cli/src/cli.rs | 29 ++- cli/tests/nonce.rs | 4 - cli/tests/stake.rs | 29 +-- cli/tests/transfer.rs | 28 +-- cli/tests/vote.rs | 1 - client/Cargo.toml | 1 + client/src/client_error.rs | 13 ++ faucet/Cargo.toml | 2 + faucet/src/bin/faucet.rs | 28 +-- faucet/src/faucet.rs | 418 ++++++++++++++++++++++--------------- faucet/src/faucet_mock.rs | 15 +- programs/bpf/Cargo.lock | 98 +++++++++ 13 files changed, 425 insertions(+), 244 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0d429dd24..aab85920a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4252,6 +4252,7 @@ dependencies = [ "serde_json", "solana-account-decoder", "solana-clap-utils", + "solana-faucet", "solana-logger 1.7.0", "solana-net-utils", "solana-sdk", @@ -4458,6 +4459,8 @@ dependencies = [ "solana-metrics", "solana-sdk", "solana-version", + "spl-memo", + "thiserror", "tokio 1.1.1", ] diff --git a/cli/src/cli.rs b/cli/src/cli.rs index b77835ba0d..ba2bf1835c 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -35,6 +35,7 @@ use solana_client::{ }; #[cfg(not(test))] use solana_faucet::faucet::request_airdrop_transaction; +use solana_faucet::faucet::FaucetError; #[cfg(test)] use solana_faucet::faucet_mock::request_airdrop_transaction; use solana_remote_wallet::remote_wallet::RemoteWalletManager; @@ -1018,11 +1019,25 @@ fn process_airdrop( faucet_addr ); - request_and_confirm_airdrop(&rpc_client, faucet_addr, &pubkey, lamports, &config)?; + let pre_balance = rpc_client.get_balance(&pubkey)?; - let current_balance = rpc_client.get_balance(&pubkey)?; + let result = request_and_confirm_airdrop(&rpc_client, faucet_addr, &pubkey, lamports); + if let Ok(signature) = result { + let signature_cli_message = log_instruction_custom_error::(result, &config)?; + println!("{}", signature_cli_message); - Ok(build_balance_message(current_balance, false, true)) + let current_balance = rpc_client.get_balance(&pubkey)?; + + if current_balance < pre_balance.saturating_add(lamports) { + println!("Balance unchanged"); + println!("Run `solana confirm -v {:?}` for more info", signature); + Ok("".to_string()) + } else { + Ok(build_balance_message(current_balance, false, true)) + } + } else { + log_instruction_custom_error::(result, &config) + } } fn process_balance( @@ -1952,7 +1967,7 @@ impl FaucetKeypair { to_pubkey: &Pubkey, lamports: u64, blockhash: Hash, - ) -> Result> { + ) -> Result { let transaction = request_airdrop_transaction(faucet_addr, to_pubkey, lamports, blockhash)?; Ok(Self { transaction }) } @@ -1986,8 +2001,7 @@ pub fn request_and_confirm_airdrop( faucet_addr: &SocketAddr, to_pubkey: &Pubkey, lamports: u64, - config: &CliConfig, -) -> ProcessResult { +) -> ClientResult { let (blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?; let keypair = { let mut retries = 5; @@ -2001,8 +2015,7 @@ pub fn request_and_confirm_airdrop( } }?; let tx = keypair.airdrop_transaction(); - let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); - log_instruction_custom_error::(result, &config) + rpc_client.send_and_confirm_transaction_with_spinner(&tx) } pub fn log_instruction_custom_error( diff --git a/cli/tests/nonce.rs b/cli/tests/nonce.rs index e5ff8ca0e5..b4841bb7d6 100644 --- a/cli/tests/nonce.rs +++ b/cli/tests/nonce.rs @@ -74,7 +74,6 @@ fn full_battery_tests( &faucet_addr, &config_payer.signers[0].pubkey(), 2000, - &config_payer, ) .unwrap(); check_recent_balance(2000, &rpc_client, &config_payer.signers[0].pubkey()); @@ -228,7 +227,6 @@ fn test_create_account_with_seed() { let offline_nonce_authority_signer = keypair_from_seed(&[1u8; 32]).unwrap(); let online_nonce_creator_signer = keypair_from_seed(&[2u8; 32]).unwrap(); let to_address = Pubkey::new(&[3u8; 32]); - let config = CliConfig::recent_for_tests(); // Setup accounts let rpc_client = @@ -238,7 +236,6 @@ fn test_create_account_with_seed() { &faucet_addr, &offline_nonce_authority_signer.pubkey(), 42, - &config, ) .unwrap(); request_and_confirm_airdrop( @@ -246,7 +243,6 @@ fn test_create_account_with_seed() { &faucet_addr, &online_nonce_creator_signer.pubkey(), 4242, - &config, ) .unwrap(); check_recent_balance(42, &rpc_client, &offline_nonce_authority_signer.pubkey()); diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 827084da34..db4378c752 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -42,7 +42,6 @@ fn test_stake_delegation_force() { &faucet_addr, &config.signers[0].pubkey(), 100_000, - &config, ) .unwrap(); @@ -136,7 +135,6 @@ fn test_seed_stake_delegation_and_deactivation() { &faucet_addr, &config_validator.signers[0].pubkey(), 100_000, - &config_validator, ) .unwrap(); check_recent_balance(100_000, &rpc_client, &config_validator.signers[0].pubkey()); @@ -222,7 +220,6 @@ fn test_stake_delegation_and_deactivation() { &faucet_addr, &config_validator.signers[0].pubkey(), 100_000, - &config_validator, ) .unwrap(); check_recent_balance(100_000, &rpc_client, &config_validator.signers[0].pubkey()); @@ -313,7 +310,6 @@ fn test_offline_stake_delegation_and_deactivation() { &faucet_addr, &config_validator.signers[0].pubkey(), 100_000, - &config_offline, ) .unwrap(); check_recent_balance(100_000, &rpc_client, &config_validator.signers[0].pubkey()); @@ -323,7 +319,6 @@ fn test_offline_stake_delegation_and_deactivation() { &faucet_addr, &config_offline.signers[0].pubkey(), 100_000, - &config_validator, ) .unwrap(); check_recent_balance(100_000, &rpc_client, &config_offline.signers[0].pubkey()); @@ -445,7 +440,6 @@ fn test_nonced_stake_delegation_and_deactivation() { &faucet_addr, &config.signers[0].pubkey(), 100_000, - &config, ) .unwrap(); @@ -561,7 +555,6 @@ fn test_stake_authorize() { &faucet_addr, &config.signers[0].pubkey(), 100_000, - &config, ) .unwrap(); @@ -579,7 +572,6 @@ fn test_stake_authorize() { &faucet_addr, &config_offline.signers[0].pubkey(), 100_000, - &config, ) .unwrap(); @@ -844,16 +836,13 @@ fn test_stake_authorize_with_fee_payer() { config_offline.command = CliCommand::ClusterVersion; process_command(&config_offline).unwrap_err(); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &default_pubkey, 100_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &default_pubkey, 100_000).unwrap(); check_recent_balance(100_000, &rpc_client, &config.signers[0].pubkey()); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &payer_pubkey, 100_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &payer_pubkey, 100_000).unwrap(); check_recent_balance(100_000, &rpc_client, &payer_pubkey); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000).unwrap(); check_recent_balance(100_000, &rpc_client, &offline_pubkey); check_ready(&rpc_client); @@ -973,13 +962,11 @@ fn test_stake_split() { &faucet_addr, &config.signers[0].pubkey(), 500_000, - &config, ) .unwrap(); check_recent_balance(500_000, &rpc_client, &config.signers[0].pubkey()); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000).unwrap(); check_recent_balance(100_000, &rpc_client, &offline_pubkey); // Create stake account, identity is authority @@ -1122,13 +1109,11 @@ fn test_stake_set_lockup() { &faucet_addr, &config.signers[0].pubkey(), 500_000, - &config, ) .unwrap(); check_recent_balance(500_000, &rpc_client, &config.signers[0].pubkey()); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000).unwrap(); check_recent_balance(100_000, &rpc_client, &offline_pubkey); // Create stake account, identity is authority @@ -1386,13 +1371,11 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { &faucet_addr, &config.signers[0].pubkey(), 200_000, - &config, ) .unwrap(); check_recent_balance(200_000, &rpc_client, &config.signers[0].pubkey()); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000).unwrap(); check_recent_balance(100_000, &rpc_client, &offline_pubkey); // Create nonce account diff --git a/cli/tests/transfer.rs b/cli/tests/transfer.rs index c9ef81a801..fe9fba125a 100644 --- a/cli/tests/transfer.rs +++ b/cli/tests/transfer.rs @@ -38,8 +38,7 @@ fn test_transfer() { let sender_pubkey = config.signers[0].pubkey(); let recipient_pubkey = Pubkey::new(&[1u8; 32]); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000).unwrap(); check_recent_balance(50_000, &rpc_client, &sender_pubkey); check_recent_balance(0, &rpc_client, &recipient_pubkey); @@ -95,7 +94,7 @@ fn test_transfer() { process_command(&offline).unwrap_err(); let offline_pubkey = offline.signers[0].pubkey(); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 50, &config).unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 50).unwrap(); check_recent_balance(50, &rpc_client, &offline_pubkey); // Offline transfer @@ -281,25 +280,17 @@ fn test_transfer_multisession_signing() { let offline_from_signer = keypair_from_seed(&[2u8; 32]).unwrap(); let offline_fee_payer_signer = keypair_from_seed(&[3u8; 32]).unwrap(); let from_null_signer = NullSigner::new(&offline_from_signer.pubkey()); - let config = CliConfig::recent_for_tests(); // Setup accounts let rpc_client = RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed()); - request_and_confirm_airdrop( - &rpc_client, - &faucet_addr, - &offline_from_signer.pubkey(), - 43, - &config, - ) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_from_signer.pubkey(), 43) + .unwrap(); request_and_confirm_airdrop( &rpc_client, &faucet_addr, &offline_fee_payer_signer.pubkey(), 3, - &config, ) .unwrap(); check_recent_balance(43, &rpc_client, &offline_from_signer.pubkey()); @@ -418,8 +409,7 @@ fn test_transfer_all() { let sender_pubkey = config.signers[0].pubkey(); let recipient_pubkey = Pubkey::new(&[1u8; 32]); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000).unwrap(); check_recent_balance(50_000, &rpc_client, &sender_pubkey); check_recent_balance(0, &rpc_client, &recipient_pubkey); @@ -466,8 +456,7 @@ fn test_transfer_unfunded_recipient() { let sender_pubkey = config.signers[0].pubkey(); let recipient_pubkey = Pubkey::new(&[1u8; 32]); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000).unwrap(); check_recent_balance(50_000, &rpc_client, &sender_pubkey); check_recent_balance(0, &rpc_client, &recipient_pubkey); @@ -522,9 +511,8 @@ fn test_transfer_with_seed() { ) .unwrap(); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 1, &config).unwrap(); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &derived_address, 50_000, &config) - .unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 1).unwrap(); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &derived_address, 50_000).unwrap(); check_recent_balance(1, &rpc_client, &sender_pubkey); check_recent_balance(50_000, &rpc_client, &derived_address); check_recent_balance(0, &rpc_client, &recipient_pubkey); diff --git a/cli/tests/vote.rs b/cli/tests/vote.rs index cbdd4a4c54..f5333cb12b 100644 --- a/cli/tests/vote.rs +++ b/cli/tests/vote.rs @@ -35,7 +35,6 @@ fn test_vote_authorize_and_withdraw() { &faucet_addr, &config.signers[0].pubkey(), 100_000, - &config, ) .unwrap(); diff --git a/client/Cargo.toml b/client/Cargo.toml index 8dfc3e66e2..4ed8a9db0b 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -26,6 +26,7 @@ serde_derive = "1.0.103" serde_json = "1.0.56" solana-account-decoder = { path = "../account-decoder", version = "=1.7.0" } solana-clap-utils = { path = "../clap-utils", version = "=1.7.0" } +solana-faucet = { path = "../faucet", version = "=1.7.0" } solana-net-utils = { path = "../net-utils", version = "=1.7.0" } solana-sdk = { path = "../sdk", version = "=1.7.0" } solana-transaction-status = { path = "../transaction-status", version = "=1.7.0" } diff --git a/client/src/client_error.rs b/client/src/client_error.rs index 67aa5717d4..fec5d047c3 100644 --- a/client/src/client_error.rs +++ b/client/src/client_error.rs @@ -1,5 +1,6 @@ use { crate::rpc_request, + solana_faucet::faucet::FaucetError, solana_sdk::{ signature::SignerError, transaction::TransactionError, transport::TransportError, }, @@ -23,6 +24,8 @@ pub enum ClientErrorKind { SigningError(#[from] SignerError), #[error(transparent)] TransactionError(#[from] TransactionError), + #[error(transparent)] + FaucetError(#[from] FaucetError), #[error("Custom: {0}")] Custom(String), } @@ -46,6 +49,7 @@ impl From for TransportError { ClientErrorKind::RpcError(err) => Self::Custom(format!("{:?}", err)), ClientErrorKind::SerdeJson(err) => Self::Custom(format!("{:?}", err)), ClientErrorKind::SigningError(err) => Self::Custom(format!("{:?}", err)), + ClientErrorKind::FaucetError(err) => Self::Custom(format!("{:?}", err)), ClientErrorKind::Custom(err) => Self::Custom(format!("{:?}", err)), } } @@ -162,4 +166,13 @@ impl From for ClientError { } } +impl From for ClientError { + fn from(err: FaucetError) -> Self { + Self { + request: None, + kind: err.into(), + } + } +} + pub type Result = std::result::Result; diff --git a/faucet/Cargo.toml b/faucet/Cargo.toml index 937b369475..1488ebd252 100644 --- a/faucet/Cargo.toml +++ b/faucet/Cargo.toml @@ -22,6 +22,8 @@ solana-logger = { path = "../logger", version = "=1.7.0" } solana-metrics = { path = "../metrics", version = "=1.7.0" } solana-sdk = { path = "../sdk", version = "=1.7.0" } solana-version = { path = "../version", version = "=1.7.0" } +spl-memo = { version = "=3.0.1", features = ["no-entrypoint"] } +thiserror = "1.0" tokio = { version = "1", features = ["full"] } [lib] diff --git a/faucet/src/bin/faucet.rs b/faucet/src/bin/faucet.rs index 0d4b9b76dc..64b6d64521 100644 --- a/faucet/src/bin/faucet.rs +++ b/faucet/src/bin/faucet.rs @@ -1,14 +1,17 @@ -use clap::{crate_description, crate_name, App, Arg}; -use solana_clap_utils::input_parsers::{lamports_of_sol, value_of}; -use solana_faucet::{ - faucet::{run_faucet, Faucet, FAUCET_PORT}, - socketaddr, -}; -use solana_sdk::signature::read_keypair_file; -use std::{ - net::{Ipv4Addr, SocketAddr}, - sync::{Arc, Mutex}, - thread, +use { + clap::{crate_description, crate_name, App, Arg}, + log::*, + solana_clap_utils::input_parsers::{lamports_of_sol, value_of}, + solana_faucet::{ + faucet::{run_faucet, Faucet, FAUCET_PORT}, + socketaddr, + }, + solana_sdk::signature::read_keypair_file, + std::{ + net::{Ipv4Addr, SocketAddr}, + sync::{Arc, Mutex}, + thread, + }, }; #[tokio::main] @@ -74,7 +77,8 @@ async fn main() { thread::spawn(move || loop { let time = faucet1.lock().unwrap().time_slice; thread::sleep(time); - faucet1.lock().unwrap().clear_request_count(); + debug!("clearing ip cache"); + faucet1.lock().unwrap().clear_ip_cache(); }); run_faucet(faucet, faucet_addr, None).await; diff --git a/faucet/src/faucet.rs b/faucet/src/faucet.rs index ea3ebc28ee..68b742d562 100644 --- a/faucet/src/faucet.rs +++ b/faucet/src/faucet.rs @@ -1,34 +1,40 @@ //! The `faucet` module provides an object for launching a Solana Faucet, //! which is the custodian of any remaining lamports in a mint. -//! The Solana Faucet builds and send airdrop transactions, -//! checking requests against a request cap for a given time time_slice -//! and (to come) an IP rate limit. +//! The Solana Faucet builds and sends airdrop transactions, +//! checking requests against a single-request cap and a per-IP limit +//! for a given time time_slice. -use bincode::{deserialize, serialize, serialized_size}; -use byteorder::{ByteOrder, LittleEndian}; -use log::*; -use serde_derive::{Deserialize, Serialize}; -use solana_metrics::datapoint_info; -use solana_sdk::{ - hash::Hash, - message::Message, - packet::PACKET_DATA_SIZE, - pubkey::Pubkey, - signature::{Keypair, Signer}, - system_instruction, - transaction::Transaction, -}; -use std::{ - io::{self, Error, ErrorKind, Read, Write}, - net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, - sync::{mpsc::Sender, Arc, Mutex}, - thread, - time::Duration, -}; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::{TcpListener, TcpStream as TokioTcpStream}, - runtime::Runtime, +use { + bincode::{deserialize, serialize, serialized_size}, + byteorder::{ByteOrder, LittleEndian}, + log::*, + serde_derive::{Deserialize, Serialize}, + solana_metrics::datapoint_info, + solana_sdk::{ + hash::Hash, + instruction::Instruction, + message::Message, + native_token::lamports_to_sol, + packet::PACKET_DATA_SIZE, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, + }, + std::{ + collections::HashMap, + io::{Read, Write}, + net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}, + sync::{mpsc::Sender, Arc, Mutex}, + thread, + time::Duration, + }, + thiserror::Error, + tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{TcpListener, TcpStream as TokioTcpStream}, + runtime::Runtime, + }, }; #[macro_export] @@ -42,11 +48,33 @@ macro_rules! socketaddr { }}; } +const ERROR_RESPONSE: [u8; 2] = 0u16.to_le_bytes(); + pub const TIME_SLICE: u64 = 60; -pub const REQUEST_CAP: u64 = solana_sdk::native_token::LAMPORTS_PER_SOL * 10_000_000; pub const FAUCET_PORT: u16 = 9900; pub const FAUCET_PORT_STR: &str = "9900"; +#[derive(Error, Debug)] +pub enum FaucetError { + #[error("IO Error: {0}")] + IoError(#[from] std::io::Error), + + #[error("serialization error: {0}")] + Serialize(#[from] bincode::Error), + + #[error("transaction_length from faucet exceeds limit: {0}")] + TransactionDataTooLarge(usize), + + #[error("transaction_length from faucet: 0")] + NoDataReceived, + + #[error("request too large; req: ◎{0}, cap: ◎{1}")] + PerRequestCapExceeded(f64, f64), + + #[error("IP limit reached; req: ◎{0}, ip: {1}, current: ◎{2}, cap: ◎{3}")] + PerTimeCapExceeded(f64, IpAddr, f64, f64), +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub enum FaucetRequest { GetAirdrop { @@ -66,13 +94,17 @@ impl Default for FaucetRequest { } } +pub enum FaucetTransaction { + Airdrop(Transaction), + Memo((Transaction, String)), +} + pub struct Faucet { faucet_keypair: Keypair, - ip_cache: Vec, + ip_cache: HashMap, pub time_slice: Duration, - per_time_cap: u64, + per_time_cap: Option, per_request_cap: Option, - pub request_current: u64, } impl Faucet { @@ -83,40 +115,67 @@ impl Faucet { per_request_cap: Option, ) -> Faucet { let time_slice = Duration::new(time_input.unwrap_or(TIME_SLICE), 0); - let per_time_cap = per_time_cap.unwrap_or(REQUEST_CAP); + if let Some((per_request_cap, per_time_cap)) = per_request_cap.zip(per_time_cap) { + if per_time_cap < per_request_cap { + warn!( + "Ip per_time_cap {} SOL < per_request_cap {} SOL; \ + maximum single requests will fail", + lamports_to_sol(per_time_cap), + lamports_to_sol(per_request_cap), + ); + } + } Faucet { faucet_keypair, - ip_cache: Vec::new(), + ip_cache: HashMap::new(), time_slice, per_time_cap, per_request_cap, - request_current: 0, } } - pub fn check_time_request_limit(&mut self, request_amount: u64) -> bool { - self.request_current - .checked_add(request_amount) - .map(|s| s <= self.per_time_cap) - .unwrap_or(false) - } - - pub fn clear_request_count(&mut self) { - self.request_current = 0; - } - - pub fn add_ip_to_cache(&mut self, ip: IpAddr) { - self.ip_cache.push(ip); + pub fn check_ip_time_request_limit( + &mut self, + request_amount: u64, + ip: IpAddr, + ) -> Result<(), FaucetError> { + let ip_new_total = self + .ip_cache + .entry(ip) + .and_modify(|total| *total = total.saturating_add(request_amount)) + .or_insert(request_amount); + datapoint_info!( + "faucet-airdrop", + ("request_amount", request_amount, i64), + ("ip", ip.to_string(), String), + ("ip_new_total", *ip_new_total, i64) + ); + if let Some(cap) = self.per_time_cap { + if *ip_new_total > cap { + return Err(FaucetError::PerTimeCapExceeded( + lamports_to_sol(request_amount), + ip, + lamports_to_sol(*ip_new_total), + lamports_to_sol(cap), + )); + } + } + Ok(()) } pub fn clear_ip_cache(&mut self) { self.ip_cache.clear(); } + /// Checks per-request and per-time-ip limits; if both pass, this method returns a signed + /// SystemProgram::Transfer transaction from the faucet keypair to the requested recipient. If + /// the request exceeds this per-request limit, this method returns a signed SPL Memo + /// transaction with the memo: "request too large; req: SOL cap: SOL" pub fn build_airdrop_transaction( &mut self, req: FaucetRequest, - ) -> Result { + ip: IpAddr, + ) -> Result { trace!("build_airdrop_transaction: {:?}", req); match req { FaucetRequest::GetAirdrop { @@ -124,72 +183,80 @@ impl Faucet { to, blockhash, } => { + let mint_pubkey = self.faucet_keypair.pubkey(); + info!( + "Requesting airdrop of {} SOL to {:?}", + lamports_to_sol(lamports), + to + ); + if let Some(cap) = self.per_request_cap { if lamports > cap { - return Err(Error::new( - ErrorKind::Other, - format!("request too large; req: {} cap: {}", lamports, cap), - )); + let memo = format!( + "{}", + FaucetError::PerRequestCapExceeded( + lamports_to_sol(lamports), + lamports_to_sol(cap), + ) + ); + let memo_instruction = Instruction { + program_id: Pubkey::new(&spl_memo::id().to_bytes()), + accounts: vec![], + data: memo.as_bytes().to_vec(), + }; + let message = Message::new(&[memo_instruction], Some(&mint_pubkey)); + return Ok(FaucetTransaction::Memo(( + Transaction::new(&[&self.faucet_keypair], message, blockhash), + memo, + ))); } } - if self.check_time_request_limit(lamports) { - self.request_current = self.request_current.saturating_add(lamports); - datapoint_info!( - "faucet-airdrop", - ("request_amount", lamports, i64), - ("request_current", self.request_current, i64) - ); - info!("Requesting airdrop of {} to {:?}", lamports, to); + self.check_ip_time_request_limit(lamports, ip)?; - let mint_pubkey = self.faucet_keypair.pubkey(); - let create_instruction = - system_instruction::transfer(&mint_pubkey, &to, lamports); - let message = Message::new(&[create_instruction], Some(&mint_pubkey)); - Ok(Transaction::new( - &[&self.faucet_keypair], - message, - blockhash, - )) - } else { - Err(Error::new( - ErrorKind::Other, - format!( - "token limit reached; req: {} current: {} cap: {}", - lamports, self.request_current, self.per_time_cap - ), - )) - } + let transfer_instruction = + system_instruction::transfer(&mint_pubkey, &to, lamports); + let message = Message::new(&[transfer_instruction], Some(&mint_pubkey)); + Ok(FaucetTransaction::Airdrop(Transaction::new( + &[&self.faucet_keypair], + message, + blockhash, + ))) } } } - pub fn process_faucet_request(&mut self, bytes: &[u8]) -> Result, io::Error> { - let req: FaucetRequest = deserialize(bytes).map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("deserialize packet in faucet: {:?}", err), - ) - })?; + + /// Deserializes a received airdrop request, and returns a serialized transaction + pub fn process_faucet_request( + &mut self, + bytes: &[u8], + ip: IpAddr, + ) -> Result, FaucetError> { + let req: FaucetRequest = deserialize(bytes)?; info!("Airdrop transaction requested...{:?}", req); - let res = self.build_airdrop_transaction(req); + let res = self.build_airdrop_transaction(req, ip); match res { Ok(tx) => { - let response_vec = bincode::serialize(&tx).map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("deserialize packet in faucet: {:?}", err), - ) - })?; + let tx = match tx { + FaucetTransaction::Airdrop(tx) => { + info!("Airdrop transaction granted"); + tx + } + FaucetTransaction::Memo((tx, memo)) => { + warn!("Memo transaction returned: {}", memo); + tx + } + }; + let response_vec = bincode::serialize(&tx)?; let mut response_vec_with_length = vec![0; 2]; LittleEndian::write_u16(&mut response_vec_with_length, response_vec.len() as u16); response_vec_with_length.extend_from_slice(&response_vec); - info!("Airdrop transaction granted"); Ok(response_vec_with_length) } Err(err) => { - warn!("Airdrop transaction failed: {:?}", err); + warn!("Airdrop transaction failed: {}", err); Err(err) } } @@ -207,7 +274,7 @@ pub fn request_airdrop_transaction( id: &Pubkey, lamports: u64, blockhash: Hash, -) -> Result { +) -> Result { info!( "request_airdrop_transaction: faucet_addr={} id={} lamports={} blockhash={}", faucet_addr, id, lamports, blockhash @@ -230,17 +297,13 @@ pub fn request_airdrop_transaction( "request_airdrop_transaction: buffer length read_exact error: {:?}", err ); - Error::new(ErrorKind::Other, "Airdrop failed") + err })?; let transaction_length = LittleEndian::read_u16(&buffer) as usize; - if transaction_length > PACKET_DATA_SIZE || transaction_length == 0 { - return Err(Error::new( - ErrorKind::Other, - format!( - "request_airdrop_transaction: invalid transaction_length from faucet: {}", - transaction_length - ), - )); + if transaction_length > PACKET_DATA_SIZE { + return Err(FaucetError::TransactionDataTooLarge(transaction_length)); + } else if transaction_length == 0 { + return Err(FaucetError::NoDataReceived); } // Read the transaction @@ -251,15 +314,10 @@ pub fn request_airdrop_transaction( "request_airdrop_transaction: buffer read_exact error: {:?}", err ); - Error::new(ErrorKind::Other, "Airdrop failed") + err })?; - let transaction: Transaction = deserialize(&buffer).map_err(|err| { - Error::new( - ErrorKind::Other, - format!("request_airdrop_transaction deserialize failure: {:?}", err), - ) - })?; + let transaction: Transaction = deserialize(&buffer)?; Ok(transaction) } @@ -347,14 +405,27 @@ async fn process( while stream.read_exact(&mut request).await.is_ok() { trace!("{:?}", request); - let response = match faucet.lock().unwrap().process_faucet_request(&request) { - Ok(response_bytes) => { - trace!("Airdrop response_bytes: {:?}", response_bytes); - response_bytes - } - Err(e) => { - info!("Error in request: {:?}", e); - 0u16.to_le_bytes().to_vec() + let response = { + match stream.peer_addr() { + Err(e) => { + info!("{:?}", e.into_inner()); + ERROR_RESPONSE.to_vec() + } + Ok(peer_addr) => { + let ip = peer_addr.ip(); + info!("Request IP: {:?}", ip); + + match faucet.lock().unwrap().process_faucet_request(&request, ip) { + Ok(response_bytes) => { + trace!("Airdrop response_bytes: {:?}", response_bytes); + response_bytes + } + Err(e) => { + info!("Error in request: {}", e); + ERROR_RESPONSE.to_vec() + } + } + } } }; stream.write_all(&response).await?; @@ -370,35 +441,13 @@ mod tests { use std::time::Duration; #[test] - fn test_check_time_request_limit() { + fn test_check_ip_time_request_limit() { let keypair = Keypair::new(); - let mut faucet = Faucet::new(keypair, None, Some(3), None); - assert!(faucet.check_time_request_limit(1)); - faucet.request_current = 3; - assert!(!faucet.check_time_request_limit(1)); - faucet.request_current = 1; - assert!(!faucet.check_time_request_limit(u64::MAX)); - } - - #[test] - fn test_clear_request_count() { - let keypair = Keypair::new(); - let mut faucet = Faucet::new(keypair, None, None, None); - faucet.request_current += 256; - assert_eq!(faucet.request_current, 256); - faucet.clear_request_count(); - assert_eq!(faucet.request_current, 0); - } - - #[test] - fn test_add_ip_to_cache() { - let keypair = Keypair::new(); - let mut faucet = Faucet::new(keypair, None, None, None); - let ip = "127.0.0.1".parse().expect("create IpAddr from string"); - assert_eq!(faucet.ip_cache.len(), 0); - faucet.add_ip_to_cache(ip); - assert_eq!(faucet.ip_cache.len(), 1); - assert!(faucet.ip_cache.contains(&ip)); + let mut faucet = Faucet::new(keypair, None, Some(2), None); + let ip = socketaddr!([203, 0, 113, 1], 1234).ip(); + assert!(faucet.check_ip_time_request_limit(1, ip).is_ok()); + assert!(faucet.check_ip_time_request_limit(1, ip).is_ok()); + assert!(faucet.check_ip_time_request_limit(1, ip).is_err()); } #[test] @@ -407,7 +456,7 @@ mod tests { let mut faucet = Faucet::new(keypair, None, None, None); let ip = "127.0.0.1".parse().expect("create IpAddr from string"); assert_eq!(faucet.ip_cache.len(), 0); - faucet.add_ip_to_cache(ip); + faucet.check_ip_time_request_limit(1, ip).unwrap(); assert_eq!(faucet.ip_cache.len(), 1); faucet.clear_ip_cache(); assert_eq!(faucet.ip_cache.len(), 0); @@ -418,11 +467,12 @@ mod tests { fn test_faucet_default_init() { let keypair = Keypair::new(); let time_slice: Option = None; - let request_cap: Option = None; - let faucet = Faucet::new(keypair, time_slice, request_cap, Some(100)); + let per_time_cap: Option = Some(200); + let per_request_cap: Option = Some(100); + let faucet = Faucet::new(keypair, time_slice, per_time_cap, per_request_cap); assert_eq!(faucet.time_slice, Duration::new(TIME_SLICE, 0)); - assert_eq!(faucet.per_time_cap, REQUEST_CAP); - assert_eq!(faucet.per_request_cap, Some(100)); + assert_eq!(faucet.per_time_cap, per_time_cap); + assert_eq!(faucet.per_request_cap, per_request_cap); } #[test] @@ -434,36 +484,63 @@ mod tests { to, blockhash, }; + let ip = socketaddr!([203, 0, 113, 1], 1234).ip(); let mint = Keypair::new(); let mint_pubkey = mint.pubkey(); let mut faucet = Faucet::new(mint, None, None, None); - let tx = faucet.build_airdrop_transaction(request).unwrap(); - let message = tx.message(); + if let FaucetTransaction::Airdrop(tx) = + faucet.build_airdrop_transaction(request, ip).unwrap() + { + let message = tx.message(); - assert_eq!(tx.signatures.len(), 1); - assert_eq!( - message.account_keys, - vec![mint_pubkey, to, Pubkey::default()] - ); - assert_eq!(message.recent_blockhash, blockhash); + assert_eq!(tx.signatures.len(), 1); + assert_eq!( + message.account_keys, + vec![mint_pubkey, to, Pubkey::default()] + ); + assert_eq!(message.recent_blockhash, blockhash); - assert_eq!(message.instructions.len(), 1); - let instruction: SystemInstruction = deserialize(&message.instructions[0].data).unwrap(); - assert_eq!(instruction, SystemInstruction::Transfer { lamports: 2 }); + assert_eq!(message.instructions.len(), 1); + let instruction: SystemInstruction = + deserialize(&message.instructions[0].data).unwrap(); + assert_eq!(instruction, SystemInstruction::Transfer { lamports: 2 }); + } else { + panic!("airdrop should succeed"); + } // Test per-time request cap let mint = Keypair::new(); faucet = Faucet::new(mint, None, Some(1), None); - let tx = faucet.build_airdrop_transaction(request); + let tx = faucet.build_airdrop_transaction(request, ip); assert!(tx.is_err()); // Test per-request cap let mint = Keypair::new(); - faucet = Faucet::new(mint, None, None, Some(1)); - let tx = faucet.build_airdrop_transaction(request); - assert!(tx.is_err()); + let mint_pubkey = mint.pubkey(); + let mut faucet = Faucet::new(mint, None, None, Some(1)); + + if let FaucetTransaction::Memo((tx, memo)) = + faucet.build_airdrop_transaction(request, ip).unwrap() + { + let message = tx.message(); + + assert_eq!(tx.signatures.len(), 1); + assert_eq!( + message.account_keys, + vec![mint_pubkey, Pubkey::new(&spl_memo::id().to_bytes())] + ); + assert_eq!(message.recent_blockhash, blockhash); + + assert_eq!(message.instructions.len(), 1); + let parsed_memo = std::str::from_utf8(&message.instructions[0].data).unwrap(); + let expected_memo = "request too large; req: ◎0.000000002, cap: ◎0.000000001"; + assert_eq!(parsed_memo, expected_memo); + assert_eq!(memo, expected_memo); + } else { + panic!("airdrop attempt should result in memo tx"); + } } #[test] @@ -476,6 +553,7 @@ mod tests { blockhash, to, }; + let ip = socketaddr!([203, 0, 113, 1], 1234).ip(); let req = serialize(&req).unwrap(); let keypair = Keypair::new(); @@ -488,11 +566,11 @@ mod tests { expected_vec_with_length.extend_from_slice(&expected_bytes); let mut faucet = Faucet::new(keypair, None, None, None); - let response = faucet.process_faucet_request(&req); + let response = faucet.process_faucet_request(&req, ip); let response_vec = response.unwrap().to_vec(); assert_eq!(expected_vec_with_length, response_vec); let bad_bytes = "bad bytes".as_bytes(); - assert!(faucet.process_faucet_request(&bad_bytes).is_err()); + assert!(faucet.process_faucet_request(&bad_bytes, ip).is_err()); } } diff --git a/faucet/src/faucet_mock.rs b/faucet/src/faucet_mock.rs index b5e27b10d9..74c740cac8 100644 --- a/faucet/src/faucet_mock.rs +++ b/faucet/src/faucet_mock.rs @@ -1,9 +1,12 @@ -use solana_sdk::{ - hash::Hash, pubkey::Pubkey, signature::Keypair, system_transaction, transaction::Transaction, -}; -use std::{ - io::{Error, ErrorKind}, - net::SocketAddr, +use { + solana_sdk::{ + hash::Hash, pubkey::Pubkey, signature::Keypair, system_transaction, + transaction::Transaction, + }, + std::{ + io::{Error, ErrorKind}, + net::SocketAddr, + }, }; pub fn request_airdrop_transaction( diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index abec2e77da..a55cf3594a 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -706,6 +706,33 @@ dependencies = [ "walkdir", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.8", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "ed25519" version = "1.0.1" @@ -1430,6 +1457,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lock_api" version = "0.3.4" @@ -2164,6 +2197,16 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.1", + "redox_syscall 0.2.4", +] + [[package]] name = "regex" version = "1.3.9" @@ -2461,6 +2504,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + [[package]] name = "sha-1" version = "0.8.2" @@ -2897,6 +2952,18 @@ dependencies = [ "url", ] +[[package]] +name = "solana-cli-config" +version = "1.7.0" +dependencies = [ + "dirs-next", + "lazy_static", + "serde", + "serde_derive", + "serde_yaml", + "url", +] + [[package]] name = "solana-cli-output" version = "1.7.0" @@ -2940,6 +3007,7 @@ dependencies = [ "serde_json", "solana-account-decoder", "solana-clap-utils", + "solana-faucet", "solana-net-utils", "solana-sdk", "solana-transaction-status", @@ -2986,6 +3054,27 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "solana-faucet" +version = "1.7.0" +dependencies = [ + "bincode", + "byteorder 1.3.4", + "clap", + "log", + "serde", + "serde_derive", + "solana-clap-utils", + "solana-cli-config", + "solana-logger 1.7.0", + "solana-metrics", + "solana-sdk", + "solana-version", + "spl-memo", + "thiserror", + "tokio 1.1.1", +] + [[package]] name = "solana-frozen-abi" version = "1.6.4" @@ -4263,6 +4352,15 @@ dependencies = [ "libc", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" version = "1.2.0"