From 6ec0e422201b996b13469f8ec48c8019495b9add Mon Sep 17 00:00:00 2001 From: anatoly yakovenko Date: Mon, 17 Sep 2018 13:36:31 -0700 Subject: [PATCH] budget as separate contract and system call contract (#1189) * budget and system contracts and verification * contract check_id methods * system call contract * verify contract execution rules * move system into its own file * allocate before transfer for budget * store error in budget context * budget contract and tests without bank * moved budget of of bank --- src/bank.rs | 434 ++++++++++++-------------------------- src/budget_contract.rs | 469 +++++++++++++++++++++++++++++++++++++++++ src/crdt.rs | 4 +- src/entry.rs | 24 ++- src/instruction.rs | 68 ++++++ src/ledger.rs | 23 +- src/lib.rs | 3 + src/mint.rs | 6 +- src/packet.rs | 2 +- src/rpc.rs | 2 +- src/system_contract.rs | 80 +++++++ src/thin_client.rs | 15 +- src/transaction.rs | 262 ++++++++++++----------- src/tvu.rs | 2 +- src/vote_stage.rs | 7 +- 15 files changed, 951 insertions(+), 450 deletions(-) create mode 100644 src/budget_contract.rs create mode 100644 src/instruction.rs create mode 100644 src/system_contract.rs diff --git a/src/bank.rs b/src/bank.rs index 5b66bce71e..09126e5601 100644 --- a/src/bank.rs +++ b/src/bank.rs @@ -3,28 +3,59 @@ //! on behalf of the caller, and a low-level API for when they have //! already been signed and verified. -use bincode::{deserialize, serialize}; -use chrono::prelude::*; +use bincode::serialize; +use budget_contract::BudgetContract; use counter::Counter; use entry::Entry; use hash::{hash, Hash}; +use instruction::Instruction; use itertools::Itertools; use ledger::Block; use log::Level; use mint::Mint; -use payment_plan::{Payment, PaymentPlan, Witness}; +use payment_plan::{Payment, PaymentPlan}; use signature::{Keypair, Pubkey, Signature}; use std; -use std::collections::hash_map::Entry::Occupied; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::result; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::RwLock; use std::time::Instant; +use system_contract::SystemContract; use timing::{duration_as_us, timestamp}; -use transaction::{Instruction, Plan, Transaction}; +use transaction::Transaction; use window::WINDOW_SIZE; +/// An Account with userdata that is stored on chain +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Account { + /// tokens in the account + pub tokens: i64, + /// user data + /// A transaction can write to its userdata + pub userdata: Vec, + /// contract id this contract belongs to + pub contract_id: Pubkey, +} +impl Account { + pub fn new(tokens: i64, space: usize, contract_id: Pubkey) -> Account { + Account { + tokens, + userdata: vec![0u8; space], + contract_id, + } + } +} + +impl Default for Account { + fn default() -> Self { + Account { + tokens: 0, + userdata: vec![], + contract_id: SystemContract::id(), + } + } +} /// 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, @@ -41,10 +72,8 @@ pub enum BankError { /// Attempt to debit from `Pubkey`, but no found no record of a prior credit. AccountNotFound(Pubkey), - /// The requested debit from `Pubkey` 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(Pubkey), + /// The from `Pubkey` does not have sufficient balance to pay the fee to schedule the transaction + InsufficientFundsForFee(Pubkey), /// 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 @@ -55,28 +84,27 @@ pub enum BankError { /// the `last_id` has been discarded. LastIdNotFound(Hash), - /// The transaction is invalid and has requested a debit or credit of negative - /// tokens. - NegativeTokens, - /// Proof of History verification failed. LedgerVerificationFailed, /// Contract's transaction token balance does not equal the balance after the transaction UnbalancedTransaction(Signature), - /// Contract location Pubkey already contains userdata - ContractAlreadyPending(Pubkey), + /// Contract's transactions resulted in an account with a negative balance + /// The difference from InsufficientFundsForFee is that the transaction was executed by the + /// contract + ResultWithNegativeTokens(Signature), + + /// Contract id is unknown + UnknownContractId(Pubkey), + + /// Contract modified an accounts contract id + ModifiedContractId(Signature), + + /// Contract spent the tokens of an account that doesn't belong to it + ExternalAccountTokenSpend(Signature), } pub type Result = result::Result; -/// An Account with userdata that is stored on chain -#[derive(Serialize, Deserialize, Debug, Clone, Default)] -pub struct Account { - /// tokens in the account - pub tokens: i64, - /// user data - /// A transaction can write to its userdata - pub userdata: Vec, -} + #[derive(Default)] struct ErrorCounters { account_not_found_validator: usize, @@ -232,104 +260,7 @@ impl Bank { last_ids.push_back(*last_id); } - /// Deduct tokens from the source account if it has sufficient funds and the contract isn't - /// pending - fn apply_debits_to_budget_payment_plan( - tx: &Transaction, - accounts: &mut [Account], - instruction: &Instruction, - ) -> Result<()> { - { - let tokens = if !accounts[0].userdata.is_empty() { - 0 - } else { - accounts[0].tokens - }; - if let Instruction::NewContract(contract) = &instruction { - if contract.tokens < 0 { - return Err(BankError::NegativeTokens); - } - - if tokens < contract.tokens { - return Err(BankError::InsufficientFunds(tx.keys[0])); - } else { - let bal = &mut accounts[0]; - bal.tokens -= contract.tokens; - } - }; - } - Ok(()) - } - - /// Apply only a transaction's credits. - /// Note: It is safe to apply credits from multiple transactions in parallel. - fn apply_credits_to_budget_payment_plan( - tx: &Transaction, - accounts: &mut [Account], - instruction: &Instruction, - ) -> Result<()> { - match instruction { - Instruction::NewContract(contract) => { - let plan = contract.plan.clone(); - if let Some(payment) = plan.final_payment() { - Self::apply_payment(&payment, &mut accounts[1]); - Ok(()) - } else if !accounts[1].userdata.is_empty() { - Err(BankError::ContractAlreadyPending(tx.keys[1])) - } else { - let mut pending = HashMap::new(); - pending.insert(tx.signature, plan); - //TODO this is a temporary on demand allocation - //until system contract requires explicit allocation of memory - accounts[1].userdata = serialize(&pending).unwrap(); - accounts[1].tokens += contract.tokens; - Ok(()) - } - } - Instruction::ApplyTimestamp(dt) => { - Self::apply_timestamp(tx.keys[0], *dt, &mut accounts[1]); - Ok(()) - } - Instruction::ApplySignature(signature) => { - Self::apply_signature(tx.keys[0], *signature, accounts); - Ok(()) - } - Instruction::NewVote(_vote) => { - // TODO: record the vote in the stake table... - trace!("GOT VOTE! last_id={}", tx.last_id); - Ok(()) - } - } - } - /// Budget DSL contract interface - /// * tx - the transaction - /// * accounts[0] - The source of the tokens - /// * accounts[1] - The contract context. Once the contract has been completed, the tokens can - /// be spent from this account . - pub fn process_transaction_of_budget_instruction( - tx: &Transaction, - accounts: &mut [Account], - ) -> Result<()> { - let instruction = tx.instruction(); - Self::apply_debits_to_budget_payment_plan(tx, accounts, &instruction)?; - Self::apply_credits_to_budget_payment_plan(tx, accounts, &instruction) - } - //TODO the contract needs to provide a "get_balance" introspection call of the userdata - pub fn get_balance_of_budget_payment_plan(account: &Account) -> i64 { - if let Ok(pending) = deserialize(&account.userdata) { - let pending: HashMap = pending; - if !pending.is_empty() { - 0 - } else { - account.tokens - } - } else { - account.tokens - } - } - - /// Process a Transaction. If it contains a payment plan that requires a witness - /// to progress, the payment plan will be stored in the bank. + /// Process a Transaction. This is used for unit tests and simply calls the vector Bank::process_transactions method. pub fn process_transaction(&self, tx: &Transaction) -> Result<()> { match self.process_transactions(vec![tx.clone()])[0] { Err(ref e) => { @@ -353,12 +284,12 @@ impl Bank { } else { error_counters.account_not_found_leader += 1; } - if let Instruction::NewVote(_vote) = tx.instruction() { + if let Some(Instruction::NewVote(_vote)) = tx.instruction() { error_counters.account_not_found_vote += 1; } Err(BankError::AccountNotFound(*tx.from())) } else if accounts.get(&tx.keys[0]).unwrap().tokens < tx.fee { - Err(BankError::InsufficientFunds(*tx.from())) + Err(BankError::InsufficientFundsForFee(*tx.from())) } else { let mut called_accounts: Vec = tx .keys @@ -382,27 +313,60 @@ impl Bank { .map(|tx| self.load_account(tx, accounts, error_counters)) .collect() } - - pub fn execute_transaction(tx: Transaction, accounts: &mut [Account]) -> Result { + pub fn verify_transaction( + tx: &Transaction, + pre_contract_id: &Pubkey, + pre_tokens: i64, + account: &Account, + ) -> Result<()> { + // Verify the transaction + // make sure that contract_id is still the same or this was just assigned by the system call contract + if !((*pre_contract_id == account.contract_id) + || (SystemContract::check_id(&tx.contract_id) + && SystemContract::check_id(&pre_contract_id))) + { + //TODO, this maybe redundant bpf should be able to guarantee this property + return Err(BankError::ModifiedContractId(tx.signature)); + } + // For accounts unassigned to the contract, the individual balance of each accounts cannot decrease. + if tx.contract_id != account.contract_id && pre_tokens > account.tokens { + return Err(BankError::ExternalAccountTokenSpend(tx.signature)); + } + if account.tokens < 0 { + return Err(BankError::ResultWithNegativeTokens(tx.signature)); + } + Ok(()) + } + /// Execute a transaction. + /// This method calls the contract's process_transaction method and verifies that the result of + /// the contract does not violate the bank's accounting rules. + /// The accounts are commited back to the bank only if this function returns Ok(_). + fn execute_transaction(tx: Transaction, accounts: &mut [Account]) -> Result { let pre_total: i64 = accounts.iter().map(|a| a.tokens).sum(); + let pre_data: Vec<_> = accounts + .iter_mut() + .map(|a| (a.contract_id, a.tokens)) + .collect(); - // TODO next steps is to add hooks to call arbitrary contracts here // Call the contract method // It's up to the contract to implement its own rules on moving funds - let e = Self::process_transaction_of_budget_instruction(&tx, accounts); - + if SystemContract::check_id(&tx.contract_id) { + SystemContract::process_transaction(&tx, accounts) + } else if BudgetContract::check_id(&tx.contract_id) { + // TODO: the runtime should be checking read/write access to memory + // we are trusting the hard coded contracts not to clobber or allocate + BudgetContract::process_transaction(&tx, accounts) + } else { + return Err(BankError::UnknownContractId(tx.contract_id)); + } // Verify the transaction - // TODO, At the moment there is only 1 contract, so 1-3 are not checked - // 1. For accounts assigned to the contract, the total sum of all the tokens in these accounts cannot increase. - // 2. For accounts unassigned to the contract, the individual balance of each accounts cannot decrease. - // 3. For accounts unassigned to the contract, the userdata cannot change. - - // 4. The total sum of all the tokens in all the pages cannot change. + for ((pre_contract_id, pre_tokens), post_account) in pre_data.iter().zip(accounts.iter()) { + Self::verify_transaction(&tx, pre_contract_id, *pre_tokens, post_account)?; + } + // The total sum of all the tokens in all the pages cannot change. let post_total: i64 = accounts.iter().map(|a| a.tokens).sum(); if pre_total != post_total { Err(BankError::UnbalancedTransaction(tx.signature)) - } else if let Err(err) = e { - Err(err) } else { Ok(tx) } @@ -594,7 +558,7 @@ impl Bank { { let tx = &entry1.transactions[0]; let instruction = tx.instruction(); - let deposit = if let Instruction::NewContract(contract) = instruction { + let deposit = if let Some(Instruction::NewContract(contract)) = instruction { contract.plan.final_payment() } else { None @@ -624,60 +588,6 @@ impl Bank { Ok((entry_count, tail)) } - /// Process a Witness Signature. Any payment plans waiting on this signature - /// will progress one step. - fn apply_signature(from: Pubkey, signature: Signature, account: &mut [Account]) { - let mut pending: HashMap = - deserialize(&account[1].userdata).unwrap_or_default(); - if let Occupied(mut e) = pending.entry(signature) { - e.get_mut().apply_witness(&Witness::Signature, &from); - if let Some(payment) = e.get().final_payment() { - //move the tokens back to the from account - account[0].tokens += payment.tokens; - account[1].tokens -= payment.tokens; - e.remove_entry(); - } - }; - //TODO this allocation needs to be changed once the runtime only allows for explicitly - //allocated memory - account[1].userdata = if pending.is_empty() { - vec![] - } else { - serialize(&pending).unwrap() - }; - } - - /// Process a Witness Timestamp. Any payment plans waiting on this timestamp - /// will progress one step. - fn apply_timestamp(from: Pubkey, dt: DateTime, account: &mut Account) { - let mut pending: HashMap = - deserialize(&account.userdata).unwrap_or_default(); - //deserialize(&account.userdata).unwrap_or(HashMap::new()); - - // Check to see if any timelocked transactions can be completed. - let mut completed = vec![]; - - // Hold 'pending' write lock until the end of this function. Otherwise another thread can - // double-spend if it enters before the modified plan is removed from 'pending'. - for (key, plan) in &mut pending { - plan.apply_witness(&Witness::Timestamp(dt), &from); - if let Some(_payment) = plan.final_payment() { - completed.push(key.clone()); - } - } - - for key in completed { - pending.remove(&key); - } - //TODO this allocation needs to be changed once the runtime only allows for explicitly - //allocated memory - account.userdata = if pending.is_empty() { - vec![] - } else { - serialize(&pending).unwrap() - }; - } - /// Create, sign, and process a Transaction from `keypair` to `to` of /// `n` tokens where `last_id` is the last Entry ID observed by the client. pub fn transfer( @@ -692,25 +602,20 @@ impl Bank { self.process_transaction(&tx).map(|_| signature) } - /// Create, sign, and process a postdated Transaction from `keypair` - /// to `to` of `n` tokens on `dt` where `last_id` is the last Entry ID - /// observed by the client. - pub fn transfer_on_date( - &self, - n: i64, - keypair: &Keypair, - to: Pubkey, - dt: DateTime, - last_id: Hash, - ) -> Result { - let tx = Transaction::new_on_date(keypair, to, dt, n, last_id); - let signature = tx.signature; - self.process_transaction(&tx).map(|_| signature) + pub fn read_balance(account: &Account) -> i64 { + if SystemContract::check_id(&account.contract_id) { + SystemContract::get_balance(account) + } else if BudgetContract::check_id(&account.contract_id) { + BudgetContract::get_balance(account) + } else { + account.tokens + } } - + /// Each contract would need to be able to introspect its own state + /// this is hard coded to the budget contract langauge pub fn get_balance(&self, pubkey: &Pubkey) -> i64 { self.get_account(pubkey) - .map(|x| Self::get_balance_of_budget_payment_plan(&x)) + .map(|x| Self::read_balance(&x)) .unwrap_or(0) } @@ -767,6 +672,7 @@ mod tests { use entry_writer::{self, EntryWriter}; use hash::hash; use ledger; + use logger; use packet::BLOB_DATA_SIZE; use signature::{GenKeys, KeypairUtil}; use std; @@ -799,13 +705,13 @@ mod tests { #[test] fn test_negative_tokens() { + logger::setup(); let mint = Mint::new(1); let pubkey = Keypair::new().pubkey(); let bank = Bank::new(&mint); - assert_eq!( - bank.transfer(-1, &mint.keypair(), pubkey, mint.last_id()), - Err(BankError::NegativeTokens) - ); + let res = bank.transfer(-1, &mint.keypair(), pubkey, mint.last_id()); + println!("{:?}", bank.get_account(&pubkey)); + assert_matches!(res, Err(BankError::ResultWithNegativeTokens(_))); assert_eq!(bank.transaction_count(), 0); } @@ -815,15 +721,20 @@ mod tests { fn test_detect_failed_duplicate_transactions_issue_1157() { let mint = Mint::new(1); let bank = Bank::new(&mint); + let dest = Keypair::new(); - let tx = Transaction::new(&mint.keypair(), mint.keypair().pubkey(), -1, mint.last_id()); + // source with 0 contract context + let tx = Transaction::new(&mint.keypair(), dest.pubkey(), 2, mint.last_id()); let signature = tx.signature; assert!(!bank.has_signature(&signature)); - assert_eq!( - bank.process_transaction(&tx), - Err(BankError::NegativeTokens) - ); + let res = bank.process_transaction(&tx); + // This is the potentially wrong behavior + // result failed, but signature is registered + assert!(!res.is_ok()); assert!(bank.has_signature(&signature)); + // sanity check that tokens didn't move + assert_eq!(bank.get_balance(&dest.pubkey()), 0); + assert_eq!(bank.get_balance(&mint.pubkey()), 1); } #[test] @@ -847,9 +758,9 @@ mod tests { .unwrap(); assert_eq!(bank.transaction_count(), 1); assert_eq!(bank.get_balance(&pubkey), 1_000); - assert_eq!( + assert_matches!( bank.transfer(10_001, &mint.keypair(), pubkey, mint.last_id()), - Err(BankError::InsufficientFunds(mint.pubkey())) + Err(BankError::ResultWithNegativeTokens(_)) ); assert_eq!(bank.transaction_count(), 1); @@ -868,85 +779,6 @@ mod tests { assert_eq!(bank.get_balance(&pubkey), 500); } - #[test] - fn test_transfer_on_date() { - let mint = Mint::new(2); - let bank = Bank::new(&mint); - let pubkey = Keypair::new().pubkey(); - let dt = Utc::now(); - bank.transfer_on_date(1, &mint.keypair(), pubkey, dt, mint.last_id()) - .unwrap(); - - // Mint's balance will be 1 because 1 of the tokens is locked up - assert_eq!(bank.get_balance(&mint.pubkey()), 1); - - // tx count is 1, because debits were applied. - assert_eq!(bank.transaction_count(), 1); - - // pubkey's balance will be 0 because the funds have not been - // sent. - assert_eq!(bank.get_balance(&pubkey), 0); - - // Now, acknowledge the time in the condition occurred and - // that pubkey's funds are now available. - let tx = Transaction::new_timestamp(&mint.keypair(), pubkey, dt, bank.last_id()); - let res = bank.process_transaction(&tx); - assert!(res.is_ok()); - assert_eq!(bank.get_balance(&pubkey), 1); - - // tx count is 2 - assert_eq!(bank.transaction_count(), 2); - - // try to replay the timestamp contract - bank.register_entry_id(&hash(bank.last_id().as_ref())); - let tx = Transaction::new_timestamp(&mint.keypair(), pubkey, dt, bank.last_id()); - let res = bank.process_transaction(&tx); - assert!(res.is_ok()); - - assert_eq!(bank.get_balance(&pubkey), 1); - } - - #[test] - fn test_cancel_transfer() { - // mint needs to have a balance to modify the external contract - let mint = Mint::new(2); - let bank = Bank::new(&mint); - let pubkey = Keypair::new().pubkey(); - let dt = Utc::now(); - let signature = bank - .transfer_on_date(1, &mint.keypair(), pubkey, dt, mint.last_id()) - .unwrap(); - - // Assert the debit counts as a transaction. - assert_eq!(bank.transaction_count(), 1); - - // Mint's balance will be 1 because 1 of the tokens is locked up. - assert_eq!(bank.get_balance(&mint.pubkey()), 1); - - // pubkey's balance will be 0 because the funds are locked up - assert_eq!(bank.get_balance(&pubkey), 0); - - // Now, cancel the transaction. Mint gets her funds back, pubkey never sees them. - let tx = Transaction::new_signature(&mint.keypair(), pubkey, signature, bank.last_id()); - let res = bank.process_transaction(&tx); - assert!(res.is_ok()); - assert_eq!(bank.get_balance(&pubkey), 0); - assert_eq!(bank.get_balance(&mint.pubkey()), 2); - - // Assert cancel counts as a tx - assert_eq!(bank.transaction_count(), 2); - - // try to replay the signature contract - bank.register_entry_id(&hash(bank.last_id().as_ref())); - let tx = Transaction::new_signature(&mint.keypair(), pubkey, signature, bank.last_id()); - let res = bank.process_transaction(&tx); //<-- attack! try to get budget dsl to pay out with another signature - assert!(res.is_ok()); - // balance is is still 2 for the mint - assert_eq!(bank.get_balance(&mint.pubkey()), 2); - // balance is is still 0 for the contract - assert_eq!(bank.get_balance(&pubkey), 0); - } - #[test] fn test_duplicate_transaction_signature() { let mint = Mint::new(1); @@ -1070,7 +902,7 @@ mod tests { let hash = mint.last_id(); let mut txs = Vec::with_capacity(length); for i in 0..length { - txs.push(Transaction::new( + txs.push(Transaction::system_new( &mint.keypair(), keypair.pubkey(), i as i64, @@ -1088,7 +920,7 @@ mod tests { let hash = mint.last_id(); let transactions: Vec<_> = keypairs .iter() - .map(|keypair| Transaction::new(&mint.keypair(), keypair.pubkey(), 1, hash)) + .map(|keypair| Transaction::system_new(&mint.keypair(), keypair.pubkey(), 1, hash)) .collect(); let entries = ledger::next_entries(&hash, 0, transactions); entries.into_iter() @@ -1100,7 +932,7 @@ mod tests { let mut num_hashes = 0; for _ in 0..length { let keypair = Keypair::new(); - let tx = Transaction::new(&mint.keypair(), keypair.pubkey(), 1, hash); + let tx = Transaction::system_new(&mint.keypair(), keypair.pubkey(), 1, hash); let entry = Entry::new_mut(&mut hash, &mut num_hashes, vec![tx], false); entries.push(entry); } diff --git a/src/budget_contract.rs b/src/budget_contract.rs new file mode 100644 index 0000000000..d5a341a744 --- /dev/null +++ b/src/budget_contract.rs @@ -0,0 +1,469 @@ +//! budget contract +use bank::Account; +use bincode::{self, deserialize, serialize_into, serialized_size}; +use chrono::prelude::{DateTime, Utc}; +use instruction::{Instruction, Plan}; +use payment_plan::{PaymentPlan, Witness}; +use signature::{Pubkey, Signature}; +use std::collections::hash_map::Entry::Occupied; +use std::collections::HashMap; +use std::io; +use transaction::Transaction; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum BudgetError { + InsufficientFunds(Pubkey), + ContractAlreadyExists(Pubkey), + ContractNotPending(Pubkey), + SourceIsPendingContract(Pubkey), + UninitializedContract(Pubkey), + NegativeTokens, + DestinationMissing(Pubkey), + FailedWitness(Signature), + SignatureUnoccupied(Signature), +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +pub struct BudgetContract { + pub initialized: bool, + pub pending: HashMap, + pub last_error: Option, +} + +pub const BUDGET_CONTRACT_ID: [u8; 32] = [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; +impl BudgetContract { + fn is_pending(&self) -> bool { + !self.pending.is_empty() + } + pub fn id() -> Pubkey { + Pubkey::new(&BUDGET_CONTRACT_ID) + } + pub fn check_id(contract_id: &Pubkey) -> bool { + contract_id.as_ref() == BUDGET_CONTRACT_ID + } + + /// Process a Witness Signature. Any payment plans waiting on this signature + /// will progress one step. + fn apply_signature( + &mut self, + tx: &Transaction, + signature: Signature, + account: &mut [Account], + ) -> Result<(), BudgetError> { + if let Occupied(mut e) = self.pending.entry(signature) { + e.get_mut().apply_witness(&Witness::Signature, &tx.keys[0]); + if let Some(payment) = e.get().final_payment() { + if tx.keys.len() > 1 && payment.to == tx.keys[2] { + trace!("apply_witness refund"); + //move the tokens back to the from account + account[1].tokens -= payment.tokens; + account[2].tokens += payment.tokens; + e.remove_entry(); + } else { + trace!("destination is missing"); + return Err(BudgetError::DestinationMissing(payment.to)); + } + } else { + trace!("failed apply_witness"); + return Err(BudgetError::FailedWitness(signature)); + } + } else { + trace!("apply_witness signature unoccupied"); + return Err(BudgetError::SignatureUnoccupied(signature)); + } + Ok(()) + } + + /// Process a Witness Timestamp. Any payment plans waiting on this timestamp + /// will progress one step. + fn apply_timestamp( + &mut self, + tx: &Transaction, + accounts: &mut [Account], + dt: DateTime, + ) -> Result<(), BudgetError> { + // Check to see if any timelocked transactions can be completed. + let mut completed = vec![]; + + // Hold 'pending' write lock until the end of this function. Otherwise another thread can + // double-spend if it enters before the modified plan is removed from 'pending'. + for (key, plan) in &mut self.pending { + plan.apply_witness(&Witness::Timestamp(dt), &tx.keys[0]); + if let Some(payment) = plan.final_payment() { + if tx.keys.len() < 2 || payment.to != tx.keys[2] { + trace!("destination missing"); + return Err(BudgetError::DestinationMissing(payment.to)); + } + completed.push(key.clone()); + accounts[2].tokens += payment.tokens; + accounts[1].tokens -= payment.tokens; + } + } + for key in completed { + self.pending.remove(&key); + } + Ok(()) + } + + /// Deduct tokens from the source account if it has sufficient funds and the contract isn't + /// pending + fn apply_debits_to_budget_state( + tx: &Transaction, + accounts: &mut [Account], + instruction: &Instruction, + ) -> Result<(), BudgetError> { + { + // if the source account userdata is not empty, this is a pending contract + if !accounts[0].userdata.is_empty() { + trace!("source is pending"); + return Err(BudgetError::SourceIsPendingContract(tx.keys[0])); + } + if let Instruction::NewContract(contract) = &instruction { + if contract.tokens < 0 { + trace!("negative tokens"); + return Err(BudgetError::NegativeTokens); + } + + if accounts[0].tokens < contract.tokens { + trace!("insufficent funds"); + return Err(BudgetError::InsufficientFunds(tx.keys[0])); + } else { + accounts[0].tokens -= contract.tokens; + } + }; + } + Ok(()) + } + + /// Apply only a transaction's credits. + /// Note: It is safe to apply credits from multiple transactions in parallel. + fn apply_credits_to_budget_state( + tx: &Transaction, + accounts: &mut [Account], + instruction: &Instruction, + ) -> Result<(), BudgetError> { + match instruction { + Instruction::NewContract(contract) => { + let plan = contract.plan.clone(); + if let Some(payment) = plan.final_payment() { + accounts[1].tokens += payment.tokens; + Ok(()) + } else { + let existing = Self::deserialize(&accounts[1].userdata).ok(); + if Some(true) == existing.map(|x| x.initialized) { + trace!("contract already exists"); + Err(BudgetError::ContractAlreadyExists(tx.keys[1])) + } else { + let mut state = BudgetContract::default(); + state.pending.insert(tx.signature, plan); + accounts[1].tokens += contract.tokens; + state.initialized = true; + state.serialize(&mut accounts[1].userdata); + Ok(()) + } + } + } + Instruction::ApplyTimestamp(dt) => { + let mut state = Self::deserialize(&accounts[1].userdata).unwrap(); + if !state.is_pending() { + return Err(BudgetError::ContractNotPending(tx.keys[1])); + } + if !state.initialized { + trace!("contract is uninitialized"); + Err(BudgetError::UninitializedContract(tx.keys[1])) + } else { + state.apply_timestamp(tx, accounts, *dt)?; + trace!("apply timestamp commited"); + state.serialize(&mut accounts[1].userdata); + Ok(()) + } + } + Instruction::ApplySignature(signature) => { + let mut state = Self::deserialize(&accounts[1].userdata).unwrap(); + if !state.is_pending() { + return Err(BudgetError::ContractNotPending(tx.keys[1])); + } + if !state.initialized { + trace!("contract is uninitialized"); + Err(BudgetError::UninitializedContract(tx.keys[1])) + } else { + trace!("apply signature"); + state.apply_signature(tx, *signature, accounts)?; + trace!("apply signature commited"); + state.serialize(&mut accounts[1].userdata); + Ok(()) + } + } + Instruction::NewVote(_vote) => { + // TODO: move vote instruction into a different contract + trace!("GOT VOTE! last_id={}", tx.last_id); + Ok(()) + } + } + } + fn serialize(&self, output: &mut [u8]) { + let len = serialized_size(self).unwrap() as u64; + { + let writer = io::BufWriter::new(&mut output[..8]); + serialize_into(writer, &len).unwrap(); + } + { + let writer = io::BufWriter::new(&mut output[8..8 + len as usize]); + serialize_into(writer, self).unwrap(); + } + } + + pub fn deserialize(input: &[u8]) -> bincode::Result { + if input.len() < 8 { + return Err(Box::new(bincode::ErrorKind::SizeLimit)); + } + let len: u64 = deserialize(&input[..8]).unwrap(); + if len < 8 { + return Err(Box::new(bincode::ErrorKind::SizeLimit)); + } + if input.len() < 8 + len as usize { + return Err(Box::new(bincode::ErrorKind::SizeLimit)); + } + deserialize(&input[8..8 + len as usize]) + } + + fn save_error_to_budget_state(e: BudgetError, accounts: &mut [Account]) -> () { + if let Ok(mut state) = BudgetContract::deserialize(&accounts[1].userdata) { + trace!("saved error {:?}", e); + state.last_error = Some(e); + state.serialize(&mut accounts[1].userdata); + } else { + trace!("error in uninitialized contract {:?}", e,); + } + } + + /// Budget DSL contract interface + /// * tx - the transaction + /// * accounts[0] - The source of the tokens + /// * accounts[1] - The contract context. Once the contract has been completed, the tokens can + /// be spent from this account . + pub fn process_transaction(tx: &Transaction, accounts: &mut [Account]) -> () { + let instruction = deserialize(&tx.userdata).unwrap(); + let _ = Self::apply_debits_to_budget_state(tx, accounts, &instruction) + .and_then(|_| Self::apply_credits_to_budget_state(tx, accounts, &instruction)) + .map_err(|e| { + trace!("saving error {:?}", e); + Self::save_error_to_budget_state(e, accounts); + }); + } + + //TODO the contract needs to provide a "get_balance" introspection call of the userdata + pub fn get_balance(account: &Account) -> i64 { + if let Ok(pending) = deserialize(&account.userdata) { + let pending: BudgetContract = pending; + if pending.is_pending() { + 0 + } else { + account.tokens + } + } else { + account.tokens + } + } +} +#[cfg(test)] +mod test { + use bank::Account; + use bincode::serialize; + use budget_contract::{BudgetContract, BudgetError}; + use chrono::prelude::Utc; + use hash::Hash; + use signature::{Keypair, KeypairUtil}; + use transaction::Transaction; + #[test] + fn test_serializer() { + let mut a = Account::new(0, 512, BudgetContract::id()); + let b = BudgetContract::default(); + b.serialize(&mut a.userdata); + let buf = serialize(&b).unwrap(); + assert_eq!(a.userdata[8..8 + buf.len()], buf[0..]); + let c = BudgetContract::deserialize(&a.userdata).unwrap(); + assert_eq!(b, c); + } + + #[test] + fn test_transfer_on_date() { + let mut accounts = vec![ + Account::new(1, 0, BudgetContract::id()), + Account::new(0, 512, BudgetContract::id()), + Account::new(0, 0, BudgetContract::id()), + ]; + let from_account = 0; + let contract_account = 1; + let to_account = 2; + let from = Keypair::new(); + let contract = Keypair::new(); + let to = Keypair::new(); + let rando = Keypair::new(); + let dt = Utc::now(); + let tx = Transaction::budget_new_on_date( + &from, + to.pubkey(), + contract.pubkey(), + dt, + 1, + Hash::default(), + ); + BudgetContract::process_transaction(&tx, &mut accounts); + assert_eq!(accounts[from_account].tokens, 0); + assert_eq!(accounts[contract_account].tokens, 1); + let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap(); + assert_eq!(state.last_error, None); + assert!(state.is_pending()); + + // Attack! Try to payout to a rando key + let tx = Transaction::budget_new_timestamp( + &from, + contract.pubkey(), + rando.pubkey(), + dt, + Hash::default(), + ); + BudgetContract::process_transaction(&tx, &mut accounts); + assert_eq!(accounts[from_account].tokens, 0); + assert_eq!(accounts[contract_account].tokens, 1); + assert_eq!(accounts[to_account].tokens, 0); + + let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap(); + assert_eq!( + state.last_error, + Some(BudgetError::DestinationMissing(to.pubkey())) + ); + assert!(state.is_pending()); + + // Now, acknowledge the time in the condition occurred and + // that pubkey's funds are now available. + let tx = Transaction::budget_new_timestamp( + &from, + contract.pubkey(), + to.pubkey(), + dt, + Hash::default(), + ); + BudgetContract::process_transaction(&tx, &mut accounts); + assert_eq!(accounts[from_account].tokens, 0); + assert_eq!(accounts[contract_account].tokens, 0); + assert_eq!(accounts[to_account].tokens, 1); + + let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap(); + assert!(!state.is_pending()); + + // try to replay the timestamp contract + BudgetContract::process_transaction(&tx, &mut accounts); + assert_eq!(accounts[from_account].tokens, 0); + assert_eq!(accounts[contract_account].tokens, 0); + assert_eq!(accounts[to_account].tokens, 1); + let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap(); + assert_eq!( + state.last_error, + Some(BudgetError::ContractNotPending(contract.pubkey())) + ); + } + #[test] + fn test_cancel_transfer() { + let mut accounts = vec![ + Account::new(1, 0, BudgetContract::id()), + Account::new(0, 512, BudgetContract::id()), + Account::new(0, 0, BudgetContract::id()), + ]; + let from_account = 0; + let contract_account = 1; + let pay_account = 2; + let from = Keypair::new(); + let contract = Keypair::new(); + let to = Keypair::new(); + let dt = Utc::now(); + let tx = Transaction::budget_new_on_date( + &from, + to.pubkey(), + contract.pubkey(), + dt, + 1, + Hash::default(), + ); + let sig = tx.signature; + BudgetContract::process_transaction(&tx, &mut accounts); + assert_eq!(accounts[from_account].tokens, 0); + assert_eq!(accounts[contract_account].tokens, 1); + let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap(); + assert_eq!(state.last_error, None); + assert!(state.is_pending()); + + // Attack! try to put the tokens into the wrong account with cancel + let tx = Transaction::budget_new_signature( + &to, + contract.pubkey(), + to.pubkey(), + sig, + Hash::default(), + ); + // unit test hack, the `from account` is passed instead of the `to` account to avoid + // creating more account vectors + BudgetContract::process_transaction(&tx, &mut accounts); + // nothing should be changed because apply witness didn't finalize a payment + assert_eq!(accounts[from_account].tokens, 0); + assert_eq!(accounts[contract_account].tokens, 1); + // this would be the `to.pubkey()` account + assert_eq!(accounts[pay_account].tokens, 0); + let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap(); + assert_eq!(state.last_error, Some(BudgetError::FailedWitness(sig))); + + // Attack! try canceling with a bad signature + let badsig = tx.signature; + let tx = Transaction::budget_new_signature( + &from, + contract.pubkey(), + from.pubkey(), + badsig, + Hash::default(), + ); + BudgetContract::process_transaction(&tx, &mut accounts); + assert_eq!(accounts[from_account].tokens, 0); + assert_eq!(accounts[contract_account].tokens, 1); + assert_eq!(accounts[pay_account].tokens, 0); + let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap(); + assert_eq!( + state.last_error, + Some(BudgetError::SignatureUnoccupied(badsig)) + ); + + // Now, cancel the transaction. from gets her funds back + let tx = Transaction::budget_new_signature( + &from, + contract.pubkey(), + from.pubkey(), + sig, + Hash::default(), + ); + BudgetContract::process_transaction(&tx, &mut accounts); + assert_eq!(accounts[from_account].tokens, 0); + assert_eq!(accounts[contract_account].tokens, 0); + assert_eq!(accounts[pay_account].tokens, 1); + + // try to replay the signature contract + let tx = Transaction::budget_new_signature( + &from, + contract.pubkey(), + from.pubkey(), + sig, + Hash::default(), + ); + BudgetContract::process_transaction(&tx, &mut accounts); + assert_eq!(accounts[from_account].tokens, 0); + assert_eq!(accounts[contract_account].tokens, 0); + assert_eq!(accounts[pay_account].tokens, 1); + + let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap(); + assert_eq!( + state.last_error, + Some(BudgetError::ContractNotPending(contract.pubkey())) + ); + } +} diff --git a/src/crdt.rs b/src/crdt.rs index b8aabbab7f..42177d79a8 100644 --- a/src/crdt.rs +++ b/src/crdt.rs @@ -16,6 +16,7 @@ use bincode::{deserialize, serialize}; use choose_gossip_peer_strategy::{ChooseGossipPeerStrategy, ChooseWeightedPeerStrategy}; use counter::Counter; use hash::Hash; +use instruction::Vote; use ledger::LedgerWindow; use log::Level; use netutil::{bind_in_range, bind_to, multi_bind_in_range}; @@ -33,7 +34,6 @@ use std::thread::{sleep, Builder, JoinHandle}; use std::time::{Duration, Instant}; use streamer::{BlobReceiver, BlobSender}; use timing::{duration_as_ms, timestamp}; -use transaction::Vote; use window::{SharedWindow, WindowIndex}; pub const FULLNODE_PORT_RANGE: (u16, u16) = (8000, 10_000); @@ -1327,6 +1327,7 @@ mod tests { }; use entry::Entry; use hash::{hash, Hash}; + use instruction::Vote; use ledger::{LedgerWindow, LedgerWriter}; use logger; use packet::BlobRecycler; @@ -1339,7 +1340,6 @@ mod tests { use std::sync::{Arc, RwLock}; use std::thread::sleep; use std::time::Duration; - use transaction::Vote; use window::default_window; #[test] diff --git a/src/entry.rs b/src/entry.rs index ac8589a9c7..d1c0bf4988 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -249,8 +249,20 @@ mod tests { // First, verify entries let keypair = Keypair::new(); - let tx0 = Transaction::new_timestamp(&keypair, keypair.pubkey(), Utc::now(), zero); - let tx1 = Transaction::new_signature(&keypair, keypair.pubkey(), Default::default(), zero); + let tx0 = Transaction::budget_new_timestamp( + &keypair, + keypair.pubkey(), + keypair.pubkey(), + Utc::now(), + zero, + ); + let tx1 = Transaction::budget_new_signature( + &keypair, + keypair.pubkey(), + keypair.pubkey(), + Default::default(), + zero, + ); let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()], false); assert!(e0.verify(&zero)); @@ -272,7 +284,13 @@ mod tests { assert_eq!(tick.id, zero); let keypair = Keypair::new(); - let tx0 = Transaction::new_timestamp(&keypair, keypair.pubkey(), Utc::now(), zero); + let tx0 = Transaction::budget_new_timestamp( + &keypair, + keypair.pubkey(), + keypair.pubkey(), + Utc::now(), + zero, + ); let entry0 = next_entry(&zero, 1, vec![tx0.clone()]); assert_eq!(entry0.num_hashes, 1); assert_eq!(entry0.id, next_hash(&zero, 1, &vec![tx0])); diff --git a/src/instruction.rs b/src/instruction.rs new file mode 100644 index 0000000000..a7b3c3b596 --- /dev/null +++ b/src/instruction.rs @@ -0,0 +1,68 @@ +use budget::Budget; +use chrono::prelude::{DateTime, Utc}; +use payment_plan::{Payment, PaymentPlan, Witness}; +use signature::Pubkey; +use signature::Signature; + +/// The type of payment plan. Each item must implement the PaymentPlan trait. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum Plan { + /// The builtin contract language Budget. + Budget(Budget), +} + +// A proxy for the underlying DSL. +impl PaymentPlan for Plan { + fn final_payment(&self) -> Option { + match self { + Plan::Budget(budget) => budget.final_payment(), + } + } + + fn verify(&self, spendable_tokens: i64) -> bool { + match self { + Plan::Budget(budget) => budget.verify(spendable_tokens), + } + } + + fn apply_witness(&mut self, witness: &Witness, from: &Pubkey) { + match self { + Plan::Budget(budget) => budget.apply_witness(witness, from), + } + } +} + +/// A smart contract. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Contract { + /// The number of tokens allocated to the `Plan` and any transaction fees. + pub tokens: i64, + pub plan: Plan, +} +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Vote { + /// We send some gossip specific membership information through the vote to shortcut + /// liveness voting + /// The version of the CRDT struct that the last_id of this network voted with + pub version: u64, + /// The version of the CRDT struct that has the same network configuration as this one + pub contact_info_version: u64, + // TODO: add signature of the state here as well +} + +/// An instruction to progress the smart contract. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum Instruction { + /// Declare and instanstansiate `Contract`. + NewContract(Contract), + + /// Tell a payment plan acknowledge the given `DateTime` has past. + ApplyTimestamp(DateTime), + + /// Tell the payment plan that the `NewContract` with `Signature` has been + /// signed by the containing transaction's `Pubkey`. + ApplySignature(Signature), + + /// Vote for a PoH that is equal to the lastid of this transaction + NewVote(Vote), +} diff --git a/src/ledger.rs b/src/ledger.rs index 3dae6cfd3a..77283817f1 100644 --- a/src/ledger.rs +++ b/src/ledger.rs @@ -5,6 +5,7 @@ use bincode::{self, deserialize, deserialize_from, serialize_into, serialized_size}; use entry::Entry; use hash::Hash; +use instruction::Vote; use log::Level::Trace; use packet::{self, SharedBlob, BLOB_DATA_SIZE}; use rayon::prelude::*; @@ -15,7 +16,7 @@ use std::io::prelude::*; use std::io::{self, BufReader, BufWriter, Seek, SeekFrom}; use std::mem::size_of; use std::path::Path; -use transaction::{Transaction, Vote}; +use transaction::Transaction; use window::WINDOW_SIZE; // @@ -548,11 +549,12 @@ mod tests { use chrono::prelude::*; use entry::{next_entry, Entry}; use hash::hash; + use instruction::Vote; use packet::{BlobRecycler, BLOB_DATA_SIZE, PACKET_DATA_SIZE}; use signature::{Keypair, KeypairUtil}; use std; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - use transaction::{Transaction, Vote}; + use transaction::Transaction; fn tmp_ledger_path(name: &str) -> String { use std::env; @@ -590,9 +592,10 @@ mod tests { Entry::new_mut( &mut id, &mut num_hashes, - vec![Transaction::new_timestamp( + vec![Transaction::budget_new_timestamp( &keypair, keypair.pubkey(), + keypair.pubkey(), Utc::now(), one, )], @@ -605,7 +608,7 @@ mod tests { let zero = Hash::default(); let one = hash(&zero.as_ref()); let keypair = Keypair::new(); - let tx0 = Transaction::new_vote( + let tx0 = Transaction::budget_new_vote( &keypair, Vote { version: 0, @@ -614,7 +617,13 @@ mod tests { one, 1, ); - let tx1 = Transaction::new_timestamp(&keypair, keypair.pubkey(), Utc::now(), one); + let tx1 = Transaction::budget_new_timestamp( + &keypair, + keypair.pubkey(), + keypair.pubkey(), + Utc::now(), + one, + ); // // TODO: this magic number and the mix of transaction types // is designed to fill up a Blob more or less exactly, @@ -659,7 +668,7 @@ mod tests { let id = Hash::default(); let next_id = hash(&id.as_ref()); let keypair = Keypair::new(); - let tx_small = Transaction::new_vote( + let tx_small = Transaction::budget_new_vote( &keypair, Vote { version: 0, @@ -668,7 +677,7 @@ mod tests { next_id, 2, ); - let tx_large = Transaction::new(&keypair, keypair.pubkey(), 1, next_id); + let tx_large = Transaction::budget_new(&keypair, keypair.pubkey(), 1, next_id); let tx_small_size = serialized_size(&tx_small).unwrap() as usize; let tx_large_size = serialized_size(&tx_large).unwrap() as usize; diff --git a/src/lib.rs b/src/lib.rs index 979d35ae22..eb58a6fe75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,8 +16,10 @@ pub mod broadcast_stage; pub mod budget; pub mod choose_gossip_peer_strategy; pub mod client; +pub mod instruction; #[macro_use] pub mod crdt; +pub mod budget_contract; pub mod drone; pub mod entry; pub mod entry_writer; @@ -50,6 +52,7 @@ pub mod signature; pub mod sigverify; pub mod sigverify_stage; pub mod streamer; +pub mod system_contract; pub mod thin_client; pub mod timing; pub mod tpu; diff --git a/src/mint.rs b/src/mint.rs index fd518c55ca..346d1bb082 100644 --- a/src/mint.rs +++ b/src/mint.rs @@ -52,7 +52,7 @@ impl Mint { pub fn create_transactions(&self) -> Vec { let keypair = self.keypair(); - let tx = Transaction::new(&keypair, self.pubkey(), self.tokens, self.seed()); + let tx = Transaction::budget_new(&keypair, self.pubkey(), self.tokens, self.seed()); vec![tx] } @@ -67,14 +67,14 @@ impl Mint { mod tests { use super::*; use budget::Budget; + use instruction::{Instruction, Plan}; use ledger::Block; - use transaction::{Instruction, Plan}; #[test] fn test_create_transactions() { let mut transactions = Mint::new(100).create_transactions().into_iter(); let tx = transactions.next().unwrap(); - if let Instruction::NewContract(contract) = tx.instruction() { + if let Some(Instruction::NewContract(contract)) = tx.instruction() { if let Plan::Budget(Budget::Pay(payment)) = contract.plan { assert_eq!(*tx.from(), payment.to); } diff --git a/src/packet.rs b/src/packet.rs index f438dbfa9c..206c522f09 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -23,7 +23,7 @@ pub type BlobRecycler = Recycler; pub const NUM_PACKETS: usize = 1024 * 8; pub const BLOB_SIZE: usize = (64 * 1024 - 128); // wikipedia says there should be 20b for ipv4 headers pub const BLOB_DATA_SIZE: usize = BLOB_SIZE - (BLOB_HEADER_SIZE * 2); -pub const PACKET_DATA_SIZE: usize = 256; +pub const PACKET_DATA_SIZE: usize = 512; pub const NUM_BLOBS: usize = (NUM_PACKETS * PACKET_DATA_SIZE) / BLOB_SIZE; #[derive(Clone, Default, Debug, PartialEq)] diff --git a/src/rpc.rs b/src/rpc.rs index ed58a5a59b..d34f94cf14 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -226,7 +226,7 @@ mod tests { let bank = Bank::new(&alice); let last_id = bank.last_id(); - let tx = Transaction::new(&alice.keypair(), bob_pubkey, 20, last_id); + let tx = Transaction::system_move(&alice.keypair(), bob_pubkey, 20, last_id, 0); bank.process_transaction(&tx).expect("process transaction"); let request_processor = JsonRpcRequestProcessor::new(Arc::new(bank)); diff --git a/src/system_contract.rs b/src/system_contract.rs new file mode 100644 index 0000000000..828ec4d468 --- /dev/null +++ b/src/system_contract.rs @@ -0,0 +1,80 @@ +//! system smart contract + +use bank::Account; +use bincode::deserialize; +use signature::Pubkey; +use transaction::Transaction; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SystemContract { + /// Create a new account + /// * Transaction::keys[0] - source + /// * Transaction::keys[1] - new account key + /// * tokens - number of tokens to transfer to the new account + /// * space - memory to allocate if greater then zero + /// * contract - the contract id of the new account + CreateAccount { + tokens: i64, + space: u64, + contract_id: Option, + }, + /// Assign account to a contract + /// * Transaction::keys[0] - account to assign + Assign { contract_id: Pubkey }, + /// Move tokens + /// * Transaction::keys[0] - source + /// * Transaction::keys[1] - destination + Move { tokens: i64 }, +} + +pub const SYSTEM_CONTRACT_ID: [u8; 32] = [0u8; 32]; + +impl SystemContract { + pub fn check_id(contract_id: &Pubkey) -> bool { + contract_id.as_ref() == SYSTEM_CONTRACT_ID + } + + pub fn id() -> Pubkey { + Pubkey::new(&SYSTEM_CONTRACT_ID) + } + pub fn get_balance(account: &Account) -> i64 { + account.tokens + } + pub fn process_transaction(tx: &Transaction, accounts: &mut [Account]) { + let syscall: SystemContract = deserialize(&tx.userdata).unwrap(); + match syscall { + SystemContract::CreateAccount { + tokens, + space, + contract_id, + } => { + if !Self::check_id(&accounts[1].contract_id) { + return; + } + if !Self::check_id(&accounts[0].contract_id) { + return; + } + if space > 0 && !accounts[1].userdata.is_empty() { + return; + } + accounts[0].tokens -= tokens; + accounts[1].tokens += tokens; + if let Some(id) = contract_id { + accounts[1].contract_id = id; + } + accounts[1].userdata = vec![0; space as usize]; + } + SystemContract::Assign { contract_id } => { + if !Self::check_id(&accounts[0].contract_id) { + return; + } + accounts[0].contract_id = contract_id; + } + SystemContract::Move { tokens } => { + //bank should be verifying correctness + accounts[0].tokens -= tokens; + accounts[1].tokens += tokens; + } + } + } +} diff --git a/src/thin_client.rs b/src/thin_client.rs index 6a53994f8e..db353f8612 100644 --- a/src/thin_client.rs +++ b/src/thin_client.rs @@ -192,7 +192,7 @@ impl ThinClient { // In the future custom contracts would need their own introspection self.balances .get(pubkey) - .map(Bank::get_balance_of_budget_payment_plan) + .map(Bank::read_balance) .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "nokey")) } @@ -429,7 +429,6 @@ pub fn poll_gossip_for_leader(leader_ncp: SocketAddr, timeout: Option) -> R mod tests { use super::*; use bank::Bank; - use budget::Budget; use crdt::Node; use fullnode::Fullnode; use ledger::LedgerWriter; @@ -437,7 +436,7 @@ mod tests { use mint::Mint; use signature::{Keypair, KeypairUtil}; use std::fs::remove_dir_all; - use transaction::{Instruction, Plan}; + use system_contract::SystemContract; fn tmp_ledger(name: &str, mint: &Mint) -> String { use std::env; @@ -542,10 +541,9 @@ mod tests { let last_id = client.get_last_id(); let mut tr2 = Transaction::new(&alice.keypair(), bob_pubkey, 501, last_id); - let mut instruction2 = tr2.instruction(); - if let Instruction::NewContract(ref mut contract) = instruction2 { - contract.tokens = 502; - contract.plan = Plan::Budget(Budget::new_payment(502, bob_pubkey)); + let mut instruction2 = deserialize(&tr2.userdata).unwrap(); + if let SystemContract::Move { ref mut tokens } = instruction2 { + *tokens = 502; } tr2.userdata = serialize(&instruction2).unwrap(); let signature = client.transfer_signed(&tr2).unwrap(); @@ -595,9 +593,8 @@ mod tests { let signature = client .transfer(500, &alice.keypair(), bob_pubkey, &last_id) .unwrap(); - sleep(Duration::from_millis(100)); - assert!(client.check_signature(&signature)); + assert!(client.poll_for_signature(&signature).is_ok()); server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); diff --git a/src/transaction.rs b/src/transaction.rs index 53a6d263b6..2e94fcb902 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -2,80 +2,19 @@ use bincode::{deserialize, serialize}; use budget::{Budget, Condition}; +use budget_contract::BudgetContract; use chrono::prelude::*; use hash::Hash; -use payment_plan::{Payment, PaymentPlan, Witness}; +use instruction::{Contract, Instruction, Plan, Vote}; +use payment_plan::{Payment, PaymentPlan}; use signature::{Keypair, KeypairUtil, Pubkey, Signature}; use std::mem::size_of; +use system_contract::SystemContract; pub const SIGNED_DATA_OFFSET: usize = size_of::(); pub const SIG_OFFSET: usize = 0; pub const PUB_KEY_OFFSET: usize = size_of::() + size_of::(); -/// The type of payment plan. Each item must implement the PaymentPlan trait. -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub enum Plan { - /// The builtin contract language Budget. - Budget(Budget), -} - -// A proxy for the underlying DSL. -impl PaymentPlan for Plan { - fn final_payment(&self) -> Option { - match self { - Plan::Budget(budget) => budget.final_payment(), - } - } - - fn verify(&self, spendable_tokens: i64) -> bool { - match self { - Plan::Budget(budget) => budget.verify(spendable_tokens), - } - } - - fn apply_witness(&mut self, witness: &Witness, from: &Pubkey) { - match self { - Plan::Budget(budget) => budget.apply_witness(witness, from), - } - } -} - -/// A smart contract. -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct Contract { - /// The number of tokens allocated to the `Plan` and any transaction fees. - pub tokens: i64, - pub plan: Plan, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct Vote { - /// We send some gossip specific membership information through the vote to shortcut - /// liveness voting - /// The version of the CRDT struct that the last_id of this network voted with - pub version: u64, - /// The version of the CRDT struct that has the same network configuration as this one - pub contact_info_version: u64, - // TODO: add signature of the state here as well -} - -/// An instruction to progress the smart contract. -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub enum Instruction { - /// Declare and instantiate `Contract`. - NewContract(Contract), - - /// Tell a payment plan acknowledge the given `DateTime` has past. - ApplyTimestamp(DateTime), - - /// Tell the payment plan that the `NewContract` with `Signature` has been - /// signed by the containing transaction's `Pubkey`. - ApplySignature(Signature), - - /// Vote for a PoH that is equal to the lastid of this transaction - NewVote(Vote), -} - /// An instruction signed by a client with `Pubkey`. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct Transaction { @@ -88,6 +27,8 @@ pub struct Transaction { /// In the future which key pays the fee and which keys have signatures would be configurable. /// * keys[1] - Typically this is the contract context or the recipient of the tokens pub keys: Vec, + /// the contract id to execute + pub contract_id: Pubkey, /// The ID of a recent ledger entry. pub last_id: Hash, @@ -110,6 +51,7 @@ impl Transaction { fn new_with_userdata( from_keypair: &Keypair, transaction_keys: &[Pubkey], + contract_id: Pubkey, userdata: Vec, last_id: Hash, fee: i64, @@ -120,6 +62,7 @@ impl Transaction { let mut tx = Transaction { signature: Signature::default(), keys, + contract_id, last_id, fee, userdata, @@ -127,20 +70,8 @@ impl Transaction { tx.sign(from_keypair); tx } - /// Create a signed transaction from the given `Instruction`. - fn new_from_instruction( - from_keypair: &Keypair, - contract: Pubkey, - instruction: &Instruction, - last_id: Hash, - fee: i64, - ) -> Self { - let userdata = serialize(instruction).unwrap(); - Self::new_with_userdata(from_keypair, &[contract], userdata, last_id, fee) - } - /// Create and sign a new Transaction. Used for unit-testing. - pub fn new_taxed( + pub fn budget_new_taxed( from_keypair: &Keypair, contract: Pubkey, tokens: i64, @@ -154,46 +85,80 @@ impl Transaction { let budget = Budget::Pay(payment); let plan = Plan::Budget(budget); let instruction = Instruction::NewContract(Contract { plan, tokens }); - Self::new_from_instruction(from_keypair, contract, &instruction, last_id, fee) + let userdata = serialize(&instruction).unwrap(); + Self::new_with_userdata( + from_keypair, + &[contract], + BudgetContract::id(), + userdata, + last_id, + fee, + ) } /// Create and sign a new Transaction. Used for unit-testing. - pub fn new(from_keypair: &Keypair, to: Pubkey, tokens: i64, last_id: Hash) -> Self { - Self::new_taxed(from_keypair, to, tokens, 0, last_id) + pub fn budget_new(from_keypair: &Keypair, to: Pubkey, tokens: i64, last_id: Hash) -> Self { + Self::budget_new_taxed(from_keypair, to, tokens, 0, last_id) } /// Create and sign a new Witness Timestamp. Used for unit-testing. - pub fn new_timestamp( + pub fn budget_new_timestamp( from_keypair: &Keypair, contract: Pubkey, + to: Pubkey, dt: DateTime, last_id: Hash, ) -> Self { let instruction = Instruction::ApplyTimestamp(dt); - Self::new_from_instruction(from_keypair, contract, &instruction, last_id, 0) + let userdata = serialize(&instruction).unwrap(); + Self::new_with_userdata( + from_keypair, + &[contract, to], + BudgetContract::id(), + userdata, + last_id, + 0, + ) } /// Create and sign a new Witness Signature. Used for unit-testing. - pub fn new_signature( + pub fn budget_new_signature( from_keypair: &Keypair, contract: Pubkey, + to: Pubkey, signature: Signature, last_id: Hash, ) -> Self { let instruction = Instruction::ApplySignature(signature); - Self::new_from_instruction(from_keypair, contract, &instruction, last_id, 0) + let userdata = serialize(&instruction).unwrap(); + Self::new_with_userdata( + from_keypair, + &[contract, to], + BudgetContract::id(), + userdata, + last_id, + 0, + ) } - pub fn new_vote(from_keypair: &Keypair, vote: Vote, last_id: Hash, fee: i64) -> Self { + pub fn budget_new_vote(from_keypair: &Keypair, vote: Vote, last_id: Hash, fee: i64) -> Self { let instruction = Instruction::NewVote(vote); - let userdata = serialize(&instruction).expect("serealize instruction"); - Self::new_with_userdata(from_keypair, &[], userdata, last_id, fee) + let userdata = serialize(&instruction).expect("serialize instruction"); + Self::new_with_userdata( + from_keypair, + &[], + BudgetContract::id(), + userdata, + last_id, + fee, + ) } /// Create and sign a postdated Transaction. Used for unit-testing. - pub fn new_on_date( + pub fn budget_new_on_date( from_keypair: &Keypair, to: Pubkey, + contract: Pubkey, dt: DateTime, tokens: i64, last_id: Hash, @@ -205,14 +170,73 @@ impl Transaction { ); let plan = Plan::Budget(budget); let instruction = Instruction::NewContract(Contract { plan, tokens }); - let userdata = serialize(&instruction).expect("serealize instruction"); - Self::new_with_userdata(from_keypair, &[to], userdata, last_id, 0) + let userdata = serialize(&instruction).expect("serialize instruction"); + Self::new_with_userdata( + from_keypair, + &[contract], + BudgetContract::id(), + userdata, + last_id, + 0, + ) + } + /// Create and sign new SystemContract::CreateAccount transaction + pub fn system_new_create( + from_keypair: &Keypair, + to: Pubkey, + last_id: Hash, + tokens: i64, + space: u64, + contract_id: Option, + fee: i64, + ) -> Self { + let create = SystemContract::CreateAccount { + tokens, //TODO, the tokens to allocate might need to be higher then 0 in the future + space, + contract_id, + }; + Transaction::new_with_userdata( + from_keypair, + &[to], + SystemContract::id(), + serialize(&create).unwrap(), + last_id, + fee, + ) + } + /// Create and sign new SystemContract::CreateAccount transaction with some defaults + pub fn system_new(from_keypair: &Keypair, to: Pubkey, tokens: i64, last_id: Hash) -> Self { + Transaction::system_new_create(from_keypair, to, last_id, tokens, 0, None, 0) + } + /// Create and sign new SystemContract::Move transaction + pub fn system_move( + from_keypair: &Keypair, + to: Pubkey, + tokens: i64, + last_id: Hash, + fee: i64, + ) -> Self { + let create = SystemContract::Move { tokens }; + Transaction::new_with_userdata( + from_keypair, + &[to], + SystemContract::id(), + serialize(&create).unwrap(), + last_id, + fee, + ) + } + /// Create and sign new SystemContract::Move transaction + pub fn new(from_keypair: &Keypair, to: Pubkey, tokens: i64, last_id: Hash) -> Self { + Transaction::system_move(from_keypair, to, tokens, last_id, 0) } - /// Get the transaction data to sign. fn get_sign_data(&self) -> Vec { let mut data = serialize(&(&self.keys)).expect("serialize keys"); + let contract_id = serialize(&(&self.contract_id)).expect("serialize contract_id"); + data.extend_from_slice(&contract_id); + let last_id_data = serialize(&(&self.last_id)).expect("serialize last_id"); data.extend_from_slice(&last_id_data); @@ -237,19 +261,8 @@ impl Transaction { .verify(&self.from().as_ref(), &self.get_sign_data()) } - /// Verify only the payment plan. - pub fn verify_plan(&self) -> bool { - let instruction = deserialize(&self.userdata); - if let Ok(Instruction::NewContract(contract)) = instruction { - self.fee >= 0 - && self.fee <= contract.tokens - && contract.plan.verify(contract.tokens - self.fee) - } else { - true - } - } pub fn vote(&self) -> Option<(Pubkey, Vote, Hash)> { - if let Instruction::NewVote(vote) = self.instruction() { + if let Some(Instruction::NewVote(vote)) = self.instruction() { Some((*self.from(), vote, self.last_id)) } else { None @@ -258,8 +271,18 @@ impl Transaction { pub fn from(&self) -> &Pubkey { &self.keys[0] } - pub fn instruction(&self) -> Instruction { - deserialize(&self.userdata).unwrap() + pub fn instruction(&self) -> Option { + deserialize(&self.userdata).ok() + } + /// Verify only the payment plan. + pub fn verify_plan(&self) -> bool { + if let Some(Instruction::NewContract(contract)) = self.instruction() { + self.fee >= 0 + && self.fee <= contract.tokens + && contract.plan.verify(contract.tokens - self.fee) + } else { + true + } } } @@ -267,7 +290,7 @@ pub fn test_tx() -> Transaction { let keypair1 = Keypair::new(); let pubkey1 = keypair1.pubkey(); let zero = Hash::default(); - Transaction::new(&keypair1, pubkey1, 42, zero) + Transaction::system_new(&keypair1, pubkey1, 42, zero) } #[cfg(test)] @@ -292,7 +315,7 @@ mod tests { fn test_claim() { let keypair = Keypair::new(); let zero = Hash::default(); - let tx0 = Transaction::new(&keypair, keypair.pubkey(), 42, zero); + let tx0 = Transaction::budget_new(&keypair, keypair.pubkey(), 42, zero); assert!(tx0.verify_plan()); } @@ -302,7 +325,7 @@ mod tests { let keypair0 = Keypair::new(); let keypair1 = Keypair::new(); let pubkey1 = keypair1.pubkey(); - let tx0 = Transaction::new(&keypair0, pubkey1, 42, zero); + let tx0 = Transaction::budget_new(&keypair0, pubkey1, 42, zero); assert!(tx0.verify_plan()); } @@ -311,9 +334,9 @@ mod tests { let zero = Hash::default(); let keypair0 = Keypair::new(); let pubkey1 = Keypair::new().pubkey(); - assert!(Transaction::new_taxed(&keypair0, pubkey1, 1, 1, zero).verify_plan()); - assert!(!Transaction::new_taxed(&keypair0, pubkey1, 1, 2, zero).verify_plan()); - assert!(!Transaction::new_taxed(&keypair0, pubkey1, 1, -1, zero).verify_plan()); + assert!(Transaction::budget_new_taxed(&keypair0, pubkey1, 1, 1, zero).verify_plan()); + assert!(!Transaction::budget_new_taxed(&keypair0, pubkey1, 1, 2, zero).verify_plan()); + assert!(!Transaction::budget_new_taxed(&keypair0, pubkey1, 1, -1, zero).verify_plan()); } #[test] @@ -329,6 +352,7 @@ mod tests { keys: vec![], last_id: Default::default(), signature: Default::default(), + contract_id: Default::default(), fee: 0, userdata, }; @@ -342,8 +366,8 @@ mod tests { let zero = Hash::default(); let keypair = Keypair::new(); let pubkey = keypair.pubkey(); - let mut tx = Transaction::new(&keypair, pubkey, 42, zero); - let mut instruction = tx.instruction(); + let mut tx = Transaction::budget_new(&keypair, pubkey, 42, zero); + let mut instruction = tx.instruction().unwrap(); if let Instruction::NewContract(ref mut contract) = instruction { contract.tokens = 1_000_000; // <-- attack, part 1! if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan { @@ -362,9 +386,9 @@ mod tests { let thief_keypair = Keypair::new(); let pubkey1 = keypair1.pubkey(); let zero = Hash::default(); - let mut tx = Transaction::new(&keypair0, pubkey1, 42, zero); + let mut tx = Transaction::budget_new(&keypair0, pubkey1, 42, zero); let mut instruction = tx.instruction(); - if let Instruction::NewContract(ref mut contract) = instruction { + if let Some(Instruction::NewContract(ref mut contract)) = instruction { if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan { payment.to = thief_keypair.pubkey(); // <-- attack! } @@ -416,8 +440,8 @@ mod tests { let keypair0 = Keypair::new(); let keypair1 = Keypair::new(); let zero = Hash::default(); - let mut tx = Transaction::new(&keypair0, keypair1.pubkey(), 1, zero); - let mut instruction = tx.instruction(); + let mut tx = Transaction::budget_new(&keypair0, keypair1.pubkey(), 1, zero); + let mut instruction = tx.instruction().unwrap(); if let Instruction::NewContract(ref mut contract) = instruction { if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan { payment.tokens = 2; // <-- attack! @@ -427,7 +451,7 @@ mod tests { assert!(!tx.verify_plan()); // Also, ensure all branchs of the plan spend all tokens - let mut instruction = tx.instruction(); + let mut instruction = tx.instruction().unwrap(); if let Instruction::NewContract(ref mut contract) = instruction { if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan { payment.tokens = 0; // <-- whoops! diff --git a/src/tvu.rs b/src/tvu.rs index eebe84abce..1998be18d0 100644 --- a/src/tvu.rs +++ b/src/tvu.rs @@ -261,7 +261,7 @@ pub mod tests { bank.register_entry_id(&cur_hash); cur_hash = hash(&cur_hash.as_ref()); - let tx0 = Transaction::new( + let tx0 = Transaction::system_new( &mint.keypair(), bob_keypair.pubkey(), transfer_amount, diff --git a/src/vote_stage.rs b/src/vote_stage.rs index 8413df3cda..0faee9ec16 100644 --- a/src/vote_stage.rs +++ b/src/vote_stage.rs @@ -45,7 +45,7 @@ pub fn create_new_signed_vote_blob( debug!("voting on {:?}", &last_id.as_ref()[..8]); wcrdt.new_vote(*last_id) }?; - let tx = Transaction::new_vote(&keypair, vote, *last_id, 0); + let tx = Transaction::budget_new_vote(&keypair, vote, *last_id, 0); { let mut blob = shared_blob.write().unwrap(); let bytes = serialize(&tx)?; @@ -227,6 +227,7 @@ pub mod tests { use crdt::{Crdt, Node, NodeInfo}; use entry::next_entry; use hash::{hash, Hash}; + use instruction::Vote; use logger; use mint::Mint; use packet::BlobRecycler; @@ -235,7 +236,7 @@ pub mod tests { use std::sync::atomic::AtomicBool; use std::sync::mpsc::channel; use std::sync::{Arc, RwLock}; - use transaction::{Transaction, Vote}; + use transaction::Transaction; /// Ensure the VoteStage issues votes at the expected cadence #[test] @@ -291,7 +292,7 @@ pub mod tests { // give the leader some tokens let give_leader_tokens_tx = - Transaction::new(&mint.keypair(), leader_pubkey.clone(), 100, entry.id); + Transaction::system_new(&mint.keypair(), leader_pubkey.clone(), 100, entry.id); bank.process_transaction(&give_leader_tokens_tx).unwrap(); leader_crdt.set_leader(leader_pubkey);