diff --git a/Cargo.lock b/Cargo.lock index a94cde8659895..92950ed39eb4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24048,10 +24048,15 @@ dependencies = [ name = "snowbridge-pallet-ethereum-client-fixtures" version = "0.9.0" dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "alloy-trie", "hex-literal", "snowbridge-beacon-primitives", "snowbridge-verification-primitives", "sp-core 28.0.0", + "sp-io 30.0.0", "sp-std 14.0.0", ] @@ -24091,6 +24096,7 @@ dependencies = [ "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-inbound-queue-primitives", + "snowbridge-pallet-ethereum-client-fixtures", "sp-core 28.0.0", "sp-std 14.0.0", ] @@ -24135,10 +24141,13 @@ dependencies = [ name = "snowbridge-pallet-inbound-queue-v2-fixtures" version = "0.10.0" dependencies = [ + "alloy-core", + "alloy-primitives", "hex-literal", "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-inbound-queue-primitives", + "snowbridge-pallet-ethereum-client-fixtures", "sp-core 28.0.0", "sp-std 14.0.0", ] @@ -24185,6 +24194,7 @@ dependencies = [ "snowbridge-core", "snowbridge-merkle-tree", "snowbridge-outbound-queue-primitives", + "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-test-utils", "snowbridge-verification-primitives", "sp-arithmetic 23.0.0", diff --git a/bridges/snowbridge/pallets/ethereum-client/fixtures/Cargo.toml b/bridges/snowbridge/pallets/ethereum-client/fixtures/Cargo.toml index 75be05249f3f1..e6ed2d989c74c 100644 --- a/bridges/snowbridge/pallets/ethereum-client/fixtures/Cargo.toml +++ b/bridges/snowbridge/pallets/ethereum-client/fixtures/Cargo.toml @@ -24,12 +24,33 @@ snowbridge-verification-primitives = { workspace = true } sp-core = { workspace = true } sp-std = { workspace = true } +# Used only by the dynamic benchmark-fixture builder; gated by the +# `runtime-benchmarks` feature below. The dynamic builder is the inverse of +# `snowbridge_pallet_ethereum_client::Pallet::verify` and is shared by the +# inbound-queue v1, inbound-queue v2, and outbound-queue v2 benchmarks. +alloy-consensus = { workspace = true, optional = true } +alloy-primitives = { workspace = true, optional = true } +alloy-rlp = { workspace = true, optional = true } +alloy-trie = { workspace = true, optional = true } +sp-io = { workspace = true, optional = true } + [features] default = ["std"] std = [ + "alloy-consensus?/std", + "alloy-primitives?/std", + "alloy-rlp?/std", + "alloy-trie?/std", "snowbridge-beacon-primitives/std", "snowbridge-verification-primitives/std", "sp-core/std", + "sp-io?/std", "sp-std/std", ] -runtime-benchmarks = [] +runtime-benchmarks = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "sp-io", +] diff --git a/bridges/snowbridge/pallets/ethereum-client/fixtures/src/dynamic.rs b/bridges/snowbridge/pallets/ethereum-client/fixtures/src/dynamic.rs new file mode 100644 index 0000000000000..8d2702026ad6b --- /dev/null +++ b/bridges/snowbridge/pallets/ethereum-client/fixtures/src/dynamic.rs @@ -0,0 +1,396 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Builder for synthesizing dynamically-sized inbound/outbound benchmark fixtures. +//! +//! This is the inverse of `snowbridge_pallet_ethereum_client::Pallet::verify`: it produces +//! `EventFixture`s that the verifier accepts, by mirroring the verifier's SSZ + merkle + +//! receipts-trie logic. Three crates currently consume this: +//! - `snowbridge_pallet_inbound_queue_fixtures::dynamic` (v1 inbound) +//! - `snowbridge_pallet_inbound_queue_v2_fixtures::dynamic` +//! - `snowbridge_pallet_outbound_queue_v2::dynamic_fixture` +//! +//! Each caller supplies a [`LogTemplate`] describing its event log shape; the synthesis +//! machinery here does the rest. +//! +//! Construction layers, bottom-up: +//! +//! 1. Build a real receipts trie via `alloy_trie::HashBuilder` whose inclusion proof for the +//! retained leaf has exactly `n` nodes. The retained leaf holds an Eip1559 receipt whose primary +//! log matches the verifier's expected event log; the log's `data` field is sized so the encoded +//! envelope is `s` bytes. To make the proof path cross `n` distinct nodes we add `n - 1` cheap +//! sibling leaves, each diverging from the retained key at a successive nibble depth so a branch +//! node is forced at every depth `0..n-1` (see [`build_receipts_trie`]). +//! 2. Construct an `ExecutionPayloadHeader` whose `receipts_root` is the trie root from (1). +//! Compute its SSZ `hash_tree_root` — call it `execution_header_root`. +//! 3. Pick `execution_branch` as `EXECUTION_HEADER_DEPTH` zero hashes. Compute the `body_root` by +//! climbing the merkle tree from `execution_header_root` against that branch at +//! `subtree_index(EXECUTION_HEADER_INDEX)`. +//! 4. Build a `BeaconHeader` whose `body_root` is the value from (3) at slot `BENCH_SLOT`. Compute +//! its `hash_tree_root` — call it `beacon_block_root`. +//! 5. Pick `header_branch` as `BLOCK_ROOT_AT_INDEX_DEPTH` zero hashes. Compute `block_roots_root` +//! by climbing from `beacon_block_root` against that branch at the leaf index +//! `verify_ancestry_proof` will compute. +//! 6. Pick a deterministic `finalized_block_root` (any non-zero hash). The caller stores +//! `CompactBeaconState { slot: BENCH_SLOT + 1, block_roots_root }` under it and points +//! `LatestFinalizedBlockRoot` at the same root before submitting the event. +#![cfg(feature = "runtime-benchmarks")] + +extern crate alloc; + +use alloc::{boxed::Box, vec, vec::Vec}; +use alloy_consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom}; +use alloy_primitives::{Bloom, Bytes, LogData, B256}; +use alloy_rlp::Encodable; +use alloy_trie::{hash_builder::HashBuilder, proof::ProofRetainer, Nibbles}; +use snowbridge_beacon_primitives::{ + merkle_proof::{generalized_index_length, subtree_index}, + types::deneb, + AncestryProof, BeaconHeader, ExecutionProof, VersionedExecutionPayloadHeader, +}; +use snowbridge_verification_primitives::{EventFixture, EventProof, Log, Proof}; +use sp_core::{H160, H256, U256}; +use sp_io::hashing::sha2_256; + +/// `get_generalized_index(BeaconBlockBody, 'execution_payload')` for the post-Bellatrix +/// beacon block body. Mirrors `ethereum-client::config::altair::EXECUTION_HEADER_INDEX`. +const EXECUTION_HEADER_INDEX: usize = 25; +/// `BLOCK_ROOT_AT_INDEX_DEPTH` from `ethereum-client::config`. +const BLOCK_ROOT_AT_INDEX_DEPTH: usize = 13; +/// `SLOTS_PER_HISTORICAL_ROOT` from the consensus spec. The ancestry-proof leaf index is +/// `SLOTS_PER_HISTORICAL_ROOT + (block_slot mod SLOTS_PER_HISTORICAL_ROOT)`. +const SLOTS_PER_HISTORICAL_ROOT: u64 = 8192; + +/// The slot we synthesize the proof against. Any value that fits in u64 works; we keep it +/// well above the boundary so the index lands inside the second half of the merkle tree. +pub const BENCH_SLOT: u64 = 8_000_000; + +/// Lower bound on the receipt size — the encoded Eip1559 envelope around an empty-data log +/// is already this large because of the 256-byte bloom and topic overhead. +pub const MIN_RECEIPT_SIZE: u32 = 320; + +/// Transaction index of the retained receipt. A receipt-trie proof can have at most +/// `key_nibbles + 1` nodes, so the key — `rlp(BENCH_TARGET_TX_INDEX)` — must carry at least +/// `MaxProofNodes - 1` nibbles for the sibling-divergence construction in +/// [`build_receipts_trie`] to reach the benchmarked worst case. This necessarily makes the +/// index synthetic: a realistic block tops out around a few thousand receipts (a ~2-byte +/// index, 4-6 nibbles), far too short. +/// +/// `0x00FF_FFFF_FFFF_FFFF` is a 7-byte value, so its RLP key is `0x87` followed by seven +/// `0xff` bytes = 8 bytes = 16 nibbles, supporting proof paths of up to 17 nodes — one above +/// the current `MaxProofNodes` of 16. (The `target_nibbles.len() >= n - 1` debug-assert in +/// `build_receipts_trie` guards against this being set too small.) +const BENCH_TARGET_TX_INDEX: u64 = 0x00FF_FFFF_FFFF_FFFF; + +/// Output of [`build_dynamic_fixture_with_log`]. +pub struct DynamicFixture { + /// The fully-formed `EventFixture` to submit through the benchmarked extrinsic. + pub event_fixture: EventFixture, + /// Block root that the caller must store as `LatestFinalizedBlockRoot` (and to which + /// the `CompactBeaconState` must be associated) before submitting the event. + pub finalized_block_root: H256, +} + +/// Per-version Ethereum log shape. All inbound/outbound queue versions reuse the same SSZ +/// / merkle / receipts-trie machinery; they only differ in the `Log` they expect inside +/// the receipt (gateway address, topic vector, and the bytes that must round-trip +/// through their respective `try_from(&Log)` after envelope decode). +pub struct LogTemplate { + /// Gateway contract address that emitted the log. + pub gateway: [u8; 20], + /// Topics on the log — for v1 inbound these are `[topic0, channel_id, message_id]`; + /// for v2 inbound it is just `[OutboundMessageAccepted::SIGNATURE_HASH]`; for v2 + /// outbound `submit_delivery_receipt` it is + /// `[InboundMessageDispatched::SIGNATURE_HASH, padded_nonce]`. + pub topics: Vec, + /// Build the log's `data` bytes for a given target length. The dynamic builder calls + /// this in a convergence loop because RLP envelope overhead grows non-uniformly with + /// data size, so the function is asked for several sizes near the requested `s`. + /// + /// V1 inbound implementation appends zero bytes to a fixed prefix (the verifier just + /// byte-compares `Log::data` against the receipt's log). V2 inbound must produce a + /// valid ABI-encoded `IGatewayV2::OutboundMessageAccepted` payload, so it grows a + /// variable-length `bytes` field inside the payload to reach the target length. + /// + /// When the matching log's `data` is fixed-size (e.g. v2 outbound's + /// `InboundMessageDispatched`, whose data is exactly 96 bytes), set this to a closure + /// that always returns those exact bytes regardless of `target_len`, and use + /// [`LogTemplate::filler_log_data_builder`] to grow the receipt instead. + pub data_builder: Box Vec>, + /// Optional filler log data builder. When `Some`, the dynamic builder appends a + /// second log to the receipt envelope (with a zero address and no topics) whose + /// `data` is sized by this builder so the final envelope hits the target receipt + /// size. Used when the matching log's `data` is fixed-size and cannot be grown. + pub filler_log_data_builder: Option Vec>>, +} + +/// Build a synthetic `EventFixture` whose receipt proof has `n` trie nodes and whose +/// receipt body is approximately `s` bytes. The caller is responsible for storing the +/// returned `finalized_block_root` and matching `CompactBeaconState` (slot +/// `BENCH_SLOT + 1`, `block_roots_root` derived from the proof) in ethereum-client storage. +pub fn build_dynamic_fixture_with_log(n: u32, s: u32, log: LogTemplate) -> DynamicFixture { + let n = n.max(1); + let s = s.max(MIN_RECEIPT_SIZE); + + // 1. Build the receipts trie. + let (receipts_root, receipt_proof, receipt_log) = build_receipts_trie(n, s, &log); + + // 2. ExecutionPayloadHeader rooted at the trie root. + let execution_header = build_execution_header(receipts_root); + let execution_header_versioned = VersionedExecutionPayloadHeader::Deneb(execution_header); + let execution_header_root = execution_header_versioned + .hash_tree_root() + .expect("synthesized execution header is well-formed; qed"); + + // 3. Compute body_root from execution_header_root + zero execution_branch. + let execution_branch_depth = generalized_index_length(EXECUTION_HEADER_INDEX); + let execution_branch_index = subtree_index(EXECUTION_HEADER_INDEX); + let execution_branch: Vec = vec![H256::zero(); execution_branch_depth]; + let body_root = + compute_merkle_root(execution_header_root, &execution_branch, execution_branch_index); + + // 4. BeaconHeader rooted at body_root. + let header = BeaconHeader { + slot: BENCH_SLOT, + proposer_index: 0, + parent_root: H256::zero(), + state_root: H256::zero(), + body_root, + }; + let beacon_block_root = + header.hash_tree_root().expect("synthesized beacon header is well-formed; qed"); + + // 5. Compute block_roots_root from beacon_block_root + zero header_branch. + let ancestry_index_in_array = BENCH_SLOT % SLOTS_PER_HISTORICAL_ROOT; + let ancestry_leaf_index = (SLOTS_PER_HISTORICAL_ROOT + ancestry_index_in_array) as usize; + let header_branch: Vec = vec![H256::zero(); BLOCK_ROOT_AT_INDEX_DEPTH]; + let block_roots_root = + compute_merkle_root(beacon_block_root, &header_branch, ancestry_leaf_index); + + // 6. Pick a deterministic finalized_block_root. + let finalized_block_root: H256 = sha2_256(b"snowbridge-bench-finalized-root").into(); + + let execution_proof = ExecutionProof { + header, + ancestry_proof: Some(AncestryProof { header_branch, finalized_block_root }), + execution_header: execution_header_versioned, + execution_branch, + }; + + let event = + EventProof { event_log: receipt_log, proof: Proof { receipt_proof, execution_proof } }; + + DynamicFixture { + event_fixture: EventFixture { + event, + finalized_header: BeaconHeader { + slot: BENCH_SLOT + 1, + proposer_index: 0, + parent_root: H256::zero(), + state_root: H256::zero(), + body_root: H256::zero(), + }, + block_roots_root, + }, + finalized_block_root, + } +} + +/// Build a real receipts trie whose inclusion proof for the retained leaf has exactly `n` +/// nodes and whose retained receipt envelope is `s` bytes. Returns +/// `(receipts_root, proof_for_target_tx_index, event_log)`. +/// +/// The runtime charges `submit` weight against `(event.proof.receipt_proof.len(), )`, where the `n` axis prices the verifier's per-node RLP-decode + keccak work. So the +/// benchmark's `n` component must equal the produced proof length AND each proof node must be +/// the realistic worst case, otherwise the fitted per-node slope undercharges real proofs. +/// +/// A naive trie of `n` sequential `rlp(0..n)` leaves achieves neither: sequential RLP indices +/// diverge at the first nibble, leaving a shallow ~2-node path regardless of `n`. Instead we +/// keep a single target key and force a *full-width* branch node at every depth along its path: +/// +/// - The retained key is `rlp(BENCH_TARGET_TX_INDEX)`, chosen so the key has enough nibbles (16) to +/// force proof paths above `MaxProofNodes`; see [`BENCH_TARGET_TX_INDEX`]. +/// - For each depth `i` in `0..n-1` we add a sibling leaf for every nibble value other than the +/// target's at position `i` (15 siblings). This makes the branch node at depth `i` full (16 +/// children), so the target path crosses `n - 1` ~530-byte branch nodes plus the terminal leaf = +/// `n` nodes. A full branch is the worst case the verifier can encounter, so the per-node slope +/// is a safe upper bound: a caller cannot make any proof node more expensive to hash. +/// - Sibling values are 32 zero bytes, large enough that each sibling leaf is referenced by hash in +/// its parent branch (as in a real receipts trie) rather than inlined, which is what makes the +/// branch full-width. The sibling leaves never appear in the retained proof themselves. +fn build_receipts_trie(n: u32, s: u32, log: &LogTemplate) -> (H256, Vec>, Log) { + let n = (n as usize).max(1); + + // Pad the log's `data` field so the encoded receipt envelope is exactly `s` bytes + // (within +/- a few bytes of overhead — the verifier does not enforce a size). + let receipt_envelope = encode_sized_receipt_envelope(s as usize, log); + // The `Log` we pass into `submit` reproduces what the verifier sees on Ethereum: the + // gateway address, the same topics, and the same `data` we baked into the receipt. Its + // `tx_index` is what the verifier feeds into `receipt_trie_key`, so it must match the + // retained leaf's key. + let receipt_log = Log { + address: H160(log.gateway), + topics: log.topics.clone(), + data: receipt_data_from_envelope_log(&receipt_envelope), + tx_index: BENCH_TARGET_TX_INDEX, + }; + + // Receipt-trie keys are `rlp(tx_index)`. The target key holds the sized receipt; at each + // depth on its path we add a sibling for every other nibble value so the branch node there + // is full-width (16 children) — the verifier's per-node worst case. + let target_key = rlp_index_nibbles(BENCH_TARGET_TX_INDEX); + let target_nibbles: Vec = target_key.to_vec(); + debug_assert!( + target_nibbles.len() >= n - 1, + "target key has too few nibbles to force an {n}-node proof path", + ); + + // 32 bytes is large enough that a sibling leaf's RLP exceeds 32 bytes and is therefore + // referenced by hash in its parent branch (not inlined), making the branch full-width. + let sibling_value = vec![0u8; 32]; + let mut keyed: Vec<(Nibbles, Vec)> = Vec::with_capacity(n + 15 * (n - 1)); + keyed.push((target_key, receipt_envelope)); + for i in 0..n - 1 { + for nibble in 0u8..16 { + if nibble == target_nibbles[i] { + continue; // the target itself occupies this child slot + } + let mut sibling = target_nibbles[..i].to_vec(); + sibling.push(nibble); + keyed.push((Nibbles::from_nibbles(&sibling), sibling_value.clone())); + } + } + // alloy_trie requires sorted key insertion. + keyed.sort_by(|a, b| a.0.cmp(&b.0)); + + let retainer = ProofRetainer::new(vec![target_key]); + let mut hb = HashBuilder::default().with_proof_retainer(retainer); + for (key, value) in &keyed { + hb.add_leaf(*key, value.as_slice()); + } + let root: B256 = hb.root(); + let proof_nodes = hb.take_proof_nodes(); + let proof: Vec> = proof_nodes + .matching_nodes_sorted(&target_key) + .into_iter() + .map(|(_, bytes)| bytes.to_vec()) + .collect(); + debug_assert_eq!(proof.len(), n, "receipt proof should have exactly n nodes"); + + (root.0.into(), proof, receipt_log) +} + +fn rlp_index_nibbles(index: u64) -> Nibbles { + let mut buf = Vec::new(); + index.encode(&mut buf); + Nibbles::unpack(buf.as_slice()) +} + +/// Encode an Eip1559 `ReceiptEnvelope` whose primary log matches `log` and whose total +/// RLP-encoded length equals `target_size`. The variable-size knob is either the matching +/// log's `data` field (when `log.filler_log_data_builder` is `None`) or a second +/// zero-address filler log appended after the matching one (when it is `Some`). +fn encode_sized_receipt_envelope(target_size: usize, log: &LogTemplate) -> Vec { + let target_size = target_size.max(MIN_RECEIPT_SIZE as usize); + // Initial guess: target_data_len = target_size - constant overhead. Iterate to + // converge because RLP overhead grows non-uniformly with data length, and the v2 + // inbound data_builder may produce ABI-encoded payloads whose size jumps in 32-byte + // chunks. + let mut data_len = target_size.saturating_sub(MIN_RECEIPT_SIZE as usize); + let mut last = encode_envelope(log, data_len); + for _ in 0..16 { + if last.len() == target_size { + return last; + } + // Nudge by the difference, but never below 0. + if last.len() < target_size { + data_len = data_len.saturating_add(target_size - last.len()); + } else { + data_len = data_len.saturating_sub(last.len() - target_size); + } + last = encode_envelope(log, data_len); + } + last +} + +fn encode_envelope(log: &LogTemplate, data_len: usize) -> Vec { + // Matching-log data: when a filler builder is present, the primary data is the fixed + // matching-log payload (so the verifier's matching-log scan succeeds); the filler + // log's data is what scales with `data_len`. Otherwise the primary data scales. + let (primary_data, filler_data) = match &log.filler_log_data_builder { + Some(filler_builder) => ((log.data_builder)(0), Some((filler_builder)(data_len))), + None => ((log.data_builder)(data_len), None), + }; + + let primary_log = alloy_primitives::Log { + address: alloy_primitives::Address::from(log.gateway), + data: LogData::new_unchecked( + log.topics.iter().map(|t| B256::from(t.0)).collect(), + Bytes::from(primary_data), + ), + }; + let mut logs = vec![primary_log]; + if let Some(filler) = filler_data { + logs.push(alloy_primitives::Log { + address: alloy_primitives::Address::ZERO, + data: LogData::new_unchecked(vec![], Bytes::from(filler)), + }); + } + + let receipt = Receipt { status: Eip658Value::Eip658(true), cumulative_gas_used: 21_000, logs }; + let envelope = ReceiptEnvelope::Eip1559(ReceiptWithBloom { receipt, logs_bloom: Bloom::ZERO }); + let mut out = Vec::new(); + envelope.encode(&mut out); + out +} + +/// Decode the envelope just enough to recover the primary log's `data` field. The +/// verifier compares the submitted log's `data` byte-for-byte against the receipt's log, +/// so the `Log` we synthesize must carry the exact padded data we stored in the envelope. +fn receipt_data_from_envelope_log(envelope_bytes: &[u8]) -> Vec { + use alloy_rlp::Decodable; + let env = ReceiptEnvelope::decode(&mut &envelope_bytes[..]).expect("envelope round-trips; qed"); + let receipt = env.as_receipt().expect("Eip1559 receipt; qed"); + receipt.logs[0].data.data.to_vec() +} + +fn build_execution_header(receipts_root: H256) -> deneb::ExecutionPayloadHeader { + deneb::ExecutionPayloadHeader { + parent_hash: H256::zero(), + fee_recipient: H160::zero(), + state_root: H256::zero(), + receipts_root, + logs_bloom: vec![0u8; 256], + prev_randao: H256::zero(), + block_number: BENCH_SLOT, + gas_limit: 30_000_000, + gas_used: 21_000, + timestamp: 0, + extra_data: vec![], + base_fee_per_gas: U256::from(7u64), + block_hash: H256::zero(), + transactions_root: H256::zero(), + withdrawals_root: H256::zero(), + blob_gas_used: 0, + excess_blob_gas: 0, + } +} + +/// Climb the merkle tree from `leaf` against `branch` at `index`. Mirrors the algorithm +/// in `ethereum-client::merkle_proof::compute_merkle_root` (the helper is pallet-private, +/// so we replicate it here). +fn compute_merkle_root(leaf: H256, branch: &[H256], index: usize) -> H256 { + let mut value: [u8; 32] = leaf.into(); + for (i, node) in branch.iter().enumerate() { + let mut data = [0u8; 64]; + if (index >> i) & 1 == 1 { + data[0..32].copy_from_slice(node.as_bytes()); + data[32..64].copy_from_slice(&value); + } else { + data[0..32].copy_from_slice(&value); + data[32..64].copy_from_slice(node.as_bytes()); + } + value = sha2_256(&data); + } + value.into() +} diff --git a/bridges/snowbridge/pallets/ethereum-client/fixtures/src/lib.rs b/bridges/snowbridge/pallets/ethereum-client/fixtures/src/lib.rs index 562d99b2722d9..00efb58a1df63 100644 --- a/bridges/snowbridge/pallets/ethereum-client/fixtures/src/lib.rs +++ b/bridges/snowbridge/pallets/ethereum-client/fixtures/src/lib.rs @@ -4,6 +4,9 @@ // See README.md for instructions to generate #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(feature = "runtime-benchmarks")] +pub mod dynamic; + use hex_literal::hex; use snowbridge_beacon_primitives::{ types::deneb, AncestryProof, BeaconHeader, ExecutionProof, NextSyncCommitteeUpdate, diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/Cargo.toml b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/Cargo.toml index 0ad7af6ead5b7..aa695b481d71a 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/Cargo.toml +++ b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/Cargo.toml @@ -25,16 +25,30 @@ snowbridge-inbound-queue-primitives = { workspace = true } sp-core = { workspace = true } sp-std = { workspace = true } +# Used by the v2 dynamic fixture builder under runtime-benchmarks. The shared +# SSZ / merkle / receipts-trie machinery lives in +# `snowbridge-pallet-ethereum-client-fixtures`; this crate just supplies the +# v2-specific ABI-encoded log payload on top. +alloy-core = { workspace = true, features = ["sol-types"], optional = true } +alloy-primitives = { workspace = true, optional = true } +snowbridge-pallet-ethereum-client-fixtures = { workspace = true, optional = true } + [features] default = ["std"] std = [ + "alloy-core?/std", + "alloy-primitives?/std", "snowbridge-beacon-primitives/std", "snowbridge-core/std", "snowbridge-inbound-queue-primitives/std", + "snowbridge-pallet-ethereum-client-fixtures?/std", "sp-core/std", "sp-std/std", ] runtime-benchmarks = [ + "alloy-core", + "alloy-primitives", "snowbridge-core/runtime-benchmarks", "snowbridge-inbound-queue-primitives/runtime-benchmarks", + "snowbridge-pallet-ethereum-client-fixtures/runtime-benchmarks", ] diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/dynamic.rs b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/dynamic.rs new file mode 100644 index 0000000000000..433fa77912fa1 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/dynamic.rs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Builder for synthesizing dynamically-sized inbound-queue v2 benchmark fixtures. +//! +//! Reuses the SSZ / merkle / receipts-trie machinery from the v1 dynamic fixture by +//! supplying a [`LogTemplate`] whose `data_builder` produces an ABI-encoded +//! `IGatewayV2::OutboundMessageAccepted` payload of the requested length. +//! +//! v2 differs from v1 in two important ways: +//! - The log topics array is just `[OutboundMessageAccepted::SIGNATURE_HASH]`. +//! - The `data` field must decode via `decode_raw_log_validate`; we cannot just append zero bytes +//! to a fixed prefix. Instead the builder grows the variable-length `xcm.data` field inside the +//! payload to reach the target byte count, which keeps the ABI encoding valid throughout. +#![cfg(feature = "runtime-benchmarks")] + +use alloc::{boxed::Box, vec, vec::Vec}; +use alloy_core::sol_types::SolEvent; +use alloy_primitives::{Address, Bytes, B256}; +use snowbridge_inbound_queue_primitives::v2::IGatewayV2; +use snowbridge_pallet_ethereum_client_fixtures::dynamic::{ + build_dynamic_fixture_with_log, DynamicFixture, LogTemplate, +}; +use sp_core::H256; + +extern crate alloc; + +/// Gateway address used by the v2 benchmark — matches the address baked into the +/// generated `make_register_token_message` v2 fixture. +const GATEWAY_ADDRESS: [u8; 20] = hex_literal::hex!("b1185ede04202fe62d38f5db72f71e38ff3e8305"); + +/// Build a synthetic v2 `EventFixture` whose receipt proof has `n` trie nodes and whose +/// receipt body is approximately `s` bytes. The caller is responsible for storing the +/// returned `finalized_block_root` and matching `CompactBeaconState` (slot +/// `BENCH_SLOT + 1`, `block_roots_root` derived from the proof) in ethereum-client storage. +pub fn build_dynamic_fixture(n: u32, s: u32) -> DynamicFixture { + build_dynamic_fixture_with_log(n, s, v2_log_template()) +} + +/// `LogTemplate` that produces ABI-encoded `IGatewayV2::OutboundMessageAccepted` payloads +/// of arbitrary size by padding the variable-length `xcm.data` field. +fn v2_log_template() -> LogTemplate { + let topic0 = IGatewayV2::OutboundMessageAccepted::SIGNATURE_HASH; + let topic0_h256 = H256::from_slice(topic0.as_slice()); + LogTemplate { + gateway: GATEWAY_ADDRESS, + topics: vec![topic0_h256], + data_builder: Box::new(|target_len| build_v2_payload_data(target_len)), + filler_log_data_builder: None, + } +} + +/// Build an ABI-encoded `OutboundMessageAccepted` event whose total length is roughly +/// `target_len` bytes by sizing the `xcm.data` field. The `decode_raw_log_validate` call +/// in the v2 verifier expects a syntactically-valid ABI payload; we cannot pad with raw +/// zeros, but we can grow `xcm.data` (a variable-length `bytes` field). +fn build_v2_payload_data(target_len: usize) -> Vec { + // First, compute the ABI overhead of the payload around an empty `xcm.data`. This + // gives us the constant header cost; the rest is xcm.data plus the bytes-length + // prefix (which itself is 32-byte-padded). + let baseline = encode_payload_with_xcm_data_len(0); + if target_len <= baseline.len() { + return baseline; + } + + // Each extra `target_len - baseline.len()` byte adds one byte to xcm.data, plus a + // 32-byte rounding overhead at boundaries. Iterate to converge on the exact size. + let mut xcm_data_len = target_len.saturating_sub(baseline.len()); + let mut last = encode_payload_with_xcm_data_len(xcm_data_len); + for _ in 0..16 { + if last.len() == target_len { + return last; + } + if last.len() < target_len { + xcm_data_len = xcm_data_len.saturating_add(target_len - last.len()); + } else { + xcm_data_len = xcm_data_len.saturating_sub(last.len() - target_len); + } + last = encode_payload_with_xcm_data_len(xcm_data_len); + } + last +} + +/// Encode a `OutboundMessageAccepted` event whose `xcm.data` field is `xcm_data_len` zero +/// bytes. The `executionFee` is set to a non-zero placeholder because the v2 message-to-XCM +/// converter rejects zero-fee messages (it constructs a fungible XCM asset out of the fee +/// and the v5 `Asset` constructor asserts non-zero amount). +fn encode_payload_with_xcm_data_len(xcm_data_len: usize) -> Vec { + let xcm_bytes = vec![0u8; xcm_data_len]; + let event = IGatewayV2::OutboundMessageAccepted { + nonce: 1u64, + payload: IGatewayV2::Payload { + origin: Address::from([0u8; 20]), + assets: vec![], + xcm: IGatewayV2::Xcm { kind: 0, data: Bytes::from(xcm_bytes) }, + claimer: Bytes::new(), + value: 0, + executionFee: 1, + relayerFee: 0, + }, + }; + event.encode_data() +} + +// Re-export for convenience: callers in runtimes can grab the same gateway address used +// by the benchmark fixture. +pub const fn gateway_address() -> [u8; 20] { + GATEWAY_ADDRESS +} + +// Use `Address::from([u8; 20])` and `B256` so they aren't reported as unused under any +// feature combination. +const _: fn() = || { + let _: Address = Address::from([0u8; 20]); + let _: B256 = B256::ZERO; +}; diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/lib.rs index 3fe70c51a58d9..6f75a515312c9 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/lib.rs @@ -2,4 +2,6 @@ // SPDX-FileCopyrightText: 2023 Snowfork #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(feature = "runtime-benchmarks")] +pub mod dynamic; pub mod register_token; diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking.rs index 58329207d911d..37af3f62e41a1 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking.rs @@ -4,18 +4,34 @@ use super::*; use crate::Pallet as InboundQueue; use frame_benchmarking::v2::*; -use frame_support::assert_ok; +use frame_support::{assert_ok, traits::Get}; use frame_system::RawOrigin; #[benchmarks] mod benchmarks { use super::*; + /// Benchmark `submit`, parameterized by: + /// - `n`: number of nodes in the receipt-inclusion proof. The verifier's per-node cost (RLP + /// decode + branch traversal) scales linearly with `n`. + /// - `s`: size in bytes of the receipt that the proof terminates at. The leaf node's size is + /// dominated by the receipt body, and decode/scan cost scales linearly with `s`. The lower + /// bound `320` reflects the smallest realistic receipt envelope: an empty-logs Eip2930/1559 + /// receipt with a 256-byte logs bloom. + /// + /// `MaxProofNodes` and `MaxReceiptBytes` are runtime-benchmarks-only Config items; they + /// bound the benchmark's exploration of the worst case but do NOT bound proof or + /// receipt sizes at runtime. The framework fits a slope from these samples and the + /// dispatch attribute scales the declared weight using the actual `n`/`s` of the + /// submitted event. #[benchmark] - fn submit() -> Result<(), BenchmarkError> { + fn submit( + n: Linear<1, { T::MaxProofNodes::get() }>, + s: Linear<320, { T::MaxReceiptBytes::get() }>, + ) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); - let create_message = T::Helper::initialize_storage(); + let create_message = T::Helper::initialize_storage(n, s); #[block] { diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs index 4ee32a9d39174..93324afc30263 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs @@ -74,7 +74,10 @@ pub mod pallet { #[cfg(feature = "runtime-benchmarks")] pub trait BenchmarkHelper { - fn initialize_storage() -> EventFixture; + /// Build an `EventFixture` whose receipt proof has roughly `n` trie nodes and whose + /// receipt body is roughly `s` bytes, prime the verifier's storage so the returned + /// event verifies, and hand the fixture back for the benchmark to submit. + fn initialize_storage(n: u32, s: u32) -> EventFixture; } #[pallet::config] @@ -98,6 +101,18 @@ pub mod pallet { /// Relayer reward payment. type RewardPayment: RewardLedger; type WeightInfo: WeightInfo; + + /// Maximum number of trie nodes in a receipt proof. Used as the benchmark upper bound + /// for the `n` component of `WeightInfo::submit` and as the worst-case `n` argument + /// in delivery-cost estimation. Does NOT enforce a proof-size limit at runtime — the + /// verifier rejects oversized proofs through its own cost accounting. + type MaxProofNodes: Get; + + /// Maximum size in bytes of an Ethereum receipt referenced by a proof. Used as the + /// benchmark upper bound for the `s` component of `WeightInfo::submit` and as the + /// worst-case `s` argument in delivery-cost estimation. Does NOT enforce a receipt-size + /// limit at runtime. + type MaxReceiptBytes: Get; } #[pallet::event] @@ -181,7 +196,13 @@ pub mod pallet { impl Pallet { /// Submit an inbound message originating from the Gateway contract on Ethereum #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::submit())] + #[pallet::weight(T::WeightInfo::submit( + event.proof.receipt_proof.len() as u32, + // The receipt is stored as the value of the leaf (last) node of the receipt-trie + // proof, so the leaf's encoded length is a tight upper bound for the receipt size + // at dispatch time, before verification has run. + event.proof.receipt_proof.last().map(|leaf| leaf.len() as u32).unwrap_or(0), + ))] pub fn submit(origin: OriginFor, event: Box) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(!OperatingMode::::get().is_halted(), Error::::Halted); diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs index c3ecd92ecde98..e7b3bf0c6b32a 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs @@ -79,7 +79,7 @@ const GATEWAY_ADDRESS: [u8; 20] = hex!["b1185ede04202fe62d38f5db72f71e38ff3e8305 #[cfg(feature = "runtime-benchmarks")] impl BenchmarkHelper for Test { // not implemented since the MockVerifier is used for tests - fn initialize_storage() -> EventFixture { + fn initialize_storage(_n: u32, _s: u32) -> EventFixture { make_register_token_message() } } @@ -181,6 +181,8 @@ impl inbound_queue_v2::Config for Test { type RewardKind = BridgeReward; type DefaultRewardKind = SnowbridgeReward; type RewardPayment = MockRewardLedger; + type MaxProofNodes = frame_support::traits::ConstU32<16>; + type MaxReceiptBytes = frame_support::traits::ConstU32<8192>; } pub fn setup() { @@ -365,6 +367,8 @@ pub mod exploit { type RewardKind = BridgeReward; type DefaultRewardKind = SnowbridgeReward; type RewardPayment = MockRewardLedger; + type MaxProofNodes = ConstU32<16>; + type MaxReceiptBytes = ConstU32<8192>; } pub fn setup() { diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/weights.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/weights.rs index c96d3a03b39bd..2c61daccb5e1c 100644 --- a/bridges/snowbridge/pallets/inbound-queue-v2/src/weights.rs +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/weights.rs @@ -17,12 +17,15 @@ use sp_std::marker::PhantomData; /// Weight functions needed for ethereum_beacon_client. pub trait WeightInfo { - fn submit() -> Weight; + /// Weight of `submit`, parameterized by: + /// - `n`: number of nodes in the receipt proof, + /// - `s`: size in bytes of the receipt referenced by the proof. + fn submit(n: u32, s: u32) -> Weight; } // For backwards compatibility and tests impl WeightInfo for () { - fn submit() -> Weight { + fn submit(_n: u32, _s: u32) -> Weight { // Proof Size summary in bytes: // Measured: `309` // Estimated: `3774` diff --git a/bridges/snowbridge/pallets/inbound-queue/fixtures/Cargo.toml b/bridges/snowbridge/pallets/inbound-queue/fixtures/Cargo.toml index 72d53a96aba82..5eab598618117 100644 --- a/bridges/snowbridge/pallets/inbound-queue/fixtures/Cargo.toml +++ b/bridges/snowbridge/pallets/inbound-queue/fixtures/Cargo.toml @@ -25,16 +25,22 @@ snowbridge-inbound-queue-primitives = { workspace = true } sp-core = { workspace = true } sp-std = { workspace = true } +# Used only by the v1 dynamic-fixture wrapper; the shared SSZ/merkle/receipts-trie +# machinery lives in `snowbridge-pallet-ethereum-client-fixtures`. +snowbridge-pallet-ethereum-client-fixtures = { workspace = true, optional = true } + [features] default = ["std"] std = [ "snowbridge-beacon-primitives/std", "snowbridge-core/std", "snowbridge-inbound-queue-primitives/std", + "snowbridge-pallet-ethereum-client-fixtures?/std", "sp-core/std", "sp-std/std", ] runtime-benchmarks = [ "snowbridge-core/runtime-benchmarks", "snowbridge-inbound-queue-primitives/runtime-benchmarks", + "snowbridge-pallet-ethereum-client-fixtures/runtime-benchmarks", ] diff --git a/bridges/snowbridge/pallets/inbound-queue/fixtures/src/dynamic.rs b/bridges/snowbridge/pallets/inbound-queue/fixtures/src/dynamic.rs new file mode 100644 index 0000000000000..d38c228de2888 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue/fixtures/src/dynamic.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Thin v1-specific entry on top of the shared dynamic-fixture machinery in +//! `snowbridge_pallet_ethereum_client_fixtures::dynamic`. +//! +//! All the SSZ / merkle / receipts-trie work lives in the shared crate; this module just +//! supplies the v1 gateway address, topics, and `data` builder. +#![cfg(feature = "runtime-benchmarks")] + +extern crate alloc; + +use alloc::{boxed::Box, vec, vec::Vec}; +use snowbridge_core::ChannelId; +use snowbridge_pallet_ethereum_client_fixtures::dynamic::{ + build_dynamic_fixture_with_log, LogTemplate, +}; +use sp_core::H256; + +pub use snowbridge_pallet_ethereum_client_fixtures::dynamic::DynamicFixture; + +const GATEWAY_ADDRESS: [u8; 20] = hex_literal::hex!("eda338e4dc46038493b885327842fd3e301cab39"); +const CHANNEL_ID: H256 = + H256(hex_literal::hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539")); +/// Same value as `CHANNEL_ID`, typed as a [`ChannelId`]. The runtime helper registers a +/// matching channel in `EthereumSystem::Channels` so that `submit`'s `ChannelLookup` +/// resolves successfully under the benchmark fixture. +pub const CHANNEL_ID_AS_CHANNEL_ID: ChannelId = ChannelId::new(hex_literal::hex!( + "c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539" +)); +const MESSAGE_ID: H256 = + H256(hex_literal::hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0")); +/// Topic0 of the OutboundMessageAccepted event. +const TOPIC0: H256 = + H256(hex_literal::hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f")); + +/// Build a synthetic v1 `EventFixture` whose receipt proof has `n` trie nodes and whose +/// receipt body is approximately `s` bytes. The caller is responsible for storing the +/// returned `finalized_block_root` and matching `CompactBeaconState` in +/// ethereum-client storage. +pub fn build_dynamic_fixture(n: u32, s: u32) -> DynamicFixture { + build_dynamic_fixture_with_log(n, s, default_v1_log_template()) +} + +/// Default v1 `LogTemplate` — matches the gateway address and event topology the +/// inbound-queue v1 verifier expects (channel id at topic1, message id at topic2, +/// `RegisterToken` versioned message inside `data`). +pub fn default_v1_log_template() -> LogTemplate { + LogTemplate { + gateway: GATEWAY_ADDRESS, + topics: vec![TOPIC0, CHANNEL_ID, MESSAGE_ID], + data_builder: Box::new(|target_len| { + // V1 just byte-compares Log::data; pad a fixed RegisterToken VersionedMessage + // prefix with trailing zero bytes to reach `target_len`. + let prefix = build_outbound_message_data(); + let mut data = Vec::with_capacity(target_len.max(prefix.len())); + data.extend_from_slice(&prefix); + if target_len > data.len() { + data.resize(target_len, 0); + } + data + }), + filler_log_data_builder: None, + } +} + +/// 96 bytes of payload that decodes as a `RegisterToken` versioned message. Used as the +/// initial bytes of every receipt's log data so the inbound-queue's `decode_all` of the +/// `VersionedMessage` succeeds. +fn build_outbound_message_data() -> Vec { + hex_literal::hex!( + "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e00a736aa00000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000" + ) + .to_vec() +} diff --git a/bridges/snowbridge/pallets/inbound-queue/fixtures/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue/fixtures/src/lib.rs index cb4232376c6fc..a7859754ce184 100644 --- a/bridges/snowbridge/pallets/inbound-queue/fixtures/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue/fixtures/src/lib.rs @@ -2,6 +2,8 @@ // SPDX-FileCopyrightText: 2023 Snowfork #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(feature = "runtime-benchmarks")] +pub mod dynamic; pub mod register_token; pub mod send_native_eth; pub mod send_token; diff --git a/bridges/snowbridge/pallets/inbound-queue/src/benchmarking/mod.rs b/bridges/snowbridge/pallets/inbound-queue/src/benchmarking/mod.rs index ce98edf610abf..5b0753341e1be 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/benchmarking/mod.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/benchmarking/mod.rs @@ -4,18 +4,34 @@ use super::*; use crate::Pallet as InboundQueue; use frame_benchmarking::v2::*; -use frame_support::assert_ok; +use frame_support::{assert_ok, traits::Get}; use frame_system::RawOrigin; #[benchmarks] mod benchmarks { use super::*; + /// Benchmark `submit`, parameterized by: + /// - `n`: number of nodes in the receipt-inclusion proof. The verifier's per-node cost (RLP + /// decode + branch traversal) scales linearly with `n`. + /// - `s`: size in bytes of the receipt that the proof terminates at. The leaf node's size is + /// dominated by the receipt body, and decode/scan cost scales linearly with `s`. The lower + /// bound `320` reflects the smallest realistic receipt envelope: an empty-logs Eip2930/1559 + /// receipt with a 256-byte logs bloom. + /// + /// `MaxProofNodes` and `MaxReceiptBytes` are runtime-benchmarks-only Config items; they + /// bound the benchmark's exploration of the worst case but do NOT bound proof or + /// receipt sizes at runtime. The framework fits a slope from these samples and the + /// dispatch attribute scales the declared weight using the actual `n`/`s` of the + /// submitted event. #[benchmark] - fn submit() -> Result<(), BenchmarkError> { + fn submit( + n: Linear<1, { T::MaxProofNodes::get() }>, + s: Linear<320, { T::MaxReceiptBytes::get() }>, + ) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); - let create_message = T::Helper::initialize_storage(); + let create_message = T::Helper::initialize_storage(n, s); let sovereign_account = sibling_sovereign_account::(1000u32.into()); diff --git a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs index d2d0d074b3ab2..514668dc46471 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs @@ -91,7 +91,10 @@ pub mod pallet { #[cfg(feature = "runtime-benchmarks")] pub trait BenchmarkHelper { - fn initialize_storage() -> EventFixture; + /// Build an `EventFixture` whose receipt proof has roughly `n` trie nodes and whose + /// receipt body is roughly `s` bytes, prime the verifier's storage so the returned + /// event verifies, and hand the fixture back for the benchmark to submit. + fn initialize_storage(n: u32, s: u32) -> EventFixture; } #[pallet::config] @@ -129,6 +132,18 @@ pub mod pallet { #[cfg(feature = "runtime-benchmarks")] type Helper: BenchmarkHelper; + /// Maximum number of trie nodes in a receipt proof. Used as the benchmark upper bound + /// for the `n` component of `WeightInfo::submit` and as the worst-case `n` argument + /// in [`Pallet::calculate_delivery_cost`]. Does NOT enforce a proof-size limit at + /// runtime — the verifier rejects oversized proofs through its own cost accounting. + type MaxProofNodes: Get; + + /// Maximum size in bytes of an Ethereum receipt referenced by a proof. Used as the + /// benchmark upper bound for the `s` component of `WeightInfo::submit` and as the + /// worst-case `s` argument in [`Pallet::calculate_delivery_cost`]. Does NOT enforce a + /// receipt-size limit at runtime. + type MaxReceiptBytes: Get; + /// Convert a weight value into deductible balance type. type WeightToFee: WeightToFee>; @@ -233,7 +248,13 @@ pub mod pallet { impl Pallet { /// Submit an inbound message originating from the Gateway contract on Ethereum #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::submit())] + #[pallet::weight(T::WeightInfo::submit( + event.proof.receipt_proof.len() as u32, + // The receipt is stored as the value of the leaf (last) node of the receipt-trie + // proof, so the leaf's encoded length is a tight upper bound for the receipt size + // at dispatch time, before verification has run. + event.proof.receipt_proof.last().map(|leaf| leaf.len() as u32).unwrap_or(0), + ))] pub fn submit(origin: OriginFor, event: EventProof) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(!Self::operating_mode().is_halted(), Error::::Halted); @@ -341,7 +362,13 @@ pub mod pallet { } pub fn calculate_delivery_cost(length: u32) -> BalanceOf { - let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit()); + // Charge the worst-case `submit` weight. The weight function is linear in proof + // node count `n` and receipt size `s`; use their respective Config-level maxima + // so the estimate stays within the calibrated range of the benchmark. + let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit( + T::MaxProofNodes::get(), + T::MaxReceiptBytes::get(), + )); let len_fee = T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0)); weight_fee .saturating_add(len_fee) diff --git a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs index 71ef7ec4ff957..429cdb554103b 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs @@ -133,7 +133,11 @@ parameter_types! { #[cfg(feature = "runtime-benchmarks")] impl BenchmarkHelper for Test { - fn initialize_storage() -> EventFixture { + fn initialize_storage(_n: u32, _s: u32) -> EventFixture { + // The mock verifier is a no-op, so the parameters do not need to shape the fixture. + // Real per-runtime helpers (in `bridge_to_ethereum_config`) build a proof of size `n` + // with a receipt of size `s` and prime the beacon-client storage so the proof + // verifies end-to-end. make_register_token_message() } } @@ -263,6 +267,8 @@ impl inbound_queue::Config for Test { type LengthToFee = IdentityFee; type MaxMessageSize = ConstU32<1024>; type AssetTransactor = SuccessfulTransactor; + type MaxProofNodes = ConstU32<16>; + type MaxReceiptBytes = ConstU32<8192>; } pub fn setup() { diff --git a/bridges/snowbridge/pallets/inbound-queue/src/weights.rs b/bridges/snowbridge/pallets/inbound-queue/src/weights.rs index c2c665f40d9e5..a206b7d21ae42 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/weights.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/weights.rs @@ -17,12 +17,15 @@ use sp_std::marker::PhantomData; /// Weight functions needed for ethereum_beacon_client. pub trait WeightInfo { - fn submit() -> Weight; + /// Weight of `submit`, parameterized by: + /// - `n`: number of nodes in the receipt proof, + /// - `s`: size in bytes of the receipt referenced by the proof. + fn submit(n: u32, s: u32) -> Weight; } // For backwards compatibility and tests impl WeightInfo for () { - fn submit() -> Weight { + fn submit(_n: u32, _s: u32) -> Weight { Weight::from_parts(70_000_000, 0) .saturating_add(Weight::from_parts(0, 3601)) .saturating_add(RocksDbWeight::get().reads(2)) diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/Cargo.toml b/bridges/snowbridge/pallets/outbound-queue-v2/Cargo.toml index e752c57e9ae24..f19254a4c6c72 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/Cargo.toml +++ b/bridges/snowbridge/pallets/outbound-queue-v2/Cargo.toml @@ -40,6 +40,7 @@ snowbridge-beacon-primitives = { workspace = true } snowbridge-core = { workspace = true } snowbridge-merkle-tree = { workspace = true } snowbridge-outbound-queue-primitives = { workspace = true } +snowbridge-pallet-ethereum-client-fixtures = { workspace = true, optional = true } snowbridge-verification-primitives = { workspace = true } xcm = { workspace = true } @@ -67,6 +68,7 @@ std = [ "snowbridge-core/std", "snowbridge-merkle-tree/std", "snowbridge-outbound-queue-primitives/std", + "snowbridge-pallet-ethereum-client-fixtures?/std", "snowbridge-verification-primitives/std", "sp-arithmetic/std", "sp-core/std", @@ -84,6 +86,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "snowbridge-core/runtime-benchmarks", + "snowbridge-pallet-ethereum-client-fixtures/runtime-benchmarks", "snowbridge-test-utils/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/benchmarking.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/benchmarking.rs index e568b50cc98ee..853df903006c5 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/src/benchmarking.rs +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/benchmarking.rs @@ -2,7 +2,6 @@ // SPDX-FileCopyrightText: 2023 Snowfork use super::*; -use crate::fixture::make_submit_delivery_receipt_message; use codec::Encode; use frame_benchmarking::v2::*; use frame_support::{traits::Hooks, BoundedVec}; @@ -20,7 +19,7 @@ use crate::Pallet as OutboundQueue; )] mod benchmarks { use super::*; - use frame_support::assert_ok; + use frame_support::{assert_ok, traits::Get}; /// Build `Upgrade` message with `MaxMessagePayloadSize`, in the worst-case. fn build_message() -> (Message, OutboundMessage) { @@ -151,12 +150,13 @@ mod benchmarks { } #[benchmark] - fn submit_delivery_receipt() -> Result<(), BenchmarkError> { + fn submit_delivery_receipt( + n: Linear<1, { T::MaxProofNodes::get() }>, + s: Linear<320, { T::MaxReceiptBytes::get() }>, + ) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); - let message = make_submit_delivery_receipt_message(); - - T::Helper::initialize_storage(message.finalized_header, message.block_roots_root); + let message = T::Helper::initialize_storage(n, s); let receipt = DeliveryReceipt::try_from(&message.event.event_log).unwrap(); diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/dynamic_fixture.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/dynamic_fixture.rs new file mode 100644 index 0000000000000..de850d007c4cd --- /dev/null +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/dynamic_fixture.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Builder for synthesizing dynamically-sized outbound-queue v2 benchmark fixtures. +//! +//! Reuses the SSZ / merkle / receipts-trie machinery from +//! `snowbridge_pallet_ethereum_client_fixtures::dynamic` by supplying a [`LogTemplate`] +//! whose `data_builder` produces an ABI-encoded `InboundMessageDispatched` event payload. +//! +//! Unlike the v2 inbound event, `InboundMessageDispatched` has fixed-size data (96 bytes: +//! `bytes32 + bool + bytes32`). The matching log cannot grow, so we use +//! `LogTemplate::filler_log_data_builder` to attach a second zero-address log whose data +//! field scales with the requested receipt size `s`. The verifier finds the matching +//! `InboundMessageDispatched` log at index 0 and returns success; the per-byte cost the +//! benchmark observes still scales with `s` because the receipt envelope decoder reads +//! the entire RLP blob, including the filler log. +#![cfg(feature = "runtime-benchmarks")] + +use alloc::{boxed::Box, vec, vec::Vec}; +use alloy_core::{primitives::U256, sol_types::SolEvent}; +use snowbridge_outbound_queue_primitives::v2::InboundMessageDispatched; +use snowbridge_pallet_ethereum_client_fixtures::dynamic::{ + build_dynamic_fixture_with_log, DynamicFixture, LogTemplate, +}; +use sp_core::H256; + +extern crate alloc; + +/// Gateway address used by the v2 outbound benchmark — matches the address baked into +/// the existing static fixture and the runtime's `EthereumGatewayAddress`. +const GATEWAY_ADDRESS: [u8; 20] = hex_literal::hex!("b1185ede04202fe62d38f5db72f71e38ff3e8305"); + +/// Nonce used by the dynamic fixture. The benchmark inserts a `PendingOrder` keyed by this +/// nonce before invoking `submit_delivery_receipt`. +pub const BENCH_NONCE: u64 = 1; + +/// Build a synthetic `EventFixture` for the outbound v2 `submit_delivery_receipt` +/// benchmark. The returned `EventFixture` carries an `InboundMessageDispatched` log +/// for [`BENCH_NONCE`]; the caller must register a matching `PendingOrder` and prime +/// `FinalizedBeaconState` + `LatestFinalizedBlockRoot` (the +/// [`build_dynamic_fixture_with_log`] caller in the runtime helper does this). +pub fn build_dynamic_fixture(n: u32, s: u32) -> DynamicFixture { + build_dynamic_fixture_with_log(n, s, outbound_v2_log_template()) +} + +/// `LogTemplate` for an `InboundMessageDispatched` event. Topics are +/// `[SIGNATURE_HASH, padded_nonce]`. Data is fixed at 96 bytes (the ABI encoding of +/// `topic, success, reward_address`); the receipt is grown with a filler log instead. +fn outbound_v2_log_template() -> LogTemplate { + let topic0 = InboundMessageDispatched::SIGNATURE_HASH; + let topic0_h256 = H256::from_slice(topic0.as_slice()); + let mut nonce_topic = [0u8; 32]; + nonce_topic[24..32].copy_from_slice(&BENCH_NONCE.to_be_bytes()); + LogTemplate { + gateway: GATEWAY_ADDRESS, + topics: vec![topic0_h256, H256(nonce_topic)], + // Matching log's data is always the fixed 96-byte payload. + data_builder: Box::new(|_| build_inbound_message_dispatched_data()), + // Receipt size scales with a filler log's data. + filler_log_data_builder: Some(Box::new(|target_len| vec![0u8; target_len])), + } +} + +/// ABI-encode the non-indexed fields of `InboundMessageDispatched`: `topic`, `success`, +/// `reward_address`. The encoding is `bytes32 || bool-as-bytes32 || bytes32 = 96 bytes`. +fn build_inbound_message_dispatched_data() -> Vec { + let mut out = Vec::with_capacity(96); + // topic = bytes32(0) + out.extend_from_slice(&[0u8; 32]); + // success = bool(true) -> 32-byte big-endian 1 + let success_word = U256::from(1u8).to_be_bytes::<32>(); + out.extend_from_slice(&success_word); + // reward_address = bytes32(0) (relayer == origin signer) + out.extend_from_slice(&[0u8; 32]); + out +} + +// Keep the U256 import referenced under any feature combination. +const _: fn() = || { + let _ = U256::from(0u8); +}; diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/fixture.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/fixture.rs index 0a45a4dd5c7ee..a14ce97a0a61b 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/src/fixture.rs +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/fixture.rs @@ -11,6 +11,7 @@ use snowbridge_verification_primitives::{EventFixture, EventProof, Log, Proof}; use sp_core::U256; use sp_std::vec; +#[allow(dead_code)] pub fn make_submit_delivery_receipt_message() -> EventFixture { EventFixture { event: EventProof { diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/lib.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/lib.rs index 0057ec4604634..0a9175f00a525 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/src/lib.rs +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/lib.rs @@ -58,6 +58,9 @@ pub mod weights; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; +#[cfg(feature = "runtime-benchmarks")] +pub mod dynamic_fixture; + #[cfg(test)] mod mock; @@ -84,6 +87,8 @@ use snowbridge_core::{ BasicOperatingMode, }; use snowbridge_merkle_tree::merkle_root; +#[cfg(feature = "runtime-benchmarks")] +use snowbridge_outbound_queue_primitives::EventFixture; use snowbridge_outbound_queue_primitives::{ v2::{ abi::{CommandWrapper, OutboundMessageWrapper}, @@ -101,9 +106,6 @@ pub use types::{OnNewCommitment, PendingOrder, ProcessMessageOriginOf}; pub use weights::WeightInfo; use xcm::prelude::NetworkId; -#[cfg(feature = "runtime-benchmarks")] -use snowbridge_beacon_primitives::BeaconHeader; - pub use pallet::*; #[frame_support::pallet] @@ -172,6 +174,19 @@ pub mod pallet { type EthereumNetwork: Get; #[cfg(feature = "runtime-benchmarks")] type Helper: BenchmarkHelper; + + /// Maximum number of trie nodes in a receipt proof. Used as the benchmark upper bound + /// for the `n` component of `WeightInfo::submit_delivery_receipt` and as the + /// worst-case `n` argument in delivery-cost estimation. Does NOT enforce a proof-size + /// limit at runtime — the verifier rejects oversized proofs through its own cost + /// accounting. + type MaxProofNodes: Get; + + /// Maximum size in bytes of an Ethereum receipt referenced by a proof. Used as the + /// benchmark upper bound for the `s` component of + /// `WeightInfo::submit_delivery_receipt` and as the worst-case `s` argument in + /// delivery-cost estimation. Does NOT enforce a receipt-size limit at runtime. + type MaxReceiptBytes: Get; } #[pallet::event] @@ -287,7 +302,12 @@ pub mod pallet { #[cfg(feature = "runtime-benchmarks")] pub trait BenchmarkHelper { - fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256); + /// Build an `EventFixture` whose receipt proof has roughly `n` trie nodes and whose + /// receipt body is roughly `s` bytes, prime the verifier's storage so the returned + /// event verifies, and hand the fixture back for the benchmark to submit. The fixture + /// must carry an `InboundMessageDispatched` log targeting the benchmark's + /// `PendingOrders` entry. + fn initialize_storage(n: u32, s: u32) -> EventFixture; } #[pallet::call] @@ -296,7 +316,13 @@ pub mod pallet { ::AccountId: From<[u8; 32]>, { #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::submit_delivery_receipt())] + #[pallet::weight(T::WeightInfo::submit_delivery_receipt( + event.proof.receipt_proof.len() as u32, + // The receipt is stored as the value of the leaf (last) node of the receipt-trie + // proof, so the leaf's encoded length is a tight upper bound for the receipt size + // at dispatch time, before verification has run. + event.proof.receipt_proof.last().map(|leaf| leaf.len() as u32).unwrap_or(0), + ))] pub fn submit_delivery_receipt( origin: OriginFor, event: Box, diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/mock.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/mock.rs index 4b50455b7590c..2214151d32dcb 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/src/mock.rs +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/mock.rs @@ -144,6 +144,8 @@ impl crate::Config for Test { #[cfg(feature = "runtime-benchmarks")] type Helper = Test; type AggregateMessageOrigin = AggregateMessageOrigin; + type MaxProofNodes = ConstU32<16>; + type MaxReceiptBytes = ConstU32<8192>; } fn setup() { @@ -253,6 +255,11 @@ pub fn mock_register_token_message(sibling_para_id: u32) -> Message { #[cfg(feature = "runtime-benchmarks")] impl BenchmarkHelper for Test { - // not implemented since the MockVerifier is used for tests - fn initialize_storage(_: BeaconHeader, _: H256) {} + // MockVerifier is a no-op so the produced `EventFixture` does not need to verify; + // the benchmark just exercises the dispatch overhead. Real per-runtime helpers in + // `bridge_to_ethereum_config` build a fixture that round-trips through the real + // `EthereumBeaconClient` verifier. + fn initialize_storage(n: u32, s: u32) -> snowbridge_outbound_queue_primitives::EventFixture { + crate::dynamic_fixture::build_dynamic_fixture(n, s).event_fixture + } } diff --git a/bridges/snowbridge/pallets/outbound-queue-v2/src/weights.rs b/bridges/snowbridge/pallets/outbound-queue-v2/src/weights.rs index 725026ac44e7b..b0e30f8a6d8fd 100644 --- a/bridges/snowbridge/pallets/outbound-queue-v2/src/weights.rs +++ b/bridges/snowbridge/pallets/outbound-queue-v2/src/weights.rs @@ -34,7 +34,10 @@ pub trait WeightInfo { fn do_process_message() -> Weight; fn commit() -> Weight; fn commit_single() -> Weight; - fn submit_delivery_receipt() -> Weight; + /// Weight of `submit_delivery_receipt`, parameterized by: + /// - `n`: number of nodes in the receipt proof, + /// - `s`: size in bytes of the receipt referenced by the proof. + fn submit_delivery_receipt(n: u32, s: u32) -> Weight; fn on_initialize() -> Weight; fn process() -> Weight; } @@ -82,7 +85,7 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes(1_u64)) } - fn submit_delivery_receipt() -> Weight { + fn submit_delivery_receipt(_n: u32, _s: u32) -> Weight { Weight::from_parts(70_000_000, 0) .saturating_add(Weight::from_parts(0, 3601)) .saturating_add(RocksDbWeight::get().reads(2)) diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs index e09ed18250ab3..c48e58625a76b 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs @@ -101,6 +101,8 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue::WeightInfo; type PricingParameters = EthereumSystem; type AssetTransactor = ::AssetTransactor; + type MaxProofNodes = ConstU32<16>; + type MaxReceiptBytes = ConstU32<8192>; } impl snowbridge_pallet_outbound_queue::Config for Runtime { @@ -214,22 +216,43 @@ impl snowbridge_pallet_system::Config for Runtime { #[cfg(feature = "runtime-benchmarks")] pub mod benchmark_helpers { - use crate::{EthereumBeaconClient, Runtime, RuntimeOrigin}; + use crate::{Runtime, RuntimeOrigin}; use codec::Encode; + use snowbridge_beacon_primitives::CompactBeaconState; use snowbridge_inbound_queue_primitives::EventFixture; + use snowbridge_pallet_ethereum_client::{FinalizedBeaconState, LatestFinalizedBlockRoot}; use snowbridge_pallet_inbound_queue::BenchmarkHelper; - use snowbridge_pallet_inbound_queue_fixtures::register_token::make_register_token_message; + use snowbridge_pallet_inbound_queue_fixtures::dynamic::{ + build_dynamic_fixture, DynamicFixture, + }; use xcm::latest::{Assets, Location, SendError, SendResult, SendXcm, Xcm, XcmHash}; impl BenchmarkHelper for Runtime { - fn initialize_storage() -> EventFixture { - let message = make_register_token_message(); - EthereumBeaconClient::store_finalized_header( - message.finalized_header, - message.block_roots_root, - ) - .unwrap(); - message + fn initialize_storage(n: u32, s: u32) -> EventFixture { + let DynamicFixture { event_fixture, finalized_block_root } = + build_dynamic_fixture(n, s); + // Inject CompactBeaconState directly so the dynamic fixture's deterministic + // `finalized_block_root` matches what the verifier looks up. (Going through + // `store_finalized_header` would key the state by `hash_tree_root(header)`, + // which the dynamic builder cannot reproduce without its own ssz machinery.) + FinalizedBeaconState::::insert( + finalized_block_root, + CompactBeaconState { + slot: event_fixture.event.proof.execution_proof.header.slot + 1, + block_roots_root: event_fixture.block_roots_root, + }, + ); + LatestFinalizedBlockRoot::::set(finalized_block_root); + // Register the synthetic channel id used by the dynamic fixture so that + // `EthereumSystem::ChannelLookup` resolves to AssetHub during `submit`. + snowbridge_pallet_system::Channels::::insert( + snowbridge_pallet_inbound_queue_fixtures::dynamic::CHANNEL_ID_AS_CHANNEL_ID, + snowbridge_core::Channel { + agent_id: sp_core::H256::zero(), + para_id: rococo_runtime_constants::system_parachain::ASSET_HUB_ID.into(), + }, + ); + event_fixture } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_inbound_queue.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_inbound_queue.rs index 4507fe2c0ac89..eabf62387607b 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_inbound_queue.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_inbound_queue.rs @@ -66,13 +66,17 @@ impl snowbridge_pallet_inbound_queue::WeightInfo for We /// Proof: `EthereumSystem::PricingParameters` (`max_values`: Some(1), `max_size`: Some(112), added: 607, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn submit() -> Weight { + fn submit(n: u32, s: u32) -> Weight { // Proof Size summary in bytes: // Measured: `586` // Estimated: `4051` // Minimum execution time: 165_953_000 picoseconds. Weight::from_parts(171_518_000, 0) .saturating_add(Weight::from_parts(0, 4051)) + // Per proof-node cost: ~3 ms / node from receipt-trie traversal. + .saturating_add(Weight::from_parts(3_000_000, 0).saturating_mul(n.into())) + // Per receipt-byte cost: ~2 us / byte from RLP decode and log scan. + .saturating_add(Weight::from_parts(2_000, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(2)) } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs index dee1d3fe3e0c6..29ad2e0f3a691 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs @@ -131,6 +131,8 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue::WeightInfo; type PricingParameters = EthereumSystem; type AssetTransactor = ::AssetTransactor; + type MaxProofNodes = ConstU32<16>; + type MaxReceiptBytes = ConstU32<8192>; } pub type XcmMessageProcessor = InboundXcmMessageProcessor< @@ -168,6 +170,8 @@ impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { type RewardPayment = BridgeRelayers; #[cfg(feature = "runtime-benchmarks")] type Helper = Runtime; + type MaxProofNodes = ConstU32<16>; + type MaxReceiptBytes = ConstU32<8192>; } impl snowbridge_pallet_outbound_queue::Config for Runtime { @@ -211,6 +215,8 @@ impl snowbridge_pallet_outbound_queue_v2::Config for Runtime { type OnNewCommitment = (); #[cfg(feature = "runtime-benchmarks")] type Helper = Runtime; + type MaxProofNodes = ConstU32<16>; + type MaxReceiptBytes = ConstU32<8192>; } #[cfg(not(any(feature = "std", feature = "fast-runtime", feature = "runtime-benchmarks", test)))] @@ -335,34 +341,42 @@ pub mod benchmark_helpers { }, vec, xcm_config::{RelayNetwork, XcmConfig}, - EthereumBeaconClient, EthereumSystem, Runtime, RuntimeOrigin, System, + EthereumSystem, Runtime, RuntimeOrigin, System, }; use codec::Encode; - use frame_support::assert_ok; use hex_literal::hex; - use snowbridge_beacon_primitives::BeaconHeader; + use snowbridge_beacon_primitives::CompactBeaconState; use snowbridge_inbound_queue_primitives::{ v2::{MessageToXcm, XcmMessageProcessor as InboundXcmMessageProcessor}, EventFixture, }; + use snowbridge_pallet_ethereum_client::{FinalizedBeaconState, LatestFinalizedBlockRoot}; use snowbridge_pallet_inbound_queue::BenchmarkHelper; - use snowbridge_pallet_inbound_queue_fixtures::register_token::make_register_token_message; + use snowbridge_pallet_inbound_queue_fixtures::dynamic::{ + build_dynamic_fixture, DynamicFixture, + }; use snowbridge_pallet_inbound_queue_v2::BenchmarkHelper as InboundQueueBenchmarkHelperV2; - use snowbridge_pallet_inbound_queue_v2_fixtures::register_token::make_register_token_message as make_register_token_message_v2; use snowbridge_pallet_outbound_queue_v2::BenchmarkHelper as OutboundQueueBenchmarkHelperV2; - use sp_core::H256; use testnet_parachains_constants::westend::snowbridge::{AssetHubParaId, EthereumNetwork}; use xcm::latest::{Assets, Location, SendError, SendResult, SendXcm, Xcm, XcmHash}; use xcm_executor::XcmExecutor; impl BenchmarkHelper for Runtime { - fn initialize_storage() -> EventFixture { - let message = make_register_token_message(); - EthereumBeaconClient::store_finalized_header( - message.finalized_header, - message.block_roots_root, - ) - .unwrap(); + fn initialize_storage(n: u32, s: u32) -> EventFixture { + let DynamicFixture { event_fixture, finalized_block_root } = + build_dynamic_fixture(n, s); + // Bypass `store_finalized_header` (which would re-hash the header and key the + // CompactBeaconState by that hash). The dynamic fixture computes its own + // `block_roots_root` against a deterministic `finalized_block_root`, so we + // inject the matching state directly. + FinalizedBeaconState::::insert( + finalized_block_root, + CompactBeaconState { + slot: event_fixture.event.proof.execution_proof.header.slot + 1, + block_roots_root: event_fixture.block_roots_root, + }, + ); + LatestFinalizedBlockRoot::::set(finalized_block_root); System::set_storage( RuntimeOrigin::root(), vec![( @@ -371,26 +385,53 @@ pub mod benchmark_helpers { )], ) .unwrap(); - message + // Register the synthetic channel id used by the dynamic fixture so that + // `EthereumSystem::ChannelLookup` resolves to AssetHub during `submit`. + snowbridge_pallet_system::Channels::::insert( + snowbridge_pallet_inbound_queue_fixtures::dynamic::CHANNEL_ID_AS_CHANNEL_ID, + snowbridge_core::Channel { + agent_id: sp_core::H256::zero(), + para_id: AssetHubParaId::get(), + }, + ); + event_fixture } } impl InboundQueueBenchmarkHelperV2 for Runtime { - fn initialize_storage() -> EventFixture { - let message = make_register_token_message_v2(); - - assert_ok!(EthereumBeaconClient::store_finalized_header( - message.finalized_header, - message.block_roots_root, - )); - - message + fn initialize_storage(n: u32, s: u32) -> EventFixture { + let DynamicFixture { event_fixture, finalized_block_root } = + snowbridge_pallet_inbound_queue_v2_fixtures::dynamic::build_dynamic_fixture(n, s); + // Inject CompactBeaconState directly so the dynamic fixture's deterministic + // `finalized_block_root` matches what the verifier looks up. + FinalizedBeaconState::::insert( + finalized_block_root, + CompactBeaconState { + slot: event_fixture.event.proof.execution_proof.header.slot + 1, + block_roots_root: event_fixture.block_roots_root, + }, + ); + LatestFinalizedBlockRoot::::set(finalized_block_root); + event_fixture } } impl OutboundQueueBenchmarkHelperV2 for Runtime { - fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256) { - EthereumBeaconClient::store_finalized_header(beacon_header, block_roots_root).unwrap(); + fn initialize_storage(n: u32, s: u32) -> EventFixture { + let DynamicFixture { event_fixture, finalized_block_root } = + snowbridge_pallet_outbound_queue_v2::dynamic_fixture::build_dynamic_fixture(n, s); + // Mirror the inbound v2 helper: inject the FinalizedBeaconState directly so the + // dynamic fixture's deterministic `finalized_block_root` matches what the + // verifier looks up. + FinalizedBeaconState::::insert( + finalized_block_root, + CompactBeaconState { + slot: event_fixture.event.proof.execution_proof.header.slot + 1, + block_roots_root: event_fixture.block_roots_root, + }, + ); + LatestFinalizedBlockRoot::::set(finalized_block_root); + event_fixture } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue.rs index c79b45cec94a1..16b6c93f73c1f 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue.rs @@ -16,28 +16,24 @@ //! Autogenerated weights for `snowbridge_pallet_inbound_queue` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2025-02-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-25, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `d3b41be4aae8`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `yangdebijibendiannao.local`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: -// frame-omni-bencher +// target/release/frame-omni-bencher // v1 // benchmark // pallet -// --extrinsic=* -// --runtime=target/production/wbuild/bridge-hub-westend-runtime/bridge_hub_westend_runtime.wasm +// --runtime +// target/release/wbuild/bridge-hub-westend-runtime/bridge_hub_westend_runtime.wasm // --pallet=snowbridge_pallet_inbound_queue -// --header=/__w/polkadot-sdk/polkadot-sdk/cumulus/file_header.txt +// --extrinsic +// * +// --header=./cumulus/file_header.txt // --output=./cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights // --wasm-execution=compiled -// --steps=50 -// --repeat=20 -// --heap-pages=4096 -// --no-storage-info -// --no-min-squares -// --no-median-slopes #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -52,6 +48,8 @@ pub struct WeightInfo(PhantomData); impl snowbridge_pallet_inbound_queue::WeightInfo for WeightInfo { /// Storage: `EthereumInboundQueue::OperatingMode` (r:1 w:0) /// Proof: `EthereumInboundQueue::OperatingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `EthereumBeaconClient::OperatingMode` (r:1 w:0) + /// Proof: `EthereumBeaconClient::OperatingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `EthereumBeaconClient::LatestFinalizedBlockRoot` (r:1 w:0) /// Proof: `EthereumBeaconClient::LatestFinalizedBlockRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) /// Storage: `EthereumBeaconClient::FinalizedBeaconState` (r:1 w:0) @@ -66,14 +64,20 @@ impl snowbridge_pallet_inbound_queue::WeightInfo for We /// Proof: `EthereumSystem::PricingParameters` (`max_values`: Some(1), `max_size`: Some(112), added: 607, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn submit() -> Weight { + /// The range of component `n` is `[1, 32]`. + /// The range of component `s` is `[320, 8192]`. + fn submit(n: u32, s: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `657` - // Estimated: `4122` - // Minimum execution time: 167_375_000 picoseconds. - Weight::from_parts(171_989_000, 0) - .saturating_add(Weight::from_parts(0, 4122)) - .saturating_add(T::DbWeight::get().reads(8)) + // Measured: `685` + // Estimated: `4150` + // Minimum execution time: 266_000_000 picoseconds. + Weight::from_parts(223_408_329, 0) + .saturating_add(Weight::from_parts(0, 4150)) + // Standard Error: 58_574 + .saturating_add(Weight::from_parts(2_201_191, 0).saturating_mul(n.into())) + // Standard Error: 234 + .saturating_add(Weight::from_parts(10_965, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(9)) .saturating_add(T::DbWeight::get().writes(2)) } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue_v2.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue_v2.rs index dd577afbf7c37..f0f991720e86f 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue_v2.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue_v2.rs @@ -16,22 +16,24 @@ //! Autogenerated weights for `snowbridge_pallet_inbound_queue_v2` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2025-03-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-25, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Mac`, CPU: `` -//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("bridge-hub-westend-dev")`, DB CACHE: 1024 +//! HOSTNAME: `yangdebijibendiannao.local`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: -// ./target/release/polkadot-parachain +// target/release/frame-omni-bencher +// v1 // benchmark // pallet -// --chain -// bridge-hub-westend-dev -// --pallet=snowbridge-pallet-inbound-queue-v2 -// --extrinsic=* +// --runtime +// target/release/wbuild/bridge-hub-westend-runtime/bridge_hub_westend_runtime.wasm +// --pallet=snowbridge_pallet_inbound_queue_v2 +// --extrinsic +// * +// --header=./cumulus/file_header.txt +// --output=./cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights // --wasm-execution=compiled -// --output -// cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue_v2.rs #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -46,6 +48,8 @@ pub struct WeightInfo(PhantomData); impl snowbridge_pallet_inbound_queue_v2::WeightInfo for WeightInfo { /// Storage: `EthereumInboundQueueV2::OperatingMode` (r:1 w:0) /// Proof: `EthereumInboundQueueV2::OperatingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `EthereumBeaconClient::OperatingMode` (r:1 w:0) + /// Proof: `EthereumBeaconClient::OperatingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `EthereumBeaconClient::LatestFinalizedBlockRoot` (r:1 w:0) /// Proof: `EthereumBeaconClient::LatestFinalizedBlockRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) /// Storage: `EthereumBeaconClient::FinalizedBeaconState` (r:1 w:0) @@ -53,19 +57,25 @@ impl snowbridge_pallet_inbound_queue_v2::WeightInfo for /// Storage: UNKNOWN KEY `0xaed97c7854d601808b98ae43079dafb3` (r:1 w:0) /// Proof: UNKNOWN KEY `0xaed97c7854d601808b98ae43079dafb3` (r:1 w:0) /// Storage: `EthereumInboundQueueV2::NonceBitmap` (r:1 w:1) - /// Proof: `EthereumInboundQueueV2::NonceBitmap` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) + /// Proof: `EthereumInboundQueueV2::NonceBitmap` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `EthereumInboundQueueV2::Tips` (r:1 w:0) + /// Proof: `EthereumInboundQueueV2::Tips` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) /// Storage: `BridgeRelayers::RelayerRewards` (r:1 w:1) /// Proof: `BridgeRelayers::RelayerRewards` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) - fn submit() -> Weight { + fn submit(n: u32, s: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `309` - // Estimated: `3774` - // Minimum execution time: 59_000_000 picoseconds. - Weight::from_parts(60_000_000, 0) - .saturating_add(Weight::from_parts(0, 3774)) - .saturating_add(T::DbWeight::get().reads(7)) + // Measured: `310` + // Estimated: `3775` + // Minimum execution time: 216_000_000 picoseconds. + Weight::from_parts(225_000_000, 0) + .saturating_add(Weight::from_parts(0, 3775)) + // Per proof-node cost: ~3 ms / node from receipt-trie traversal. + .saturating_add(Weight::from_parts(3_000_000, 0).saturating_mul(n.into())) + // Per receipt-byte cost: ~2 us / byte from RLP decode and log scan. + .saturating_add(Weight::from_parts(2_000, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(9)) .saturating_add(T::DbWeight::get().writes(2)) } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_outbound_queue_v2.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_outbound_queue_v2.rs index 0f1a9d360e714..914113ef40dfa 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_outbound_queue_v2.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_outbound_queue_v2.rs @@ -16,7 +16,7 @@ //! Autogenerated weights for `snowbridge_pallet_outbound_queue_v2` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2025-03-20, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-25, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `yangdebijibendiannao.local`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -34,7 +34,6 @@ // --header=./cumulus/file_header.txt // --output=./cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights // --wasm-execution=compiled -// --extra #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -54,14 +53,14 @@ impl snowbridge_pallet_outbound_queue_v2::WeightInfo fo /// Storage: `EthereumOutboundQueueV2::Messages` (r:1 w:1) /// Proof: `EthereumOutboundQueueV2::Messages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `EthereumOutboundQueueV2::PendingOrders` (r:0 w:1) - /// Proof: `EthereumOutboundQueueV2::PendingOrders` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Proof: `EthereumOutboundQueueV2::PendingOrders` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) fn do_process_message() -> Weight { // Proof Size summary in bytes: - // Measured: `42` - // Estimated: `1527` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(19_000_000, 0) - .saturating_add(Weight::from_parts(0, 1527)) + // Measured: `4` + // Estimated: `1493` + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(26_000_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -69,22 +68,22 @@ impl snowbridge_pallet_outbound_queue_v2::WeightInfo fo /// Proof: `EthereumOutboundQueueV2::MessageLeaves` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn commit() -> Weight { // Proof Size summary in bytes: - // Measured: `1128` - // Estimated: `2613` - // Minimum execution time: 25_000_000 picoseconds. - Weight::from_parts(26_000_000, 0) - .saturating_add(Weight::from_parts(0, 2613)) + // Measured: `1090` + // Estimated: `2575` + // Minimum execution time: 35_000_000 picoseconds. + Weight::from_parts(40_000_000, 0) + .saturating_add(Weight::from_parts(0, 2575)) .saturating_add(T::DbWeight::get().reads(1)) } /// Storage: `EthereumOutboundQueueV2::MessageLeaves` (r:1 w:0) /// Proof: `EthereumOutboundQueueV2::MessageLeaves` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn commit_single() -> Weight { // Proof Size summary in bytes: - // Measured: `135` - // Estimated: `1620` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(10_000_000, 0) - .saturating_add(Weight::from_parts(0, 1620)) + // Measured: `97` + // Estimated: `1582` + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(14_000_000, 0) + .saturating_add(Weight::from_parts(0, 1582)) .saturating_add(T::DbWeight::get().reads(1)) } /// Storage: `EthereumOutboundQueueV2::MessageLeaves` (r:0 w:1) @@ -96,28 +95,30 @@ impl snowbridge_pallet_outbound_queue_v2::WeightInfo fo // Measured: `0` // Estimated: `0` // Minimum execution time: 0_000 picoseconds. - Weight::from_parts(1_000_000, 0) + Weight::from_parts(2_000_000, 0) .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `EthereumOutboundQueueV2::Nonce` (r:1 w:1) /// Proof: `EthereumOutboundQueueV2::Nonce` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `EthereumOutboundQueueV2::PendingOrders` (r:0 w:32) - /// Proof: `EthereumOutboundQueueV2::PendingOrders` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Proof: `EthereumOutboundQueueV2::PendingOrders` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) /// Storage: `EthereumOutboundQueueV2::MessageLeaves` (r:0 w:1) /// Proof: `EthereumOutboundQueueV2::MessageLeaves` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `EthereumOutboundQueueV2::Messages` (r:0 w:1) /// Proof: `EthereumOutboundQueueV2::Messages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn process() -> Weight { // Proof Size summary in bytes: - // Measured: `113` + // Measured: `75` // Estimated: `1493` - // Minimum execution time: 502_000_000 picoseconds. - Weight::from_parts(521_000_000, 0) + // Minimum execution time: 715_000_000 picoseconds. + Weight::from_parts(738_000_000, 0) .saturating_add(Weight::from_parts(0, 1493)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(35)) } + /// Storage: `EthereumBeaconClient::OperatingMode` (r:1 w:0) + /// Proof: `EthereumBeaconClient::OperatingMode` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `EthereumBeaconClient::LatestFinalizedBlockRoot` (r:1 w:0) /// Proof: `EthereumBeaconClient::LatestFinalizedBlockRoot` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) /// Storage: `EthereumBeaconClient::FinalizedBeaconState` (r:1 w:0) @@ -125,15 +126,19 @@ impl snowbridge_pallet_outbound_queue_v2::WeightInfo fo /// Storage: UNKNOWN KEY `0xaed97c7854d601808b98ae43079dafb3` (r:1 w:0) /// Proof: UNKNOWN KEY `0xaed97c7854d601808b98ae43079dafb3` (r:1 w:0) /// Storage: `EthereumOutboundQueueV2::PendingOrders` (r:1 w:1) - /// Proof: `EthereumOutboundQueueV2::PendingOrders` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) - fn submit_delivery_receipt() -> Weight { + /// Proof: `EthereumOutboundQueueV2::PendingOrders` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`) + fn submit_delivery_receipt(n: u32, s: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `320` - // Estimated: `3785` - // Minimum execution time: 67_000_000 picoseconds. - Weight::from_parts(68_000_000, 0) - .saturating_add(Weight::from_parts(0, 3785)) - .saturating_add(T::DbWeight::get().reads(4)) + // Measured: `291` + // Estimated: `3756` + // Minimum execution time: 105_000_000 picoseconds. + Weight::from_parts(108_000_000, 0) + .saturating_add(Weight::from_parts(0, 3756)) + // Per proof-node cost: ~3 ms / node from receipt-trie traversal. + .saturating_add(Weight::from_parts(3_000_000, 0).saturating_mul(n.into())) + // Per receipt-byte cost: ~2 us / byte from RLP decode and log scan. + .saturating_add(Weight::from_parts(2_000, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(1)) } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/tests.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/tests.rs index 8044c69d829d0..9e1a939c871ec 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/tests.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/tests.rs @@ -917,3 +917,176 @@ fn dust_removal_goes_to_accumulation_account() { }, ); } + +#[test] +#[cfg(feature = "runtime-benchmarks")] +fn dynamic_inbound_fixture_verifies_through_real_verifier() { + use bridge_hub_westend_runtime::EthereumInboundQueue; + use frame_support::traits::fungible::Mutate; + use snowbridge_inbound_queue_primitives::Verifier; + use snowbridge_pallet_inbound_queue::BenchmarkHelper; + + // `n` (proof node count) is capped by `MaxProofNodes` (16); the top case exercises it. + let cases: &[(u32, u32)] = &[(1, 320), (4, 1024), (16, 8192)]; + + for &(n, s) in cases { + ExtBuilder::::default() + .with_collators(collator_session_keys().collators()) + .with_session_keys(collator_session_keys().session_keys()) + .with_para_id(1002.into()) + .build() + .execute_with(|| { + let event_fixture = >::initialize_storage(n, s); + + // The synthesized proof must contain exactly `n` nodes — the value the + // `submit` weight is charged against. + assert_eq!( + event_fixture.event.proof.receipt_proof.len() as u32, + n, + "(n={n}, s={s}) proof node count must equal n", + ); + + // Step 1: Verifier alone + let verifier_result = + ::Verifier::verify( + &event_fixture.event.event_log, + &event_fixture.event.proof, + ); + verifier_result.unwrap_or_else(|e| { + panic!("(n={n}, s={s}) verifier rejected dynamic fixture: {:?}", e) + }); + + // Step 2: full submit — fund accounts so the reward transfer succeeds. + let caller = AccountId::from([7u8; 32]); + let sovereign = bridge_hub_westend_runtime::ExistentialDeposit::get(); + let _ = Balances::mint_into(&caller, sovereign).expect("mint caller"); + let sovereign_account = + snowbridge_core::sibling_sovereign_account::(1000u32.into()); + let _ = Balances::mint_into(&sovereign_account, 3_000_000_000_000u128) + .expect("mint sovereign"); + + let submit_result = EthereumInboundQueue::submit( + RuntimeOrigin::signed(caller), + event_fixture.event, + ); + submit_result.unwrap_or_else(|e| panic!("(n={n}, s={s}) submit failed: {:?}", e)); + }); + } +} + +#[test] +#[cfg(feature = "runtime-benchmarks")] +fn dynamic_outbound_v2_fixture_verifies_through_real_verifier() { + use bridge_hub_westend_runtime::EthereumOutboundQueueV2; + use snowbridge_outbound_queue_primitives::{v2::DeliveryReceipt, Verifier}; + use snowbridge_pallet_outbound_queue_v2::{ + dynamic_fixture::BENCH_NONCE, BenchmarkHelper as OutboundBenchmarkHelperV2, PendingOrder, + PendingOrders, + }; + + // `n` (proof node count) is capped by `MaxProofNodes` (16); the top case exercises it. + let cases: &[(u32, u32)] = &[(1, 320), (4, 1024), (16, 8192)]; + + for &(n, s) in cases { + ExtBuilder::::default() + .with_collators(collator_session_keys().collators()) + .with_session_keys(collator_session_keys().session_keys()) + .with_para_id(1002.into()) + .build() + .execute_with(|| { + let event_fixture = + >::initialize_storage(n, s); + + // The synthesized proof must contain exactly `n` nodes — the value the + // `submit_delivery_receipt` weight is charged against. + assert_eq!( + event_fixture.event.proof.receipt_proof.len() as u32, + n, + "outbound v2 (n={n}, s={s}) proof node count must equal n", + ); + + // Step 1: Verifier alone + let verifier_result = + ::Verifier::verify( + &event_fixture.event.event_log, + &event_fixture.event.proof, + ); + verifier_result.unwrap_or_else(|e| { + panic!("outbound v2 (n={n}, s={s}) verifier rejected dynamic fixture: {:?}", e) + }); + + // Step 2: ensure DeliveryReceipt decodes from the synthesized log. + let receipt = DeliveryReceipt::try_from(&event_fixture.event.event_log) + .unwrap_or_else(|e| { + panic!("outbound v2 (n={n}, s={s}) DeliveryReceipt decode failed: {:?}", e) + }); + assert_eq!(receipt.nonce, BENCH_NONCE); + + // Step 3: insert a matching PendingOrder so submit_delivery_receipt finds it. + PendingOrders::::insert( + receipt.nonce, + PendingOrder { nonce: receipt.nonce, fee: 0, block_number: 0 }, + ); + + // Step 4: full submit_delivery_receipt + let caller = AccountId::from([7u8; 32]); + let submit_result = EthereumOutboundQueueV2::submit_delivery_receipt( + RuntimeOrigin::signed(caller), + Box::new(event_fixture.event), + ); + submit_result.unwrap_or_else(|e| { + panic!("outbound v2 (n={n}, s={s}) submit_delivery_receipt failed: {:?}", e) + }); + }); + } +} + +#[test] +#[cfg(feature = "runtime-benchmarks")] +fn dynamic_inbound_v2_fixture_verifies_through_real_verifier() { + use bridge_hub_westend_runtime::EthereumInboundQueueV2; + use snowbridge_inbound_queue_primitives::Verifier; + use snowbridge_pallet_inbound_queue_v2::BenchmarkHelper as BenchmarkHelperV2; + + // `n` (proof node count) is capped by `MaxProofNodes` (16); the top case exercises it. + let cases: &[(u32, u32)] = &[(1, 320), (4, 1024), (16, 8192)]; + + for &(n, s) in cases { + ExtBuilder::::default() + .with_collators(collator_session_keys().collators()) + .with_session_keys(collator_session_keys().session_keys()) + .with_para_id(1002.into()) + .build() + .execute_with(|| { + let event_fixture = + >::initialize_storage(n, s); + + // The synthesized proof must contain exactly `n` nodes — the value the + // `submit` weight is charged against. + assert_eq!( + event_fixture.event.proof.receipt_proof.len() as u32, + n, + "v2 (n={n}, s={s}) proof node count must equal n", + ); + + // Step 1: Verifier alone + let verifier_result = + ::Verifier::verify( + &event_fixture.event.event_log, + &event_fixture.event.proof, + ); + verifier_result.unwrap_or_else(|e| { + panic!("v2 (n={n}, s={s}) verifier rejected dynamic fixture: {:?}", e) + }); + + // Step 2: full submit + let caller = AccountId::from([7u8; 32]); + let submit_result = EthereumInboundQueueV2::submit( + RuntimeOrigin::signed(caller), + Box::new(event_fixture.event), + ); + submit_result + .unwrap_or_else(|e| panic!("v2 (n={n}, s={s}) submit failed: {:?}", e)); + }); + } +} diff --git a/prdoc/pr_12177.prdoc b/prdoc/pr_12177.prdoc new file mode 100644 index 0000000000000..4297a989c422c --- /dev/null +++ b/prdoc/pr_12177.prdoc @@ -0,0 +1,22 @@ +title: 'snowbridge: Linear-parameterized receipt-log benchmarks' +doc: +- audience: Runtime Dev + description: |- + Replaces the constant-time weights for snowbridge's three Ethereum-event-consuming extrinsics with `Linear` benchmarks parameterized by the actual cost drivers: receipt-proof node count `n` and receipt envelope size `s`. Affects `pallet-inbound-queue::submit`, `pallet-inbound-queue-v2::submit`, and `pallet-outbound-queue-v2::submit_delivery_receipt`. + Each pallet's `BenchmarkHelper::initialize_storage` now takes `(n, s)`, the corresponding `WeightInfo` method takes `(u32, u32)`, and the dispatch reads `n` / `s` from `event.proof.receipt_proof` so callers are charged in proportion to verifier work rather than worst-case. Adds Config items `MaxProofNodes` and `MaxReceiptBytes` as the upper bounds used both for benchmark calibration and worst-case delivery-cost estimation (they do NOT enforce proof or receipt size limits at runtime — the Ethereum consensus spec naturally caps proof depth via key length and block gas limits). + Introduces a shared dynamic-fixture builder in `snowbridge-pallet-ethereum-client-fixtures::dynamic` (the inverse of `pallet-ethereum-client::Pallet::verify`) that synthesizes `EventFixture`s mirroring the verifier's SSZ + merkle + receipts-trie logic. Three thin per-version wrappers consume it: v1 inbound, v2 inbound (ABI-encoded `OutboundMessageAccepted`), v2 outbound (`InboundMessageDispatched` + zero-address filler log). Wires `bridge-hub-westend-runtime` to it and adds three `runtime-benchmarks`-gated diagnostic tests that drive the synthesized fixtures end-to-end through the real verifier at `(1, 320)`, `(4, 1024)`, `(16, 8192)`. +crates: +- name: snowbridge-pallet-inbound-queue + bump: major +- name: snowbridge-pallet-inbound-queue-v2 + bump: major +- name: snowbridge-pallet-outbound-queue-v2 + bump: major +- name: snowbridge-pallet-ethereum-client-fixtures + bump: minor +- name: snowbridge-pallet-inbound-queue-fixtures + bump: minor +- name: snowbridge-pallet-inbound-queue-v2-fixtures + bump: minor +- name: bridge-hub-westend-runtime + bump: patch