Merge pull request #64 from garious/dumb-contracts
Entry-level smart contracts
This commit is contained in:
commit
67f4f4fb49
|
@ -5,7 +5,7 @@
|
|||
use hash::Hash;
|
||||
use entry::Entry;
|
||||
use event::Event;
|
||||
use transaction::{Condition, Transaction};
|
||||
use transaction::{Action, Plan, Transaction};
|
||||
use signature::{KeyPair, PublicKey, Signature};
|
||||
use mint::Mint;
|
||||
use historian::{reserve_signature, Historian};
|
||||
|
@ -30,7 +30,7 @@ pub struct Accountant {
|
|||
pub balances: HashMap<PublicKey, i64>,
|
||||
pub first_id: Hash,
|
||||
pub last_id: Hash,
|
||||
pending: HashMap<Signature, Transaction<i64>>,
|
||||
pending: HashMap<Signature, Plan<i64>>,
|
||||
time_sources: HashSet<PublicKey>,
|
||||
last_time: DateTime<Utc>,
|
||||
}
|
||||
|
@ -83,8 +83,12 @@ impl Accountant {
|
|||
self.last_id
|
||||
}
|
||||
|
||||
fn is_deposit(allow_deposits: bool, from: &PublicKey, to: &PublicKey) -> bool {
|
||||
allow_deposits && from == to
|
||||
fn is_deposit(allow_deposits: bool, from: &PublicKey, plan: &Plan<i64>) -> bool {
|
||||
if let Plan::Action(Action::Pay(ref payment)) = *plan {
|
||||
allow_deposits && *from == payment.to
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_transaction(self: &mut Self, tr: Transaction<i64>) -> Result<()> {
|
||||
|
@ -108,36 +112,16 @@ impl Accountant {
|
|||
}
|
||||
|
||||
/// Commit funds to the 'to' party.
|
||||
fn complete_transaction(self: &mut Self, tr: &Transaction<i64>) {
|
||||
if self.balances.contains_key(&tr.to) {
|
||||
if let Some(x) = self.balances.get_mut(&tr.to) {
|
||||
*x += tr.asset;
|
||||
fn complete_transaction(self: &mut Self, plan: &Plan<i64>) {
|
||||
if let Plan::Action(Action::Pay(ref payment)) = *plan {
|
||||
if self.balances.contains_key(&payment.to) {
|
||||
if let Some(x) = self.balances.get_mut(&payment.to) {
|
||||
*x += payment.asset;
|
||||
}
|
||||
} else {
|
||||
self.balances.insert(tr.to, tr.asset);
|
||||
self.balances.insert(payment.to, payment.asset);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return funds to the 'from' party.
|
||||
fn cancel_transaction(self: &mut Self, tr: &Transaction<i64>) {
|
||||
if let Some(x) = self.balances.get_mut(&tr.from) {
|
||||
*x += tr.asset;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move this to transaction.rs
|
||||
fn all_satisfied(&self, conds: &[Condition]) -> bool {
|
||||
let mut satisfied = true;
|
||||
for cond in conds {
|
||||
if let &Condition::Timestamp(dt) = cond {
|
||||
if dt > self.last_time {
|
||||
satisfied = false;
|
||||
}
|
||||
} else {
|
||||
satisfied = false;
|
||||
}
|
||||
}
|
||||
satisfied
|
||||
}
|
||||
|
||||
fn process_verified_transaction(
|
||||
|
@ -149,51 +133,37 @@ 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 !Self::is_deposit(allow_deposits, &tr.from, &tr.plan) {
|
||||
if let Some(x) = self.balances.get_mut(&tr.from) {
|
||||
*x -= tr.asset;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.all_satisfied(&tr.if_all) {
|
||||
self.pending.insert(tr.sig, tr.clone());
|
||||
let mut plan = tr.plan.clone();
|
||||
let actionable = plan.process_verified_timestamp(self.last_time);
|
||||
|
||||
if !actionable {
|
||||
self.pending.insert(tr.sig, plan);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.complete_transaction(tr);
|
||||
self.complete_transaction(&plan);
|
||||
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.
|
||||
let actionable = if let Some(plan) = self.pending.get_mut(&tx_sig) {
|
||||
plan.process_verified_sig(from)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// TODO: Use find().
|
||||
for cond in &tr.unless_any {
|
||||
if let Condition::Signature(pubkey) = *cond {
|
||||
if from == pubkey {
|
||||
cancel = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if actionable {
|
||||
if let Some(plan) = self.pending.remove(&tx_sig) {
|
||||
self.complete_transaction(&plan);
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -211,34 +181,18 @@ impl Accountant {
|
|||
} else {
|
||||
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);
|
||||
for (key, plan) in &mut self.pending {
|
||||
if plan.process_verified_timestamp(self.last_time) {
|
||||
completed.push(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
if let Some(plan) = self.pending.remove(&key) {
|
||||
self.complete_transaction(&plan);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,6 +281,30 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overspend_attack() {
|
||||
let alice = Mint::new(1);
|
||||
let mut acc = Accountant::new(&alice, None);
|
||||
let bob_pubkey = KeyPair::new().pubkey();
|
||||
let mut tr = Transaction::new(&alice.keypair(), bob_pubkey, 1, alice.seed());
|
||||
if let Plan::Action(Action::Pay(ref mut payment)) = tr.plan {
|
||||
payment.asset = 2; // <-- attack!
|
||||
}
|
||||
assert_eq!(
|
||||
acc.process_transaction(tr.clone()),
|
||||
Err(AccountingError::InvalidTransfer)
|
||||
);
|
||||
|
||||
// Also, ensure all branchs of the plan spend all assets
|
||||
if let Plan::Action(Action::Pay(ref mut payment)) = tr.plan {
|
||||
payment.asset = 0; // <-- whoops!
|
||||
}
|
||||
assert_eq!(
|
||||
acc.process_transaction(tr.clone()),
|
||||
Err(AccountingError::InvalidTransfer)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_to_newb() {
|
||||
let alice = Mint::new(10_000);
|
||||
|
|
|
@ -58,12 +58,15 @@ impl Mint {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use log::verify_slice;
|
||||
use transaction::{Action, Plan};
|
||||
|
||||
#[test]
|
||||
fn test_create_events() {
|
||||
let mut events = Mint::new(100).create_events().into_iter();
|
||||
if let Event::Transaction(tr) = events.next().unwrap() {
|
||||
assert_eq!(tr.from, tr.to);
|
||||
if let Plan::Action(Action::Pay(payment)) = tr.plan {
|
||||
assert_eq!(tr.from, payment.to);
|
||||
}
|
||||
}
|
||||
assert_eq!(events.next(), None);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ use serde::Serialize;
|
|||
use bincode::serialize;
|
||||
use hash::Hash;
|
||||
use chrono::prelude::*;
|
||||
use std::mem;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Condition {
|
||||
|
@ -12,24 +13,138 @@ pub enum Condition {
|
|||
Signature(PublicKey),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Action<T> {
|
||||
Pay(Payment<T>),
|
||||
}
|
||||
|
||||
impl<T: Clone> Action<T> {
|
||||
pub fn spendable(&self) -> T {
|
||||
match *self {
|
||||
Action::Pay(ref payment) => payment.asset.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Payment<T> {
|
||||
pub asset: T,
|
||||
pub to: PublicKey,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Plan<T> {
|
||||
Action(Action<T>),
|
||||
After(Condition, Action<T>),
|
||||
Race(Box<Plan<T>>, Box<Plan<T>>),
|
||||
}
|
||||
|
||||
impl<T: Clone + Eq> Plan<T> {
|
||||
pub fn verify(&self, spendable_assets: &T) -> bool {
|
||||
match *self {
|
||||
Plan::Action(ref action) => action.spendable() == *spendable_assets,
|
||||
Plan::Race(ref plan_a, ref plan_b) => {
|
||||
plan_a.verify(spendable_assets) && plan_b.verify(spendable_assets)
|
||||
}
|
||||
Plan::After(_, ref action) => action.spendable() == *spendable_assets,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_race(&mut self) -> bool {
|
||||
let new_plan = if let Plan::Race(ref a, ref b) = *self {
|
||||
if let Plan::Action(_) = **a {
|
||||
Some((**a).clone())
|
||||
} else if let Plan::Action(_) = **b {
|
||||
Some((**b).clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(plan) = new_plan {
|
||||
mem::replace(self, plan);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_verified_sig(&mut self, from: PublicKey) -> bool {
|
||||
let mut new_plan = None;
|
||||
match *self {
|
||||
Plan::Action(_) => return true,
|
||||
Plan::Race(ref mut plan_a, ref mut plan_b) => {
|
||||
plan_a.process_verified_sig(from);
|
||||
plan_b.process_verified_sig(from);
|
||||
}
|
||||
Plan::After(Condition::Signature(pubkey), ref action) => {
|
||||
if from == pubkey {
|
||||
new_plan = Some(Plan::Action(action.clone()));
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if self.run_race() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(plan) = new_plan {
|
||||
mem::replace(self, plan);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_verified_timestamp(&mut self, last_time: DateTime<Utc>) -> bool {
|
||||
let mut new_plan = None;
|
||||
match *self {
|
||||
Plan::Action(_) => return true,
|
||||
Plan::Race(ref mut plan_a, ref mut plan_b) => {
|
||||
plan_a.process_verified_timestamp(last_time);
|
||||
plan_b.process_verified_timestamp(last_time);
|
||||
}
|
||||
Plan::After(Condition::Timestamp(dt), ref action) => {
|
||||
if dt <= last_time {
|
||||
new_plan = Some(Plan::Action(action.clone()));
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if self.run_race() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(plan) = new_plan {
|
||||
mem::replace(self, plan);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Transaction<T> {
|
||||
pub from: PublicKey,
|
||||
pub to: PublicKey,
|
||||
pub if_all: Vec<Condition>,
|
||||
pub unless_any: Vec<Condition>,
|
||||
pub plan: Plan<T>,
|
||||
pub asset: T,
|
||||
pub last_id: Hash,
|
||||
pub sig: Signature,
|
||||
}
|
||||
|
||||
impl<T: Serialize> Transaction<T> {
|
||||
impl<T: Serialize + Clone + Eq> Transaction<T> {
|
||||
pub fn new(from_keypair: &KeyPair, to: PublicKey, asset: T, last_id: Hash) -> Self {
|
||||
let mut tr = Transaction {
|
||||
from: from_keypair.pubkey(),
|
||||
let from = from_keypair.pubkey();
|
||||
let plan = Plan::Action(Action::Pay(Payment {
|
||||
asset: asset.clone(),
|
||||
to,
|
||||
if_all: vec![],
|
||||
unless_any: vec![],
|
||||
}));
|
||||
let mut tr = Transaction {
|
||||
from,
|
||||
plan,
|
||||
asset,
|
||||
last_id,
|
||||
sig: Signature::default(),
|
||||
|
@ -46,11 +161,25 @@ impl<T: Serialize> Transaction<T> {
|
|||
last_id: Hash,
|
||||
) -> Self {
|
||||
let from = from_keypair.pubkey();
|
||||
let plan = Plan::Race(
|
||||
Box::new(Plan::After(
|
||||
Condition::Timestamp(dt),
|
||||
Action::Pay(Payment {
|
||||
asset: asset.clone(),
|
||||
to,
|
||||
}),
|
||||
)),
|
||||
Box::new(Plan::After(
|
||||
Condition::Signature(from),
|
||||
Action::Pay(Payment {
|
||||
asset: asset.clone(),
|
||||
to: from,
|
||||
}),
|
||||
)),
|
||||
);
|
||||
let mut tr = Transaction {
|
||||
from,
|
||||
to,
|
||||
if_all: vec![Condition::Timestamp(dt)],
|
||||
unless_any: vec![Condition::Signature(from)],
|
||||
plan,
|
||||
asset,
|
||||
last_id,
|
||||
sig: Signature::default(),
|
||||
|
@ -60,14 +189,7 @@ impl<T: Serialize> Transaction<T> {
|
|||
}
|
||||
|
||||
fn get_sign_data(&self) -> Vec<u8> {
|
||||
serialize(&(
|
||||
&self.from,
|
||||
&self.to,
|
||||
&self.if_all,
|
||||
&self.unless_any,
|
||||
&self.asset,
|
||||
&self.last_id,
|
||||
)).unwrap()
|
||||
serialize(&(&self.from, &self.plan, &self.asset, &self.last_id)).unwrap()
|
||||
}
|
||||
|
||||
pub fn sign(&mut self, keypair: &KeyPair) {
|
||||
|
@ -76,7 +198,7 @@ impl<T: Serialize> Transaction<T> {
|
|||
}
|
||||
|
||||
pub fn verify(&self) -> bool {
|
||||
self.sig.verify(&self.from, &self.get_sign_data())
|
||||
self.sig.verify(&self.from, &self.get_sign_data()) && self.plan.verify(&self.asset)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,11 +230,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_serialize_claim() {
|
||||
let plan = Plan::Action(Action::Pay(Payment {
|
||||
asset: 0,
|
||||
to: Default::default(),
|
||||
}));
|
||||
let claim0 = Transaction {
|
||||
from: Default::default(),
|
||||
to: Default::default(),
|
||||
if_all: Default::default(),
|
||||
unless_any: Default::default(),
|
||||
plan,
|
||||
asset: 0u8,
|
||||
last_id: Default::default(),
|
||||
sig: Default::default(),
|
||||
|
@ -140,9 +264,12 @@ mod tests {
|
|||
let thief_keypair = KeyPair::new();
|
||||
let pubkey1 = keypair1.pubkey();
|
||||
let zero = Hash::default();
|
||||
let mut tr = Transaction::new(&keypair0, pubkey1, hash(b"hello, world"), zero);
|
||||
let asset = hash(b"hello, world");
|
||||
let mut tr = Transaction::new(&keypair0, pubkey1, asset, zero);
|
||||
tr.sign(&keypair0);
|
||||
tr.to = thief_keypair.pubkey(); // <-- attack!
|
||||
if let Plan::Action(Action::Pay(ref mut payment)) = tr.plan {
|
||||
payment.to = thief_keypair.pubkey(); // <-- attack!
|
||||
};
|
||||
assert!(!tr.verify());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue