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:
anatoly yakovenko 2018-09-17 13:36:31 -07:00 committed by GitHub
parent 072b244575
commit 6ec0e42220
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 951 additions and 450 deletions

View File

@ -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);
}

469
src/budget_contract.rs Normal file
View File

@ -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()))
);
}
}

View File

@ -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]

View File

@ -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]));

68
src/instruction.rs Normal file
View File

@ -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),
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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)]

View File

@ -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));

80
src/system_contract.rs Normal file
View File

@ -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;
}
}
}
}

View File

@ -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();

View File

@ -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!

View File

@ -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,

View File

@ -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);