Remove budget program (#17816)
This commit is contained in:
parent
e76c275867
commit
a66566e75b
|
@ -4306,22 +4306,6 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-budget-program"
|
|
||||||
version = "1.8.0"
|
|
||||||
dependencies = [
|
|
||||||
"bincode",
|
|
||||||
"chrono",
|
|
||||||
"log 0.4.11",
|
|
||||||
"num-derive",
|
|
||||||
"num-traits",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"solana-runtime",
|
|
||||||
"solana-sdk",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solana-cargo-build-bpf"
|
name = "solana-cargo-build-bpf"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -4740,7 +4724,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"solana-budget-program",
|
|
||||||
"solana-clap-utils",
|
"solana-clap-utils",
|
||||||
"solana-cli-config",
|
"solana-cli-config",
|
||||||
"solana-exchange-program",
|
"solana-exchange-program",
|
||||||
|
@ -4888,7 +4871,6 @@ dependencies = [
|
||||||
"sha2 0.9.5",
|
"sha2 0.9.5",
|
||||||
"solana-account-decoder",
|
"solana-account-decoder",
|
||||||
"solana-bpf-loader-program",
|
"solana-bpf-loader-program",
|
||||||
"solana-budget-program",
|
|
||||||
"solana-frozen-abi 1.8.0",
|
"solana-frozen-abi 1.8.0",
|
||||||
"solana-frozen-abi-macro 1.8.0",
|
"solana-frozen-abi-macro 1.8.0",
|
||||||
"solana-logger 1.8.0",
|
"solana-logger 1.8.0",
|
||||||
|
@ -5132,11 +5114,11 @@ dependencies = [
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"solana-budget-program",
|
|
||||||
"solana-logger 1.8.0",
|
"solana-logger 1.8.0",
|
||||||
"solana-metrics",
|
"solana-metrics",
|
||||||
"solana-rayon-threadlimit",
|
"solana-rayon-threadlimit",
|
||||||
"solana-sdk",
|
"solana-sdk",
|
||||||
|
"solana-stake-program",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -44,7 +44,6 @@ members = [
|
||||||
"program-test",
|
"program-test",
|
||||||
"programs/secp256k1",
|
"programs/secp256k1",
|
||||||
"programs/bpf_loader",
|
"programs/bpf_loader",
|
||||||
"programs/budget",
|
|
||||||
"programs/config",
|
"programs/config",
|
||||||
"programs/exchange",
|
"programs/exchange",
|
||||||
"programs/failure",
|
"programs/failure",
|
||||||
|
|
|
@ -16,7 +16,6 @@ chrono = "0.4"
|
||||||
serde = "1.0.126"
|
serde = "1.0.126"
|
||||||
serde_json = "1.0.64"
|
serde_json = "1.0.64"
|
||||||
serde_yaml = "0.8.13"
|
serde_yaml = "0.8.13"
|
||||||
solana-budget-program = { path = "../programs/budget", version = "=1.8.0" }
|
|
||||||
solana-clap-utils = { path = "../clap-utils", version = "=1.8.0" }
|
solana-clap-utils = { path = "../clap-utils", version = "=1.8.0" }
|
||||||
solana-cli-config = { path = "../cli-config", version = "=1.8.0" }
|
solana-cli-config = { path = "../cli-config", version = "=1.8.0" }
|
||||||
solana-exchange-program = { path = "../programs/exchange", version = "=1.8.0" }
|
solana-exchange-program = { path = "../programs/exchange", version = "=1.8.0" }
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
//! A command-line executable for generating the chain's genesis config.
|
//! A command-line executable for generating the chain's genesis config.
|
||||||
#![allow(clippy::integer_arithmetic)]
|
#![allow(clippy::integer_arithmetic)]
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate solana_budget_program;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate solana_exchange_program;
|
extern crate solana_exchange_program;
|
||||||
|
|
||||||
|
@ -492,7 +490,7 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||||
);
|
);
|
||||||
|
|
||||||
let native_instruction_processors = if cluster_type == ClusterType::Development {
|
let native_instruction_processors = if cluster_type == ClusterType::Development {
|
||||||
vec![solana_budget_program!(), solana_exchange_program!()]
|
vec![solana_exchange_program!()]
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
|
@ -67,7 +67,7 @@ features = ["lz4"]
|
||||||
assert_matches = "1.3.0"
|
assert_matches = "1.3.0"
|
||||||
matches = "0.1.6"
|
matches = "0.1.6"
|
||||||
solana-account-decoder = { path = "../account-decoder", version = "=1.8.0" }
|
solana-account-decoder = { path = "../account-decoder", version = "=1.8.0" }
|
||||||
solana-budget-program = { path = "../programs/budget", version = "=1.8.0" }
|
solana-stake-program = { path = "../programs/stake", version = "=1.8.0" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
rustc_version = "0.2"
|
rustc_version = "0.2"
|
||||||
|
|
|
@ -724,40 +724,16 @@ pub fn next_entry(prev_hash: &Hash, num_hashes: u64, transactions: Vec<Transacti
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::entry::Entry;
|
use crate::entry::Entry;
|
||||||
use chrono::prelude::Utc;
|
|
||||||
use solana_budget_program::budget_instruction;
|
|
||||||
use solana_sdk::{
|
use solana_sdk::{
|
||||||
hash::{hash, new_rand as hash_new_rand, Hash},
|
hash::{hash, new_rand as hash_new_rand, Hash},
|
||||||
message::Message,
|
message::Message,
|
||||||
packet::PACKET_DATA_SIZE,
|
packet::PACKET_DATA_SIZE,
|
||||||
|
pubkey::Pubkey,
|
||||||
signature::{Keypair, Signer},
|
signature::{Keypair, Signer},
|
||||||
system_transaction,
|
system_instruction, system_transaction,
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn create_sample_payment(keypair: &Keypair, hash: Hash) -> Transaction {
|
|
||||||
let pubkey = keypair.pubkey();
|
|
||||||
let budget_contract = Keypair::new();
|
|
||||||
let budget_pubkey = budget_contract.pubkey();
|
|
||||||
let ixs = budget_instruction::payment(&pubkey, &pubkey, &budget_pubkey, 1);
|
|
||||||
let message = Message::new(&ixs, Some(&pubkey));
|
|
||||||
Transaction::new(&[keypair, &budget_contract], message, hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_sample_timestamp(keypair: &Keypair, hash: Hash) -> Transaction {
|
|
||||||
let pubkey = keypair.pubkey();
|
|
||||||
let ix = budget_instruction::apply_timestamp(&pubkey, &pubkey, &pubkey, Utc::now());
|
|
||||||
let message = Message::new(&[ix], Some(&pubkey));
|
|
||||||
Transaction::new(&[keypair], message, hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_sample_apply_signature(keypair: &Keypair, hash: Hash) -> Transaction {
|
|
||||||
let pubkey = keypair.pubkey();
|
|
||||||
let ix = budget_instruction::apply_signature(&pubkey, &pubkey, &pubkey);
|
|
||||||
let message = Message::new(&[ix], Some(&pubkey));
|
|
||||||
Transaction::new(&[keypair], message, hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_entry_verify() {
|
fn test_entry_verify() {
|
||||||
let zero = Hash::default();
|
let zero = Hash::default();
|
||||||
|
@ -819,23 +795,6 @@ mod tests {
|
||||||
assert!(e0.verify(&zero));
|
assert!(e0.verify(&zero));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_witness_reorder_attack() {
|
|
||||||
let zero = Hash::default();
|
|
||||||
|
|
||||||
// First, verify entries
|
|
||||||
let keypair = Keypair::new();
|
|
||||||
let tx0 = create_sample_timestamp(&keypair, zero);
|
|
||||||
let tx1 = create_sample_apply_signature(&keypair, zero);
|
|
||||||
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()]);
|
|
||||||
assert!(e0.verify(&zero));
|
|
||||||
|
|
||||||
// Next, swap two witness transactions and ensure verification fails.
|
|
||||||
e0.transactions[0] = tx1; // <-- attack
|
|
||||||
e0.transactions[1] = tx0;
|
|
||||||
assert!(!e0.verify(&zero));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_next_entry() {
|
fn test_next_entry() {
|
||||||
let zero = Hash::default();
|
let zero = Hash::default();
|
||||||
|
@ -848,7 +807,7 @@ mod tests {
|
||||||
assert_eq!(tick.hash, zero);
|
assert_eq!(tick.hash, zero);
|
||||||
|
|
||||||
let keypair = Keypair::new();
|
let keypair = Keypair::new();
|
||||||
let tx0 = create_sample_timestamp(&keypair, zero);
|
let tx0 = system_transaction::transfer(&keypair, &Pubkey::new_unique(), 42, zero);
|
||||||
let entry0 = next_entry(&zero, 1, vec![tx0.clone()]);
|
let entry0 = next_entry(&zero, 1, vec![tx0.clone()]);
|
||||||
assert_eq!(entry0.num_hashes, 1);
|
assert_eq!(entry0.num_hashes, 1);
|
||||||
assert_eq!(entry0.hash, next_hash(&zero, 1, &[tx0]));
|
assert_eq!(entry0.hash, next_hash(&zero, 1, &[tx0]));
|
||||||
|
@ -904,9 +863,10 @@ mod tests {
|
||||||
let zero = Hash::default();
|
let zero = Hash::default();
|
||||||
let one = hash(&zero.as_ref());
|
let one = hash(&zero.as_ref());
|
||||||
let two = hash(&one.as_ref());
|
let two = hash(&one.as_ref());
|
||||||
let alice_pubkey = Keypair::new();
|
let alice_keypair = Keypair::new();
|
||||||
let tx0 = create_sample_payment(&alice_pubkey, one);
|
let bob_keypair = Keypair::new();
|
||||||
let tx1 = create_sample_timestamp(&alice_pubkey, one);
|
let tx0 = system_transaction::transfer(&alice_keypair, &bob_keypair.pubkey(), 1, one);
|
||||||
|
let tx1 = system_transaction::transfer(&bob_keypair, &alice_keypair.pubkey(), 1, one);
|
||||||
assert!(vec![][..].verify(&one)); // base case
|
assert!(vec![][..].verify(&one)); // base case
|
||||||
assert!(vec![next_entry(&one, 1, vec![tx0.clone()])][..].verify(&one)); // singleton case 1
|
assert!(vec![next_entry(&one, 1, vec![tx0.clone()])][..].verify(&one)); // singleton case 1
|
||||||
assert!(!vec![next_entry(&one, 1, vec![tx0.clone()])][..].verify(&two)); // singleton case 2, bad
|
assert!(!vec![next_entry(&one, 1, vec![tx0.clone()])][..].verify(&two)); // singleton case 2, bad
|
||||||
|
@ -931,17 +891,14 @@ mod tests {
|
||||||
let recent_blockhash = hash_new_rand(&mut rng);
|
let recent_blockhash = hash_new_rand(&mut rng);
|
||||||
let keypair = Keypair::new();
|
let keypair = Keypair::new();
|
||||||
let pubkey = keypair.pubkey();
|
let pubkey = keypair.pubkey();
|
||||||
let budget_contract = Keypair::new();
|
|
||||||
let budget_pubkey = budget_contract.pubkey();
|
|
||||||
let make_transaction = |size| {
|
let make_transaction = |size| {
|
||||||
let ixs: Vec<_> = std::iter::repeat_with(|| {
|
let ixs: Vec<_> = std::iter::repeat_with(|| {
|
||||||
budget_instruction::payment(&pubkey, &pubkey, &budget_pubkey, 1)
|
system_instruction::transfer(&pubkey, &Pubkey::new_unique(), 1)
|
||||||
})
|
})
|
||||||
.take(size)
|
.take(size)
|
||||||
.flat_map(|x| x.into_iter())
|
|
||||||
.collect();
|
.collect();
|
||||||
let message = Message::new(&ixs[..], Some(&pubkey));
|
let message = Message::new(&ixs[..], Some(&pubkey));
|
||||||
Transaction::new(&[&keypair, &budget_contract], message, recent_blockhash)
|
Transaction::new(&[&keypair], message, recent_blockhash)
|
||||||
};
|
};
|
||||||
// Small transaction.
|
// Small transaction.
|
||||||
{
|
{
|
||||||
|
@ -954,7 +911,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
// Big transaction.
|
// Big transaction.
|
||||||
{
|
{
|
||||||
let tx = make_transaction(15);
|
let tx = make_transaction(25);
|
||||||
let entries = vec![next_entry(&recent_blockhash, 1, vec![tx.clone()])];
|
let entries = vec![next_entry(&recent_blockhash, 1, vec![tx.clone()])];
|
||||||
assert!(bincode::serialized_size(&tx).unwrap() > PACKET_DATA_SIZE as u64);
|
assert!(bincode::serialized_size(&tx).unwrap() > PACKET_DATA_SIZE as u64);
|
||||||
assert!(entries[..]
|
assert!(entries[..]
|
||||||
|
@ -963,7 +920,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
// Assert that verify fails as soon as serialized
|
// Assert that verify fails as soon as serialized
|
||||||
// size exceeds packet data size.
|
// size exceeds packet data size.
|
||||||
for size in 1..20 {
|
for size in 1..30 {
|
||||||
let tx = make_transaction(size);
|
let tx = make_transaction(size);
|
||||||
let entries = vec![next_entry(&recent_blockhash, 1, vec![tx.clone()])];
|
let entries = vec![next_entry(&recent_blockhash, 1, vec![tx.clone()])];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -10,20 +10,20 @@ documentation = "https://docs.rs/solana-perf"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.7.0"
|
|
||||||
dlopen = "0.1.8"
|
|
||||||
bincode = "1.3.1"
|
bincode = "1.3.1"
|
||||||
rayon = "1.5.0"
|
curve25519-dalek = { version = "2" }
|
||||||
serde = "1.0.126"
|
dlopen = "0.1.8"
|
||||||
dlopen_derive = "0.1.4"
|
dlopen_derive = "0.1.4"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
solana-sdk = { path = "../sdk", version = "=1.8.0" }
|
rand = "0.7.0"
|
||||||
solana-rayon-threadlimit = { path = "../rayon-threadlimit", version = "=1.8.0" }
|
rayon = "1.5.0"
|
||||||
solana-budget-program = { path = "../programs/budget", version = "=1.8.0" }
|
serde = "1.0.126"
|
||||||
solana-logger = { path = "../logger", version = "=1.8.0" }
|
solana-logger = { path = "../logger", version = "=1.8.0" }
|
||||||
solana-metrics = { path = "../metrics", version = "=1.8.0" }
|
solana-metrics = { path = "../metrics", version = "=1.8.0" }
|
||||||
curve25519-dalek = { version = "2" }
|
solana-sdk = { path = "../sdk", version = "=1.8.0" }
|
||||||
|
solana-rayon-threadlimit = { path = "../rayon-threadlimit", version = "=1.8.0" }
|
||||||
|
solana-stake-program = { path = "../programs/stake", version = "=1.8.0" }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "solana_perf"
|
name = "solana_perf"
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub fn test_multisig_tx() -> Transaction {
|
||||||
|
|
||||||
let transfer_instruction = SystemInstruction::Transfer { lamports };
|
let transfer_instruction = SystemInstruction::Transfer { lamports };
|
||||||
|
|
||||||
let program_ids = vec![system_program::id(), solana_budget_program::id()];
|
let program_ids = vec![system_program::id(), solana_stake_program::id()];
|
||||||
|
|
||||||
let instructions = vec![CompiledInstruction::new(
|
let instructions = vec![CompiledInstruction::new(
|
||||||
0,
|
0,
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "solana-budget-program"
|
|
||||||
version = "1.8.0"
|
|
||||||
description = "Solana Budget program"
|
|
||||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
|
||||||
repository = "https://github.com/solana-labs/solana"
|
|
||||||
license = "Apache-2.0"
|
|
||||||
homepage = "https://solana.com/"
|
|
||||||
documentation = "https://docs.rs/solana-budget-program"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bincode = "1.3.1"
|
|
||||||
chrono = { version = "0.4.11", features = ["serde"] }
|
|
||||||
log = "0.4.11"
|
|
||||||
num-derive = "0.3"
|
|
||||||
num-traits = "0.2"
|
|
||||||
serde = "1.0.126"
|
|
||||||
serde_derive = "1.0.103"
|
|
||||||
solana-sdk = { path = "../../sdk", version = "=1.8.0" }
|
|
||||||
thiserror = "1.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
solana-runtime = { path = "../../runtime", version = "=1.8.0" }
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["lib", "cdylib"]
|
|
||||||
name = "solana_budget_program"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
targets = ["x86_64-unknown-linux-gnu"]
|
|
|
@ -1,390 +0,0 @@
|
||||||
//! The `budget_expr` module provides a domain-specific language for payment 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::hash::Hash;
|
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
|
|
||||||
/// Account snapshot.
|
|
||||||
AccountData(Hash, Pubkey),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
/// Wait for a `Timestamp` `Witness` at or after the given `DateTime`.
|
|
||||||
Timestamp(DateTime<Utc>, Pubkey),
|
|
||||||
|
|
||||||
/// Wait for a `Signature` `Witness` from `Pubkey`.
|
|
||||||
Signature(Pubkey),
|
|
||||||
|
|
||||||
/// Wait for the account with the given constraints.
|
|
||||||
AccountData(AccountConstraints),
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
(
|
|
||||||
Condition::AccountData(constraints),
|
|
||||||
Witness::AccountData(actual_hash, program_id),
|
|
||||||
) => {
|
|
||||||
constraints.program_id == *program_id
|
|
||||||
&& constraints.key == *from
|
|
||||||
&& constraints.data_hash == *actual_hash
|
|
||||||
}
|
|
||||||
_ => 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 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(
|
|
||||||
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 {
|
|
||||||
*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 = solana_sdk::pubkey::new_rand();
|
|
||||||
let to = solana_sdk::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 = solana_sdk::pubkey::new_rand();
|
|
||||||
let to = solana_sdk::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 = solana_sdk::pubkey::new_rand();
|
|
||||||
let from1 = solana_sdk::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 = solana_sdk::pubkey::new_rand();
|
|
||||||
let from1 = solana_sdk::pubkey::new_rand();
|
|
||||||
let from2 = solana_sdk::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 = solana_sdk::pubkey::new_rand();
|
|
||||||
let from1 = solana_sdk::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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,195 +0,0 @@
|
||||||
use crate::{budget_expr::BudgetExpr, budget_state::BudgetState, id};
|
|
||||||
use bincode::serialized_size;
|
|
||||||
use chrono::prelude::{DateTime, Utc};
|
|
||||||
use num_derive::{FromPrimitive, ToPrimitive};
|
|
||||||
use serde_derive::{Deserialize, Serialize};
|
|
||||||
use solana_sdk::{
|
|
||||||
decode_error::DecodeError,
|
|
||||||
hash::Hash,
|
|
||||||
instruction::{AccountMeta, Instruction},
|
|
||||||
pubkey::Pubkey,
|
|
||||||
system_instruction,
|
|
||||||
};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug, Clone, PartialEq, FromPrimitive, ToPrimitive)]
|
|
||||||
pub enum BudgetError {
|
|
||||||
#[error("destination missing")]
|
|
||||||
DestinationMissing,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> DecodeError<T> for BudgetError {
|
|
||||||
fn type_of() -> &'static str {
|
|
||||||
"BudgetError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An instruction to progress the smart contract.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum BudgetInstruction {
|
|
||||||
/// Declare and instantiate `BudgetExpr`.
|
|
||||||
InitializeAccount(Box<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,
|
|
||||||
|
|
||||||
/// Load an account and pass its data to the budget for inspection.
|
|
||||||
ApplyAccountData,
|
|
||||||
}
|
|
||||||
|
|
||||||
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_with_bincode(
|
|
||||||
id(),
|
|
||||||
&BudgetInstruction::InitializeAccount(Box::new(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, contract: &Pubkey, lamports: u64) -> Vec<Instruction> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<Instruction> {
|
|
||||||
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,
|
|
||||||
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_with_bincode(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_with_bincode(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_readonly(*witness_pubkey, false),
|
|
||||||
AccountMeta::new(*contract, false),
|
|
||||||
AccountMeta::new(*to, false),
|
|
||||||
];
|
|
||||||
Instruction::new_with_bincode(id(), &BudgetInstruction::ApplyAccountData, account_metas)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::budget_expr::BudgetExpr;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_budget_instruction_verify() {
|
|
||||||
let alice_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
let budget_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
payment(&alice_pubkey, &bob_pubkey, &budget_pubkey, 1); // No panic! indicates success.
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_budget_instruction_overspend() {
|
|
||||||
let alice_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
let budget_pubkey = solana_sdk::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 = solana_sdk::pubkey::new_rand();
|
|
||||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
let budget_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
let expr = BudgetExpr::new_payment(1, &bob_pubkey);
|
|
||||||
create_account(&alice_pubkey, &budget_pubkey, 2, expr);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,627 +0,0 @@
|
||||||
//! budget program
|
|
||||||
use crate::{
|
|
||||||
budget_expr::Witness,
|
|
||||||
budget_instruction::{BudgetError, BudgetInstruction},
|
|
||||||
budget_state::BudgetState,
|
|
||||||
};
|
|
||||||
use chrono::prelude::{DateTime, Utc};
|
|
||||||
use log::*;
|
|
||||||
use solana_sdk::{
|
|
||||||
account::{ReadableAccount, WritableAccount},
|
|
||||||
hash::hash,
|
|
||||||
instruction::InstructionError,
|
|
||||||
keyed_account::{keyed_account_at_index, KeyedAccount},
|
|
||||||
process_instruction::InvokeContext,
|
|
||||||
program_utils::limited_deserialize,
|
|
||||||
pubkey::Pubkey,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Process a Witness Signature. Any payment plans waiting on this signature
|
|
||||||
/// will progress one step.
|
|
||||||
fn apply_signature(
|
|
||||||
budget_state: &mut BudgetState,
|
|
||||||
witness_keyed_account: &KeyedAccount,
|
|
||||||
contract_keyed_account: &KeyedAccount,
|
|
||||||
to_keyed_account: Result<&KeyedAccount, InstructionError>,
|
|
||||||
) -> Result<(), InstructionError> {
|
|
||||||
let mut final_payment = None;
|
|
||||||
if let Some(ref mut expr) = budget_state.pending_budget {
|
|
||||||
let key = witness_keyed_account.signer_key().unwrap();
|
|
||||||
expr.apply_witness(&Witness::Signature, key);
|
|
||||||
final_payment = expr.final_payment();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(payment) = final_payment {
|
|
||||||
if let Some(key) = witness_keyed_account.signer_key() {
|
|
||||||
if &payment.to == key {
|
|
||||||
budget_state.pending_budget = None;
|
|
||||||
contract_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.checked_sub_lamports(payment.lamports)?;
|
|
||||||
witness_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.checked_add_lamports(payment.lamports)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let to_keyed_account = to_keyed_account?;
|
|
||||||
if &payment.to != to_keyed_account.unsigned_key() {
|
|
||||||
trace!("destination missing");
|
|
||||||
return Err(BudgetError::DestinationMissing.into());
|
|
||||||
}
|
|
||||||
budget_state.pending_budget = None;
|
|
||||||
contract_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.checked_sub_lamports(payment.lamports)?;
|
|
||||||
to_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.checked_add_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,
|
|
||||||
witness_keyed_account: &KeyedAccount,
|
|
||||||
contract_keyed_account: &KeyedAccount,
|
|
||||||
to_keyed_account: Result<&KeyedAccount, InstructionError>,
|
|
||||||
dt: DateTime<Utc>,
|
|
||||||
) -> Result<(), InstructionError> {
|
|
||||||
// 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 = witness_keyed_account.signer_key().unwrap();
|
|
||||||
expr.apply_witness(&Witness::Timestamp(dt), key);
|
|
||||||
final_payment = expr.final_payment();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(payment) = final_payment {
|
|
||||||
let to_keyed_account = to_keyed_account?;
|
|
||||||
if &payment.to != to_keyed_account.unsigned_key() {
|
|
||||||
trace!("destination missing");
|
|
||||||
return Err(BudgetError::DestinationMissing.into());
|
|
||||||
}
|
|
||||||
budget_state.pending_budget = None;
|
|
||||||
contract_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.checked_sub_lamports(payment.lamports)?;
|
|
||||||
to_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.checked_add_lamports(payment.lamports)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process an AccountData Witness and any payment waiting on it.
|
|
||||||
fn apply_account_data(
|
|
||||||
budget_state: &mut BudgetState,
|
|
||||||
witness_keyed_account: &KeyedAccount,
|
|
||||||
contract_keyed_account: &KeyedAccount,
|
|
||||||
to_keyed_account: Result<&KeyedAccount, InstructionError>,
|
|
||||||
) -> Result<(), InstructionError> {
|
|
||||||
// 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 = witness_keyed_account.unsigned_key();
|
|
||||||
let program_id = witness_keyed_account.owner()?;
|
|
||||||
let actual_hash = hash(&witness_keyed_account.try_account_ref()?.data());
|
|
||||||
expr.apply_witness(&Witness::AccountData(actual_hash, program_id), key);
|
|
||||||
final_payment = expr.final_payment();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(payment) = final_payment {
|
|
||||||
let to_keyed_account = to_keyed_account?;
|
|
||||||
if &payment.to != to_keyed_account.unsigned_key() {
|
|
||||||
trace!("destination missing");
|
|
||||||
return Err(BudgetError::DestinationMissing.into());
|
|
||||||
}
|
|
||||||
budget_state.pending_budget = None;
|
|
||||||
contract_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.checked_sub_lamports(payment.lamports)?;
|
|
||||||
to_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.checked_add_lamports(payment.lamports)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process_instruction(
|
|
||||||
_program_id: &Pubkey,
|
|
||||||
data: &[u8],
|
|
||||||
invoke_context: &mut dyn InvokeContext,
|
|
||||||
) -> Result<(), InstructionError> {
|
|
||||||
let keyed_accounts = invoke_context.get_keyed_accounts()?;
|
|
||||||
|
|
||||||
let instruction = limited_deserialize(data)?;
|
|
||||||
|
|
||||||
trace!("process_instruction: {:?}", instruction);
|
|
||||||
|
|
||||||
match instruction {
|
|
||||||
BudgetInstruction::InitializeAccount(expr) => {
|
|
||||||
let contract_keyed_account = keyed_account_at_index(keyed_accounts, 0)?;
|
|
||||||
|
|
||||||
if let Some(payment) = expr.final_payment() {
|
|
||||||
let to_keyed_account = contract_keyed_account;
|
|
||||||
let contract_keyed_account = keyed_account_at_index(keyed_accounts, 1)?;
|
|
||||||
contract_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.set_lamports(0);
|
|
||||||
to_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.checked_add_lamports(payment.lamports)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let existing =
|
|
||||||
BudgetState::deserialize(&contract_keyed_account.try_account_ref_mut()?.data())
|
|
||||||
.ok();
|
|
||||||
if Some(true) == existing.map(|x| x.initialized) {
|
|
||||||
trace!("contract already exists");
|
|
||||||
return Err(InstructionError::AccountAlreadyInitialized);
|
|
||||||
}
|
|
||||||
let budget_state = BudgetState {
|
|
||||||
pending_budget: Some(*expr),
|
|
||||||
initialized: true,
|
|
||||||
};
|
|
||||||
budget_state.serialize(
|
|
||||||
&mut contract_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.data_as_mut_slice(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BudgetInstruction::ApplyTimestamp(dt) => {
|
|
||||||
let witness_keyed_account = keyed_account_at_index(keyed_accounts, 0)?;
|
|
||||||
let contract_keyed_account = keyed_account_at_index(keyed_accounts, 1)?;
|
|
||||||
let mut budget_state =
|
|
||||||
BudgetState::deserialize(&contract_keyed_account.try_account_ref()?.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 witness_keyed_account.signer_key().is_none() {
|
|
||||||
return Err(InstructionError::MissingRequiredSignature);
|
|
||||||
}
|
|
||||||
trace!("apply timestamp");
|
|
||||||
apply_timestamp(
|
|
||||||
&mut budget_state,
|
|
||||||
witness_keyed_account,
|
|
||||||
contract_keyed_account,
|
|
||||||
keyed_account_at_index(keyed_accounts, 2),
|
|
||||||
dt,
|
|
||||||
)?;
|
|
||||||
trace!("apply timestamp committed");
|
|
||||||
budget_state.serialize(
|
|
||||||
&mut contract_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.data_as_mut_slice(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BudgetInstruction::ApplySignature => {
|
|
||||||
let witness_keyed_account = keyed_account_at_index(keyed_accounts, 0)?;
|
|
||||||
let contract_keyed_account = keyed_account_at_index(keyed_accounts, 1)?;
|
|
||||||
let mut budget_state =
|
|
||||||
BudgetState::deserialize(&contract_keyed_account.try_account_ref()?.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 witness_keyed_account.signer_key().is_none() {
|
|
||||||
return Err(InstructionError::MissingRequiredSignature);
|
|
||||||
}
|
|
||||||
trace!("apply signature");
|
|
||||||
apply_signature(
|
|
||||||
&mut budget_state,
|
|
||||||
witness_keyed_account,
|
|
||||||
contract_keyed_account,
|
|
||||||
keyed_account_at_index(keyed_accounts, 2),
|
|
||||||
)?;
|
|
||||||
trace!("apply signature committed");
|
|
||||||
budget_state.serialize(
|
|
||||||
&mut contract_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.data_as_mut_slice(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BudgetInstruction::ApplyAccountData => {
|
|
||||||
let witness_keyed_account = keyed_account_at_index(keyed_accounts, 0)?;
|
|
||||||
let contract_keyed_account = keyed_account_at_index(keyed_accounts, 1)?;
|
|
||||||
let mut budget_state =
|
|
||||||
BudgetState::deserialize(&contract_keyed_account.try_account_ref()?.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,
|
|
||||||
witness_keyed_account,
|
|
||||||
contract_keyed_account,
|
|
||||||
keyed_account_at_index(keyed_accounts, 2),
|
|
||||||
)?;
|
|
||||||
trace!("apply account data committed");
|
|
||||||
budget_state.serialize(
|
|
||||||
&mut contract_keyed_account
|
|
||||||
.try_account_ref_mut()?
|
|
||||||
.data_as_mut_slice(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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::account::{Account, AccountSharedData};
|
|
||||||
use solana_sdk::client::SyncClient;
|
|
||||||
use solana_sdk::genesis_config::create_genesis_config;
|
|
||||||
use solana_sdk::hash::hash;
|
|
||||||
use solana_sdk::instruction::InstructionError;
|
|
||||||
use solana_sdk::message::Message;
|
|
||||||
use solana_sdk::signature::{Keypair, Signer};
|
|
||||||
use solana_sdk::transaction::TransactionError;
|
|
||||||
|
|
||||||
fn create_bank(lamports: u64) -> (Bank, Keypair) {
|
|
||||||
let (genesis_config, mint_keypair) = create_genesis_config(lamports);
|
|
||||||
let mut bank = Bank::new(&genesis_config);
|
|
||||||
bank.add_builtin("budget_program", id(), process_instruction);
|
|
||||||
(bank, mint_keypair)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_initialize_no_panic() {
|
|
||||||
let (bank, alice_keypair) = create_bank(1);
|
|
||||||
let bank_client = BankClient::new(bank);
|
|
||||||
|
|
||||||
let alice_pubkey = alice_keypair.pubkey();
|
|
||||||
let budget_keypair = Keypair::new();
|
|
||||||
let budget_pubkey = budget_keypair.pubkey();
|
|
||||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
|
|
||||||
let mut instructions =
|
|
||||||
budget_instruction::payment(&alice_pubkey, &bob_pubkey, &budget_pubkey, 1);
|
|
||||||
instructions[1].accounts = vec![]; // <!-- Attack! Prevent accounts from being passed into processor.
|
|
||||||
|
|
||||||
let message = Message::new(&instructions, Some(&alice_pubkey));
|
|
||||||
assert_eq!(
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_message(&[&alice_keypair, &budget_keypair], message)
|
|
||||||
.unwrap_err()
|
|
||||||
.unwrap(),
|
|
||||||
TransactionError::InstructionError(1, InstructionError::NotEnoughAccountKeys)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 = solana_sdk::pubkey::new_rand();
|
|
||||||
let budget_keypair = Keypair::new();
|
|
||||||
let budget_pubkey = budget_keypair.pubkey();
|
|
||||||
let instructions =
|
|
||||||
budget_instruction::payment(&alice_pubkey, &bob_pubkey, &budget_pubkey, 100);
|
|
||||||
let message = Message::new(&instructions, Some(&alice_pubkey));
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_message(&[&alice_keypair, &budget_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_keypair = Keypair::new();
|
|
||||||
let budget_pubkey = budget_keypair.pubkey();
|
|
||||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
let witness = solana_sdk::pubkey::new_rand();
|
|
||||||
let instructions = budget_instruction::when_signed(
|
|
||||||
&alice_pubkey,
|
|
||||||
&bob_pubkey,
|
|
||||||
&budget_pubkey,
|
|
||||||
&witness,
|
|
||||||
None,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
let message = Message::new(&instructions, Some(&alice_pubkey));
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_message(&[&alice_keypair, &budget_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_and_confirm(1, &alice_keypair, &mallory_pubkey)
|
|
||||||
.unwrap();
|
|
||||||
let instruction =
|
|
||||||
budget_instruction::apply_signature(&mallory_pubkey, &budget_pubkey, &bob_pubkey);
|
|
||||||
let mut message = Message::new(&[instruction], Some(&mallory_pubkey));
|
|
||||||
|
|
||||||
// Attack! Part 2: Point the instruction to the expected, but unsigned, key.
|
|
||||||
message.account_keys.insert(3, alice_pubkey);
|
|
||||||
message.instructions[0].accounts[0] = 3;
|
|
||||||
message.instructions[0].program_id_index = 4;
|
|
||||||
|
|
||||||
// Ensure the transaction fails because of the unsigned key.
|
|
||||||
assert_eq!(
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_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_keypair = Keypair::new();
|
|
||||||
let budget_pubkey = budget_keypair.pubkey();
|
|
||||||
let bob_pubkey = solana_sdk::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, Some(&alice_pubkey));
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_message(&[&alice_keypair, &budget_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_and_confirm(1, &alice_keypair, &mallory_pubkey)
|
|
||||||
.unwrap();
|
|
||||||
let instruction =
|
|
||||||
budget_instruction::apply_timestamp(&mallory_pubkey, &budget_pubkey, &bob_pubkey, dt);
|
|
||||||
let mut message = Message::new(&[instruction], Some(&mallory_pubkey));
|
|
||||||
|
|
||||||
// Attack! Part 2: Point the instruction to the expected, but unsigned, key.
|
|
||||||
message.account_keys.insert(3, alice_pubkey);
|
|
||||||
message.instructions[0].accounts[0] = 3;
|
|
||||||
message.instructions[0].program_id_index = 4;
|
|
||||||
|
|
||||||
// Ensure the transaction fails because of the unsigned key.
|
|
||||||
assert_eq!(
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_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_keypair = Keypair::new();
|
|
||||||
let budget_pubkey = budget_keypair.pubkey();
|
|
||||||
let bob_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
let mallory_pubkey = solana_sdk::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, Some(&alice_pubkey));
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_message(&[&alice_keypair, &budget_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_and_confirm_instruction(&alice_keypair, instruction)
|
|
||||||
.unwrap_err()
|
|
||||||
.unwrap(),
|
|
||||||
TransactionError::InstructionError(
|
|
||||||
0,
|
|
||||||
InstructionError::Custom(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_and_confirm_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_keypair = Keypair::new();
|
|
||||||
let budget_pubkey = budget_keypair.pubkey();
|
|
||||||
let bob_pubkey = solana_sdk::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, Some(&alice_pubkey));
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_message(&[&alice_keypair, &budget_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_and_confirm(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_and_confirm_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_and_confirm_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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pay_when_account_data() {
|
|
||||||
let (bank, alice_keypair) = create_bank(42);
|
|
||||||
let game_pubkey = solana_sdk::pubkey::new_rand();
|
|
||||||
let game_account = AccountSharedData::from(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_keypair = Keypair::new();
|
|
||||||
let budget_pubkey = budget_keypair.pubkey();
|
|
||||||
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_and_confirm(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, Some(&alice_pubkey));
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_message(&[&alice_keypair, &budget_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(&[instruction], Some(&bob_pubkey));
|
|
||||||
bank_client
|
|
||||||
.send_and_confirm_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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
//! 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, 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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
#![allow(clippy::integer_arithmetic)]
|
|
||||||
pub mod budget_expr;
|
|
||||||
pub mod budget_instruction;
|
|
||||||
pub mod budget_processor;
|
|
||||||
pub mod budget_state;
|
|
||||||
|
|
||||||
use crate::budget_processor::process_instruction;
|
|
||||||
|
|
||||||
solana_sdk::declare_program!(
|
|
||||||
"Budget1111111111111111111111111111111111111",
|
|
||||||
solana_budget_program,
|
|
||||||
process_instruction
|
|
||||||
);
|
|
Loading…
Reference in New Issue