Compare commits

...

16 Commits

Author SHA1 Message Date
bb22522e45 Remove assertions that fail in the kcov docker container 2018-02-19 12:54:01 -07:00
41053b6d0b Merge pull request #12 from garious/add-historian
Add historian demo
2018-02-19 12:40:44 -07:00
bd3fe5fac9 Sleep a little longer to ensure Travis context switches 2018-02-19 12:33:33 -07:00
10a70a238b Cleanup demo 2018-02-19 12:25:57 -07:00
0bead4d410 Fix markdown link 2018-02-19 12:12:45 -07:00
4a7156de43 Move hash generation into stateless function 2018-02-19 12:09:58 -07:00
d88d1b2a09 Reset historian's hasher between events
Hasher will generate different hashes for the same input if it
had already generated a hash.

Also add a binary to ensure the example in the README works.
2018-02-19 12:03:06 -07:00
a7186328e0 Add docs
Fixes #11
2018-02-19 09:27:14 -07:00
5e3c7816bd Ensure verify_slice succeeds 2018-02-19 09:09:24 -07:00
a2fa60fa31 Merge pull request #10 from garious/add-historian
Better names
2018-02-18 10:16:05 -07:00
ceb65c2669 Better function names 2018-02-18 10:05:54 -07:00
fd209ef1a9 Rename event crate to log 2018-02-18 09:59:33 -07:00
471f036444 Better names
Event -> Entry
EventData -> Event
2018-02-18 09:53:40 -07:00
6ec0e5834c Merge pull request #9 from garious/add-historian
Add historian
2018-02-17 21:22:48 -07:00
4c94754661 More coverage 2018-02-17 21:13:52 -07:00
831e2cbdc9 Add historian
A microservice that continuously generates hashes, only stopping to
tag messages with the latest hash.

Fixes #8
2018-02-17 20:58:23 -07:00
6 changed files with 261 additions and 38 deletions

View File

@ -1,7 +1,7 @@
[package]
name = "silk"
description = "A silky smooth implementation of the Loom architecture"
version = "0.1.3"
version = "0.2.0"
documentation = "https://docs.rs/silk"
homepage = "http://loomprotocol.com/"
repository = "https://github.com/loomprotocol/silk"
@ -11,6 +11,10 @@ authors = [
]
license = "Apache-2.0"
[[bin]]
name = "silk-demo"
path = "src/bin/demo.rs"
[badges]
codecov = { repository = "loomprotocol/silk", branch = "master", service = "github" }

View File

@ -15,6 +15,54 @@ corresponding benchmarks are also added that demonstrate real performance boots.
feature set here will always be a ways behind the loom repo, but that this is an implementation
you can take to the bank, literally.
# Usage
Add the latest [silk package](https://crates.io/crates/silk) to the `[dependencies]` section
of your Cargo.toml.
Create a *Historian* and send it *events* to generate an *event log*, where each log *entry*
is tagged with the historian's latest *hash*. Then ensure the order of events was not tampered
with by verifying each entry's hash can be generated from the hash in the previous entry:
```rust
extern crate silk;
use silk::historian::Historian;
use silk::log::{verify_slice, Entry, Event};
use std::{thread, time};
use std::sync::mpsc::SendError;
fn create_log(hist: &Historian) -> Result<(), SendError<Event>> {
hist.sender.send(Event::Tick)?;
thread::sleep(time::Duration::new(0, 100_000));
hist.sender.send(Event::UserDataKey(0xdeadbeef))?;
thread::sleep(time::Duration::new(0, 100_000));
hist.sender.send(Event::Tick)?;
Ok(())
}
fn main() {
let seed = 0;
let hist = Historian::new(seed);
create_log(&hist).expect("send error");
drop(hist.sender);
let entries: Vec<Entry> = hist.receiver.iter().collect();
for entry in &entries {
println!("{:?}", entry);
}
assert!(verify_slice(&entries, seed));
}
```
Running the program should produce a log similar to:
```
Entry { num_hashes: 0, end_hash: 0, event: Tick }
Entry { num_hashes: 245, end_hash: 11504657626326377539, event: UserDataKey(3735928559) }
Entry { num_hashes: 154, end_hash: 13410333856574024888, event: Tick }
```
# Developing
Building

27
src/bin/demo.rs Normal file
View File

@ -0,0 +1,27 @@
extern crate silk;
use silk::historian::Historian;
use silk::log::{verify_slice, Entry, Event};
use std::{thread, time};
use std::sync::mpsc::SendError;
fn create_log(hist: &Historian) -> Result<(), SendError<Event>> {
hist.sender.send(Event::Tick)?;
thread::sleep(time::Duration::new(0, 100_000));
hist.sender.send(Event::UserDataKey(0xdeadbeef))?;
thread::sleep(time::Duration::new(0, 100_000));
hist.sender.send(Event::Tick)?;
Ok(())
}
fn main() {
let seed = 0;
let hist = Historian::new(seed);
create_log(&hist).expect("send error");
drop(hist.sender);
let entries: Vec<Entry> = hist.receiver.iter().collect();
for entry in &entries {
println!("{:?}", entry);
}
assert!(verify_slice(&entries, seed));
}

137
src/historian.rs Normal file
View File

@ -0,0 +1,137 @@
//! The `historian` crate provides a microservice for generating a Proof-of-History.
//! It logs Event items on behalf of its users. It continuously generates
//! new hashes, only stopping to check if it has been sent an Event item. It
//! tags each Event with an Entry and sends it back. The Entry includes the
//! Event, the latest hash, and the number of hashes since the last event.
//! The resulting stream of entries represents ordered events in time.
use std::thread::JoinHandle;
use std::sync::mpsc::{Receiver, Sender};
use log::{hash, Entry, Event};
pub struct Historian {
pub sender: Sender<Event>,
pub receiver: Receiver<Entry>,
pub thread_hdl: JoinHandle<(Entry, ExitReason)>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ExitReason {
RecvDisconnected,
SendDisconnected,
}
fn log_events(
receiver: &Receiver<Event>,
sender: &Sender<Entry>,
num_hashes: u64,
end_hash: u64,
) -> Result<u64, (Entry, ExitReason)> {
use std::sync::mpsc::TryRecvError;
let mut num_hashes = num_hashes;
loop {
match receiver.try_recv() {
Ok(event) => {
let entry = Entry {
end_hash,
num_hashes,
event,
};
if let Err(_) = sender.send(entry.clone()) {
return Err((entry, ExitReason::SendDisconnected));
}
num_hashes = 0;
}
Err(TryRecvError::Empty) => {
return Ok(num_hashes);
}
Err(TryRecvError::Disconnected) => {
let entry = Entry {
end_hash,
num_hashes,
event: Event::Tick,
};
return Err((entry, ExitReason::RecvDisconnected));
}
}
}
}
/// A background thread that will continue tagging received Event messages and
/// sending back Entry messages until either the receiver or sender channel is closed.
pub fn create_logger(
start_hash: u64,
receiver: Receiver<Event>,
sender: Sender<Entry>,
) -> JoinHandle<(Entry, ExitReason)> {
use std::thread;
thread::spawn(move || {
let mut end_hash = start_hash;
let mut num_hashes = 0;
loop {
match log_events(&receiver, &sender, num_hashes, end_hash) {
Ok(n) => num_hashes = n,
Err(err) => return err,
}
end_hash = hash(end_hash);
num_hashes += 1;
}
})
}
impl Historian {
pub fn new(start_hash: u64) -> Self {
use std::sync::mpsc::channel;
let (sender, event_receiver) = channel();
let (entry_sender, receiver) = channel();
let thread_hdl = create_logger(start_hash, event_receiver, entry_sender);
Historian {
sender,
receiver,
thread_hdl,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use log::*;
#[test]
fn test_historian() {
use std::thread::sleep;
use std::time::Duration;
let hist = Historian::new(0);
hist.sender.send(Event::Tick).unwrap();
sleep(Duration::new(0, 1_000_000));
hist.sender.send(Event::UserDataKey(0xdeadbeef)).unwrap();
sleep(Duration::new(0, 1_000_000));
hist.sender.send(Event::Tick).unwrap();
let entry0 = hist.receiver.recv().unwrap();
let entry1 = hist.receiver.recv().unwrap();
let entry2 = hist.receiver.recv().unwrap();
drop(hist.sender);
assert_eq!(
hist.thread_hdl.join().unwrap().1,
ExitReason::RecvDisconnected
);
assert!(verify_slice(&[entry0, entry1, entry2], 0));
}
#[test]
fn test_historian_closed_sender() {
let hist = Historian::new(0);
drop(hist.receiver);
hist.sender.send(Event::Tick).unwrap();
assert_eq!(
hist.thread_hdl.join().unwrap().1,
ExitReason::SendDisconnected
);
}
}

View File

@ -1,4 +1,5 @@
#![cfg_attr(feature = "unstable", feature(test))]
pub mod event;
pub mod log;
pub mod historian;
extern crate itertools;
extern crate rayon;

View File

@ -1,10 +1,10 @@
//! The `event` crate provides the foundational data structures for Proof-of-History
//! The `log` crate provides the foundational data structures for Proof-of-History,
//! an ordered log of events in time.
/// A Proof-of-History is an ordered log of events in time. Each entry contains three
/// pieces of data. The 'num_hashes' field is the number of hashes performed since the previous
/// entry. The 'end_hash' field is the result of hashing 'end_hash' from the previous entry
/// 'num_hashes' times. The 'data' field is an optional foreign key (a hash) pointing to some
/// arbitrary data that a client is looking to associate with the entry.
/// Each log entry contains three pieces of data. The 'num_hashes' field is the number
/// of hashes performed since the previous entry. The 'end_hash' field is the result
/// of hashing 'end_hash' from the previous entry 'num_hashes' times. The 'event'
/// field points to an Event that took place shortly after 'end_hash' 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
@ -12,31 +12,33 @@
/// 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.
pub struct Event {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Entry {
pub num_hashes: u64,
pub end_hash: u64,
pub data: EventData,
pub event: Event,
}
/// When 'data' 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
/// be generated in 'num_hashes' hashes and verified in 'num_hashes' hashes. By logging
/// a hash alongside the tick, each tick and be verified in parallel using the 'end_hash'
/// of the preceding tick to seed its hashing.
pub enum EventData {
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Event {
Tick,
UserDataKey(u64),
}
impl Event {
/// Creates an Event from the number of hashes 'num_hashes' since the previous event
impl Entry {
/// Creates a Entry from the number of hashes 'num_hashes' since the previous event
/// and that resulting 'end_hash'.
pub fn new_tick(num_hashes: u64, end_hash: u64) -> Self {
let data = EventData::Tick;
Event {
let event = Event::Tick;
Entry {
num_hashes,
end_hash,
data,
event,
}
}
@ -46,36 +48,40 @@ impl Event {
}
}
/// Creates the next Tick Event 'num_hashes' after 'start_hash'.
pub fn next_tick(start_hash: u64, num_hashes: u64) -> Event {
pub fn hash(val: u64) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut end_hash = start_hash;
let mut hasher = DefaultHasher::new();
val.hash(&mut hasher);
hasher.finish()
}
/// Creates the next Tick Entry 'num_hashes' after 'start_hash'.
pub fn next_tick(start_hash: u64, num_hashes: u64) -> Entry {
let mut end_hash = start_hash;
for _ in 0..num_hashes {
end_hash.hash(&mut hasher);
end_hash = hasher.finish();
end_hash = hash(end_hash);
}
Event::new_tick(num_hashes, end_hash)
Entry::new_tick(num_hashes, end_hash)
}
/// Verifies the hashes and counts of a slice of events are all consistent.
pub fn verify_slice(events: &[Event], start_hash: u64) -> bool {
pub fn verify_slice(events: &[Entry], start_hash: u64) -> bool {
use rayon::prelude::*;
let genesis = [Event::new_tick(0, start_hash)];
let genesis = [Entry::new_tick(0, start_hash)];
let event_pairs = genesis.par_iter().chain(events).zip(events);
event_pairs.all(|(x0, x1)| x1.verify(x0.end_hash))
}
/// Verifies the hashes and events serially. Exists only for reference.
pub fn verify_slice_seq(events: &[Event], start_hash: u64) -> bool {
let genesis = [Event::new_tick(0, start_hash)];
pub fn verify_slice_seq(events: &[Entry], start_hash: u64) -> bool {
let genesis = [Entry::new_tick(0, start_hash)];
let mut event_pairs = genesis.iter().chain(events).zip(events);
event_pairs.all(|(x0, x1)| x1.verify(x0.end_hash))
}
/// Create a vector of Ticks of length 'len' from 'start_hash' hash and 'num_hashes'.
pub fn create_ticks(start_hash: u64, num_hashes: u64, len: usize) -> Vec<Event> {
pub fn create_ticks(start_hash: u64, num_hashes: u64, len: usize) -> Vec<Entry> {
use itertools::unfold;
let mut events = unfold(start_hash, |state| {
let event = next_tick(*state, num_hashes);
@ -91,8 +97,8 @@ mod tests {
#[test]
fn test_event_verify() {
assert!(Event::new_tick(0, 0).verify(0)); // base case
assert!(!Event::new_tick(0, 0).verify(1)); // base case, bad
assert!(Entry::new_tick(0, 0).verify(0)); // base case
assert!(!Entry::new_tick(0, 0).verify(1)); // base case, bad
assert!(next_tick(0, 1).verify(0)); // inductive step
assert!(!next_tick(0, 1).verify(1)); // inductive step, bad
}
@ -102,10 +108,10 @@ mod tests {
assert_eq!(next_tick(0, 1).num_hashes, 1)
}
fn verify_slice_generic(verify_slice: fn(&[Event], u64) -> bool) {
fn verify_slice_generic(verify_slice: fn(&[Entry], u64) -> bool) {
assert!(verify_slice(&vec![], 0)); // base case
assert!(verify_slice(&vec![Event::new_tick(0, 0)], 0)); // singleton case 1
assert!(!verify_slice(&vec![Event::new_tick(0, 0)], 1)); // singleton case 2, bad
assert!(verify_slice(&vec![Entry::new_tick(0, 0)], 0)); // singleton case 1
assert!(!verify_slice(&vec![Entry::new_tick(0, 0)], 1)); // singleton case 2, bad
assert!(verify_slice(&create_ticks(0, 0, 2), 0)); // inductive step
let mut bad_ticks = create_ticks(0, 0, 2);
@ -129,23 +135,23 @@ mod tests {
mod bench {
extern crate test;
use self::test::Bencher;
use event;
use log::*;
#[bench]
fn event_bench(bencher: &mut Bencher) {
let start_hash = 0;
let events = event::create_ticks(start_hash, 100_000, 8);
let events = create_ticks(start_hash, 100_000, 8);
bencher.iter(|| {
assert!(event::verify_slice(&events, start_hash));
assert!(verify_slice(&events, start_hash));
});
}
#[bench]
fn event_bench_seq(bencher: &mut Bencher) {
let start_hash = 0;
let events = event::create_ticks(start_hash, 100_000, 8);
let events = create_ticks(start_hash, 100_000, 8);
bencher.iter(|| {
assert!(event::verify_slice_seq(&events, start_hash));
assert!(verify_slice_seq(&events, start_hash));
});
}
}