Browse Source

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 <mvines@gmail.com>

* Differentiate faucet tx-length errors

* Populate signature in cli airdrop memo case

Co-authored-by: Michael Vines <mvines@gmail.com>
Tyera Eulberg 4 years ago
parent
commit
03d3ae1cb9

+ 3 - 0
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",
 ]
 

+ 21 - 8
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::<SystemError>(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::<SystemError>(result, &config)
+    }
 }
 
 fn process_balance(
@@ -1952,7 +1967,7 @@ impl FaucetKeypair {
         to_pubkey: &Pubkey,
         lamports: u64,
         blockhash: Hash,
-    ) -> Result<Self, Box<dyn error::Error>> {
+    ) -> Result<Self, FaucetError> {
         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<Signature> {
     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::<SystemError>(result, &config)
+    rpc_client.send_and_confirm_transaction_with_spinner(&tx)
 }
 
 pub fn log_instruction_custom_error<E>(

+ 0 - 4
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());

+ 6 - 23
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

+ 8 - 20
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);

+ 0 - 1
cli/tests/vote.rs

@@ -35,7 +35,6 @@ fn test_vote_authorize_and_withdraw() {
         &faucet_addr,
         &config.signers[0].pubkey(),
         100_000,
-        &config,
     )
     .unwrap();
 

+ 1 - 0
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" }

+ 13 - 0
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<ClientErrorKind> 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<TransactionError> for ClientError {
     }
 }
 
+impl From<FaucetError> for ClientError {
+    fn from(err: FaucetError) -> Self {
+        Self {
+            request: None,
+            kind: err.into(),
+        }
+    }
+}
+
 pub type Result<T> = std::result::Result<T, ClientError>;

+ 2 - 0
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]

+ 16 - 12
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;

+ 252 - 174
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.
-
-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,
+//! 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},
+    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<IpAddr>,
+    ip_cache: HashMap<IpAddr, u64>,
     pub time_slice: Duration,
-    per_time_cap: u64,
+    per_time_cap: Option<u64>,
     per_request_cap: Option<u64>,
-    pub request_current: u64,
 }
 
 impl Faucet {
@@ -83,40 +115,67 @@ impl Faucet {
         per_request_cap: Option<u64>,
     ) -> 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: <REQUEST> SOL cap: <CAP> SOL"
     pub fn build_airdrop_transaction(
         &mut self,
         req: FaucetRequest,
-    ) -> Result<Transaction, io::Error> {
+        ip: IpAddr,
+    ) -> Result<FaucetTransaction, FaucetError> {
         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);
-
-                    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
-                        ),
-                    ))
-                }
+                self.check_ip_time_request_limit(lamports, ip)?;
+
+                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<Vec<u8>, 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<Vec<u8>, 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<Transaction, Error> {
+) -> Result<Transaction, FaucetError> {
     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<u64> = None;
-        let request_cap: Option<u64> = None;
-        let faucet = Faucet::new(keypair, time_slice, request_cap, Some(100));
+        let per_time_cap: Option<u64> = Some(200);
+        let per_request_cap: Option<u64> = 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();
-
-        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 });
+        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!(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());
     }
 }

+ 9 - 6
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(

+ 98 - 0
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"