//! budget program use bincode::{self, deserialize, serialize_into, serialized_size}; use chrono::prelude::{DateTime, Utc}; use log::*; use serde_derive::{Deserialize, Serialize}; use solana_sdk::account::KeyedAccount; use solana_sdk::budget_expr::BudgetExpr; use solana_sdk::budget_instruction::Instruction; use solana_sdk::payment_plan::Witness; use std::io; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum BudgetError { InsufficientFunds, ContractAlreadyExists, ContractNotPending, SourceIsPendingContract, UninitializedContract, NegativeTokens, DestinationMissing, FailedWitness, UserdataTooSmall, UserdataDeserializeFailure, UnsignedKey, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct BudgetProgram { pub initialized: bool, pub pending_budget: Option, } fn apply_debits( keyed_accounts: &mut [KeyedAccount], instruction: &Instruction, ) -> Result<(), BudgetError> { if !keyed_accounts[0].account.userdata.is_empty() { trace!("source is pending"); return Err(BudgetError::SourceIsPendingContract); } match instruction { Instruction::NewBudget(expr) => { let expr = expr.clone(); if let Some(payment) = expr.final_payment() { keyed_accounts[1].account.tokens += payment.tokens; Ok(()) } else { let existing = BudgetProgram::deserialize(&keyed_accounts[1].account.userdata).ok(); if Some(true) == existing.map(|x| x.initialized) { trace!("contract already exists"); Err(BudgetError::ContractAlreadyExists) } else { let mut program = BudgetProgram::default(); program.pending_budget = Some(expr); keyed_accounts[1].account.tokens += keyed_accounts[0].account.tokens; keyed_accounts[0].account.tokens = 0; program.initialized = true; program.serialize(&mut keyed_accounts[1].account.userdata) } } } Instruction::ApplyTimestamp(dt) => { if let Ok(mut program) = BudgetProgram::deserialize(&keyed_accounts[1].account.userdata) { if !program.is_pending() { Err(BudgetError::ContractNotPending) } else if !program.initialized { trace!("contract is uninitialized"); Err(BudgetError::UninitializedContract) } else { trace!("apply timestamp"); program.apply_timestamp(keyed_accounts, *dt)?; trace!("apply timestamp committed"); program.serialize(&mut keyed_accounts[1].account.userdata) } } else { Err(BudgetError::UninitializedContract) } } Instruction::ApplySignature => { if let Ok(mut program) = BudgetProgram::deserialize(&keyed_accounts[1].account.userdata) { if !program.is_pending() { Err(BudgetError::ContractNotPending) } else if !program.initialized { trace!("contract is uninitialized"); Err(BudgetError::UninitializedContract) } else { trace!("apply signature"); program.apply_signature(keyed_accounts)?; trace!("apply signature committed"); program.serialize(&mut keyed_accounts[1].account.userdata) } } else { Err(BudgetError::UninitializedContract) } } } } /// Budget DSL contract interface /// * 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_instruction( keyed_accounts: &mut [KeyedAccount], data: &[u8], ) -> Result<(), BudgetError> { if let Ok(instruction) = deserialize(data) { trace!("process_instruction: {:?}", instruction); apply_debits(keyed_accounts, &instruction) } else { info!("Invalid transaction userdata: {:?}", data); Err(BudgetError::UserdataDeserializeFailure) } } // TODO: Re-instate budget_program special case in bank.rs? /* pub fn get_balance(account: &Account) -> u64 { if let Ok(program) = deserialize(&account.userdata) { let program: BudgetProgram = program; if program.is_pending() { 0 } else { account.tokens } } else { account.tokens } } */ impl BudgetProgram { fn is_pending(&self) -> bool { self.pending_budget != None } /// Process a Witness Signature. Any payment plans waiting on this signature /// will progress one step. fn apply_signature(&mut self, keyed_accounts: &mut [KeyedAccount]) -> Result<(), BudgetError> { let mut final_payment = None; if let Some(ref mut expr) = self.pending_budget { let key = match keyed_accounts[0].signer_key() { None => return Err(BudgetError::UnsignedKey), Some(key) => key, }; expr.apply_witness(&Witness::Signature, key); final_payment = expr.final_payment(); } if let Some(payment) = final_payment { if let Some(key) = keyed_accounts[0].signer_key() { if &payment.to == key { self.pending_budget = None; keyed_accounts[1].account.tokens -= payment.tokens; keyed_accounts[0].account.tokens += payment.tokens; return Ok(()); } } if &payment.to != keyed_accounts[2].unsigned_key() { trace!("destination missing"); return Err(BudgetError::DestinationMissing); } self.pending_budget = None; keyed_accounts[1].account.tokens -= payment.tokens; keyed_accounts[2].account.tokens += payment.tokens; } Ok(()) } /// Process a Witness Timestamp. Any payment plans waiting on this timestamp /// will progress one step. fn apply_timestamp( &mut self, keyed_accounts: &mut [KeyedAccount], dt: DateTime, ) -> Result<(), BudgetError> { // Check to see if any timelocked transactions can be completed. let mut final_payment = None; if let Some(ref mut expr) = self.pending_budget { let key = match keyed_accounts[0].signer_key() { None => return Err(BudgetError::UnsignedKey), Some(key) => key, }; expr.apply_witness(&Witness::Timestamp(dt), key); final_payment = expr.final_payment(); } if let Some(payment) = final_payment { if &payment.to != keyed_accounts[2].unsigned_key() { trace!("destination missing"); return Err(BudgetError::DestinationMissing); } self.pending_budget = None; keyed_accounts[1].account.tokens -= payment.tokens; keyed_accounts[2].account.tokens += payment.tokens; } Ok(()) } fn serialize(&self, output: &mut [u8]) -> Result<(), BudgetError> { let len = serialized_size(self).unwrap() as u64; if output.len() < len as usize { warn!( "{} bytes required to serialize, only have {} bytes", len, output.len() ); return Err(BudgetError::UserdataTooSmall); } { 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(); } Ok(()) } 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 < 2 { 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]) } } #[cfg(test)] mod test { use super::*; use bincode::serialize; use solana_sdk::account::Account; use solana_sdk::budget_program::*; use solana_sdk::budget_transaction::BudgetTransaction; use solana_sdk::hash::Hash; use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::transaction::{Instruction, Transaction}; fn process_transaction( tx: &Transaction, program_accounts: &mut [Account], ) -> Result<(), BudgetError> { assert_eq!(tx.instructions.len(), 1); let Instruction { ref accounts, ref userdata, .. } = tx.instructions[0]; let mut keyed_accounts: Vec<_> = accounts .iter() .map(|&index| { let index = index as usize; let key = &tx.account_keys[index]; (key, index < tx.signatures.len()) }) .zip(program_accounts.iter_mut()) .map(|((key, is_signer), account)| KeyedAccount::new(key, is_signer, account)) .collect(); super::process_instruction(&mut keyed_accounts, &userdata) } #[test] fn test_serializer() { let mut a = Account::new(0, 512, id()); let b = BudgetProgram::default(); b.serialize(&mut a.userdata).unwrap(); let buf = serialize(&b).unwrap(); assert_eq!(a.userdata[8..8 + buf.len()], buf[0..]); let c = BudgetProgram::deserialize(&a.userdata).unwrap(); assert_eq!(b, c); } #[test] fn test_serializer_userdata_too_small() { let mut a = Account::new(0, 1, id()); let b = BudgetProgram::default(); assert_eq!( b.serialize(&mut a.userdata), Err(BudgetError::UserdataTooSmall) ); } #[test] fn test_invalid_instruction() { let mut accounts = vec![Account::new(1, 0, id()), Account::new(0, 512, id())]; let from = Keypair::new(); let contract = Keypair::new(); let userdata = (1u8, 2u8, 3u8); let tx = Transaction::new( &from, &[contract.pubkey()], id(), &userdata, Hash::default(), 0, ); assert!(process_transaction(&tx, &mut accounts).is_err()); } #[test] fn test_unsigned_witness_key() { let mut accounts = vec![ Account::new(1, 0, id()), Account::new(0, 512, id()), Account::new(0, 0, id()), ]; // Initialize BudgetProgram let from = Keypair::new(); let contract = Keypair::new().pubkey(); let to = Keypair::new().pubkey(); let witness = Keypair::new().pubkey(); let tx = BudgetTransaction::new_when_signed( &from, to, contract, witness, None, 1, Hash::default(), ); process_transaction(&tx, &mut accounts).unwrap(); // Attack! Part 1: Sign a witness transaction with a random key. let rando = Keypair::new(); let mut tx = BudgetTransaction::new_signature(&rando, contract, to, Hash::default()); // Attack! Part 2: Point the instruction to the expected, but unsigned, key. tx.account_keys.push(from.pubkey()); tx.instructions[0].accounts[0] = 3; // Ensure the transaction fails because of the unsigned key. assert_eq!( process_transaction(&tx, &mut accounts), Err(BudgetError::UnsignedKey) ); } #[test] fn test_unsigned_timestamp() { let mut accounts = vec![ Account::new(1, 0, id()), Account::new(0, 512, id()), Account::new(0, 0, id()), ]; // Initialize BudgetProgram let from = Keypair::new(); let contract = Keypair::new().pubkey(); let to = Keypair::new().pubkey(); let dt = Utc::now(); let tx = BudgetTransaction::new_on_date( &from, to, contract, dt, from.pubkey(), None, 1, Hash::default(), ); process_transaction(&tx, &mut accounts).unwrap(); // Attack! Part 1: Sign a timestamp transaction with a random key. let rando = Keypair::new(); let mut tx = BudgetTransaction::new_timestamp(&rando, contract, to, dt, Hash::default()); // Attack! Part 2: Point the instruction to the expected, but unsigned, key. tx.account_keys.push(from.pubkey()); tx.instructions[0].accounts[0] = 3; // Ensure the transaction fails because of the unsigned key. assert_eq!( process_transaction(&tx, &mut accounts), Err(BudgetError::UnsignedKey) ); } #[test] fn test_transfer_on_date() { let mut accounts = vec![ Account::new(1, 0, id()), Account::new(0, 512, id()), Account::new(0, 0, 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 = BudgetTransaction::new_on_date( &from, to.pubkey(), contract.pubkey(), dt, from.pubkey(), None, 1, Hash::default(), ); process_transaction(&tx, &mut accounts).unwrap(); assert_eq!(accounts[from_account].tokens, 0); assert_eq!(accounts[contract_account].tokens, 1); let program = BudgetProgram::deserialize(&accounts[contract_account].userdata).unwrap(); assert!(program.is_pending()); // Attack! Try to payout to a rando key let tx = BudgetTransaction::new_timestamp( &from, contract.pubkey(), rando.pubkey(), dt, Hash::default(), ); assert_eq!( process_transaction(&tx, &mut accounts), Err(BudgetError::DestinationMissing) ); assert_eq!(accounts[from_account].tokens, 0); assert_eq!(accounts[contract_account].tokens, 1); assert_eq!(accounts[to_account].tokens, 0); let program = BudgetProgram::deserialize(&accounts[contract_account].userdata).unwrap(); assert!(program.is_pending()); // Now, acknowledge the time in the condition occurred and // that pubkey's funds are now available. let tx = BudgetTransaction::new_timestamp( &from, contract.pubkey(), to.pubkey(), dt, Hash::default(), ); process_transaction(&tx, &mut accounts).unwrap(); assert_eq!(accounts[from_account].tokens, 0); assert_eq!(accounts[contract_account].tokens, 0); assert_eq!(accounts[to_account].tokens, 1); let program = BudgetProgram::deserialize(&accounts[contract_account].userdata).unwrap(); assert!(!program.is_pending()); // try to replay the timestamp contract assert_eq!( process_transaction(&tx, &mut accounts), Err(BudgetError::ContractNotPending) ); assert_eq!(accounts[from_account].tokens, 0); assert_eq!(accounts[contract_account].tokens, 0); assert_eq!(accounts[to_account].tokens, 1); } #[test] fn test_cancel_transfer() { let mut accounts = vec![ Account::new(1, 0, id()), Account::new(0, 512, id()), Account::new(0, 0, 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 = BudgetTransaction::new_on_date( &from, to.pubkey(), contract.pubkey(), dt, from.pubkey(), Some(from.pubkey()), 1, Hash::default(), ); process_transaction(&tx, &mut accounts).unwrap(); assert_eq!(accounts[from_account].tokens, 0); assert_eq!(accounts[contract_account].tokens, 1); let program = BudgetProgram::deserialize(&accounts[contract_account].userdata).unwrap(); assert!(program.is_pending()); // Attack! try to put the tokens into the wrong account with cancel let tx = BudgetTransaction::new_signature(&to, contract.pubkey(), to.pubkey(), Hash::default()); // unit test hack, the `from account` is passed instead of the `to` account to avoid // creating more account vectors process_transaction(&tx, &mut accounts).unwrap(); // 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 is the `to.pubkey()` account assert_eq!(accounts[pay_account].tokens, 0); // Now, cancel the transaction. from gets her funds back let tx = BudgetTransaction::new_signature( &from, contract.pubkey(), from.pubkey(), Hash::default(), ); process_transaction(&tx, &mut accounts).unwrap(); assert_eq!(accounts[from_account].tokens, 1); assert_eq!(accounts[contract_account].tokens, 0); assert_eq!(accounts[pay_account].tokens, 0); // try to replay the cancel contract let tx = BudgetTransaction::new_signature( &from, contract.pubkey(), from.pubkey(), Hash::default(), ); assert_eq!( process_transaction(&tx, &mut accounts), Err(BudgetError::ContractNotPending) ); assert_eq!(accounts[from_account].tokens, 1); assert_eq!(accounts[contract_account].tokens, 0); assert_eq!(accounts[pay_account].tokens, 0); } #[test] fn test_userdata_too_small() { let mut accounts = vec![ Account::new(1, 0, id()), Account::new(1, 0, id()), // <== userdata is 0, which is not enough Account::new(1, 0, id()), ]; let from = Keypair::new(); let contract = Keypair::new(); let to = Keypair::new(); let tx = BudgetTransaction::new_on_date( &from, to.pubkey(), contract.pubkey(), Utc::now(), from.pubkey(), None, 1, Hash::default(), ); assert!(process_transaction(&tx, &mut accounts).is_err()); assert!(BudgetProgram::deserialize(&accounts[1].userdata).is_err()); let tx = BudgetTransaction::new_timestamp( &from, contract.pubkey(), to.pubkey(), Utc::now(), Hash::default(), ); assert!(process_transaction(&tx, &mut accounts).is_err()); assert!(BudgetProgram::deserialize(&accounts[1].userdata).is_err()); // Success if there was no panic... } }