From 2379792e0aa948882706d3e25e137f268b8c4afc Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Wed, 7 Mar 2018 21:55:49 -0700 Subject: [PATCH 1/7] Add DateTime and Cancel conditions Fixes #32, #33 --- Cargo.toml | 3 ++- src/accountant.rs | 13 +++++++++++++ src/lib.rs | 1 + src/transaction.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) 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..3a54c7c46a 100644 --- a/src/accountant.rs +++ b/src/accountant.rs @@ -12,6 +12,7 @@ use historian::{reserve_signature, Historian}; use std::sync::mpsc::SendError; use std::collections::HashMap; use std::result; +use chrono::prelude::*; #[derive(Debug, PartialEq, Eq)] pub enum AccountingError { @@ -138,6 +139,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) } 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/transaction.rs b/src/transaction.rs index dc3d400065..8256da28d3 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 { + DateTime(DateTime), + Cancel, +} #[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,28 @@ 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 mut tr = Transaction { + from: from_keypair.pubkey(), + to, + if_all: vec![Condition::DateTime(dt)], + unless_any: vec![Condition::Cancel], asset, last_id, sig: Signature::default(), @@ -72,6 +103,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(), From 94daf4cea413ff1ab9cb1229c6324a4180f51af3 Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Wed, 7 Mar 2018 22:25:45 -0700 Subject: [PATCH 2/7] Add Cancel and Timestamp events Fixes #31, #34, #39 --- src/accountant.rs | 39 +++++++++++++++++++++++++++++++++++++++ src/event.rs | 19 ++++++++++++++++++- src/transaction.rs | 11 ++++++----- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/accountant.rs b/src/accountant.rs index 3a54c7c46a..50b76e529a 100644 --- a/src/accountant.rs +++ b/src/accountant.rs @@ -29,6 +29,7 @@ pub struct Accountant { pub balances: HashMap, pub first_id: Hash, pub last_id: Hash, + pub pending: HashMap>, } impl Accountant { @@ -49,6 +50,7 @@ impl Accountant { balances: HashMap::new(), first_id: start_hash, last_id: start_hash, + pending: HashMap::new(), }; // The second item in the log is a special transaction where the to and from @@ -104,12 +106,20 @@ 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 !tr.if_all.is_empty() { + self.pending.insert(tr.sig, tr.clone()); + } + if self.balances.contains_key(&tr.to) { if let Some(x) = self.balances.get_mut(&tr.to) { *x += tr.asset; @@ -121,10 +131,39 @@ impl Accountant { Ok(()) } + fn process_verified_sig(&mut self, _from: PublicKey, tx_sig: Signature) -> Result<()> { + if self.pending.contains_key(&tx_sig) { + if let Some(_tx) = self.pending.get_mut(&tx_sig) { + // Cancel: + // if Signature(from) is in unless_any, return funds to tx.from, and remove the tx from this map. + + // 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<()> { + // 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. + + // Process postponed: + // 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_event(self: &mut Self, event: &Event, allow_deposits: bool) -> Result<()> { 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), } } diff --git a/src/event.rs b/src/event.rs index 12a848ab5f..444f21b9c8 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::{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,26 @@ 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 { + // 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 +41,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/transaction.rs b/src/transaction.rs index 8256da28d3..3f2a1e99d4 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -8,8 +8,8 @@ use chrono::prelude::*; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum Condition { - DateTime(DateTime), - Cancel, + Timestamp(DateTime), + Signature(PublicKey), } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] @@ -45,11 +45,12 @@ impl Transaction { asset: T, last_id: Hash, ) -> Self { + let from = from_keypair.pubkey(); let mut tr = Transaction { - from: from_keypair.pubkey(), + from, to, - if_all: vec![Condition::DateTime(dt)], - unless_any: vec![Condition::Cancel], + if_all: vec![Condition::Timestamp(dt)], + unless_any: vec![Condition::Signature(from)], asset, last_id, sig: Signature::default(), From e759bd1a997b0488731672177738429236656f9c Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Wed, 7 Mar 2018 23:37:16 -0700 Subject: [PATCH 3/7] Add conditions to the signature to reject duplicates --- src/accountant.rs | 1 + src/transaction.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/accountant.rs b/src/accountant.rs index 50b76e529a..afd4bea103 100644 --- a/src/accountant.rs +++ b/src/accountant.rs @@ -118,6 +118,7 @@ impl Accountant { if !tr.if_all.is_empty() { self.pending.insert(tr.sig, tr.clone()); + return Ok(()); } if self.balances.contains_key(&tr.to) { diff --git a/src/transaction.rs b/src/transaction.rs index 3f2a1e99d4..046d8485a0 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -60,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) { From d500bbff047193b162d64a1850610d91889e4f05 Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Thu, 8 Mar 2018 08:32:57 -0700 Subject: [PATCH 4/7] Add public key to mint This turns the mint into a handy way to generate public keys without throwing the private key away. --- src/mint.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/mint.rs b/src/mint.rs index f357737771..5a7d297d82 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,7 +38,7 @@ impl Mint { } pub fn pubkey(&self) -> PublicKey { - self.keypair().pubkey() + self.pubkey } pub fn create_events(&self) -> Vec { From dd2bd6704945aca98d18be959773fbf109082a7e Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Thu, 8 Mar 2018 08:58:01 -0700 Subject: [PATCH 5/7] Add a barebones test for transaction conditions --- src/accountant.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/accountant.rs b/src/accountant.rs index afd4bea103..d496b32075 100644 --- a/src/accountant.rs +++ b/src/accountant.rs @@ -257,4 +257,29 @@ 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(); + + // TODO: Uncomment this once process_verified_timestamp is implemented. + //assert_eq!(acc.get_balance(&bob_pubkey), Some(1)); + } } From 923162ae9d2696cd065d268f28a2db7888922ad6 Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Thu, 8 Mar 2018 10:05:00 -0700 Subject: [PATCH 6/7] WIP: process timestamps --- src/accountant.rs | 42 ++++++++++++++++++++++++++++++------------ src/event.rs | 12 +++++++++++- src/mint.rs | 3 ++- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/accountant.rs b/src/accountant.rs index d496b32075..2bbe4090ee 100644 --- a/src/accountant.rs +++ b/src/accountant.rs @@ -10,7 +10,7 @@ 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::*; @@ -29,7 +29,9 @@ pub struct Accountant { pub balances: HashMap, pub first_id: Hash, pub last_id: Hash, - pub pending: HashMap>, + pending: HashMap>, + time_sources: HashSet, + last_time: DateTime, } impl Accountant { @@ -51,6 +53,8 @@ impl Accountant { 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 @@ -97,6 +101,16 @@ impl Accountant { Ok(()) } + 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); + } + } + fn process_verified_transaction( self: &mut Self, tr: &Transaction, @@ -121,14 +135,7 @@ impl Accountant { return Ok(()); } - 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); - } - + self.complete_transaction(tr); Ok(()) } @@ -146,7 +153,18 @@ impl Accountant { Ok(()) } - fn process_verified_timestamp(&mut self, _from: PublicKey, _dt: DateTime) -> Result<()> { + 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; + } + } // TODO: Lookup pending Transaction waiting on time, signed by a whitelisted PublicKey. // Expire: @@ -154,7 +172,7 @@ impl Accountant { // and remove the tx from this map. // Process postponed: - // otherwise, if "Signature(from) is in if_all, remove it. If that causes that list + // otherwise, if "Timestamp(dt) >= self.last_time" 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(()) } diff --git a/src/event.rs b/src/event.rs index 444f21b9c8..0d6642b3f3 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,6 +1,6 @@ //! The `event` crate provides the data structures for log events. -use signature::{PublicKey, Signature, SignatureUtil}; +use signature::{KeyPair, KeyPairUtil, PublicKey, Signature, SignatureUtil}; use transaction::Transaction; use chrono::prelude::*; use bincode::serialize; @@ -27,6 +27,16 @@ pub enum Event { } 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 { diff --git a/src/mint.rs b/src/mint.rs index 5a7d297d82..6beeeea986 100644 --- a/src/mint.rs +++ b/src/mint.rs @@ -42,7 +42,8 @@ impl Mint { } 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)] } From ad6665c8b6d1052bd04b90f828dde06e46a7dd1e Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Thu, 8 Mar 2018 11:06:52 -0700 Subject: [PATCH 7/7] Complete timestamp and signature transactions --- src/accountant.rs | 102 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 14 deletions(-) diff --git a/src/accountant.rs b/src/accountant.rs index 2bbe4090ee..97985645e7 100644 --- a/src/accountant.rs +++ b/src/accountant.rs @@ -5,7 +5,7 @@ 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}; @@ -101,6 +101,7 @@ 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) { @@ -111,6 +112,13 @@ impl Accountant { } } + /// 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, @@ -139,17 +147,32 @@ impl Accountant { Ok(()) } - fn process_verified_sig(&mut self, _from: PublicKey, tx_sig: Signature) -> Result<()> { - if self.pending.contains_key(&tx_sig) { - if let Some(_tx) = self.pending.get_mut(&tx_sig) { - // Cancel: - // if Signature(from) is in unless_any, return funds to tx.from, and remove the tx from this map. + 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. - // 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. + // 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(()) } @@ -164,6 +187,8 @@ impl Accountant { if dt > self.last_time { self.last_time = dt; } + } else { + return Ok(()); } // TODO: Lookup pending Transaction waiting on time, signed by a whitelisted PublicKey. @@ -171,9 +196,31 @@ impl Accountant { // if a Timestamp after this DateTime is in unless_any, return funds to tx.from, // and remove the tx from this map. - // Process postponed: - // otherwise, if "Timestamp(dt) >= self.last_time" 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. + // 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(()) } @@ -296,8 +343,35 @@ mod tests { // 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)); - // TODO: Uncomment this once process_verified_timestamp is implemented. - //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)); } }