//! budget program use bincode::{self, deserialize, serialize_into, serialized_size}; use budget_expr::BudgetExpr; use budget_instruction::Instruction; use chrono::prelude::{DateTime, Utc}; use payment_plan::Witness; use solana_sdk::account::Account; use solana_sdk::pubkey::Pubkey; use std::io; use transaction::Transaction; #[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 BudgetState { pub initialized: bool, pub pending_budget: Option, } const BUDGET_PROGRAM_ID: [u8; 32] = [ 129, 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 BudgetState { fn is_pending(&self) -> bool { self.pending_budget != None } pub fn id() -> Pubkey { Pubkey::new(&BUDGET_PROGRAM_ID) } pub fn check_id(program_id: &Pubkey) -> bool { program_id.as_ref() == BUDGET_PROGRAM_ID } /// Process a Witness Signature. Any payment plans waiting on this signature /// will progress one step. fn apply_signature( &mut self, tx: &Transaction, instruction_index: usize, accounts: &mut [&mut Account], ) -> Result<(), BudgetError> { let mut final_payment = None; if let Some(ref mut expr) = self.pending_budget { let key = match tx.signed_key(instruction_index, 0) { 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 Some(&payment.to) != tx.key(instruction_index, 2) { trace!("destination missing"); return Err(BudgetError::DestinationMissing); } self.pending_budget = None; accounts[1].tokens -= payment.tokens; accounts[2].tokens += payment.tokens; } Ok(()) } /// Process a Witness Timestamp. Any payment plans waiting on this timestamp /// will progress one step. fn apply_timestamp( &mut self, tx: &Transaction, instruction_index: usize, accounts: &mut [&mut Account], 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 tx.signed_key(instruction_index, 0) { 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 Some(&payment.to) != tx.key(instruction_index, 2) { trace!("destination missing"); return Err(BudgetError::DestinationMissing); } self.pending_budget = None; accounts[1].tokens -= payment.tokens; accounts[2].tokens += payment.tokens; } Ok(()) } fn apply_debits_to_budget_state( tx: &Transaction, instruction_index: usize, accounts: &mut [&mut Account], instruction: &Instruction, ) -> Result<(), BudgetError> { if !accounts[0].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() { 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) } else { let mut state = BudgetState::default(); state.pending_budget = Some(expr); accounts[1].tokens += accounts[0].tokens; accounts[0].tokens = 0; state.initialized = true; state.serialize(&mut accounts[1].userdata) } } } Instruction::ApplyTimestamp(dt) => { if let Ok(mut state) = Self::deserialize(&accounts[1].userdata) { if !state.is_pending() { Err(BudgetError::ContractNotPending) } else if !state.initialized { trace!("contract is uninitialized"); Err(BudgetError::UninitializedContract) } else { trace!("apply timestamp"); state.apply_timestamp(tx, instruction_index, accounts, *dt)?; trace!("apply timestamp committed"); state.serialize(&mut accounts[1].userdata) } } else { Err(BudgetError::UninitializedContract) } } Instruction::ApplySignature => { if let Ok(mut state) = Self::deserialize(&accounts[1].userdata) { if !state.is_pending() { Err(BudgetError::ContractNotPending) } else if !state.initialized { trace!("contract is uninitialized"); Err(BudgetError::UninitializedContract) } else { trace!("apply signature"); state.apply_signature(tx, instruction_index, accounts)?; trace!("apply signature committed"); state.serialize(&mut accounts[1].userdata) } } else { Err(BudgetError::UninitializedContract) } } } } 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]) } /// 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, instruction_index: usize, accounts: &mut [&mut Account], ) -> Result<(), BudgetError> { if let Ok(instruction) = deserialize(tx.userdata(instruction_index)) { trace!("process_transaction: {:?}", instruction); Self::apply_debits_to_budget_state(tx, instruction_index, accounts, &instruction) } else { info!( "Invalid transaction userdata: {:?}", tx.userdata(instruction_index) ); Err(BudgetError::UserdataDeserializeFailure) } } //TODO the contract needs to provide a "get_balance" introspection call of the userdata pub fn get_balance(account: &Account) -> u64 { if let Ok(state) = deserialize(&account.userdata) { let state: BudgetState = state; if state.is_pending() { 0 } else { account.tokens } } else { account.tokens } } } #[cfg(test)] mod test { use bincode::serialize; use budget_program::{BudgetError, BudgetState}; use budget_transaction::BudgetTransaction; use chrono::prelude::{DateTime, NaiveDate, Utc}; use hash::Hash; use signature::{GenKeys, Keypair, KeypairUtil}; use solana_sdk::account::Account; use solana_sdk::pubkey::Pubkey; use transaction::Transaction; fn process_transaction(tx: &Transaction, accounts: &mut [Account]) -> Result<(), BudgetError> { let mut refs: Vec<&mut Account> = accounts.iter_mut().collect(); BudgetState::process_transaction(&tx, 0, &mut refs[..]) } #[test] fn test_serializer() { let mut a = Account::new(0, 512, BudgetState::id()); let b = BudgetState::default(); b.serialize(&mut a.userdata).unwrap(); let buf = serialize(&b).unwrap(); assert_eq!(a.userdata[8..8 + buf.len()], buf[0..]); let c = BudgetState::deserialize(&a.userdata).unwrap(); assert_eq!(b, c); } #[test] fn test_serializer_userdata_too_small() { let mut a = Account::new(0, 1, BudgetState::id()); let b = BudgetState::default(); assert_eq!( b.serialize(&mut a.userdata), Err(BudgetError::UserdataTooSmall) ); } #[test] fn test_invalid_instruction() { let mut accounts = vec![ Account::new(1, 0, BudgetState::id()), Account::new(0, 512, BudgetState::id()), ]; let from = Keypair::new(); let contract = Keypair::new(); let userdata = (1u8, 2u8, 3u8); let tx = Transaction::new( &from, &[contract.pubkey()], BudgetState::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, BudgetState::id()), Account::new(0, 512, BudgetState::id()), Account::new(0, 0, BudgetState::id()), ]; // Initialize BudgetState let from = Keypair::new(); let contract = Keypair::new().pubkey(); let to = Keypair::new().pubkey(); let witness = Keypair::new().pubkey(); let tx = Transaction::budget_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 = Transaction::budget_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, BudgetState::id()), Account::new(0, 512, BudgetState::id()), Account::new(0, 0, BudgetState::id()), ]; // Initialize BudgetState let from = Keypair::new(); let contract = Keypair::new().pubkey(); let to = Keypair::new().pubkey(); let dt = Utc::now(); let tx = Transaction::budget_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 = Transaction::budget_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, BudgetState::id()), Account::new(0, 512, BudgetState::id()), Account::new(0, 0, BudgetState::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, 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 state = BudgetState::deserialize(&accounts[contract_account].userdata).unwrap(); 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(), ); 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 state = BudgetState::deserialize(&accounts[contract_account].userdata).unwrap(); 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(), ); 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 state = BudgetState::deserialize(&accounts[contract_account].userdata).unwrap(); assert!(!state.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, BudgetState::id()), Account::new(0, 512, BudgetState::id()), Account::new(0, 0, BudgetState::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, 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 state = BudgetState::deserialize(&accounts[contract_account].userdata).unwrap(); 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(), 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 would be the `to.pubkey()` account assert_eq!(accounts[pay_account].tokens, 0); // Now, cancel the transaction. from gets her funds back let tx = Transaction::budget_new_signature( &from, contract.pubkey(), from.pubkey(), 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[pay_account].tokens, 1); // try to replay the signature contract let tx = Transaction::budget_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, 0); assert_eq!(accounts[contract_account].tokens, 0); assert_eq!(accounts[pay_account].tokens, 1); } #[test] fn test_userdata_too_small() { let mut accounts = vec![ Account::new(1, 0, BudgetState::id()), Account::new(1, 0, BudgetState::id()), // <== userdata is 0, which is not enough Account::new(1, 0, BudgetState::id()), ]; let from = Keypair::new(); let contract = Keypair::new(); let to = Keypair::new(); let tx = Transaction::budget_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!(BudgetState::deserialize(&accounts[1].userdata).is_err()); let tx = Transaction::budget_new_timestamp( &from, contract.pubkey(), to.pubkey(), Utc::now(), Hash::default(), ); assert!(process_transaction(&tx, &mut accounts).is_err()); assert!(BudgetState::deserialize(&accounts[1].userdata).is_err()); // Success if there was no panic... } /// Detect binary changes in the serialized contract userdata, which could have a downstream /// affect on SDKs and DApps #[test] fn test_sdk_serialize() { let keypair = &GenKeys::new([0u8; 32]).gen_n_keypairs(1)[0]; let to = Pubkey::new(&[ 1, 1, 1, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 8, 7, 6, 5, 4, 1, 1, 1, ]); let contract = Pubkey::new(&[ 2, 2, 2, 4, 5, 6, 7, 8, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 8, 7, 6, 5, 4, 2, 2, 2, ]); let date = DateTime::::from_utc(NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11), Utc); let date_iso8601 = "2016-07-08T09:10:11Z"; let tx = Transaction::budget_new(&keypair, to, 192, Hash::default()); assert_eq!( tx.userdata(0).to_vec(), vec![2, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0] ); assert_eq!( tx.userdata(1).to_vec(), vec![ 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 8, 7, 6, 5, 4, 1, 1, 1 ] ); let tx = Transaction::budget_new_on_date( &keypair, to, contract, date, keypair.pubkey(), Some(keypair.pubkey()), 192, Hash::default(), ); assert_eq!( tx.userdata(0).to_vec(), vec![ 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 50, 48, 49, 54, 45, 48, 55, 45, 48, 56, 84, 48, 57, 58, 49, 48, 58, 49, 49, 90, 32, 253, 186, 201, 177, 11, 117, 135, 187, 167, 181, 188, 22, 59, 206, 105, 231, 150, 215, 30, 78, 212, 76, 16, 252, 180, 72, 134, 137, 247, 161, 68, 192, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 8, 7, 6, 5, 4, 1, 1, 1, 1, 0, 0, 0, 32, 253, 186, 201, 177, 11, 117, 135, 187, 167, 181, 188, 22, 59, 206, 105, 231, 150, 215, 30, 78, 212, 76, 16, 252, 180, 72, 134, 137, 247, 161, 68, 192, 0, 0, 0, 0, 0, 0, 0, 32, 253, 186, 201, 177, 11, 117, 135, 187, 167, 181, 188, 22, 59, 206, 105, 231, 150, 215, 30, 78, 212, 76, 16, 252, 180, 72, 134, 137, 247, 161, 68 ] ); // ApplyTimestamp(date) let tx = Transaction::budget_new_timestamp( &keypair, keypair.pubkey(), to, date, Hash::default(), ); let mut expected_userdata = vec![1, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0]; expected_userdata.extend(date_iso8601.as_bytes()); assert_eq!(tx.userdata(0).to_vec(), expected_userdata); // ApplySignature let tx = Transaction::budget_new_signature(&keypair, keypair.pubkey(), to, Hash::default()); assert_eq!(tx.userdata(0).to_vec(), vec![2, 0, 0, 0]); } }