Revert "Rename programs to instruction_processors (#3789)" (#3824)

This reverts commit 34344982a9.
This commit is contained in:
Greg Fitzgerald
2019-04-17 15:05:49 -06:00
committed by GitHub
parent 083090817a
commit 51a2988bb2
93 changed files with 44 additions and 44 deletions

View File

@ -0,0 +1,24 @@
[package]
name = "solana-budget-api"
version = "0.14.0"
description = "Solana Budget program API"
authors = ["Solana Maintainers <maintainers@solana.com>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
edition = "2018"
[dependencies]
bincode = "1.1.3"
chrono = { version = "0.4.0", features = ["serde"] }
log = "0.4.2"
serde = "1.0.90"
serde_derive = "1.0.90"
solana-sdk = { path = "../../sdk", version = "0.14.0" }
[dev-dependencies]
solana-runtime = { path = "../../runtime", version = "0.14.0" }
[lib]
name = "solana_budget_api"
crate-type = ["lib"]

View File

@ -0,0 +1,341 @@
//! The `budget_expr` module provides a domain-specific language for pa&yment plans. Users create BudgetExpr objects that
//! are given to an interpreter. The interpreter listens for `Witness` transactions,
//! which it uses to reduce the payment plan. When the budget is reduced to a
//! `Payment`, the payment is executed.
use chrono::prelude::*;
use serde_derive::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use std::mem;
/// The types of events a payment plan can process.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum Witness {
/// The current time.
Timestamp(DateTime<Utc>),
/// A signature from Pubkey.
Signature,
}
/// Some amount of lamports that should be sent to the `to` `Pubkey`.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Payment {
/// Amount to be paid.
pub lamports: u64,
/// The `Pubkey` that `lamports` should be paid to.
pub to: Pubkey,
}
/// A data type representing a `Witness` that the payment plan is waiting on.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum Condition {
/// Wait for a `Timestamp` `Witness` at or after the given `DateTime`.
Timestamp(DateTime<Utc>, Pubkey),
/// Wait for a `Signature` `Witness` from `Pubkey`.
Signature(Pubkey),
}
impl Condition {
/// Return true if the given Witness satisfies this Condition.
pub fn is_satisfied(&self, witness: &Witness, from: &Pubkey) -> bool {
match (self, witness) {
(Condition::Signature(pubkey), Witness::Signature) => pubkey == from,
(Condition::Timestamp(dt, pubkey), Witness::Timestamp(last_time)) => {
pubkey == from && dt <= last_time
}
_ => false,
}
}
}
/// A data type representing a payment plan.
#[repr(C)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum BudgetExpr {
/// Make a payment.
Pay(Payment),
/// Make a payment after some condition.
After(Condition, Box<BudgetExpr>),
/// Either make a payment after one condition or a different payment after another
/// condition, which ever condition is satisfied first.
Or((Condition, Box<BudgetExpr>), (Condition, Box<BudgetExpr>)),
/// Make a payment after both of two conditions are satisfied
And(Condition, Condition, Box<BudgetExpr>),
}
impl BudgetExpr {
/// Create the simplest budget - one that pays `lamports` to Pubkey.
pub fn new_payment(lamports: u64, to: &Pubkey) -> Self {
BudgetExpr::Pay(Payment { lamports, to: *to })
}
/// Create a budget that pays `lamports` to `to` after being witnessed by `from`.
pub fn new_authorized_payment(from: &Pubkey, lamports: u64, to: &Pubkey) -> Self {
BudgetExpr::After(
Condition::Signature(*from),
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(
witness: &Pubkey,
lamports: u64,
to: &Pubkey,
from: Option<Pubkey>,
) -> Self {
if from.is_none() {
return Self::new_authorized_payment(witness, lamports, to);
}
let from = from.unwrap();
BudgetExpr::Or(
(
Condition::Signature(*witness),
Box::new(BudgetExpr::new_payment(lamports, to)),
),
(
Condition::Signature(from),
Box::new(BudgetExpr::new_payment(lamports, &from)),
),
)
}
/// Create a budget that pays lamports` to `to` after being witnessed by 2x `from`s
pub fn new_2_2_multisig_payment(
from0: &Pubkey,
from1: &Pubkey,
lamports: u64,
to: &Pubkey,
) -> Self {
BudgetExpr::And(
Condition::Signature(*from0),
Condition::Signature(*from1),
Box::new(Self::new_payment(lamports, to)),
)
}
/// Create a budget that pays `lamports` to `to` after the given DateTime signed
/// by `dt_pubkey`.
pub fn new_future_payment(
dt: DateTime<Utc>,
dt_pubkey: &Pubkey,
lamports: u64,
to: &Pubkey,
) -> Self {
BudgetExpr::After(
Condition::Timestamp(dt, *dt_pubkey),
Box::new(Self::new_payment(lamports, to)),
)
}
/// Create a budget that pays `lamports` to `to` after the given DateTime
/// signed by `dt_pubkey` unless canceled by `from`.
pub fn new_cancelable_future_payment(
dt: DateTime<Utc>,
dt_pubkey: &Pubkey,
lamports: u64,
to: &Pubkey,
from: Option<Pubkey>,
) -> Self {
if from.is_none() {
return Self::new_future_payment(dt, dt_pubkey, lamports, to);
}
let from = from.unwrap();
BudgetExpr::Or(
(
Condition::Timestamp(dt, *dt_pubkey),
Box::new(Self::new_payment(lamports, to)),
),
(
Condition::Signature(from),
Box::new(Self::new_payment(lamports, &from)),
),
)
}
/// Return Payment if the budget requires no additional Witnesses.
pub fn final_payment(&self) -> Option<Payment> {
match self {
BudgetExpr::Pay(payment) => Some(payment.clone()),
_ => None,
}
}
/// Return true if the budget spends exactly `spendable_lamports`.
pub fn verify(&self, spendable_lamports: u64) -> bool {
match self {
BudgetExpr::Pay(payment) => payment.lamports == spendable_lamports,
BudgetExpr::After(_, sub_expr) | BudgetExpr::And(_, _, sub_expr) => {
sub_expr.verify(spendable_lamports)
}
BudgetExpr::Or(a, b) => {
a.1.verify(spendable_lamports) && b.1.verify(spendable_lamports)
}
}
}
/// Apply a witness to the budget to see if the budget can be reduced.
/// If so, modify the budget in-place.
pub fn apply_witness(&mut self, witness: &Witness, from: &Pubkey) {
let new_expr = match self {
BudgetExpr::After(cond, sub_expr) if cond.is_satisfied(witness, from) => {
Some(sub_expr.clone())
}
BudgetExpr::Or((cond, sub_expr), _) if cond.is_satisfied(witness, from) => {
Some(sub_expr.clone())
}
BudgetExpr::Or(_, (cond, sub_expr)) if cond.is_satisfied(witness, from) => {
Some(sub_expr.clone())
}
BudgetExpr::And(cond0, cond1, sub_expr) => {
if cond0.is_satisfied(witness, from) {
Some(Box::new(BudgetExpr::After(cond1.clone(), sub_expr.clone())))
} else if cond1.is_satisfied(witness, from) {
Some(Box::new(BudgetExpr::After(cond0.clone(), sub_expr.clone())))
} else {
None
}
}
_ => None,
};
if let Some(expr) = new_expr {
mem::replace(self, *expr);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signature_satisfied() {
let from = Pubkey::default();
assert!(Condition::Signature(from).is_satisfied(&Witness::Signature, &from));
}
#[test]
fn test_timestamp_satisfied() {
let dt1 = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
let dt2 = Utc.ymd(2014, 11, 14).and_hms(10, 9, 8);
let from = Pubkey::default();
assert!(Condition::Timestamp(dt1, from).is_satisfied(&Witness::Timestamp(dt1), &from));
assert!(Condition::Timestamp(dt1, from).is_satisfied(&Witness::Timestamp(dt2), &from));
assert!(!Condition::Timestamp(dt2, from).is_satisfied(&Witness::Timestamp(dt1), &from));
}
#[test]
fn test_verify() {
let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
let from = Pubkey::default();
let to = Pubkey::default();
assert!(BudgetExpr::new_payment(42, &to).verify(42));
assert!(BudgetExpr::new_authorized_payment(&from, 42, &to).verify(42));
assert!(BudgetExpr::new_future_payment(dt, &from, 42, &to).verify(42));
assert!(
BudgetExpr::new_cancelable_future_payment(dt, &from, 42, &to, Some(from)).verify(42)
);
}
#[test]
fn test_authorized_payment() {
let from = Pubkey::default();
let to = Pubkey::default();
let mut expr = BudgetExpr::new_authorized_payment(&from, 42, &to);
expr.apply_witness(&Witness::Signature, &from);
assert_eq!(expr, BudgetExpr::new_payment(42, &to));
}
#[test]
fn test_future_payment() {
let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
let from = Pubkey::new_rand();
let to = Pubkey::new_rand();
let mut expr = BudgetExpr::new_future_payment(dt, &from, 42, &to);
expr.apply_witness(&Witness::Timestamp(dt), &from);
assert_eq!(expr, BudgetExpr::new_payment(42, &to));
}
#[test]
fn test_unauthorized_future_payment() {
// Ensure timestamp will only be acknowledged if it came from the
// whitelisted public key.
let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
let from = Pubkey::new_rand();
let to = Pubkey::new_rand();
let mut expr = BudgetExpr::new_future_payment(dt, &from, 42, &to);
let orig_expr = expr.clone();
expr.apply_witness(&Witness::Timestamp(dt), &to); // <-- Attack!
assert_eq!(expr, orig_expr);
}
#[test]
fn test_cancelable_future_payment() {
let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
let from = Pubkey::default();
let to = Pubkey::default();
let mut expr = BudgetExpr::new_cancelable_future_payment(dt, &from, 42, &to, Some(from));
expr.apply_witness(&Witness::Timestamp(dt), &from);
assert_eq!(expr, BudgetExpr::new_payment(42, &to));
let mut expr = BudgetExpr::new_cancelable_future_payment(dt, &from, 42, &to, Some(from));
expr.apply_witness(&Witness::Signature, &from);
assert_eq!(expr, BudgetExpr::new_payment(42, &from));
}
#[test]
fn test_2_2_multisig_payment() {
let from0 = Pubkey::new_rand();
let from1 = Pubkey::new_rand();
let to = Pubkey::default();
let mut expr = BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to);
expr.apply_witness(&Witness::Signature, &from0);
assert_eq!(expr, BudgetExpr::new_authorized_payment(&from1, 42, &to));
}
#[test]
fn test_multisig_after_sig() {
let from0 = Pubkey::new_rand();
let from1 = Pubkey::new_rand();
let from2 = Pubkey::new_rand();
let to = Pubkey::default();
let expr = BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to);
let mut expr = BudgetExpr::After(Condition::Signature(from2), Box::new(expr));
expr.apply_witness(&Witness::Signature, &from2);
expr.apply_witness(&Witness::Signature, &from0);
assert_eq!(expr, BudgetExpr::new_authorized_payment(&from1, 42, &to));
}
#[test]
fn test_multisig_after_ts() {
let from0 = Pubkey::new_rand();
let from1 = Pubkey::new_rand();
let dt = Utc.ymd(2014, 11, 11).and_hms(7, 7, 7);
let to = Pubkey::default();
let expr = BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to);
let mut expr = BudgetExpr::After(Condition::Timestamp(dt, from0), Box::new(expr));
expr.apply_witness(&Witness::Timestamp(dt), &from0);
assert_eq!(
expr,
BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to)
);
expr.apply_witness(&Witness::Signature, &from0);
assert_eq!(expr, BudgetExpr::new_authorized_payment(&from1, 42, &to));
}
}

View File

@ -0,0 +1,150 @@
use crate::budget_expr::BudgetExpr;
use crate::budget_state::BudgetState;
use crate::id;
use bincode::serialized_size;
use chrono::prelude::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::system_instruction;
/// A smart contract.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Contract {
/// The number of lamports allocated to the `BudgetExpr` and any transaction fees.
pub lamports: u64,
pub budget_expr: BudgetExpr,
}
/// An instruction to progress the smart contract.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum BudgetInstruction {
/// Declare and instantiate `BudgetExpr`.
InitializeAccount(BudgetExpr),
/// Tell a payment plan acknowledge the given `DateTime` has past.
ApplyTimestamp(DateTime<Utc>),
/// Tell the budget that the `InitializeAccount` with `Signature` has been
/// signed by the containing transaction's `Pubkey`.
ApplySignature,
}
fn initialize_account(contract: &Pubkey, expr: BudgetExpr) -> Instruction {
let mut keys = vec![];
if let BudgetExpr::Pay(payment) = &expr {
keys.push(AccountMeta::new(payment.to, false));
}
keys.push(AccountMeta::new(*contract, false));
Instruction::new(id(), &BudgetInstruction::InitializeAccount(expr), keys)
}
pub fn create_account(
from: &Pubkey,
contract: &Pubkey,
lamports: u64,
expr: BudgetExpr,
) -> Vec<Instruction> {
if !expr.verify(lamports) {
panic!("invalid budget expression");
}
let space = serialized_size(&BudgetState::new(expr.clone())).unwrap();
vec![
system_instruction::create_account(&from, contract, lamports, space, &id()),
initialize_account(contract, expr),
]
}
/// Create a new payment script.
pub fn payment(from: &Pubkey, to: &Pubkey, lamports: u64) -> Vec<Instruction> {
let contract = Pubkey::new_rand();
let expr = BudgetExpr::new_payment(lamports, to);
create_account(from, &contract, lamports, expr)
}
/// Create a future payment script.
pub fn on_date(
from: &Pubkey,
to: &Pubkey,
contract: &Pubkey,
dt: DateTime<Utc>,
dt_pubkey: &Pubkey,
cancelable: Option<Pubkey>,
lamports: u64,
) -> Vec<Instruction> {
let expr = BudgetExpr::new_cancelable_future_payment(dt, dt_pubkey, lamports, to, cancelable);
create_account(from, contract, lamports, expr)
}
/// Create a multisig payment script.
pub fn when_signed(
from: &Pubkey,
to: &Pubkey,
contract: &Pubkey,
witness: &Pubkey,
cancelable: Option<Pubkey>,
lamports: u64,
) -> Vec<Instruction> {
let expr = BudgetExpr::new_cancelable_authorized_payment(witness, lamports, to, cancelable);
create_account(from, contract, lamports, expr)
}
pub fn apply_timestamp(
from: &Pubkey,
contract: &Pubkey,
to: &Pubkey,
dt: DateTime<Utc>,
) -> Instruction {
let mut account_metas = vec![
AccountMeta::new(*from, true),
AccountMeta::new(*contract, false),
];
if from != to {
account_metas.push(AccountMeta::new(*to, false));
}
Instruction::new(id(), &BudgetInstruction::ApplyTimestamp(dt), account_metas)
}
pub fn apply_signature(from: &Pubkey, contract: &Pubkey, to: &Pubkey) -> Instruction {
let mut account_metas = vec![
AccountMeta::new(*from, true),
AccountMeta::new(*contract, false),
];
if from != to {
account_metas.push(AccountMeta::new(*to, false));
}
Instruction::new(id(), &BudgetInstruction::ApplySignature, account_metas)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::budget_expr::BudgetExpr;
#[test]
fn test_budget_instruction_verify() {
let alice_pubkey = Pubkey::new_rand();
let bob_pubkey = Pubkey::new_rand();
payment(&alice_pubkey, &bob_pubkey, 1); // No panic! indicates success.
}
#[test]
#[should_panic]
fn test_budget_instruction_overspend() {
let alice_pubkey = Pubkey::new_rand();
let bob_pubkey = Pubkey::new_rand();
let budget_pubkey = Pubkey::new_rand();
let expr = BudgetExpr::new_payment(2, &bob_pubkey);
create_account(&alice_pubkey, &budget_pubkey, 1, expr);
}
#[test]
#[should_panic]
fn test_budget_instruction_underspend() {
let alice_pubkey = Pubkey::new_rand();
let bob_pubkey = Pubkey::new_rand();
let budget_pubkey = Pubkey::new_rand();
let expr = BudgetExpr::new_payment(1, &bob_pubkey);
create_account(&alice_pubkey, &budget_pubkey, 2, expr);
}
}

View File

@ -0,0 +1,402 @@
//! budget program
use crate::budget_expr::Witness;
use crate::budget_instruction::BudgetInstruction;
use crate::budget_state::{BudgetError, BudgetState};
use bincode::deserialize;
use chrono::prelude::{DateTime, Utc};
use log::*;
use solana_sdk::account::KeyedAccount;
use solana_sdk::instruction::InstructionError;
use solana_sdk::pubkey::Pubkey;
/// Process a Witness Signature. Any payment plans waiting on this signature
/// will progress one step.
fn apply_signature(
budget_state: &mut BudgetState,
keyed_accounts: &mut [KeyedAccount],
) -> Result<(), BudgetError> {
let mut final_payment = None;
if let Some(ref mut expr) = budget_state.pending_budget {
let key = keyed_accounts[0].signer_key().unwrap();
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 {
budget_state.pending_budget = None;
keyed_accounts[1].account.lamports -= payment.lamports;
keyed_accounts[0].account.lamports += payment.lamports;
return Ok(());
}
}
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(())
}
/// Process a Witness Timestamp. Any payment plans waiting on this timestamp
/// will progress one step.
fn apply_timestamp(
budget_state: &mut BudgetState,
keyed_accounts: &mut [KeyedAccount],
dt: DateTime<Utc>,
) -> 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 key = keyed_accounts[0].signer_key().unwrap();
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);
}
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],
data: &[u8],
_tick_height: u64,
) -> Result<(), InstructionError> {
let instruction = deserialize(data).map_err(|err| {
info!("Invalid transaction data: {:?} {:?}", data, err);
InstructionError::InvalidInstructionData
})?;
trace!("process_instruction: {:?}", instruction);
match instruction {
BudgetInstruction::InitializeAccount(expr) => {
let expr = expr.clone();
if let Some(payment) = expr.final_payment() {
keyed_accounts[1].account.lamports = 0;
keyed_accounts[0].account.lamports += payment.lamports;
return Ok(());
}
let existing = BudgetState::deserialize(&keyed_accounts[0].account.data).ok();
if Some(true) == existing.map(|x| x.initialized) {
trace!("contract already exists");
return Err(InstructionError::AccountAlreadyInitialized);
}
let mut budget_state = BudgetState::default();
budget_state.pending_budget = Some(expr);
budget_state.initialized = true;
budget_state.serialize(&mut keyed_accounts[0].account.data)
}
BudgetInstruction::ApplyTimestamp(dt) => {
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);
}
if keyed_accounts[0].signer_key().is_none() {
return Err(InstructionError::MissingRequiredSignature);
}
trace!("apply timestamp");
apply_timestamp(&mut budget_state, keyed_accounts, dt)
.map_err(|e| InstructionError::CustomError(e as u32))?;
trace!("apply timestamp committed");
budget_state.serialize(&mut keyed_accounts[1].account.data)
}
BudgetInstruction::ApplySignature => {
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);
}
if keyed_accounts[0].signer_key().is_none() {
return Err(InstructionError::MissingRequiredSignature);
}
trace!("apply signature");
apply_signature(&mut budget_state, keyed_accounts)
.map_err(|e| InstructionError::CustomError(e as u32))?;
trace!("apply signature committed");
budget_state.serialize(&mut keyed_accounts[1].account.data)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::budget_instruction;
use crate::id;
use solana_runtime::bank::Bank;
use solana_runtime::bank_client::BankClient;
use solana_sdk::client::SyncClient;
use solana_sdk::genesis_block::GenesisBlock;
use solana_sdk::instruction::InstructionError;
use solana_sdk::message::Message;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::transaction::TransactionError;
fn create_bank(lamports: u64) -> (Bank, Keypair) {
let (genesis_block, mint_keypair) = GenesisBlock::new(lamports);
let mut bank = Bank::new(&genesis_block);
bank.add_instruction_processor(id(), process_instruction);
(bank, mint_keypair)
}
#[test]
fn test_budget_payment() {
let (bank, alice_keypair) = create_bank(10_000);
let bank_client = BankClient::new(bank);
let alice_pubkey = alice_keypair.pubkey();
let bob_pubkey = Pubkey::new_rand();
let instructions = budget_instruction::payment(&alice_pubkey, &bob_pubkey, 100);
let message = Message::new(instructions);
bank_client
.send_message(&[&alice_keypair], message)
.unwrap();
assert_eq!(bank_client.get_balance(&bob_pubkey).unwrap(), 100);
}
#[test]
fn test_unsigned_witness_key() {
let (bank, alice_keypair) = create_bank(10_000);
let bank_client = BankClient::new(bank);
let alice_pubkey = alice_keypair.pubkey();
// Initialize BudgetState
let budget_pubkey = Pubkey::new_rand();
let bob_pubkey = Pubkey::new_rand();
let witness = Pubkey::new_rand();
let instructions = budget_instruction::when_signed(
&alice_pubkey,
&bob_pubkey,
&budget_pubkey,
&witness,
None,
1,
);
let message = Message::new(instructions);
bank_client
.send_message(&[&alice_keypair], message)
.unwrap();
// Attack! Part 1: Sign a witness transaction with a random key.
let mallory_keypair = Keypair::new();
let mallory_pubkey = mallory_keypair.pubkey();
bank_client
.transfer(1, &alice_keypair, &mallory_pubkey)
.unwrap();
let instruction =
budget_instruction::apply_signature(&mallory_pubkey, &budget_pubkey, &bob_pubkey);
let mut message = Message::new(vec![instruction]);
// Attack! Part 2: Point the instruction to the expected, but unsigned, key.
message.account_keys.push(alice_pubkey);
message.instructions[0].accounts[0] = 3;
// Ensure the transaction fails because of the unsigned key.
assert_eq!(
bank_client
.send_message(&[&mallory_keypair], message)
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature)
);
}
#[test]
fn test_unsigned_timestamp() {
let (bank, alice_keypair) = create_bank(10_000);
let bank_client = BankClient::new(bank);
let alice_pubkey = alice_keypair.pubkey();
// Initialize BudgetState
let budget_pubkey = Pubkey::new_rand();
let bob_pubkey = Pubkey::new_rand();
let dt = Utc::now();
let instructions = budget_instruction::on_date(
&alice_pubkey,
&bob_pubkey,
&budget_pubkey,
dt,
&alice_pubkey,
None,
1,
);
let message = Message::new(instructions);
bank_client
.send_message(&[&alice_keypair], message)
.unwrap();
// Attack! Part 1: Sign a timestamp transaction with a random key.
let mallory_keypair = Keypair::new();
let mallory_pubkey = mallory_keypair.pubkey();
bank_client
.transfer(1, &alice_keypair, &mallory_pubkey)
.unwrap();
let instruction =
budget_instruction::apply_timestamp(&mallory_pubkey, &budget_pubkey, &bob_pubkey, dt);
let mut message = Message::new(vec![instruction]);
// Attack! Part 2: Point the instruction to the expected, but unsigned, key.
message.account_keys.push(alice_pubkey);
message.instructions[0].accounts[0] = 3;
// Ensure the transaction fails because of the unsigned key.
assert_eq!(
bank_client
.send_message(&[&mallory_keypair], message)
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature)
);
}
#[test]
fn test_pay_on_date() {
let (bank, alice_keypair) = create_bank(2);
let bank_client = BankClient::new(bank);
let alice_pubkey = alice_keypair.pubkey();
let budget_pubkey = Pubkey::new_rand();
let bob_pubkey = Pubkey::new_rand();
let mallory_pubkey = Pubkey::new_rand();
let dt = Utc::now();
let instructions = budget_instruction::on_date(
&alice_pubkey,
&bob_pubkey,
&budget_pubkey,
dt,
&alice_pubkey,
None,
1,
);
let message = Message::new(instructions);
bank_client
.send_message(&[&alice_keypair], message)
.unwrap();
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 1);
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());
// Attack! Try to payout to mallory_pubkey
let instruction =
budget_instruction::apply_timestamp(&alice_pubkey, &budget_pubkey, &mallory_pubkey, dt);
assert_eq!(
bank_client
.send_instruction(&alice_keypair, instruction)
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(
0,
InstructionError::CustomError(BudgetError::DestinationMissing as u32)
)
);
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 1);
assert_eq!(bank_client.get_balance(&bob_pubkey).unwrap(), 0);
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());
// Now, acknowledge the time in the condition occurred and
// that pubkey's funds are now available.
let instruction =
budget_instruction::apply_timestamp(&alice_pubkey, &budget_pubkey, &bob_pubkey, dt);
bank_client
.send_instruction(&alice_keypair, instruction)
.unwrap();
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 0);
assert_eq!(bank_client.get_balance(&bob_pubkey).unwrap(), 1);
assert_eq!(bank_client.get_account_data(&budget_pubkey).unwrap(), None);
}
#[test]
fn test_cancel_payment() {
let (bank, alice_keypair) = create_bank(3);
let bank_client = BankClient::new(bank);
let alice_pubkey = alice_keypair.pubkey();
let budget_pubkey = Pubkey::new_rand();
let bob_pubkey = Pubkey::new_rand();
let dt = Utc::now();
let instructions = budget_instruction::on_date(
&alice_pubkey,
&bob_pubkey,
&budget_pubkey,
dt,
&alice_pubkey,
Some(alice_pubkey),
1,
);
let message = Message::new(instructions);
bank_client
.send_message(&[&alice_keypair], message)
.unwrap();
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 2);
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 1);
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());
// Attack! try to put the lamports into the wrong account with cancel
let mallory_keypair = Keypair::new();
let mallory_pubkey = mallory_keypair.pubkey();
bank_client
.transfer(1, &alice_keypair, &mallory_pubkey)
.unwrap();
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
let instruction =
budget_instruction::apply_signature(&mallory_pubkey, &budget_pubkey, &bob_pubkey);
bank_client
.send_instruction(&mallory_keypair, instruction)
.unwrap();
// nothing should be changed because apply witness didn't finalize a payment
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 1);
assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None);
// Now, cancel the transaction. mint gets her funds back
let instruction =
budget_instruction::apply_signature(&alice_pubkey, &budget_pubkey, &alice_pubkey);
bank_client
.send_instruction(&alice_keypair, instruction)
.unwrap();
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 2);
assert_eq!(bank_client.get_account_data(&budget_pubkey).unwrap(), None);
assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None);
}
}

View File

@ -0,0 +1,63 @@
//! budget state
use crate::budget_expr::BudgetExpr;
use bincode::{self, deserialize, serialize_into};
use serde_derive::{Deserialize, Serialize};
use solana_sdk::instruction::InstructionError;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum BudgetError {
DestinationMissing,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
pub struct BudgetState {
pub initialized: bool,
pub pending_budget: Option<BudgetExpr>,
}
impl BudgetState {
pub fn new(budget_expr: BudgetExpr) -> Self {
Self {
initialized: true,
pending_budget: Some(budget_expr),
}
}
pub fn is_pending(&self) -> bool {
self.pending_budget.is_some()
}
pub fn serialize(&self, output: &mut [u8]) -> Result<(), InstructionError> {
serialize_into(output, self).map_err(|_| InstructionError::AccountDataTooSmall)
}
pub fn deserialize(input: &[u8]) -> Result<Self, InstructionError> {
deserialize(input).map_err(|_| InstructionError::InvalidAccountData)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::id;
use solana_sdk::account::Account;
#[test]
fn test_serializer() {
let mut a = Account::new(0, 512, &id());
let b = BudgetState::default();
b.serialize(&mut a.data).unwrap();
let c = BudgetState::deserialize(&a.data).unwrap();
assert_eq!(b, c);
}
#[test]
fn test_serializer_data_too_small() {
let mut a = Account::new(0, 1, &id());
let b = BudgetState::default();
assert_eq!(
b.serialize(&mut a.data),
Err(InstructionError::AccountDataTooSmall)
);
}
}

View File

@ -0,0 +1,19 @@
pub mod budget_expr;
pub mod budget_instruction;
pub mod budget_processor;
pub mod budget_state;
use solana_sdk::pubkey::Pubkey;
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,
];
pub fn id() -> Pubkey {
Pubkey::new(&BUDGET_PROGRAM_ID)
}
pub fn check_id(program_id: &Pubkey) -> bool {
program_id.as_ref() == BUDGET_PROGRAM_ID
}