@ -3,7 +3,9 @@
|
|||||||
//! transfer funds to other users.
|
//! transfer funds to other users.
|
||||||
|
|
||||||
use log::{Entry, Sha256Hash};
|
use log::{Entry, Sha256Hash};
|
||||||
use event::{get_pubkey, sign_transaction_data, verify_event, Event, PublicKey, Signature};
|
use event::Event;
|
||||||
|
use transaction::{sign_transaction_data, Transaction};
|
||||||
|
use signature::{get_pubkey, PublicKey, Signature};
|
||||||
use genesis::Genesis;
|
use genesis::Genesis;
|
||||||
use historian::{reserve_signature, Historian};
|
use historian::{reserve_signature, Historian};
|
||||||
use ring::signature::Ed25519KeyPair;
|
use ring::signature::Ed25519KeyPair;
|
||||||
@ -14,7 +16,8 @@ use std::result;
|
|||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum AccountingError {
|
pub enum AccountingError {
|
||||||
InsufficientFunds,
|
InsufficientFunds,
|
||||||
InvalidEvent,
|
InvalidTransfer,
|
||||||
|
InvalidTransferSignature,
|
||||||
SendError,
|
SendError,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,20 +77,44 @@ impl Accountant {
|
|||||||
allow_deposits && from == to
|
allow_deposits && from == to
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_event(self: &mut Self, event: Event<i64>) -> Result<()> {
|
pub fn process_transaction(self: &mut Self, tr: Transaction<i64>) -> Result<()> {
|
||||||
if !verify_event(&event) {
|
if !tr.verify() {
|
||||||
return Err(AccountingError::InvalidEvent);
|
return Err(AccountingError::InvalidTransfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Event::Transaction { from, data, .. } = event {
|
if self.get_balance(&tr.from).unwrap_or(0) < tr.data {
|
||||||
if self.get_balance(&from).unwrap_or(0) < data {
|
return Err(AccountingError::InsufficientFunds);
|
||||||
return Err(AccountingError::InsufficientFunds);
|
}
|
||||||
|
|
||||||
|
self.process_verified_transaction(&tr, false)?;
|
||||||
|
if let Err(SendError(_)) = self.historian.sender.send(Event::Transaction(tr)) {
|
||||||
|
return Err(AccountingError::SendError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_verified_transaction(
|
||||||
|
self: &mut Self,
|
||||||
|
tr: &Transaction<i64>,
|
||||||
|
allow_deposits: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !reserve_signature(&mut self.historian.signatures, &tr.sig) {
|
||||||
|
return Err(AccountingError::InvalidTransferSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Self::is_deposit(allow_deposits, &tr.from, &tr.to) {
|
||||||
|
if let Some(x) = self.balances.get_mut(&tr.from) {
|
||||||
|
*x -= tr.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.process_verified_event(&event, false)?;
|
if self.balances.contains_key(&tr.to) {
|
||||||
if let Err(SendError(_)) = self.historian.sender.send(event) {
|
if let Some(x) = self.balances.get_mut(&tr.to) {
|
||||||
return Err(AccountingError::SendError);
|
*x += tr.data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.balances.insert(tr.to, tr.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -98,26 +125,10 @@ impl Accountant {
|
|||||||
event: &Event<i64>,
|
event: &Event<i64>,
|
||||||
allow_deposits: bool,
|
allow_deposits: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if !reserve_signature(&mut self.historian.signatures, event) {
|
match *event {
|
||||||
return Err(AccountingError::InvalidEvent);
|
Event::Tick => Ok(()),
|
||||||
|
Event::Transaction(ref tr) => self.process_verified_transaction(tr, allow_deposits),
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Event::Transaction { from, to, data, .. } = *event {
|
|
||||||
if !Self::is_deposit(allow_deposits, &from, &to) {
|
|
||||||
if let Some(x) = self.balances.get_mut(&from) {
|
|
||||||
*x -= data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.balances.contains_key(&to) {
|
|
||||||
if let Some(x) = self.balances.get_mut(&to) {
|
|
||||||
*x += data;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.balances.insert(to, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn transfer(
|
pub fn transfer(
|
||||||
@ -129,14 +140,14 @@ impl Accountant {
|
|||||||
let from = get_pubkey(keypair);
|
let from = get_pubkey(keypair);
|
||||||
let last_id = self.last_id;
|
let last_id = self.last_id;
|
||||||
let sig = sign_transaction_data(&n, keypair, &to, &last_id);
|
let sig = sign_transaction_data(&n, keypair, &to, &last_id);
|
||||||
let event = Event::Transaction {
|
let tr = Transaction {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
data: n,
|
data: n,
|
||||||
last_id,
|
last_id,
|
||||||
sig,
|
sig,
|
||||||
};
|
};
|
||||||
self.process_event(event).map(|_| sig)
|
self.process_transaction(tr).map(|_| sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_balance(self: &Self, pubkey: &PublicKey) -> Option<i64> {
|
pub fn get_balance(self: &Self, pubkey: &PublicKey) -> Option<i64> {
|
||||||
@ -147,7 +158,7 @@ impl Accountant {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use event::{generate_keypair, get_pubkey};
|
use signature::{generate_keypair, get_pubkey};
|
||||||
use logger::ExitReason;
|
use logger::ExitReason;
|
||||||
use genesis::Creator;
|
use genesis::Creator;
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use accountant::Accountant;
|
use accountant::Accountant;
|
||||||
use event::{Event, PublicKey, Signature};
|
use transaction::Transaction;
|
||||||
|
use signature::PublicKey;
|
||||||
use log::{Entry, Sha256Hash};
|
use log::{Entry, Sha256Hash};
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
use bincode::{deserialize, serialize};
|
use bincode::{deserialize, serialize};
|
||||||
@ -11,22 +12,10 @@ pub struct AccountantSkel {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
Transfer {
|
Transaction(Transaction<i64>),
|
||||||
from: PublicKey,
|
GetBalance { key: PublicKey },
|
||||||
to: PublicKey,
|
GetEntries { last_id: Sha256Hash },
|
||||||
val: i64,
|
GetId { is_last: bool },
|
||||||
last_id: Sha256Hash,
|
|
||||||
sig: Signature,
|
|
||||||
},
|
|
||||||
GetBalance {
|
|
||||||
key: PublicKey,
|
|
||||||
},
|
|
||||||
GetEntries {
|
|
||||||
last_id: Sha256Hash,
|
|
||||||
},
|
|
||||||
GetId {
|
|
||||||
is_last: bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@ -43,22 +32,9 @@ impl AccountantSkel {
|
|||||||
|
|
||||||
pub fn process_request(self: &mut Self, msg: Request) -> Option<Response> {
|
pub fn process_request(self: &mut Self, msg: Request) -> Option<Response> {
|
||||||
match msg {
|
match msg {
|
||||||
Request::Transfer {
|
Request::Transaction(tr) => {
|
||||||
from,
|
if let Err(err) = self.acc.process_transaction(tr) {
|
||||||
to,
|
eprintln!("Transaction error: {:?}", err);
|
||||||
val,
|
|
||||||
last_id,
|
|
||||||
sig,
|
|
||||||
} => {
|
|
||||||
let event = Event::Transaction {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
data: val,
|
|
||||||
last_id,
|
|
||||||
sig,
|
|
||||||
};
|
|
||||||
if let Err(err) = self.acc.process_event(event) {
|
|
||||||
eprintln!("Transfer error: {:?}", err);
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
use std::io;
|
use std::io;
|
||||||
use bincode::{deserialize, serialize};
|
use bincode::{deserialize, serialize};
|
||||||
use event::{get_pubkey, get_signature, sign_transaction_data, PublicKey, Signature};
|
use transaction::{sign_transaction_data, Transaction};
|
||||||
|
use signature::{get_pubkey, PublicKey, Signature};
|
||||||
use log::{Entry, Sha256Hash};
|
use log::{Entry, Sha256Hash};
|
||||||
use ring::signature::Ed25519KeyPair;
|
use ring::signature::Ed25519KeyPair;
|
||||||
use accountant_skel::{Request, Response};
|
use accountant_skel::{Request, Response};
|
||||||
@ -33,13 +34,13 @@ impl AccountantStub {
|
|||||||
last_id: Sha256Hash,
|
last_id: Sha256Hash,
|
||||||
sig: Signature,
|
sig: Signature,
|
||||||
) -> io::Result<usize> {
|
) -> io::Result<usize> {
|
||||||
let req = Request::Transfer {
|
let req = Request::Transaction(Transaction {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
val,
|
data: val,
|
||||||
last_id,
|
last_id,
|
||||||
sig,
|
sig,
|
||||||
};
|
});
|
||||||
let data = serialize(&req).unwrap();
|
let data = serialize(&req).unwrap();
|
||||||
self.socket.send_to(&data, &self.addr)
|
self.socket.send_to(&data, &self.addr)
|
||||||
}
|
}
|
||||||
@ -108,7 +109,7 @@ impl AccountantStub {
|
|||||||
if let Response::Entries { entries } = resp {
|
if let Response::Entries { entries } = resp {
|
||||||
for Entry { id, event, .. } in entries {
|
for Entry { id, event, .. } in entries {
|
||||||
self.last_id = Some(id);
|
self.last_id = Some(id);
|
||||||
if let Some(sig) = get_signature(&event) {
|
if let Some(sig) = event.get_signature() {
|
||||||
if sig == *wait_sig {
|
if sig == *wait_sig {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ extern crate serde_json;
|
|||||||
extern crate silk;
|
extern crate silk;
|
||||||
|
|
||||||
use silk::accountant_stub::AccountantStub;
|
use silk::accountant_stub::AccountantStub;
|
||||||
use silk::event::{generate_keypair, get_pubkey, sign_transaction_data, verify_event, Event};
|
use silk::event::Event;
|
||||||
|
use silk::signature::{generate_keypair, get_pubkey};
|
||||||
|
use silk::transaction::{sign_transaction_data, Transaction};
|
||||||
use silk::genesis::Genesis;
|
use silk::genesis::Genesis;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
@ -47,14 +49,14 @@ fn main() {
|
|||||||
println!("Verify signatures...");
|
println!("Verify signatures...");
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
for &(k, s) in &sigs {
|
for &(k, s) in &sigs {
|
||||||
let e = Event::Transaction {
|
let e = Event::Transaction(Transaction {
|
||||||
from: alice_pubkey,
|
from: alice_pubkey,
|
||||||
to: k,
|
to: k,
|
||||||
data: one,
|
data: one,
|
||||||
last_id,
|
last_id,
|
||||||
sig: s,
|
sig: s,
|
||||||
};
|
});
|
||||||
assert!(verify_event(&e));
|
assert!(e.verify());
|
||||||
}
|
}
|
||||||
let duration = now.elapsed();
|
let duration = now.elapsed();
|
||||||
let ns = duration.as_secs() * 1_000_000_000 + duration.subsec_nanos() as u64;
|
let ns = duration.as_secs() * 1_000_000_000 + duration.subsec_nanos() as u64;
|
||||||
|
@ -2,7 +2,9 @@ extern crate silk;
|
|||||||
|
|
||||||
use silk::historian::Historian;
|
use silk::historian::Historian;
|
||||||
use silk::log::{verify_slice, Entry, Sha256Hash};
|
use silk::log::{verify_slice, Entry, Sha256Hash};
|
||||||
use silk::event::{generate_keypair, get_pubkey, sign_claim_data, Event};
|
use silk::signature::{generate_keypair, get_pubkey};
|
||||||
|
use silk::transaction::sign_claim_data;
|
||||||
|
use silk::event::Event;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::sync::mpsc::SendError;
|
use std::sync::mpsc::SendError;
|
||||||
|
@ -2,7 +2,7 @@ extern crate serde_json;
|
|||||||
extern crate silk;
|
extern crate silk;
|
||||||
|
|
||||||
use silk::genesis::{Creator, Genesis};
|
use silk::genesis::{Creator, Genesis};
|
||||||
use silk::event::{generate_keypair, get_pubkey};
|
use silk::signature::{generate_keypair, get_pubkey};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let alice = Creator {
|
let alice = Creator {
|
||||||
|
145
src/event.rs
145
src/event.rs
@ -1,30 +1,10 @@
|
|||||||
//! The `log` crate provides the foundational data structures for Proof-of-History,
|
//! The `event` crate provides the data structures for log events.
|
||||||
//! an ordered log of events in time.
|
|
||||||
|
|
||||||
/// Each log entry contains three pieces of data. The 'num_hashes' field is the number
|
use signature::{PublicKey, Signature};
|
||||||
/// of hashes performed since the previous entry. The 'id' field is the result
|
use transaction::Transaction;
|
||||||
/// of hashing 'id' from the previous entry 'num_hashes' times. The 'event'
|
|
||||||
/// field points to an Event that took place shortly after 'id' was generated.
|
|
||||||
///
|
|
||||||
/// If you divide 'num_hashes' by the amount of time it takes to generate a new hash, you
|
|
||||||
/// get a duration estimate since the last event. Since processing power increases
|
|
||||||
/// over time, one should expect the duration 'num_hashes' represents to decrease proportionally.
|
|
||||||
/// Though processing power varies across nodes, the network gives priority to the
|
|
||||||
/// fastest processor. Duration should therefore be estimated by assuming that the hash
|
|
||||||
/// was generated by the fastest processor at the time the entry was logged.
|
|
||||||
|
|
||||||
use generic_array::GenericArray;
|
|
||||||
use generic_array::typenum::{U32, U64};
|
|
||||||
use ring::signature::Ed25519KeyPair;
|
|
||||||
use ring::{rand, signature};
|
|
||||||
use untrusted;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use bincode::serialize;
|
|
||||||
use log::Sha256Hash;
|
use log::Sha256Hash;
|
||||||
|
|
||||||
pub type PublicKey = GenericArray<u8, U32>;
|
|
||||||
pub type Signature = GenericArray<u8, U64>;
|
|
||||||
|
|
||||||
/// When 'event' is Tick, the event represents a simple clock tick, and exists for the
|
/// 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
|
/// sole purpose of improving the performance of event log verification. A tick can
|
||||||
/// be generated in 'num_hashes' hashes and verified in 'num_hashes' hashes. By logging
|
/// be generated in 'num_hashes' hashes and verified in 'num_hashes' hashes. By logging
|
||||||
@ -33,112 +13,25 @@ pub type Signature = GenericArray<u8, U64>;
|
|||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||||
pub enum Event<T> {
|
pub enum Event<T> {
|
||||||
Tick,
|
Tick,
|
||||||
Transaction {
|
Transaction(Transaction<T>),
|
||||||
from: PublicKey,
|
|
||||||
to: PublicKey,
|
|
||||||
data: T,
|
|
||||||
last_id: Sha256Hash,
|
|
||||||
sig: Signature,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Event<T> {
|
impl<T: Serialize> Event<T> {
|
||||||
pub fn new_claim(to: PublicKey, data: T, last_id: Sha256Hash, sig: Signature) -> Self {
|
pub fn new_claim(to: PublicKey, data: T, last_id: Sha256Hash, sig: Signature) -> Self {
|
||||||
Event::Transaction {
|
Event::Transaction(Transaction::new_claim(to, data, last_id, sig))
|
||||||
from: to,
|
}
|
||||||
to,
|
|
||||||
data,
|
pub fn get_signature(&self) -> Option<Signature> {
|
||||||
last_id,
|
match *self {
|
||||||
sig,
|
Event::Tick => None,
|
||||||
|
Event::Transaction(ref tr) => Some(tr.sig),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self) -> bool {
|
||||||
|
match *self {
|
||||||
|
Event::Tick => true,
|
||||||
|
Event::Transaction(ref tr) => tr.verify(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a new ED25519 keypair
|
|
||||||
pub fn generate_keypair() -> Ed25519KeyPair {
|
|
||||||
let rng = rand::SystemRandom::new();
|
|
||||||
let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
|
|
||||||
signature::Ed25519KeyPair::from_pkcs8(untrusted::Input::from(&pkcs8_bytes)).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the public key for the given keypair
|
|
||||||
pub fn get_pubkey(keypair: &Ed25519KeyPair) -> PublicKey {
|
|
||||||
GenericArray::clone_from_slice(keypair.public_key_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a signature for the given data using the private key from the given keypair.
|
|
||||||
fn sign_serialized<T: Serialize>(data: &T, keypair: &Ed25519KeyPair) -> Signature {
|
|
||||||
let serialized = serialize(data).unwrap();
|
|
||||||
GenericArray::clone_from_slice(keypair.sign(&serialized).as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a signature for the given transaction data using the private key from the given keypair.
|
|
||||||
pub fn sign_transaction_data<T: Serialize>(
|
|
||||||
data: &T,
|
|
||||||
keypair: &Ed25519KeyPair,
|
|
||||||
to: &PublicKey,
|
|
||||||
last_id: &Sha256Hash,
|
|
||||||
) -> Signature {
|
|
||||||
let from = &get_pubkey(keypair);
|
|
||||||
sign_serialized(&(from, to, data, last_id), keypair)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a signature for the given data using the private key from the given keypair.
|
|
||||||
pub fn sign_claim_data<T: Serialize>(
|
|
||||||
data: &T,
|
|
||||||
keypair: &Ed25519KeyPair,
|
|
||||||
last_id: &Sha256Hash,
|
|
||||||
) -> Signature {
|
|
||||||
sign_transaction_data(data, keypair, &get_pubkey(keypair), last_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify a signed message with the given public key.
|
|
||||||
pub fn verify_signature(peer_public_key_bytes: &[u8], msg_bytes: &[u8], sig_bytes: &[u8]) -> bool {
|
|
||||||
let peer_public_key = untrusted::Input::from(peer_public_key_bytes);
|
|
||||||
let msg = untrusted::Input::from(msg_bytes);
|
|
||||||
let sig = untrusted::Input::from(sig_bytes);
|
|
||||||
signature::verify(&signature::ED25519, peer_public_key, msg, sig).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_signature<T>(event: &Event<T>) -> Option<Signature> {
|
|
||||||
match *event {
|
|
||||||
Event::Tick => None,
|
|
||||||
Event::Transaction { sig, .. } => Some(sig),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify_event<T: Serialize>(event: &Event<T>) -> bool {
|
|
||||||
if let Event::Transaction {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
ref data,
|
|
||||||
last_id,
|
|
||||||
sig,
|
|
||||||
} = *event
|
|
||||||
{
|
|
||||||
let sign_data = serialize(&(&from, &to, &data, &last_id)).unwrap();
|
|
||||||
if !verify_signature(&from, &sign_data, &sig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use bincode::{deserialize, serialize};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_claim() {
|
|
||||||
let claim0 = Event::new_claim(
|
|
||||||
Default::default(),
|
|
||||||
0u8,
|
|
||||||
Default::default(),
|
|
||||||
Default::default(),
|
|
||||||
);
|
|
||||||
let buf = serialize(&claim0).unwrap();
|
|
||||||
let claim1: Event<u8> = deserialize(&buf).unwrap();
|
|
||||||
assert_eq!(claim1, claim0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
//! A library for generating the chain's genesis block.
|
//! A library for generating the chain's genesis block.
|
||||||
|
|
||||||
use event::{generate_keypair, get_pubkey, sign_transaction_data, Event, PublicKey};
|
use event::Event;
|
||||||
|
use transaction::{sign_transaction_data, Transaction};
|
||||||
|
use signature::{generate_keypair, get_pubkey, PublicKey};
|
||||||
use log::{create_entries, hash, Entry, Sha256Hash};
|
use log::{create_entries, hash, Entry, Sha256Hash};
|
||||||
use ring::rand::SystemRandom;
|
use ring::rand::SystemRandom;
|
||||||
use ring::signature::Ed25519KeyPair;
|
use ring::signature::Ed25519KeyPair;
|
||||||
@ -56,13 +58,13 @@ impl Genesis {
|
|||||||
let last_id = self.get_seed();
|
let last_id = self.get_seed();
|
||||||
let from = self.get_pubkey();
|
let from = self.get_pubkey();
|
||||||
let sig = sign_transaction_data(&data, &self.get_keypair(), to, &last_id);
|
let sig = sign_transaction_data(&data, &self.get_keypair(), to, &last_id);
|
||||||
Event::Transaction {
|
Event::Transaction(Transaction {
|
||||||
from,
|
from,
|
||||||
to: *to,
|
to: *to,
|
||||||
data,
|
data,
|
||||||
last_id,
|
last_id,
|
||||||
sig,
|
sig,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_events(&self) -> Vec<Event<i64>> {
|
pub fn create_events(&self) -> Vec<Event<i64>> {
|
||||||
@ -93,7 +95,7 @@ mod tests {
|
|||||||
fn test_create_events() {
|
fn test_create_events() {
|
||||||
let mut events = Genesis::new(100, vec![]).create_events().into_iter();
|
let mut events = Genesis::new(100, vec![]).create_events().into_iter();
|
||||||
assert_eq!(events.next().unwrap(), Event::Tick);
|
assert_eq!(events.next().unwrap(), Event::Tick);
|
||||||
if let Event::Transaction { from, to, .. } = events.next().unwrap() {
|
if let Event::Transaction(Transaction { from, to, .. }) = events.next().unwrap() {
|
||||||
assert_eq!(from, to);
|
assert_eq!(from, to);
|
||||||
} else {
|
} else {
|
||||||
assert!(false);
|
assert!(false);
|
||||||
|
@ -7,7 +7,8 @@ use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use log::{hash, Entry, Sha256Hash};
|
use log::{hash, Entry, Sha256Hash};
|
||||||
use logger::{ExitReason, Logger};
|
use logger::{ExitReason, Logger};
|
||||||
use event::{get_signature, Event, Signature};
|
use signature::Signature;
|
||||||
|
use event::Event;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
@ -55,13 +56,11 @@ impl<T: 'static + Serialize + Clone + Debug + Send> Historian<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reserve_signature<T>(sigs: &mut HashSet<Signature>, event: &Event<T>) -> bool {
|
pub fn reserve_signature(sigs: &mut HashSet<Signature>, sig: &Signature) -> bool {
|
||||||
if let Some(sig) = get_signature(&event) {
|
if sigs.contains(sig) {
|
||||||
if sigs.contains(&sig) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
sigs.insert(sig);
|
|
||||||
}
|
}
|
||||||
|
sigs.insert(*sig);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +69,8 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use log::*;
|
use log::*;
|
||||||
use event::*;
|
use event::*;
|
||||||
|
use transaction::*;
|
||||||
|
use signature::*;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -116,10 +117,10 @@ mod tests {
|
|||||||
let data = b"hello, world";
|
let data = b"hello, world";
|
||||||
let zero = Sha256Hash::default();
|
let zero = Sha256Hash::default();
|
||||||
let sig = sign_claim_data(&data, &keypair, &zero);
|
let sig = sign_claim_data(&data, &keypair, &zero);
|
||||||
let event0 = Event::new_claim(to, &data, zero, sig);
|
let tr0 = Transaction::new_claim(to, &data, zero, sig);
|
||||||
let mut sigs = HashSet::new();
|
let mut sigs = HashSet::new();
|
||||||
assert!(reserve_signature(&mut sigs, &event0));
|
assert!(reserve_signature(&mut sigs, &tr0.sig));
|
||||||
assert!(!reserve_signature(&mut sigs, &event0));
|
assert!(!reserve_signature(&mut sigs, &tr0.sig));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
pub mod log;
|
pub mod log;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod transaction;
|
||||||
|
pub mod signature;
|
||||||
pub mod genesis;
|
pub mod genesis;
|
||||||
pub mod historian;
|
pub mod historian;
|
||||||
pub mod accountant;
|
pub mod accountant;
|
||||||
|
55
src/log.rs
55
src/log.rs
@ -16,7 +16,7 @@
|
|||||||
use generic_array::GenericArray;
|
use generic_array::GenericArray;
|
||||||
use generic_array::typenum::U32;
|
use generic_array::typenum::U32;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use event::{get_signature, verify_event, Event};
|
use event::Event;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ pub struct Entry<T> {
|
|||||||
pub event: Event<T>,
|
pub event: Event<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Entry<T> {
|
impl<T: Serialize> Entry<T> {
|
||||||
/// Creates a Entry from the number of hashes 'num_hashes' since the previous event
|
/// Creates a Entry from the number of hashes 'num_hashes' since the previous event
|
||||||
/// and that resulting 'id'.
|
/// and that resulting 'id'.
|
||||||
pub fn new_tick(num_hashes: u64, id: &Sha256Hash) -> Self {
|
pub fn new_tick(num_hashes: u64, id: &Sha256Hash) -> Self {
|
||||||
@ -39,6 +39,15 @@ impl<T> Entry<T> {
|
|||||||
event: Event::Tick,
|
event: Event::Tick,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verifies self.id is the result of hashing a 'start_hash' 'self.num_hashes' times.
|
||||||
|
/// If the event is not a Tick, then hash that as well.
|
||||||
|
pub fn verify(&self, start_hash: &Sha256Hash) -> bool {
|
||||||
|
if !self.event.verify() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.id == next_hash(start_hash, self.num_hashes, &self.event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a Sha256 hash for the given data.
|
/// Return a Sha256 hash for the given data.
|
||||||
@ -64,7 +73,7 @@ pub fn next_hash<T: Serialize>(
|
|||||||
event: &Event<T>,
|
event: &Event<T>,
|
||||||
) -> Sha256Hash {
|
) -> Sha256Hash {
|
||||||
let mut id = *start_hash;
|
let mut id = *start_hash;
|
||||||
let sig = get_signature(event);
|
let sig = event.get_signature();
|
||||||
let start_index = if sig.is_some() { 1 } else { 0 };
|
let start_index = if sig.is_some() { 1 } else { 0 };
|
||||||
for _ in start_index..num_hashes {
|
for _ in start_index..num_hashes {
|
||||||
id = hash(&id);
|
id = hash(&id);
|
||||||
@ -81,7 +90,7 @@ pub fn create_entry<T: Serialize>(
|
|||||||
cur_hashes: u64,
|
cur_hashes: u64,
|
||||||
event: Event<T>,
|
event: Event<T>,
|
||||||
) -> Entry<T> {
|
) -> Entry<T> {
|
||||||
let sig = get_signature(&event);
|
let sig = event.get_signature();
|
||||||
let num_hashes = cur_hashes + if sig.is_some() { 1 } else { 0 };
|
let num_hashes = cur_hashes + if sig.is_some() { 1 } else { 0 };
|
||||||
let id = next_hash(start_hash, 0, &event);
|
let id = next_hash(start_hash, 0, &event);
|
||||||
Entry {
|
Entry {
|
||||||
@ -113,34 +122,25 @@ pub fn next_tick<T: Serialize>(start_hash: &Sha256Hash, num_hashes: u64) -> Entr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies self.id is the result of hashing a 'start_hash' 'self.num_hashes' times.
|
|
||||||
/// If the event is not a Tick, then hash that as well.
|
|
||||||
pub fn verify_entry<T: Serialize>(entry: &Entry<T>, start_hash: &Sha256Hash) -> bool {
|
|
||||||
if !verify_event(&entry.event) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
entry.id == next_hash(start_hash, entry.num_hashes, &entry.event)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifies the hashes and counts of a slice of events are all consistent.
|
/// Verifies the hashes and counts of a slice of events are all consistent.
|
||||||
pub fn verify_slice(events: &[Entry<Sha256Hash>], start_hash: &Sha256Hash) -> bool {
|
pub fn verify_slice(events: &[Entry<Sha256Hash>], start_hash: &Sha256Hash) -> bool {
|
||||||
let genesis = [Entry::new_tick(Default::default(), start_hash)];
|
let genesis = [Entry::new_tick(Default::default(), start_hash)];
|
||||||
let event_pairs = genesis.par_iter().chain(events).zip(events);
|
let event_pairs = genesis.par_iter().chain(events).zip(events);
|
||||||
event_pairs.all(|(x0, x1)| verify_entry(&x1, &x0.id))
|
event_pairs.all(|(x0, x1)| x1.verify(&x0.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies the hashes and counts of a slice of events are all consistent.
|
/// Verifies the hashes and counts of a slice of events are all consistent.
|
||||||
pub fn verify_slice_i64(events: &[Entry<i64>], start_hash: &Sha256Hash) -> bool {
|
pub fn verify_slice_i64(events: &[Entry<i64>], start_hash: &Sha256Hash) -> bool {
|
||||||
let genesis = [Entry::new_tick(Default::default(), start_hash)];
|
let genesis = [Entry::new_tick(Default::default(), start_hash)];
|
||||||
let event_pairs = genesis.par_iter().chain(events).zip(events);
|
let event_pairs = genesis.par_iter().chain(events).zip(events);
|
||||||
event_pairs.all(|(x0, x1)| verify_entry(&x1, &x0.id))
|
event_pairs.all(|(x0, x1)| x1.verify(&x0.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies the hashes and events serially. Exists only for reference.
|
/// Verifies the hashes and events serially. Exists only for reference.
|
||||||
pub fn verify_slice_seq<T: Serialize>(events: &[Entry<T>], start_hash: &Sha256Hash) -> bool {
|
pub fn verify_slice_seq<T: Serialize>(events: &[Entry<T>], start_hash: &Sha256Hash) -> bool {
|
||||||
let genesis = [Entry::new_tick(0, start_hash)];
|
let genesis = [Entry::new_tick(0, start_hash)];
|
||||||
let mut event_pairs = genesis.iter().chain(events).zip(events);
|
let mut event_pairs = genesis.iter().chain(events).zip(events);
|
||||||
event_pairs.all(|(x0, x1)| verify_entry(&x1, &x0.id))
|
event_pairs.all(|(x0, x1)| x1.verify(&x0.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_entries<T: Serialize>(
|
pub fn create_entries<T: Serialize>(
|
||||||
@ -169,16 +169,17 @@ pub fn next_ticks(start_hash: &Sha256Hash, num_hashes: u64, len: usize) -> Vec<E
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use event::{generate_keypair, get_pubkey, sign_claim_data, sign_transaction_data};
|
use signature::{generate_keypair, get_pubkey};
|
||||||
|
use transaction::{sign_claim_data, sign_transaction_data, Transaction};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_event_verify() {
|
fn test_event_verify() {
|
||||||
let zero = Sha256Hash::default();
|
let zero = Sha256Hash::default();
|
||||||
let one = hash(&zero);
|
let one = hash(&zero);
|
||||||
assert!(verify_entry::<u8>(&Entry::new_tick(0, &zero), &zero)); // base case
|
assert!(Entry::<u8>::new_tick(0, &zero).verify(&zero)); // base case
|
||||||
assert!(!verify_entry::<u8>(&Entry::new_tick(0, &zero), &one)); // base case, bad
|
assert!(!Entry::<u8>::new_tick(0, &zero).verify(&one)); // base case, bad
|
||||||
assert!(verify_entry::<u8>(&next_tick(&zero, 1), &zero)); // inductive step
|
assert!(next_tick::<u8>(&zero, 1).verify(&zero)); // inductive step
|
||||||
assert!(!verify_entry::<u8>(&next_tick(&zero, 1), &one)); // inductive step, bad
|
assert!(!next_tick::<u8>(&zero, 1).verify(&one)); // inductive step, bad
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -277,13 +278,13 @@ mod tests {
|
|||||||
let keypair1 = generate_keypair();
|
let keypair1 = generate_keypair();
|
||||||
let pubkey1 = get_pubkey(&keypair1);
|
let pubkey1 = get_pubkey(&keypair1);
|
||||||
let data = hash(b"hello, world");
|
let data = hash(b"hello, world");
|
||||||
let event0 = Event::Transaction {
|
let event0 = Event::Transaction(Transaction {
|
||||||
from: get_pubkey(&keypair0),
|
from: get_pubkey(&keypair0),
|
||||||
to: pubkey1,
|
to: pubkey1,
|
||||||
data,
|
data,
|
||||||
last_id: zero,
|
last_id: zero,
|
||||||
sig: sign_transaction_data(&data, &keypair0, &pubkey1, &zero),
|
sig: sign_transaction_data(&data, &keypair0, &pubkey1, &zero),
|
||||||
};
|
});
|
||||||
let entries = create_entries(&zero, vec![event0]);
|
let entries = create_entries(&zero, vec![event0]);
|
||||||
assert!(verify_slice(&entries, &zero));
|
assert!(verify_slice(&entries, &zero));
|
||||||
}
|
}
|
||||||
@ -295,13 +296,13 @@ mod tests {
|
|||||||
let pubkey1 = get_pubkey(&keypair1);
|
let pubkey1 = get_pubkey(&keypair1);
|
||||||
let data = hash(b"hello, world");
|
let data = hash(b"hello, world");
|
||||||
let zero = Sha256Hash::default();
|
let zero = Sha256Hash::default();
|
||||||
let event0 = Event::Transaction {
|
let event0 = Event::Transaction(Transaction {
|
||||||
from: get_pubkey(&keypair0),
|
from: get_pubkey(&keypair0),
|
||||||
to: pubkey1,
|
to: pubkey1,
|
||||||
data: hash(b"goodbye cruel world"), // <-- attack!
|
data: hash(b"goodbye cruel world"), // <-- attack!
|
||||||
last_id: zero,
|
last_id: zero,
|
||||||
sig: sign_transaction_data(&data, &keypair0, &pubkey1, &zero),
|
sig: sign_transaction_data(&data, &keypair0, &pubkey1, &zero),
|
||||||
};
|
});
|
||||||
let entries = create_entries(&zero, vec![event0]);
|
let entries = create_entries(&zero, vec![event0]);
|
||||||
assert!(!verify_slice(&entries, &zero));
|
assert!(!verify_slice(&entries, &zero));
|
||||||
}
|
}
|
||||||
@ -314,13 +315,13 @@ mod tests {
|
|||||||
let pubkey1 = get_pubkey(&keypair1);
|
let pubkey1 = get_pubkey(&keypair1);
|
||||||
let data = hash(b"hello, world");
|
let data = hash(b"hello, world");
|
||||||
let zero = Sha256Hash::default();
|
let zero = Sha256Hash::default();
|
||||||
let event0 = Event::Transaction {
|
let event0 = Event::Transaction(Transaction {
|
||||||
from: get_pubkey(&keypair0),
|
from: get_pubkey(&keypair0),
|
||||||
to: get_pubkey(&thief_keypair), // <-- attack!
|
to: get_pubkey(&thief_keypair), // <-- attack!
|
||||||
data: hash(b"goodbye cruel world"),
|
data: hash(b"goodbye cruel world"),
|
||||||
last_id: zero,
|
last_id: zero,
|
||||||
sig: sign_transaction_data(&data, &keypair0, &pubkey1, &zero),
|
sig: sign_transaction_data(&data, &keypair0, &pubkey1, &zero),
|
||||||
};
|
});
|
||||||
let entries = create_entries(&zero, vec![event0]);
|
let entries = create_entries(&zero, vec![event0]);
|
||||||
assert!(!verify_slice(&entries, &zero));
|
assert!(!verify_slice(&entries, &zero));
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,8 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use log::*;
|
use log::*;
|
||||||
use event::*;
|
use event::*;
|
||||||
|
use signature::*;
|
||||||
|
use transaction::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bad_event_signature() {
|
fn test_bad_event_signature() {
|
||||||
@ -92,6 +94,6 @@ mod tests {
|
|||||||
zero,
|
zero,
|
||||||
sig,
|
sig,
|
||||||
);
|
);
|
||||||
assert!(!verify_event(&event0));
|
assert!(!event0.verify());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
30
src/signature.rs
Normal file
30
src/signature.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//! The `signature` crate provides functionality for public and private keys
|
||||||
|
|
||||||
|
use generic_array::GenericArray;
|
||||||
|
use generic_array::typenum::{U32, U64};
|
||||||
|
use ring::signature::Ed25519KeyPair;
|
||||||
|
use ring::{rand, signature};
|
||||||
|
use untrusted;
|
||||||
|
|
||||||
|
pub type PublicKey = GenericArray<u8, U32>;
|
||||||
|
pub type Signature = GenericArray<u8, U64>;
|
||||||
|
|
||||||
|
/// Return a new ED25519 keypair
|
||||||
|
pub fn generate_keypair() -> Ed25519KeyPair {
|
||||||
|
let rng = rand::SystemRandom::new();
|
||||||
|
let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
|
||||||
|
signature::Ed25519KeyPair::from_pkcs8(untrusted::Input::from(&pkcs8_bytes)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the public key for the given keypair
|
||||||
|
pub fn get_pubkey(keypair: &Ed25519KeyPair) -> PublicKey {
|
||||||
|
GenericArray::clone_from_slice(keypair.public_key_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a signed message with the given public key.
|
||||||
|
pub fn verify_signature(peer_public_key_bytes: &[u8], msg_bytes: &[u8], sig_bytes: &[u8]) -> bool {
|
||||||
|
let peer_public_key = untrusted::Input::from(peer_public_key_bytes);
|
||||||
|
let msg = untrusted::Input::from(msg_bytes);
|
||||||
|
let sig = untrusted::Input::from(sig_bytes);
|
||||||
|
signature::verify(&signature::ED25519, peer_public_key, msg, sig).is_ok()
|
||||||
|
}
|
77
src/transaction.rs
Normal file
77
src/transaction.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
//! The `transaction` crate provides functionality for creating log transactions.
|
||||||
|
|
||||||
|
use signature::{get_pubkey, verify_signature, PublicKey, Signature};
|
||||||
|
use ring::signature::Ed25519KeyPair;
|
||||||
|
use serde::Serialize;
|
||||||
|
use bincode::serialize;
|
||||||
|
use log::Sha256Hash;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct Transaction<T> {
|
||||||
|
pub from: PublicKey,
|
||||||
|
pub to: PublicKey,
|
||||||
|
pub data: T,
|
||||||
|
pub last_id: Sha256Hash,
|
||||||
|
pub sig: Signature,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize> Transaction<T> {
|
||||||
|
pub fn new_claim(to: PublicKey, data: T, last_id: Sha256Hash, sig: Signature) -> Self {
|
||||||
|
Transaction {
|
||||||
|
from: to,
|
||||||
|
to,
|
||||||
|
data,
|
||||||
|
last_id,
|
||||||
|
sig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self) -> bool {
|
||||||
|
let sign_data = serialize(&(&self.from, &self.to, &self.data, &self.last_id)).unwrap();
|
||||||
|
verify_signature(&self.from, &sign_data, &self.sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_serialized<T: Serialize>(data: &T, keypair: &Ed25519KeyPair) -> Signature {
|
||||||
|
let serialized = serialize(data).unwrap();
|
||||||
|
Signature::clone_from_slice(keypair.sign(&serialized).as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a signature for the given transaction data using the private key from the given keypair.
|
||||||
|
pub fn sign_transaction_data<T: Serialize>(
|
||||||
|
data: &T,
|
||||||
|
keypair: &Ed25519KeyPair,
|
||||||
|
to: &PublicKey,
|
||||||
|
last_id: &Sha256Hash,
|
||||||
|
) -> Signature {
|
||||||
|
let from = &get_pubkey(keypair);
|
||||||
|
sign_serialized(&(from, to, data, last_id), keypair)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a signature for the given data using the private key from the given keypair.
|
||||||
|
pub fn sign_claim_data<T: Serialize>(
|
||||||
|
data: &T,
|
||||||
|
keypair: &Ed25519KeyPair,
|
||||||
|
last_id: &Sha256Hash,
|
||||||
|
) -> Signature {
|
||||||
|
sign_transaction_data(data, keypair, &get_pubkey(keypair), last_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use bincode::{deserialize, serialize};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_claim() {
|
||||||
|
let claim0 = Transaction::new_claim(
|
||||||
|
Default::default(),
|
||||||
|
0u8,
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
);
|
||||||
|
let buf = serialize(&claim0).unwrap();
|
||||||
|
let claim1: Transaction<u8> = deserialize(&buf).unwrap();
|
||||||
|
assert_eq!(claim1, claim0);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user