From ee44e51b30e16df889bdb3931d4653ab8e50da88 Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Wed, 6 Jun 2018 09:49:22 -0600 Subject: [PATCH] Better docs for the bank --- src/bank.rs | 94 +++++++++++++++++++++++++++++++++++++++++------------ src/lib.rs | 6 ++++ 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/src/bank.rs b/src/bank.rs index 8bd1f844f9..b1b6838b8f 100644 --- a/src/bank.rs +++ b/src/bank.rs @@ -1,6 +1,6 @@ -//! The `bank` module tracks client balances, and the progress of pending -//! transactions. It offers a high-level public API that signs transactions -//! on behalf of the caller, and a private low-level API for when they have +//! The `bank` module tracks client balances and the progress of smart +//! contracts. It offers a high-level API that signs transactions +//! on behalf of the caller, and a low-level API for when they have //! already been signed and verified. extern crate libc; @@ -19,25 +19,69 @@ use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering}; use std::sync::RwLock; use transaction::{Instruction, Plan, Transaction}; +/// The number of most recent `last_id` values that the bank will track the signatures +/// of. Once the bank discards a `last_id`, it will reject any transactions that use +/// that `last_id` in a transaction. Lowering this value reduces memory consumption, +/// but requires clients to update its `last_id` more frequently. Raising the value +/// lengthens the time a client must wait to be certain a missing transaction will +/// not be processed by the network. pub const MAX_ENTRY_IDS: usize = 1024 * 4; +/// Reasons a transaction might be rejected. #[derive(Debug, PartialEq, Eq)] pub enum BankError { + /// Attempt to debit from `PublicKey`, but no found no record of a prior credit. AccountNotFound(PublicKey), + + /// The requested debit from `PublicKey` has the potential to draw the balance + /// below zero. This can occur when a debit and credit are processed in parallel. + /// The bank may reject the debit or push it to a future entry. InsufficientFunds(PublicKey), + + /// The bank has seen `Signature` before. This can occur under normal operation + /// when a UDP packet is duplicated, as a user error from a client not updating + /// its `last_id`, or as a double-spend attack. DuplicateSiganture(Signature), + + /// The bank has not seen the given `last_id` or the transaction is too old and + /// the `last_id` has been discarded. LastIdNotFound(Hash), + + /// The transaction is invalid and has requested a debit or credit of negative + /// tokens. NegativeTokens, } pub type Result = result::Result; +/// The state of all accounts and contracts after processing its entries. pub struct Bank { + /// A map of account public keys to the balance in that account. balances: RwLock>, + + /// A map of smart contract transaction signatures to what remains of its payment + /// plan. Each transaction that targets the plan should cause it to be reduced. + /// Once it cannot be reduced, final payments are made and it is discarded. pending: RwLock>, + + /// A FIFO queue of `last_id` items, where each item is a set of signatures + /// that have been processed using that `last_id`. The bank uses this data to + /// reject transactions with signatures its seen before as well as `last_id` + /// values that are so old that its `last_id` has been pulled out of the queue. last_ids: RwLock>)>>, + + /// The set of trusted timekeepers. A Timestamp transaction from a `PublicKey` + /// outside this set will be discarded. Note that if validators do not have the + /// same set as leaders, they may interpret the ledger differently. time_sources: RwLock>, + + /// The most recent timestamp from a trusted timekeeper. This timestamp is applied + /// to every smart contract when it enters the system. If it is waiting on a + /// timestamp witness before that timestamp, the bank will execute it immediately. last_time: RwLock>, + + /// The number of transactions the bank has processed without error since the + /// start of the ledger. transaction_count: AtomicUsize, } @@ -67,7 +111,7 @@ impl Bank { bank } - /// Commit funds to the 'to' party. + /// Commit funds to the `payment.to` party. fn apply_payment(&self, payment: &Payment) { // First we check balances with a read lock to maximize potential parallelization. if self.balances @@ -89,13 +133,14 @@ impl Bank { } } - /// Return the last entry ID registered + /// Return the last entry ID registered. pub fn last_id(&self) -> Hash { let last_ids = self.last_ids.read().expect("'last_ids' read lock"); let last_item = last_ids.iter().last().expect("empty 'last_ids' list"); last_item.0 } + /// Store the given signature. The bank will reject any transaction with the same signature. fn reserve_signature(signatures: &RwLock>, sig: &Signature) -> Result<()> { if signatures .read() @@ -111,14 +156,16 @@ impl Bank { Ok(()) } - fn forget_signature(signatures: &RwLock>, sig: &Signature) { + /// Forget the given `signature` because its transaction was rejected. + fn forget_signature(signatures: &RwLock>, signature: &Signature) { signatures .write() .expect("'signatures' write lock in forget_signature") - .remove(sig); + .remove(signature); } - fn forget_signature_with_last_id(&self, sig: &Signature, last_id: &Hash) { + /// Forget the given `signature` with `last_id` because the transaction was rejected. + fn forget_signature_with_last_id(&self, signature: &Signature, last_id: &Hash) { if let Some(entry) = self.last_ids .read() .expect("'last_ids' read lock in forget_signature_with_last_id") @@ -126,11 +173,11 @@ impl Bank { .rev() .find(|x| x.0 == *last_id) { - Self::forget_signature(&entry.1, sig); + Self::forget_signature(&entry.1, signature); } } - fn reserve_signature_with_last_id(&self, sig: &Signature, last_id: &Hash) -> Result<()> { + fn reserve_signature_with_last_id(&self, signature: &Signature, last_id: &Hash) -> Result<()> { if let Some(entry) = self.last_ids .read() .expect("'last_ids' read lock in reserve_signature_with_last_id") @@ -138,7 +185,7 @@ impl Bank { .rev() .find(|x| x.0 == *last_id) { - return Self::reserve_signature(&entry.1, sig); + return Self::reserve_signature(&entry.1, signature); } Err(BankError::LastIdNotFound(*last_id)) } @@ -207,6 +254,8 @@ impl Bank { } } + /// Apply only a transaction's credits. Credits from multiple transactions + /// may safely be applied in parallel. fn apply_credits(&self, tx: &Transaction) { match &tx.instruction { Instruction::NewContract(contract) => { @@ -233,17 +282,17 @@ impl Bank { } } - /// Process a Transaction. + /// Process a Transaction. If it contains a payment plan that requires a witness + /// to progress, the payment plan will be stored in the bank. fn process_transaction(&self, tx: &Transaction) -> Result<()> { self.apply_debits(tx)?; self.apply_credits(tx); Ok(()) } - /// Process a batch of transactions. + /// Process a batch of transactions. It runs all debits first to filter out any + /// transactions that can't be processed in parallel deterministically. pub fn process_transactions(&self, txs: Vec) -> Vec> { - // Run all debits first to filter out any transactions that can't be processed - // in parallel deterministically. info!("processing Transactions {}", txs.len()); let results: Vec<_> = txs.into_par_iter() .map(|tx| self.apply_debits(&tx).map(|_| tx)) @@ -260,6 +309,7 @@ impl Bank { .collect() } + /// Process an ordered list of entries. pub fn process_entries(&self, entries: I) -> Result<()> where I: IntoIterator, @@ -273,7 +323,8 @@ impl Bank { Ok(()) } - /// Process a Witness Signature. + /// Process a Witness Signature. Any payment plans waiting on this signature + /// will progress one step. fn apply_signature(&self, from: PublicKey, tx_sig: Signature) -> Result<()> { if let Occupied(mut e) = self.pending .write() @@ -290,7 +341,8 @@ impl Bank { Ok(()) } - /// Process a Witness Timestamp. + /// Process a Witness Timestamp. Any payment plans waiting on this timestamp + /// will progress one step. fn apply_timestamp(&self, from: PublicKey, dt: DateTime) -> Result<()> { // If this is the first timestamp we've seen, it probably came from the genesis block, // so we'll trust it. @@ -392,7 +444,7 @@ mod tests { use signature::KeyPairUtil; #[test] - fn test_bank() { + fn test_two_payments_to_one_party() { let mint = Mint::new(10_000); let pubkey = KeyPair::new().pubkey(); let bank = Bank::new(&mint); @@ -409,7 +461,7 @@ mod tests { } #[test] - fn test_invalid_tokens() { + fn test_negative_tokens() { let mint = Mint::new(1); let pubkey = KeyPair::new().pubkey(); let bank = Bank::new(&mint); @@ -433,7 +485,7 @@ mod tests { } #[test] - fn test_invalid_transfer() { + fn test_insufficient_funds() { let mint = Mint::new(11_000); let bank = Bank::new(&mint); let pubkey = KeyPair::new().pubkey(); @@ -570,7 +622,7 @@ mod tests { } #[test] - fn test_max_entry_ids() { + fn test_reject_old_last_id() { let mint = Mint::new(1); let bank = Bank::new(&mint); let sig = Signature::default(); diff --git a/src/lib.rs b/src/lib.rs index 8583e75466..cf30c5d814 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,9 @@ +//! The `solana` library implements the Solana high-performance blockchain architecture. +//! It includes a full Rust implementation of the architecture as well as hooks to GPU +//! implementations of its most paralellizable components. It also includes command-line +//! tools to spin up fullnodes and a Rust library `thin_client` to interact with them. +//! + #![cfg_attr(feature = "unstable", feature(test))] pub mod bank; pub mod banking_stage;