refactor: docstrings + fix sig verification + toy example

buddies-main-deployment
James Prestwich 4 years ago
parent 445bdd0ce6
commit 823a3497aa
No known key found for this signature in database
GPG Key ID: 75A7F5C06D747046
  1. 17
      rust/optics-core/Cargo.lock
  2. 9
      rust/optics-core/Cargo.toml
  3. 64
      rust/optics-core/bin/toy.rs
  4. 22
      rust/optics-core/src/accumulator/incremental.rs
  5. 26
      rust/optics-core/src/accumulator/merkle.rs
  6. 12
      rust/optics-core/src/accumulator/mod.rs
  7. 79
      rust/optics-core/src/accumulator/prover.rs
  8. 51
      rust/optics-core/src/home.rs
  9. 111
      rust/optics-core/src/lib.rs
  10. 40
      rust/optics-core/src/replica.rs

@ -23,6 +23,12 @@ dependencies = [
"syn",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitvec"
version = "0.17.4"
@ -493,6 +499,7 @@ dependencies = [
"lazy_static",
"sha3",
"thiserror",
"tokio",
]
[[package]]
@ -873,6 +880,16 @@ dependencies = [
"crunchy",
]
[[package]]
name = "tokio"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d258221f566b6c803c7b4714abadc080172b272090cdc5e244a6d4dd13c3a6bd"
dependencies = [
"autocfg",
"pin-project-lite",
]
[[package]]
name = "typenum"
version = "1.12.0"

@ -11,4 +11,11 @@ ethers-core = { git = "https://github.com/gakonst/ethers-rs" }
ethers-signers = { git = "https://github.com/gakonst/ethers-rs" }
sha3 = "0.9.1"
lazy_static = "*"
thiserror = "*"
thiserror = "*"
[dev-dependencies]
tokio = {version = "1.0.1", features = ["rt", "time"]}
[[example]]
name = "toy"
path = "./bin/toy.rs"

@ -0,0 +1,64 @@
use ethers_core::types::{H256, U256};
use ethers_signers::LocalWallet;
use std::time::Duration;
use tokio::time::interval;
use optics_core::{home::Home, replica::Replica};
fn main() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(_main())
}
async fn _main() {
let signer: LocalWallet = "1111111111111111111111111111111111111111111111111111111111111111"
.parse()
.unwrap();
let updater = signer.address();
let mut home = Home::init(1, updater);
let mut replica = Replica::init(1, 2, updater, 1.into());
let mut ticks = interval(Duration::from_secs(5));
for i in 0u8..50 {
// delay 5 seconds
ticks.tick().await;
println!("Enqueue 3 messages");
home.enqueue(H256::repeat_byte(i), 2, H256::repeat_byte(i), &[]);
home.enqueue(H256::repeat_byte(i), 2, H256::repeat_byte(i), &[]);
home.enqueue(H256::repeat_byte(i), 2, H256::repeat_byte(i), &[]);
let update = home.produce_update();
let signed = update.sign_with(&signer).await.expect("!sign_with");
println!("\tHome is at\t{}", &home.root());
println!("Create update");
println!("\tfrom\t\t{}", &update.previous_root);
println!("\tto\t\t{}", &update.new_root);
println!("Enqueue 1 message");
home.enqueue(H256::repeat_byte(i), 2, H256::repeat_byte(i), &[]);
println!("\tUpdate Home\t{}", &home.root());
let now = || U256::from(i);
let later = || U256::from(i + 100);
println!("Submit replica Update");
home.update(&signed).expect("!home.update");
let pending = replica.update(&signed, now).expect("!replica.update");
println!("\tReplica is at\t{}", &pending.root());
println!("Confirm replica update");
replica = pending
.confirm_update(later)
.expect("!replica.confirm_update");
println!("\tUpdate Repl \t{}", &replica.root());
println!("--------\n");
}
}

@ -1,19 +1,12 @@
use crate::accumulator::{TREE_DEPTH, ZERO_HASHES};
use ethers_core::types::H256;
use sha3::{Digest, Keccak256};
use crate::accumulator::{hash_concat, TREE_DEPTH, ZERO_HASHES};
#[derive(Debug, Clone, Copy)]
/// An incremental merkle tree, modeled on the eth2 deposit contract
pub struct IncrementalMerkle {
branch: [H256; TREE_DEPTH],
count: u32,
}
fn hash_concat(left: impl AsRef<[u8]>, right: impl AsRef<[u8]>) -> H256 {
let mut k = Keccak256::new();
k.update(left.as_ref());
k.update(right.as_ref());
let digest = k.finalize();
H256::from_slice(digest.as_slice())
count: usize,
}
impl Default for IncrementalMerkle {
@ -28,8 +21,10 @@ impl Default for IncrementalMerkle {
}
impl IncrementalMerkle {
/// Ingest a leaf into the tree.
pub fn ingest(&mut self, element: H256) {
let mut node = element;
assert!(self.count < u32::MAX as usize);
self.count += 1;
let mut size = self.count;
for i in 0..TREE_DEPTH {
@ -43,6 +38,7 @@ impl IncrementalMerkle {
unreachable!()
}
/// Calculate the current tree root
pub fn root(&self) -> H256 {
let mut node: H256 = Default::default();
let mut size = self.count;
@ -59,10 +55,12 @@ impl IncrementalMerkle {
node
}
pub fn count(&self) -> u32 {
/// Get the number of items in the tree
pub fn count(&self) -> usize {
self.count
}
/// Get the leading-edge branch.
pub fn branch(&self) -> &[H256; TREE_DEPTH] {
&self.branch
}

@ -1,7 +1,8 @@
use ethers_core::types::H256;
use lazy_static::lazy_static;
use thiserror::Error;
use crate::accumulator::{hash_concat, EMPTY_SLICE, ZERO_HASHES, ZERO_NODES};
use crate::accumulator::{hash_concat, EMPTY_SLICE, TREE_DEPTH, ZERO_HASHES};
// Some code has been derived from
// https://github.com/sigp/lighthouse/blob/c6baa0eed131c5e8ecc5860778ffc7d4a4c18d2d/consensus/merkle_proof/src/lib.rs#L25
@ -12,6 +13,13 @@ use crate::accumulator::{hash_concat, EMPTY_SLICE, ZERO_HASHES, ZERO_NODES};
// - remove ring dependency
// In accordance with its license terms, the apache2 license is reproduced below
lazy_static! {
/// Zero nodes to act as "synthetic" left and right subtrees of other zero nodes.
pub static ref ZERO_NODES: Vec<MerkleTree> = {
(0..=TREE_DEPTH).map(MerkleTree::Zero).collect()
};
}
/// Right-sparse Merkle tree.
///
/// Efficiently represents a Merkle tree of fixed depth where only the first N
@ -28,18 +36,19 @@ pub enum MerkleTree {
Zero(usize),
}
/// Error type for merkle tree ops.
#[derive(Debug, PartialEq, Clone, Error)]
pub enum MerkleTreeError {
// Trying to push in a leaf
/// Trying to push in a leaf
#[error("Trying to push in a leaf")]
LeafReached,
// No more space in the MerkleTree
/// No more space in the MerkleTree
#[error("No more space in the MerkleTree")]
MerkleTreeFull,
// MerkleTree is invalid
/// MerkleTree is invalid
#[error("MerkleTree is invalid")]
Invalid,
// Incorrect Depth provided
/// Incorrect Depth provided
#[error("Incorrect Depth provided")]
DepthTooSmall,
}
@ -206,7 +215,12 @@ pub fn verify_merkle_proof(
}
/// Compute a root hash from a leaf and a Merkle proof.
fn merkle_root_from_branch(leaf: H256, branch: &[H256], depth: usize, index: usize) -> H256 {
pub(crate) fn merkle_root_from_branch(
leaf: H256,
branch: &[H256],
depth: usize,
index: usize,
) -> H256 {
assert_eq!(branch.len(), depth, "proof length should equal depth");
let mut merkle_root = leaf;

@ -1,7 +1,13 @@
/// A lightweight incremental merkle, suitable for running on-chain. Stores O
/// (1) data
pub mod incremental;
/// A full incremental merkle. Suitable for running off-chain.
pub mod merkle;
/// A wrapper around an incremental and a full merkle, with added safety and
/// convenience. Useful for producing proofs that either may verify.
pub mod prover;
/// Use the prover where possible :)
pub use prover::Prover;
use ethers_core::types::H256;
@ -25,11 +31,7 @@ pub(super) fn hash_concat(left: impl AsRef<[u8]>, right: impl AsRef<[u8]>) -> H2
}
lazy_static! {
/// Zero nodes to act as "synthetic" left and right subtrees of other zero nodes.
pub static ref ZERO_NODES: Vec<merkle::MerkleTree> = {
(0..=TREE_DEPTH).map(merkle::MerkleTree::Zero).collect()
};
/// A cache of the zero hashes for each layer of the tree.
pub static ref ZERO_HASHES: [H256; TREE_DEPTH + 1] = {
let mut hashes = [H256::zero(); TREE_DEPTH + 1];
for i in 0..TREE_DEPTH {

@ -1,11 +1,13 @@
use crate::accumulator::{
incremental::IncrementalMerkle,
merkle::{verify_merkle_proof, MerkleTree},
merkle::{merkle_root_from_branch, MerkleTree, MerkleTreeError},
TREE_DEPTH,
};
use ethers_core::types::H256;
/// A merkle proof object. The leaf, its path to the root, and its index in the
/// tree.
#[derive(Debug, Clone, Copy)]
pub struct Proof {
leaf: H256,
@ -13,12 +15,41 @@ pub struct Proof {
path: [H256; TREE_DEPTH],
}
/// A depth-32 sparse Merkle tree capable of producing proofs for arbitrary
/// elements.
#[derive(Debug)]
pub struct Prover {
light: IncrementalMerkle,
full: MerkleTree,
}
/// Prover Errors
#[derive(Debug, thiserror::Error)]
pub enum ProverError {
/// Index is above tree max size
#[error("Requested proof for index above u32::MAX: {0}")]
IndexTooHigh(usize),
/// Requested proof for a zero element
#[error("Requested proof for a zero element. Requested: {index}. Tree has: {count}")]
ZeroProof {
/// The index requested
index: usize,
/// The number of leaves
count: usize,
},
/// Bubbled up from underlying
#[error(transparent)]
MerkleTreeError(#[from] MerkleTreeError),
/// Failed proof verification
#[error("Proof verification failed. Root is {expected}, produced is {actual}")]
VerificationFailed {
/// The expected root (this tree's current root)
expected: H256,
/// The root produced by branch evaluation
actual: H256,
},
}
impl Default for Prover {
fn default() -> Self {
let light = IncrementalMerkle::default();
@ -28,42 +59,52 @@ impl Default for Prover {
}
impl Prover {
pub fn ingest(&mut self, element: H256) -> H256 {
/// Push a leaf to the tree. Appends it to the first unoccupied slot
///
/// This will fail if the underlying tree is full.
pub fn ingest(&mut self, element: H256) -> Result<H256, ProverError> {
self.light.ingest(element);
self.full.push_leaf(element, TREE_DEPTH).unwrap();
self.full.push_leaf(element, TREE_DEPTH)?;
debug_assert_eq!(self.light.root(), self.full.hash());
self.full.hash()
Ok(self.full.hash())
}
/// Return the current root hash of the tree
pub fn root(&self) -> H256 {
self.full.hash()
}
pub fn count(&self) -> u32 {
/// Return the number of leaves that have been ingested
pub fn count(&self) -> usize {
self.light.count()
}
pub fn prove(&self, index: usize) -> Result<Proof, ()> {
/// Create a proof of a leaf in this tree.
///
/// Note, if the tree ingests more leaves, the root will need to be recalculated.
pub fn prove(&self, index: usize) -> Result<Proof, ProverError> {
if index > u32::MAX as usize {
return Err(());
return Err(ProverError::IndexTooHigh(index));
}
let count = self.count();
if index >= count as usize {
return Err(ProverError::ZeroProof { index, count });
}
let (leaf, hashes) = self.full.generate_proof(index, TREE_DEPTH);
let mut path = [H256::zero(); 32];
path.copy_from_slice(&hashes[..32]);
Ok(Proof { leaf, index, path })
}
pub fn verify(&self, proof: &Proof) -> Result<(), ()> {
if verify_merkle_proof(
proof.leaf,
proof.path.as_ref(),
TREE_DEPTH,
proof.index,
self.root(),
) {
/// Verify a proof against this tree's root.
pub fn verify(&self, proof: &Proof) -> Result<(), ProverError> {
let actual = merkle_root_from_branch(proof.leaf, &proof.path, TREE_DEPTH, proof.index);
let expected = self.root();
if expected == actual {
Ok(())
} else {
Err(())
Err(ProverError::VerificationFailed { expected, actual })
}
}
}
@ -77,9 +118,9 @@ mod test {
let mut tree = Prover::default();
let elements: Vec<_> = (1..32).map(|i| H256::repeat_byte(i as u8)).collect();
tree.ingest(elements[0]);
tree.ingest(elements[1]);
tree.ingest(elements[2]);
tree.ingest(elements[0]).unwrap();
tree.ingest(elements[1]).unwrap();
tree.ingest(elements[2]).unwrap();
assert_eq!(tree.count(), 3);

@ -3,15 +3,17 @@ use std::{collections::VecDeque, io::Write};
use crate::{
accumulator::{hash, incremental::IncrementalMerkle},
SignedUpdate, Update,
OpticsError, SignedUpdate, Update,
};
/// Waiting state
#[derive(Default, Debug, Clone)]
pub struct Waiting {
queue: VecDeque<H256>,
accumulator: IncrementalMerkle,
}
/// Failed state
#[derive(Debug, Clone)]
pub struct Failed {
queue: VecDeque<H256>,
@ -19,20 +21,24 @@ pub struct Failed {
}
impl Waiting {
/// Return a reference to the root queue
pub fn queue(&self) -> &VecDeque<H256> {
&self.queue
}
/// Return a reference to the incremental merkle tree
pub fn accumulator(&self) -> &IncrementalMerkle {
&self.accumulator
}
}
impl Failed {
/// Return a reference to the root queue
pub fn queue(&self) -> &VecDeque<H256> {
&self.queue
}
/// Return a reference to the incremental merkle tree
pub fn accumulator(&self) -> &IncrementalMerkle {
&self.accumulator
}
@ -54,6 +60,7 @@ fn format_message(
buf
}
/// The Home-chain Optics object
#[derive(Debug, Clone)]
pub struct Home<S> {
origin: u32,
@ -63,25 +70,23 @@ pub struct Home<S> {
}
impl<S> Home<S> {
/// SLIP-44 id of the Home chain
pub fn origin(&self) -> u32 {
self.origin
}
/// Ethereum address of the updater
pub fn updater(&self) -> Address {
self.updater
}
/// Current state
pub fn state(&self) -> &S {
&self.state
}
fn check_sig(&self, update: &SignedUpdate) -> Result<(), ()> {
let signer = update.recover()?;
if signer == self.updater {
Ok(())
} else {
Err(())
}
fn check_sig(&self, update: &SignedUpdate) -> Result<(), OpticsError> {
update.verify(self.updater)
}
}
@ -100,6 +105,12 @@ impl From<Home<Waiting>> for Home<Failed> {
}
impl Home<Waiting> {
/// Get the current accumulator root
pub fn root(&self) -> H256 {
self.state().accumulator().root()
}
/// Instantiate a new Home.
pub fn init(origin: u32, updater: Address) -> Home<Waiting> {
Self {
origin,
@ -109,6 +120,7 @@ impl Home<Waiting> {
}
}
/// Enqueue a message
pub fn enqueue(&mut self, sender: H256, destination: u32, recipient: H256, body: &[u8]) {
let message = format_message(self.origin, sender, destination, recipient, body);
let message_hash = hash(&message);
@ -116,9 +128,12 @@ impl Home<Waiting> {
self.state.queue.push_back(self.state.accumulator.root());
}
fn _update(&mut self, update: &Update) -> Result<(), ()> {
fn _update(&mut self, update: &Update) -> Result<(), OpticsError> {
if update.previous_root != self.current_root {
return Err(());
return Err(OpticsError::WrongCurrentRoot {
actual: update.previous_root,
expected: self.current_root,
});
}
if self.state.queue.contains(&update.new_root) {
@ -130,14 +145,25 @@ impl Home<Waiting> {
}
}
Err(())
Err(OpticsError::UnknownNewRoot(update.new_root))
}
/// Produce an update from the current root to the new root.
pub fn produce_update(&self) -> Update {
Update {
origin_chain: self.origin,
previous_root: self.current_root,
new_root: self.state.accumulator.root(),
}
}
pub fn update(&mut self, update: &SignedUpdate) -> Result<(), ()> {
/// Update the root
pub fn update(&mut self, update: &SignedUpdate) -> Result<(), OpticsError> {
self.check_sig(update)?;
self._update(&update.update)
}
/// Notify the Home of a double update, and set failed.
pub fn double_update(
self,
first: &SignedUpdate,
@ -150,6 +176,7 @@ impl Home<Waiting> {
}
}
/// Notify the Home of an improper update, and set failed.
pub fn improper_update(self, update: &SignedUpdate) -> Result<Home<Failed>, Home<Waiting>> {
if self.check_sig(update).is_err() || self.state.queue.contains(&update.update.new_root) {
Err(self)

@ -1,12 +1,49 @@
//! Optics. OPTimistic Interchain Communication
//!
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(unused_extern_crates)]
/// Accumulator management
pub mod accumulator;
/// A simple Home chain Optics implementation
pub mod home;
/// A simple Replica chain Optics implementation
pub mod replica;
use ethers_core::types::{Address, Signature, H256};
use ethers_core::{
types::{Address, Signature, H256},
utils::hash_message,
};
use ethers_signers::Signer;
use sha3::{Digest, Keccak256};
/// Error types for Optics
#[derive(Debug, thiserror::Error)]
pub enum OpticsError {
/// Signature Error pasthrough
#[error(transparent)]
SignatureError(#[from] ethers_core::types::SignatureError),
/// Update does not build off the current root
#[error("Update has wrong current root. Expected: {expected}. Got: {actual}.")]
WrongCurrentRoot {
/// The provided root
actual: H256,
/// The current root
expected: H256,
},
/// Update specifies a new root that is not in the queue. This is an
/// improper update and is slashable
#[error("Update has unknown new root: {0}")]
UnknownNewRoot(H256),
}
/// Simple trait for types with a canonical encoding
pub trait Encode {
/// Write the canonical encoding to the writer
fn write_to<W>(&self, writer: &mut W) -> std::io::Result<usize>
where
W: std::io::Write;
@ -18,7 +55,7 @@ impl Encode for Signature {
W: std::io::Write,
{
writer.write_all(&self.to_vec())?;
Ok(64)
Ok(65)
}
}
@ -32,6 +69,7 @@ fn domain_hash(origin_slip44_id: u32) -> H256 {
)
}
/// An Optics message between chains
#[derive(Debug, Clone)]
pub struct Message {
origin: u32, // 4 SLIP-44 ID
@ -56,11 +94,15 @@ impl Encode for Message {
}
}
/// An Optics update message
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Update {
origin_chain: u32,
previous_root: H256,
new_root: H256,
/// The origin chain
pub origin_chain: u32,
/// The previous root
pub previous_root: H256,
/// The new root
pub new_root: H256,
}
impl Encode for Update {
@ -89,7 +131,12 @@ impl Update {
)
}
pub async fn sign_update<S>(self, signer: S) -> Result<SignedUpdate, S::Error>
fn prepended_hash(&self) -> H256 {
hash_message(self.signing_hash())
}
/// Sign an update using the specified signer
pub async fn sign_with<S>(self, signer: &S) -> Result<SignedUpdate, S::Error>
where
S: Signer,
{
@ -101,12 +148,13 @@ impl Update {
}
}
// 129 bytes.
// serialized as tightly-packed, sig in RSV format
/// A Signed Optics Update
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SignedUpdate {
update: Update,
signature: Signature,
/// The update
pub update: Update,
/// The signature
pub signature: Signature,
}
impl Encode for SignedUpdate {
@ -122,9 +170,44 @@ impl Encode for SignedUpdate {
}
impl SignedUpdate {
pub fn recover(&self) -> Result<Address, ()> {
self.signature
.recover(self.update.signing_hash())
.map_err(|_| ())
/// Recover the Ethereum address of the signer
pub fn recover(&self) -> Result<Address, OpticsError> {
dbg!(self.update.prepended_hash());
Ok(self.signature.recover(self.update.prepended_hash())?)
}
/// Check whether a message was signed by a specific address
pub fn verify(&self, signer: Address) -> Result<(), OpticsError> {
Ok(self
.signature
.verify(self.update.prepended_hash(), signer)?)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn it_sign() {
let t = async {
let signer: ethers_signers::LocalWallet =
"1111111111111111111111111111111111111111111111111111111111111111"
.parse()
.unwrap();
let message = Update {
origin_chain: 5,
new_root: H256::repeat_byte(1),
previous_root: H256::repeat_byte(2),
};
let signed = message.sign_with(&signer).await.expect("!sign_with");
signed.verify(signer.address()).expect("!verify");
};
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(t)
}
}

@ -1,11 +1,13 @@
use crate::SignedUpdate;
use crate::{OpticsError, SignedUpdate};
use ethers_core::types::{Address, H256, U256};
/// Waiting state
#[derive(Debug, Clone, Copy, Default)]
pub struct Waiting {
root: H256,
}
/// Pending update state
#[derive(Debug, Clone, Copy)]
pub struct Pending {
root: H256,
@ -13,9 +15,11 @@ pub struct Pending {
timeout: U256,
}
/// Failed state
#[derive(Debug, Clone, Copy)]
pub struct Failed {}
/// The Replica-chain Optics object
#[derive(Debug, Clone, Copy, Default)]
pub struct Replica<S> {
origin: u32,
@ -26,35 +30,37 @@ pub struct Replica<S> {
}
impl<S> Replica<S> {
/// SLIP-44 id of the Home chain
pub fn origin(&self) -> u32 {
self.origin
}
/// SLIP-44 id of this Replica chain
pub fn local(&self) -> u32 {
self.local
}
/// Ethereum address of the updater
pub fn updater(&self) -> Address {
self.updater
}
/// The number of seconds to wait before optimistically accepting an update
pub fn wait(&self) -> U256 {
self.optimistic_wait
}
/// Current state
pub fn state(&self) -> &S {
&self.state
}
fn check_sig(&self, update: &SignedUpdate) -> Result<(), ()> {
let signer = update.recover()?;
if signer == self.updater {
Ok(())
} else {
Err(())
}
fn check_sig(&self, update: &SignedUpdate) -> Result<(), OpticsError> {
update.verify(self.updater)
}
/// Notify Replica of double update, and set to failed
pub fn double_update(
self,
first: &SignedUpdate,
@ -75,6 +81,12 @@ impl<S> Replica<S> {
}
impl Replica<Waiting> {
/// Get the current root
pub fn root(&self) -> H256 {
self.state().root
}
/// Instantiate a new Replica.
pub fn init(origin: u32, local: u32, updater: Address, optimistic_wait: U256) -> Self {
Self {
origin,
@ -85,11 +97,12 @@ impl Replica<Waiting> {
}
}
/// Queue a pending update
pub fn update(
self,
update: &SignedUpdate,
now: impl FnOnce() -> U256,
) -> Result<Replica<Pending>, Replica<Waiting>> {
) -> Result<Replica<Pending>, Self> {
if self.check_sig(update).is_err() {
return Err(self);
}
@ -109,8 +122,15 @@ impl Replica<Waiting> {
}
impl Replica<Pending> {
/// Get the current root
pub fn root(&self) -> H256 {
self.state().root
}
/// Confirm a queued update after the timer has elapsed
pub fn confirm_update(self, now: impl FnOnce() -> U256) -> Result<Replica<Waiting>, Self> {
if self.state.timeout < now() {
if now() < self.state.timeout {
// timeout hasn't elapsed
return Err(self);
}

Loading…
Cancel
Save