diff --git a/substrate/client/serai/Cargo.toml b/substrate/client/serai/Cargo.toml index f4036fd5..d5ff35eb 100644 --- a/substrate/client/serai/Cargo.toml +++ b/substrate/client/serai/Cargo.toml @@ -29,6 +29,8 @@ serai-abi = { path = "../../abi", version = "0.1" } async-lock = "3" [dev-dependencies] +blake2 = { version = "0.11.0-rc.3", default-features = false } + tokio = { version = "1", default-features = false, features = ["rt", "macros"] } dockertest = "0.5" serai-docker-tests = { path = "../../../tests/docker" } diff --git a/substrate/client/serai/src/lib.rs b/substrate/client/serai/src/lib.rs index a396f4c1..6797a36f 100644 --- a/substrate/client/serai/src/lib.rs +++ b/substrate/client/serai/src/lib.rs @@ -59,7 +59,7 @@ pub struct Serai { pub struct TemporalSerai<'a> { serai: &'a Serai, block: BlockHash, - events: Arc>>>, + events: Arc>>>>, } impl Serai { @@ -195,7 +195,9 @@ impl<'a> TemporalSerai<'a> { /// Fetch the events for this block. /// /// The returned `Option` will always be `Some(_)`. - async fn events(&self) -> Result>>, RpcError> { + async fn events_borrowed( + &self, + ) -> Result>>>, RpcError> { let mut events = self.events.read().await; if events.is_none() { drop(events); @@ -204,20 +206,25 @@ impl<'a> TemporalSerai<'a> { if events_mut.is_none() { *events_mut = Some( self - .call::>("blockchain/events", "") + .call::>>("blockchain/events", "") .await? .into_iter() - .map(|event| { - Event::deserialize( - &mut hex::decode(&event) - .map_err(|_| { - RpcError::InvalidNode("node returned non-hex-encoded event".to_string()) - })? - .as_slice(), - ) - .map_err(|_| RpcError::InvalidNode("node returned invalid event".to_string())) + .map(|events_per_tx| { + events_per_tx + .into_iter() + .map(|event| { + Event::deserialize( + &mut hex::decode(&event) + .map_err(|_| { + RpcError::InvalidNode("node returned non-hex-encoded event".to_string()) + })? + .as_slice(), + ) + .map_err(|_| RpcError::InvalidNode("node returned invalid event".to_string())) + }) + .collect::, _>>() }) - .collect::>()?, + .collect::, _>>()?, ); } } @@ -226,6 +233,14 @@ impl<'a> TemporalSerai<'a> { Ok(events) } + /// Fetch the events for this block. + /// + /// These will be grouped by the transactions which emitted them, including the inherent + /// transactions at the start and end of every block. + pub async fn events(&self) -> Result>, RpcError> { + Ok(self.events_borrowed().await?.clone().expect("`TemporalSerai::events` returned None")) + } + /// Scope to the validator sets module. pub fn validator_sets(&self) -> ValidatorSets<'_> { ValidatorSets(self) diff --git a/substrate/client/serai/src/validator_sets.rs b/substrate/client/serai/src/validator_sets.rs index 80623838..8c4e440b 100644 --- a/substrate/client/serai/src/validator_sets.rs +++ b/substrate/client/serai/src/validator_sets.rs @@ -32,11 +32,12 @@ impl<'a> ValidatorSets<'a> { Ok( self .0 - .events() + .events_borrowed() .await? .as_ref() .expect("`TemporalSerai::events` returned None") .iter() + .flat_map(IntoIterator::into_iter) .filter_map(|event| match event { serai_abi::Event::ValidatorSets(event) => Some(event.clone()), _ => None, @@ -127,11 +128,7 @@ impl<'a> ValidatorSets<'a> { .0 .call::>( "validator-sets/keys", - &format!( - r#", "network": {}, "session": {} "#, - rpc_network(set.network)?, - set.session.0 - ), + &format!(r#", "network": {}, "session": {} "#, rpc_network(set.network)?, set.session.0), ) .await? else { diff --git a/substrate/client/serai/tests/blockchain.rs b/substrate/client/serai/tests/blockchain.rs index 78866aa3..0c9a3bbf 100644 --- a/substrate/client/serai/tests/blockchain.rs +++ b/substrate/client/serai/tests/blockchain.rs @@ -1,3 +1,14 @@ +use std::collections::HashSet; + +use blake2::{Digest, Blake2b256}; + +use serai_abi::{ + primitives::merkle::UnbalancedMerkleTree, BLOCK_HEADER_LEAF_TAG, BLOCK_HEADER_BRANCH_TAG, + TRANSACTION_COMMITMENT_LEAF_TAG, TRANSACTION_COMMITMENT_BRANCH_TAG, + TRANSACTION_EVENTS_COMMITMENT_LEAF_TAG, TRANSACTION_EVENTS_COMMITMENT_BRANCH_TAG, + EVENTS_COMMITMENT_LEAF_TAG, EVENTS_COMMITMENT_BRANCH_TAG, +}; + use serai_client_serai::*; #[tokio::test] @@ -91,6 +102,99 @@ async fn blockchain() { test_finalized_block(next_finalized).await; } + // Check the blocks have the expected headers + { + let mut last_block_number = serai.latest_finalized_block_number().await.unwrap(); + let mut observed_consensus_commitments = HashSet::new(); + let mut tagged_block_hashes = vec![]; + for i in 0 ..= last_block_number { + let block = serai.block_by_number(i).await.unwrap(); + + assert_eq!(block.header.number(), i); + + { + assert_eq!( + UnbalancedMerkleTree::new(BLOCK_HEADER_BRANCH_TAG, tagged_block_hashes.clone()).root, + block.header.builds_upon().root, + ); + tagged_block_hashes.push({ + let mut tagged = vec![BLOCK_HEADER_LEAF_TAG]; + tagged.extend(&block.header.hash().0); + Blake2b256::digest(tagged).into() + }); + } + + { + let mut start_transaction = [0; 32]; + start_transaction[24 ..].copy_from_slice(&i.to_be_bytes()); + let mut end_transaction = start_transaction; + end_transaction[.. 16].copy_from_slice(&[0xff; 16]); + let transactions_iter = core::iter::once(start_transaction) + .chain(block.transactions.iter().map(serai_abi::Transaction::hash)) + .chain(core::iter::once(end_transaction)); + + let events = serai.as_of(block.header.hash()).await.unwrap().events().await.unwrap(); + assert_eq!(events.len(), 2 + block.transactions.len()); + + let mut transaction_leaves = vec![]; + let mut events_leaves = vec![]; + for (transaction, events) in transactions_iter.zip(events) { + { + let mut tagged = vec![TRANSACTION_COMMITMENT_LEAF_TAG]; + tagged.extend(&transaction); + transaction_leaves.push(Blake2b256::digest(tagged).into()); + } + { + let events = UnbalancedMerkleTree::new( + TRANSACTION_EVENTS_COMMITMENT_BRANCH_TAG, + events + .into_iter() + .map(|event| { + let mut tagged = vec![TRANSACTION_EVENTS_COMMITMENT_LEAF_TAG]; + tagged.extend(&borsh::to_vec(&event).unwrap()); + Blake2b256::digest(tagged).into() + }) + .collect(), + ) + .root; + + let mut tagged = vec![EVENTS_COMMITMENT_LEAF_TAG]; + tagged.extend(&transaction); + tagged.extend(&events); + events_leaves.push(Blake2b256::digest(tagged).into()); + } + } + assert_eq!( + UnbalancedMerkleTree::new(TRANSACTION_COMMITMENT_BRANCH_TAG, transaction_leaves).root, + block.header.transactions_commitment().root + ); + assert_eq!( + UnbalancedMerkleTree::new(EVENTS_COMMITMENT_BRANCH_TAG, events_leaves).root, + block.header.events_commitment().root + ); + } + + match block.header { + serai_abi::Header::V1(serai_abi::HeaderV1 { + unix_time_in_millis, + consensus_commitment, + .. + }) => { + if i == 0 { + assert_eq!(unix_time_in_millis, 0); + } else { + assert!(unix_time_in_millis != 0); + } + + // We treat the `consensus_commitment` as opaque, but we do want to make sure it's set + // This check practically ensures it's being properly defined for each block + assert!(!observed_consensus_commitments.contains(&consensus_commitment)); + observed_consensus_commitments.insert(consensus_commitment); + } + } + } + } + println!("Finished `serai-client/blockchain` test"); }) .await; diff --git a/substrate/coins/src/tests.rs b/substrate/coins/src/tests.rs index f6cdc8f4..35bd3628 100644 --- a/substrate/coins/src/tests.rs +++ b/substrate/coins/src/tests.rs @@ -29,6 +29,7 @@ fn mint() { // test events let mint_events = Core::events() .iter() + .flat_map(IntoIterator::into_iter) .map(|event| borsh::from_slice::(event.as_slice()).unwrap()) .filter_map(|event| { if let serai_abi::Event::Coins(e) = &event { @@ -83,6 +84,7 @@ fn burn_with_instruction() { let burn_events = Core::events() .iter() + .flat_map(IntoIterator::into_iter) .map(|event| borsh::from_slice::(event.as_slice()).unwrap()) .filter_map(|event| { if let serai_abi::Event::Coins(e) = &event { diff --git a/substrate/core/src/lib.rs b/substrate/core/src/lib.rs index fd43f781..332d7b0c 100644 --- a/substrate/core/src/lib.rs +++ b/substrate/core/src/lib.rs @@ -14,7 +14,7 @@ pub use iumt::*; #[expect(clippy::cast_possible_truncation)] #[frame_support::pallet] pub mod pallet { - use alloc::vec::Vec; + use alloc::{vec::Vec, vec}; use frame_support::{ sp_runtime::traits::{Header, Block}, @@ -75,6 +75,8 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { + /// A transaction begun. + BeginTransaction, /// An event from Serai. Event(Vec), } @@ -130,6 +132,7 @@ pub mod pallet { /// The caller MUST ensure two transactions aren't simultaneously started. pub fn start_transaction() { TransactionEventsMerkle::::new_expecting_none(); + Self::deposit_event(Event::BeginTransaction); } /// Emit an event. @@ -150,19 +153,24 @@ pub mod pallet { BlockEventsCommitmentMerkle::::append(&(&transaction_hash, &transaction_events_root)); } - /// Fetch all of Serai's events. + /// Fetch all of Serai's events for each transaction. /// /// This MUST NOT be called during a transaction/block's execution. - pub fn events() -> Vec> + pub fn events() -> Vec>> where T::RuntimeEvent: TryInto>, { - frame_system::Pallet::::read_events_no_consensus() - .filter_map(|e| match e.event.try_into() { - Ok(Event::Event(bytes)) => Some(bytes), - _ => None, - }) - .collect() + let mut result = vec![]; + for event in frame_system::Pallet::::read_events_no_consensus() { + match event.event.try_into() { + Ok(Event::BeginTransaction) => result.push(vec![]), + Ok(Event::Event(bytes)) => { + result.last_mut().expect("Serai event outside of a transaction").push(bytes) + } + Err(_) => {} + } + } + result } } } diff --git a/substrate/node/src/rpc/blockchain.rs b/substrate/node/src/rpc/blockchain.rs index 2c5e6bd8..58621eec 100644 --- a/substrate/node/src/rpc/blockchain.rs +++ b/substrate/node/src/rpc/blockchain.rs @@ -66,7 +66,12 @@ pub(crate) fn module< let Ok(events) = client.runtime_api().events(block_hash) else { Err(Error::Missing("couldn't fetch the events for the requested block"))? }; - Ok(events.into_iter().map(hex::encode).collect::>()) + Ok( + events + .into_iter() + .map(|events_per_tx| events_per_tx.into_iter().map(hex::encode).collect::>()) + .collect::>(), + ) })?; Ok(module) diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index c27df819..4129c27c 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -34,7 +34,7 @@ sp_api::decl_runtime_apis! { fn build(genesis: GenesisConfig); } pub trait SeraiApi { - fn events() -> Vec>; + fn events() -> Vec>>; fn validators(network: NetworkId) -> Vec; fn current_session(network: NetworkId) -> Option; fn current_stake(network: NetworkId) -> Option; @@ -179,7 +179,7 @@ mod apis { } impl crate::SeraiApi for Runtime { - fn events() -> Vec> { + fn events() -> Vec>> { unimplemented!("runtime is only implemented when WASM") } fn validators( diff --git a/substrate/runtime/src/wasm/mod.rs b/substrate/runtime/src/wasm/mod.rs index eed826eb..28bb816c 100644 --- a/substrate/runtime/src/wasm/mod.rs +++ b/substrate/runtime/src/wasm/mod.rs @@ -550,7 +550,7 @@ sp_api::impl_runtime_apis! { } impl crate::SeraiApi for Runtime { - fn events() -> Vec> { + fn events() -> Vec>> { Core::events() } fn validators(network: NetworkId) -> Vec {