diff --git a/Cargo.lock b/Cargo.lock index bf8e8a54..5eac6601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9127,6 +9127,7 @@ dependencies = [ name = "serai-runtime" version = "0.1.0" dependencies = [ + "borsh", "frame-executive", "frame-support", "frame-system", diff --git a/substrate/abi/src/block.rs b/substrate/abi/src/block.rs index 3f1f1644..2afce2fc 100644 --- a/substrate/abi/src/block.rs +++ b/substrate/abi/src/block.rs @@ -8,14 +8,14 @@ use crate::{ }; /// The tag for the hash of a transaction's event, forming a leaf of the Merkle tree of its events. -pub const EVENTS_COMMITMENT_TRANSACTION_EVENT_TAG: u8 = 0; +pub const TRANSACTION_EVENTS_COMMITMENT_LEAF_TAG: u8 = 0; /// The tag for the branch hashes of transaction events. -pub const EVENTS_COMMITMENT_TRANSACTION_EVENTS_TAG: u8 = 1; +pub const TRANSACTION_EVENTS_COMMITMENT_BRANCH_TAG: u8 = 1; /// The tag for the hash of a transaction's hash and its events' Merkle root, forming a leaf of the /// Merkle tree which is the events commitment. -pub const EVENTS_COMMITMENT_TRANSACTION_TAG: u8 = 2; +pub const EVENTS_COMMITMENT_LEAF_TAG: u8 = 2; /// The tag for for the branch hashes of the Merkle tree which is the events commitments. -pub const EVENTS_COMMITMENT_TRANSACTIONS_TAG: u8 = 3; +pub const EVENTS_COMMITMENT_BRANCH_TAG: u8 = 3; /// A V1 header for a block. #[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] @@ -25,13 +25,27 @@ pub struct HeaderV1 { /// The genesis block has number 0. pub number: u64, /// The commitment to the DAG this header builds upon. - pub builds_upon: BlockHash, + /// + /// This is defined as an unbalanced Merkle tree so light clients may sync one header per epoch, + /// and then may prove the inclusion of any header in logarithmic depth (without providing the + /// entire header chain). + /// + /// Alternative popular options would be a Merkle Mountain Range, which makes more recent blocks + /// cheaper to prove at the sacrifice of older blocks being more expensive to prove. An MMR isn't + /// used in order to minimize the protocol's surface area. Additionally, even though the + /// unbalanced Merkle tree doesn't achieve such notably short paths for recent blocks, it does + /// inherently provide lower-depth paths to more recent items *on imbalance*. + pub builds_upon: UnbalancedMerkleTree, /// The UNIX time in milliseconds this block was created at. pub unix_time_in_millis: u64, /// The commitment to the transactions within this block. - // TODO: Some transactions don't have unique hashes due to assuming validators set unique keys - pub transactions_commitment: [u8; 32], + pub transactions_commitment: UnbalancedMerkleTree, /// The commitment to the events within this block. + /// + /// The leaves of this tree will be of the form + /// `(EVENTS_COMMITMENT_LEAF_TAG, transaction hash, transaction's events' Merkle tree root)`. + /// A transaction may have the same event multiple times, yet an event may be uniquely identified + /// by its path within the tree. pub events_commitment: UnbalancedMerkleTree, /// A commitment to the consensus data used to justify adding this block to the blockchain. pub consensus_commitment: [u8; 32], @@ -52,17 +66,23 @@ impl Header { } } /// Get the commitment to the DAG this header builds upon. - pub fn builds_upon(&self) -> BlockHash { + pub fn builds_upon(&self) -> UnbalancedMerkleTree { match self { Header::V1(HeaderV1 { builds_upon, .. }) => *builds_upon, } } /// The commitment to the transactions within this block. - pub fn transactions_commitment(&self) -> [u8; 32] { + pub fn transactions_commitment(&self) -> UnbalancedMerkleTree { match self { Header::V1(HeaderV1 { transactions_commitment, .. }) => *transactions_commitment, } } + /// The commitment to the events within this block. + pub fn events_commitment(&self) -> UnbalancedMerkleTree { + match self { + Header::V1(HeaderV1 { events_commitment, .. }) => *events_commitment, + } + } /// Get the hash of the header. pub fn hash(&self) -> BlockHash { BlockHash(sp_core::blake2_256(&borsh::to_vec(self).unwrap())) @@ -96,21 +116,33 @@ mod substrate { use super::*; - /// The digest for all of the Serai-specific header fields. + /// The digest for all of the Serai-specific header fields added before execution of the block. #[derive(Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)] - pub struct SeraiDigest { - /// The commitment to the DAG this header builds upon. - pub builds_upon: BlockHash, + pub struct SeraiPreExecutionDigest { /// The UNIX time in milliseconds this block was created at. pub unix_time_in_millis: u64, + } + + /// The digest for all of the Serai-specific header fields determined during execution of the + /// block. + #[derive(Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)] + pub struct SeraiExecutionDigest { + /// The commitment to the DAG this header builds upon. + pub builds_upon: UnbalancedMerkleTree, /// The commitment to the transactions within this block. - pub transactions_commitment: [u8; 32], + pub transactions_commitment: UnbalancedMerkleTree, /// The commitment to the events within this block. pub events_commitment: UnbalancedMerkleTree, } - impl SeraiDigest { - const CONSENSUS_ID: [u8; 4] = *b"SRID"; + impl SeraiPreExecutionDigest { + /// The consensus ID for a Serai pre-execution digest. + pub const CONSENSUS_ID: [u8; 4] = *b"SRIP"; + } + + impl SeraiExecutionDigest { + /// The consensus ID for a Serai execution digest. + pub const CONSENSUS_ID: [u8; 4] = *b"SRIE"; } /// The consensus data for a V1 header. @@ -149,34 +181,43 @@ mod substrate { fn from(header: &SubstrateHeader) -> Self { match header { SubstrateHeader::V1(header) => { - let digest = - header.consensus.digest.logs().iter().find_map(|digest_item| match digest_item { + let mut pre_execution_digest = None; + let mut execution_digest = None; + for log in header.consensus.digest.logs() { + match log { DigestItem::PreRuntime(consensus, encoded) - if *consensus == SeraiDigest::CONSENSUS_ID => + if *consensus == SeraiExecutionDigest::CONSENSUS_ID => { - SeraiDigest::deserialize_reader(&mut encoded.as_slice()).ok() + pre_execution_digest = + SeraiPreExecutionDigest::deserialize_reader(&mut encoded.as_slice()).ok(); } - _ => None, - }); + DigestItem::Consensus(consensus, encoded) + if *consensus == SeraiExecutionDigest::CONSENSUS_ID => + { + execution_digest = + SeraiExecutionDigest::deserialize_reader(&mut encoded.as_slice()).ok(); + } + _ => {} + } + } Header::V1(HeaderV1 { number: header.number, - builds_upon: digest + builds_upon: execution_digest .as_ref() .map(|digest| digest.builds_upon) - .unwrap_or(BlockHash::from([0; 32])), - unix_time_in_millis: digest + .unwrap_or(UnbalancedMerkleTree::EMPTY), + unix_time_in_millis: pre_execution_digest .as_ref() .map(|digest| digest.unix_time_in_millis) .unwrap_or(0), - transactions_commitment: digest + transactions_commitment: execution_digest .as_ref() .map(|digest| digest.transactions_commitment) - .unwrap_or([0; 32]), - events_commitment: digest + .unwrap_or(UnbalancedMerkleTree::EMPTY), + events_commitment: execution_digest .as_ref() .map(|digest| digest.events_commitment) .unwrap_or(UnbalancedMerkleTree::EMPTY), - // TODO: This hashes the digest *including seals*, doesn't it? consensus_commitment: sp_core::blake2_256(&header.consensus.encode()), }) } diff --git a/substrate/primitives/src/merkle.rs b/substrate/primitives/src/merkle.rs index 0e56d523..65a2a649 100644 --- a/substrate/primitives/src/merkle.rs +++ b/substrate/primitives/src/merkle.rs @@ -64,84 +64,6 @@ impl UnbalancedMerkleTree { } Self { root: current[0] } } - - /// Calculate the Merkle tree root for a list of hashes, passed in as their SCALE encoding. - /// - /// This method does not perform any allocations and is quite optimized. It is intended to be - /// called from within the Substrate runtime, a resource-constrained environment. It does take in - /// an owned Vec, despite solely using it as a mutable slice, due to the trashing of its content. - /// - /// Please see the documentation of `UnbalancedMerkleTree` and `UnbalancedMerkleTree::new` for - /// context on structure. - /// - /// A SCALE encoding will be length-prefixed with a Compact number per - /// https://docs.polkadot.com/polkadot-protocol/basics/data-encoding/#data-types. - #[doc(hidden)] - pub fn from_scale_encoded_list_of_hashes(tag: u8, encoding: Vec<u8>) -> Self { - let mut hashes = encoding; - - // Learn the length of the length prefix - let length_prefix_len = { - let mut slice = hashes.as_slice(); - <scale::Compact<u32> as scale::Decode>::skip(&mut slice).unwrap(); - hashes.len() - slice.len() - }; - - // We calculate the hashes in-place to avoid redundant allocations - let mut hashes = hashes.as_mut_slice(); - - let mut amount_of_hashes; - while { - amount_of_hashes = (hashes.len() - length_prefix_len) / 32; - amount_of_hashes > 1 - } { - let complete_pairs = amount_of_hashes / 2; - for i in 0 .. complete_pairs { - // We hash the i'th pair of 32-byte elements - let hash = { - // The starting position of these elements - let start = length_prefix_len + ((2 * i) * 32); - /* - We write the tag to the byte before this pair starts. - - In the case of the first pair, this corrupts a byte of the length prefix. - - In the case of the nth pair, this corrupts the prior-hashed pair's second element. - This is safe as it was already hashed and the data there won't be read again. While - we do write, and later read, the carried hash outputs to this buffer, those will - always be written to either a pair's first element or a (n * prior-)hashed pair's - second element (where n > 2), never the immediately preceding pair's second element. - */ - hashes[start - 1] = tag; - sp_core::blake2_256(&hashes[(start - 1) .. (start + 64)]) - }; - // We save this hash to the i'th position - { - let start = length_prefix_len + (i * 32); - hashes[start .. (start + 32)].copy_from_slice(hash.as_slice()); - } - } - - let mut end_of_hashes_on_next_layer = length_prefix_len + (complete_pairs * 32); - - // If there was an odd hash which wasn't hashed on this layer, carry it - if (amount_of_hashes % 2) == 1 { - let mut hash = [0xff; 32]; - hash.copy_from_slice(&hashes[(hashes.len() - 32) ..]); - - let start = end_of_hashes_on_next_layer; - end_of_hashes_on_next_layer = start + 32; - hashes[start .. end_of_hashes_on_next_layer].copy_from_slice(&hash); - } - - hashes = &mut hashes[.. end_of_hashes_on_next_layer]; - } - - match hashes[length_prefix_len ..].try_into() { - Ok(root) => Self { root }, - Err(_) => Self::EMPTY, - } - } } /// An unbalanced Merkle tree which is incrementally created. @@ -177,6 +99,8 @@ impl IncrementalUnbalancedMerkleTree { /// Append a leaf to this merkle tree. /// /// The conditions on this leaf are the same as defined by `UnbalancedMerkleTree::new`. + /// + /// This will not calculate any hashes not necessary for the eventual root. pub fn append(&mut self, tag: u8, leaf: [u8; 32]) { self.branches.push((1, leaf)); self.reduce(tag); diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index 0400ab1d..ca89da83 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -19,6 +19,8 @@ ignored = ["scale", "scale-info"] workspace = true [dependencies] +borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] } + scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"] } diff --git a/substrate/runtime/src/core_pallet.rs b/substrate/runtime/src/core_pallet.rs index 6a311fc9..5ef17b85 100644 --- a/substrate/runtime/src/core_pallet.rs +++ b/substrate/runtime/src/core_pallet.rs @@ -1,18 +1,172 @@ -#[frame_support::pallet] -mod core_pallet { - use ::alloc::*; - use frame_support::pallet_prelude::*; +use core::marker::PhantomData; +use alloc::{vec, vec::Vec}; +use borsh::{BorshSerialize, BorshDeserialize}; + +use frame_support::pallet_prelude::*; + +use serai_abi::{ + primitives::merkle::{UnbalancedMerkleTree, IncrementalUnbalancedMerkleTree as Iumt}, + *, +}; + +struct IncrementalUnbalancedMerkleTree< + T: frame_support::StorageValue<Vec<u8>, Query = Option<Vec<u8>>>, + const BRANCH_TAG: u8 = 1, + const LEAF_TAG: u8 = 0, +>(PhantomData<T>); +impl< + T: frame_support::StorageValue<Vec<u8>, Query = Option<Vec<u8>>>, + const BRANCH_TAG: u8, + const LEAF_TAG: u8, + > IncrementalUnbalancedMerkleTree<T, BRANCH_TAG, LEAF_TAG> +{ + /// Create a new Merkle tree, expecting there to be none already present. + /// + /// Panics if a Merkle tree was already present. + fn new_expecting_none() { + T::mutate(|value| { + assert!(value.is_none()); + *value = Some(borsh::to_vec(&Iumt::new()).unwrap()); + }); + } + /// Append a leaf to the Merkle tree. + /// + /// Panics if no Merkle tree was present. + fn append<L: BorshSerialize>(leaf: &L) { + let leaf = sp_core::blake2_256(&borsh::to_vec(&(LEAF_TAG, leaf)).unwrap()); + + T::mutate(|value| { + let mut tree = Iumt::deserialize_reader(&mut value.as_ref().unwrap().as_slice()).unwrap(); + tree.append(BRANCH_TAG, leaf); + *value = Some(borsh::to_vec(&tree).unwrap()); + }) + } + /// Get the unbalanced merkle tree. + /// + /// Panics if no Merkle tree was present. + fn get() -> UnbalancedMerkleTree { + Iumt::deserialize_reader(&mut T::get().unwrap().as_slice()).unwrap().calculate(BRANCH_TAG) + } + /// Take the Merkle tree. + /// + /// Panics if no Merkle tree was present. + fn take() -> UnbalancedMerkleTree { + T::mutate(|value| { + let tree = Iumt::deserialize_reader(&mut value.as_ref().unwrap().as_slice()).unwrap(); + *value = None; + tree.calculate(BRANCH_TAG) + }) + } +} + +#[frame_support::pallet] +mod pallet { + use super::*; + + /// The set of all blocks prior added to the blockchain. + #[pallet::storage] + pub type Blocks<T: Config> = StorageMap<_, Identity, T::Hash, (), OptionQuery>; + /// The Merkle tree of all blocks added to the blockchain. + #[pallet::storage] + #[pallet::unbounded] + pub(super) type BlocksCommitment<T: Config> = StorageValue<_, Vec<u8>, OptionQuery>; + pub(super) type BlocksCommitmentMerkle<T> = IncrementalUnbalancedMerkleTree<BlocksCommitment<T>>; + + /// The Merkle tree of all transactions within the current block. + #[pallet::storage] + #[pallet::unbounded] + pub(super) type BlockTransactionsCommitment<T: Config> = StorageValue<_, Vec<u8>, OptionQuery>; + pub(super) type BlockTransactionsCommitmentMerkle<T> = + IncrementalUnbalancedMerkleTree<BlockTransactionsCommitment<T>>; + + /// The hashes of events caused by the current transaction. + #[pallet::storage] + #[pallet::unbounded] + pub(super) type TransactionEvents<T: Config> = StorageValue<_, Vec<u8>, OptionQuery>; + pub(super) type TransactionEventsMerkle<T> = IncrementalUnbalancedMerkleTree< + TransactionEvents<T>, + TRANSACTION_EVENTS_COMMITMENT_BRANCH_TAG, + TRANSACTION_EVENTS_COMMITMENT_LEAF_TAG, + >; + /// The roots of the Merkle trees of each transaction's events. + #[pallet::storage] + #[pallet::unbounded] + pub(super) type BlockEventsCommitment<T: Config> = StorageValue<_, Vec<u8>, OptionQuery>; + pub(super) type BlockEventsCommitmentMerkle<T> = IncrementalUnbalancedMerkleTree< + BlockEventsCommitment<T>, + EVENTS_COMMITMENT_BRANCH_TAG, + EVENTS_COMMITMENT_LEAF_TAG, + >; + + /// A mapping from an account to its next nonce. #[pallet::storage] pub type NextNonce<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, T::Nonce, ValueQuery>; - #[pallet::storage] - pub type Blocks<T: Config> = StorageMap<_, Identity, T::Hash, (), OptionQuery>; #[pallet::config] - pub trait Config: frame_system::Config {} + pub trait Config: + frame_system::Config< + Block: sp_runtime::traits::Block<Header: sp_runtime::traits::Header<Hash: Into<[u8; 32]>>>, + > + { + } #[pallet::pallet] pub struct Pallet<T>(_); + + impl<T: Config> Pallet<T> { + pub fn start_transaction() { + TransactionEventsMerkle::<T>::new_expecting_none(); + } + // TODO: Have this called + pub fn on_event(event: impl TryInto<serai_abi::Event>) { + if let Ok(event) = event.try_into() { + TransactionEventsMerkle::<T>::append(&event); + } + } + pub fn end_transaction(transaction_hash: [u8; 32]) { + BlockTransactionsCommitmentMerkle::<T>::append(&transaction_hash); + + let transaction_events_root = TransactionEventsMerkle::<T>::take().root; + + // Append the leaf (the transaction's hash and its events' root) to the block's events' + // commitment + BlockEventsCommitmentMerkle::<T>::append(&(&transaction_hash, &transaction_events_root)); + } + } +} +pub(super) use pallet::*; + +pub struct StartOfBlock<T: Config>(PhantomData<T>); +impl<T: Config> frame_support::traits::PreInherents for StartOfBlock<T> { + fn pre_inherents() { + let parent_hash = frame_system::Pallet::<T>::parent_hash(); + Blocks::<T>::set(parent_hash, Some(())); + // TODO: Better detection of genesis + if parent_hash == Default::default() { + BlocksCommitmentMerkle::<T>::new_expecting_none(); + } else { + let parent_hash: [u8; 32] = parent_hash.into(); + BlocksCommitmentMerkle::<T>::append(&parent_hash); + } + + BlockTransactionsCommitmentMerkle::<T>::new_expecting_none(); + BlockEventsCommitmentMerkle::<T>::new_expecting_none(); + } +} + +pub struct EndOfBlock<T: Config>(PhantomData<T>); +impl<T: Config> frame_support::traits::PostTransactions for EndOfBlock<T> { + fn post_transactions() { + frame_system::Pallet::<T>::deposit_log(sp_runtime::generic::DigestItem::Consensus( + SeraiExecutionDigest::CONSENSUS_ID, + borsh::to_vec(&SeraiExecutionDigest { + builds_upon: BlocksCommitmentMerkle::<T>::get(), + transactions_commitment: BlockTransactionsCommitmentMerkle::<T>::take(), + events_commitment: BlockEventsCommitmentMerkle::<T>::take(), + }) + .unwrap(), + )); + } } -pub(super) use core_pallet::*; diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index c2376018..8dde5fb8 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -110,10 +110,10 @@ impl frame_system::Config for Runtime { // No migrations set type SingleBlockMigrations = (); type MultiBlockMigrator = (); - // We don't define any block-level hooks at this time - type PreInherents = (); + + type PreInherents = core_pallet::StartOfBlock<Runtime>; type PostInherents = (); - type PostTransactions = (); + type PostTransactions = core_pallet::EndOfBlock<Runtime>; } impl core_pallet::Config for Runtime {} @@ -226,16 +226,9 @@ impl serai_abi::TransactionContext for Context { fn current_time(&self) -> Option<u64> { todo!("TODO") } - /// Get, and consume, the next nonce for an account. - fn get_and_consume_next_nonce(&self, signer: &SeraiAddress) -> u32 { - core_pallet::NextNonce::<Runtime>::mutate(signer, |value| { - // Copy the current value for the next nonce - let next_nonce = *value; - // Increment the next nonce in the DB, consuming the current value - *value += 1; - // Return the existing value - next_nonce - }) + /// Get the next nonce for an account. + fn next_nonce(&self, signer: &SeraiAddress) -> u32 { + core_pallet::NextNonce::<Runtime>::get(signer) } /// If the signer can pay the SRI fee. fn can_pay_fee( @@ -245,6 +238,14 @@ impl serai_abi::TransactionContext for Context { ) -> Result<(), sp_runtime::transaction_validity::TransactionValidityError> { todo!("TODO") } + + fn start_transaction(&self) { + Core::start_transaction(); + } + /// Consume the next nonce for an account. + fn consume_next_nonce(&self, signer: &SeraiAddress) { + core_pallet::NextNonce::<Runtime>::mutate(signer, |value| *value += 1); + } /// Have the transaction pay its SRI fee. fn pay_fee( &self, @@ -253,6 +254,9 @@ impl serai_abi::TransactionContext for Context { ) -> Result<(), sp_runtime::transaction_validity::TransactionValidityError> { todo!("TODO") } + fn end_transaction(&self, transaction_hash: [u8; 32]) { + Core::end_transaction(transaction_hash); + } } /* TODO