diff --git a/Cargo.lock b/Cargo.lock index 8168979..cf9d5a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,6 +385,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-secret-scalar" +version = "0.0.2" +source = "git+https://github.com/w3f/ring-vrf.git#0fef8266d851932ad25d6b41bc4b34d834d1e11d" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", + "ark-transcript 0.0.2 (git+https://github.com/w3f/ring-vrf.git)", + "digest 0.10.7", + "getrandom_or_panic", + "zeroize", +] + [[package]] name = "ark-serialize" version = "0.4.2" @@ -446,6 +461,19 @@ dependencies = [ "sha3", ] +[[package]] +name = "ark-transcript" +version = "0.0.2" +source = "git+https://github.com/w3f/ring-vrf.git#0fef8266d851932ad25d6b41bc4b34d834d1e11d" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "digest 0.10.7", + "rand_core", + "sha3", +] + [[package]] name = "array-bytes" version = "6.2.3" @@ -648,7 +676,7 @@ dependencies = [ "ark-ff", "ark-serialize", "ark-std", - "dleq_vrf", + "dleq_vrf 0.0.2 (git+https://github.com/w3f/ring-vrf?rev=e9782f9)", "fflonk", "merlin", "rand_chacha", @@ -1284,17 +1312,6 @@ dependencies = [ "syn 2.0.79", ] -[[package]] -name = "derive-where" -version = "1.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "derive_more" version = "0.99.18" @@ -1337,7 +1354,7 @@ dependencies = [ "ark-ec", "ark-ff", "ark-scale", - "ark-secret-scalar", + "ark-secret-scalar 0.0.2 (git+https://github.com/w3f/ring-vrf?rev=e9782f9)", "ark-serialize", "ark-std", "ark-transcript 0.0.2 (git+https://github.com/w3f/ring-vrf?rev=e9782f9)", @@ -1345,6 +1362,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "dleq_vrf" +version = "0.0.2" +source = "git+https://github.com/w3f/ring-vrf.git#0fef8266d851932ad25d6b41bc4b34d834d1e11d" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-secret-scalar 0.0.2 (git+https://github.com/w3f/ring-vrf.git)", + "ark-serialize", + "ark-std", + "ark-transcript 0.0.2 (git+https://github.com/w3f/ring-vrf.git)", + "arrayvec 0.7.6", + "zeroize", +] + [[package]] name = "docify" version = "0.2.8" @@ -1507,33 +1539,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "etf-crypto-primitives" -version = "0.2.4" -source = "git+https://github.com/ideal-lab5/etf-sdk/?branch=dev#e2b3615e0936c2cdd1ab8673e15208bef880b040" -dependencies = [ - "aes-gcm", - "ark-bls12-377", - "ark-bls12-381", - "ark-ec", - "ark-ff", - "ark-poly", - "ark-serialize", - "ark-std", - "array-bytes", - "chacha20poly1305", - "generic-array", - "parity-scale-codec", - "rand_chacha", - "scale-info", - "serde", - "serde_cbor", - "serde_json", - "sha2 0.10.8", - "sha3", - "w3f-bls", -] - [[package]] name = "etf-crypto-primitives" version = "0.2.4" @@ -2008,15 +2013,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac 0.12.1", -] - [[package]] name = "hmac" version = "0.8.1" @@ -2525,16 +2521,19 @@ name = "murmur-core" version = "0.1.0" dependencies = [ "ark-bls12-377", + "ark-ec", + "ark-ff", "ark-serialize", "ark-std", + "ark-transcript 0.0.2 (git+https://github.com/w3f/ring-vrf.git)", "ckb-merkle-mountain-range", - "etf-crypto-primitives 0.2.4 (git+https://github.com/ideal-lab5/etf-sdk/?branch=dev)", - "hkdf", + "dleq_vrf 0.0.2 (git+https://github.com/w3f/ring-vrf.git)", "parity-scale-codec", "rand_chacha", "rand_core", "serde", "sha3", + "timelock", "totp-rs", "w3f-bls", "zeroize", @@ -2550,19 +2549,17 @@ dependencies = [ "ark-std", "ckb-merkle-mountain-range", "clap", - "hkdf", "murmur-core", "murmur-test-utils", "parity-scale-codec", "rand_chacha", + "rand_core", "serde", "serde_cbor", "sha3", "sp-consensus-beefy-etf", "sp-core", "subxt", - "subxt-core", - "subxt-metadata 0.37.0", "subxt-signer", "thiserror", "tokio", @@ -2574,7 +2571,10 @@ dependencies = [ name = "murmur-test-utils" version = "0.1.0" dependencies = [ + "ark-ec", "ark-serialize", + "ark-std", + "dleq_vrf 0.0.2 (git+https://github.com/w3f/ring-vrf.git)", "murmur-core", "rand_core", "w3f-bls", @@ -3409,19 +3409,7 @@ checksum = "662d10dcd57b1c2a3c41c9cf68f71fb09747ada1ea932ad961aca7e2ca28315f" dependencies = [ "parity-scale-codec", "scale-info", - "scale-type-resolver 0.1.1", - "serde", -] - -[[package]] -name = "scale-bits" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57b1e7f6b65ed1f04e79a85a57d755ad56d76fdf1e9bddcc9ae14f71fcdcf54" -dependencies = [ - "parity-scale-codec", - "scale-info", - "scale-type-resolver 0.2.0", + "scale-type-resolver", "serde", ] @@ -3434,24 +3422,9 @@ dependencies = [ "derive_more", "parity-scale-codec", "primitive-types", - "scale-bits 0.5.0", - "scale-decode-derive 0.11.1", - "scale-type-resolver 0.1.1", - "smallvec", -] - -[[package]] -name = "scale-decode" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98f3262c250d90e700bb802eb704e1f841e03331c2eb815e46516c4edbf5b27" -dependencies = [ - "derive_more", - "parity-scale-codec", - "primitive-types", - "scale-bits 0.6.0", - "scale-decode-derive 0.13.1", - "scale-type-resolver 0.2.0", + "scale-bits", + "scale-decode-derive", + "scale-type-resolver", "smallvec", ] @@ -3467,18 +3440,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "scale-decode-derive" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb22f574168103cdd3133b19281639ca65ad985e24612728f727339dcaf4021" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "scale-encode" version = "0.6.0" @@ -3488,24 +3449,9 @@ dependencies = [ "derive_more", "parity-scale-codec", "primitive-types", - "scale-bits 0.5.0", - "scale-encode-derive 0.6.0", - "scale-type-resolver 0.1.1", - "smallvec", -] - -[[package]] -name = "scale-encode" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba0b9c48dc0eb20c60b083c29447c0c4617cb7c4a4c9fef72aa5c5bc539e15e" -dependencies = [ - "derive_more", - "parity-scale-codec", - "primitive-types", - "scale-bits 0.6.0", - "scale-encode-derive 0.7.1", - "scale-type-resolver 0.2.0", + "scale-bits", + "scale-encode-derive", + "scale-type-resolver", "smallvec", ] @@ -3522,19 +3468,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "scale-encode-derive" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ab7e60e2d9c8d47105f44527b26f04418e5e624ffc034f6b4a86c0ba19c5bf" -dependencies = [ - "darling 0.14.4", - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "scale-info" version = "2.11.3" @@ -3571,16 +3504,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "scale-type-resolver" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0cded6518aa0bd6c1be2b88ac81bf7044992f0f154bfbabd5ad34f43512abcb" -dependencies = [ - "scale-info", - "smallvec", -] - [[package]] name = "scale-typegen" version = "0.2.1" @@ -3606,33 +3529,15 @@ dependencies = [ "either", "frame-metadata 15.1.0", "parity-scale-codec", - "scale-bits 0.5.0", - "scale-decode 0.11.1", - "scale-encode 0.6.0", + "scale-bits", + "scale-decode", + "scale-encode", "scale-info", - "scale-type-resolver 0.1.1", + "scale-type-resolver", "serde", "yap", ] -[[package]] -name = "scale-value" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6ab090d823e75cfdb258aad5fe92e13f2af7d04b43a55d607d25fcc38c811" -dependencies = [ - "derive_more", - "either", - "frame-metadata 15.1.0", - "parity-scale-codec", - "scale-bits 0.6.0", - "scale-decode 0.13.1", - "scale-encode 0.7.1", - "scale-info", - "scale-type-resolver 0.2.0", - "serde", -] - [[package]] name = "schannel" version = "0.1.24" @@ -4769,17 +4674,17 @@ dependencies = [ "jsonrpsee", "parity-scale-codec", "primitive-types", - "scale-bits 0.5.0", - "scale-decode 0.11.1", - "scale-encode 0.6.0", + "scale-bits", + "scale-decode", + "scale-encode", "scale-info", - "scale-value 0.14.1", + "scale-value", "serde", "serde_json", "sp-crypto-hashing 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "subxt-lightclient", "subxt-macro", - "subxt-metadata 0.35.3", + "subxt-metadata", "thiserror", "tokio-util", "tracing", @@ -4801,39 +4706,12 @@ dependencies = [ "quote", "scale-info", "scale-typegen", - "subxt-metadata 0.35.3", + "subxt-metadata", "syn 2.0.79", "thiserror", "tokio", ] -[[package]] -name = "subxt-core" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f41eb2e2eea6ed45649508cc735f92c27f1fcfb15229e75f8270ea73177345" -dependencies = [ - "base58", - "blake2", - "derive-where", - "frame-metadata 16.0.0", - "hashbrown 0.14.5", - "hex", - "impl-serde", - "parity-scale-codec", - "primitive-types", - "scale-bits 0.6.0", - "scale-decode 0.13.1", - "scale-encode 0.7.1", - "scale-info", - "scale-value 0.16.3", - "serde", - "serde_json", - "sp-crypto-hashing 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "subxt-metadata 0.37.0", - "tracing", -] - [[package]] name = "subxt-lightclient" version = "0.35.3" @@ -4880,19 +4758,6 @@ dependencies = [ "sp-crypto-hashing 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "subxt-metadata" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738be5890fdeff899bbffff4d9c0f244fe2a952fb861301b937e3aa40ebb55da" -dependencies = [ - "frame-metadata 16.0.0", - "hashbrown 0.14.5", - "parity-scale-codec", - "scale-info", - "sp-crypto-hashing 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "subxt-signer" version = "0.35.3" @@ -5014,6 +4879,34 @@ dependencies = [ "time-core", ] +[[package]] +name = "timelock" +version = "0.0.1" +source = "git+https://github.com/ideal-lab5/tle.git#6420b2231f0fb22d05fb0783d74d8394330d0ae1" +dependencies = [ + "aes-gcm", + "ark-bls12-377", + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "array-bytes", + "chacha20poly1305", + "generic-array", + "parity-scale-codec", + "rand_chacha", + "rand_core", + "scale-info", + "serde", + "serde_cbor", + "serde_json", + "sha2 0.10.8", + "sha3", + "w3f-bls", +] + [[package]] name = "tinyvec" version = "1.8.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index 462aa80..6eae973 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -2,7 +2,7 @@ name = "murmur-core" version = "0.1.0" edition = "2021" -description = "Murmur core" +description = "The core implementation of the Murmur protocol" documentation = "https://docs.rs/murmur-core" readme = "README.md" keywords = ["crypto", "wallet", "keyless"] @@ -21,20 +21,23 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] totp-rs = { version = "5.5.1", default-features = false, optional = true } codec = { package = "parity-scale-codec", version = "3.6.12", features = ["derive"], default-features = false } -etf-crypto-primitives = { git = "https://github.com/ideal-lab5/etf-sdk/", branch = "dev", default-features = false} +timelock = { git = "https://github.com/ideal-lab5/timelock.git", default-features = false } ckb-merkle-mountain-range = { version = "0.5.2", default-features = false } sha3 = { version = "0.10.8", default-features = false } -serde = { version = "1.0.188", features = ["alloc", "derive"], default-features = false} +serde = { version = "1.0.188", features = ["alloc", "derive"], default-features = false } ark-bls12-377 = { version = "0.4.0", default-features = false } ark-std = { version = "0.4.0", default-features = false } ark-serialize = { version = "0.4.0", default-features = false } -w3f-bls = { version = "0.1.3", default-features = false } +w3f-bls = { version = "0.1.4", default-features = false } zeroize = { version = "1.8.1", default-features = false } -rand_chacha = { version = "0.3.1" } +ark-ec = { version = "0.4", default-features = false } +ark-ff = { version = "0.4", default-features = false } +dleq_vrf = { git = "https://github.com/w3f/ring-vrf.git", default-features = false } +ark-transcript = { git = "https://github.com/w3f/ring-vrf.git", default-features = false } [dev-dependencies] -rand_core = { version = "0.6.4", features = ["getrandom"], default-features = false } -hkdf = "0.12.4" +rand_chacha = { version = "0.3.1" } +rand_core = { version = "0.6.4" } [features] default = ["client"] @@ -42,15 +45,20 @@ std = [ "ark-std/std", "ark-serialize/std", "ark-bls12-377/std", + "ark-ec/std", + "ark-ff/std", + "ark-transcript/std", + "dleq_vrf/std", "w3f-bls/std", "serde/std", + "ckb-merkle-mountain-range/std", "codec/std", - "etf-crypto-primitives/std", + "timelock/std", "sha3/std", - "ckb-merkle-mountain-range/std", "zeroize/std", ] no_std = [] client = [ "totp-rs", + "dleq_vrf/getrandom" ] diff --git a/core/README.md b/core/README.md index b058f88..f8e7cad 100644 --- a/core/README.md +++ b/core/README.md @@ -1,6 +1,104 @@ # Murmur Core -This library contains the core implementation of the murmur protocol. This implementation can support both BLS12-377 and BLS12-381, but is left curve-agnostic, only expecting that the beacon is produced by an ETF-PFG instance. +This library contains the core implementation of the Murmur protocol. This implementation can support both BLS12-377 and BLS12-381, but is left curve-agnostic. This crate can support the randomness beacon produced by the [Ideal Network](https://idealabs.network) as well as [Drand](https://drand.love)'s Quicknet. In general, this library is intended to work with a blockchain whose runtime includes the corresponding [Murmur Pallet](https://github.com/ideal-lab5/idn-sdk/tree/main/pallets/murmur). More specifcially, it is intended to run against the [Ideal Network](https://idealabs.network). For examples of usage against the Ideal Network, refer to the [CLI](../lib/src/bin/murmur/main.rs). + +## Usage + +### Create, Update, Execute + +To create and update murmur wallets, you must define an `IdentityBuilder`. The [BasicIdentityBuilder](../lib/src/lib.rs) struct implements allows for the construction of valid identities on the Ideal Network. In the future, we will add support for Drand's Quicknet as well. + +#### Create a Murmur Store + +``` rust +use ark_serialize::CanonicalDeserialize; +use ark_std::rand::SeedableRng; +use rand_chacha::ChaCha20Rng; +use rand_core::OsRng; +use murmur_core::{ + murmur::{EngineTinyBLS377, Error, MurmurStore}, +}; +use w3f_bls::{DoublePublicKeyScheme, KeypairVT, TinyBLS377}; + +// This simulates the production of a randomness beacon public key +// In practice, this would be fetched from the beacon (e.g. as a hex string) and must be deseraialized +let keypair = KeypairVT::::generate(&mut rng); +let double_public: DoublePublicKey = + DoublePublicKey(keypair.into_public_key_in_signature_group().0, keypair.public.0); + +// The 'lifetime' of the Murmur wallet for the given session +// This corresponds to future rounds of the randomness beacon +// for which timelocked commitments can be made +let schedule = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + +// This is your 'secret' seed, a short password used while constructing OTP codes +let seed = vec![1, 2, 3]; + +// The nonce functions similarly to nonce's for standard accounts, except instead of updating on a +// "per transaction" basis, it only updates on a "per session" basis +// +let nonce = 0; + +let murmur_store = MurmurStore::::new::< + BasicIdentityBuilder, + OsRng, + ChaCha20Rng, +>(seed.clone(), schedule.to_vec(), nonce, double_public, &mut rng) +.unwrap(); +``` +#### Update a Murmur Store + +Updating a Murmur store is done by calling the same `new` function as above and incrementing the previous nonce by 1. The data any single MMR can contain is finite, so when a Murmur wallet is created it can only be functional for a finite number of blocks (the block schedule). In this sense, Murmur is a "session-based" wallet. To ensure wallet lifetimes can be extended, Murmur wallets can be updated by generating a new Murmur store and submitting the result to a system that implements a verifier (see below). More specifically, when a Murmur store is created a DLEQ proof is generated (Discrete Log Equivalence Proof - a type of zkp) and attached to the store. This proof allows the Murmur store creator to convince a verifier that it knows the secret input (seed) without exposing it. + +``` rust +// Compute the next nonce +let nonce = 1; +// Construct a new murmur store +let murmur_store = MurmurStore::::new::< + BasicIdentityBuilder, + OsRng, + ChaCha20Rng, +>(seed.clone(), block_schedule.to_vec(), nonce, double_public, &mut rng) +.unwrap(); +``` + +#### Prepare Execution Parameters + +This library allows for arbitary payloads to be strictly associated with the reveal of a future OTP code. That is, given an OTP created for a future round r, it allows for the creation of a commitment to that data that cannot be verified until the future round `r` happens and the beacon outputs a signature allowing for decryption of the OTP code. We can consider this a form of `timelocked` commitments. + +``` rust +// The round when the commitment will be verifiable +let when = 156921; +// Generates a Merkle proof, the hash Sha256(OTP || aux_data), +// the timelocked OTP code ciphertext and its position in the MMR. +// This data is used later on by the verifier to verify the commitment. +let (proof, commitment, ciphertext, pos) = + murmur_store.execute(seed.clone(), when, aux_data.clone()).unwrap(); +``` + +### Verification + +The verifier module provides functionality to verify Murmur store data and timelocked commitments. In general, this would be executed by whichever actor in the system implementing Murmur has agency to directly manipulate the system or otherwise proxy user input to meaningful actions. This is intended to be run in trustless systems, specifically in the context of a blockchain runtime. + +#### Verify Updates + +When a Murmur store is updated, a DLEQ proof is generated and attached to the store. This is intended to allow the creator of the Murmur store to prove that they know the secret inputs without revealing them, allowing them to update the Murmur store whenever they need to. The `verify_update` function verifies the DLEQ proof. If it is true, then the prover (Murmur store creator) has convinced the verifier (e.g. Blockchain Runtime) that it was generated with the same seed. + +``` rust +verifier::verify_update::(proof, public_key, nonce).unwrap() +``` + +#### Verify Execution Parameters + +This function allows for "timelocked" commitments to be verified. The OTP input should be the timelock decrypted ciphertext. More specifically, it: +1) verifies a Merkle proof to prove that the ciphertext is indeed at the given position in the MMR defined by the given root. +2) reconstructs the commitment and compares it against the given one + +``` rust +verifier::verify_execute( + root, proof, commitment, ciphertext, OTP, &aux_data, pos, +); +``` ## Build @@ -33,8 +131,6 @@ cargo test --features "client" ## Future Work/Notes - **OTPAuth Feature**: There is an 'otpauth' feature that can be enabled on the totp lib. It allows for the inclusion of an issuer and account_name. We can investigate usage of this in the future. [TOTP Library Reference](https://github.com/constantoine/totp-rs/blob/da78569b0c233adbce126dbe0c35452340fd3929/src/lib.rs#L160) -- **Wallet Update logic**: Each murmur wallet is ephemeral, since any MMR must be limited in size. We can use a zkp to prove knowledge of the seed in order to allow the wallet owner to update the wallet by providing a new MMR root. - ## Contributing Contributions are welcome! Please open an issue or submit a pull request. diff --git a/core/src/murmur.rs b/core/src/murmur.rs index abe1cdb..4069111 100644 --- a/core/src/murmur.rs +++ b/core/src/murmur.rs @@ -14,31 +14,47 @@ * limitations under the License. */ -//! The murmur protocol implementation +//! The Murmur protocol implementation use alloc::{collections::BTreeMap, vec, vec::Vec}; #[cfg(feature = "client")] use crate::otp::BOTPGenerator; use ark_std::rand::{CryptoRng, Rng, SeedableRng}; -use rand_chacha::ChaCha20Rng; #[cfg(feature = "client")] use zeroize::Zeroize; #[cfg(feature = "client")] -use ark_serialize::CanonicalSerialize; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use crate::types::*; +use ark_ec::CurveGroup; +use ark_transcript::{digest::Update, Transcript}; use ckb_merkle_mountain_range::{ helper::leaf_index_to_pos, util::{MemMMR, MemStore}, MerkleProof, }; use codec::{Decode, Encode}; -use etf_crypto_primitives::{encryption::tlock::*, ibe::fullident::Identity}; +use core::marker::PhantomData; +use dleq_vrf::{EcVrfVerifier, PublicKey, SecretKey}; use sha3::Digest; -use w3f_bls::{DoublePublicKey, EngineBLS}; +use timelock::{ + ibe::fullident::Identity, + stream_ciphers::{AESGCMStreamCipherProvider, AESOutput, StreamCipherProvider}, + tlock::*, +}; +use w3f_bls::DoublePublicKey; + +/// The base Murmur protocol label +pub const MURMUR_PROTO: &[u8] = b"Murmur://"; +/// The Murmur protocol label used for transcripts used while generation/encryption OTP codes +pub const MURMUR_PROTO_OTP: &[u8] = b"MurmurOTP://"; +/// The Murmur protocol label used for generating DLEQ proofs +pub const MURMUR_PROTO_VRF: &[u8] = b"MurmurVRF://"; +/// The size of a 32-bit buffer +pub const ALLOCATED_BUFFER_BYTES: usize = 32; /// Error types for murmur wallet usage #[derive(Debug, PartialEq)] @@ -47,6 +63,7 @@ pub enum Error { ExecuteError, /// An error occurred when creating a murmur wallet MMRError, + /// Some data in the murmur store is corrupted InconsistentStore, /// No leaf could be identified in the MMR at the specified position NoLeafFound, @@ -54,76 +71,170 @@ pub enum Error { NoCiphertextFound, /// There was an error when executing timelock encryption (is the ciphertext too large?) TlockFailed, + /// There was an error when executing AES GCM decryption + AesDecryptFailed, /// The buffer does not have enough space allocated InvalidBufferSize, /// The seed was invalid InvalidSeed, /// The public key was invalid (could not be decoded) InvalidPubkey, - /// The key derivation failed - KeyDerivationFailed, + /// The ciphertext could not be deserialized to a TLECiphertext + CiphertextDeserializationFailed, + /// The input could not be deserialized, is it right sized? + DeserializationFailure, +} + +pub trait ProtocolEngine { + type Engine: EngineBLS; + fn protocol_id() -> ProtocolId; +} + +/// The supported protocols +#[derive(Clone, serde::Serialize, serde::Deserialize, Encode, Decode)] +pub enum ProtocolId { + /// A curve config with small signatures and large pubkyes + /// SignatureGroup = G1 (48 bytes), PublicKeyGroup = G2 (96 bytes) + TinyBLS377, +} + +/// Use the TinyBLS377 engine +pub struct EngineTinyBLS377; +impl ProtocolEngine for EngineTinyBLS377 { + type Engine = w3f_bls::TinyBLS377; + fn protocol_id() -> ProtocolId { + ProtocolId::TinyBLS377 + } } /// The murmur store contains minimal data required to use a murmur wallet #[cfg(feature = "client")] #[derive(Clone, serde::Serialize, serde::Deserialize, Encode, Decode)] -pub struct MurmurStore { +pub struct MurmurStore { + /// The nonce of this murmur store + pub nonce: u64, /// A map of block numbers to leaf positions in the mmr pub metadata: BTreeMap, /// The root of the mmr pub root: Leaf, + /// A serialized VRF proof + pub proof: Vec, + /// A serialized public key associated with the VRF proof + pub public_key: Vec, + /// The id of the protocol used to generate the murmur store + pub protocol_id: ProtocolId, + /// The protocol engine used + _phantom: PhantomData

, } #[cfg(feature = "client")] -impl MurmurStore { - /// Create a new Murmur store +impl MurmurStore

{ + /// Create a new Murmur store. + /// + /// This function allows for two separate RNGs to be specified. In general, the first RNG type R + /// should be created externally from this function and passed as an argument. + /// In practice, this should probably be the OsRng or something similar. + /// The second type of RNG, S, must be seedable from [u8;32]. This RNG is instantiated within + /// the function, where we seed a new RNG each time we encrypt a new OTP code using timelock + /// encryption. /// /// * `seed`: An any-length seed (i.e. password) /// * `block_schedule`: The blocks for which OTP codes will be generated - /// * `ephemeral_msk`: Any 32 bytes + /// * `nonce`: A value representing the 'number of times' the Murmur wallet has been created or updated. + /// Should be monotonically increasing with each subsequent call. /// * `round_public_key`: The IDN beacon's public key - pub fn new>( + /// * `rng`: An instance of an CPRNG of type `R` + + pub fn new, R, S>( mut seed: Vec, block_schedule: Vec, - mut ephemeral_msk: [u8; 32], - round_public_key: DoublePublicKey, - ) -> Result { - let totp = build_generator(seed.clone())?; - seed.zeroize(); - let mut metadata = BTreeMap::new(); + nonce: u64, + round_public_key: DoublePublicKey, + rng: &mut R, + ) -> Result + where + R: Rng + CryptoRng + Sized, + S: Rng + CryptoRng + SeedableRng + Sized, + { + let mut transcript = Transcript::new_labeled(MURMUR_PROTO); + transcript.write_bytes(&seed); + + let mut challenge: [u8; 32] = transcript.challenge(b"challenge").read_byte_array(); + + let mut secret_key = SecretKey::< + <::SignatureGroup as CurveGroup>::Affine, + >::from_seed(&challenge); + + let pubkey = secret_key.as_publickey(); + let mut pubkey_bytes = Vec::new(); + pubkey.serialize_compressed(&mut pubkey_bytes).unwrap(); + + let mut vrf_transcript = Transcript::new_labeled(MURMUR_PROTO_VRF); + // generate a transcript such that the proof verification requires the nonce + vrf_transcript.write_bytes(&nonce.to_be_bytes()); + let signature = secret_key.sign_thin_vrf_detached(vrf_transcript.clone(), &[]); + + let mut sig_bytes = Vec::new(); + signature.serialize_compressed(&mut sig_bytes).unwrap(); + + let mut witness: [u8; 32] = transcript.clone().witness(rng).read_byte_array(); + let totp = BOTPGenerator::new(witness.to_vec()).map_err(|_| Error::InvalidSeed)?; + + // drop secret data + witness.zeroize(); + challenge.zeroize(); + secret_key.zeroize(); + let mut metadata = BTreeMap::new(); let store = MemStore::default(); let mut mmr = MemMMR::<_, MergeLeaves>::new(0, store); - for i in &block_schedule { - let otp_code = totp.generate(*i); - let identity = I::build_identity(*i); + let mut transcript_otp = Transcript::new_labeled(MURMUR_PROTO_OTP); + transcript_otp.write_bytes(&seed); + transcript_otp.write_bytes(&nonce.to_be_bytes()); - // we need to seed a new rng here - let mut hasher = sha3::Sha3_256::default(); - hasher.update(ephemeral_msk.to_vec().clone()); - hasher.update(otp_code.as_bytes().to_vec().clone()); - let hash = hasher.finalize(); + seed.zeroize(); + + for &i in &block_schedule { + let mut otp_code = totp.generate(i as u64); + let identity = I::build_identity(i); + let mut ephemeral_msk: [u8; 32] = transcript_otp + .clone() + .fork(b"otp-leaf-gen") + .chain(&i.to_be_bytes()) + .challenge(b"ephemeral_msk") + .read_byte_array(); - let ephem_rng = ChaCha20Rng::from_seed(hash.into()); - let ct_bytes = timelock_encrypt::( + let ephem_rng = S::from_seed(ephemeral_msk); + + let ct_bytes = timelock_encrypt::( identity, round_public_key.1, ephemeral_msk, otp_code.as_bytes(), ephem_rng, )?; + ephemeral_msk.zeroize(); + otp_code.zeroize(); + let leaf = Leaf(ct_bytes.clone()); - // Q: How can I test this? + // Q: How can I test this line? // https://github.com/nervosnetwork/merkle-mountain-range/blob/9e77d3ef81ddfdd9b7dd9583762582e859849dde/src/mmr.rs#L60 let _pos = mmr.push(leaf).map_err(|_| Error::InconsistentStore)?; - metadata.insert(*i, ct_bytes); + metadata.insert(i, ct_bytes); } - ephemeral_msk.zeroize(); let root = mmr.get_root().map_err(|_| Error::InconsistentStore)?; - Ok(MurmurStore { metadata, root }) + Ok(MurmurStore { + nonce, + metadata, + root, + proof: sig_bytes, + public_key: pubkey_bytes, + protocol_id: P::protocol_id(), + _phantom: PhantomData, + }) } /// Build data required (proof and commitment) to execute a valid call from a murmur wallet @@ -138,10 +249,12 @@ impl MurmurStore { call_data: Vec, ) -> Result<(MerkleProof, Vec, Ciphertext, u64), Error> { if let Some(ciphertext) = self.metadata.get(&when) { - let commitment = MurmurStore::commit(seed.clone(), when, &call_data.clone())?; + let commitment = + self.commit(seed.clone(), ciphertext.clone(), &call_data.clone(), when)?; + seed.zeroize(); - let idx = get_key_index(&self.metadata, &when) - .expect("The key must exist within the metadata."); + + let idx = self.metadata.keys().position(|k| k == &when).expect("The leaf exists"); let pos = leaf_index_to_pos(idx as u64); let mmr = self.to_mmr()?; let proof = mmr.gen_proof(vec![pos]).map_err(|_| Error::InconsistentStore)?; @@ -156,14 +269,33 @@ impl MurmurStore { /// * `seed`: The seed used to generated the MMR /// * `when`: The block number when the commitment is verifiable /// * `data`: The data to commit to - fn commit(mut seed: Vec, when: BlockNumber, data: &[u8]) -> Result, Error> { - let botp = build_generator(seed.clone())?; + fn commit( + &self, + mut seed: Vec, + ciphertext: Vec, + data: &[u8], + when: BlockNumber, + ) -> Result, Error> { + let mut transcript = Transcript::new_labeled(MURMUR_PROTO_OTP); + transcript.write_bytes(&seed); + transcript.write_bytes(&self.nonce.to_be_bytes()); + + let mut ephemeral_msk: [u8; 32] = transcript + .clone() + .fork(b"otp-leaf-gen") + .chain(&when.to_be_bytes()) + .challenge(b"ephemeral_msk") + .read_byte_array(); + seed.zeroize(); - let otp_code = botp.generate(when); + let mut otp_code = aes_decrypt::(ciphertext, ephemeral_msk)?; + ephemeral_msk.zeroize(); let mut hasher = sha3::Sha3_256::default(); - hasher.update(otp_code.as_bytes()); - hasher.update(data); + Digest::update(&mut hasher, &otp_code); + Digest::update(&mut hasher, data); + + otp_code.zeroize(); Ok(hasher.finalize().to_vec()) } @@ -182,8 +314,48 @@ impl MurmurStore { } #[cfg(feature = "client")] -/// Timelock encryption helper function -pub fn timelock_encrypt( +impl MurmurStore

{ + // Serialize MurmurStore without the PhantomData + pub fn encode(&self) -> Vec { + (self.nonce, &self.metadata, &self.root, &self.proof, &self.public_key, &self.protocol_id) + .encode() + } + + pub fn decode(data: Vec) -> Result, Error> { + // Decode fields from `data` without `_phantom` + let (nonce, metadata, root, proof, public_key, protocol_id): ( + u64, + BTreeMap, + Leaf, + Vec, + Vec, + ProtocolId, + ) = Decode::decode(&mut &data[..]).unwrap(); + + match protocol_id { + ProtocolId::TinyBLS377 => Ok(MurmurStore { + nonce, + metadata, + root, + proof, + public_key, + protocol_id, + _phantom: PhantomData, + }), + } + } +} + +/// A helper function to perform timelock encryption. +/// NOTE: this function is opionated to use AES_GCM +/// +/// * `identity`: The identity to encrypt for +/// * `pk`: The public key of the randomness beacon +/// * `ephemeral_msk`: A randomly sampled 32-byte secret key +/// * `message`: The message to be timelock encrypted +/// * `rng`: A CSPRNG +#[cfg(feature = "client")] +fn timelock_encrypt( identity: Identity, pk: E::PublicKeyGroup, ephemeral_msk: [u8; 32], @@ -191,66 +363,146 @@ pub fn timelock_encrypt( rng: R, ) -> Result, Error> { let ciphertext = - tle::(pk, ephemeral_msk, message, identity, rng).map_err(|_| Error::TlockFailed)?; + tle::(pk, ephemeral_msk, message, identity, rng) + .map_err(|_| Error::TlockFailed)?; + let mut ct_bytes = Vec::new(); ciphertext .serialize_compressed(&mut ct_bytes) .map_err(|_| Error::InvalidBufferSize)?; + Ok(ct_bytes) } -/// Build a block-otp generator from the seed +/// Use a secret key to decrypt the ciphertext before the beacon outputs +/// This uses AES_GCM decryption. It returns the plaintext if decryption is successful, else an +/// error +/// +/// * `ciphertext_bytes`: The ciphertext to decrypt +/// * `secret`: A decryption key (32 bytes) #[cfg(feature = "client")] -fn build_generator(mut seed: Vec) -> Result { - let mut hasher = sha3::Sha3_256::default(); - hasher.update(&seed); - seed.zeroize(); - let hash = hasher.finalize(); - BOTPGenerator::new(hash.to_vec()).map_err(|_| Error::InvalidSeed) +fn aes_decrypt( + ciphertext_bytes: Vec, + secret: [u8; 32], +) -> Result, Error> { + let ciphertext: TLECiphertext = + TLECiphertext::deserialize_compressed(&mut &ciphertext_bytes[..]) + .map_err(|_| Error::CiphertextDeserializationFailed)?; + + let aes_ct = AESOutput::deserialize_compressed(&mut &ciphertext.body[..]) + .map_err(|_| Error::DeserializationFailure)?; + + let plaintext = + AESGCMStreamCipherProvider::decrypt(aes_ct, secret).map_err(|_| Error::AesDecryptFailed)?; + + Ok(plaintext) } -// verify the correctness of execution parameters -pub fn verify( - root: Leaf, - proof: MerkleProof, - hash: Vec, - ciphertext: Vec, - otp: Vec, - aux_data: Vec, - pos: u64, -) -> bool { - let mut validity = proof.verify(root, vec![(pos, Leaf(ciphertext))]).unwrap_or(false); - - if validity { - let mut hasher = sha3::Sha3_256::default(); - hasher.update(otp); - hasher.update(aux_data); - let expected_hash = hasher.finalize(); +/// Functions for verifying execution and update requests +/// These functions would typically be called by an untrusted verifier (e.g. a blockchain runtime) +pub mod verifier { + use super::*; + use ark_serialize::CanonicalDeserialize; + use dleq_vrf::ThinVrfProof; + + #[derive(Debug, PartialEq)] + pub enum VerificationError { + /// The Schnorr proof could not be deserialized + UnserializableProof, + /// The public key could not be deserialized + UnserializablePubkey, + } + + /// Verify the correctness of execution parameters by checking that the Merkle proof, `Proof`, + /// and hash `H` are valid. The function outputs true if both conditions are true: + /// + /// 1. Proof.Verify(root, [(pos, Leaf(ciphertext))]) + /// 2. H == Sha256(otp || aux_data) + //// + /// It outputs false otherwise. + /// + /// * `root`: The root of the MMR + /// * `proof`: The Merkle proof to verify + /// * `hash`: A (potential) commitment to the OTP and aux_data + /// * `ciphertext`: A timelocked ciphertext + /// * `otp`: The OTP + /// * `aux_data`: The expected aux data used to generate the commitment + /// * `pos`: The position of the Ciphertext within the MMR + pub fn verify_execute( + root: Leaf, + proof: MerkleProof, + hash: Vec, + ciphertext: Vec, + otp: &[u8], + aux_data: &[u8], + pos: u64, + ) -> bool { + let mut validity = proof.verify(root, vec![(pos, Leaf(ciphertext))]).unwrap_or(false); + + if validity { + let mut hasher = sha3::Sha3_256::default(); + Digest::update(&mut hasher, otp); + + Digest::update(&mut hasher, aux_data); + let expected_hash = hasher.finalize().to_vec(); + validity = validity && expected_hash == hash; + } - validity = validity && expected_hash.to_vec() == hash; + validity } - validity -} + /// Verifies a Schnorr proof + /// This is used to ensure that subsequent calls to the 'new' function are called with the same + /// seed where the output is non-deterministic + /// + /// * `serialized_proof`: The serialized proof + /// * `serialized_pubkey`: The serialized public key + /// * `nonce`: A nonce value + pub fn verify_update( + serialized_proof: Vec, + serialized_pubkey: Vec, + nonce: u64, + ) -> Result { + // build transcript + let mut transcript = Transcript::new_labeled(MURMUR_PROTO_VRF); + transcript.write_bytes(&nonce.to_be_bytes()); + + // deserialize proof and pubkey + let proof = + ThinVrfProof::<::Affine>::deserialize_compressed( + &mut &serialized_proof[..], + ) + .map_err(|_| VerificationError::UnserializableProof)?; + + let pk = PublicKey::<::Affine>::deserialize_compressed( + &mut &serialized_pubkey[..], + ) + .map_err(|_| VerificationError::UnserializablePubkey)?; -/// get the index of a key in a BTreeMap -pub fn get_key_index(b: &BTreeMap, key: &K) -> Option { - b.keys().position(|k| k == key) + Ok(pk.vrf_verify_detached(transcript, &[], &proof).is_ok()) + } } #[cfg(test)] mod tests { use super::*; + use ark_serialize::CanonicalDeserialize; use ark_std::rand::SeedableRng; use rand_chacha::ChaCha20Rng; use rand_core::OsRng; use w3f_bls::{DoublePublicKeyScheme, TinyBLS377}; + pub const BLOCK_SCHEDULE: &[BlockNumber] = + &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + + pub const WHEN: BlockNumber = 10; + pub const OTP: &[u8] = &[52, 54, 49, 53, 51, 54]; + pub struct DummyIdBuilder; impl IdentityBuilder for DummyIdBuilder { fn build_identity(at: BlockNumber) -> Identity { - Identity::new(&[at as u8]) + Identity::new(b"", vec![vec![at as u8]]) } } @@ -263,101 +515,65 @@ mod tests { DoublePublicKey(keypair.into_public_key_in_signature_group().0, keypair.public.0); let seed = vec![1, 2, 3]; - let schedule = vec![1, 2, 3]; - let hk = hkdf::Hkdf::::new(None, &seed); - let mut ephem_msk = [0u8; 32]; - hk.expand(b"ephemeral key", &mut ephem_msk).unwrap(); - - let murmur_store = MurmurStore::new::( - seed.clone(), - schedule.clone(), - ephem_msk, - double_public, - ) + let murmur_store = MurmurStore::::new::< + DummyIdBuilder, + ChaCha20Rng, + ChaCha20Rng, + >(seed.clone(), BLOCK_SCHEDULE.to_vec(), 0, double_public, &mut rng) .unwrap(); - assert!(murmur_store.metadata.keys().len() == 3); + assert!(murmur_store.metadata.keys().len() == BLOCK_SCHEDULE.len()); + assert!(murmur_store.root.0.len() == 32); + assert!(murmur_store.proof.len() == 80); + assert!(murmur_store.public_key.len() == 48); } #[cfg(feature = "client")] #[test] pub fn it_can_generate_valid_output_and_verify_it() { - let keypair = w3f_bls::KeypairVT::::generate(&mut OsRng); + let mut rng = ChaCha20Rng::seed_from_u64(0); + let keypair = w3f_bls::KeypairVT::::generate(&mut rng); let double_public: DoublePublicKey = DoublePublicKey(keypair.into_public_key_in_signature_group().0, keypair.public.0); let seed = vec![1, 2, 3]; - let schedule = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; - let aux_data = vec![2, 3, 4, 5]; - let hk = hkdf::Hkdf::::new(None, &seed); - let mut ephem_msk = [0u8; 32]; - hk.expand(b"ephemeral key", &mut ephem_msk).unwrap(); - - let murmur_store = MurmurStore::new::( - seed.clone(), - schedule.clone(), - ephem_msk, - double_public, - ) + let murmur_store = MurmurStore::::new::< + DummyIdBuilder, + ChaCha20Rng, + ChaCha20Rng, + >(seed.clone(), BLOCK_SCHEDULE.to_vec(), 0, double_public, &mut rng) .unwrap(); - // the block number when this would execute - let when = 1; - let root = murmur_store.root.clone(); let (proof, commitment, ciphertext, pos) = - murmur_store.execute(seed.clone(), when, aux_data.clone()).unwrap(); + murmur_store.execute(seed.clone(), WHEN, aux_data.clone()).unwrap(); - // sanity check - assert!(proof.verify(root.clone(), vec![(pos, Leaf(ciphertext.clone()))]).unwrap()); - - // in practice, the otp code would be timelock decrypted - // but for testing purposes, we will just calculate the expected one now - let botp = build_generator(seed.clone()).unwrap(); - let otp_code = botp.generate(when); - - assert!(verify( - root, - proof, - commitment, - ciphertext, - otp_code.as_bytes().to_vec(), - aux_data, - pos, - )); + assert!(verifier::verify_execute(root, proof, commitment, ciphertext, OTP, &aux_data, pos)); } #[cfg(feature = "client")] #[test] pub fn it_fails_to_generate_execute_output_when_ciphertext_dne() { - let keypair = w3f_bls::KeypairVT::::generate(&mut OsRng); + let mut rng = ChaCha20Rng::seed_from_u64(0); + let keypair = w3f_bls::KeypairVT::::generate(&mut rng); let double_public: DoublePublicKey = DoublePublicKey(keypair.into_public_key_in_signature_group().0, keypair.public.0); let seed = vec![1, 2, 3]; - let schedule = vec![1, 2, 3, 4, 5]; - - let aux_data = vec![2, 3, 4, 5]; - - let hk = hkdf::Hkdf::::new(None, &seed); - let mut ephem_msk = [0u8; 32]; - hk.expand(b"ephemeral key", &mut ephem_msk).unwrap(); - let murmur_store = MurmurStore::new::( - seed.clone(), - schedule.clone(), - ephem_msk, - double_public, - ) + let murmur_store = MurmurStore::::new::< + DummyIdBuilder, + ChaCha20Rng, + ChaCha20Rng, + >(seed.clone(), BLOCK_SCHEDULE.to_vec(), 0, double_public, &mut rng) .unwrap(); - // the block number when this would execute - let when = 1000; + let aux_data = vec![2, 3, 4, 5]; - match murmur_store.execute(seed.clone(), when, aux_data.clone()) { + match murmur_store.execute(seed.clone(), 10000, aux_data.clone()) { Ok(_) => panic!("There should be an error"), Err(e) => assert_eq!(e, Error::NoCiphertextFound), } @@ -366,103 +582,140 @@ mod tests { #[cfg(feature = "client")] #[test] pub fn it_fails_on_verify_bad_aux_data() { - let keypair = w3f_bls::KeypairVT::::generate(&mut OsRng); + let mut rng = ChaCha20Rng::seed_from_u64(0); + let keypair = w3f_bls::KeypairVT::::generate(&mut rng); let double_public: DoublePublicKey = DoublePublicKey(keypair.into_public_key_in_signature_group().0, keypair.public.0); let seed = vec![1, 2, 3]; - let schedule = vec![1, 2, 3]; - let aux_data = vec![2, 3, 4, 5]; - let hk = hkdf::Hkdf::::new(None, &seed); - let mut ephem_msk = [0u8; 32]; - hk.expand(b"ephemeral key", &mut ephem_msk).unwrap(); - - let murmur_store = MurmurStore::new::( - seed.clone(), - schedule.clone(), - ephem_msk, - double_public, - ) + let murmur_store = MurmurStore::::new::< + DummyIdBuilder, + ChaCha20Rng, + ChaCha20Rng, + >(seed.clone(), BLOCK_SCHEDULE.to_vec(), 0, double_public, &mut rng) .unwrap(); - // the block number when this would execute - let when = 1; let root = murmur_store.root.clone(); let (proof, commitment, ciphertext, pos) = - murmur_store.execute(seed.clone(), when, aux_data.clone()).unwrap(); - - // in practice, the otp code would be timelock decrypted - // but for testing purposes, we will just calculate the expected one now - let botp = build_generator(seed.clone()).unwrap(); - let otp_code = botp.generate(when); + murmur_store.execute(seed.clone(), 1, aux_data.clone()).unwrap(); let bad_aux = vec![2, 3, 13, 3]; - assert!(!verify( - root, - proof, - commitment, - ciphertext, - otp_code.as_bytes().to_vec(), - bad_aux, - pos, - )); + assert!( + !verifier::verify_execute(root, proof, commitment, ciphertext, OTP, &bad_aux, pos,) + ); } #[test] pub fn it_fails_on_verify_bad_proof() { - let keypair = w3f_bls::KeypairVT::::generate(&mut OsRng); + let mut rng = ChaCha20Rng::seed_from_u64(0); + let keypair = w3f_bls::KeypairVT::::generate(&mut rng); let double_public: DoublePublicKey = DoublePublicKey(keypair.into_public_key_in_signature_group().0, keypair.public.0); - let other_double_public: DoublePublicKey = - DoublePublicKey(keypair.into_public_key_in_signature_group().0, keypair.public.0); + let other_keypair = w3f_bls::KeypairVT::::generate(&mut rng); + let other_double_public: DoublePublicKey = DoublePublicKey( + other_keypair.into_public_key_in_signature_group().0, + other_keypair.public.0, + ); let seed = vec![1, 2, 3]; - let schedule = vec![1, 2, 3]; - let other_schedule = vec![1, 2, 3, 4, 5]; - - let aux_data = vec![2, 3, 4, 5]; - - let hk = hkdf::Hkdf::::new(None, &seed); - let mut ephem_msk = [0u8; 32]; - hk.expand(b"ephemeral key", &mut ephem_msk).unwrap(); + let other_seed = vec![2, 3, 4]; - let murmur_store = MurmurStore::new::( - seed.clone(), - schedule.clone(), - ephem_msk, - double_public, - ) + let murmur_store = MurmurStore::::new::< + DummyIdBuilder, + ChaCha20Rng, + ChaCha20Rng, + >(seed.clone(), BLOCK_SCHEDULE.to_vec(), 0, double_public, &mut rng) .unwrap(); - let other_murmur_store = MurmurStore::new::( - seed.clone(), - other_schedule.clone(), - ephem_msk, - other_double_public, - ) - .unwrap(); + let other_murmur_store = + MurmurStore::::new::( + other_seed.clone(), + BLOCK_SCHEDULE.to_vec(), + 0, + other_double_public, + &mut rng, + ) + .unwrap(); + + let aux_data = vec![2, 3, 13, 3]; // the block number when this would execute - let when = 1; let root = murmur_store.root.clone(); let (proof, commitment, ciphertext, pos) = - other_murmur_store.execute(seed.clone(), when, aux_data.clone()).unwrap(); + other_murmur_store.execute(other_seed.clone(), WHEN, aux_data.clone()).unwrap(); - // in practice, the otp code would be timelock decrypted - // but for testing purposes, we will just calculate the expected one now - let botp = build_generator(seed.clone()).unwrap(); - let otp_code = botp.generate(when); - assert!(!verify( - root, - proof, - commitment, - ciphertext, - otp_code.as_bytes().to_vec(), - aux_data, - pos, + assert!(!verifier::verify_execute( + root, proof, commitment, ciphertext, OTP, &aux_data, pos, )); } + + #[test] + fn it_can_generate_and_verify_schnorr_proofs() { + let mut rng = ChaCha20Rng::seed_from_u64(0); + let keypair = w3f_bls::KeypairVT::::generate(&mut OsRng); + let double_public: DoublePublicKey = + DoublePublicKey(keypair.into_public_key_in_signature_group().0, keypair.public.0); + + let mut bytes = Vec::new(); + double_public.serialize_compressed(&mut bytes).unwrap(); + let same_double_public = + DoublePublicKey::::deserialize_compressed(&mut &bytes[..]).unwrap(); + + let seed = vec![1, 2, 3]; + + let murmur_store = MurmurStore::::new::< + DummyIdBuilder, + ChaCha20Rng, + ChaCha20Rng, + >(seed.clone(), BLOCK_SCHEDULE.to_vec(), 0, double_public, &mut rng) + .unwrap(); + + let proof = murmur_store.proof; + let pk = murmur_store.public_key; + // now verify the proof for nonce = 0 + assert!(verifier::verify_update::(proof, pk.clone(), 0,).unwrap()); + + let mut not_same_rng = ChaCha20Rng::seed_from_u64(1); + let another_murmur_store = + MurmurStore::::new::( + seed.clone(), + BLOCK_SCHEDULE.to_vec(), + 1, + same_double_public, + &mut not_same_rng, + ) + .unwrap(); + + let another_proof = another_murmur_store.proof; + let another_pk = another_murmur_store.public_key; + assert!(pk == another_pk); + + // now verify the proof for nonce = 1 + assert!(verifier::verify_update::(another_proof, pk, 1,).unwrap()); + } + + #[test] + fn it_cannot_verify_schnorr_proof_with_bad_nonce() { + let mut rng = ChaCha20Rng::seed_from_u64(0); + let keypair = w3f_bls::KeypairVT::::generate(&mut rng); + let double_public: DoublePublicKey = + DoublePublicKey(keypair.into_public_key_in_signature_group().0, keypair.public.0); + + let seed = vec![1, 2, 3]; + + let murmur_store = MurmurStore::::new::< + DummyIdBuilder, + ChaCha20Rng, + ChaCha20Rng, + >(seed.clone(), BLOCK_SCHEDULE.to_vec(), 0, double_public, &mut rng) + .unwrap(); + + let proof = murmur_store.proof; + let pk = murmur_store.public_key; + // now verify the proof for nonce = 1 + assert!(!verifier::verify_update::(proof, pk, 1,).unwrap()); + } } diff --git a/core/src/otp.rs b/core/src/otp.rs index f0d5217..eb79f3f 100644 --- a/core/src/otp.rs +++ b/core/src/otp.rs @@ -16,6 +16,7 @@ use alloc::{string::String, vec::Vec}; use totp_rs::{Algorithm, Secret, TOTP}; +use zeroize::Zeroize; #[derive(Debug)] pub enum OTPError { @@ -33,25 +34,27 @@ impl BOTPGenerator { /// Create a new BOTP generator with the given seed /// /// * `seed`: The seed used to generate OTP codes - pub fn new(seed: Vec) -> Result { - let secret = Secret::Raw(seed.to_vec()).to_bytes().map_err(|_| OTPError::InvalidSecret)?; + pub fn new(mut seed: Vec) -> Result { + let mut secret = + Secret::Raw(seed.clone()).to_bytes().map_err(|_| OTPError::InvalidSecret)?; + seed.zeroize(); let totp = TOTP::new( Algorithm::SHA256, // algorithm 6, // num digits 1, // skew 1, // step - secret, // secret + secret.clone(), // secret ) .map_err(|_| OTPError::InvalidSecret)?; - + secret.zeroize(); Ok(BOTPGenerator { totp }) } /// Generate an otp code /// /// * `block_height`: The block for which the code is valid - pub fn generate(&self, block_height: u32) -> String { - self.totp.generate(block_height as u64) + pub fn generate(&self, block_height: u64) -> String { + self.totp.generate(block_height) } } @@ -66,7 +69,7 @@ mod tests { let otp_min = botp.generate(0); assert!(otp_min.len() == 6); - let otp_max = botp.generate(u32::MAX); + let otp_max = botp.generate(u64::MAX); assert!(otp_max.len() == 6); } diff --git a/core/src/types.rs b/core/src/types.rs index 4145723..ff76d62 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -19,7 +19,7 @@ use ckb_merkle_mountain_range::{Merge, Result as MMRResult}; use codec::{Decode, Encode}; use sha3::Digest; -pub use etf_crypto_primitives::ibe::fullident::Identity; +pub use timelock::ibe::fullident::Identity; /// The type to represent a block number pub type BlockNumber = u32; diff --git a/docs/murmur/docs/quick_start/discord.md b/docs/murmur/docs/quick_start/discord.md index dabd6af..8da18d3 100644 --- a/docs/murmur/docs/quick_start/discord.md +++ b/docs/murmur/docs/quick_start/discord.md @@ -4,7 +4,7 @@ sidebar_position: 4 # Murmur on Discord -The [murmur-discord-bot](https://github.com/ideal-lab5/murmur-bots/tree/main/discord), built with [discord.js](https://discord.js.org/) and [murmur.js](https://github.com/ideal-lab5/murmur.js), enables Discord users to easily and securely create and execute crypto transactions on the Ideal Network from within Discord itself. This provides powerful new crypto capabilities for Discord, enabling both discord-wide wallets that can be used interoperably between servers, as well as server-specific crypto wallets and all of the economic implications that come with it. +The [murmur discord bot](https://github.com/ideal-lab5/murmur-bots/tree/main/src/discord.js), built with [discord.js](https://discord.js.org/) and [murmur.js](https://github.com/ideal-lab5/murmur.js), enables Discord users to easily and securely create and execute crypto transactions on the Ideal Network from within Discord itself. This provides powerful new crypto capabilities for Discord, enabling both discord-wide wallets that can be used interoperably between servers, as well as server-specific crypto wallets and all of the economic implications that come with it.

![discord_bot](../../assets/discord_bot_in_action.png) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index cb1e731..80ffe1f 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -27,6 +27,7 @@ subxt-signer = "0.35.2" tokio = { version = "1.35", features = ["macros", "time", "rt-multi-thread"] } clap = { version = "4.1.1", features = ["derive"] } rand_chacha = "0.3.1" +rand_core = "0.6.4" ckb-merkle-mountain-range = "0.5.2" sha3 = "0.10.8" thiserror = "1.0" @@ -44,7 +45,6 @@ ark-serialize = "0.4.0" w3f-bls = "0.1.3" murmur-core = { path = "../core/", features = ["client"] } zeroize = "1.8.1" -hkdf = "0.12.4" [dev-dependencies] diff --git a/lib/README.md b/lib/README.md index d7312b4..7c53341 100644 --- a/lib/README.md +++ b/lib/README.md @@ -32,7 +32,7 @@ To generate a wallet valid for the next 1000 blocks, use: To send a balance transfer, use: ```shell -./target/debug/murmur execute --name test --seed my_secret_key --to CuqfkE3QieYPAWPpwiygDufmyrKecDcVCF7PN1psaLEn8yr --amount 100_000_000 +./target/debug/murmur execute --name test --seed my_secret_key --to CuqfkE3QieYPAWPpwiygDufmyrKecDcVCF7PN1psaLEn8yr --amount 100000000 ``` ## Test diff --git a/lib/artifacts/metadata.scale b/lib/artifacts/metadata.scale index 578d096..610fd52 100644 Binary files a/lib/artifacts/metadata.scale and b/lib/artifacts/metadata.scale differ diff --git a/lib/src/bin/murmur/main.rs b/lib/src/bin/murmur/main.rs index 50b4caa..0f0d4fd 100644 --- a/lib/src/bin/murmur/main.rs +++ b/lib/src/bin/murmur/main.rs @@ -16,10 +16,13 @@ use clap::{Parser, Subcommand}; use murmur_lib::{ - create, etf, idn_connect, prepare_execute, BlockNumber, BoundedVec, MurmurStore, RuntimeCall, + create, etf, prepare_execute, BlockNumber, BoundedVec, EngineTinyBLS377, MurmurStore, + RuntimeCall, }; + use sp_core::crypto::Ss58Codec; use std::{fs::File, time::Instant}; +use subxt::{backend::rpc::RpcClient, client::OnlineClient, config::SubstrateConfig}; use subxt_signer::sr25519::dev; use thiserror::Error; @@ -32,36 +35,49 @@ struct Cli { commands: Commands, } +/// Commands available to the user #[derive(Subcommand)] enum Commands { - /// create a new murmur wallet + /// create a new Murmur wallet New(WalletCreationDetails), - /// dispatch (proxy) a call to a murmur wallet + /// Update an existing Murmur wallet + Update(WalletCreationDetails), + /// dispatch (proxy) a call to a Murmur wallet Execute(WalletExecuteDetails), } +/// Arguments for creation and updating a wallet #[derive(Parser)] struct WalletCreationDetails { + /// The name of the wallet #[arg(long, short)] name: String, + /// The seed of the wallet (i.e. password) #[arg(long, short)] seed: String, + /// The lifetime (from now) of the wallet in blocks #[clap(long, short)] validity: u32, } +/// Arguments for executing a balance transfer from the wallet #[derive(Parser)] struct WalletExecuteDetails { + /// The name of the wallet #[arg(long, short)] name: String, + /// The seed of the wallet (i.e. password) #[arg(long, short)] seed: String, + /// The recipient (ss58 encoded) #[arg(long, short)] to: String, + /// The balance to send #[arg(short, long, value_parser = clap::value_parser!(u128))] amount: u128, } +/// Errors that can be thrown by this crate #[derive(Error, Debug)] pub enum CLIError { #[error("invalid public key")] @@ -78,8 +94,7 @@ pub enum CLIError { CorruptedMurmurStore, } -/// the mmr_store file location -/// in future, make configurable +/// The default mmr_store file location pub const MMR_STORE_FILEPATH: &str = "mmr_store"; #[tokio::main] @@ -90,91 +105,185 @@ async fn main() -> Result<(), Box> { let (client, current_block_number, round_pubkey_bytes) = idn_connect().await?; match &cli.commands { - Commands::New(args) => { - println!("🏭 Murmur: Generating Merkle mountain range"); - - // 1. prepare block schedule - let mut schedule: Vec = Vec::new(); - for i in 2..args.validity + 2 { - // wallet is 'active' in 2 blocks - let next_block_number: BlockNumber = current_block_number + i; - schedule.push(next_block_number); - } - - // 2. create mmr - let create_data = create(args.seed.as_bytes().to_vec(), schedule, round_pubkey_bytes) - .map_err(|_| CLIError::MurmurCreationFailed)?; - - // 3. add to storage - write_mmr_store(create_data.mmr_store.clone(), MMR_STORE_FILEPATH); - - // 4. build the call - let call = etf::tx().murmur().create( - create_data.root, - create_data.size, - BoundedVec(args.name.as_bytes().to_vec()), - ); - - // 5. sign and send the call - client.tx().sign_and_submit_then_watch_default(&call, &dev::alice()).await?; - - println!("✅ MMR proxy account creation successful!"); - }, - Commands::Execute(args) => { - // 1. build proxied call - let from_ss58 = sp_core::crypto::AccountId32::from_ss58check(&args.to) - .map_err(|_| CLIError::InvalidRecipient)?; - let bytes: &[u8] = from_ss58.as_ref(); - let from_ss58_sized: [u8; 32] = - bytes.try_into().map_err(|_| CLIError::InvalidRecipient)?; - let to = subxt::utils::AccountId32::from(from_ss58_sized); - let balance_transfer_call = - RuntimeCall::Balances(etf::balances::Call::transfer_allow_death { - dest: subxt::utils::MultiAddress::<_, u32>::from(to), - value: args.amount, - }); - - // 2. load the MMR store - let store: MurmurStore = load_mmr_store(MMR_STORE_FILEPATH)?; - println!("💾 Recovered Murmur store from local file"); - - // 3. get the proxy data - let proxy_data = prepare_execute( - args.seed.as_bytes().to_vec(), - current_block_number + 1, - store, - &balance_transfer_call, - ) - .map_err(|_| CLIError::MurmurExecutionFailed)?; - - // 4. build the call - let call = etf::tx().murmur().proxy( - BoundedVec(args.name.as_bytes().to_vec()), - proxy_data.position, - proxy_data.hash, - proxy_data.ciphertext, - proxy_data.proof_items, - proxy_data.size, - balance_transfer_call, - ); - // 5. sign and send the call - client.tx().sign_and_submit_then_watch_default(&call, &dev::alice()).await?; - }, + Commands::New(args) => + handle_create(args, client, current_block_number, round_pubkey_bytes).await?, + Commands::Update(args) => + handle_update(args, client, current_block_number, round_pubkey_bytes).await?, + Commands::Execute(args) => handle_execute(args, client, current_block_number).await?, } - println!("Elapsed time: {:.2?}", before.elapsed()); + + println!("Done! Time elapsed: {:.2?}", before.elapsed()); + + Ok(()) +} + +/// This function creates a new Murmur wallet +async fn handle_create( + args: &WalletCreationDetails, + client: OnlineClient, + current_block_number: BlockNumber, + round_pubkey_bytes: Vec, +) -> Result<(), Box> { + let mmr_store = build_mmr_store(args, current_block_number, round_pubkey_bytes, 0)?; + + let call = etf::tx().murmur().create( + BoundedVec(args.name.as_bytes().to_vec()), + BoundedVec(mmr_store.root.0), + mmr_store.metadata.keys().len() as u64, + BoundedVec(mmr_store.proof), + BoundedVec(mmr_store.public_key), + ); + + client.tx().sign_and_submit_then_watch_default(&call, &dev::alice()).await?; + + println!("✅ Murmur Proxy Creation: Successful!"); Ok(()) } +/// This function updates an existing Murmur wallet +async fn handle_update( + args: &WalletCreationDetails, + client: OnlineClient, + current_block_number: BlockNumber, + round_pubkey_bytes: Vec, +) -> Result<(), Box> { + // existing mmr_store, we only need this to get the nonce + // note that there are many other ways to get this as well (e.g. query the runtime) + let store: MurmurStore = load_mmr_store(MMR_STORE_FILEPATH)?; + + let mmr_store = + build_mmr_store(args, current_block_number, round_pubkey_bytes, store.nonce + 1)?; + + let call = etf::tx().murmur().update( + BoundedVec(args.name.as_bytes().to_vec()), + BoundedVec(mmr_store.root.0), + mmr_store.metadata.keys().len() as u64, + BoundedVec(mmr_store.proof), + ); + + client.tx().sign_and_submit_then_watch_default(&call, &dev::alice()).await?; + + println!("✅ Murmur Proxy Update: Successful!"); + + Ok(()) +} + +/// Build a new Murmur store for each block from `current_block_number + 2` to +/// `current_block_number + args.validity` +fn build_mmr_store( + args: &WalletCreationDetails, + current_block_number: BlockNumber, + round_pubkey_bytes: Vec, + nonce: u64, +) -> Result, Box> { + println!("🏗️ Murmur: Generating Merkle mountain range"); + let mut schedule: Vec = Vec::new(); + for i in 2..args.validity + 2 { + // the wallet is 'active' 2 blocks from the current block + let next_block_number: BlockNumber = current_block_number + i; + schedule.push(next_block_number); + } + + let mmr_store: MurmurStore = + create(args.seed.as_bytes().to_vec(), nonce, schedule, round_pubkey_bytes) + .map_err(|_| CLIError::MurmurCreationFailed)?; + + write_mmr_store(mmr_store.encode(), MMR_STORE_FILEPATH); + + Ok(mmr_store) +} + +/// This function executes a call from the Murmur proxy at block after the provided +/// `current_block_height` note that this does not guarantee execution. If the transaction is not +/// included in a block in the upcoming block then it will never be executed +async fn handle_execute( + args: &WalletExecuteDetails, + client: OnlineClient, + current_block_number: BlockNumber, +) -> Result<(), Box> { + // 1. build proxied call + let from_ss58 = sp_core::crypto::AccountId32::from_ss58check(&args.to) + .map_err(|_| CLIError::InvalidRecipient)?; + let bytes: &[u8] = from_ss58.as_ref(); + let from_ss58_sized: [u8; 32] = bytes.try_into().map_err(|_| CLIError::InvalidRecipient)?; + let to = subxt::utils::AccountId32::from(from_ss58_sized); + let balance_transfer_call = RuntimeCall::Balances(etf::balances::Call::transfer_allow_death { + dest: subxt::utils::MultiAddress::<_, u32>::from(to), + value: args.amount, + }); + + // 2. load the MMR store + let store: MurmurStore = load_mmr_store(MMR_STORE_FILEPATH)?; + println!("💾 Recovered Murmur store from local file"); + + // 3. get the proxy data + let proxy_data = prepare_execute( + args.seed.as_bytes().to_vec(), + current_block_number + 1, + store, + &balance_transfer_call, + ) + .map_err(|_| CLIError::MurmurExecutionFailed)?; + + // 4. build the call + let call = etf::tx().murmur().proxy( + BoundedVec(args.name.as_bytes().to_vec()), + proxy_data.position, + proxy_data.hash, + proxy_data.ciphertext, + proxy_data.proof_items, + proxy_data.size, + balance_transfer_call, + ); + // 5. sign and send the call + client.tx().sign_and_submit_then_watch_default(&call, &dev::alice()).await?; + Ok(()) +} + +/// Async connection to the Ideal Network +/// if successful then fetch data +/// else error if unreachable +async fn idn_connect( +) -> Result<(OnlineClient, BlockNumber, Vec), Box> { + println!("🎲 Connecting to Ideal Network"); + let ws_url = std::env::var("WS_URL").unwrap_or_else(|_| { + let fallback_url = "ws://localhost:9944".to_string(); + println!("⚠️ WS_URL environment variable not set. Using fallback URL: {}", fallback_url); + fallback_url + }); + + let rpc_client = RpcClient::from_url(&ws_url).await?; + let client = OnlineClient::::from_rpc_client(rpc_client.clone()).await?; + println!("🔗 RPC Client: connection established"); + + // fetch the round public key from etf runtime storage + let round_key_query = subxt::dynamic::storage("Etf", "RoundPublic", ()); + let result = client.storage().at_latest().await?.fetch(&round_key_query).await?; + let round_pubkey_bytes = result.unwrap().as_type::>()?; + + println!("🔑 Successfully retrieved the round public key."); + + let current_block = client.blocks().at_latest().await?; + let current_block_number: BlockNumber = current_block.header().number; + println!("🧊 Current block number: #{:?}", current_block_number); + Ok((client, current_block_number, round_pubkey_bytes)) +} + /// read an MMR from a file -fn load_mmr_store(path: &str) -> Result { +/// * `path`: The mmr_store path +fn load_mmr_store(path: &str) -> Result, CLIError> { let mmr_store_file = File::open(path).expect("Unable to open file"); - let data: MurmurStore = + let data: Vec = serde_cbor::from_reader(mmr_store_file).map_err(|_| CLIError::CorruptedMurmurStore)?; - Ok(data) + let mmr_store = MurmurStore::::decode(data).expect("The data is corrupted."); + Ok(mmr_store) } /// Write the MMR data to a file -fn write_mmr_store(mmr_store: MurmurStore, path: &str) { +/// * `mmr_store_data`: The serialized mmr store +/// * `path`: The file path to write to +fn write_mmr_store(mmr_store_data: Vec, path: &str) { let mmr_store_file = File::create(path).expect("It should create the file"); - serde_cbor::to_writer(mmr_store_file, &mmr_store).unwrap(); + serde_cbor::to_writer(mmr_store_file, &mmr_store_data) + .expect("It failed to write to the local filesystem."); } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 633cb60..87fedf2 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -15,12 +15,9 @@ */ use beefy::{known_payloads, Commitment, Payload}; -use hkdf::Hkdf; use murmur_core::types::{Identity, IdentityBuilder}; +use rand_core::OsRng; use serde::Serialize; -use subxt::{ - backend::rpc::RpcClient, client::OnlineClient, config::SubstrateConfig, ext::codec::Encode, -}; use w3f_bls::{DoublePublicKey, SerializableToBytes, TinyBLS377}; use zeroize::Zeroize; @@ -28,9 +25,11 @@ pub use etf::runtime_types::{ bounded_collections::bounded_vec::BoundedVec, node_template_runtime::RuntimeCall, }; pub use murmur_core::{ - murmur::{Error, MurmurStore}, + murmur::{EngineTinyBLS377, Error, MurmurStore}, types::BlockNumber, }; +use rand_chacha::ChaCha20Rng; +use subxt::ext::codec::Encode; // Generate an interface that we can use from the node's metadata. #[subxt::subxt(runtime_metadata_path = "artifacts/metadata.scale")] @@ -45,31 +44,27 @@ impl IdentityBuilder for BasicIdBuilder { let commitment = Commitment { payload, block_number: when, - validator_set_id: 0, /* TODO: how to ensure correct validator set ID is used? could - * just always set to 1 for now, else set input param. */ + // Note: Currently the validator set id is always set to 0 by the IDN runtime. + // We have a backlog item to properly update this, which will require + // that we properly estimate future validator set ids here + // see: https://github.com/ideal-lab5/pallets/issues/29 + validator_set_id: 0, }; - Identity::new(&commitment.encode()) + Identity::new(b"", vec![commitment.encode()]) } } -#[derive(Serialize)] -/// Data needed to build a valid call for creating a murmur wallet. -pub struct CreateData { - /// The root of the MMR - pub root: Vec, - /// The size of the MMR - pub size: u64, - pub mmr_store: MurmurStore, -} - #[derive(Serialize)] /// Data needed to build a valid call for a proxied execution. pub struct ProxyData { pub position: u64, /// The hash of the commitment pub hash: Vec, + /// The timelocked ciphertext pub ciphertext: Vec, + /// The Merkle proof items pub proof_items: Vec>, + /// The size of the Merkle proof pub size: u64, } @@ -80,28 +75,22 @@ pub struct ProxyData { /// * `round_pubkey_bytes`: The Ideal Network randomness beacon public key pub fn create( mut seed: Vec, + nonce: u64, block_schedule: Vec, round_pubkey_bytes: Vec, -) -> Result { - // Derive ephem_msk from seed using HKDF - let hk = Hkdf::::new(None, &seed); - let mut ephem_msk = [0u8; 32]; - hk.expand(b"ephemeral key", &mut ephem_msk) - .map_err(|_| Error::KeyDerivationFailed)?; - +) -> Result, Error> { let round_pubkey = DoublePublicKey::::from_bytes(&round_pubkey_bytes) .map_err(|_| Error::InvalidPubkey)?; - let mmr_store = MurmurStore::new::( + + let mmr_store = MurmurStore::::new::( seed.clone(), block_schedule.clone(), - ephem_msk, + nonce, round_pubkey, + &mut OsRng, )?; - ephem_msk.zeroize(); seed.zeroize(); - let root = mmr_store.root.clone(); - - Ok(CreateData { root: root.0, size: mmr_store.metadata.len() as u64, mmr_store }) + Ok(mmr_store) } /// Return the data needed for the immediate execution of the proxied call. @@ -109,18 +98,10 @@ pub fn create( /// * `when`: The block number when OTP codeds should be generated /// * `store`: A murmur store /// * `call`: Proxied call. Any valid runtime call -// Note to self: in the future, we can consider ways to prune the murmurstore as OTP codes are -// consumed for example, we can take the next values from the map, reducing storage to 0 over -// time However, to do this we need to think of a way to prove it with a merkle proof -// my thought is that we would have a subtree, so first we prove that the subtree is indeed in -// the parent MMR then we prove that the specific leaf is in the subtree. -// We could potentially use that idea as a way to optimize the execute function in general. Rather -// than loading the entire MMR into memory, we really only need to load a minimal subtree -// containing the leaf we want to consume -> add this to the 'future work' section later pub fn prepare_execute( mut seed: Vec, when: BlockNumber, - store: MurmurStore, + store: MurmurStore, call: &RuntimeCall, ) -> Result { let (proof, commitment, ciphertext, pos) = store.execute(seed.clone(), when, call.encode())?; @@ -132,61 +113,20 @@ pub fn prepare_execute( Ok(ProxyData { position: pos, hash: commitment, ciphertext, proof_items, size }) } -/// Async connection to the Ideal Network -/// if successful then fetch data -/// else error if unreachable -pub async fn idn_connect( -) -> Result<(OnlineClient, BlockNumber, Vec), Box> { - println!("🎲 Connecting to Ideal network (local node)"); - let ws_url = std::env::var("WS_URL").unwrap_or_else(|_| { - let fallback_url = "ws://localhost:9944".to_string(); - println!("⚠️ WS_URL environment variable not set. Using fallback URL: {}", fallback_url); - fallback_url - }); - - let rpc_client = RpcClient::from_url(&ws_url).await?; - let client = OnlineClient::::from_rpc_client(rpc_client.clone()).await?; - println!("🔗 RPC Client: connection established"); - - // fetch the round public key from etf runtime storage - let round_key_query = subxt::dynamic::storage("Etf", "RoundPublic", ()); - let result = client.storage().at_latest().await?.fetch(&round_key_query).await?; - let round_pubkey_bytes = result.unwrap().as_type::>()?; - - println!("🔑 Successfully retrieved the round public key."); - - let current_block = client.blocks().at_latest().await?; - let current_block_number: BlockNumber = current_block.header().number; - println!("🧊 Current block number: #{:?}", current_block_number); - Ok((client, current_block_number, round_pubkey_bytes)) -} - #[cfg(test)] mod tests { use super::*; #[test] - pub fn it_can_create_an_mmr_store() { + pub fn it_can_create_an_mmr_store_and_call_data() { let seed = b"seed".to_vec(); let block_schedule = vec![1, 2, 3, 4, 5, 6, 7]; let double_public_bytes = murmur_test_utils::get_dummy_beacon_pubkey(); - let create_data = - create(seed.clone(), block_schedule.clone(), double_public_bytes.clone()).unwrap(); - - let hk = Hkdf::::new(None, &seed); - let mut ephem_msk = [0u8; 32]; - hk.expand(b"ephemeral key", &mut ephem_msk).unwrap(); - - let mmr_store = MurmurStore::new::( - seed, - block_schedule, - ephem_msk, - DoublePublicKey::::from_bytes(&double_public_bytes).unwrap(), - ) - .unwrap(); - - assert_eq!(create_data.mmr_store.root, mmr_store.root); - assert_eq!(create_data.size, 7); + let mmr_store = + create(seed.clone(), 0, block_schedule.clone(), double_public_bytes.clone()).unwrap(); + + assert_eq!(mmr_store.root.0.len(), 32); + assert_eq!(mmr_store.metadata.keys().len(), 7); } #[test] @@ -194,34 +134,23 @@ mod tests { let seed = b"seed".to_vec(); let block_schedule = vec![1, 2, 3, 4, 5, 6, 7]; let double_public_bytes = murmur_test_utils::get_dummy_beacon_pubkey(); - let create_data = create(seed.clone(), block_schedule, double_public_bytes).unwrap(); + let mmr_store = create(seed.clone(), 0, block_schedule, double_public_bytes).unwrap(); let bob = subxt_signer::sr25519::dev::bob().public_key(); let balance_transfer_call = - &etf::runtime_types::node_template_runtime::RuntimeCall::Balances( + etf::runtime_types::node_template_runtime::RuntimeCall::Balances( etf::balances::Call::transfer_allow_death { dest: subxt::utils::MultiAddress::<_, u32>::from(bob), value: 1, }, ); - let proxy_data = - prepare_execute(seed.clone(), 1, create_data.mmr_store.clone(), balance_transfer_call) - .unwrap(); - - let (proof, commitment, ciphertext, _pos) = create_data - .mmr_store - .execute(seed.clone(), 1, balance_transfer_call.encode()) - .unwrap(); + let when = 1; - let size = proof.mmr_size(); - let proof_items: Vec> = - proof.proof_items().iter().map(|leaf| leaf.0.clone()).collect::>(); + let proxy_data = prepare_execute(seed, when, mmr_store, &balance_transfer_call).unwrap(); assert_eq!(proxy_data.position, 0); - assert_eq!(proxy_data.hash, commitment); - assert_eq!(proxy_data.ciphertext, ciphertext); - assert_eq!(proxy_data.proof_items, proof_items); - assert_eq!(proxy_data.size, size); + assert_eq!(proxy_data.hash.len(), 32); + assert_eq!(proxy_data.ciphertext.len(), 250); } } diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml index ad4742e..d14bccd 100644 --- a/test-utils/Cargo.toml +++ b/test-utils/Cargo.toml @@ -18,15 +18,21 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] rand_core = { version = "0.6.4", default-features = false } +ark-ec = { version = "0.4", default-features = false } ark-serialize = { version = "0.4.0", default-features = false } +ark-std = { version = "0.4", default-features = false } w3f-bls = { version = "0.1.3", default-features = false } murmur-core = { package = "murmur-core", path = "../core/", features = ["client"] } +dleq_vrf = { git = "https://github.com/w3f/ring-vrf.git", default-features = false, features = ["getrandom"]} [features] default = ["std"] std = [ + "ark-ec/std", "ark-serialize/std", + "ark-std/std", "w3f-bls/std", "murmur-core/std", + "dleq_vrf/std", ] no_std = [] \ No newline at end of file diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 0e8e54d..089d60a 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -25,9 +25,7 @@ use w3f_bls::{DoublePublicKey, DoublePublicKeyScheme, TinyBLS377}; extern crate alloc; -pub use murmur_core::otp::BOTPGenerator; - -pub use murmur_core::murmur::MurmurStore; +pub use murmur_core::{murmur::MurmurStore, otp::BOTPGenerator}; pub fn get_dummy_beacon_pubkey() -> Vec { let keypair = w3f_bls::KeypairVT::::generate(&mut OsRng);