This reverts commit 34344982a9
.
This commit is contained in:
24
programs/budget_api/Cargo.toml
Normal file
24
programs/budget_api/Cargo.toml
Normal 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"]
|
341
programs/budget_api/src/budget_expr.rs
Normal file
341
programs/budget_api/src/budget_expr.rs
Normal 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));
|
||||
}
|
||||
}
|
150
programs/budget_api/src/budget_instruction.rs
Normal file
150
programs/budget_api/src/budget_instruction.rs
Normal 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);
|
||||
}
|
||||
}
|
402
programs/budget_api/src/budget_processor.rs
Normal file
402
programs/budget_api/src/budget_processor.rs
Normal 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);
|
||||
}
|
||||
}
|
63
programs/budget_api/src/budget_state.rs
Normal file
63
programs/budget_api/src/budget_state.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
19
programs/budget_api/src/lib.rs
Normal file
19
programs/budget_api/src/lib.rs
Normal 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
|
||||
}
|
Reference in New Issue
Block a user