Migrate Budget DSL to use the Account state (#979)

* Migrate Budget DSL to use the Account state instead of global bank data structures.

* Serialize Instruction into Transaction::userdata.
* Store the pending set in the Account::userdata
* Enforce the token balance rules on contract execution. This becomes the entry point for generic contracts.
* This pr will have a performance impact on the bank. The next set of changes will fix this by locking each account during multi threaded execution of all the contracts.
* With this change a contract transaction needs to store its state under an address. That address could be the destination of the tokens, or any random address. For the latter, an extra step would be needed to claim the tokens which isn't implemented by budget_dsl at the moment.
* test tracking issue 1157
This commit is contained in:
anatoly yakovenko 2018-09-07 20:18:36 -07:00 committed by GitHub
parent ddd1871840
commit c34d911eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 417 additions and 242 deletions

View File

@ -3,7 +3,7 @@
//! on behalf of the caller, and a low-level API for when they have //! on behalf of the caller, and a low-level API for when they have
//! already been signed and verified. //! already been signed and verified.
use bincode::serialize; use bincode::{deserialize, serialize};
use chrono::prelude::*; use chrono::prelude::*;
use counter::Counter; use counter::Counter;
use entry::Entry; use entry::Entry;
@ -36,7 +36,7 @@ pub const MAX_ENTRY_IDS: usize = 1024 * 16;
pub const VERIFY_BLOCK_SIZE: usize = 16; pub const VERIFY_BLOCK_SIZE: usize = 16;
/// Reasons a transaction might be rejected. /// Reasons a transaction might be rejected.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, Clone)]
pub enum BankError { pub enum BankError {
/// Attempt to debit from `Pubkey`, but no found no record of a prior credit. /// Attempt to debit from `Pubkey`, but no found no record of a prior credit.
AccountNotFound(Pubkey), AccountNotFound(Pubkey),
@ -61,6 +61,10 @@ pub enum BankError {
/// Proof of History verification failed. /// Proof of History verification failed.
LedgerVerificationFailed, LedgerVerificationFailed,
/// Contract's transaction token balance does not equal the balance after the transaction
UnbalancedTransaction(Signature),
/// ContractAlreadyPending
ContractAlreadyPending(Pubkey),
} }
pub type Result<T> = result::Result<T, BankError>; pub type Result<T> = result::Result<T, BankError>;
@ -73,17 +77,17 @@ pub struct Account {
/// A transaction can write to its userdata /// A transaction can write to its userdata
pub userdata: Vec<u8>, pub userdata: Vec<u8>,
} }
#[derive(Default)]
struct ErrorCounters {
account_not_found_validator: usize,
account_not_found_leader: usize,
account_not_found_vote: usize,
}
/// The state of all accounts and contracts after processing its entries. /// The state of all accounts and contracts after processing its entries.
pub struct Bank { pub struct Bank {
/// A map of account public keys to the balance in that account. /// A map of account public keys to the balance in that account.
accounts: RwLock<HashMap<Pubkey, Account>>, accounts: RwLock<HashMap<Pubkey, Account>>,
/// 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<HashMap<Signature, Plan>>,
/// A FIFO queue of `last_id` items, where each item is a set of signatures /// A FIFO queue of `last_id` items, where each item is a set of signatures
/// that have been processed using that `last_id`. Rejected `last_id` /// that have been processed using that `last_id`. Rejected `last_id`
/// values are so old that the `last_id` has been pulled out of the queue. /// values are so old that the `last_id` has been pulled out of the queue.
@ -109,7 +113,6 @@ impl Default for Bank {
fn default() -> Self { fn default() -> Self {
Bank { Bank {
accounts: RwLock::new(HashMap::new()), accounts: RwLock::new(HashMap::new()),
pending: RwLock::new(HashMap::new()),
last_ids: RwLock::new(VecDeque::new()), last_ids: RwLock::new(VecDeque::new()),
last_ids_sigs: RwLock::new(HashMap::new()), last_ids_sigs: RwLock::new(HashMap::new()),
transaction_count: AtomicUsize::new(0), transaction_count: AtomicUsize::new(0),
@ -129,7 +132,11 @@ impl Bank {
/// Create an Bank using a deposit. /// Create an Bank using a deposit.
pub fn new_from_deposit(deposit: &Payment) -> Self { pub fn new_from_deposit(deposit: &Payment) -> Self {
let bank = Self::default(); let bank = Self::default();
bank.apply_payment(deposit, &mut bank.accounts.write().unwrap()); {
let mut accounts = bank.accounts.write().unwrap();
let account = accounts.entry(deposit.to).or_insert_with(Account::default);
Self::apply_payment(deposit, account);
}
bank bank
} }
@ -144,12 +151,10 @@ impl Bank {
bank bank
} }
/// Commit funds to the `payment.to` party. /// Commit funds to the given account
fn apply_payment(&self, payment: &Payment, accounts: &mut HashMap<Pubkey, Account>) { fn apply_payment(payment: &Payment, account: &mut Account) {
accounts trace!("apply payments {}", payment.tokens);
.entry(payment.to) account.tokens += payment.tokens;
.or_insert_with(Account::default)
.tokens += payment.tokens;
} }
/// Return the last entry ID registered. /// Return the last entry ID registered.
@ -171,23 +176,6 @@ impl Bank {
Ok(()) Ok(())
} }
/// Forget the given `signature` because its transaction was rejected.
fn forget_signature(signatures: &mut HashSet<Signature>, signature: &Signature) {
signatures.remove(signature);
}
/// 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_sigs
.write()
.expect("'last_ids' read lock in forget_signature_with_last_id")
.get_mut(last_id)
{
Self::forget_signature(&mut entry.0, signature);
}
}
/// Forget all signatures. Useful for benchmarking. /// Forget all signatures. Useful for benchmarking.
pub fn clear_signatures(&self) { pub fn clear_signatures(&self) {
for (_, sigs) in self.last_ids_sigs.write().unwrap().iter_mut() { for (_, sigs) in self.last_ids_sigs.write().unwrap().iter_mut() {
@ -247,141 +235,230 @@ impl Bank {
/// Deduct tokens from the 'from' address the account has sufficient /// Deduct tokens from the 'from' address the account has sufficient
/// funds and isn't a duplicate. /// funds and isn't a duplicate.
fn apply_debits( fn apply_debits(
&self,
tx: &Transaction, tx: &Transaction,
accounts: &mut HashMap<Pubkey, Account>, accounts: &mut [Account],
vote_no_account_err: &mut usize, instruction: &Instruction,
no_account_err: &mut usize,
) -> Result<()> { ) -> Result<()> {
let mut purge = false;
{ {
let option = accounts.get_mut(&tx.from); let empty = accounts[0].userdata.is_empty();
if option.is_none() { let tokens = if !empty { 0 } else { accounts[0].tokens };
if let Instruction::NewVote(_) = &tx.instruction { if let Instruction::NewContract(contract) = &instruction {
*vote_no_account_err += 1;
} else {
*no_account_err += 1;
}
return Err(BankError::AccountNotFound(tx.from));
}
let bal = option.unwrap();
self.reserve_signature_with_last_id(&tx.signature, &tx.last_id)?;
if let Instruction::NewContract(contract) = &tx.instruction {
if contract.tokens < 0 { if contract.tokens < 0 {
return Err(BankError::NegativeTokens); return Err(BankError::NegativeTokens);
} }
if bal.tokens < contract.tokens { if tokens < contract.tokens {
self.forget_signature_with_last_id(&tx.signature, &tx.last_id); return Err(BankError::InsufficientFunds(tx.keys[0]));
return Err(BankError::InsufficientFunds(tx.from));
} else if bal.tokens == contract.tokens {
purge = true;
} else { } else {
let bal = &mut accounts[0];
bal.tokens -= contract.tokens; bal.tokens -= contract.tokens;
} }
}; };
} }
if purge {
accounts.remove(&tx.from);
}
Ok(()) Ok(())
} }
/// Apply only a transaction's credits. /// Apply only a transaction's credits.
/// Note: It is safe to apply credits from multiple transactions in parallel. /// Note: It is safe to apply credits from multiple transactions in parallel.
fn apply_credits(&self, tx: &Transaction, accounts: &mut HashMap<Pubkey, Account>) { fn apply_credits(
match &tx.instruction { tx: &Transaction,
accounts: &mut [Account],
instruction: &Instruction,
) -> Result<()> {
match instruction {
Instruction::NewContract(contract) => { Instruction::NewContract(contract) => {
let plan = contract.plan.clone(); let plan = contract.plan.clone();
if let Some(payment) = plan.final_payment() { if let Some(payment) = plan.final_payment() {
self.apply_payment(&payment, accounts); Self::apply_payment(&payment, &mut accounts[1]);
Ok(())
} else if !accounts[1].userdata.is_empty() {
Err(BankError::ContractAlreadyPending(tx.keys[1]))
} else { } else {
let mut pending = self let mut pending = HashMap::new();
.pending
.write()
.expect("'pending' write lock in apply_credits");
pending.insert(tx.signature, plan); pending.insert(tx.signature, plan);
//TODO this is a temporary on demand allocaiton
//until system contract requires explicit allocation of memory
accounts[1].userdata = serialize(&pending).unwrap();
accounts[1].tokens += contract.tokens;
Ok(())
} }
} }
Instruction::ApplyTimestamp(dt) => { Instruction::ApplyTimestamp(dt) => {
let _ = self.apply_timestamp(tx.from, *dt); Self::apply_timestamp(tx.keys[0], *dt, &mut accounts[1]);
Ok(())
} }
Instruction::ApplySignature(signature) => { Instruction::ApplySignature(signature) => {
let _ = self.apply_signature(tx.from, *signature); Self::apply_signature(tx.keys[0], *signature, accounts);
Ok(())
} }
Instruction::NewVote(_vote) => { Instruction::NewVote(_vote) => {
trace!("GOT VOTE! last_id={:?}", &tx.last_id.as_ref()[..8]);
// TODO: record the vote in the stake table... // TODO: record the vote in the stake table...
trace!("GOT VOTE! last_id={}", tx.last_id);
Ok(())
} }
} }
} }
fn save_data(&self, tx: &Transaction, accounts: &mut HashMap<Pubkey, Account>) { /// Budget DSL contract interface
//TODO This is a temporary implementation until the full rules on memory management for /// * tx - the transaction
//smart contracts are implemented. See github issue #953 /// * accounts[0] - The source of the tokens
if !tx.userdata.is_empty() { /// * accounts[1] - The contract context. Once the contract has been completed, the tokens can
if let Some(ref mut account) = accounts.get_mut(&tx.from) { /// be spent from this account .
if account.userdata.len() != tx.userdata.len() { pub fn process_transaction_of_budget_instruction(
account.userdata.resize(tx.userdata.len(), 0); tx: &Transaction,
} accounts: &mut [Account],
account.userdata.copy_from_slice(&tx.userdata); ) -> Result<()> {
let instruction = tx.instruction();
Self::apply_debits(tx, accounts, &instruction)?;
Self::apply_credits(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<Signature, Plan> = 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 /// Process a Transaction. If it contains a payment plan that requires a witness
/// to progress, the payment plan will be stored in the bank. /// to progress, the payment plan will be stored in the bank.
pub fn process_transaction(&self, tx: &Transaction) -> Result<()> { pub fn process_transaction(&self, tx: &Transaction) -> Result<()> {
let accounts = &mut self.accounts.write().unwrap(); match self.process_transactions(vec![tx.clone()])[0] {
let mut vote_no_account_err = 0; Err(ref e) => {
let mut no_account_err = 0; info!("process_transaction error: {:?}", e);
self.apply_debits(tx, accounts, &mut vote_no_account_err, &mut no_account_err)?; Err((*e).clone())
self.apply_credits(tx, accounts); }
self.save_data(tx, accounts); Ok(_) => Ok(()),
self.transaction_count.fetch_add(1, Ordering::Relaxed); }
Ok(()) }
fn load_account(
&self,
tx: &Transaction,
accounts: &HashMap<Pubkey, Account>,
error_counters: &mut ErrorCounters,
) -> Result<Vec<Account>> {
// Copy all the accounts
if accounts.get(&tx.keys[0]).is_none() {
if !self.is_leader {
error_counters.account_not_found_validator += 1;
} else {
error_counters.account_not_found_leader += 1;
}
if let 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()))
} else {
let mut called_accounts: Vec<Account> = tx
.keys
.iter()
.map(|key| accounts.get(key).cloned().unwrap_or(Account::default()))
.collect();
// There is no way to predict what contract will execute without an error
// If a fee can pay for execution then the contract will be scheduled
self.reserve_signature_with_last_id(&tx.signature, &tx.last_id)?;
called_accounts[0].tokens -= tx.fee;
Ok(called_accounts)
}
}
fn load_accounts(
&self,
txs: &Vec<Transaction>,
accounts: &HashMap<Pubkey, Account>,
error_counters: &mut ErrorCounters,
) -> Vec<Result<Vec<Account>>> {
txs.iter()
.map(|tx| self.load_account(tx, accounts, error_counters))
.collect()
}
pub fn execute_transaction(tx: Transaction, accounts: &mut [Account]) -> Result<Transaction> {
let pre_total: i64 = accounts.iter().map(|a| a.tokens).sum();
// 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);
// 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.
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)
}
}
pub fn store_accounts(
res: &Vec<Result<Transaction>>,
loaded: &Vec<Result<Vec<Account>>>,
accounts: &mut HashMap<Pubkey, Account>,
) {
loaded.iter().zip(res.iter()).for_each(|(racc, rtx)| {
if let (Ok(acc), Ok(tx)) = (racc, rtx) {
tx.keys.iter().zip(acc.iter()).for_each(|(key, account)| {
//purge if 0
if account.tokens == 0 {
accounts.remove(&key);
} else {
*accounts.entry(*key).or_insert_with(Account::default) = account.clone();
assert_eq!(accounts.get(key).unwrap().tokens, account.tokens);
}
});
};
});
} }
/// Process a batch of transactions. /// Process a batch of transactions.
#[must_use] #[must_use]
pub fn process_transactions(&self, txs: Vec<Transaction>) -> Vec<Result<Transaction>> { pub fn process_transactions(&self, txs: Vec<Transaction>) -> Vec<Result<Transaction>> {
let accounts = &mut self.accounts.write().unwrap();
debug!("processing Transactions {}", txs.len()); debug!("processing Transactions {}", txs.len());
// TODO right now a single write lock is held for the duration of processing all the
// transactions
// To break this lock each account needs to be locked to prevent concurrent access
let mut accounts = self.accounts.write().unwrap();
let txs_len = txs.len(); let txs_len = txs.len();
let mut vote_no_account_err = 0; let mut error_counters = ErrorCounters::default();
let mut no_account_err = 0;
let now = Instant::now(); let now = Instant::now();
let results: Vec<_> = txs let mut loaded_accounts = self.load_accounts(&txs, &mut accounts, &mut error_counters);
.into_iter() let load_elapsed = now.elapsed();
.map(|tx| {
self.apply_debits(&tx, accounts, &mut vote_no_account_err, &mut no_account_err)
.map(|_| tx)
})
.collect(); // Calling collect() here forces all debits to complete before moving on.
let debits = now.elapsed();
let now = Instant::now(); let now = Instant::now();
let res: Vec<_> = results let res: Vec<Result<Transaction>> = loaded_accounts
.into_iter() .iter_mut()
.map(|result| { .zip(txs.into_iter())
result.map(|tx| { .map(|(acc, tx)| match acc {
self.apply_credits(&tx, accounts); Err(e) => Err(e.clone()),
tx Ok(ref mut accounts) => Self::execute_transaction(tx, accounts),
})
}) })
.collect(); .collect();
let execution_elapsed = now.elapsed();
let now = Instant::now();
Self::store_accounts(&res, &loaded_accounts, &mut accounts);
let write_elapsed = now.elapsed();
debug!( debug!(
"debits: {} us credits: {:?} us tx: {}", "load: {} us execution: {} us write: {} us tx: {}",
duration_as_us(&debits), duration_as_us(&load_elapsed),
duration_as_us(&now.elapsed()), duration_as_us(&execution_elapsed),
duration_as_us(&write_elapsed),
txs_len txs_len
); );
let mut tx_count = 0; let mut tx_count = 0;
let mut err_count = 0; let mut err_count = 0;
for r in &res { for r in &res {
@ -389,7 +466,7 @@ impl Bank {
tx_count += 1; tx_count += 1;
} else { } else {
if err_count == 0 { if err_count == 0 {
info!("tx error: {:?}", r); trace!("tx error: {:?}", r);
} }
err_count += 1; err_count += 1;
} }
@ -400,14 +477,17 @@ impl Bank {
inc_new_counter_info!("bank-process_transactions_err-validator", err_count); inc_new_counter_info!("bank-process_transactions_err-validator", err_count);
inc_new_counter_info!( inc_new_counter_info!(
"bank-appy_debits-account_not_found-validator", "bank-appy_debits-account_not_found-validator",
no_account_err error_counters.account_not_found_validator
); );
} else { } else {
inc_new_counter_info!("bank-process_transactions_err-leader", err_count); inc_new_counter_info!("bank-process_transactions_err-leader", err_count);
inc_new_counter_info!("bank-appy_debits-generic_account_not_found", no_account_err); inc_new_counter_info!(
"bank-appy_debits-account_not_found-leader",
error_counters.account_not_found_leader
);
inc_new_counter_info!( inc_new_counter_info!(
"bank-appy_debits-vote_account_not_found", "bank-appy_debits-vote_account_not_found",
vote_no_account_err error_counters.account_not_found_vote
); );
} }
} }
@ -507,13 +587,18 @@ impl Bank {
.expect("invalid ledger: need at least 2 entries"); .expect("invalid ledger: need at least 2 entries");
{ {
let tx = &entry1.transactions[0]; let tx = &entry1.transactions[0];
let deposit = if let Instruction::NewContract(contract) = &tx.instruction { let instruction = tx.instruction();
let deposit = if let Instruction::NewContract(contract) = instruction {
contract.plan.final_payment() contract.plan.final_payment()
} else { } else {
None None
}.expect("invalid ledger, needs to start with a contract"); }.expect("invalid ledger, needs to start with a contract");
{
self.apply_payment(&deposit, &mut self.accounts.write().unwrap()); let mut accounts = self.accounts.write().unwrap();
let entry = accounts.entry(tx.keys[0]).or_insert_with(Account::default);
Self::apply_payment(&deposit, entry);
trace!("applied genesis payment {:?} {:?}", deposit, entry);
}
} }
self.register_entry_id(&entry0.id); self.register_entry_id(&entry0.id);
self.register_entry_id(&entry1.id); self.register_entry_id(&entry1.id);
@ -535,39 +620,40 @@ impl Bank {
/// Process a Witness Signature. Any payment plans waiting on this signature /// Process a Witness Signature. Any payment plans waiting on this signature
/// will progress one step. /// will progress one step.
fn apply_signature(&self, from: Pubkey, signature: Signature) -> Result<()> { fn apply_signature(from: Pubkey, signature: Signature, account: &mut [Account]) {
if let Occupied(mut e) = self let mut pending: HashMap<Signature, Plan> =
.pending deserialize(&account[1].userdata).unwrap_or(HashMap::new());
.write() if let Occupied(mut e) = pending.entry(signature) {
.expect("write() in apply_signature")
.entry(signature)
{
e.get_mut().apply_witness(&Witness::Signature, &from); e.get_mut().apply_witness(&Witness::Signature, &from);
if let Some(payment) = e.get().final_payment() { if let Some(payment) = e.get().final_payment() {
self.apply_payment(&payment, &mut self.accounts.write().unwrap()); //move the tokens back to the from account
account[0].tokens += payment.tokens;
account[1].tokens -= payment.tokens;
e.remove_entry(); e.remove_entry();
} }
}; };
//TODO this allocation needs to be changed once the runtime only allows for explitly
Ok(()) //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 /// Process a Witness Timestamp. Any payment plans waiting on this timestamp
/// will progress one step. /// will progress one step.
fn apply_timestamp(&self, from: Pubkey, dt: DateTime<Utc>) -> Result<()> { fn apply_timestamp(from: Pubkey, dt: DateTime<Utc>, account: &mut Account) {
let mut pending: HashMap<Signature, Plan> =
deserialize(&account.userdata).unwrap_or(HashMap::new());
// Check to see if any timelocked transactions can be completed. // Check to see if any timelocked transactions can be completed.
let mut completed = vec![]; let mut completed = vec![];
// Hold 'pending' write lock until the end of this function. Otherwise another thread can // 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'. // double-spend if it enters before the modified plan is removed from 'pending'.
let mut pending = self
.pending
.write()
.expect("'pending' write lock in apply_timestamp");
for (key, plan) in pending.iter_mut() { for (key, plan) in pending.iter_mut() {
plan.apply_witness(&Witness::Timestamp(dt), &from); plan.apply_witness(&Witness::Timestamp(dt), &from);
if let Some(payment) = plan.final_payment() { if let Some(_payment) = plan.final_payment() {
self.apply_payment(&payment, &mut self.accounts.write().unwrap());
completed.push(key.clone()); completed.push(key.clone());
} }
} }
@ -575,8 +661,13 @@ impl Bank {
for key in completed { for key in completed {
pending.remove(&key); pending.remove(&key);
} }
//TODO this allocation needs to be changed once the runtime only allows for explitly
Ok(()) //allocated memory
account.userdata = if pending.is_empty() {
vec![]
} else {
serialize(&pending).unwrap()
};
} }
/// Create, sign, and process a Transaction from `keypair` to `to` of /// Create, sign, and process a Transaction from `keypair` to `to` of
@ -610,7 +701,9 @@ impl Bank {
} }
pub fn get_balance(&self, pubkey: &Pubkey) -> i64 { pub fn get_balance(&self, pubkey: &Pubkey) -> i64 {
self.get_account(pubkey).map(|a| a.tokens).unwrap_or(0) self.get_account(pubkey)
.map(|x| Self::get_balance_of_budget_payment_plan(&x))
.unwrap_or(0)
} }
pub fn get_account(&self, pubkey: &Pubkey) -> Option<Account> { pub fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
@ -672,6 +765,13 @@ mod tests {
use std::io::{BufReader, Cursor, Seek, SeekFrom}; use std::io::{BufReader, Cursor, Seek, SeekFrom};
use std::mem::size_of; use std::mem::size_of;
#[test]
fn test_bank_new() {
let mint = Mint::new(10_000);
let bank = Bank::new(&mint);
assert_eq!(bank.get_balance(&mint.pubkey()), 10_000);
}
#[test] #[test]
fn test_two_payments_to_one_party() { fn test_two_payments_to_one_party() {
let mint = Mint::new(10_000); let mint = Mint::new(10_000);
@ -701,6 +801,23 @@ mod tests {
assert_eq!(bank.transaction_count(), 0); assert_eq!(bank.transaction_count(), 0);
} }
// TODO: This test verifies potentially undesirable behavior
// See github issue 1157 (https://github.com/solana-labs/solana/issues/1157)
#[test]
fn test_detect_failed_duplicate_transactions_issue_1157() {
let mint = Mint::new(1);
let bank = Bank::new(&mint);
let tx = Transaction::new(&mint.keypair(), mint.keypair().pubkey(), -1, mint.last_id());
let signature = tx.signature;
assert!(!bank.has_signature(&signature));
assert_eq!(
bank.process_transaction(&tx),
Err(BankError::NegativeTokens)
);
assert!(bank.has_signature(&signature));
}
#[test] #[test]
fn test_account_not_found() { fn test_account_not_found() {
let mint = Mint::new(1); let mint = Mint::new(1);
@ -721,6 +838,7 @@ mod tests {
bank.transfer(1_000, &mint.keypair(), pubkey, mint.last_id()) bank.transfer(1_000, &mint.keypair(), pubkey, mint.last_id())
.unwrap(); .unwrap();
assert_eq!(bank.transaction_count(), 1); assert_eq!(bank.transaction_count(), 1);
assert_eq!(bank.get_balance(&pubkey), 1_000);
assert_eq!( assert_eq!(
bank.transfer(10_001, &mint.keypair(), pubkey, mint.last_id()), bank.transfer(10_001, &mint.keypair(), pubkey, mint.last_id()),
Err(BankError::InsufficientFunds(mint.pubkey())) Err(BankError::InsufficientFunds(mint.pubkey()))
@ -742,56 +860,48 @@ mod tests {
assert_eq!(bank.get_balance(&pubkey), 500); assert_eq!(bank.get_balance(&pubkey), 500);
} }
#[test]
fn test_userdata() {
let mint = Mint::new(10_000);
let bank = Bank::new(&mint);
let pubkey = mint.keypair().pubkey();
let mut tx = Transaction::new(&mint.keypair(), pubkey, 0, bank.last_id());
tx.userdata = vec![1, 2, 3];
let rv = bank.process_transaction(&tx);
assert!(rv.is_ok());
let account = bank.get_account(&pubkey);
assert!(account.is_some());
assert_eq!(account.unwrap().userdata, vec![1, 2, 3]);
}
#[test] #[test]
fn test_transfer_on_date() { fn test_transfer_on_date() {
let mint = Mint::new(1); let mint = Mint::new(2);
let bank = Bank::new(&mint); let bank = Bank::new(&mint);
let pubkey = Keypair::new().pubkey(); let pubkey = Keypair::new().pubkey();
let dt = Utc::now(); let dt = Utc::now();
bank.transfer_on_date(1, &mint.keypair(), pubkey, dt, mint.last_id()) bank.transfer_on_date(1, &mint.keypair(), pubkey, dt, mint.last_id())
.unwrap(); .unwrap();
// Mint's balance will be zero because all funds are locked up. // Mint's balance will be 1 because 1 of the tokens is locked up
assert_eq!(bank.get_balance(&mint.pubkey()), 0); assert_eq!(bank.get_balance(&mint.pubkey()), 1);
// tx count is 1, because debits were applied. // tx count is 1, because debits were applied.
assert_eq!(bank.transaction_count(), 1); assert_eq!(bank.transaction_count(), 1);
// pubkey's balance will be None because the funds have not been // pubkey's balance will be 0 because the funds have not been
// sent. // sent.
assert_eq!(bank.get_balance(&pubkey), 0); assert_eq!(bank.get_balance(&pubkey), 0);
// Now, acknowledge the time in the condition occurred and // Now, acknowledge the time in the condition occurred and
// that pubkey's funds are now available. // that pubkey's funds are now available.
bank.apply_timestamp(mint.pubkey(), dt).unwrap(); 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); assert_eq!(bank.get_balance(&pubkey), 1);
// tx count is still 1, because we chose not to count timestamp transactions // tx count is 2
// tx count. assert_eq!(bank.transaction_count(), 2);
assert_eq!(bank.transaction_count(), 1);
bank.apply_timestamp(mint.pubkey(), dt).unwrap(); // <-- Attack! Attempt to process completed transaction. // try to replay the timestamp contract
assert_ne!(bank.get_balance(&pubkey), 2); 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] #[test]
fn test_cancel_transfer() { fn test_cancel_transfer() {
let mint = Mint::new(1); // mint needs to have a balance to modify the external contract
let mint = Mint::new(2);
let bank = Bank::new(&mint); let bank = Bank::new(&mint);
let pubkey = Keypair::new().pubkey(); let pubkey = Keypair::new().pubkey();
let dt = Utc::now(); let dt = Utc::now();
@ -802,23 +912,31 @@ mod tests {
// Assert the debit counts as a transaction. // Assert the debit counts as a transaction.
assert_eq!(bank.transaction_count(), 1); assert_eq!(bank.transaction_count(), 1);
// Mint's balance will be zero because all funds are locked up. // Mint's balance will be 1 because 1 of the tokens is locked up.
assert_eq!(bank.get_balance(&mint.pubkey()), 0); assert_eq!(bank.get_balance(&mint.pubkey()), 1);
// pubkey's balance will be None because the funds have not been // pubkey's balance will be 0 because the funds are locked up
// sent.
assert_eq!(bank.get_balance(&pubkey), 0); assert_eq!(bank.get_balance(&pubkey), 0);
// Now, cancel the trancaction. Mint gets her funds back, pubkey never sees them. // Now, cancel the trancaction. Mint gets her funds back, pubkey never sees them.
bank.apply_signature(mint.pubkey(), signature).unwrap(); let tx = Transaction::new_signature(&mint.keypair(), pubkey, signature, bank.last_id());
assert_eq!(bank.get_balance(&mint.pubkey()), 1); let res = bank.process_transaction(&tx);
assert!(res.is_ok());
assert_eq!(bank.get_balance(&pubkey), 0); assert_eq!(bank.get_balance(&pubkey), 0);
assert_eq!(bank.get_balance(&mint.pubkey()), 2);
// Assert cancel doesn't cause count to go backward. // Assert cancel counts as a tx
assert_eq!(bank.transaction_count(), 1); assert_eq!(bank.transaction_count(), 2);
bank.apply_signature(mint.pubkey(), signature).unwrap(); // <-- Attack! Attempt to cancel completed transaction. // try to replay the signature contract
assert_ne!(bank.get_balance(&mint.pubkey()), 2); 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] #[test]
@ -837,13 +955,13 @@ mod tests {
} }
#[test] #[test]
fn test_forget_signature() { fn test_clear_signatures() {
let mint = Mint::new(1); let mint = Mint::new(1);
let bank = Bank::new(&mint); let bank = Bank::new(&mint);
let signature = Signature::default(); let signature = Signature::default();
bank.reserve_signature_with_last_id(&signature, &mint.last_id()) bank.reserve_signature_with_last_id(&signature, &mint.last_id())
.unwrap(); .unwrap();
bank.forget_signature_with_last_id(&signature, &mint.last_id()); bank.clear_signatures();
assert!( assert!(
bank.reserve_signature_with_last_id(&signature, &mint.last_id()) bank.reserve_signature_with_last_id(&signature, &mint.last_id())
.is_ok() .is_ok()
@ -1128,5 +1246,4 @@ mod tests {
def_bank.set_finality(90); def_bank.set_finality(90);
assert_eq!(def_bank.finality(), 90); assert_eq!(def_bank.finality(), 90);
} }
} }

View File

@ -248,8 +248,8 @@ mod tests {
// First, verify entries // First, verify entries
let keypair = Keypair::new(); let keypair = Keypair::new();
let tx0 = Transaction::new_timestamp(&keypair, Utc::now(), zero); let tx0 = Transaction::new_timestamp(&keypair, keypair.pubkey(), Utc::now(), zero);
let tx1 = Transaction::new_signature(&keypair, Default::default(), zero); let tx1 = Transaction::new_signature(&keypair, keypair.pubkey(), Default::default(), zero);
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()], false); let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()], false);
assert!(e0.verify(&zero)); assert!(e0.verify(&zero));
@ -271,7 +271,7 @@ mod tests {
assert_eq!(tick.id, zero); assert_eq!(tick.id, zero);
let keypair = Keypair::new(); let keypair = Keypair::new();
let tx0 = Transaction::new_timestamp(&keypair, Utc::now(), zero); let tx0 = Transaction::new_timestamp(&keypair, keypair.pubkey(), Utc::now(), zero);
let entry0 = next_entry(&zero, 1, vec![tx0.clone()]); let entry0 = next_entry(&zero, 1, vec![tx0.clone()]);
assert_eq!(entry0.num_hashes, 1); assert_eq!(entry0.num_hashes, 1);
assert_eq!(entry0.id, next_hash(&zero, 1, &vec![tx0])); assert_eq!(entry0.id, next_hash(&zero, 1, &vec![tx0]));

View File

@ -101,9 +101,11 @@ pub fn read_entries<R: BufRead>(reader: R) -> impl Iterator<Item = io::Result<En
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use bincode::serialize;
use ledger; use ledger;
use mint::Mint; use mint::Mint;
use packet::BLOB_DATA_SIZE; use packet::BLOB_DATA_SIZE;
use packet::PACKET_DATA_SIZE;
use signature::{Keypair, KeypairUtil}; use signature::{Keypair, KeypairUtil};
use std::io::Cursor; use std::io::Cursor;
use transaction::Transaction; use transaction::Transaction;
@ -117,9 +119,11 @@ mod tests {
let mut entry_writer = EntryWriter::new(&bank, writer); let mut entry_writer = EntryWriter::new(&bank, writer);
let keypair = Keypair::new(); let keypair = Keypair::new();
let tx = Transaction::new(&mint.keypair(), keypair.pubkey(), 1, mint.last_id()); let tx = Transaction::new(&mint.keypair(), keypair.pubkey(), 1, mint.last_id());
let tx_size = serialize(&tx).unwrap().len();
// NOTE: if Entry grows to larger than a transaction, the code below falls over assert!(tx_size <= PACKET_DATA_SIZE);
let threshold = (BLOB_DATA_SIZE / 256) - 1; // 256 is transaction size assert!(BLOB_DATA_SIZE >= PACKET_DATA_SIZE);
let threshold = (BLOB_DATA_SIZE / tx_size) - 1; // PACKET_DATA_SIZE is transaction size
// Verify large entries are split up and the first sets has_more. // Verify large entries are split up and the first sets has_more.
let txs = vec![tx.clone(); threshold * 2]; let txs = vec![tx.clone(); threshold * 2];

View File

@ -590,7 +590,12 @@ mod tests {
Entry::new_mut( Entry::new_mut(
&mut id, &mut id,
&mut num_hashes, &mut num_hashes,
vec![Transaction::new_timestamp(&keypair, Utc::now(), one)], vec![Transaction::new_timestamp(
&keypair,
keypair.pubkey(),
Utc::now(),
one,
)],
false, false,
) )
}) })
@ -610,7 +615,7 @@ mod tests {
one, one,
1, 1,
); );
let tx1 = Transaction::new_timestamp(&keypair, Utc::now(), one); let tx1 = Transaction::new_timestamp(&keypair, keypair.pubkey(), Utc::now(), one);
// //
// TODO: this magic number and the mix of transaction types // TODO: this magic number and the mix of transaction types
// is designed to fill up a Blob more or less exactly, // is designed to fill up a Blob more or less exactly,

View File

@ -74,9 +74,9 @@ mod tests {
fn test_create_transactions() { fn test_create_transactions() {
let mut transactions = Mint::new(100).create_transactions().into_iter(); let mut transactions = Mint::new(100).create_transactions().into_iter();
let tx = transactions.next().unwrap(); let tx = transactions.next().unwrap();
if let Instruction::NewContract(contract) = tx.instruction { if let Instruction::NewContract(contract) = tx.instruction() {
if let Plan::Budget(Budget::Pay(payment)) = contract.plan { if let Plan::Budget(Budget::Pay(payment)) = contract.plan {
assert_eq!(tx.from, payment.to); assert_eq!(*tx.from(), payment.to);
} }
} }
assert_eq!(transactions.next(), None); assert_eq!(transactions.next(), None);

View File

@ -503,7 +503,7 @@ impl Blob {
mod tests { mod tests {
use packet::{ use packet::{
to_packets, Blob, BlobRecycler, Meta, Packet, PacketRecycler, Packets, Recycler, Reset, to_packets, Blob, BlobRecycler, Meta, Packet, PacketRecycler, Packets, Recycler, Reset,
BLOB_HEADER_SIZE, NUM_PACKETS, BLOB_HEADER_SIZE, NUM_PACKETS, PACKET_DATA_SIZE,
}; };
use request::Request; use request::Request;
use std::io; use std::io;
@ -577,12 +577,12 @@ mod tests {
p.write().unwrap().packets.resize(10, Packet::default()); p.write().unwrap().packets.resize(10, Packet::default());
for m in p.write().unwrap().packets.iter_mut() { for m in p.write().unwrap().packets.iter_mut() {
m.meta.set_addr(&addr); m.meta.set_addr(&addr);
m.meta.size = 256; m.meta.size = PACKET_DATA_SIZE;
} }
p.read().unwrap().send_to(&sender).unwrap(); p.read().unwrap().send_to(&sender).unwrap();
p.write().unwrap().recv_from(&reader).unwrap(); p.write().unwrap().recv_from(&reader).unwrap();
for m in p.write().unwrap().packets.iter_mut() { for m in p.write().unwrap().packets.iter_mut() {
assert_eq!(m.meta.size, 256); assert_eq!(m.meta.size, PACKET_DATA_SIZE);
assert_eq!(m.meta.addr(), saddr); assert_eq!(m.meta.addr(), saddr);
} }

View File

@ -105,7 +105,7 @@ pub fn ed25519_verify_cpu(batches: &[SharedPackets]) -> Vec<Vec<u8>> {
pub fn ed25519_verify_disabled(batches: &[SharedPackets]) -> Vec<Vec<u8>> { pub fn ed25519_verify_disabled(batches: &[SharedPackets]) -> Vec<Vec<u8>> {
use rayon::prelude::*; use rayon::prelude::*;
let count = batch_size(batches); let count = batch_size(batches);
info!("CPU ECDSA for {}", batch_size(batches)); info!("disabled ECDSA for {}", batch_size(batches));
let rv = batches let rv = batches
.into_par_iter() .into_par_iter()
.map(|p| { .map(|p| {

View File

@ -3,7 +3,7 @@
//! messages to the network directly. The binary encoding of its messages are //! messages to the network directly. The binary encoding of its messages are
//! unstable and may change in future releases. //! unstable and may change in future releases.
use bank::Account; use bank::{Account, Bank};
use bincode::{deserialize, serialize}; use bincode::{deserialize, serialize};
use crdt::{Crdt, CrdtError, NodeInfo}; use crdt::{Crdt, CrdtError, NodeInfo};
use hash::Hash; use hash::Hash;
@ -177,9 +177,12 @@ impl ThinClient {
} }
self.process_response(&resp); self.process_response(&resp);
} }
trace!("get_balance {:?}", self.balances.get(pubkey));
//TODO: call the contract specific get_balance for contract_id's thin client can introspect
//instead of hard coding to budget_dsl only
self.balances self.balances
.get(pubkey) .get(pubkey)
.map(|a| a.tokens) .map(Bank::get_balance_of_budget_payment_plan)
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "nokey")) .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "nokey"))
} }
@ -508,10 +511,12 @@ mod tests {
let last_id = client.get_last_id(); let last_id = client.get_last_id();
let mut tr2 = Transaction::new(&alice.keypair(), bob_pubkey, 501, last_id); let mut tr2 = Transaction::new(&alice.keypair(), bob_pubkey, 501, last_id);
if let Instruction::NewContract(contract) = &mut tr2.instruction { let mut instruction2 = tr2.instruction();
if let Instruction::NewContract(ref mut contract) = instruction2 {
contract.tokens = 502; contract.tokens = 502;
contract.plan = Plan::Budget(Budget::new_payment(502, bob_pubkey)); contract.plan = Plan::Budget(Budget::new_payment(502, bob_pubkey));
} }
tr2.userdata = serialize(&instruction2).unwrap();
let signature = client.transfer_signed(&tr2).unwrap(); let signature = client.transfer_signed(&tr2).unwrap();
client.poll_for_signature(&signature).unwrap(); client.poll_for_signature(&signature).unwrap();

View File

@ -1,6 +1,6 @@
//! The `transaction` module provides functionality for creating log transactions. //! The `transaction` module provides functionality for creating log transactions.
use bincode::serialize; use bincode::{deserialize, serialize};
use budget::{Budget, Condition}; use budget::{Budget, Condition};
use chrono::prelude::*; use chrono::prelude::*;
use hash::Hash; use hash::Hash;
@ -8,9 +8,9 @@ use payment_plan::{Payment, PaymentPlan, Witness};
use signature::{Keypair, KeypairUtil, Pubkey, Signature}; use signature::{Keypair, KeypairUtil, Pubkey, Signature};
use std::mem::size_of; use std::mem::size_of;
pub const SIGNED_DATA_OFFSET: usize = PUB_KEY_OFFSET + size_of::<Pubkey>(); pub const SIGNED_DATA_OFFSET: usize = size_of::<Signature>();
pub const SIG_OFFSET: usize = 0; pub const SIG_OFFSET: usize = 0;
pub const PUB_KEY_OFFSET: usize = size_of::<Signature>(); pub const PUB_KEY_OFFSET: usize = size_of::<Signature>() + size_of::<u64>();
/// The type of payment plan. Each item must implement the PaymentPlan trait. /// The type of payment plan. Each item must implement the PaymentPlan trait.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
@ -82,38 +82,45 @@ pub struct Transaction {
/// A digital signature of `instruction`, `last_id` and `fee`, signed by `Pubkey`. /// A digital signature of `instruction`, `last_id` and `fee`, signed by `Pubkey`.
pub signature: Signature, pub signature: Signature,
/// The `Pubkey` of the entity that signed the transaction data. /// The `Pubkeys` that are executing this transaction userdata. The meaning of each key is
pub from: Pubkey, /// contract-specific.
/// * keys[0] - Typically this is the `from` public key. `signature` is verified with keys[0].
/// The action the server should take. /// In the future which key pays the fee and which keys have signatures would be configurable.
pub instruction: Instruction, /// * keys[1] - Typically this is the contract context or the recepient of the tokens
pub keys: Vec<Pubkey>,
/// The ID of a recent ledger entry. /// The ID of a recent ledger entry.
pub last_id: Hash, pub last_id: Hash,
/// The number of tokens paid for processing and storage of this transaction. /// The number of tokens paid for processing and storage of this transaction.
pub fee: i64, pub fee: i64,
/// Optional user data to be stored in the account
/// TODO: This will be a required field for all contract operations including a simple spend. /// Userdata to be stored in the account
/// `instruction` will be serialized into `userdata` once Budget is its own generic contract.
pub userdata: Vec<u8>, pub userdata: Vec<u8>,
} }
impl Transaction { impl Transaction {
/// Create a signed transaction with userdata and an instruction /// Create a signed transaction from the given `Instruction`.
fn new_with_userdata_and_instruction( /// * `from_keypair` - The key used to sign the transcation. This key is stored as keys[0]
/// * `transaction_keys` - The keys for the transaction. These are the contract state
/// instances or token recepient keys.
/// * `userdata` - The input data that the contract will execute with
/// * `last_id` - The PoH hash.
/// * `fee` - The transaction fee.
fn new_with_userdata(
from_keypair: &Keypair, from_keypair: &Keypair,
instruction: Instruction, transaction_keys: &[Pubkey],
userdata: Vec<u8>,
last_id: Hash, last_id: Hash,
fee: i64, fee: i64,
userdata: Vec<u8>,
) -> Self { ) -> Self {
let from = from_keypair.pubkey(); let from = from_keypair.pubkey();
let mut keys = vec![from];
keys.extend_from_slice(transaction_keys);
let mut tx = Transaction { let mut tx = Transaction {
signature: Signature::default(), signature: Signature::default(),
instruction, keys,
last_id, last_id,
from,
fee, fee,
userdata, userdata,
}; };
@ -123,29 +130,31 @@ impl Transaction {
/// Create a signed transaction from the given `Instruction`. /// Create a signed transaction from the given `Instruction`.
fn new_from_instruction( fn new_from_instruction(
from_keypair: &Keypair, from_keypair: &Keypair,
contract: Pubkey,
instruction: Instruction, instruction: Instruction,
last_id: Hash, last_id: Hash,
fee: i64, fee: i64,
) -> Self { ) -> Self {
Self::new_with_userdata_and_instruction(from_keypair, instruction, last_id, fee, vec![]) let userdata = serialize(&instruction).expect("serealize instruction");
Self::new_with_userdata(from_keypair, &[contract], userdata, last_id, fee)
} }
/// Create and sign a new Transaction. Used for unit-testing. /// Create and sign a new Transaction. Used for unit-testing.
pub fn new_taxed( pub fn new_taxed(
from_keypair: &Keypair, from_keypair: &Keypair,
to: Pubkey, contract: Pubkey,
tokens: i64, tokens: i64,
fee: i64, fee: i64,
last_id: Hash, last_id: Hash,
) -> Self { ) -> Self {
let payment = Payment { let payment = Payment {
tokens: tokens - fee, tokens: tokens - fee,
to, to: contract,
}; };
let budget = Budget::Pay(payment); let budget = Budget::Pay(payment);
let plan = Plan::Budget(budget); let plan = Plan::Budget(budget);
let instruction = Instruction::NewContract(Contract { plan, tokens }); let instruction = Instruction::NewContract(Contract { plan, tokens });
Self::new_from_instruction(from_keypair, instruction, last_id, fee) Self::new_from_instruction(from_keypair, contract, instruction, last_id, fee)
} }
/// Create and sign a new Transaction. Used for unit-testing. /// Create and sign a new Transaction. Used for unit-testing.
@ -154,19 +163,31 @@ impl Transaction {
} }
/// Create and sign a new Witness Timestamp. Used for unit-testing. /// Create and sign a new Witness Timestamp. Used for unit-testing.
pub fn new_timestamp(from_keypair: &Keypair, dt: DateTime<Utc>, last_id: Hash) -> Self { pub fn new_timestamp(
from_keypair: &Keypair,
contract: Pubkey,
dt: DateTime<Utc>,
last_id: Hash,
) -> Self {
let instruction = Instruction::ApplyTimestamp(dt); let instruction = Instruction::ApplyTimestamp(dt);
Self::new_from_instruction(from_keypair, instruction, last_id, 0) Self::new_from_instruction(from_keypair, contract, instruction, last_id, 0)
} }
/// Create and sign a new Witness Signature. Used for unit-testing. /// Create and sign a new Witness Signature. Used for unit-testing.
pub fn new_signature(from_keypair: &Keypair, signature: Signature, last_id: Hash) -> Self { pub fn new_signature(
from_keypair: &Keypair,
contract: Pubkey,
signature: Signature,
last_id: Hash,
) -> Self {
let instruction = Instruction::ApplySignature(signature); let instruction = Instruction::ApplySignature(signature);
Self::new_from_instruction(from_keypair, instruction, last_id, 0) Self::new_from_instruction(from_keypair, contract, instruction, last_id, 0)
} }
pub fn new_vote(from_keypair: &Keypair, vote: Vote, last_id: Hash, fee: i64) -> Self { pub fn new_vote(from_keypair: &Keypair, vote: Vote, last_id: Hash, fee: i64) -> Self {
Transaction::new_from_instruction(&from_keypair, Instruction::NewVote(vote), last_id, fee) let instruction = Instruction::NewVote(vote);
let userdata = serialize(&instruction).expect("serealize instruction");
Self::new_with_userdata(from_keypair, &[], userdata, last_id, fee)
} }
/// Create and sign a postdated Transaction. Used for unit-testing. /// Create and sign a postdated Transaction. Used for unit-testing.
@ -184,12 +205,14 @@ impl Transaction {
); );
let plan = Plan::Budget(budget); let plan = Plan::Budget(budget);
let instruction = Instruction::NewContract(Contract { plan, tokens }); let instruction = Instruction::NewContract(Contract { plan, tokens });
Self::new_from_instruction(from_keypair, instruction, last_id, 0) let userdata = serialize(&instruction).expect("serealize instruction");
Self::new_with_userdata(from_keypair, &[to], userdata, last_id, 0)
} }
/// Get the transaction data to sign. /// Get the transaction data to sign.
fn get_sign_data(&self) -> Vec<u8> { fn get_sign_data(&self) -> Vec<u8> {
let mut data = serialize(&(&self.instruction)).expect("serialize Contract"); let mut data = serialize(&(&self.keys)).expect("serialize keys");
let last_id_data = serialize(&(&self.last_id)).expect("serialize last_id"); let last_id_data = serialize(&(&self.last_id)).expect("serialize last_id");
data.extend_from_slice(&last_id_data); data.extend_from_slice(&last_id_data);
@ -198,7 +221,6 @@ impl Transaction {
let userdata = serialize(&(&self.userdata)).expect("serialize userdata"); let userdata = serialize(&(&self.userdata)).expect("serialize userdata");
data.extend_from_slice(&userdata); data.extend_from_slice(&userdata);
data data
} }
@ -212,12 +234,13 @@ impl Transaction {
pub fn verify_signature(&self) -> bool { pub fn verify_signature(&self) -> bool {
warn!("transaction signature verification called"); warn!("transaction signature verification called");
self.signature self.signature
.verify(&self.from.as_ref(), &self.get_sign_data()) .verify(&self.from().as_ref(), &self.get_sign_data())
} }
/// Verify only the payment plan. /// Verify only the payment plan.
pub fn verify_plan(&self) -> bool { pub fn verify_plan(&self) -> bool {
if let Instruction::NewContract(contract) = &self.instruction { let instruction = deserialize(&self.userdata);
if let Ok(Instruction::NewContract(contract)) = instruction {
self.fee >= 0 self.fee >= 0
&& self.fee <= contract.tokens && self.fee <= contract.tokens
&& contract.plan.verify(contract.tokens - self.fee) && contract.plan.verify(contract.tokens - self.fee)
@ -225,14 +248,19 @@ impl Transaction {
true true
} }
} }
pub fn vote(&self) -> Option<(Pubkey, Vote, Hash)> { pub fn vote(&self) -> Option<(Pubkey, Vote, Hash)> {
if let Instruction::NewVote(ref vote) = self.instruction { if let Instruction::NewVote(vote) = self.instruction() {
Some((self.from, vote.clone(), self.last_id)) Some((*self.from(), vote, self.last_id))
} else { } else {
None None
} }
} }
pub fn from(&self) -> &Pubkey {
&self.keys[0]
}
pub fn instruction(&self) -> Instruction {
deserialize(&self.userdata).unwrap()
}
} }
pub fn test_tx() -> Transaction { pub fn test_tx() -> Transaction {
@ -258,6 +286,7 @@ pub fn memfind<A: Eq>(a: &[A], b: &[A]) -> Option<usize> {
mod tests { mod tests {
use super::*; use super::*;
use bincode::{deserialize, serialize}; use bincode::{deserialize, serialize};
use packet::PACKET_DATA_SIZE;
#[test] #[test]
fn test_claim() { fn test_claim() {
@ -295,13 +324,13 @@ mod tests {
}); });
let plan = Plan::Budget(budget); let plan = Plan::Budget(budget);
let instruction = Instruction::NewContract(Contract { plan, tokens: 0 }); let instruction = Instruction::NewContract(Contract { plan, tokens: 0 });
let userdata = serialize(&instruction).unwrap();
let claim0 = Transaction { let claim0 = Transaction {
instruction, keys: vec![],
from: Default::default(),
last_id: Default::default(), last_id: Default::default(),
signature: Default::default(), signature: Default::default(),
fee: 0, fee: 0,
userdata: vec![], userdata,
}; };
let buf = serialize(&claim0).unwrap(); let buf = serialize(&claim0).unwrap();
let claim1: Transaction = deserialize(&buf).unwrap(); let claim1: Transaction = deserialize(&buf).unwrap();
@ -314,12 +343,14 @@ mod tests {
let keypair = Keypair::new(); let keypair = Keypair::new();
let pubkey = keypair.pubkey(); let pubkey = keypair.pubkey();
let mut tx = Transaction::new(&keypair, pubkey, 42, zero); let mut tx = Transaction::new(&keypair, pubkey, 42, zero);
if let Instruction::NewContract(contract) = &mut tx.instruction { let mut instruction = tx.instruction();
if let Instruction::NewContract(ref mut contract) = instruction {
contract.tokens = 1_000_000; // <-- attack, part 1! contract.tokens = 1_000_000; // <-- attack, part 1!
if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan { if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan {
payment.tokens = contract.tokens; // <-- attack, part 2! payment.tokens = contract.tokens; // <-- attack, part 2!
} }
} }
tx.userdata = serialize(&instruction).unwrap();
assert!(tx.verify_plan()); assert!(tx.verify_plan());
assert!(!tx.verify_signature()); assert!(!tx.verify_signature());
} }
@ -332,11 +363,13 @@ mod tests {
let pubkey1 = keypair1.pubkey(); let pubkey1 = keypair1.pubkey();
let zero = Hash::default(); let zero = Hash::default();
let mut tx = Transaction::new(&keypair0, pubkey1, 42, zero); let mut tx = Transaction::new(&keypair0, pubkey1, 42, zero);
if let Instruction::NewContract(contract) = &mut tx.instruction { let mut instruction = tx.instruction();
if let Instruction::NewContract(ref mut contract) = instruction {
if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan { if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan {
payment.to = thief_keypair.pubkey(); // <-- attack! payment.to = thief_keypair.pubkey(); // <-- attack!
} }
} }
tx.userdata = serialize(&instruction).unwrap();
assert!(tx.verify_plan()); assert!(tx.verify_plan());
assert!(!tx.verify_signature()); assert!(!tx.verify_signature());
} }
@ -345,9 +378,13 @@ mod tests {
let tx = test_tx(); let tx = test_tx();
let sign_data = tx.get_sign_data(); let sign_data = tx.get_sign_data();
let tx_bytes = serialize(&tx).unwrap(); let tx_bytes = serialize(&tx).unwrap();
assert_matches!(memfind(&tx_bytes, &sign_data), Some(SIGNED_DATA_OFFSET)); assert_eq!(memfind(&tx_bytes, &sign_data), Some(SIGNED_DATA_OFFSET));
assert_matches!(memfind(&tx_bytes, &tx.signature.as_ref()), Some(SIG_OFFSET)); assert_eq!(memfind(&tx_bytes, &tx.signature.as_ref()), Some(SIG_OFFSET));
assert_matches!(memfind(&tx_bytes, &tx.from.as_ref()), Some(PUB_KEY_OFFSET)); assert_eq!(
memfind(&tx_bytes, &tx.from().as_ref()),
Some(PUB_KEY_OFFSET)
);
assert!(tx.verify_signature());
} }
#[test] #[test]
fn test_userdata_layout() { fn test_userdata_layout() {
@ -355,13 +392,16 @@ mod tests {
tx0.userdata = vec![1, 2, 3]; tx0.userdata = vec![1, 2, 3];
let sign_data0a = tx0.get_sign_data(); let sign_data0a = tx0.get_sign_data();
let tx_bytes = serialize(&tx0).unwrap(); let tx_bytes = serialize(&tx0).unwrap();
assert!(tx_bytes.len() < 256); assert!(tx_bytes.len() < PACKET_DATA_SIZE);
assert_eq!(memfind(&tx_bytes, &sign_data0a), Some(SIGNED_DATA_OFFSET)); assert_eq!(memfind(&tx_bytes, &sign_data0a), Some(SIGNED_DATA_OFFSET));
assert_eq!( assert_eq!(
memfind(&tx_bytes, &tx0.signature.as_ref()), memfind(&tx_bytes, &tx0.signature.as_ref()),
Some(SIG_OFFSET) Some(SIG_OFFSET)
); );
assert_eq!(memfind(&tx_bytes, &tx0.from.as_ref()), Some(PUB_KEY_OFFSET)); assert_eq!(
memfind(&tx_bytes, &tx0.from().as_ref()),
Some(PUB_KEY_OFFSET)
);
let tx1 = deserialize(&tx_bytes).unwrap(); let tx1 = deserialize(&tx_bytes).unwrap();
assert_eq!(tx0, tx1); assert_eq!(tx0, tx1);
assert_eq!(tx1.userdata, vec![1, 2, 3]); assert_eq!(tx1.userdata, vec![1, 2, 3]);
@ -377,19 +417,23 @@ mod tests {
let keypair1 = Keypair::new(); let keypair1 = Keypair::new();
let zero = Hash::default(); let zero = Hash::default();
let mut tx = Transaction::new(&keypair0, keypair1.pubkey(), 1, zero); let mut tx = Transaction::new(&keypair0, keypair1.pubkey(), 1, zero);
if let Instruction::NewContract(contract) = &mut tx.instruction { let mut instruction = tx.instruction();
if let Instruction::NewContract(ref mut contract) = instruction {
if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan { if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan {
payment.tokens = 2; // <-- attack! payment.tokens = 2; // <-- attack!
} }
} }
tx.userdata = serialize(&instruction).unwrap();
assert!(!tx.verify_plan()); assert!(!tx.verify_plan());
// Also, ensure all branchs of the plan spend all tokens // Also, ensure all branchs of the plan spend all tokens
if let Instruction::NewContract(contract) = &mut tx.instruction { let mut instruction = tx.instruction();
if let Instruction::NewContract(ref mut contract) = instruction {
if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan { if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan {
payment.tokens = 0; // <-- whoops! payment.tokens = 0; // <-- whoops!
} }
} }
tx.userdata = serialize(&instruction).unwrap();
assert!(!tx.verify_plan()); assert!(!tx.verify_plan());
} }
} }

View File

@ -522,7 +522,7 @@ fn test_multi_node_dynamic_network() {
Ok(val) => val Ok(val) => val
.parse() .parse()
.expect(&format!("env var {} is not parse-able as usize", key)), .expect(&format!("env var {} is not parse-able as usize", key)),
Err(_) => 160, Err(_) => 120,
}; };
let leader_keypair = Keypair::new(); let leader_keypair = Keypair::new();