diff --git a/Cargo.toml b/Cargo.toml index a51cf84e43..6c0911ab12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "silk" description = "A silky smooth implementation of the Loom architecture" -version = "0.3.0" +version = "0.3.1" documentation = "https://docs.rs/silk" homepage = "http://loomprotocol.com/" repository = "https://github.com/loomprotocol/silk" @@ -53,3 +53,4 @@ serde_json = "1.0.10" ring = "0.12.1" untrusted = "0.5.1" bincode = "1.0.0" +chrono = { version = "0.4.0", features = ["serde"] } diff --git a/src/accountant.rs b/src/accountant.rs index c84cb70125..97985645e7 100644 --- a/src/accountant.rs +++ b/src/accountant.rs @@ -5,13 +5,14 @@ use hash::Hash; use entry::Entry; use event::Event; -use transaction::Transaction; +use transaction::{Condition, Transaction}; use signature::{KeyPair, PublicKey, Signature}; use mint::Mint; use historian::{reserve_signature, Historian}; use std::sync::mpsc::SendError; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::result; +use chrono::prelude::*; #[derive(Debug, PartialEq, Eq)] pub enum AccountingError { @@ -28,6 +29,9 @@ pub struct Accountant { pub balances: HashMap, pub first_id: Hash, pub last_id: Hash, + pending: HashMap>, + time_sources: HashSet, + last_time: DateTime, } impl Accountant { @@ -48,6 +52,9 @@ impl Accountant { balances: HashMap::new(), first_id: start_hash, last_id: start_hash, + pending: HashMap::new(), + time_sources: HashSet::new(), + last_time: Utc.timestamp(0, 0), }; // The second item in the log is a special transaction where the to and from @@ -94,6 +101,24 @@ impl Accountant { Ok(()) } + /// Commit funds to the 'to' party. + fn complete_transaction(self: &mut Self, tr: &Transaction) { + if self.balances.contains_key(&tr.to) { + if let Some(x) = self.balances.get_mut(&tr.to) { + *x += tr.asset; + } + } else { + self.balances.insert(tr.to, tr.asset); + } + } + + /// Return funds to the 'from' party. + fn cancel_transaction(self: &mut Self, tr: &Transaction) { + if let Some(x) = self.balances.get_mut(&tr.from) { + *x += tr.asset; + } + } + fn process_verified_transaction( self: &mut Self, tr: &Transaction, @@ -103,18 +128,97 @@ impl Accountant { return Err(AccountingError::InvalidTransferSignature); } + if !tr.unless_any.is_empty() { + // TODO: Check to see if the transaction is expired. + } + if !Self::is_deposit(allow_deposits, &tr.from, &tr.to) { if let Some(x) = self.balances.get_mut(&tr.from) { *x -= tr.asset; } } - if self.balances.contains_key(&tr.to) { - if let Some(x) = self.balances.get_mut(&tr.to) { - *x += tr.asset; + if !tr.if_all.is_empty() { + self.pending.insert(tr.sig, tr.clone()); + return Ok(()); + } + + self.complete_transaction(tr); + Ok(()) + } + + fn process_verified_sig(&mut self, from: PublicKey, tx_sig: Signature) -> Result<()> { + let mut cancel = false; + if let Some(tr) = self.pending.get(&tx_sig) { + // Cancel: + // if Signature(from) is in unless_any, return funds to tx.from, and remove the tx from this map. + + // TODO: Use find(). + for cond in &tr.unless_any { + if let Condition::Signature(pubkey) = *cond { + if from == pubkey { + cancel = true; + break; + } + } + } + } + + if cancel { + if let Some(tr) = self.pending.remove(&tx_sig) { + self.cancel_transaction(&tr); + } + } + + // Process Multisig: + // otherwise, if "Signature(from) is in if_all, remove it. If that causes that list + // to be empty, add the asset to to, and remove the tx from this map. + Ok(()) + } + + fn process_verified_timestamp(&mut self, from: PublicKey, dt: DateTime) -> Result<()> { + // If this is the first timestamp we've seen, it probably came from the genesis block, + // so we'll trust it. + if self.last_time == Utc.timestamp(0, 0) { + self.time_sources.insert(from); + } + + if self.time_sources.contains(&from) { + if dt > self.last_time { + self.last_time = dt; } } else { - self.balances.insert(tr.to, tr.asset); + return Ok(()); + } + // TODO: Lookup pending Transaction waiting on time, signed by a whitelisted PublicKey. + + // Expire: + // if a Timestamp after this DateTime is in unless_any, return funds to tx.from, + // and remove the tx from this map. + + // Check to see if any timelocked transactions can be completed. + let mut completed = vec![]; + for (key, tr) in &self.pending { + for cond in &tr.if_all { + if let Condition::Timestamp(dt) = *cond { + if self.last_time >= dt { + if tr.if_all.len() == 1 { + completed.push(*key); + } + } + } + } + // TODO: Add this in once we start removing constraints + //if tr.if_all.is_empty() { + // // TODO: Remove tr from pending + // self.complete_transaction(tr); + //} + } + + for key in completed { + if let Some(tr) = self.pending.remove(&key) { + self.complete_transaction(&tr); + } } Ok(()) @@ -124,6 +228,8 @@ impl Accountant { match *event { Event::Tick => Ok(()), Event::Transaction(ref tr) => self.process_verified_transaction(tr, allow_deposits), + Event::Signature { from, tx_sig, .. } => self.process_verified_sig(from, tx_sig), + Event::Timestamp { from, dt, .. } => self.process_verified_timestamp(from, dt), } } @@ -138,6 +244,18 @@ impl Accountant { self.process_transaction(tr).map(|_| sig) } + pub fn transfer_on_date( + self: &mut Self, + n: i64, + keypair: &KeyPair, + to: PublicKey, + dt: DateTime, + ) -> Result { + let tr = Transaction::new_on_date(keypair, to, dt, n, self.last_id); + let sig = tr.sig; + self.process_transaction(tr).map(|_| sig) + } + pub fn get_balance(self: &Self, pubkey: &PublicKey) -> Option { self.balances.get(pubkey).map(|x| *x) } @@ -204,4 +322,56 @@ mod tests { ExitReason::RecvDisconnected ); } + + #[test] + fn test_transfer_on_date() { + let alice = Mint::new(1); + let mut acc = Accountant::new(&alice, Some(2)); + let alice_keypair = alice.keypair(); + let bob_pubkey = KeyPair::new().pubkey(); + let dt = Utc::now(); + acc.transfer_on_date(1, &alice_keypair, bob_pubkey, dt) + .unwrap(); + + // Alice's balance will be zero because all funds are locked up. + assert_eq!(acc.get_balance(&alice.pubkey()), Some(0)); + + // Bob's balance will be None because the funds have not been + // sent. + assert_eq!(acc.get_balance(&bob_pubkey), None); + + // Now, acknowledge the time in the condition occurred and + // that bob's funds are now available. + acc.process_verified_timestamp(alice.pubkey(), dt).unwrap(); + assert_eq!(acc.get_balance(&bob_pubkey), Some(1)); + + acc.process_verified_timestamp(alice.pubkey(), dt).unwrap(); // <-- Attack! Attempt to process completed transaction. + assert_ne!(acc.get_balance(&bob_pubkey), Some(2)); + } + + #[test] + fn test_cancel_transfer() { + let alice = Mint::new(1); + let mut acc = Accountant::new(&alice, Some(2)); + let alice_keypair = alice.keypair(); + let bob_pubkey = KeyPair::new().pubkey(); + let dt = Utc::now(); + let sig = acc.transfer_on_date(1, &alice_keypair, bob_pubkey, dt) + .unwrap(); + + // Alice's balance will be zero because all funds are locked up. + assert_eq!(acc.get_balance(&alice.pubkey()), Some(0)); + + // Bob's balance will be None because the funds have not been + // sent. + assert_eq!(acc.get_balance(&bob_pubkey), None); + + // Now, cancel the trancaction. Alice gets her funds back, Bob never sees them. + acc.process_verified_sig(alice.pubkey(), sig).unwrap(); + assert_eq!(acc.get_balance(&alice.pubkey()), Some(1)); + assert_eq!(acc.get_balance(&bob_pubkey), None); + + acc.process_verified_sig(alice.pubkey(), sig).unwrap(); // <-- Attack! Attempt to cancel completed transaction. + assert_ne!(acc.get_balance(&alice.pubkey()), Some(2)); + } } diff --git a/src/event.rs b/src/event.rs index 12a848ab5f..0d6642b3f3 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,7 +1,9 @@ //! The `event` crate provides the data structures for log events. -use signature::Signature; +use signature::{KeyPair, KeyPairUtil, PublicKey, Signature, SignatureUtil}; use transaction::Transaction; +use chrono::prelude::*; +use bincode::serialize; /// When 'event' is Tick, the event represents a simple clock tick, and exists for the /// sole purpose of improving the performance of event log verification. A tick can @@ -12,13 +14,36 @@ use transaction::Transaction; pub enum Event { Tick, Transaction(Transaction), + Signature { + from: PublicKey, + tx_sig: Signature, + sig: Signature, + }, + Timestamp { + from: PublicKey, + dt: DateTime, + sig: Signature, + }, } impl Event { + pub fn new_timestamp(from: &KeyPair, dt: DateTime) -> Self { + let sign_data = serialize(&dt).unwrap(); + let sig = Signature::clone_from_slice(from.sign(&sign_data).as_ref()); + Event::Timestamp { + from: from.pubkey(), + dt, + sig, + } + } + + // TODO: Rename this to transaction_signature(). pub fn get_signature(&self) -> Option { match *self { Event::Tick => None, Event::Transaction(ref tr) => Some(tr.sig), + Event::Signature { .. } => None, + Event::Timestamp { .. } => None, } } @@ -26,6 +51,8 @@ impl Event { match *self { Event::Tick => true, Event::Transaction(ref tr) => tr.verify(), + Event::Signature { from, tx_sig, sig } => sig.verify(&from, &tx_sig), + Event::Timestamp { from, dt, sig } => sig.verify(&from, &serialize(&dt).unwrap()), } } } diff --git a/src/lib.rs b/src/lib.rs index 01cbc93aea..f9f98c2e82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub mod accountant; pub mod accountant_skel; pub mod accountant_stub; extern crate bincode; +extern crate chrono; extern crate generic_array; extern crate rayon; extern crate ring; diff --git a/src/mint.rs b/src/mint.rs index f357737771..6beeeea986 100644 --- a/src/mint.rs +++ b/src/mint.rs @@ -12,6 +12,7 @@ use untrusted::Input; #[derive(Serialize, Deserialize, Debug)] pub struct Mint { pub pkcs8: Vec, + pubkey: PublicKey, pub tokens: i64, } @@ -19,7 +20,13 @@ impl Mint { pub fn new(tokens: i64) -> Self { let rnd = SystemRandom::new(); let pkcs8 = KeyPair::generate_pkcs8(&rnd).unwrap().to_vec(); - Mint { pkcs8, tokens } + let keypair = KeyPair::from_pkcs8(Input::from(&pkcs8)).unwrap(); + let pubkey = keypair.pubkey(); + Mint { + pkcs8, + pubkey, + tokens, + } } pub fn seed(&self) -> Hash { @@ -31,11 +38,12 @@ impl Mint { } pub fn pubkey(&self) -> PublicKey { - self.keypair().pubkey() + self.pubkey } pub fn create_events(&self) -> Vec { - let tr = Transaction::new(&self.keypair(), self.pubkey(), self.tokens, self.seed()); + let keypair = self.keypair(); + let tr = Transaction::new(&keypair, self.pubkey(), self.tokens, self.seed()); vec![Event::Tick, Event::Transaction(tr)] } diff --git a/src/transaction.rs b/src/transaction.rs index dc3d400065..046d8485a0 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -4,11 +4,20 @@ use signature::{KeyPair, KeyPairUtil, PublicKey, Signature, SignatureUtil}; use serde::Serialize; use bincode::serialize; use hash::Hash; +use chrono::prelude::*; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum Condition { + Timestamp(DateTime), + Signature(PublicKey), +} #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct Transaction { pub from: PublicKey, pub to: PublicKey, + pub if_all: Vec, + pub unless_any: Vec, pub asset: T, pub last_id: Hash, pub sig: Signature, @@ -19,6 +28,29 @@ impl Transaction { let mut tr = Transaction { from: from_keypair.pubkey(), to, + if_all: vec![], + unless_any: vec![], + asset, + last_id, + sig: Signature::default(), + }; + tr.sign(from_keypair); + tr + } + + pub fn new_on_date( + from_keypair: &KeyPair, + to: PublicKey, + dt: DateTime, + asset: T, + last_id: Hash, + ) -> Self { + let from = from_keypair.pubkey(); + let mut tr = Transaction { + from, + to, + if_all: vec![Condition::Timestamp(dt)], + unless_any: vec![Condition::Signature(from)], asset, last_id, sig: Signature::default(), @@ -28,7 +60,14 @@ impl Transaction { } fn get_sign_data(&self) -> Vec { - serialize(&(&self.from, &self.to, &self.asset, &self.last_id)).unwrap() + serialize(&( + &self.from, + &self.to, + &self.if_all, + &self.unless_any, + &self.asset, + &self.last_id, + )).unwrap() } pub fn sign(&mut self, keypair: &KeyPair) { @@ -72,6 +111,8 @@ mod tests { let claim0 = Transaction { from: Default::default(), to: Default::default(), + if_all: Default::default(), + unless_any: Default::default(), asset: 0u8, last_id: Default::default(), sig: Default::default(),