committed by
Michael Vines
parent
c23fa289c3
commit
88d6fea999
102
src/bank.rs
102
src/bank.rs
@@ -1,4 +1,4 @@
|
||||
//! The `bank` module tracks client accounts and the progress of smart
|
||||
//! The `bank` module tracks client balances and the progress of smart
|
||||
//! contracts. It offers a high-level API that signs transactions
|
||||
//! on behalf of the caller, and a low-level API for when they have
|
||||
//! already been signed and verified.
|
||||
@@ -65,20 +65,11 @@ pub enum BankError {
|
||||
}
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
/// The state of all accounts and contracts after processing its entries.
|
||||
pub struct Bank {
|
||||
/// A map of account public keys to the balance in that account.
|
||||
accounts: RwLock<HashMap<Pubkey, Account>>,
|
||||
balances: RwLock<HashMap<Pubkey, i64>>,
|
||||
|
||||
/// A map of smart contract transaction signatures to what remains of its payment
|
||||
/// plan. Each transaction that targets the plan should cause it to be reduced.
|
||||
@@ -109,7 +100,7 @@ pub struct Bank {
|
||||
impl Default for Bank {
|
||||
fn default() -> Self {
|
||||
Bank {
|
||||
accounts: RwLock::new(HashMap::new()),
|
||||
balances: RwLock::new(HashMap::new()),
|
||||
pending: RwLock::new(HashMap::new()),
|
||||
last_ids: RwLock::new(VecDeque::new()),
|
||||
last_ids_sigs: RwLock::new(HashMap::new()),
|
||||
@@ -130,7 +121,7 @@ impl Bank {
|
||||
/// Create an Bank using a deposit.
|
||||
pub fn new_from_deposit(deposit: &Payment) -> Self {
|
||||
let bank = Self::default();
|
||||
bank.apply_payment(deposit, &mut bank.accounts.write().unwrap());
|
||||
bank.apply_payment(deposit, &mut bank.balances.write().unwrap());
|
||||
bank
|
||||
}
|
||||
|
||||
@@ -146,11 +137,8 @@ impl Bank {
|
||||
}
|
||||
|
||||
/// Commit funds to the `payment.to` party.
|
||||
fn apply_payment(&self, payment: &Payment, accounts: &mut HashMap<Pubkey, Account>) {
|
||||
accounts
|
||||
.entry(payment.to)
|
||||
.or_insert_with(Account::default)
|
||||
.tokens += payment.tokens;
|
||||
fn apply_payment(&self, payment: &Payment, balances: &mut HashMap<Pubkey, i64>) {
|
||||
*balances.entry(payment.to).or_insert(0) += payment.tokens;
|
||||
}
|
||||
|
||||
/// Return the last entry ID registered.
|
||||
@@ -247,14 +235,10 @@ impl Bank {
|
||||
|
||||
/// Deduct tokens from the 'from' address the account has sufficient
|
||||
/// funds and isn't a duplicate.
|
||||
fn apply_debits(
|
||||
&self,
|
||||
tx: &Transaction,
|
||||
accounts: &mut HashMap<Pubkey, Account>,
|
||||
) -> Result<()> {
|
||||
fn apply_debits(&self, tx: &Transaction, bals: &mut HashMap<Pubkey, i64>) -> Result<()> {
|
||||
let mut purge = false;
|
||||
{
|
||||
let option = accounts.get_mut(&tx.from);
|
||||
let option = bals.get_mut(&tx.from);
|
||||
if option.is_none() {
|
||||
// TODO: this is gnarly because the counters are static atomics
|
||||
if !self.is_leader {
|
||||
@@ -275,19 +259,19 @@ impl Bank {
|
||||
return Err(BankError::NegativeTokens);
|
||||
}
|
||||
|
||||
if bal.tokens < contract.tokens {
|
||||
if *bal < contract.tokens {
|
||||
self.forget_signature_with_last_id(&tx.signature, &tx.last_id);
|
||||
return Err(BankError::InsufficientFunds(tx.from));
|
||||
} else if bal.tokens == contract.tokens {
|
||||
} else if *bal == contract.tokens {
|
||||
purge = true;
|
||||
} else {
|
||||
bal.tokens -= contract.tokens;
|
||||
*bal -= contract.tokens;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if purge {
|
||||
accounts.remove(&tx.from);
|
||||
bals.remove(&tx.from);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -295,12 +279,12 @@ impl Bank {
|
||||
|
||||
/// Apply only a transaction's credits.
|
||||
/// Note: It is safe to apply credits from multiple transactions in parallel.
|
||||
fn apply_credits(&self, tx: &Transaction, accounts: &mut HashMap<Pubkey, Account>) {
|
||||
fn apply_credits(&self, tx: &Transaction, balances: &mut HashMap<Pubkey, i64>) {
|
||||
match &tx.instruction {
|
||||
Instruction::NewContract(contract) => {
|
||||
let plan = contract.plan.clone();
|
||||
if let Some(payment) = plan.final_payment() {
|
||||
self.apply_payment(&payment, accounts);
|
||||
self.apply_payment(&payment, balances);
|
||||
} else {
|
||||
let mut pending = self
|
||||
.pending
|
||||
@@ -321,26 +305,13 @@ impl Bank {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn save_data(&self, tx: &Transaction, accounts: &mut HashMap<Pubkey, Account>) {
|
||||
//TODO This is a temporary implementation until the full rules on memory management for
|
||||
//smart contracts are implemented. See github issue #953
|
||||
if !tx.userdata.is_empty() {
|
||||
if let Some(ref mut account) = accounts.get_mut(&tx.from) {
|
||||
if account.userdata.len() != tx.userdata.len() {
|
||||
account.userdata.resize(tx.userdata.len(), 0);
|
||||
}
|
||||
account.userdata.copy_from_slice(&tx.userdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a Transaction. If it contains a payment plan that requires a witness
|
||||
/// to progress, the payment plan will be stored in the bank.
|
||||
pub fn process_transaction(&self, tx: &Transaction) -> Result<()> {
|
||||
let accounts = &mut self.accounts.write().unwrap();
|
||||
self.apply_debits(tx, accounts)?;
|
||||
self.apply_credits(tx, accounts);
|
||||
self.save_data(tx, accounts);
|
||||
let bals = &mut self.balances.write().unwrap();
|
||||
self.apply_debits(tx, bals)?;
|
||||
self.apply_credits(tx, bals);
|
||||
self.transaction_count.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
@@ -348,13 +319,13 @@ impl Bank {
|
||||
/// Process a batch of transactions.
|
||||
#[must_use]
|
||||
pub fn process_transactions(&self, txs: Vec<Transaction>) -> Vec<Result<Transaction>> {
|
||||
let accounts = &mut self.accounts.write().unwrap();
|
||||
let bals = &mut self.balances.write().unwrap();
|
||||
debug!("processing Transactions {}", txs.len());
|
||||
let txs_len = txs.len();
|
||||
let now = Instant::now();
|
||||
let results: Vec<_> = txs
|
||||
.into_iter()
|
||||
.map(|tx| self.apply_debits(&tx, accounts).map(|_| tx))
|
||||
.map(|tx| self.apply_debits(&tx, bals).map(|_| tx))
|
||||
.collect(); // Calling collect() here forces all debits to complete before moving on.
|
||||
|
||||
let debits = now.elapsed();
|
||||
@@ -364,7 +335,7 @@ impl Bank {
|
||||
.into_iter()
|
||||
.map(|result| {
|
||||
result.map(|tx| {
|
||||
self.apply_credits(&tx, accounts);
|
||||
self.apply_credits(&tx, bals);
|
||||
tx
|
||||
})
|
||||
})
|
||||
@@ -496,7 +467,7 @@ impl Bank {
|
||||
None
|
||||
}.expect("invalid ledger, needs to start with a contract");
|
||||
|
||||
self.apply_payment(&deposit, &mut self.accounts.write().unwrap());
|
||||
self.apply_payment(&deposit, &mut self.balances.write().unwrap());
|
||||
}
|
||||
self.register_entry_id(&entry0.id);
|
||||
self.register_entry_id(&entry1.id);
|
||||
@@ -527,7 +498,7 @@ impl Bank {
|
||||
{
|
||||
e.get_mut().apply_witness(&Witness::Signature, &from);
|
||||
if let Some(payment) = e.get().final_payment() {
|
||||
self.apply_payment(&payment, &mut self.accounts.write().unwrap());
|
||||
self.apply_payment(&payment, &mut self.balances.write().unwrap());
|
||||
e.remove_entry();
|
||||
}
|
||||
};
|
||||
@@ -550,7 +521,7 @@ impl Bank {
|
||||
for (key, plan) in pending.iter_mut() {
|
||||
plan.apply_witness(&Witness::Timestamp(dt), &from);
|
||||
if let Some(payment) = plan.final_payment() {
|
||||
self.apply_payment(&payment, &mut self.accounts.write().unwrap());
|
||||
self.apply_payment(&payment, &mut self.balances.write().unwrap());
|
||||
completed.push(key.clone());
|
||||
}
|
||||
}
|
||||
@@ -593,15 +564,11 @@ impl Bank {
|
||||
}
|
||||
|
||||
pub fn get_balance(&self, pubkey: &Pubkey) -> i64 {
|
||||
self.get_account(pubkey).map(|a| a.tokens).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
|
||||
let accounts = self
|
||||
.accounts
|
||||
let bals = self
|
||||
.balances
|
||||
.read()
|
||||
.expect("'accounts' read lock in get_balance");
|
||||
accounts.get(pubkey).cloned()
|
||||
.expect("'balances' read lock in get_balance");
|
||||
bals.get(pubkey).cloned().unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn transaction_count(&self) -> usize {
|
||||
@@ -715,21 +682,6 @@ mod tests {
|
||||
assert_eq!(bank.get_balance(&pubkey), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_userdata() {
|
||||
let mint = Mint::new(10_000);
|
||||
let bank = Bank::new(&mint);
|
||||
let pubkey = mint.keypair().pubkey();
|
||||
|
||||
let mut tx = Transaction::new(&mint.keypair(), pubkey, 0, bank.last_id());
|
||||
tx.userdata = vec![1, 2, 3];
|
||||
let rv = bank.process_transaction(&tx);
|
||||
assert!(rv.is_ok());
|
||||
let account = bank.get_account(&pubkey);
|
||||
assert!(account.is_some());
|
||||
assert_eq!(account.unwrap().userdata, vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_on_date() {
|
||||
let mint = Mint::new(1);
|
||||
|
@@ -91,7 +91,7 @@ fn main() -> () {
|
||||
};
|
||||
|
||||
let mut client = mk_client(&repl_clone);
|
||||
let previous_balance = client.poll_get_balance(&leader_pubkey).unwrap_or(0);
|
||||
let previous_balance = client.poll_get_balance(&leader_pubkey).unwrap();
|
||||
eprintln!("balance is {}", previous_balance);
|
||||
|
||||
if previous_balance == 0 {
|
||||
|
@@ -254,7 +254,7 @@ fn process_command(
|
||||
"Requesting airdrop of {:?} tokens from {}",
|
||||
tokens, config.drone_addr
|
||||
);
|
||||
let previous_balance = client.poll_get_balance(&config.id.pubkey()).unwrap_or(0);
|
||||
let previous_balance = client.poll_get_balance(&config.id.pubkey())?;
|
||||
request_airdrop(&config.drone_addr, &config.id.pubkey(), tokens as u64)?;
|
||||
|
||||
// TODO: return airdrop Result from Drone instead of polling the
|
||||
@@ -262,10 +262,7 @@ fn process_command(
|
||||
let mut current_balance = previous_balance;
|
||||
for _ in 0..20 {
|
||||
sleep(Duration::from_millis(500));
|
||||
current_balance = client
|
||||
.poll_get_balance(&config.id.pubkey())
|
||||
.unwrap_or(previous_balance);
|
||||
|
||||
current_balance = client.poll_get_balance(&config.id.pubkey())?;
|
||||
if previous_balance != current_balance {
|
||||
break;
|
||||
}
|
||||
|
@@ -691,7 +691,7 @@ mod tests {
|
||||
transactions.extend(large_transactions);
|
||||
|
||||
let entries0 = next_entries(&id, 0, transactions.clone());
|
||||
assert!(entries0.len() >= 2);
|
||||
assert!(entries0.len() > 2);
|
||||
assert!(entries0[0].has_more);
|
||||
assert!(!entries0[entries0.len() - 1].has_more);
|
||||
assert!(entries0.verify(&id));
|
||||
|
@@ -1,13 +1,12 @@
|
||||
//! The `request` module defines the messages for the thin client.
|
||||
|
||||
use bank::Account;
|
||||
use hash::Hash;
|
||||
use signature::{Pubkey, Signature};
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(large_enum_variant))]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub enum Request {
|
||||
GetAccount { key: Pubkey },
|
||||
GetBalance { key: Pubkey },
|
||||
GetLastId,
|
||||
GetTransactionCount,
|
||||
GetSignature { signature: Signature },
|
||||
@@ -23,20 +22,9 @@ impl Request {
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Response {
|
||||
Account {
|
||||
key: Pubkey,
|
||||
account: Option<Account>,
|
||||
},
|
||||
LastId {
|
||||
id: Hash,
|
||||
},
|
||||
TransactionCount {
|
||||
transaction_count: u64,
|
||||
},
|
||||
SignatureStatus {
|
||||
signature_status: bool,
|
||||
},
|
||||
Finality {
|
||||
time: usize,
|
||||
},
|
||||
Balance { key: Pubkey, val: i64 },
|
||||
LastId { id: Hash },
|
||||
TransactionCount { transaction_count: u64 },
|
||||
SignatureStatus { signature_status: bool },
|
||||
Finality { time: usize },
|
||||
}
|
||||
|
@@ -22,10 +22,10 @@ impl RequestProcessor {
|
||||
rsp_addr: SocketAddr,
|
||||
) -> Option<(Response, SocketAddr)> {
|
||||
match msg {
|
||||
Request::GetAccount { key } => {
|
||||
let account = self.bank.get_account(&key);
|
||||
let rsp = (Response::Account { key, account }, rsp_addr);
|
||||
info!("Response::Account {:?}", rsp);
|
||||
Request::GetBalance { key } => {
|
||||
let val = self.bank.get_balance(&key);
|
||||
let rsp = (Response::Balance { key, val }, rsp_addr);
|
||||
info!("Response::Balance {:?}", rsp);
|
||||
Some(rsp)
|
||||
}
|
||||
Request::GetLastId => {
|
||||
|
@@ -3,7 +3,6 @@
|
||||
//! messages to the network directly. The binary encoding of its messages are
|
||||
//! unstable and may change in future releases.
|
||||
|
||||
use bank::Account;
|
||||
use bincode::{deserialize, serialize};
|
||||
use hash::Hash;
|
||||
use request::{Request, Response};
|
||||
@@ -28,7 +27,7 @@ pub struct ThinClient {
|
||||
transactions_socket: UdpSocket,
|
||||
last_id: Option<Hash>,
|
||||
transaction_count: u64,
|
||||
balances: HashMap<Pubkey, Account>,
|
||||
balances: HashMap<Pubkey, i64>,
|
||||
signature_status: bool,
|
||||
finality: Option<usize>,
|
||||
}
|
||||
@@ -66,15 +65,9 @@ impl ThinClient {
|
||||
|
||||
pub fn process_response(&mut self, resp: &Response) {
|
||||
match *resp {
|
||||
Response::Account {
|
||||
key,
|
||||
account: Some(ref account),
|
||||
} => {
|
||||
trace!("Response account {:?} {:?}", key, account);
|
||||
self.balances.insert(key, account.clone());
|
||||
}
|
||||
Response::Account { key, account: None } => {
|
||||
debug!("Response account {}: None ", key);
|
||||
Response::Balance { key, val } => {
|
||||
trace!("Response balance {:?} {:?}", key, val);
|
||||
self.balances.insert(key, val);
|
||||
}
|
||||
Response::LastId { id } => {
|
||||
trace!("Response last_id {:?}", id);
|
||||
@@ -136,8 +129,8 @@ impl ThinClient {
|
||||
/// by the network, this method will hang indefinitely.
|
||||
pub fn get_balance(&mut self, pubkey: &Pubkey) -> io::Result<i64> {
|
||||
trace!("get_balance");
|
||||
let req = Request::GetAccount { key: *pubkey };
|
||||
let data = serialize(&req).expect("serialize GetAccount in pub fn get_balance");
|
||||
let req = Request::GetBalance { key: *pubkey };
|
||||
let data = serialize(&req).expect("serialize GetBalance in pub fn get_balance");
|
||||
self.requests_socket
|
||||
.send_to(&data, &self.requests_addr)
|
||||
.expect("buffer error in pub fn get_balance");
|
||||
@@ -145,14 +138,14 @@ impl ThinClient {
|
||||
while !done {
|
||||
let resp = self.recv_response()?;
|
||||
trace!("recv_response {:?}", resp);
|
||||
if let Response::Account { key, .. } = &resp {
|
||||
if let Response::Balance { key, .. } = &resp {
|
||||
done = key == pubkey;
|
||||
}
|
||||
self.process_response(&resp);
|
||||
}
|
||||
self.balances
|
||||
.get(pubkey)
|
||||
.map(|a| a.tokens)
|
||||
.cloned()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "nokey"))
|
||||
}
|
||||
|
||||
|
@@ -92,20 +92,15 @@ pub struct Transaction {
|
||||
|
||||
/// The number of tokens paid for processing and storage of this transaction.
|
||||
pub fee: i64,
|
||||
/// Optional user data to be stored in the account
|
||||
/// TODO: This will be a required field for all contract operations including a simple spend.
|
||||
/// `instruction` will be serialized into `userdata` once Budget is its own generic contract.
|
||||
pub userdata: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
/// Create a signed transaction with userdata and an instruction
|
||||
fn new_with_userdata_and_instruction(
|
||||
/// Create a signed transaction from the given `Instruction`.
|
||||
fn new_from_instruction(
|
||||
from_keypair: &Keypair,
|
||||
instruction: Instruction,
|
||||
last_id: Hash,
|
||||
fee: i64,
|
||||
userdata: Vec<u8>,
|
||||
) -> Self {
|
||||
let from = from_keypair.pubkey();
|
||||
let mut tx = Transaction {
|
||||
@@ -114,20 +109,10 @@ impl Transaction {
|
||||
last_id,
|
||||
from,
|
||||
fee,
|
||||
userdata,
|
||||
};
|
||||
tx.sign(from_keypair);
|
||||
tx
|
||||
}
|
||||
/// Create a signed transaction from the given `Instruction`.
|
||||
fn new_from_instruction(
|
||||
from_keypair: &Keypair,
|
||||
instruction: Instruction,
|
||||
last_id: Hash,
|
||||
fee: i64,
|
||||
) -> Self {
|
||||
Self::new_with_userdata_and_instruction(from_keypair, instruction, last_id, fee, vec![])
|
||||
}
|
||||
|
||||
/// Create and sign a new Transaction. Used for unit-testing.
|
||||
pub fn new_taxed(
|
||||
@@ -195,9 +180,6 @@ impl Transaction {
|
||||
let fee_data = serialize(&(&self.fee)).expect("serialize last_id");
|
||||
data.extend_from_slice(&fee_data);
|
||||
|
||||
let userdata = serialize(&(&self.userdata)).expect("serialize userdata");
|
||||
data.extend_from_slice(&userdata);
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
@@ -292,7 +274,6 @@ mod tests {
|
||||
last_id: Default::default(),
|
||||
signature: Default::default(),
|
||||
fee: 0,
|
||||
userdata: vec![],
|
||||
};
|
||||
let buf = serialize(&claim0).unwrap();
|
||||
let claim1: Transaction = deserialize(&buf).unwrap();
|
||||
@@ -340,27 +321,6 @@ mod tests {
|
||||
assert_matches!(memfind(&tx_bytes, &tx.signature.as_ref()), Some(SIG_OFFSET));
|
||||
assert_matches!(memfind(&tx_bytes, &tx.from.as_ref()), Some(PUB_KEY_OFFSET));
|
||||
}
|
||||
#[test]
|
||||
fn test_userdata_layout() {
|
||||
let mut tx0 = test_tx();
|
||||
tx0.userdata = vec![1, 2, 3];
|
||||
let sign_data0a = tx0.get_sign_data();
|
||||
let tx_bytes = serialize(&tx0).unwrap();
|
||||
assert!(tx_bytes.len() < 256);
|
||||
assert_eq!(memfind(&tx_bytes, &sign_data0a), Some(SIGNED_DATA_OFFSET));
|
||||
assert_eq!(
|
||||
memfind(&tx_bytes, &tx0.signature.as_ref()),
|
||||
Some(SIG_OFFSET)
|
||||
);
|
||||
assert_eq!(memfind(&tx_bytes, &tx0.from.as_ref()), Some(PUB_KEY_OFFSET));
|
||||
let tx1 = deserialize(&tx_bytes).unwrap();
|
||||
assert_eq!(tx0, tx1);
|
||||
assert_eq!(tx1.userdata, vec![1, 2, 3]);
|
||||
|
||||
tx0.userdata = vec![1, 2, 4];
|
||||
let sign_data0b = tx0.get_sign_data();
|
||||
assert_ne!(sign_data0a, sign_data0b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overspend_attack() {
|
||||
|
Reference in New Issue
Block a user