Add an UnbalancedMerkleTree primitive

The reasoning for it is documented with itself. The plan is to use it within
our header for committing to the DAG (allowing one header per epoch, yet
logarithmic proofs for any header within the epoch), the transactions
commitment (allowing logarithmic proofs of a transaction within a block,
without padding), and the events commitment (allowing logarithmic proofs of
unique events within a block, despite events not having a unique ID inherent).

This also defines transaction hashes and performs the necessary modifications
for transactions to be unique.
This commit is contained in:
Luke Parker
2025-03-04 04:00:05 -05:00
parent 352af85498
commit bfff823bf7
6 changed files with 333 additions and 11 deletions

View File

@@ -2,7 +2,20 @@ use alloc::vec::Vec;
use borsh::{BorshSerialize, BorshDeserialize};
use crate::{primitives::BlockHash, Transaction};
use crate::{
primitives::{BlockHash, merkle::UnbalancedMerkleTree},
Transaction,
};
/// 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;
/// The tag for the branch hashes of transaction events.
pub const EVENTS_COMMITMENT_TRANSACTION_EVENTS_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;
/// The tag for for the branch hashes of the Merkle tree which is the events commitments.
pub const EVENTS_COMMITMENT_TRANSACTIONS_TAG: u8 = 3;
/// A V1 header for a block.
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
@@ -18,6 +31,8 @@ pub struct HeaderV1 {
/// 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],
/// The commitment to the events within this block.
pub events_commitment: UnbalancedMerkleTree,
/// A commitment to the consensus data used to justify adding this block to the blockchain.
pub consensus_commitment: [u8; 32],
}
@@ -90,6 +105,8 @@ mod substrate {
pub unix_time_in_millis: u64,
/// The commitment to the transactions within this block.
pub transactions_commitment: [u8; 32],
/// The commitment to the events within this block.
pub events_commitment: UnbalancedMerkleTree,
}
impl SeraiDigest {
@@ -155,6 +172,11 @@ mod substrate {
.as_ref()
.map(|digest| digest.transactions_commitment)
.unwrap_or([0; 32]),
events_commitment: 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()),
})
}

View File

@@ -204,6 +204,26 @@ impl Transaction {
explicit_context.serialize(&mut message).unwrap();
message
}
/// The unique hash of this transaction.
///
/// No two transactions on the blockchain will share a hash, making this a unique identifier.
/// For signed transactions, this is due to the `(signer, nonce)` pair present within the
/// `ExplicitContext`. For unsigned transactions, this is due to inherent properties of their
/// execution (e.g. only being able to set a `ValidatorSet`'s keys once).
pub fn hash(&self) -> [u8; 32] {
sp_core::blake2_256(&match self {
Transaction::Unsigned { call } => borsh::to_vec(&call).unwrap(),
Transaction::Signed {
calls,
contextualized_signature: ContextualizedSignature { explicit_context, signature: _ },
} => {
// We explicitly don't hash the signature, so signatures can be replaced in the future if
// desired (such as with half-aggregated Schnorr signatures)
borsh::to_vec(&(calls, explicit_context)).unwrap()
}
})
}
}
#[cfg(feature = "substrate")]
@@ -276,16 +296,23 @@ mod substrate {
///
/// Returns `None` if the time has yet to be set.
fn current_time(&self) -> Option<u64>;
/// Get, and consume, the next nonce for an account.
fn get_and_consume_next_nonce(&self, signer: &SeraiAddress) -> u32;
/// Get the next nonce for an account.
fn next_nonce(&self, signer: &SeraiAddress) -> u32;
/// If the signer can pay the SRI fee.
fn can_pay_fee(
&self,
signer: &SeraiAddress,
fee: Amount,
) -> Result<(), TransactionValidityError>;
/// Begin execution of a transaction.
fn start_transaction(&self);
/// Consume the next nonce for an account.
fn consume_next_nonce(&self, signer: &SeraiAddress);
/// Have the transaction pay its SRI fee.
fn pay_fee(&self, signer: &SeraiAddress, fee: Amount) -> Result<(), TransactionValidityError>;
/// End execution of a transaction.
fn end_transaction(&self, transaction_hash: [u8; 32]);
}
/// A transaction with the context necessary to evaluate it within Substrate.
@@ -402,7 +429,7 @@ mod substrate {
Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))?;
}
}
match self.1.get_and_consume_next_nonce(signer).cmp(nonce) {
match self.1.next_nonce(signer).cmp(nonce) {
core::cmp::Ordering::Less => {
Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))?
}
@@ -472,7 +499,12 @@ mod substrate {
// We use 0 for the mempool priority, as this is no longer in the mempool so it's irrelevant
self.validate_except_fee::<V>(TransactionSource::InBlock, 0)?;
match self.0 {
// Start the transaction
self.1.start_transaction();
let transaction_hash = self.0.hash();
let res = match self.0 {
Transaction::Unsigned { call } => {
let call = Context::RuntimeCall::from(call.0);
V::pre_dispatch(&call)?;
@@ -487,7 +519,9 @@ mod substrate {
contextualized_signature:
ContextualizedSignature { explicit_context: ExplicitContext { signer, fee, .. }, .. },
} => {
// Start by paying the fee
// Consume the signer's next nonce
self.1.consume_next_nonce(&signer);
// Pay the fee
self.1.pay_fee(&signer, fee)?;
let _res = frame_support::storage::transactional::with_storage_layer(|| {
@@ -514,7 +548,14 @@ mod substrate {
pays_fee: Pays::Yes,
}))
}
}
};
// TODO: TransactionSuccess/TransactionFailure event?
// End the transaction
self.1.end_transaction(transaction_hash);
res
}
}
}

View File

@@ -15,8 +15,8 @@ use serai_primitives::{
pub enum Call {
/// Set the keys for a validator set.
set_keys {
/// The network whose latest validator set is setting their keys.
network: ExternalNetworkId,
/// The validator set which is setting their keys.
validator_set: ExternalValidatorSet,
/// The keys being set.
key_pair: KeyPair,
/// The participants in the validator set who signed off on these keys.
@@ -31,8 +31,8 @@ pub enum Call {
},
/// Report a validator set's slashes onto Serai.
report_slashes {
/// The network whose retiring validator set is setting their keys.
network: ExternalNetworkId,
/// The validator set which is setting their keys.
validator_set: ExternalValidatorSet,
/// The slashes they're reporting.
slashes: SlashReport,
/// The signature confirming the validity of this slash report.