budget as separate contract and system call contract (#1189)
* budget and system contracts and verification * contract check_id methods * system call contract * verify contract execution rules * move system into its own file * allocate before transfer for budget * store error in budget context * budget contract and tests without bank * moved budget of of bank
This commit is contained in:
parent
072b244575
commit
6ec0e42220
434
src/bank.rs
434
src/bank.rs
|
@ -3,28 +3,59 @@
|
|||
//! on behalf of the caller, and a low-level API for when they have
|
||||
//! already been signed and verified.
|
||||
|
||||
use bincode::{deserialize, serialize};
|
||||
use chrono::prelude::*;
|
||||
use bincode::serialize;
|
||||
use budget_contract::BudgetContract;
|
||||
use counter::Counter;
|
||||
use entry::Entry;
|
||||
use hash::{hash, Hash};
|
||||
use instruction::Instruction;
|
||||
use itertools::Itertools;
|
||||
use ledger::Block;
|
||||
use log::Level;
|
||||
use mint::Mint;
|
||||
use payment_plan::{Payment, PaymentPlan, Witness};
|
||||
use payment_plan::{Payment, PaymentPlan};
|
||||
use signature::{Keypair, Pubkey, Signature};
|
||||
use std;
|
||||
use std::collections::hash_map::Entry::Occupied;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use std::result;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::RwLock;
|
||||
use std::time::Instant;
|
||||
use system_contract::SystemContract;
|
||||
use timing::{duration_as_us, timestamp};
|
||||
use transaction::{Instruction, Plan, Transaction};
|
||||
use transaction::Transaction;
|
||||
use window::WINDOW_SIZE;
|
||||
|
||||
/// An Account with userdata that is stored on chain
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Account {
|
||||
/// tokens in the account
|
||||
pub tokens: i64,
|
||||
/// user data
|
||||
/// A transaction can write to its userdata
|
||||
pub userdata: Vec<u8>,
|
||||
/// contract id this contract belongs to
|
||||
pub contract_id: Pubkey,
|
||||
}
|
||||
impl Account {
|
||||
pub fn new(tokens: i64, space: usize, contract_id: Pubkey) -> Account {
|
||||
Account {
|
||||
tokens,
|
||||
userdata: vec![0u8; space],
|
||||
contract_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Account {
|
||||
fn default() -> Self {
|
||||
Account {
|
||||
tokens: 0,
|
||||
userdata: vec![],
|
||||
contract_id: SystemContract::id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The number of most recent `last_id` values that the bank will track the signatures
|
||||
/// of. Once the bank discards a `last_id`, it will reject any transactions that use
|
||||
/// that `last_id` in a transaction. Lowering this value reduces memory consumption,
|
||||
|
@ -41,10 +72,8 @@ pub enum BankError {
|
|||
/// Attempt to debit from `Pubkey`, but no found no record of a prior credit.
|
||||
AccountNotFound(Pubkey),
|
||||
|
||||
/// The requested debit from `Pubkey` has the potential to draw the balance
|
||||
/// below zero. This can occur when a debit and credit are processed in parallel.
|
||||
/// The bank may reject the debit or push it to a future entry.
|
||||
InsufficientFunds(Pubkey),
|
||||
/// The from `Pubkey` does not have sufficient balance to pay the fee to schedule the transaction
|
||||
InsufficientFundsForFee(Pubkey),
|
||||
|
||||
/// The bank has seen `Signature` before. This can occur under normal operation
|
||||
/// when a UDP packet is duplicated, as a user error from a client not updating
|
||||
|
@ -55,28 +84,27 @@ pub enum BankError {
|
|||
/// the `last_id` has been discarded.
|
||||
LastIdNotFound(Hash),
|
||||
|
||||
/// The transaction is invalid and has requested a debit or credit of negative
|
||||
/// tokens.
|
||||
NegativeTokens,
|
||||
|
||||
/// Proof of History verification failed.
|
||||
LedgerVerificationFailed,
|
||||
/// Contract's transaction token balance does not equal the balance after the transaction
|
||||
UnbalancedTransaction(Signature),
|
||||
/// Contract location Pubkey already contains userdata
|
||||
ContractAlreadyPending(Pubkey),
|
||||
/// Contract's transactions resulted in an account with a negative balance
|
||||
/// The difference from InsufficientFundsForFee is that the transaction was executed by the
|
||||
/// contract
|
||||
ResultWithNegativeTokens(Signature),
|
||||
|
||||
/// Contract id is unknown
|
||||
UnknownContractId(Pubkey),
|
||||
|
||||
/// Contract modified an accounts contract id
|
||||
ModifiedContractId(Signature),
|
||||
|
||||
/// Contract spent the tokens of an account that doesn't belong to it
|
||||
ExternalAccountTokenSpend(Signature),
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, BankError>;
|
||||
/// An Account with userdata that is stored on chain
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct Account {
|
||||
/// tokens in the account
|
||||
pub tokens: i64,
|
||||
/// user data
|
||||
/// A transaction can write to its userdata
|
||||
pub userdata: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ErrorCounters {
|
||||
account_not_found_validator: usize,
|
||||
|
@ -232,104 +260,7 @@ impl Bank {
|
|||
last_ids.push_back(*last_id);
|
||||
}
|
||||
|
||||
/// Deduct tokens from the source account if it has sufficient funds and the contract isn't
|
||||
/// pending
|
||||
fn apply_debits_to_budget_payment_plan(
|
||||
tx: &Transaction,
|
||||
accounts: &mut [Account],
|
||||
instruction: &Instruction,
|
||||
) -> Result<()> {
|
||||
{
|
||||
let tokens = if !accounts[0].userdata.is_empty() {
|
||||
0
|
||||
} else {
|
||||
accounts[0].tokens
|
||||
};
|
||||
if let Instruction::NewContract(contract) = &instruction {
|
||||
if contract.tokens < 0 {
|
||||
return Err(BankError::NegativeTokens);
|
||||
}
|
||||
|
||||
if tokens < contract.tokens {
|
||||
return Err(BankError::InsufficientFunds(tx.keys[0]));
|
||||
} else {
|
||||
let bal = &mut accounts[0];
|
||||
bal.tokens -= contract.tokens;
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply only a transaction's credits.
|
||||
/// Note: It is safe to apply credits from multiple transactions in parallel.
|
||||
fn apply_credits_to_budget_payment_plan(
|
||||
tx: &Transaction,
|
||||
accounts: &mut [Account],
|
||||
instruction: &Instruction,
|
||||
) -> Result<()> {
|
||||
match instruction {
|
||||
Instruction::NewContract(contract) => {
|
||||
let plan = contract.plan.clone();
|
||||
if let Some(payment) = plan.final_payment() {
|
||||
Self::apply_payment(&payment, &mut accounts[1]);
|
||||
Ok(())
|
||||
} else if !accounts[1].userdata.is_empty() {
|
||||
Err(BankError::ContractAlreadyPending(tx.keys[1]))
|
||||
} else {
|
||||
let mut pending = HashMap::new();
|
||||
pending.insert(tx.signature, plan);
|
||||
//TODO this is a temporary on demand allocation
|
||||
//until system contract requires explicit allocation of memory
|
||||
accounts[1].userdata = serialize(&pending).unwrap();
|
||||
accounts[1].tokens += contract.tokens;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Instruction::ApplyTimestamp(dt) => {
|
||||
Self::apply_timestamp(tx.keys[0], *dt, &mut accounts[1]);
|
||||
Ok(())
|
||||
}
|
||||
Instruction::ApplySignature(signature) => {
|
||||
Self::apply_signature(tx.keys[0], *signature, accounts);
|
||||
Ok(())
|
||||
}
|
||||
Instruction::NewVote(_vote) => {
|
||||
// TODO: record the vote in the stake table...
|
||||
trace!("GOT VOTE! last_id={}", tx.last_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Budget DSL contract interface
|
||||
/// * tx - the transaction
|
||||
/// * accounts[0] - The source of the tokens
|
||||
/// * accounts[1] - The contract context. Once the contract has been completed, the tokens can
|
||||
/// be spent from this account .
|
||||
pub fn process_transaction_of_budget_instruction(
|
||||
tx: &Transaction,
|
||||
accounts: &mut [Account],
|
||||
) -> Result<()> {
|
||||
let instruction = tx.instruction();
|
||||
Self::apply_debits_to_budget_payment_plan(tx, accounts, &instruction)?;
|
||||
Self::apply_credits_to_budget_payment_plan(tx, accounts, &instruction)
|
||||
}
|
||||
//TODO the contract needs to provide a "get_balance" introspection call of the userdata
|
||||
pub fn get_balance_of_budget_payment_plan(account: &Account) -> i64 {
|
||||
if let Ok(pending) = deserialize(&account.userdata) {
|
||||
let pending: HashMap<Signature, Plan> = pending;
|
||||
if !pending.is_empty() {
|
||||
0
|
||||
} else {
|
||||
account.tokens
|
||||
}
|
||||
} else {
|
||||
account.tokens
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a Transaction. If it contains a payment plan that requires a witness
|
||||
/// to progress, the payment plan will be stored in the bank.
|
||||
/// Process a Transaction. This is used for unit tests and simply calls the vector Bank::process_transactions method.
|
||||
pub fn process_transaction(&self, tx: &Transaction) -> Result<()> {
|
||||
match self.process_transactions(vec![tx.clone()])[0] {
|
||||
Err(ref e) => {
|
||||
|
@ -353,12 +284,12 @@ impl Bank {
|
|||
} else {
|
||||
error_counters.account_not_found_leader += 1;
|
||||
}
|
||||
if let Instruction::NewVote(_vote) = tx.instruction() {
|
||||
if let Some(Instruction::NewVote(_vote)) = tx.instruction() {
|
||||
error_counters.account_not_found_vote += 1;
|
||||
}
|
||||
Err(BankError::AccountNotFound(*tx.from()))
|
||||
} else if accounts.get(&tx.keys[0]).unwrap().tokens < tx.fee {
|
||||
Err(BankError::InsufficientFunds(*tx.from()))
|
||||
Err(BankError::InsufficientFundsForFee(*tx.from()))
|
||||
} else {
|
||||
let mut called_accounts: Vec<Account> = tx
|
||||
.keys
|
||||
|
@ -382,27 +313,60 @@ impl Bank {
|
|||
.map(|tx| self.load_account(tx, accounts, error_counters))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn execute_transaction(tx: Transaction, accounts: &mut [Account]) -> Result<Transaction> {
|
||||
pub fn verify_transaction(
|
||||
tx: &Transaction,
|
||||
pre_contract_id: &Pubkey,
|
||||
pre_tokens: i64,
|
||||
account: &Account,
|
||||
) -> Result<()> {
|
||||
// Verify the transaction
|
||||
// make sure that contract_id is still the same or this was just assigned by the system call contract
|
||||
if !((*pre_contract_id == account.contract_id)
|
||||
|| (SystemContract::check_id(&tx.contract_id)
|
||||
&& SystemContract::check_id(&pre_contract_id)))
|
||||
{
|
||||
//TODO, this maybe redundant bpf should be able to guarantee this property
|
||||
return Err(BankError::ModifiedContractId(tx.signature));
|
||||
}
|
||||
// For accounts unassigned to the contract, the individual balance of each accounts cannot decrease.
|
||||
if tx.contract_id != account.contract_id && pre_tokens > account.tokens {
|
||||
return Err(BankError::ExternalAccountTokenSpend(tx.signature));
|
||||
}
|
||||
if account.tokens < 0 {
|
||||
return Err(BankError::ResultWithNegativeTokens(tx.signature));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Execute a transaction.
|
||||
/// This method calls the contract's process_transaction method and verifies that the result of
|
||||
/// the contract does not violate the bank's accounting rules.
|
||||
/// The accounts are commited back to the bank only if this function returns Ok(_).
|
||||
fn execute_transaction(tx: Transaction, accounts: &mut [Account]) -> Result<Transaction> {
|
||||
let pre_total: i64 = accounts.iter().map(|a| a.tokens).sum();
|
||||
let pre_data: Vec<_> = accounts
|
||||
.iter_mut()
|
||||
.map(|a| (a.contract_id, a.tokens))
|
||||
.collect();
|
||||
|
||||
// TODO next steps is to add hooks to call arbitrary contracts here
|
||||
// Call the contract method
|
||||
// It's up to the contract to implement its own rules on moving funds
|
||||
let e = Self::process_transaction_of_budget_instruction(&tx, accounts);
|
||||
|
||||
if SystemContract::check_id(&tx.contract_id) {
|
||||
SystemContract::process_transaction(&tx, accounts)
|
||||
} else if BudgetContract::check_id(&tx.contract_id) {
|
||||
// TODO: the runtime should be checking read/write access to memory
|
||||
// we are trusting the hard coded contracts not to clobber or allocate
|
||||
BudgetContract::process_transaction(&tx, accounts)
|
||||
} else {
|
||||
return Err(BankError::UnknownContractId(tx.contract_id));
|
||||
}
|
||||
// Verify the transaction
|
||||
// TODO, At the moment there is only 1 contract, so 1-3 are not checked
|
||||
// 1. For accounts assigned to the contract, the total sum of all the tokens in these accounts cannot increase.
|
||||
// 2. For accounts unassigned to the contract, the individual balance of each accounts cannot decrease.
|
||||
// 3. For accounts unassigned to the contract, the userdata cannot change.
|
||||
|
||||
// 4. The total sum of all the tokens in all the pages cannot change.
|
||||
for ((pre_contract_id, pre_tokens), post_account) in pre_data.iter().zip(accounts.iter()) {
|
||||
Self::verify_transaction(&tx, pre_contract_id, *pre_tokens, post_account)?;
|
||||
}
|
||||
// The total sum of all the tokens in all the pages cannot change.
|
||||
let post_total: i64 = accounts.iter().map(|a| a.tokens).sum();
|
||||
if pre_total != post_total {
|
||||
Err(BankError::UnbalancedTransaction(tx.signature))
|
||||
} else if let Err(err) = e {
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(tx)
|
||||
}
|
||||
|
@ -594,7 +558,7 @@ impl Bank {
|
|||
{
|
||||
let tx = &entry1.transactions[0];
|
||||
let instruction = tx.instruction();
|
||||
let deposit = if let Instruction::NewContract(contract) = instruction {
|
||||
let deposit = if let Some(Instruction::NewContract(contract)) = instruction {
|
||||
contract.plan.final_payment()
|
||||
} else {
|
||||
None
|
||||
|
@ -624,60 +588,6 @@ impl Bank {
|
|||
Ok((entry_count, tail))
|
||||
}
|
||||
|
||||
/// Process a Witness Signature. Any payment plans waiting on this signature
|
||||
/// will progress one step.
|
||||
fn apply_signature(from: Pubkey, signature: Signature, account: &mut [Account]) {
|
||||
let mut pending: HashMap<Signature, Plan> =
|
||||
deserialize(&account[1].userdata).unwrap_or_default();
|
||||
if let Occupied(mut e) = pending.entry(signature) {
|
||||
e.get_mut().apply_witness(&Witness::Signature, &from);
|
||||
if let Some(payment) = e.get().final_payment() {
|
||||
//move the tokens back to the from account
|
||||
account[0].tokens += payment.tokens;
|
||||
account[1].tokens -= payment.tokens;
|
||||
e.remove_entry();
|
||||
}
|
||||
};
|
||||
//TODO this allocation needs to be changed once the runtime only allows for explicitly
|
||||
//allocated memory
|
||||
account[1].userdata = if pending.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
serialize(&pending).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
/// Process a Witness Timestamp. Any payment plans waiting on this timestamp
|
||||
/// will progress one step.
|
||||
fn apply_timestamp(from: Pubkey, dt: DateTime<Utc>, account: &mut Account) {
|
||||
let mut pending: HashMap<Signature, Plan> =
|
||||
deserialize(&account.userdata).unwrap_or_default();
|
||||
//deserialize(&account.userdata).unwrap_or(HashMap::new());
|
||||
|
||||
// Check to see if any timelocked transactions can be completed.
|
||||
let mut completed = vec![];
|
||||
|
||||
// Hold 'pending' write lock until the end of this function. Otherwise another thread can
|
||||
// double-spend if it enters before the modified plan is removed from 'pending'.
|
||||
for (key, plan) in &mut pending {
|
||||
plan.apply_witness(&Witness::Timestamp(dt), &from);
|
||||
if let Some(_payment) = plan.final_payment() {
|
||||
completed.push(key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for key in completed {
|
||||
pending.remove(&key);
|
||||
}
|
||||
//TODO this allocation needs to be changed once the runtime only allows for explicitly
|
||||
//allocated memory
|
||||
account.userdata = if pending.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
serialize(&pending).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
/// Create, sign, and process a Transaction from `keypair` to `to` of
|
||||
/// `n` tokens where `last_id` is the last Entry ID observed by the client.
|
||||
pub fn transfer(
|
||||
|
@ -692,25 +602,20 @@ impl Bank {
|
|||
self.process_transaction(&tx).map(|_| signature)
|
||||
}
|
||||
|
||||
/// Create, sign, and process a postdated Transaction from `keypair`
|
||||
/// to `to` of `n` tokens on `dt` where `last_id` is the last Entry ID
|
||||
/// observed by the client.
|
||||
pub fn transfer_on_date(
|
||||
&self,
|
||||
n: i64,
|
||||
keypair: &Keypair,
|
||||
to: Pubkey,
|
||||
dt: DateTime<Utc>,
|
||||
last_id: Hash,
|
||||
) -> Result<Signature> {
|
||||
let tx = Transaction::new_on_date(keypair, to, dt, n, last_id);
|
||||
let signature = tx.signature;
|
||||
self.process_transaction(&tx).map(|_| signature)
|
||||
pub fn read_balance(account: &Account) -> i64 {
|
||||
if SystemContract::check_id(&account.contract_id) {
|
||||
SystemContract::get_balance(account)
|
||||
} else if BudgetContract::check_id(&account.contract_id) {
|
||||
BudgetContract::get_balance(account)
|
||||
} else {
|
||||
account.tokens
|
||||
}
|
||||
|
||||
}
|
||||
/// Each contract would need to be able to introspect its own state
|
||||
/// this is hard coded to the budget contract langauge
|
||||
pub fn get_balance(&self, pubkey: &Pubkey) -> i64 {
|
||||
self.get_account(pubkey)
|
||||
.map(|x| Self::get_balance_of_budget_payment_plan(&x))
|
||||
.map(|x| Self::read_balance(&x))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
|
@ -767,6 +672,7 @@ mod tests {
|
|||
use entry_writer::{self, EntryWriter};
|
||||
use hash::hash;
|
||||
use ledger;
|
||||
use logger;
|
||||
use packet::BLOB_DATA_SIZE;
|
||||
use signature::{GenKeys, KeypairUtil};
|
||||
use std;
|
||||
|
@ -799,13 +705,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_negative_tokens() {
|
||||
logger::setup();
|
||||
let mint = Mint::new(1);
|
||||
let pubkey = Keypair::new().pubkey();
|
||||
let bank = Bank::new(&mint);
|
||||
assert_eq!(
|
||||
bank.transfer(-1, &mint.keypair(), pubkey, mint.last_id()),
|
||||
Err(BankError::NegativeTokens)
|
||||
);
|
||||
let res = bank.transfer(-1, &mint.keypair(), pubkey, mint.last_id());
|
||||
println!("{:?}", bank.get_account(&pubkey));
|
||||
assert_matches!(res, Err(BankError::ResultWithNegativeTokens(_)));
|
||||
assert_eq!(bank.transaction_count(), 0);
|
||||
}
|
||||
|
||||
|
@ -815,15 +721,20 @@ mod tests {
|
|||
fn test_detect_failed_duplicate_transactions_issue_1157() {
|
||||
let mint = Mint::new(1);
|
||||
let bank = Bank::new(&mint);
|
||||
let dest = Keypair::new();
|
||||
|
||||
let tx = Transaction::new(&mint.keypair(), mint.keypair().pubkey(), -1, mint.last_id());
|
||||
// source with 0 contract context
|
||||
let tx = Transaction::new(&mint.keypair(), dest.pubkey(), 2, mint.last_id());
|
||||
let signature = tx.signature;
|
||||
assert!(!bank.has_signature(&signature));
|
||||
assert_eq!(
|
||||
bank.process_transaction(&tx),
|
||||
Err(BankError::NegativeTokens)
|
||||
);
|
||||
let res = bank.process_transaction(&tx);
|
||||
// This is the potentially wrong behavior
|
||||
// result failed, but signature is registered
|
||||
assert!(!res.is_ok());
|
||||
assert!(bank.has_signature(&signature));
|
||||
// sanity check that tokens didn't move
|
||||
assert_eq!(bank.get_balance(&dest.pubkey()), 0);
|
||||
assert_eq!(bank.get_balance(&mint.pubkey()), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -847,9 +758,9 @@ mod tests {
|
|||
.unwrap();
|
||||
assert_eq!(bank.transaction_count(), 1);
|
||||
assert_eq!(bank.get_balance(&pubkey), 1_000);
|
||||
assert_eq!(
|
||||
assert_matches!(
|
||||
bank.transfer(10_001, &mint.keypair(), pubkey, mint.last_id()),
|
||||
Err(BankError::InsufficientFunds(mint.pubkey()))
|
||||
Err(BankError::ResultWithNegativeTokens(_))
|
||||
);
|
||||
assert_eq!(bank.transaction_count(), 1);
|
||||
|
||||
|
@ -868,85 +779,6 @@ mod tests {
|
|||
assert_eq!(bank.get_balance(&pubkey), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_on_date() {
|
||||
let mint = Mint::new(2);
|
||||
let bank = Bank::new(&mint);
|
||||
let pubkey = Keypair::new().pubkey();
|
||||
let dt = Utc::now();
|
||||
bank.transfer_on_date(1, &mint.keypair(), pubkey, dt, mint.last_id())
|
||||
.unwrap();
|
||||
|
||||
// Mint's balance will be 1 because 1 of the tokens is locked up
|
||||
assert_eq!(bank.get_balance(&mint.pubkey()), 1);
|
||||
|
||||
// tx count is 1, because debits were applied.
|
||||
assert_eq!(bank.transaction_count(), 1);
|
||||
|
||||
// pubkey's balance will be 0 because the funds have not been
|
||||
// sent.
|
||||
assert_eq!(bank.get_balance(&pubkey), 0);
|
||||
|
||||
// Now, acknowledge the time in the condition occurred and
|
||||
// that pubkey's funds are now available.
|
||||
let tx = Transaction::new_timestamp(&mint.keypair(), pubkey, dt, bank.last_id());
|
||||
let res = bank.process_transaction(&tx);
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(bank.get_balance(&pubkey), 1);
|
||||
|
||||
// tx count is 2
|
||||
assert_eq!(bank.transaction_count(), 2);
|
||||
|
||||
// try to replay the timestamp contract
|
||||
bank.register_entry_id(&hash(bank.last_id().as_ref()));
|
||||
let tx = Transaction::new_timestamp(&mint.keypair(), pubkey, dt, bank.last_id());
|
||||
let res = bank.process_transaction(&tx);
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert_eq!(bank.get_balance(&pubkey), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cancel_transfer() {
|
||||
// mint needs to have a balance to modify the external contract
|
||||
let mint = Mint::new(2);
|
||||
let bank = Bank::new(&mint);
|
||||
let pubkey = Keypair::new().pubkey();
|
||||
let dt = Utc::now();
|
||||
let signature = bank
|
||||
.transfer_on_date(1, &mint.keypair(), pubkey, dt, mint.last_id())
|
||||
.unwrap();
|
||||
|
||||
// Assert the debit counts as a transaction.
|
||||
assert_eq!(bank.transaction_count(), 1);
|
||||
|
||||
// Mint's balance will be 1 because 1 of the tokens is locked up.
|
||||
assert_eq!(bank.get_balance(&mint.pubkey()), 1);
|
||||
|
||||
// pubkey's balance will be 0 because the funds are locked up
|
||||
assert_eq!(bank.get_balance(&pubkey), 0);
|
||||
|
||||
// Now, cancel the transaction. Mint gets her funds back, pubkey never sees them.
|
||||
let tx = Transaction::new_signature(&mint.keypair(), pubkey, signature, bank.last_id());
|
||||
let res = bank.process_transaction(&tx);
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(bank.get_balance(&pubkey), 0);
|
||||
assert_eq!(bank.get_balance(&mint.pubkey()), 2);
|
||||
|
||||
// Assert cancel counts as a tx
|
||||
assert_eq!(bank.transaction_count(), 2);
|
||||
|
||||
// try to replay the signature contract
|
||||
bank.register_entry_id(&hash(bank.last_id().as_ref()));
|
||||
let tx = Transaction::new_signature(&mint.keypair(), pubkey, signature, bank.last_id());
|
||||
let res = bank.process_transaction(&tx); //<-- attack! try to get budget dsl to pay out with another signature
|
||||
assert!(res.is_ok());
|
||||
// balance is is still 2 for the mint
|
||||
assert_eq!(bank.get_balance(&mint.pubkey()), 2);
|
||||
// balance is is still 0 for the contract
|
||||
assert_eq!(bank.get_balance(&pubkey), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_transaction_signature() {
|
||||
let mint = Mint::new(1);
|
||||
|
@ -1070,7 +902,7 @@ mod tests {
|
|||
let hash = mint.last_id();
|
||||
let mut txs = Vec::with_capacity(length);
|
||||
for i in 0..length {
|
||||
txs.push(Transaction::new(
|
||||
txs.push(Transaction::system_new(
|
||||
&mint.keypair(),
|
||||
keypair.pubkey(),
|
||||
i as i64,
|
||||
|
@ -1088,7 +920,7 @@ mod tests {
|
|||
let hash = mint.last_id();
|
||||
let transactions: Vec<_> = keypairs
|
||||
.iter()
|
||||
.map(|keypair| Transaction::new(&mint.keypair(), keypair.pubkey(), 1, hash))
|
||||
.map(|keypair| Transaction::system_new(&mint.keypair(), keypair.pubkey(), 1, hash))
|
||||
.collect();
|
||||
let entries = ledger::next_entries(&hash, 0, transactions);
|
||||
entries.into_iter()
|
||||
|
@ -1100,7 +932,7 @@ mod tests {
|
|||
let mut num_hashes = 0;
|
||||
for _ in 0..length {
|
||||
let keypair = Keypair::new();
|
||||
let tx = Transaction::new(&mint.keypair(), keypair.pubkey(), 1, hash);
|
||||
let tx = Transaction::system_new(&mint.keypair(), keypair.pubkey(), 1, hash);
|
||||
let entry = Entry::new_mut(&mut hash, &mut num_hashes, vec![tx], false);
|
||||
entries.push(entry);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,469 @@
|
|||
//! budget contract
|
||||
use bank::Account;
|
||||
use bincode::{self, deserialize, serialize_into, serialized_size};
|
||||
use chrono::prelude::{DateTime, Utc};
|
||||
use instruction::{Instruction, Plan};
|
||||
use payment_plan::{PaymentPlan, Witness};
|
||||
use signature::{Pubkey, Signature};
|
||||
use std::collections::hash_map::Entry::Occupied;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use transaction::Transaction;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub enum BudgetError {
|
||||
InsufficientFunds(Pubkey),
|
||||
ContractAlreadyExists(Pubkey),
|
||||
ContractNotPending(Pubkey),
|
||||
SourceIsPendingContract(Pubkey),
|
||||
UninitializedContract(Pubkey),
|
||||
NegativeTokens,
|
||||
DestinationMissing(Pubkey),
|
||||
FailedWitness(Signature),
|
||||
SignatureUnoccupied(Signature),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
pub struct BudgetContract {
|
||||
pub initialized: bool,
|
||||
pub pending: HashMap<Signature, Plan>,
|
||||
pub last_error: Option<BudgetError>,
|
||||
}
|
||||
|
||||
pub const BUDGET_CONTRACT_ID: [u8; 32] = [
|
||||
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
impl BudgetContract {
|
||||
fn is_pending(&self) -> bool {
|
||||
!self.pending.is_empty()
|
||||
}
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&BUDGET_CONTRACT_ID)
|
||||
}
|
||||
pub fn check_id(contract_id: &Pubkey) -> bool {
|
||||
contract_id.as_ref() == BUDGET_CONTRACT_ID
|
||||
}
|
||||
|
||||
/// Process a Witness Signature. Any payment plans waiting on this signature
|
||||
/// will progress one step.
|
||||
fn apply_signature(
|
||||
&mut self,
|
||||
tx: &Transaction,
|
||||
signature: Signature,
|
||||
account: &mut [Account],
|
||||
) -> Result<(), BudgetError> {
|
||||
if let Occupied(mut e) = self.pending.entry(signature) {
|
||||
e.get_mut().apply_witness(&Witness::Signature, &tx.keys[0]);
|
||||
if let Some(payment) = e.get().final_payment() {
|
||||
if tx.keys.len() > 1 && payment.to == tx.keys[2] {
|
||||
trace!("apply_witness refund");
|
||||
//move the tokens back to the from account
|
||||
account[1].tokens -= payment.tokens;
|
||||
account[2].tokens += payment.tokens;
|
||||
e.remove_entry();
|
||||
} else {
|
||||
trace!("destination is missing");
|
||||
return Err(BudgetError::DestinationMissing(payment.to));
|
||||
}
|
||||
} else {
|
||||
trace!("failed apply_witness");
|
||||
return Err(BudgetError::FailedWitness(signature));
|
||||
}
|
||||
} else {
|
||||
trace!("apply_witness signature unoccupied");
|
||||
return Err(BudgetError::SignatureUnoccupied(signature));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process a Witness Timestamp. Any payment plans waiting on this timestamp
|
||||
/// will progress one step.
|
||||
fn apply_timestamp(
|
||||
&mut self,
|
||||
tx: &Transaction,
|
||||
accounts: &mut [Account],
|
||||
dt: DateTime<Utc>,
|
||||
) -> Result<(), BudgetError> {
|
||||
// Check to see if any timelocked transactions can be completed.
|
||||
let mut completed = vec![];
|
||||
|
||||
// Hold 'pending' write lock until the end of this function. Otherwise another thread can
|
||||
// double-spend if it enters before the modified plan is removed from 'pending'.
|
||||
for (key, plan) in &mut self.pending {
|
||||
plan.apply_witness(&Witness::Timestamp(dt), &tx.keys[0]);
|
||||
if let Some(payment) = plan.final_payment() {
|
||||
if tx.keys.len() < 2 || payment.to != tx.keys[2] {
|
||||
trace!("destination missing");
|
||||
return Err(BudgetError::DestinationMissing(payment.to));
|
||||
}
|
||||
completed.push(key.clone());
|
||||
accounts[2].tokens += payment.tokens;
|
||||
accounts[1].tokens -= payment.tokens;
|
||||
}
|
||||
}
|
||||
for key in completed {
|
||||
self.pending.remove(&key);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deduct tokens from the source account if it has sufficient funds and the contract isn't
|
||||
/// pending
|
||||
fn apply_debits_to_budget_state(
|
||||
tx: &Transaction,
|
||||
accounts: &mut [Account],
|
||||
instruction: &Instruction,
|
||||
) -> Result<(), BudgetError> {
|
||||
{
|
||||
// if the source account userdata is not empty, this is a pending contract
|
||||
if !accounts[0].userdata.is_empty() {
|
||||
trace!("source is pending");
|
||||
return Err(BudgetError::SourceIsPendingContract(tx.keys[0]));
|
||||
}
|
||||
if let Instruction::NewContract(contract) = &instruction {
|
||||
if contract.tokens < 0 {
|
||||
trace!("negative tokens");
|
||||
return Err(BudgetError::NegativeTokens);
|
||||
}
|
||||
|
||||
if accounts[0].tokens < contract.tokens {
|
||||
trace!("insufficent funds");
|
||||
return Err(BudgetError::InsufficientFunds(tx.keys[0]));
|
||||
} else {
|
||||
accounts[0].tokens -= contract.tokens;
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply only a transaction's credits.
|
||||
/// Note: It is safe to apply credits from multiple transactions in parallel.
|
||||
fn apply_credits_to_budget_state(
|
||||
tx: &Transaction,
|
||||
accounts: &mut [Account],
|
||||
instruction: &Instruction,
|
||||
) -> Result<(), BudgetError> {
|
||||
match instruction {
|
||||
Instruction::NewContract(contract) => {
|
||||
let plan = contract.plan.clone();
|
||||
if let Some(payment) = plan.final_payment() {
|
||||
accounts[1].tokens += payment.tokens;
|
||||
Ok(())
|
||||
} else {
|
||||
let existing = Self::deserialize(&accounts[1].userdata).ok();
|
||||
if Some(true) == existing.map(|x| x.initialized) {
|
||||
trace!("contract already exists");
|
||||
Err(BudgetError::ContractAlreadyExists(tx.keys[1]))
|
||||
} else {
|
||||
let mut state = BudgetContract::default();
|
||||
state.pending.insert(tx.signature, plan);
|
||||
accounts[1].tokens += contract.tokens;
|
||||
state.initialized = true;
|
||||
state.serialize(&mut accounts[1].userdata);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Instruction::ApplyTimestamp(dt) => {
|
||||
let mut state = Self::deserialize(&accounts[1].userdata).unwrap();
|
||||
if !state.is_pending() {
|
||||
return Err(BudgetError::ContractNotPending(tx.keys[1]));
|
||||
}
|
||||
if !state.initialized {
|
||||
trace!("contract is uninitialized");
|
||||
Err(BudgetError::UninitializedContract(tx.keys[1]))
|
||||
} else {
|
||||
state.apply_timestamp(tx, accounts, *dt)?;
|
||||
trace!("apply timestamp commited");
|
||||
state.serialize(&mut accounts[1].userdata);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Instruction::ApplySignature(signature) => {
|
||||
let mut state = Self::deserialize(&accounts[1].userdata).unwrap();
|
||||
if !state.is_pending() {
|
||||
return Err(BudgetError::ContractNotPending(tx.keys[1]));
|
||||
}
|
||||
if !state.initialized {
|
||||
trace!("contract is uninitialized");
|
||||
Err(BudgetError::UninitializedContract(tx.keys[1]))
|
||||
} else {
|
||||
trace!("apply signature");
|
||||
state.apply_signature(tx, *signature, accounts)?;
|
||||
trace!("apply signature commited");
|
||||
state.serialize(&mut accounts[1].userdata);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Instruction::NewVote(_vote) => {
|
||||
// TODO: move vote instruction into a different contract
|
||||
trace!("GOT VOTE! last_id={}", tx.last_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
fn serialize(&self, output: &mut [u8]) {
|
||||
let len = serialized_size(self).unwrap() as u64;
|
||||
{
|
||||
let writer = io::BufWriter::new(&mut output[..8]);
|
||||
serialize_into(writer, &len).unwrap();
|
||||
}
|
||||
{
|
||||
let writer = io::BufWriter::new(&mut output[8..8 + len as usize]);
|
||||
serialize_into(writer, self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize(input: &[u8]) -> bincode::Result<Self> {
|
||||
if input.len() < 8 {
|
||||
return Err(Box::new(bincode::ErrorKind::SizeLimit));
|
||||
}
|
||||
let len: u64 = deserialize(&input[..8]).unwrap();
|
||||
if len < 8 {
|
||||
return Err(Box::new(bincode::ErrorKind::SizeLimit));
|
||||
}
|
||||
if input.len() < 8 + len as usize {
|
||||
return Err(Box::new(bincode::ErrorKind::SizeLimit));
|
||||
}
|
||||
deserialize(&input[8..8 + len as usize])
|
||||
}
|
||||
|
||||
fn save_error_to_budget_state(e: BudgetError, accounts: &mut [Account]) -> () {
|
||||
if let Ok(mut state) = BudgetContract::deserialize(&accounts[1].userdata) {
|
||||
trace!("saved error {:?}", e);
|
||||
state.last_error = Some(e);
|
||||
state.serialize(&mut accounts[1].userdata);
|
||||
} else {
|
||||
trace!("error in uninitialized contract {:?}", e,);
|
||||
}
|
||||
}
|
||||
|
||||
/// Budget DSL contract interface
|
||||
/// * tx - the transaction
|
||||
/// * accounts[0] - The source of the tokens
|
||||
/// * accounts[1] - The contract context. Once the contract has been completed, the tokens can
|
||||
/// be spent from this account .
|
||||
pub fn process_transaction(tx: &Transaction, accounts: &mut [Account]) -> () {
|
||||
let instruction = deserialize(&tx.userdata).unwrap();
|
||||
let _ = Self::apply_debits_to_budget_state(tx, accounts, &instruction)
|
||||
.and_then(|_| Self::apply_credits_to_budget_state(tx, accounts, &instruction))
|
||||
.map_err(|e| {
|
||||
trace!("saving error {:?}", e);
|
||||
Self::save_error_to_budget_state(e, accounts);
|
||||
});
|
||||
}
|
||||
|
||||
//TODO the contract needs to provide a "get_balance" introspection call of the userdata
|
||||
pub fn get_balance(account: &Account) -> i64 {
|
||||
if let Ok(pending) = deserialize(&account.userdata) {
|
||||
let pending: BudgetContract = pending;
|
||||
if pending.is_pending() {
|
||||
0
|
||||
} else {
|
||||
account.tokens
|
||||
}
|
||||
} else {
|
||||
account.tokens
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use bank::Account;
|
||||
use bincode::serialize;
|
||||
use budget_contract::{BudgetContract, BudgetError};
|
||||
use chrono::prelude::Utc;
|
||||
use hash::Hash;
|
||||
use signature::{Keypair, KeypairUtil};
|
||||
use transaction::Transaction;
|
||||
#[test]
|
||||
fn test_serializer() {
|
||||
let mut a = Account::new(0, 512, BudgetContract::id());
|
||||
let b = BudgetContract::default();
|
||||
b.serialize(&mut a.userdata);
|
||||
let buf = serialize(&b).unwrap();
|
||||
assert_eq!(a.userdata[8..8 + buf.len()], buf[0..]);
|
||||
let c = BudgetContract::deserialize(&a.userdata).unwrap();
|
||||
assert_eq!(b, c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_on_date() {
|
||||
let mut accounts = vec![
|
||||
Account::new(1, 0, BudgetContract::id()),
|
||||
Account::new(0, 512, BudgetContract::id()),
|
||||
Account::new(0, 0, BudgetContract::id()),
|
||||
];
|
||||
let from_account = 0;
|
||||
let contract_account = 1;
|
||||
let to_account = 2;
|
||||
let from = Keypair::new();
|
||||
let contract = Keypair::new();
|
||||
let to = Keypair::new();
|
||||
let rando = Keypair::new();
|
||||
let dt = Utc::now();
|
||||
let tx = Transaction::budget_new_on_date(
|
||||
&from,
|
||||
to.pubkey(),
|
||||
contract.pubkey(),
|
||||
dt,
|
||||
1,
|
||||
Hash::default(),
|
||||
);
|
||||
BudgetContract::process_transaction(&tx, &mut accounts);
|
||||
assert_eq!(accounts[from_account].tokens, 0);
|
||||
assert_eq!(accounts[contract_account].tokens, 1);
|
||||
let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap();
|
||||
assert_eq!(state.last_error, None);
|
||||
assert!(state.is_pending());
|
||||
|
||||
// Attack! Try to payout to a rando key
|
||||
let tx = Transaction::budget_new_timestamp(
|
||||
&from,
|
||||
contract.pubkey(),
|
||||
rando.pubkey(),
|
||||
dt,
|
||||
Hash::default(),
|
||||
);
|
||||
BudgetContract::process_transaction(&tx, &mut accounts);
|
||||
assert_eq!(accounts[from_account].tokens, 0);
|
||||
assert_eq!(accounts[contract_account].tokens, 1);
|
||||
assert_eq!(accounts[to_account].tokens, 0);
|
||||
|
||||
let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap();
|
||||
assert_eq!(
|
||||
state.last_error,
|
||||
Some(BudgetError::DestinationMissing(to.pubkey()))
|
||||
);
|
||||
assert!(state.is_pending());
|
||||
|
||||
// Now, acknowledge the time in the condition occurred and
|
||||
// that pubkey's funds are now available.
|
||||
let tx = Transaction::budget_new_timestamp(
|
||||
&from,
|
||||
contract.pubkey(),
|
||||
to.pubkey(),
|
||||
dt,
|
||||
Hash::default(),
|
||||
);
|
||||
BudgetContract::process_transaction(&tx, &mut accounts);
|
||||
assert_eq!(accounts[from_account].tokens, 0);
|
||||
assert_eq!(accounts[contract_account].tokens, 0);
|
||||
assert_eq!(accounts[to_account].tokens, 1);
|
||||
|
||||
let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap();
|
||||
assert!(!state.is_pending());
|
||||
|
||||
// try to replay the timestamp contract
|
||||
BudgetContract::process_transaction(&tx, &mut accounts);
|
||||
assert_eq!(accounts[from_account].tokens, 0);
|
||||
assert_eq!(accounts[contract_account].tokens, 0);
|
||||
assert_eq!(accounts[to_account].tokens, 1);
|
||||
let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap();
|
||||
assert_eq!(
|
||||
state.last_error,
|
||||
Some(BudgetError::ContractNotPending(contract.pubkey()))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_cancel_transfer() {
|
||||
let mut accounts = vec![
|
||||
Account::new(1, 0, BudgetContract::id()),
|
||||
Account::new(0, 512, BudgetContract::id()),
|
||||
Account::new(0, 0, BudgetContract::id()),
|
||||
];
|
||||
let from_account = 0;
|
||||
let contract_account = 1;
|
||||
let pay_account = 2;
|
||||
let from = Keypair::new();
|
||||
let contract = Keypair::new();
|
||||
let to = Keypair::new();
|
||||
let dt = Utc::now();
|
||||
let tx = Transaction::budget_new_on_date(
|
||||
&from,
|
||||
to.pubkey(),
|
||||
contract.pubkey(),
|
||||
dt,
|
||||
1,
|
||||
Hash::default(),
|
||||
);
|
||||
let sig = tx.signature;
|
||||
BudgetContract::process_transaction(&tx, &mut accounts);
|
||||
assert_eq!(accounts[from_account].tokens, 0);
|
||||
assert_eq!(accounts[contract_account].tokens, 1);
|
||||
let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap();
|
||||
assert_eq!(state.last_error, None);
|
||||
assert!(state.is_pending());
|
||||
|
||||
// Attack! try to put the tokens into the wrong account with cancel
|
||||
let tx = Transaction::budget_new_signature(
|
||||
&to,
|
||||
contract.pubkey(),
|
||||
to.pubkey(),
|
||||
sig,
|
||||
Hash::default(),
|
||||
);
|
||||
// unit test hack, the `from account` is passed instead of the `to` account to avoid
|
||||
// creating more account vectors
|
||||
BudgetContract::process_transaction(&tx, &mut accounts);
|
||||
// nothing should be changed because apply witness didn't finalize a payment
|
||||
assert_eq!(accounts[from_account].tokens, 0);
|
||||
assert_eq!(accounts[contract_account].tokens, 1);
|
||||
// this would be the `to.pubkey()` account
|
||||
assert_eq!(accounts[pay_account].tokens, 0);
|
||||
let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap();
|
||||
assert_eq!(state.last_error, Some(BudgetError::FailedWitness(sig)));
|
||||
|
||||
// Attack! try canceling with a bad signature
|
||||
let badsig = tx.signature;
|
||||
let tx = Transaction::budget_new_signature(
|
||||
&from,
|
||||
contract.pubkey(),
|
||||
from.pubkey(),
|
||||
badsig,
|
||||
Hash::default(),
|
||||
);
|
||||
BudgetContract::process_transaction(&tx, &mut accounts);
|
||||
assert_eq!(accounts[from_account].tokens, 0);
|
||||
assert_eq!(accounts[contract_account].tokens, 1);
|
||||
assert_eq!(accounts[pay_account].tokens, 0);
|
||||
let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap();
|
||||
assert_eq!(
|
||||
state.last_error,
|
||||
Some(BudgetError::SignatureUnoccupied(badsig))
|
||||
);
|
||||
|
||||
// Now, cancel the transaction. from gets her funds back
|
||||
let tx = Transaction::budget_new_signature(
|
||||
&from,
|
||||
contract.pubkey(),
|
||||
from.pubkey(),
|
||||
sig,
|
||||
Hash::default(),
|
||||
);
|
||||
BudgetContract::process_transaction(&tx, &mut accounts);
|
||||
assert_eq!(accounts[from_account].tokens, 0);
|
||||
assert_eq!(accounts[contract_account].tokens, 0);
|
||||
assert_eq!(accounts[pay_account].tokens, 1);
|
||||
|
||||
// try to replay the signature contract
|
||||
let tx = Transaction::budget_new_signature(
|
||||
&from,
|
||||
contract.pubkey(),
|
||||
from.pubkey(),
|
||||
sig,
|
||||
Hash::default(),
|
||||
);
|
||||
BudgetContract::process_transaction(&tx, &mut accounts);
|
||||
assert_eq!(accounts[from_account].tokens, 0);
|
||||
assert_eq!(accounts[contract_account].tokens, 0);
|
||||
assert_eq!(accounts[pay_account].tokens, 1);
|
||||
|
||||
let state = BudgetContract::deserialize(&accounts[contract_account].userdata).unwrap();
|
||||
assert_eq!(
|
||||
state.last_error,
|
||||
Some(BudgetError::ContractNotPending(contract.pubkey()))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ use bincode::{deserialize, serialize};
|
|||
use choose_gossip_peer_strategy::{ChooseGossipPeerStrategy, ChooseWeightedPeerStrategy};
|
||||
use counter::Counter;
|
||||
use hash::Hash;
|
||||
use instruction::Vote;
|
||||
use ledger::LedgerWindow;
|
||||
use log::Level;
|
||||
use netutil::{bind_in_range, bind_to, multi_bind_in_range};
|
||||
|
@ -33,7 +34,6 @@ use std::thread::{sleep, Builder, JoinHandle};
|
|||
use std::time::{Duration, Instant};
|
||||
use streamer::{BlobReceiver, BlobSender};
|
||||
use timing::{duration_as_ms, timestamp};
|
||||
use transaction::Vote;
|
||||
use window::{SharedWindow, WindowIndex};
|
||||
|
||||
pub const FULLNODE_PORT_RANGE: (u16, u16) = (8000, 10_000);
|
||||
|
@ -1327,6 +1327,7 @@ mod tests {
|
|||
};
|
||||
use entry::Entry;
|
||||
use hash::{hash, Hash};
|
||||
use instruction::Vote;
|
||||
use ledger::{LedgerWindow, LedgerWriter};
|
||||
use logger;
|
||||
use packet::BlobRecycler;
|
||||
|
@ -1339,7 +1340,6 @@ mod tests {
|
|||
use std::sync::{Arc, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use transaction::Vote;
|
||||
use window::default_window;
|
||||
|
||||
#[test]
|
||||
|
|
24
src/entry.rs
24
src/entry.rs
|
@ -249,8 +249,20 @@ mod tests {
|
|||
|
||||
// First, verify entries
|
||||
let keypair = Keypair::new();
|
||||
let tx0 = Transaction::new_timestamp(&keypair, keypair.pubkey(), Utc::now(), zero);
|
||||
let tx1 = Transaction::new_signature(&keypair, keypair.pubkey(), Default::default(), zero);
|
||||
let tx0 = Transaction::budget_new_timestamp(
|
||||
&keypair,
|
||||
keypair.pubkey(),
|
||||
keypair.pubkey(),
|
||||
Utc::now(),
|
||||
zero,
|
||||
);
|
||||
let tx1 = Transaction::budget_new_signature(
|
||||
&keypair,
|
||||
keypair.pubkey(),
|
||||
keypair.pubkey(),
|
||||
Default::default(),
|
||||
zero,
|
||||
);
|
||||
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()], false);
|
||||
assert!(e0.verify(&zero));
|
||||
|
||||
|
@ -272,7 +284,13 @@ mod tests {
|
|||
assert_eq!(tick.id, zero);
|
||||
|
||||
let keypair = Keypair::new();
|
||||
let tx0 = Transaction::new_timestamp(&keypair, keypair.pubkey(), Utc::now(), zero);
|
||||
let tx0 = Transaction::budget_new_timestamp(
|
||||
&keypair,
|
||||
keypair.pubkey(),
|
||||
keypair.pubkey(),
|
||||
Utc::now(),
|
||||
zero,
|
||||
);
|
||||
let entry0 = next_entry(&zero, 1, vec![tx0.clone()]);
|
||||
assert_eq!(entry0.num_hashes, 1);
|
||||
assert_eq!(entry0.id, next_hash(&zero, 1, &vec![tx0]));
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
use budget::Budget;
|
||||
use chrono::prelude::{DateTime, Utc};
|
||||
use payment_plan::{Payment, PaymentPlan, Witness};
|
||||
use signature::Pubkey;
|
||||
use signature::Signature;
|
||||
|
||||
/// The type of payment plan. Each item must implement the PaymentPlan trait.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Plan {
|
||||
/// The builtin contract language Budget.
|
||||
Budget(Budget),
|
||||
}
|
||||
|
||||
// A proxy for the underlying DSL.
|
||||
impl PaymentPlan for Plan {
|
||||
fn final_payment(&self) -> Option<Payment> {
|
||||
match self {
|
||||
Plan::Budget(budget) => budget.final_payment(),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify(&self, spendable_tokens: i64) -> bool {
|
||||
match self {
|
||||
Plan::Budget(budget) => budget.verify(spendable_tokens),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_witness(&mut self, witness: &Witness, from: &Pubkey) {
|
||||
match self {
|
||||
Plan::Budget(budget) => budget.apply_witness(witness, from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A smart contract.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Contract {
|
||||
/// The number of tokens allocated to the `Plan` and any transaction fees.
|
||||
pub tokens: i64,
|
||||
pub plan: Plan,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Vote {
|
||||
/// We send some gossip specific membership information through the vote to shortcut
|
||||
/// liveness voting
|
||||
/// The version of the CRDT struct that the last_id of this network voted with
|
||||
pub version: u64,
|
||||
/// The version of the CRDT struct that has the same network configuration as this one
|
||||
pub contact_info_version: u64,
|
||||
// TODO: add signature of the state here as well
|
||||
}
|
||||
|
||||
/// An instruction to progress the smart contract.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Instruction {
|
||||
/// Declare and instanstansiate `Contract`.
|
||||
NewContract(Contract),
|
||||
|
||||
/// Tell a payment plan acknowledge the given `DateTime` has past.
|
||||
ApplyTimestamp(DateTime<Utc>),
|
||||
|
||||
/// Tell the payment plan that the `NewContract` with `Signature` has been
|
||||
/// signed by the containing transaction's `Pubkey`.
|
||||
ApplySignature(Signature),
|
||||
|
||||
/// Vote for a PoH that is equal to the lastid of this transaction
|
||||
NewVote(Vote),
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
use bincode::{self, deserialize, deserialize_from, serialize_into, serialized_size};
|
||||
use entry::Entry;
|
||||
use hash::Hash;
|
||||
use instruction::Vote;
|
||||
use log::Level::Trace;
|
||||
use packet::{self, SharedBlob, BLOB_DATA_SIZE};
|
||||
use rayon::prelude::*;
|
||||
|
@ -15,7 +16,7 @@ use std::io::prelude::*;
|
|||
use std::io::{self, BufReader, BufWriter, Seek, SeekFrom};
|
||||
use std::mem::size_of;
|
||||
use std::path::Path;
|
||||
use transaction::{Transaction, Vote};
|
||||
use transaction::Transaction;
|
||||
use window::WINDOW_SIZE;
|
||||
|
||||
//
|
||||
|
@ -548,11 +549,12 @@ mod tests {
|
|||
use chrono::prelude::*;
|
||||
use entry::{next_entry, Entry};
|
||||
use hash::hash;
|
||||
use instruction::Vote;
|
||||
use packet::{BlobRecycler, BLOB_DATA_SIZE, PACKET_DATA_SIZE};
|
||||
use signature::{Keypair, KeypairUtil};
|
||||
use std;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use transaction::{Transaction, Vote};
|
||||
use transaction::Transaction;
|
||||
|
||||
fn tmp_ledger_path(name: &str) -> String {
|
||||
use std::env;
|
||||
|
@ -590,9 +592,10 @@ mod tests {
|
|||
Entry::new_mut(
|
||||
&mut id,
|
||||
&mut num_hashes,
|
||||
vec![Transaction::new_timestamp(
|
||||
vec![Transaction::budget_new_timestamp(
|
||||
&keypair,
|
||||
keypair.pubkey(),
|
||||
keypair.pubkey(),
|
||||
Utc::now(),
|
||||
one,
|
||||
)],
|
||||
|
@ -605,7 +608,7 @@ mod tests {
|
|||
let zero = Hash::default();
|
||||
let one = hash(&zero.as_ref());
|
||||
let keypair = Keypair::new();
|
||||
let tx0 = Transaction::new_vote(
|
||||
let tx0 = Transaction::budget_new_vote(
|
||||
&keypair,
|
||||
Vote {
|
||||
version: 0,
|
||||
|
@ -614,7 +617,13 @@ mod tests {
|
|||
one,
|
||||
1,
|
||||
);
|
||||
let tx1 = Transaction::new_timestamp(&keypair, keypair.pubkey(), Utc::now(), one);
|
||||
let tx1 = Transaction::budget_new_timestamp(
|
||||
&keypair,
|
||||
keypair.pubkey(),
|
||||
keypair.pubkey(),
|
||||
Utc::now(),
|
||||
one,
|
||||
);
|
||||
//
|
||||
// TODO: this magic number and the mix of transaction types
|
||||
// is designed to fill up a Blob more or less exactly,
|
||||
|
@ -659,7 +668,7 @@ mod tests {
|
|||
let id = Hash::default();
|
||||
let next_id = hash(&id.as_ref());
|
||||
let keypair = Keypair::new();
|
||||
let tx_small = Transaction::new_vote(
|
||||
let tx_small = Transaction::budget_new_vote(
|
||||
&keypair,
|
||||
Vote {
|
||||
version: 0,
|
||||
|
@ -668,7 +677,7 @@ mod tests {
|
|||
next_id,
|
||||
2,
|
||||
);
|
||||
let tx_large = Transaction::new(&keypair, keypair.pubkey(), 1, next_id);
|
||||
let tx_large = Transaction::budget_new(&keypair, keypair.pubkey(), 1, next_id);
|
||||
|
||||
let tx_small_size = serialized_size(&tx_small).unwrap() as usize;
|
||||
let tx_large_size = serialized_size(&tx_large).unwrap() as usize;
|
||||
|
|
|
@ -16,8 +16,10 @@ pub mod broadcast_stage;
|
|||
pub mod budget;
|
||||
pub mod choose_gossip_peer_strategy;
|
||||
pub mod client;
|
||||
pub mod instruction;
|
||||
#[macro_use]
|
||||
pub mod crdt;
|
||||
pub mod budget_contract;
|
||||
pub mod drone;
|
||||
pub mod entry;
|
||||
pub mod entry_writer;
|
||||
|
@ -50,6 +52,7 @@ pub mod signature;
|
|||
pub mod sigverify;
|
||||
pub mod sigverify_stage;
|
||||
pub mod streamer;
|
||||
pub mod system_contract;
|
||||
pub mod thin_client;
|
||||
pub mod timing;
|
||||
pub mod tpu;
|
||||
|
|
|
@ -52,7 +52,7 @@ impl Mint {
|
|||
|
||||
pub fn create_transactions(&self) -> Vec<Transaction> {
|
||||
let keypair = self.keypair();
|
||||
let tx = Transaction::new(&keypair, self.pubkey(), self.tokens, self.seed());
|
||||
let tx = Transaction::budget_new(&keypair, self.pubkey(), self.tokens, self.seed());
|
||||
vec![tx]
|
||||
}
|
||||
|
||||
|
@ -67,14 +67,14 @@ impl Mint {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use budget::Budget;
|
||||
use instruction::{Instruction, Plan};
|
||||
use ledger::Block;
|
||||
use transaction::{Instruction, Plan};
|
||||
|
||||
#[test]
|
||||
fn test_create_transactions() {
|
||||
let mut transactions = Mint::new(100).create_transactions().into_iter();
|
||||
let tx = transactions.next().unwrap();
|
||||
if let Instruction::NewContract(contract) = tx.instruction() {
|
||||
if let Some(Instruction::NewContract(contract)) = tx.instruction() {
|
||||
if let Plan::Budget(Budget::Pay(payment)) = contract.plan {
|
||||
assert_eq!(*tx.from(), payment.to);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ pub type BlobRecycler = Recycler<Blob>;
|
|||
pub const NUM_PACKETS: usize = 1024 * 8;
|
||||
pub const BLOB_SIZE: usize = (64 * 1024 - 128); // wikipedia says there should be 20b for ipv4 headers
|
||||
pub const BLOB_DATA_SIZE: usize = BLOB_SIZE - (BLOB_HEADER_SIZE * 2);
|
||||
pub const PACKET_DATA_SIZE: usize = 256;
|
||||
pub const PACKET_DATA_SIZE: usize = 512;
|
||||
pub const NUM_BLOBS: usize = (NUM_PACKETS * PACKET_DATA_SIZE) / BLOB_SIZE;
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq)]
|
||||
|
|
|
@ -226,7 +226,7 @@ mod tests {
|
|||
let bank = Bank::new(&alice);
|
||||
|
||||
let last_id = bank.last_id();
|
||||
let tx = Transaction::new(&alice.keypair(), bob_pubkey, 20, last_id);
|
||||
let tx = Transaction::system_move(&alice.keypair(), bob_pubkey, 20, last_id, 0);
|
||||
bank.process_transaction(&tx).expect("process transaction");
|
||||
|
||||
let request_processor = JsonRpcRequestProcessor::new(Arc::new(bank));
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
//! system smart contract
|
||||
|
||||
use bank::Account;
|
||||
use bincode::deserialize;
|
||||
use signature::Pubkey;
|
||||
use transaction::Transaction;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum SystemContract {
|
||||
/// Create a new account
|
||||
/// * Transaction::keys[0] - source
|
||||
/// * Transaction::keys[1] - new account key
|
||||
/// * tokens - number of tokens to transfer to the new account
|
||||
/// * space - memory to allocate if greater then zero
|
||||
/// * contract - the contract id of the new account
|
||||
CreateAccount {
|
||||
tokens: i64,
|
||||
space: u64,
|
||||
contract_id: Option<Pubkey>,
|
||||
},
|
||||
/// Assign account to a contract
|
||||
/// * Transaction::keys[0] - account to assign
|
||||
Assign { contract_id: Pubkey },
|
||||
/// Move tokens
|
||||
/// * Transaction::keys[0] - source
|
||||
/// * Transaction::keys[1] - destination
|
||||
Move { tokens: i64 },
|
||||
}
|
||||
|
||||
pub const SYSTEM_CONTRACT_ID: [u8; 32] = [0u8; 32];
|
||||
|
||||
impl SystemContract {
|
||||
pub fn check_id(contract_id: &Pubkey) -> bool {
|
||||
contract_id.as_ref() == SYSTEM_CONTRACT_ID
|
||||
}
|
||||
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&SYSTEM_CONTRACT_ID)
|
||||
}
|
||||
pub fn get_balance(account: &Account) -> i64 {
|
||||
account.tokens
|
||||
}
|
||||
pub fn process_transaction(tx: &Transaction, accounts: &mut [Account]) {
|
||||
let syscall: SystemContract = deserialize(&tx.userdata).unwrap();
|
||||
match syscall {
|
||||
SystemContract::CreateAccount {
|
||||
tokens,
|
||||
space,
|
||||
contract_id,
|
||||
} => {
|
||||
if !Self::check_id(&accounts[1].contract_id) {
|
||||
return;
|
||||
}
|
||||
if !Self::check_id(&accounts[0].contract_id) {
|
||||
return;
|
||||
}
|
||||
if space > 0 && !accounts[1].userdata.is_empty() {
|
||||
return;
|
||||
}
|
||||
accounts[0].tokens -= tokens;
|
||||
accounts[1].tokens += tokens;
|
||||
if let Some(id) = contract_id {
|
||||
accounts[1].contract_id = id;
|
||||
}
|
||||
accounts[1].userdata = vec![0; space as usize];
|
||||
}
|
||||
SystemContract::Assign { contract_id } => {
|
||||
if !Self::check_id(&accounts[0].contract_id) {
|
||||
return;
|
||||
}
|
||||
accounts[0].contract_id = contract_id;
|
||||
}
|
||||
SystemContract::Move { tokens } => {
|
||||
//bank should be verifying correctness
|
||||
accounts[0].tokens -= tokens;
|
||||
accounts[1].tokens += tokens;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -192,7 +192,7 @@ impl ThinClient {
|
|||
// In the future custom contracts would need their own introspection
|
||||
self.balances
|
||||
.get(pubkey)
|
||||
.map(Bank::get_balance_of_budget_payment_plan)
|
||||
.map(Bank::read_balance)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "nokey"))
|
||||
}
|
||||
|
||||
|
@ -429,7 +429,6 @@ pub fn poll_gossip_for_leader(leader_ncp: SocketAddr, timeout: Option<u64>) -> R
|
|||
mod tests {
|
||||
use super::*;
|
||||
use bank::Bank;
|
||||
use budget::Budget;
|
||||
use crdt::Node;
|
||||
use fullnode::Fullnode;
|
||||
use ledger::LedgerWriter;
|
||||
|
@ -437,7 +436,7 @@ mod tests {
|
|||
use mint::Mint;
|
||||
use signature::{Keypair, KeypairUtil};
|
||||
use std::fs::remove_dir_all;
|
||||
use transaction::{Instruction, Plan};
|
||||
use system_contract::SystemContract;
|
||||
|
||||
fn tmp_ledger(name: &str, mint: &Mint) -> String {
|
||||
use std::env;
|
||||
|
@ -542,10 +541,9 @@ mod tests {
|
|||
let last_id = client.get_last_id();
|
||||
|
||||
let mut tr2 = Transaction::new(&alice.keypair(), bob_pubkey, 501, last_id);
|
||||
let mut instruction2 = tr2.instruction();
|
||||
if let Instruction::NewContract(ref mut contract) = instruction2 {
|
||||
contract.tokens = 502;
|
||||
contract.plan = Plan::Budget(Budget::new_payment(502, bob_pubkey));
|
||||
let mut instruction2 = deserialize(&tr2.userdata).unwrap();
|
||||
if let SystemContract::Move { ref mut tokens } = instruction2 {
|
||||
*tokens = 502;
|
||||
}
|
||||
tr2.userdata = serialize(&instruction2).unwrap();
|
||||
let signature = client.transfer_signed(&tr2).unwrap();
|
||||
|
@ -595,9 +593,8 @@ mod tests {
|
|||
let signature = client
|
||||
.transfer(500, &alice.keypair(), bob_pubkey, &last_id)
|
||||
.unwrap();
|
||||
sleep(Duration::from_millis(100));
|
||||
|
||||
assert!(client.check_signature(&signature));
|
||||
assert!(client.poll_for_signature(&signature).is_ok());
|
||||
|
||||
server.close().unwrap();
|
||||
remove_dir_all(ledger_path).unwrap();
|
||||
|
|
|
@ -2,80 +2,19 @@
|
|||
|
||||
use bincode::{deserialize, serialize};
|
||||
use budget::{Budget, Condition};
|
||||
use budget_contract::BudgetContract;
|
||||
use chrono::prelude::*;
|
||||
use hash::Hash;
|
||||
use payment_plan::{Payment, PaymentPlan, Witness};
|
||||
use instruction::{Contract, Instruction, Plan, Vote};
|
||||
use payment_plan::{Payment, PaymentPlan};
|
||||
use signature::{Keypair, KeypairUtil, Pubkey, Signature};
|
||||
use std::mem::size_of;
|
||||
use system_contract::SystemContract;
|
||||
|
||||
pub const SIGNED_DATA_OFFSET: usize = size_of::<Signature>();
|
||||
pub const SIG_OFFSET: usize = 0;
|
||||
pub const PUB_KEY_OFFSET: usize = size_of::<Signature>() + size_of::<u64>();
|
||||
|
||||
/// The type of payment plan. Each item must implement the PaymentPlan trait.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Plan {
|
||||
/// The builtin contract language Budget.
|
||||
Budget(Budget),
|
||||
}
|
||||
|
||||
// A proxy for the underlying DSL.
|
||||
impl PaymentPlan for Plan {
|
||||
fn final_payment(&self) -> Option<Payment> {
|
||||
match self {
|
||||
Plan::Budget(budget) => budget.final_payment(),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify(&self, spendable_tokens: i64) -> bool {
|
||||
match self {
|
||||
Plan::Budget(budget) => budget.verify(spendable_tokens),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_witness(&mut self, witness: &Witness, from: &Pubkey) {
|
||||
match self {
|
||||
Plan::Budget(budget) => budget.apply_witness(witness, from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A smart contract.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Contract {
|
||||
/// The number of tokens allocated to the `Plan` and any transaction fees.
|
||||
pub tokens: i64,
|
||||
pub plan: Plan,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Vote {
|
||||
/// We send some gossip specific membership information through the vote to shortcut
|
||||
/// liveness voting
|
||||
/// The version of the CRDT struct that the last_id of this network voted with
|
||||
pub version: u64,
|
||||
/// The version of the CRDT struct that has the same network configuration as this one
|
||||
pub contact_info_version: u64,
|
||||
// TODO: add signature of the state here as well
|
||||
}
|
||||
|
||||
/// An instruction to progress the smart contract.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Instruction {
|
||||
/// Declare and instantiate `Contract`.
|
||||
NewContract(Contract),
|
||||
|
||||
/// Tell a payment plan acknowledge the given `DateTime` has past.
|
||||
ApplyTimestamp(DateTime<Utc>),
|
||||
|
||||
/// Tell the payment plan that the `NewContract` with `Signature` has been
|
||||
/// signed by the containing transaction's `Pubkey`.
|
||||
ApplySignature(Signature),
|
||||
|
||||
/// Vote for a PoH that is equal to the lastid of this transaction
|
||||
NewVote(Vote),
|
||||
}
|
||||
|
||||
/// An instruction signed by a client with `Pubkey`.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Transaction {
|
||||
|
@ -88,6 +27,8 @@ pub struct Transaction {
|
|||
/// In the future which key pays the fee and which keys have signatures would be configurable.
|
||||
/// * keys[1] - Typically this is the contract context or the recipient of the tokens
|
||||
pub keys: Vec<Pubkey>,
|
||||
/// the contract id to execute
|
||||
pub contract_id: Pubkey,
|
||||
|
||||
/// The ID of a recent ledger entry.
|
||||
pub last_id: Hash,
|
||||
|
@ -110,6 +51,7 @@ impl Transaction {
|
|||
fn new_with_userdata(
|
||||
from_keypair: &Keypair,
|
||||
transaction_keys: &[Pubkey],
|
||||
contract_id: Pubkey,
|
||||
userdata: Vec<u8>,
|
||||
last_id: Hash,
|
||||
fee: i64,
|
||||
|
@ -120,6 +62,7 @@ impl Transaction {
|
|||
let mut tx = Transaction {
|
||||
signature: Signature::default(),
|
||||
keys,
|
||||
contract_id,
|
||||
last_id,
|
||||
fee,
|
||||
userdata,
|
||||
|
@ -127,20 +70,8 @@ impl Transaction {
|
|||
tx.sign(from_keypair);
|
||||
tx
|
||||
}
|
||||
/// Create a signed transaction from the given `Instruction`.
|
||||
fn new_from_instruction(
|
||||
from_keypair: &Keypair,
|
||||
contract: Pubkey,
|
||||
instruction: &Instruction,
|
||||
last_id: Hash,
|
||||
fee: i64,
|
||||
) -> Self {
|
||||
let userdata = serialize(instruction).unwrap();
|
||||
Self::new_with_userdata(from_keypair, &[contract], userdata, last_id, fee)
|
||||
}
|
||||
|
||||
/// Create and sign a new Transaction. Used for unit-testing.
|
||||
pub fn new_taxed(
|
||||
pub fn budget_new_taxed(
|
||||
from_keypair: &Keypair,
|
||||
contract: Pubkey,
|
||||
tokens: i64,
|
||||
|
@ -154,46 +85,80 @@ impl Transaction {
|
|||
let budget = Budget::Pay(payment);
|
||||
let plan = Plan::Budget(budget);
|
||||
let instruction = Instruction::NewContract(Contract { plan, tokens });
|
||||
Self::new_from_instruction(from_keypair, contract, &instruction, last_id, fee)
|
||||
let userdata = serialize(&instruction).unwrap();
|
||||
Self::new_with_userdata(
|
||||
from_keypair,
|
||||
&[contract],
|
||||
BudgetContract::id(),
|
||||
userdata,
|
||||
last_id,
|
||||
fee,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create and sign a new Transaction. Used for unit-testing.
|
||||
pub fn new(from_keypair: &Keypair, to: Pubkey, tokens: i64, last_id: Hash) -> Self {
|
||||
Self::new_taxed(from_keypair, to, tokens, 0, last_id)
|
||||
pub fn budget_new(from_keypair: &Keypair, to: Pubkey, tokens: i64, last_id: Hash) -> Self {
|
||||
Self::budget_new_taxed(from_keypair, to, tokens, 0, last_id)
|
||||
}
|
||||
|
||||
/// Create and sign a new Witness Timestamp. Used for unit-testing.
|
||||
pub fn new_timestamp(
|
||||
pub fn budget_new_timestamp(
|
||||
from_keypair: &Keypair,
|
||||
contract: Pubkey,
|
||||
to: Pubkey,
|
||||
dt: DateTime<Utc>,
|
||||
last_id: Hash,
|
||||
) -> Self {
|
||||
let instruction = Instruction::ApplyTimestamp(dt);
|
||||
Self::new_from_instruction(from_keypair, contract, &instruction, last_id, 0)
|
||||
let userdata = serialize(&instruction).unwrap();
|
||||
Self::new_with_userdata(
|
||||
from_keypair,
|
||||
&[contract, to],
|
||||
BudgetContract::id(),
|
||||
userdata,
|
||||
last_id,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create and sign a new Witness Signature. Used for unit-testing.
|
||||
pub fn new_signature(
|
||||
pub fn budget_new_signature(
|
||||
from_keypair: &Keypair,
|
||||
contract: Pubkey,
|
||||
to: Pubkey,
|
||||
signature: Signature,
|
||||
last_id: Hash,
|
||||
) -> Self {
|
||||
let instruction = Instruction::ApplySignature(signature);
|
||||
Self::new_from_instruction(from_keypair, contract, &instruction, last_id, 0)
|
||||
let userdata = serialize(&instruction).unwrap();
|
||||
Self::new_with_userdata(
|
||||
from_keypair,
|
||||
&[contract, to],
|
||||
BudgetContract::id(),
|
||||
userdata,
|
||||
last_id,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_vote(from_keypair: &Keypair, vote: Vote, last_id: Hash, fee: i64) -> Self {
|
||||
pub fn budget_new_vote(from_keypair: &Keypair, vote: Vote, last_id: Hash, fee: i64) -> Self {
|
||||
let instruction = Instruction::NewVote(vote);
|
||||
let userdata = serialize(&instruction).expect("serealize instruction");
|
||||
Self::new_with_userdata(from_keypair, &[], userdata, last_id, fee)
|
||||
let userdata = serialize(&instruction).expect("serialize instruction");
|
||||
Self::new_with_userdata(
|
||||
from_keypair,
|
||||
&[],
|
||||
BudgetContract::id(),
|
||||
userdata,
|
||||
last_id,
|
||||
fee,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create and sign a postdated Transaction. Used for unit-testing.
|
||||
pub fn new_on_date(
|
||||
pub fn budget_new_on_date(
|
||||
from_keypair: &Keypair,
|
||||
to: Pubkey,
|
||||
contract: Pubkey,
|
||||
dt: DateTime<Utc>,
|
||||
tokens: i64,
|
||||
last_id: Hash,
|
||||
|
@ -205,14 +170,73 @@ impl Transaction {
|
|||
);
|
||||
let plan = Plan::Budget(budget);
|
||||
let instruction = Instruction::NewContract(Contract { plan, tokens });
|
||||
let userdata = serialize(&instruction).expect("serealize instruction");
|
||||
Self::new_with_userdata(from_keypair, &[to], userdata, last_id, 0)
|
||||
let userdata = serialize(&instruction).expect("serialize instruction");
|
||||
Self::new_with_userdata(
|
||||
from_keypair,
|
||||
&[contract],
|
||||
BudgetContract::id(),
|
||||
userdata,
|
||||
last_id,
|
||||
0,
|
||||
)
|
||||
}
|
||||
/// Create and sign new SystemContract::CreateAccount transaction
|
||||
pub fn system_new_create(
|
||||
from_keypair: &Keypair,
|
||||
to: Pubkey,
|
||||
last_id: Hash,
|
||||
tokens: i64,
|
||||
space: u64,
|
||||
contract_id: Option<Pubkey>,
|
||||
fee: i64,
|
||||
) -> Self {
|
||||
let create = SystemContract::CreateAccount {
|
||||
tokens, //TODO, the tokens to allocate might need to be higher then 0 in the future
|
||||
space,
|
||||
contract_id,
|
||||
};
|
||||
Transaction::new_with_userdata(
|
||||
from_keypair,
|
||||
&[to],
|
||||
SystemContract::id(),
|
||||
serialize(&create).unwrap(),
|
||||
last_id,
|
||||
fee,
|
||||
)
|
||||
}
|
||||
/// Create and sign new SystemContract::CreateAccount transaction with some defaults
|
||||
pub fn system_new(from_keypair: &Keypair, to: Pubkey, tokens: i64, last_id: Hash) -> Self {
|
||||
Transaction::system_new_create(from_keypair, to, last_id, tokens, 0, None, 0)
|
||||
}
|
||||
/// Create and sign new SystemContract::Move transaction
|
||||
pub fn system_move(
|
||||
from_keypair: &Keypair,
|
||||
to: Pubkey,
|
||||
tokens: i64,
|
||||
last_id: Hash,
|
||||
fee: i64,
|
||||
) -> Self {
|
||||
let create = SystemContract::Move { tokens };
|
||||
Transaction::new_with_userdata(
|
||||
from_keypair,
|
||||
&[to],
|
||||
SystemContract::id(),
|
||||
serialize(&create).unwrap(),
|
||||
last_id,
|
||||
fee,
|
||||
)
|
||||
}
|
||||
/// Create and sign new SystemContract::Move transaction
|
||||
pub fn new(from_keypair: &Keypair, to: Pubkey, tokens: i64, last_id: Hash) -> Self {
|
||||
Transaction::system_move(from_keypair, to, tokens, last_id, 0)
|
||||
}
|
||||
|
||||
/// Get the transaction data to sign.
|
||||
fn get_sign_data(&self) -> Vec<u8> {
|
||||
let mut data = serialize(&(&self.keys)).expect("serialize keys");
|
||||
|
||||
let contract_id = serialize(&(&self.contract_id)).expect("serialize contract_id");
|
||||
data.extend_from_slice(&contract_id);
|
||||
|
||||
let last_id_data = serialize(&(&self.last_id)).expect("serialize last_id");
|
||||
data.extend_from_slice(&last_id_data);
|
||||
|
||||
|
@ -237,19 +261,8 @@ impl Transaction {
|
|||
.verify(&self.from().as_ref(), &self.get_sign_data())
|
||||
}
|
||||
|
||||
/// Verify only the payment plan.
|
||||
pub fn verify_plan(&self) -> bool {
|
||||
let instruction = deserialize(&self.userdata);
|
||||
if let Ok(Instruction::NewContract(contract)) = instruction {
|
||||
self.fee >= 0
|
||||
&& self.fee <= contract.tokens
|
||||
&& contract.plan.verify(contract.tokens - self.fee)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
pub fn vote(&self) -> Option<(Pubkey, Vote, Hash)> {
|
||||
if let Instruction::NewVote(vote) = self.instruction() {
|
||||
if let Some(Instruction::NewVote(vote)) = self.instruction() {
|
||||
Some((*self.from(), vote, self.last_id))
|
||||
} else {
|
||||
None
|
||||
|
@ -258,8 +271,18 @@ impl Transaction {
|
|||
pub fn from(&self) -> &Pubkey {
|
||||
&self.keys[0]
|
||||
}
|
||||
pub fn instruction(&self) -> Instruction {
|
||||
deserialize(&self.userdata).unwrap()
|
||||
pub fn instruction(&self) -> Option<Instruction> {
|
||||
deserialize(&self.userdata).ok()
|
||||
}
|
||||
/// Verify only the payment plan.
|
||||
pub fn verify_plan(&self) -> bool {
|
||||
if let Some(Instruction::NewContract(contract)) = self.instruction() {
|
||||
self.fee >= 0
|
||||
&& self.fee <= contract.tokens
|
||||
&& contract.plan.verify(contract.tokens - self.fee)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,7 +290,7 @@ pub fn test_tx() -> Transaction {
|
|||
let keypair1 = Keypair::new();
|
||||
let pubkey1 = keypair1.pubkey();
|
||||
let zero = Hash::default();
|
||||
Transaction::new(&keypair1, pubkey1, 42, zero)
|
||||
Transaction::system_new(&keypair1, pubkey1, 42, zero)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -292,7 +315,7 @@ mod tests {
|
|||
fn test_claim() {
|
||||
let keypair = Keypair::new();
|
||||
let zero = Hash::default();
|
||||
let tx0 = Transaction::new(&keypair, keypair.pubkey(), 42, zero);
|
||||
let tx0 = Transaction::budget_new(&keypair, keypair.pubkey(), 42, zero);
|
||||
assert!(tx0.verify_plan());
|
||||
}
|
||||
|
||||
|
@ -302,7 +325,7 @@ mod tests {
|
|||
let keypair0 = Keypair::new();
|
||||
let keypair1 = Keypair::new();
|
||||
let pubkey1 = keypair1.pubkey();
|
||||
let tx0 = Transaction::new(&keypair0, pubkey1, 42, zero);
|
||||
let tx0 = Transaction::budget_new(&keypair0, pubkey1, 42, zero);
|
||||
assert!(tx0.verify_plan());
|
||||
}
|
||||
|
||||
|
@ -311,9 +334,9 @@ mod tests {
|
|||
let zero = Hash::default();
|
||||
let keypair0 = Keypair::new();
|
||||
let pubkey1 = Keypair::new().pubkey();
|
||||
assert!(Transaction::new_taxed(&keypair0, pubkey1, 1, 1, zero).verify_plan());
|
||||
assert!(!Transaction::new_taxed(&keypair0, pubkey1, 1, 2, zero).verify_plan());
|
||||
assert!(!Transaction::new_taxed(&keypair0, pubkey1, 1, -1, zero).verify_plan());
|
||||
assert!(Transaction::budget_new_taxed(&keypair0, pubkey1, 1, 1, zero).verify_plan());
|
||||
assert!(!Transaction::budget_new_taxed(&keypair0, pubkey1, 1, 2, zero).verify_plan());
|
||||
assert!(!Transaction::budget_new_taxed(&keypair0, pubkey1, 1, -1, zero).verify_plan());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -329,6 +352,7 @@ mod tests {
|
|||
keys: vec![],
|
||||
last_id: Default::default(),
|
||||
signature: Default::default(),
|
||||
contract_id: Default::default(),
|
||||
fee: 0,
|
||||
userdata,
|
||||
};
|
||||
|
@ -342,8 +366,8 @@ mod tests {
|
|||
let zero = Hash::default();
|
||||
let keypair = Keypair::new();
|
||||
let pubkey = keypair.pubkey();
|
||||
let mut tx = Transaction::new(&keypair, pubkey, 42, zero);
|
||||
let mut instruction = tx.instruction();
|
||||
let mut tx = Transaction::budget_new(&keypair, pubkey, 42, zero);
|
||||
let mut instruction = tx.instruction().unwrap();
|
||||
if let Instruction::NewContract(ref mut contract) = instruction {
|
||||
contract.tokens = 1_000_000; // <-- attack, part 1!
|
||||
if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan {
|
||||
|
@ -362,9 +386,9 @@ mod tests {
|
|||
let thief_keypair = Keypair::new();
|
||||
let pubkey1 = keypair1.pubkey();
|
||||
let zero = Hash::default();
|
||||
let mut tx = Transaction::new(&keypair0, pubkey1, 42, zero);
|
||||
let mut tx = Transaction::budget_new(&keypair0, pubkey1, 42, zero);
|
||||
let mut instruction = tx.instruction();
|
||||
if let Instruction::NewContract(ref mut contract) = instruction {
|
||||
if let Some(Instruction::NewContract(ref mut contract)) = instruction {
|
||||
if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan {
|
||||
payment.to = thief_keypair.pubkey(); // <-- attack!
|
||||
}
|
||||
|
@ -416,8 +440,8 @@ mod tests {
|
|||
let keypair0 = Keypair::new();
|
||||
let keypair1 = Keypair::new();
|
||||
let zero = Hash::default();
|
||||
let mut tx = Transaction::new(&keypair0, keypair1.pubkey(), 1, zero);
|
||||
let mut instruction = tx.instruction();
|
||||
let mut tx = Transaction::budget_new(&keypair0, keypair1.pubkey(), 1, zero);
|
||||
let mut instruction = tx.instruction().unwrap();
|
||||
if let Instruction::NewContract(ref mut contract) = instruction {
|
||||
if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan {
|
||||
payment.tokens = 2; // <-- attack!
|
||||
|
@ -427,7 +451,7 @@ mod tests {
|
|||
assert!(!tx.verify_plan());
|
||||
|
||||
// Also, ensure all branchs of the plan spend all tokens
|
||||
let mut instruction = tx.instruction();
|
||||
let mut instruction = tx.instruction().unwrap();
|
||||
if let Instruction::NewContract(ref mut contract) = instruction {
|
||||
if let Plan::Budget(Budget::Pay(ref mut payment)) = contract.plan {
|
||||
payment.tokens = 0; // <-- whoops!
|
||||
|
|
|
@ -261,7 +261,7 @@ pub mod tests {
|
|||
bank.register_entry_id(&cur_hash);
|
||||
cur_hash = hash(&cur_hash.as_ref());
|
||||
|
||||
let tx0 = Transaction::new(
|
||||
let tx0 = Transaction::system_new(
|
||||
&mint.keypair(),
|
||||
bob_keypair.pubkey(),
|
||||
transfer_amount,
|
||||
|
|
|
@ -45,7 +45,7 @@ pub fn create_new_signed_vote_blob(
|
|||
debug!("voting on {:?}", &last_id.as_ref()[..8]);
|
||||
wcrdt.new_vote(*last_id)
|
||||
}?;
|
||||
let tx = Transaction::new_vote(&keypair, vote, *last_id, 0);
|
||||
let tx = Transaction::budget_new_vote(&keypair, vote, *last_id, 0);
|
||||
{
|
||||
let mut blob = shared_blob.write().unwrap();
|
||||
let bytes = serialize(&tx)?;
|
||||
|
@ -227,6 +227,7 @@ pub mod tests {
|
|||
use crdt::{Crdt, Node, NodeInfo};
|
||||
use entry::next_entry;
|
||||
use hash::{hash, Hash};
|
||||
use instruction::Vote;
|
||||
use logger;
|
||||
use mint::Mint;
|
||||
use packet::BlobRecycler;
|
||||
|
@ -235,7 +236,7 @@ pub mod tests {
|
|||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use transaction::{Transaction, Vote};
|
||||
use transaction::Transaction;
|
||||
|
||||
/// Ensure the VoteStage issues votes at the expected cadence
|
||||
#[test]
|
||||
|
@ -291,7 +292,7 @@ pub mod tests {
|
|||
|
||||
// give the leader some tokens
|
||||
let give_leader_tokens_tx =
|
||||
Transaction::new(&mint.keypair(), leader_pubkey.clone(), 100, entry.id);
|
||||
Transaction::system_new(&mint.keypair(), leader_pubkey.clone(), 100, entry.id);
|
||||
bank.process_transaction(&give_leader_tokens_tx).unwrap();
|
||||
|
||||
leader_crdt.set_leader(leader_pubkey);
|
||||
|
|
Loading…
Reference in New Issue