From c6b4a3a7064a788aeac4143dcba486de7062d5c5 Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Thu, 13 Jun 2019 18:20:28 -0700 Subject: [PATCH] Witness account data in Budget (#4650) * Add support for contracts based on account data to Budget * Add program_id to account constraints * No longer require a signature for the account data witness * Rename bank::store to store_account * fmt * Add a doc * clippy --- programs/budget_api/src/budget_expr.rs | 50 ++++++++ programs/budget_api/src/budget_instruction.rs | 42 ++++++- programs/budget_api/src/budget_processor.rs | 111 +++++++++++++++++- runtime/src/bank.rs | 20 ++-- 4 files changed, 210 insertions(+), 13 deletions(-) diff --git a/programs/budget_api/src/budget_expr.rs b/programs/budget_api/src/budget_expr.rs index 200e97d319..413f7ae35d 100644 --- a/programs/budget_api/src/budget_expr.rs +++ b/programs/budget_api/src/budget_expr.rs @@ -5,6 +5,7 @@ use chrono::prelude::*; use serde_derive::{Deserialize, Serialize}; +use solana_sdk::hash::Hash; use solana_sdk::pubkey::Pubkey; use std::mem; @@ -16,6 +17,9 @@ pub enum Witness { /// A signature from Pubkey. Signature, + + /// Account snapshot. + AccountData(Hash, Pubkey), } /// Some amount of lamports that should be sent to the `to` `Pubkey`. @@ -28,6 +32,23 @@ pub struct Payment { pub to: Pubkey, } +/// The account constraints a Condition would wait on. +/// Note: ideally this would be function that accepts an Account and returns +/// a bool, but we don't have a way to pass functions over the wire. To simulate +/// higher order programming, create your own program that includes an instruction +/// that sets account data to a boolean. Pass that account key and program_id here. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct AccountConstraints { + /// The account holder. + pub key: Pubkey, + + /// The program id that must own the account at `key`. + pub program_id: Pubkey, + + /// The hash of the data in the account at `key`. + pub data_hash: Hash, +} + /// A data type representing a `Witness` that the payment plan is waiting on. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum Condition { @@ -36,6 +57,9 @@ pub enum Condition { /// Wait for a `Signature` `Witness` from `Pubkey`. Signature(Pubkey), + + /// Wait for the account with the given constraints. + AccountData(AccountConstraints), } impl Condition { @@ -46,6 +70,14 @@ impl Condition { (Condition::Timestamp(dt, pubkey), Witness::Timestamp(last_time)) => { pubkey == from && dt <= last_time } + ( + Condition::AccountData(constraints), + Witness::AccountData(actual_hash, program_id), + ) => { + constraints.program_id == *program_id + && constraints.key == *from + && constraints.data_hash == *actual_hash + } _ => false, } } @@ -83,6 +115,24 @@ impl BudgetExpr { ) } + /// Create a budget that pays `lamports` to `to` after witnessing account data in `account_pubkey` with the given hash. + pub fn new_payment_when_account_data( + account_pubkey: &Pubkey, + account_program_id: &Pubkey, + account_hash: Hash, + lamports: u64, + to: &Pubkey, + ) -> Self { + BudgetExpr::After( + Condition::AccountData(AccountConstraints { + key: *account_pubkey, + program_id: *account_program_id, + data_hash: account_hash, + }), + Box::new(Self::new_payment(lamports, to)), + ) + } + /// Create a budget that pays `lamports` to `to` after being witnessed by `witness` unless /// canceled with a signature from `from`. pub fn new_cancelable_authorized_payment( diff --git a/programs/budget_api/src/budget_instruction.rs b/programs/budget_api/src/budget_instruction.rs index 4441d9b85a..3a5817e833 100644 --- a/programs/budget_api/src/budget_instruction.rs +++ b/programs/budget_api/src/budget_instruction.rs @@ -4,6 +4,7 @@ use crate::id; use bincode::serialized_size; use chrono::prelude::{DateTime, Utc}; use serde_derive::{Deserialize, Serialize}; +use solana_sdk::hash::Hash; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::pubkey::Pubkey; use solana_sdk::system_instruction; @@ -20,7 +21,7 @@ pub struct Contract { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum BudgetInstruction { /// Declare and instantiate `BudgetExpr`. - InitializeAccount(BudgetExpr), + InitializeAccount(Box), /// Tell a payment plan acknowledge the given `DateTime` has past. ApplyTimestamp(DateTime), @@ -28,6 +29,9 @@ pub enum BudgetInstruction { /// Tell the budget that the `InitializeAccount` with `Signature` has been /// signed by the containing transaction's `Pubkey`. ApplySignature, + + /// Load an account and pass its data to the budget for inspection. + ApplyAccountData, } fn initialize_account(contract: &Pubkey, expr: BudgetExpr) -> Instruction { @@ -36,7 +40,11 @@ fn initialize_account(contract: &Pubkey, expr: BudgetExpr) -> Instruction { keys.push(AccountMeta::new(payment.to, false)); } keys.push(AccountMeta::new(*contract, false)); - Instruction::new(id(), &BudgetInstruction::InitializeAccount(expr), keys) + Instruction::new( + id(), + &BudgetInstruction::InitializeAccount(Box::new(expr)), + keys, + ) } pub fn create_account( @@ -89,6 +97,26 @@ pub fn when_signed( create_account(from, contract, lamports, expr) } +/// Make a payment when an account has the given data +pub fn when_account_data( + from: &Pubkey, + to: &Pubkey, + contract: &Pubkey, + account_pubkey: &Pubkey, + account_program_id: &Pubkey, + account_hash: Hash, + lamports: u64, +) -> Vec { + let expr = BudgetExpr::new_payment_when_account_data( + account_pubkey, + account_program_id, + account_hash, + lamports, + to, + ); + create_account(from, contract, lamports, expr) +} + pub fn apply_timestamp( from: &Pubkey, contract: &Pubkey, @@ -116,6 +144,16 @@ pub fn apply_signature(from: &Pubkey, contract: &Pubkey, to: &Pubkey) -> Instruc Instruction::new(id(), &BudgetInstruction::ApplySignature, account_metas) } +/// Apply account data to a contract waiting on an AccountData witness. +pub fn apply_account_data(witness_pubkey: &Pubkey, contract: &Pubkey, to: &Pubkey) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*witness_pubkey, false), + AccountMeta::new(*contract, false), + AccountMeta::new(*to, false), + ]; + Instruction::new(id(), &BudgetInstruction::ApplyAccountData, account_metas) +} + #[cfg(test)] mod tests { use super::*; diff --git a/programs/budget_api/src/budget_processor.rs b/programs/budget_api/src/budget_processor.rs index 6b7b8cf938..569898e6a7 100644 --- a/programs/budget_api/src/budget_processor.rs +++ b/programs/budget_api/src/budget_processor.rs @@ -6,6 +6,7 @@ use bincode::deserialize; use chrono::prelude::{DateTime, Utc}; use log::*; use solana_sdk::account::KeyedAccount; +use solana_sdk::hash::hash; use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; @@ -70,6 +71,35 @@ fn apply_timestamp( Ok(()) } +/// Process an AccountData Witness and any payment waiting on it. +fn apply_account_data( + budget_state: &mut BudgetState, + keyed_accounts: &mut [KeyedAccount], +) -> Result<(), BudgetError> { + // Check to see if any timelocked transactions can be completed. + let mut final_payment = None; + + if let Some(ref mut expr) = budget_state.pending_budget { + let witness_keyed_account = &keyed_accounts[0]; + let key = witness_keyed_account.unsigned_key(); + let program_id = witness_keyed_account.account.owner; + let actual_hash = hash(&witness_keyed_account.account.data); + expr.apply_witness(&Witness::AccountData(actual_hash, program_id), 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); + } + budget_state.pending_budget = None; + keyed_accounts[1].account.lamports -= payment.lamports; + keyed_accounts[2].account.lamports += payment.lamports; + } + Ok(()) +} + pub fn process_instruction( _program_id: &Pubkey, keyed_accounts: &mut [KeyedAccount], @@ -96,7 +126,7 @@ pub fn process_instruction( return Err(InstructionError::AccountAlreadyInitialized); } let mut budget_state = BudgetState::default(); - budget_state.pending_budget = Some(expr); + budget_state.pending_budget = Some(*expr); budget_state.initialized = true; budget_state.serialize(&mut keyed_accounts[0].account.data) } @@ -136,6 +166,20 @@ pub fn process_instruction( trace!("apply signature committed"); budget_state.serialize(&mut keyed_accounts[1].account.data) } + BudgetInstruction::ApplyAccountData => { + let mut budget_state = BudgetState::deserialize(&keyed_accounts[1].account.data)?; + if !budget_state.is_pending() { + return Ok(()); // Nothing to do here. + } + if !budget_state.initialized { + trace!("contract is uninitialized"); + return Err(InstructionError::UninitializedAccount); + } + apply_account_data(&mut budget_state, keyed_accounts) + .map_err(|e| InstructionError::CustomError(e as u32))?; + trace!("apply account data committed"); + budget_state.serialize(&mut keyed_accounts[1].account.data) + } } } @@ -146,8 +190,10 @@ mod tests { use crate::id; use solana_runtime::bank::Bank; use solana_runtime::bank_client::BankClient; + use solana_sdk::account::Account; use solana_sdk::client::SyncClient; use solana_sdk::genesis_block::create_genesis_block; + use solana_sdk::hash::hash; use solana_sdk::instruction::InstructionError; use solana_sdk::message::Message; use solana_sdk::signature::{Keypair, KeypairUtil}; @@ -400,4 +446,67 @@ mod tests { assert_eq!(bank_client.get_account_data(&budget_pubkey).unwrap(), None); assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None); } + + #[test] + fn test_pay_when_account_data() { + let (bank, alice_keypair) = create_bank(42); + let game_pubkey = Pubkey::new_rand(); + let game_account = Account { + lamports: 1, + data: vec![1, 2, 3], + ..Account::default() + }; + bank.store_account(&game_pubkey, &game_account); + assert_eq!(bank.get_account(&game_pubkey).unwrap().data, vec![1, 2, 3]); + + let bank_client = BankClient::new(bank); + + let alice_pubkey = alice_keypair.pubkey(); + let game_hash = hash(&[1, 2, 3]); + let budget_pubkey = Pubkey::new_rand(); + let bob_keypair = Keypair::new(); + let bob_pubkey = bob_keypair.pubkey(); + + // Give Bob some lamports so he can sign the witness transaction. + bank_client + .transfer(1, &alice_keypair, &bob_pubkey) + .unwrap(); + + let instructions = budget_instruction::when_account_data( + &alice_pubkey, + &bob_pubkey, + &budget_pubkey, + &game_pubkey, + &game_account.owner, + game_hash, + 41, + ); + let message = Message::new(instructions); + bank_client + .send_message(&[&alice_keypair], message) + .unwrap(); + assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 0); + assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 41); + + let contract_account = bank_client + .get_account_data(&budget_pubkey) + .unwrap() + .unwrap(); + let budget_state = BudgetState::deserialize(&contract_account).unwrap(); + assert!(budget_state.is_pending()); + + // Acknowledge the condition occurred and that Bob's funds are now available. + let instruction = + budget_instruction::apply_account_data(&game_pubkey, &budget_pubkey, &bob_pubkey); + + // Anyone can sign the message, but presumably it's Bob, since he's the + // one claiming the payout. + let message = Message::new_with_payer(vec![instruction], Some(&bob_pubkey)); + bank_client.send_message(&[&bob_keypair], message).unwrap(); + + assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 0); + assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 0); + assert_eq!(bank_client.get_balance(&bob_pubkey).unwrap(), 42); + assert_eq!(bank_client.get_account_data(&budget_pubkey).unwrap(), None); + } } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index b205f885c1..f798a48855 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -379,7 +379,7 @@ impl Bank { }; current.to(&mut account).unwrap(); - self.store(¤t::id(), &account); + self.store_account(¤t::id(), &account); } fn update_slot_hashes(&self) { @@ -391,7 +391,7 @@ impl Bank { slot_hashes.add(self.slot(), self.hash()); slot_hashes.to(&mut account).unwrap(); - self.store(&slot_hashes::id(), &account); + self.store_account(&slot_hashes::id(), &account); } fn update_fees(&self) { @@ -403,7 +403,7 @@ impl Bank { fees.fee_calculator = self.fee_calculator.clone(); fees.to(&mut account).unwrap(); - self.store(&fees::id(), &account); + self.store_account(&fees::id(), &account); } fn update_tick_height(&self) { @@ -413,7 +413,7 @@ impl Bank { TickHeight::to(self.tick_height(), &mut account).unwrap(); - self.store(&tick_height::id(), &account); + self.store_account(&tick_height::id(), &account); } fn set_hash(&self) -> bool { @@ -482,7 +482,7 @@ impl Bank { self.update_fees(); for (pubkey, account) in genesis_block.accounts.iter() { - self.store(pubkey, account); + self.store_account(pubkey, account); self.capitalization .fetch_add(account.lamports as usize, Ordering::Relaxed); } @@ -526,7 +526,7 @@ impl Bank { pub fn register_native_instruction_processor(&self, name: &str, program_id: &Pubkey) { debug!("Adding native program {} under {:?}", name, program_id); let account = native_loader::create_loadable_account(name); - self.store(program_id, &account); + self.store_account(program_id, &account); } /// Return the last block hash registered. @@ -930,7 +930,7 @@ impl Bank { Err(TransactionError::InstructionError(_, _)) => { // credit the transaction fee even in case of InstructionError // necessary to withdraw from account[0] here because previous - // work of doing so (in accounts.load()) is ignored by store() + // work of doing so (in accounts.load()) is ignored by store_account() self.withdraw(&message.account_keys[0], fee)?; fees += fee; Ok(()) @@ -1034,7 +1034,7 @@ impl Bank { parents } - fn store(&self, pubkey: &Pubkey, account: &Account) { + pub fn store_account(&self, pubkey: &Pubkey, account: &Account) { self.rc.accounts.store_slow(self.slot(), pubkey, account); if Stakes::is_stake(account) { @@ -1055,7 +1055,7 @@ impl Bank { } account.lamports -= lamports; - self.store(pubkey, &account); + self.store_account(pubkey, &account); Ok(()) } @@ -1066,7 +1066,7 @@ impl Bank { pub fn deposit(&self, pubkey: &Pubkey, lamports: u64) { let mut account = self.get_account(pubkey).unwrap_or_default(); account.lamports += lamports; - self.store(pubkey, &account); + self.store_account(pubkey, &account); } pub fn accounts(&self) -> Arc {