diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 38ac38b..61d8125 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,21 +2,24 @@ name: Rust on: push: - branches: [ "main", "dev" ] + branches: ["main", "dev"] pull_request: - branches: [ "main", "dev" ] + branches: ["main", "dev"] env: CARGO_TERM_COLOR: always jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v4 + - name: Install Rust + run: rustup update stable + - name: Check formatting + run: cargo fmt -- --check + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/core/src/murmur.rs b/core/src/murmur.rs index 8f32a4f..abe1cdb 100644 --- a/core/src/murmur.rs +++ b/core/src/murmur.rs @@ -15,14 +15,12 @@ */ //! The murmur protocol implementation -//! use alloc::{collections::BTreeMap, vec, vec::Vec}; #[cfg(feature = "client")] use crate::otp::BOTPGenerator; -use ark_std::rand::SeedableRng; -use ark_std::rand::{CryptoRng, Rng}; +use ark_std::rand::{CryptoRng, Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; #[cfg(feature = "client")] @@ -33,9 +31,9 @@ use ark_serialize::CanonicalSerialize; use crate::types::*; use ckb_merkle_mountain_range::{ - helper::leaf_index_to_pos, - util::{MemMMR, MemStore}, - MerkleProof, + helper::leaf_index_to_pos, + util::{MemMMR, MemStore}, + MerkleProof, }; use codec::{Decode, Encode}; use etf_crypto_primitives::{encryption::tlock::*, ibe::fullident::Identity}; @@ -45,454 +43,426 @@ use w3f_bls::{DoublePublicKey, EngineBLS}; /// Error types for murmur wallet usage #[derive(Debug, PartialEq)] pub enum Error { - /// An error occurred when executing a call - ExecuteError, - /// An error occurred when creating a murmur wallet - MMRError, - InconsistentStore, - /// No leaf could be identified in the MMR at the specified position - NoLeafFound, - /// No ciphertext could be identified for the block within the current murmur store - NoCiphertextFound, - /// There was an error when executing timelock encryption (is the ciphertext too large?) - TlockFailed, - /// 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, + /// An error occurred when executing a call + ExecuteError, + /// An error occurred when creating a murmur wallet + MMRError, + InconsistentStore, + /// No leaf could be identified in the MMR at the specified position + NoLeafFound, + /// No ciphertext could be identified for the block within the current murmur store + NoCiphertextFound, + /// There was an error when executing timelock encryption (is the ciphertext too large?) + TlockFailed, + /// 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 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 { - /// A map of block numbers to leaf positions in the mmr - pub metadata: BTreeMap, - /// The root of the mmr - pub root: Leaf, + /// A map of block numbers to leaf positions in the mmr + pub metadata: BTreeMap, + /// The root of the mmr + pub root: Leaf, } #[cfg(feature = "client")] impl MurmurStore { - /// Create a new Murmur store - /// - /// * `seed`: An any-length seed (i.e. password) - /// * `block_schedule`: The blocks for which OTP codes will be generated - /// * `ephemeral_msk`: Any 32 bytes - /// * `round_public_key`: The IDN beacon's public key - /// - pub fn new>( - 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(); - - 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); - - // 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(); - - let ephem_rng = ChaCha20Rng::from_seed(hash.into()); - let ct_bytes = timelock_encrypt::( - identity, - round_public_key.1, - ephemeral_msk, - otp_code.as_bytes(), - ephem_rng, - )?; - let leaf = Leaf(ct_bytes.clone()); - // Q: How can I test this? - // 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); - } - - ephemeral_msk.zeroize(); - let root = mmr.get_root().map_err(|_| Error::InconsistentStore)?; - - Ok(MurmurStore { metadata, root }) - } - - /// Build data required (proof and commitment) to execute a valid call from a murmur wallet - /// - /// * `seed`: The seed used to create the mmr - /// * `when`: The block number when the wallet is being used (or will be) - /// * `call_data`: The call to be executed with the wallet (at `when`) - /// - pub fn execute( - &self, - mut seed: Vec, - when: BlockNumber, - 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())?; - seed.zeroize(); - let idx = get_key_index(&self.metadata, &when) - .expect("The key must exist within the metadata."); - 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)?; - return Ok((proof, commitment, ciphertext.clone(), pos)); - } - - Err(Error::NoCiphertextFound) - } - - /// Generate a commitment (hash) to commit to executing a call at a specific block number - /// - /// * `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())?; - seed.zeroize(); - let otp_code = botp.generate(when); - - let mut hasher = sha3::Sha3_256::default(); - hasher.update(otp_code.as_bytes()); - hasher.update(data); - Ok(hasher.finalize().to_vec()) - } - - /// Builds an mmr from the mmr store - /// - /// * `mmr`: a MemMMR instance (to be populated) - /// - fn to_mmr(&self) -> Result, Error> { - let store = MemStore::default(); - let mut mmr = MemMMR::<_, MergeLeaves>::new(0, store); - for (_block_number, ciphertext) in self.metadata.clone() { - mmr.push(Leaf(ciphertext)) - .map_err(|_| Error::InconsistentStore)?; - } - - Ok(mmr) - } + /// Create a new Murmur store + /// + /// * `seed`: An any-length seed (i.e. password) + /// * `block_schedule`: The blocks for which OTP codes will be generated + /// * `ephemeral_msk`: Any 32 bytes + /// * `round_public_key`: The IDN beacon's public key + pub fn new>( + 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(); + + 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); + + // 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(); + + let ephem_rng = ChaCha20Rng::from_seed(hash.into()); + let ct_bytes = timelock_encrypt::( + identity, + round_public_key.1, + ephemeral_msk, + otp_code.as_bytes(), + ephem_rng, + )?; + let leaf = Leaf(ct_bytes.clone()); + // Q: How can I test this? + // 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); + } + + ephemeral_msk.zeroize(); + let root = mmr.get_root().map_err(|_| Error::InconsistentStore)?; + + Ok(MurmurStore { metadata, root }) + } + + /// Build data required (proof and commitment) to execute a valid call from a murmur wallet + /// + /// * `seed`: The seed used to create the mmr + /// * `when`: The block number when the wallet is being used (or will be) + /// * `call_data`: The call to be executed with the wallet (at `when`) + pub fn execute( + &self, + mut seed: Vec, + when: BlockNumber, + 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())?; + seed.zeroize(); + let idx = get_key_index(&self.metadata, &when) + .expect("The key must exist within the metadata."); + 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)?; + return Ok((proof, commitment, ciphertext.clone(), pos)); + } + + Err(Error::NoCiphertextFound) + } + + /// Generate a commitment (hash) to commit to executing a call at a specific block number + /// + /// * `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())?; + seed.zeroize(); + let otp_code = botp.generate(when); + + let mut hasher = sha3::Sha3_256::default(); + hasher.update(otp_code.as_bytes()); + hasher.update(data); + Ok(hasher.finalize().to_vec()) + } + + /// Builds an mmr from the mmr store + /// + /// * `mmr`: a MemMMR instance (to be populated) + fn to_mmr(&self) -> Result, Error> { + let store = MemStore::default(); + let mut mmr = MemMMR::<_, MergeLeaves>::new(0, store); + for (_block_number, ciphertext) in self.metadata.clone() { + mmr.push(Leaf(ciphertext)).map_err(|_| Error::InconsistentStore)?; + } + + Ok(mmr) + } } #[cfg(feature = "client")] /// Timelock encryption helper function pub fn timelock_encrypt( - identity: Identity, - pk: E::PublicKeyGroup, - ephemeral_msk: [u8; 32], - message: &[u8], - rng: R, + identity: Identity, + pk: E::PublicKeyGroup, + ephemeral_msk: [u8; 32], + message: &[u8], + rng: R, ) -> Result, Error> { - let ciphertext = - 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) + let ciphertext = + 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 #[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) + 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) } // verify the correctness of execution parameters pub fn verify( - root: Leaf, - proof: MerkleProof, - hash: Vec, - ciphertext: Vec, - otp: Vec, - aux_data: Vec, - pos: u64, + 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); + 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(); + if validity { + let mut hasher = sha3::Sha3_256::default(); + hasher.update(otp); + hasher.update(aux_data); + let expected_hash = hasher.finalize(); - validity = validity && expected_hash.to_vec() == hash; - } + validity = validity && expected_hash.to_vec() == hash; + } - validity + validity } /// 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) + b.keys().position(|k| k == key) } #[cfg(test)] mod tests { - use super::*; - use ark_std::rand::SeedableRng; - use rand_chacha::ChaCha20Rng; - use rand_core::OsRng; - use w3f_bls::{DoublePublicKeyScheme, TinyBLS377}; - - pub struct DummyIdBuilder; - impl IdentityBuilder for DummyIdBuilder { - fn build_identity(at: BlockNumber) -> Identity { - Identity::new(&[at as u8]) - } - } - - #[cfg(feature = "client")] - #[test] - pub fn it_can_generate_mmr_data_store() { - 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 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, - ) - .unwrap(); - - assert!(murmur_store.metadata.keys().len() == 3); - } - - #[cfg(feature = "client")] - #[test] - pub fn it_can_generate_valid_output_and_verify_it() { - 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 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, - ) - .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(); - - // 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, - )); - } - - #[cfg(feature = "client")] - #[test] - pub fn it_fails_to_generate_execute_output_when_ciphertext_dne() { - 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 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, - ) - .unwrap(); - - // the block number when this would execute - let when = 1000; - - match murmur_store.execute(seed.clone(), when, aux_data.clone()) { - Ok(_) => panic!("There should be an error"), - Err(e) => assert_eq!(e, Error::NoCiphertextFound), - } - } - - #[cfg(feature = "client")] - #[test] - pub fn it_fails_on_verify_bad_aux_data() { - 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 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, - ) - .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); - - let bad_aux = vec![2, 3, 13, 3]; - assert!(!verify( - root, - proof, - commitment, - ciphertext, - otp_code.as_bytes().to_vec(), - bad_aux, - pos, - )); - } - - #[test] - pub fn it_fails_on_verify_bad_proof() { - 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 other_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 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 murmur_store = MurmurStore::new::( - seed.clone(), - schedule.clone(), - ephem_msk, - double_public, - ) - .unwrap(); - - let other_murmur_store = MurmurStore::new::( - seed.clone(), - other_schedule.clone(), - ephem_msk, - other_double_public, - ) - .unwrap(); - - // 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(); - - // 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, - )); - } + use super::*; + use ark_std::rand::SeedableRng; + use rand_chacha::ChaCha20Rng; + use rand_core::OsRng; + use w3f_bls::{DoublePublicKeyScheme, TinyBLS377}; + + pub struct DummyIdBuilder; + impl IdentityBuilder for DummyIdBuilder { + fn build_identity(at: BlockNumber) -> Identity { + Identity::new(&[at as u8]) + } + } + + #[cfg(feature = "client")] + #[test] + pub fn it_can_generate_mmr_data_store() { + 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 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, + ) + .unwrap(); + + assert!(murmur_store.metadata.keys().len() == 3); + } + + #[cfg(feature = "client")] + #[test] + pub fn it_can_generate_valid_output_and_verify_it() { + 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 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, + ) + .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(); + + // 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, + )); + } + + #[cfg(feature = "client")] + #[test] + pub fn it_fails_to_generate_execute_output_when_ciphertext_dne() { + 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 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, + ) + .unwrap(); + + // the block number when this would execute + let when = 1000; + + match murmur_store.execute(seed.clone(), when, aux_data.clone()) { + Ok(_) => panic!("There should be an error"), + Err(e) => assert_eq!(e, Error::NoCiphertextFound), + } + } + + #[cfg(feature = "client")] + #[test] + pub fn it_fails_on_verify_bad_aux_data() { + 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 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, + ) + .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); + + let bad_aux = vec![2, 3, 13, 3]; + assert!(!verify( + root, + proof, + commitment, + ciphertext, + otp_code.as_bytes().to_vec(), + bad_aux, + pos, + )); + } + + #[test] + pub fn it_fails_on_verify_bad_proof() { + 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 other_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 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 murmur_store = MurmurStore::new::( + seed.clone(), + schedule.clone(), + ephem_msk, + double_public, + ) + .unwrap(); + + let other_murmur_store = MurmurStore::new::( + seed.clone(), + other_schedule.clone(), + ephem_msk, + other_double_public, + ) + .unwrap(); + + // 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(); + + // 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, + )); + } } diff --git a/core/src/otp.rs b/core/src/otp.rs index 1e86787..f0d5217 100644 --- a/core/src/otp.rs +++ b/core/src/otp.rs @@ -14,71 +14,64 @@ * limitations under the License. */ -use totp_rs::{Secret, TOTP, Algorithm}; -use alloc::{ - vec::Vec, - string::String, -}; +use alloc::{string::String, vec::Vec}; +use totp_rs::{Algorithm, Secret, TOTP}; #[derive(Debug)] pub enum OTPError { - /// The secret size is too large - InvalidSecret, + /// The secret size is too large + InvalidSecret, } /// A block based otp generator pub struct BOTPGenerator { - /// The time-based otp generator - totp: TOTP + /// The time-based otp generator + totp: TOTP, } 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)?; - let totp = TOTP::new( - Algorithm::SHA256, // algorithm - 6, // num digits - 1, // skew - 1, // step - secret // secret - ).map_err(|_| OTPError::InvalidSecret)?; + /// 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)?; + let totp = TOTP::new( + Algorithm::SHA256, // algorithm + 6, // num digits + 1, // skew + 1, // step + secret, // secret + ) + .map_err(|_| OTPError::InvalidSecret)?; - 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) - } + 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) + } } #[cfg(test)] mod tests { - use super::*; - use alloc::vec; - - #[test] - pub fn it_can_generate_otp_codes_with_valid_seed() { - let botp = BOTPGenerator::new([1;32].to_vec()).unwrap(); - let otp_min = botp.generate(0); - assert!(otp_min.len() == 6); + use super::*; + use alloc::vec; - let otp_max = botp.generate(u32::MAX); - assert!(otp_max.len() == 6); - } + #[test] + pub fn it_can_generate_otp_codes_with_valid_seed() { + let botp = BOTPGenerator::new([1; 32].to_vec()).unwrap(); + let otp_min = botp.generate(0); + assert!(otp_min.len() == 6); - #[test] - pub fn it_fails_to_build_otp_generator_with_invalid_seed() { - assert!(BOTPGenerator::new(vec![1]).is_err()); - } + let otp_max = botp.generate(u32::MAX); + assert!(otp_max.len() == 6); + } -} \ No newline at end of file + #[test] + pub fn it_fails_to_build_otp_generator_with_invalid_seed() { + assert!(BOTPGenerator::new(vec![1]).is_err()); + } +} diff --git a/core/src/types.rs b/core/src/types.rs index d9b2c63..4145723 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -30,33 +30,33 @@ pub type Ciphertext = Vec; /// A leaf in the MMR /// The payload is an opaque, any-length vec #[derive( - Eq, PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize, Encode, Decode, + Eq, PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize, Encode, Decode, )] pub struct Leaf(pub Vec); impl From> for Leaf { - fn from(data: Vec) -> Self { - let mut hasher = sha3::Sha3_256::default(); - hasher.update(&data); - let hash = hasher.finalize(); - Leaf(hash.to_vec().into()) - } + fn from(data: Vec) -> Self { + let mut hasher = sha3::Sha3_256::default(); + hasher.update(&data); + let hash = hasher.finalize(); + Leaf(hash.to_vec().into()) + } } /// Merge leaves together with a sha256 hasher #[derive(Debug)] pub struct MergeLeaves; impl Merge for MergeLeaves { - type Item = Leaf; - fn merge(lhs: &Self::Item, rhs: &Self::Item) -> MMRResult { - let mut hasher = sha3::Sha3_256::default(); - hasher.update(&lhs.0); - hasher.update(&rhs.0); - let hash = hasher.finalize(); - Ok(Leaf(hash.to_vec().into())) - } + type Item = Leaf; + fn merge(lhs: &Self::Item, rhs: &Self::Item) -> MMRResult { + let mut hasher = sha3::Sha3_256::default(); + hasher.update(&lhs.0); + hasher.update(&rhs.0); + let hash = hasher.finalize(); + Ok(Leaf(hash.to_vec().into())) + } } /// Something that builds unique identities (e.g. using crypto hash function) for any block number pub trait IdentityBuilder { - fn build_identity(at: BlockNumber) -> Identity; + fn build_identity(at: BlockNumber) -> Identity; } diff --git a/lib/src/bin/murmur/main.rs b/lib/src/bin/murmur/main.rs index 5337c2d..50b4caa 100644 --- a/lib/src/bin/murmur/main.rs +++ b/lib/src/bin/murmur/main.rs @@ -16,11 +16,10 @@ use clap::{Parser, Subcommand}; use murmur_lib::{ - create, etf, idn_connect, prepare_execute, BlockNumber, BoundedVec, MurmurStore, RuntimeCall, + create, etf, idn_connect, prepare_execute, BlockNumber, BoundedVec, MurmurStore, RuntimeCall, }; use sp_core::crypto::Ss58Codec; -use std::fs::File; -use std::time::Instant; +use std::{fs::File, time::Instant}; use subxt_signer::sr25519::dev; use thiserror::Error; @@ -29,54 +28,54 @@ use thiserror::Error; #[command(author, version, about, long_about = None)] #[command(propagate_version = true)] struct Cli { - #[command(subcommand)] - commands: Commands, + #[command(subcommand)] + commands: Commands, } #[derive(Subcommand)] enum Commands { - /// create a new murmur wallet - New(WalletCreationDetails), - /// dispatch (proxy) a call to a murmur wallet - Execute(WalletExecuteDetails), + /// create a new murmur wallet + New(WalletCreationDetails), + /// dispatch (proxy) a call to a murmur wallet + Execute(WalletExecuteDetails), } #[derive(Parser)] struct WalletCreationDetails { - #[arg(long, short)] - name: String, - #[arg(long, short)] - seed: String, - #[clap(long, short)] - validity: u32, + #[arg(long, short)] + name: String, + #[arg(long, short)] + seed: String, + #[clap(long, short)] + validity: u32, } #[derive(Parser)] struct WalletExecuteDetails { - #[arg(long, short)] - name: String, - #[arg(long, short)] - seed: String, - #[arg(long, short)] - to: String, - #[arg(short, long, value_parser = clap::value_parser!(u128))] - amount: u128, + #[arg(long, short)] + name: String, + #[arg(long, short)] + seed: String, + #[arg(long, short)] + to: String, + #[arg(short, long, value_parser = clap::value_parser!(u128))] + amount: u128, } #[derive(Error, Debug)] pub enum CLIError { - #[error("invalid public key")] - InvalidPubkey, - #[error("invalid address")] - InvalidRecipient, - #[error("could not parse input to a u128")] - InvalidSendAmount, - #[error("something went wrong while creating the MMR")] - MurmurCreationFailed, - #[error("something went wrong while executing the MMR wallet")] - MurmurExecutionFailed, - #[error("the murmur store is corrupted or empty")] - CorruptedMurmurStore, + #[error("invalid public key")] + InvalidPubkey, + #[error("invalid address")] + InvalidRecipient, + #[error("could not parse input to a u128")] + InvalidSendAmount, + #[error("something went wrong while creating the MMR")] + MurmurCreationFailed, + #[error("something went wrong while executing the MMR wallet")] + MurmurExecutionFailed, + #[error("the murmur store is corrupted or empty")] + CorruptedMurmurStore, } /// the mmr_store file location @@ -85,103 +84,97 @@ pub const MMR_STORE_FILEPATH: &str = "mmr_store"; #[tokio::main] async fn main() -> Result<(), Box> { - let cli = Cli::parse(); - let before = Instant::now(); - - 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?; - } - } - println!("Elapsed time: {:.2?}", before.elapsed()); - Ok(()) + let cli = Cli::parse(); + let before = Instant::now(); + + 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?; + }, + } + println!("Elapsed time: {:.2?}", before.elapsed()); + Ok(()) } /// read an MMR from a file fn load_mmr_store(path: &str) -> Result { - let mmr_store_file = File::open(path).expect("Unable to open file"); - let data: MurmurStore = - serde_cbor::from_reader(mmr_store_file).map_err(|_| CLIError::CorruptedMurmurStore)?; - Ok(data) + let mmr_store_file = File::open(path).expect("Unable to open file"); + let data: MurmurStore = + serde_cbor::from_reader(mmr_store_file).map_err(|_| CLIError::CorruptedMurmurStore)?; + Ok(data) } /// Write the MMR data to a file fn write_mmr_store(mmr_store: MurmurStore, 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(); + let mmr_store_file = File::create(path).expect("It should create the file"); + serde_cbor::to_writer(mmr_store_file, &mmr_store).unwrap(); } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 9d26338..633cb60 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -19,17 +19,17 @@ use hkdf::Hkdf; use murmur_core::types::{Identity, IdentityBuilder}; use serde::Serialize; use subxt::{ - backend::rpc::RpcClient, client::OnlineClient, config::SubstrateConfig, ext::codec::Encode, + backend::rpc::RpcClient, client::OnlineClient, config::SubstrateConfig, ext::codec::Encode, }; use w3f_bls::{DoublePublicKey, SerializableToBytes, TinyBLS377}; use zeroize::Zeroize; pub use etf::runtime_types::{ - bounded_collections::bounded_vec::BoundedVec, node_template_runtime::RuntimeCall, + bounded_collections::bounded_vec::BoundedVec, node_template_runtime::RuntimeCall, }; pub use murmur_core::{ - murmur::{Error, MurmurStore}, - types::BlockNumber, + murmur::{Error, MurmurStore}, + types::BlockNumber, }; // Generate an interface that we can use from the node's metadata. @@ -40,36 +40,37 @@ pub mod etf {} #[derive(Debug)] pub struct BasicIdBuilder; impl IdentityBuilder for BasicIdBuilder { - fn build_identity(when: BlockNumber) -> Identity { - let payload = Payload::from_single_entry(known_payloads::ETF_SIGNATURE, Vec::new()); - 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. - }; - Identity::new(&commitment.encode()) - } + fn build_identity(when: BlockNumber) -> Identity { + let payload = Payload::from_single_entry(known_payloads::ETF_SIGNATURE, Vec::new()); + 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. */ + }; + Identity::new(&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, + /// 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, - pub ciphertext: Vec, - pub proof_items: Vec>, - pub size: u64, + pub position: u64, + /// The hash of the commitment + pub hash: Vec, + pub ciphertext: Vec, + pub proof_items: Vec>, + pub size: u64, } /// Create a new MMR and return the data needed to build a valid call for creating a murmur wallet. @@ -77,35 +78,30 @@ pub struct ProxyData { /// * `seed`: The seed used to generate otp codes /// * `block_schedule`: A list of block numbers when the wallet will be executable /// * `round_pubkey_bytes`: The Ideal Network randomness beacon public key -/// pub fn create( - mut seed: Vec, - block_schedule: Vec, - round_pubkey_bytes: Vec, + mut seed: Vec, + 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)?; - - let round_pubkey = DoublePublicKey::::from_bytes(&round_pubkey_bytes) - .map_err(|_| Error::InvalidPubkey)?; - let mmr_store = MurmurStore::new::( - seed.clone(), - block_schedule.clone(), - ephem_msk, - round_pubkey, - )?; - 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, - }) + // 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)?; + + let round_pubkey = DoublePublicKey::::from_bytes(&round_pubkey_bytes) + .map_err(|_| Error::InvalidPubkey)?; + let mmr_store = MurmurStore::new::( + seed.clone(), + block_schedule.clone(), + ephem_msk, + round_pubkey, + )?; + 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 }) } /// Return the data needed for the immediate execution of the proxied call. @@ -113,37 +109,27 @@ 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 +// 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, - call: &RuntimeCall, + mut seed: Vec, + when: BlockNumber, + store: MurmurStore, + call: &RuntimeCall, ) -> Result { - let (proof, commitment, ciphertext, pos) = store.execute(seed.clone(), when, call.encode())?; - seed.zeroize(); - let size = proof.mmr_size(); - let proof_items: Vec> = proof - .proof_items() - .iter() - .map(|leaf| leaf.0.clone()) - .collect::>(); + let (proof, commitment, ciphertext, pos) = store.execute(seed.clone(), when, call.encode())?; + seed.zeroize(); + let size = proof.mmr_size(); + let proof_items: Vec> = + proof.proof_items().iter().map(|leaf| leaf.0.clone()).collect::>(); - Ok(ProxyData { - position: pos, - hash: commitment, - ciphertext, - proof_items, - size, - }) + Ok(ProxyData { position: pos, hash: commitment, ciphertext, proof_items, size }) } /// Async connection to the Ideal Network @@ -151,111 +137,91 @@ pub fn prepare_execute( /// 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)) + 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() { - 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); - } - - #[test] - pub fn it_can_prepare_valid_execution_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, 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::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 size = proof.mmr_size(); - let proof_items: Vec> = proof - .proof_items() - .iter() - .map(|leaf| leaf.0.clone()) - .collect::>(); - - 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); - } + use super::*; + + #[test] + pub fn it_can_create_an_mmr_store() { + 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); + } + + #[test] + pub fn it_can_prepare_valid_execution_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, 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::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 size = proof.mmr_size(); + let proof_items: Vec> = + proof.proof_items().iter().map(|leaf| leaf.0.clone()).collect::>(); + + 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); + } } diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 69a239d..0e8e54d 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -18,10 +18,10 @@ //! various utilities helpful for testing -use w3f_bls::{DoublePublicKey, DoublePublicKeyScheme, TinyBLS377}; -use rand_core::OsRng; -use ark_serialize::CanonicalSerialize; use alloc::vec::Vec; +use ark_serialize::CanonicalSerialize; +use rand_core::OsRng; +use w3f_bls::{DoublePublicKey, DoublePublicKeyScheme, TinyBLS377}; extern crate alloc; @@ -30,12 +30,10 @@ pub use murmur_core::otp::BOTPGenerator; pub use murmur_core::murmur::MurmurStore; pub fn get_dummy_beacon_pubkey() -> Vec { - 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(); - bytes -} \ No newline at end of file + 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(); + bytes +}