diff --git a/Cargo.lock b/Cargo.lock index 708364481..3aa275820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2856,6 +2856,7 @@ dependencies = [ "beefy-verifier-primitives", "ckb-merkle-mountain-range", "futures", + "hex", "hex-literal 0.4.1", "ismp", "ismp-solidity-abi", @@ -8229,6 +8230,7 @@ dependencies = [ "ismp-tendermint", "log", "mmr-primitives", + "pallet-beefy-consensus-proofs", "pallet-bridge-airdrop", "pallet-call-decompressor", "pallet-fishermen", @@ -10559,9 +10561,7 @@ dependencies = [ "alloy-transport", "anyhow", "beefy-verifier-primitives", - "ckb-merkle-mountain-range", "ismp", - "mmr-primitives", "polkadot-sdk", "primitive-types 0.13.1", ] @@ -14405,6 +14405,28 @@ dependencies = [ "sp-staking", ] +[[package]] +name = "pallet-beefy-consensus-proofs" +version = "0.1.0" +dependencies = [ + "alloy-sol-types 1.5.7", + "anyhow", + "beefy-verifier", + "beefy-verifier-primitives", + "hex-literal 0.4.1", + "ismp", + "ismp-beefy", + "ismp-solidity-abi", + "log", + "pallet-ismp", + "parity-scale-codec", + "polkadot-sdk", + "primitive-types 0.13.1", + "scale-info", + "sp-core", + "sp-io", +] + [[package]] name = "pallet-beefy-mmr" version = "46.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4c5e9c3cf..aa26d2410 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ members = [ "modules/ismp/state-machines/pharos", "modules/pallets/consensus-incentives", "modules/pallets/messaging-fees", + "modules/pallets/beefy-consensus-proofs", # evm stuff # "evm/integration-tests", @@ -128,7 +129,6 @@ members = [ "tesseract/consensus/tendermint", "tesseract/consensus/pharos", - # Airdrop "modules/pallets/bridge-drop", ] @@ -320,6 +320,7 @@ pallet-ismp-host-executive = { path = "modules/pallets/host-executive", default- pallet-call-decompressor = { path = "modules/pallets/call-decompressor", default-features = false } pallet-consensus-incentives = { path = "modules/pallets/consensus-incentives", default-features = false } pallet-messaging-fees= { path = "modules/pallets/messaging-fees", default-features = false } +pallet-beefy-consensus-proofs = { path = "modules/pallets/beefy-consensus-proofs", default-features = false } pallet-collator-manager = { path = "modules/pallets/collator-manager", default-features = false } pallet-xcm-gateway = { path = "modules/pallets/xcm-gateway", default-features = false } pallet-token-governor = { path = "modules/pallets/token-governor", default-features = false } @@ -409,5 +410,3 @@ features = ["derive"] [workspace.dependencies.reconnecting-jsonrpsee-ws-client] version = "0.5.0" default-features = false - -[patch.crates-io] diff --git a/evm/abi/Cargo.toml b/evm/abi/Cargo.toml index f332222fb..cf60ebd83 100644 --- a/evm/abi/Cargo.toml +++ b/evm/abi/Cargo.toml @@ -7,39 +7,39 @@ description = "Generated rust types for the ISMP solidity ABI" publish = false [dependencies] -primitive-types = { workspace = true } -anyhow = { workspace = true, default-features = true } -ismp = { workspace = true } +primitive-types = { workspace = true, default-features = false } +anyhow = { workspace = true, default-features = false } +ismp = { workspace = true, default-features = false } -alloy-sol-types = { workspace = true } +alloy-sol-types = { workspace = true, default-features = false } alloy-sol-macro = { workspace = true } -alloy-primitives = { workspace = true } -alloy-contract = { workspace = true } -alloy-provider = { workspace = true } -alloy-network = { workspace = true } -alloy-transport = { workspace = true } +alloy-primitives = { workspace = true, default-features = false } -mmr-primitives = { workspace = true, default-features = true, optional = true } -merkle-mountain-range = { workspace = true, default-features = true, optional = true } -beefy-verifier-primitives = { workspace = true, default-features = true, optional = true } +# RPC / contract bindings — std-only. +alloy-contract = { workspace = true, optional = true } +alloy-provider = { workspace = true, optional = true } +alloy-network = { workspace = true, optional = true } +alloy-transport = { workspace = true, optional = true } + +beefy-verifier-primitives = { workspace = true, default-features = false } [dependencies.polkadot-sdk] workspace = true -optional = true -features = ["sp-consensus-beefy"] +default-features = false +features = ["sp-consensus-beefy", "sp-mmr-primitives"] [features] -default = ["beefy", "std"] +default = ["std"] std = [ - "primitive-types/std", + "dep:alloy-contract", + "dep:alloy-provider", + "dep:alloy-network", + "dep:alloy-transport", "anyhow/std", + "primitive-types/std", "ismp/std", "alloy-sol-types/std", "alloy-primitives/std", -] -beefy = [ - "merkle-mountain-range", - "polkadot-sdk", - "beefy-verifier-primitives", - "mmr-primitives", + "beefy-verifier-primitives/std", + "polkadot-sdk/std", ] diff --git a/evm/abi/src/conversions.rs b/evm/abi/src/conversions.rs index f4d68139b..f4582d194 100644 --- a/evm/abi/src/conversions.rs +++ b/evm/abi/src/conversions.rs @@ -14,7 +14,6 @@ // limitations under the License. //! Convenient type conversions -#![allow(unused_imports)] use crate::{ beefy::Beefy::IntermediateState, @@ -28,18 +27,17 @@ use crate::{ }, }; +use alloc::string::{String, ToString}; use alloy_primitives::{FixedBytes, U256}; use anyhow::anyhow; -use ismp::{host::StateMachine, router}; - -#[cfg(feature = "beefy")] -pub use beefy::*; +use core::str::FromStr; use ismp::{ consensus::StateMachineId, events::{StateCommitmentVetoed, StateMachineUpdated, TimeoutHandled}, + host::StateMachine, + router, }; use primitive_types::{H160, H256}; -use std::str::FromStr; /// Helper trait for converting primitive types to alloy U256 trait ToU256 { @@ -64,24 +62,30 @@ impl ToU256 for usize { } } -#[cfg(feature = "beefy")] mod beefy { use super::ToU256; use crate::{ beefy::Beefy::{ AuthoritySetCommitment, BeefyConsensusProof, BeefyConsensusState, BeefyMmrLeaf, - Commitment, Parachain, ParachainProof, Payload, RelayChainProof, - SignedCommitment, Vote, + Commitment, Parachain, ParachainProof, Payload, RelayChainProof, SignedCommitment, + Vote, }, sp1_beefy::SP1Beefy::{MiniCommitment, ParachainHeader, PartialBeefyMmrLeaf}, }; + use alloc::{vec, vec::Vec}; use alloy_primitives::{Bytes, FixedBytes, U256}; use beefy_verifier_primitives::{ - ConsensusMessage, ConsensusState, MmrProof, Sp1BeefyProof, + ConsensusMessage, ConsensusState, MmrProof, ParachainHeader as BvpParachainHeader, + ParachainProof as BvpParachainProof, SignatureWithAuthorityIndex, + SignedCommitment as BvpSignedCommitment, Sp1BeefyProof, TSignature, }; use polkadot_sdk::*; use primitive_types::H256; - use sp_consensus_beefy::mmr::{BeefyNextAuthoritySet, MmrLeafVersion}; + use sp_consensus_beefy::{ + mmr::{BeefyNextAuthoritySet, MmrLeafVersion}, + Payload as BeefyPayload, + }; + use sp_mmr_primitives::LeafProof; impl From for ParachainProof { fn from(value: beefy_verifier_primitives::ParachainProof) -> Self { @@ -95,11 +99,7 @@ mod beefy { header: Bytes::from(parachain.header), }) .collect(), - proof: value - .proof - .into_iter() - .map(|hash| FixedBytes::from(hash)) - .collect(), + proof: value.proof.into_iter().map(|hash| FixedBytes::from(hash)).collect(), leafCount: value.total_leaves.to_u256(), } } @@ -266,8 +266,7 @@ mod beefy { impl From for sp_consensus_beefy::mmr::MmrLeaf { fn from(value: PartialBeefyMmrLeaf) -> Self { - let version: u8 = - value.version.try_into().expect("mmr leaf version out of bounds"); + let version: u8 = value.version.try_into().expect("mmr leaf version out of bounds"); sp_consensus_beefy::mmr::MmrLeaf { version: MmrLeafVersion::new(version >> 5, version & 0b11111), parent_number_and_hash: ( @@ -303,29 +302,133 @@ mod beefy { } } - /// Build an [`Sp1BeefyProof`] from the solidity-side SP1 proof components. - /// - /// Mirrors `SP1Beefy.verifyConsensus`'s decode step (see - /// `evm/src/consensus/SP1Beefy.sol`): the contract ABI-decodes the proof payload into - /// `(MiniCommitment, PartialBeefyMmrLeaf, ParachainHeader[], bytes)`. - pub fn sp1_beefy_proof_from_solidity( - commitment: MiniCommitment, - leaf: PartialBeefyMmrLeaf, - headers: Vec, - proof: Bytes, - ) -> Sp1BeefyProof { - Sp1BeefyProof { - block_number: commitment - .blockNumber - .try_into() - .expect("block number out of bounds"), - validator_set_id: commitment - .validatorSetId - .try_into() - .expect("validator set id out of bounds"), - mmr_leaf: leaf.into(), - headers: headers.into_iter().map(Into::into).collect(), - proof: proof.to_vec(), + impl From for BvpParachainHeader { + fn from(value: Parachain) -> Self { + BvpParachainHeader { + header: value.header.to_vec(), + index: value.index.try_into().expect("parachain leaf index out of bounds"), + para_id: value.id.try_into().expect("para id out of bounds"), + } + } + } + + impl From for BvpParachainProof { + fn from(value: ParachainProof) -> Self { + BvpParachainProof { + parachains: value.parachains.into_iter().map(Into::into).collect(), + proof: value.proof.into_iter().map(|h| h.0).collect(), + total_leaves: value.leafCount.try_into().expect("leaf count out of bounds"), + } + } + } + + impl From for SpMmrLeaf { + fn from(value: BeefyMmrLeaf) -> Self { + let version: u8 = value.version.try_into().expect("mmr leaf version out of bounds"); + sp_consensus_beefy::mmr::MmrLeaf { + version: MmrLeafVersion::new(version >> 5, version & 0b11111), + parent_number_and_hash: ( + value.parentNumber.try_into().expect("parent number out of bounds"), + H256(value.parentHash.0), + ), + beefy_next_authority_set: BeefyNextAuthoritySet { + id: value + .nextAuthoritySet + .id + .try_into() + .expect("next authority set id out of bounds"), + len: value + .nextAuthoritySet + .len + .try_into() + .expect("next authority set len out of bounds"), + keyset_commitment: H256(value.nextAuthoritySet.root.0), + }, + leaf_extra: H256(value.extra.0), + } + } + } + + impl From for SpCommitment { + /// BEEFY commitment reconstruction. Reassembles the `Payload` from its + /// `(id, data)` entries, starting with the first entry and pushing the rest via + /// `push_raw` (which re-sorts by id to keep the invariant `Payload` expects). + fn from(value: Commitment) -> Self { + let mut iter = value.payload.into_iter(); + let first = iter.next().expect("commitment has at least one payload entry"); + let mut payload = BeefyPayload::from_single_entry(first.id.0, first.data.to_vec()); + for p in iter { + payload = payload.push_raw(p.id.0, p.data.to_vec()); + } + sp_consensus_beefy::Commitment { + payload, + block_number: value.blockNumber.try_into().expect("block number out of bounds"), + validator_set_id: value + .validatorSetId + .try_into() + .expect("validator set id out of bounds"), + } + } + } + + impl From for SignatureWithAuthorityIndex { + fn from(value: Vote) -> Self { + let sig_bytes = value.signature.to_vec(); + let mut signature: TSignature = [0u8; 65]; + signature.copy_from_slice(&sig_bytes); + SignatureWithAuthorityIndex { + signature, + index: value.authorityIndex.try_into().expect("authority index out of bounds"), + } + } + } + + impl From for MmrProof { + fn from(value: RelayChainProof) -> Self { + let leaf_index: u64 = + value.latestMmrLeaf.leafIndex.try_into().expect("mmr leaf index out of bounds"); + let items: Vec = value.mmrProof.into_iter().map(|h| H256(h.0)).collect(); + let mmr_proof = LeafProof { + leaf_indices: vec![leaf_index], + leaf_count: leaf_index.saturating_add(1), + items, + }; + + MmrProof { + signed_commitment: BvpSignedCommitment { + commitment: value.signedCommitment.commitment.into(), + signatures: value.signedCommitment.votes.into_iter().map(Into::into).collect(), + }, + latest_mmr_leaf: value.latestMmrLeaf.into(), + mmr_proof, + authority_proof: value.proof.into_iter().map(|h| h.0).collect(), + } + } + } + + impl From for ConsensusMessage { + fn from(value: BeefyConsensusProof) -> Self { + ConsensusMessage { mmr: value.relay.into(), parachain: value.parachain.into() } + } + } + + impl From for Sp1BeefyProof { + fn from(value: crate::sp1_beefy::SP1Beefy::SP1BeefyProof) -> Self { + Sp1BeefyProof { + block_number: value + .commitment + .blockNumber + .try_into() + .expect("block number out of bounds"), + validator_set_id: value + .commitment + .validatorSetId + .try_into() + .expect("validator set id out of bounds"), + mmr_leaf: value.mmrLeaf.into(), + headers: value.headers.into_iter().map(Into::into).collect(), + proof: value.proof.to_vec(), + } } } } @@ -401,14 +504,18 @@ impl TryFrom for router::PostRequest { type Error = anyhow::Error; fn try_from(value: PostRequest) -> Result { Ok(router::PostRequest { - source: StateMachine::from_str(&String::from_utf8(value.source.to_vec())?) - .map_err(|err| anyhow!("{err}"))?, - dest: StateMachine::from_str(&String::from_utf8(value.dest.to_vec())?) - .map_err(|err| anyhow!("{err}"))?, - nonce: value.nonce.try_into()?, + source: StateMachine::from_str( + &String::from_utf8(value.source.to_vec()).map_err(|e| anyhow!("{e}"))?, + ) + .map_err(|err| anyhow!("{err}"))?, + dest: StateMachine::from_str( + &String::from_utf8(value.dest.to_vec()).map_err(|e| anyhow!("{e}"))?, + ) + .map_err(|err| anyhow!("{err}"))?, + nonce: value.nonce.try_into().map_err(|e| anyhow!("{e}"))?, from: value.from.to_vec(), to: value.to.to_vec(), - timeout_timestamp: value.timeoutTimestamp.try_into()?, + timeout_timestamp: value.timeoutTimestamp.try_into().map_err(|e| anyhow!("{e}"))?, body: value.body.to_vec(), }) } @@ -506,14 +613,20 @@ impl TryFrom for ismp::events::Event { post: router::PostRequest { source: StateMachine::from_str(&resp.dest).map_err(|e| anyhow!("{}", e))?, dest: StateMachine::from_str(&resp.source).map_err(|e| anyhow!("{}", e))?, - nonce: resp.nonce.try_into()?, + nonce: resp.nonce.try_into().map_err(|e| anyhow!("{e}"))?, from: resp.to.0.to_vec(), to: resp.from.0.to_vec(), - timeout_timestamp: resp.timeoutTimestamp.try_into()?, + timeout_timestamp: resp + .timeoutTimestamp + .try_into() + .map_err(|e| anyhow!("{e}"))?, body: resp.body.to_vec(), }, response: resp.response.to_vec(), - timeout_timestamp: resp.responseTimeoutTimestamp.try_into()?, + timeout_timestamp: resp + .responseTimeoutTimestamp + .try_into() + .map_err(|e| anyhow!("{e}"))?, })), EvmHostEvents::PostRequestHandled(handled) => Ok(ismp::events::Event::PostRequestHandled(ismp::events::RequestResponseHandled { @@ -538,7 +651,7 @@ impl TryFrom for ismp::events::Event { .map_err(|e| anyhow!("{}", e))?, consensus_state_id: Default::default(), }, - latest_height: filter.height.try_into()?, + latest_height: filter.height.try_into().map_err(|e| anyhow!("{e}"))?, })), EvmHostEvents::PostRequestTimeoutHandled(handled) => { let dest = StateMachine::from_str(&handled.dest).map_err(|e| anyhow!("{}", e))?; @@ -572,7 +685,7 @@ impl TryFrom for ismp::events::Event { .map_err(|e| anyhow!("{}", e))?, consensus_state_id: Default::default(), }, - height: vetoed.height.try_into()?, + height: vetoed.height.try_into().map_err(|e| anyhow!("{e}"))?, }, fisherman: vetoed.fisherman.0.to_vec(), })), @@ -593,10 +706,10 @@ impl TryFrom for router::PostRequest { Ok(router::PostRequest { source: StateMachine::from_str(&post.source).map_err(|e| anyhow!("{}", e))?, dest: StateMachine::from_str(&post.dest).map_err(|e| anyhow!("{}", e))?, - nonce: post.nonce.try_into()?, + nonce: post.nonce.try_into().map_err(|e| anyhow!("{e}"))?, from: post.from.0.to_vec(), to: post.to.0.to_vec(), - timeout_timestamp: post.timeoutTimestamp.try_into()?, + timeout_timestamp: post.timeoutTimestamp.try_into().map_err(|e| anyhow!("{e}"))?, body: post.body.to_vec(), }) } @@ -609,12 +722,12 @@ impl TryFrom for router::GetRequest { Ok(router::GetRequest { source: StateMachine::from_str(&get.source).map_err(|e| anyhow!("{}", e))?, dest: StateMachine::from_str(&get.dest).map_err(|e| anyhow!("{}", e))?, - nonce: get.nonce.try_into()?, + nonce: get.nonce.try_into().map_err(|e| anyhow!("{e}"))?, from: get.from.0.to_vec(), keys: get.keys.into_iter().map(|key| key.to_vec()).collect(), - height: get.height.try_into()?, + height: get.height.try_into().map_err(|e| anyhow!("{e}"))?, context: get.context.to_vec(), - timeout_timestamp: get.timeoutTimestamp.try_into()?, + timeout_timestamp: get.timeoutTimestamp.try_into().map_err(|e| anyhow!("{e}"))?, }) } } diff --git a/evm/abi/src/generated/beefy.rs b/evm/abi/src/generated/beefy.rs index 47d951c69..0efbda4e6 100644 --- a/evm/abi/src/generated/beefy.rs +++ b/evm/abi/src/generated/beefy.rs @@ -1,7 +1,14 @@ -//! Beefy contract bindings generated with alloy sol! macro +//! Beefy contract bindings generated with alloy sol! macro. +//! +//! `#[sol(rpc)]` makes `sol!` also emit `ContractInstance` / provider-backed call +//! bindings alongside the plain ABI types. Those bindings depend on `alloy-contract`, +//! `alloy-provider`, `alloy-network` and `alloy-transport`, all of which are std-only. +//! `#[sol(...)]` is recognised by the `sol!` macro at expansion time, which runs after +//! `cfg_attr` is resolved — so we pick between two invocations with `#[cfg]` instead. use alloy_sol_macro::sol; +#[cfg(feature = "std")] sol!( #[allow(missing_docs)] #[sol(rpc, ignore_unlinked)] @@ -10,4 +17,13 @@ sol!( "../out/BeefyV1.sol/BeefyV1.json" ); +#[cfg(not(feature = "std"))] +sol!( + #[allow(missing_docs)] + #[sol(ignore_unlinked)] + #[derive(Debug, PartialEq, Eq)] + Beefy, + "../out/BeefyV1.sol/BeefyV1.json" +); + pub use Beefy::*; diff --git a/evm/abi/src/generated/erc20.rs b/evm/abi/src/generated/erc20.rs index 711796aaa..79ca3c20b 100644 --- a/evm/abi/src/generated/erc20.rs +++ b/evm/abi/src/generated/erc20.rs @@ -1,7 +1,10 @@ -//! ERC20 contract bindings generated with alloy sol! macro +//! ERC20 contract bindings generated with alloy sol! macro. +//! +//! See `beefy.rs` for why the std / no_std variants are distinct `sol!` invocations. use alloy_sol_macro::sol; +#[cfg(feature = "std")] sol!( #[allow(missing_docs)] #[sol(rpc)] @@ -10,4 +13,12 @@ sol!( "../out/ERC20.sol/ERC20.json" ); +#[cfg(not(feature = "std"))] +sol!( + #[allow(missing_docs)] + #[derive(Debug, PartialEq, Eq)] + ERC20, + "../out/ERC20.sol/ERC20.json" +); + pub use ERC20::*; diff --git a/evm/abi/src/generated/evm_host.rs b/evm/abi/src/generated/evm_host.rs index e7cb727e3..aec28ed36 100644 --- a/evm/abi/src/generated/evm_host.rs +++ b/evm/abi/src/generated/evm_host.rs @@ -1,7 +1,10 @@ -//! EvmHost contract bindings generated with alloy sol! macro +//! EvmHost contract bindings generated with alloy sol! macro. +//! +//! See `beefy.rs` for why the std / no_std variants are distinct `sol!` invocations. use alloy_sol_macro::sol; +#[cfg(feature = "std")] sol!( #[allow(missing_docs)] #[sol(rpc)] @@ -10,4 +13,12 @@ sol!( "../out/EvmHost.sol/EvmHost.json" ); +#[cfg(not(feature = "std"))] +sol!( + #[allow(missing_docs)] + #[derive(Debug, PartialEq, Eq)] + EvmHost, + "../out/EvmHost.sol/EvmHost.json" +); + pub use EvmHost::*; diff --git a/evm/abi/src/generated/handler.rs b/evm/abi/src/generated/handler.rs index da4c8dd70..3efcdf6cd 100644 --- a/evm/abi/src/generated/handler.rs +++ b/evm/abi/src/generated/handler.rs @@ -1,7 +1,10 @@ -//! Handler contract bindings generated with alloy sol! macro +//! Handler contract bindings generated with alloy sol! macro. +//! +//! See `beefy.rs` for why the std / no_std variants are distinct `sol!` invocations. use alloy_sol_macro::sol; +#[cfg(feature = "std")] sol!( #[allow(missing_docs)] #[sol(rpc, ignore_unlinked)] @@ -10,4 +13,13 @@ sol!( "../out/HandlerV1.sol/HandlerV1.json" ); +#[cfg(not(feature = "std"))] +sol!( + #[allow(missing_docs)] + #[sol(ignore_unlinked)] + #[derive(Debug, PartialEq, Eq)] + Handler, + "../out/HandlerV1.sol/HandlerV1.json" +); + pub use Handler::*; diff --git a/evm/abi/src/generated/host_manager.rs b/evm/abi/src/generated/host_manager.rs index fd4de355d..e08a0a9d1 100644 --- a/evm/abi/src/generated/host_manager.rs +++ b/evm/abi/src/generated/host_manager.rs @@ -1,7 +1,10 @@ -//! HostManager contract bindings generated with alloy sol! macro +//! HostManager contract bindings generated with alloy sol! macro. +//! +//! See `beefy.rs` for why the std / no_std variants are distinct `sol!` invocations. use alloy_sol_macro::sol; +#[cfg(feature = "std")] sol!( #[allow(missing_docs)] #[sol(rpc)] @@ -10,4 +13,12 @@ sol!( "../out/HostManager.sol/HostManager.json" ); +#[cfg(not(feature = "std"))] +sol!( + #[allow(missing_docs)] + #[derive(Debug, PartialEq, Eq)] + HostManager, + "../out/HostManager.sol/HostManager.json" +); + pub use HostManager::*; diff --git a/evm/abi/src/generated/mod.rs b/evm/abi/src/generated/mod.rs index 97c091de9..3757947fc 100644 --- a/evm/abi/src/generated/mod.rs +++ b/evm/abi/src/generated/mod.rs @@ -2,6 +2,12 @@ #![allow(missing_docs)] //! This module contains sol! macro generated bindings for solidity contracts. //! These bindings are generated using alloy-sol-macro from compiled ABI JSON files. +//! +//! Each binding compiles under both `std` and `no_std`: the `std` variant adds +//! `#[sol(rpc)]` to emit provider-backed contract call bindings (which depend on +//! `alloy-contract`, `alloy-provider`, `alloy-network` and `alloy-transport`, all of +//! which are std-only). Under `no_std` only the ABI types + codec impls are emitted, +//! which is what substrate pallets consume. pub mod beefy; pub mod erc20; diff --git a/evm/abi/src/generated/ping_module.rs b/evm/abi/src/generated/ping_module.rs index a41d99d71..3d051b9c7 100644 --- a/evm/abi/src/generated/ping_module.rs +++ b/evm/abi/src/generated/ping_module.rs @@ -1,7 +1,10 @@ -//! PingModule contract bindings generated with alloy sol! macro +//! PingModule contract bindings generated with alloy sol! macro. +//! +//! See `beefy.rs` for why the std / no_std variants are distinct `sol!` invocations. use alloy_sol_macro::sol; +#[cfg(feature = "std")] sol!( #[allow(missing_docs)] #[sol(rpc)] @@ -10,4 +13,12 @@ sol!( "../out/PingModule.sol/PingModule.json" ); +#[cfg(not(feature = "std"))] +sol!( + #[allow(missing_docs)] + #[derive(Debug, PartialEq, Eq)] + PingModule, + "../out/PingModule.sol/PingModule.json" +); + pub use PingModule::*; diff --git a/evm/abi/src/generated/sp1_beefy.rs b/evm/abi/src/generated/sp1_beefy.rs index 39ff48df8..d2881fb72 100644 --- a/evm/abi/src/generated/sp1_beefy.rs +++ b/evm/abi/src/generated/sp1_beefy.rs @@ -1,7 +1,10 @@ -//! SP1Beefy contract bindings generated with alloy sol! macro +//! SP1Beefy contract bindings generated with alloy sol! macro. +//! +//! See `beefy.rs` for why the std / no_std variants are distinct `sol!` invocations. use alloy_sol_macro::sol; +#[cfg(feature = "std")] sol!( #[allow(missing_docs)] #[sol(rpc, ignore_unlinked)] @@ -10,4 +13,13 @@ sol!( "../out/SP1Beefy.sol/SP1Beefy.json" ); +#[cfg(not(feature = "std"))] +sol!( + #[allow(missing_docs)] + #[sol(ignore_unlinked)] + #[derive(Debug, PartialEq, Eq)] + SP1Beefy, + "../out/SP1Beefy.sol/SP1Beefy.json" +); + pub use SP1Beefy::*; diff --git a/evm/abi/src/lib.rs b/evm/abi/src/lib.rs index 16515d76a..9b874a55d 100644 --- a/evm/abi/src/lib.rs +++ b/evm/abi/src/lib.rs @@ -13,10 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Generated types for the ismp-solidity ABI +//! Generated types for the ismp-solidity ABI. +//! +//! Every `sol!` binding under [`mod@generated`] and the conversion impls in +//! [`mod@conversions`] compile in both `std` and `no_std`. Only `#[sol(rpc)]` (the +//! provider-backed contract-call bindings, via `alloy-contract` / `alloy-provider` / +//! `alloy-network` / `alloy-transport`) is gated on `std`, since those crates are std-only. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; -pub mod conversions; mod generated; + +pub mod conversions; pub mod shared_types; pub use conversions::*; diff --git a/evm/pnpm-lock.yaml b/evm/pnpm-lock.yaml index 8aa599539..c6bbfffe6 100644 --- a/evm/pnpm-lock.yaml +++ b/evm/pnpm-lock.yaml @@ -202,10 +202,6 @@ packages: resolution: {integrity: sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==} engines: {node: '>= 12'} - '@openzeppelin/community-contracts@https://codeload.github.com/OpenZeppelin/openzeppelin-community-contracts/tar.gz/9e1baed': - resolution: {tarball: https://codeload.github.com/OpenZeppelin/openzeppelin-community-contracts/tar.gz/9e1baed} - version: 0.0.1 - '@openzeppelin/contracts@3.4.2-solc-0.7': resolution: {integrity: sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==} @@ -1179,8 +1175,6 @@ snapshots: '@nomicfoundation/solidity-analyzer-linux-x64-musl': 0.1.2 '@nomicfoundation/solidity-analyzer-win32-x64-msvc': 0.1.2 - '@openzeppelin/community-contracts@https://codeload.github.com/OpenZeppelin/openzeppelin-community-contracts/tar.gz/9e1baed': {} - '@openzeppelin/contracts@3.4.2-solc-0.7': {} '@openzeppelin/contracts@5.0.2': {} @@ -1194,7 +1188,6 @@ snapshots: '@polytope-labs/solidity-merkle-trees@https://codeload.github.com/polytope-labs/solidity-merkle-trees/tar.gz/135f2251c1e10677384f76a3514d0a6230bd8a56': dependencies: - prettier: 3.7.4 prettier-plugin-solidity: 1.4.3(prettier@3.7.4) diff --git a/modules/consensus/beefy/prover/src/fiat_shamir.rs b/modules/consensus/beefy/prover/src/fiat_shamir.rs index 62b155d45..ff49e4753 100644 --- a/modules/consensus/beefy/prover/src/fiat_shamir.rs +++ b/modules/consensus/beefy/prover/src/fiat_shamir.rs @@ -351,10 +351,7 @@ pub fn filter_signatures_for_challenge( let last = temp.last_mut().unwrap(); *last += 27; - filtered.push(SignatureWithAuthorityIndex { - index: authority_index, - signature: temp, - }); + filtered.push(SignatureWithAuthorityIndex { index: authority_index, signature: temp }); } Ok(filtered) diff --git a/modules/consensus/beefy/prover/src/lib.rs b/modules/consensus/beefy/prover/src/lib.rs index 631cea519..2db71e8a6 100644 --- a/modules/consensus/beefy/prover/src/lib.rs +++ b/modules/consensus/beefy/prover/src/lib.rs @@ -122,10 +122,7 @@ fn build_authority_proof( } /// Build the parachain header merkle proof from the heads committed in an MMR leaf. -fn build_parachain_proof( - para_ids: &[u32], - heads: &[(u32, Vec)], -) -> ParachainProof { +fn build_parachain_proof(para_ids: &[u32], heads: &[(u32, Vec)]) -> ParachainProof { let leaves: Vec<[u8; 32]> = heads.iter().map(|pair| keccak_256(&pair.encode())).collect(); let leaf_count = leaves.len(); @@ -279,17 +276,13 @@ impl Prover { let current_authorities = self.beefy_authorities(Some(block_hash)).await?; // Extract and process only the challenged signatures - let signatures = filter_signatures_for_challenge( - &signed_commitment, - &challenged_indices, - )?; + let signatures = filter_signatures_for_challenge(&signed_commitment, &challenged_indices)?; let authority_address_hashes = hash_authority_addresses( current_authorities.into_iter().map(|x| x.encode()).collect(), )?; - let authority_proof = - build_authority_proof(&signatures, &authority_address_hashes); + let authority_proof = build_authority_proof(&signatures, &authority_address_hashes); let mmr = MmrProof { signed_commitment: SignedCommitment { @@ -344,10 +337,7 @@ impl Prover { temp.copy_from_slice(&*sig.encode()); let last = temp.last_mut().unwrap(); *last = *last + 27; - Some(SignatureWithAuthorityIndex { - index: index as u32, - signature: temp, - }) + Some(SignatureWithAuthorityIndex { index: index as u32, signature: temp }) }) .collect::>(); @@ -355,8 +345,7 @@ impl Prover { current_authorities.into_iter().map(|x| x.encode()).collect(), )?; - let authority_proof = - build_authority_proof(&signatures, &authority_address_hashes); + let authority_proof = build_authority_proof(&signatures, &authority_address_hashes); let mmr = MmrProof { signed_commitment: SignedCommitment { diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index 568a255f8..4848eb8af 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -32,6 +32,7 @@ features = [ [dev-dependencies] hex-literal = { workspace = true } +hex = { workspace = true, default-features = true } beefy-prover = { workspace = true } ismp-solidity-abi = { workspace = true, default-features = true } subxt = { workspace = true, default-features = true } diff --git a/modules/consensus/beefy/verifier/src/lib.rs b/modules/consensus/beefy/verifier/src/lib.rs index 5c465f6d1..d0209e2aa 100644 --- a/modules/consensus/beefy/verifier/src/lib.rs +++ b/modules/consensus/beefy/verifier/src/lib.rs @@ -128,8 +128,8 @@ pub fn verify_mmr_update_proof( let commitment = mmr.signed_commitment.commitment.clone(); - if commitment.validator_set_id != trusted_state.current_authorities.id - && commitment.validator_set_id != trusted_state.next_authorities.id + if commitment.validator_set_id != trusted_state.current_authorities.id && + commitment.validator_set_id != trusted_state.next_authorities.id { return Err(Error::UnknownAuthoritySet { id: commitment.validator_set_id }); } diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 8c2dbd3db..b9bba6f8d 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -217,6 +217,66 @@ async fn test_verify_consensus() { println!("Successfully verified beefy justification for block #{}", block_number); } +/// Prints the SCALE-encoded `ConsensusState` and SP1 `Sp1BeefyProof` wire bytes +/// (prefixed with `PROOF_TYPE_SP1`) for the fixture used by +/// `test_sp1_verify_consensus_accepts_solidity_fixture`. Run with: +/// +/// cargo test -p beefy-verifier --lib dump_sp1_fixture_scale_bytes -- --nocapture --ignored +/// +/// Output is copied into `pallet-beefy-consensus-proofs`'s benchmark to avoid +/// pulling solidity-abi (std-only) into the wasm runtime build. +#[test] +#[ignore] +fn dump_sp1_fixture_scale_bytes() { + use alloy_sol_types::{SolType, SolValue, sol}; + use beefy_verifier_primitives::{ConsensusState, PROOF_TYPE_SP1, Sp1BeefyProof}; + use ismp_solidity_abi::{ + beefy::Beefy::BeefyConsensusState, + sp1_beefy::SP1Beefy::{MiniCommitment, ParachainHeader, PartialBeefyMmrLeaf}, + }; + + let state_bytes = hex!( + "0000000000000000000000000000000000000000000000000000000001d6792200000000000000000000000000000000000000000000000000000000012a531800000000000000000000000000000000000000000000000000000000000012750000000000000000000000000000000000000000000000000000000000000257a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f4900000000000000000000000000000000000000000000000000000000000012760000000000000000000000000000000000000000000000000000000000000257a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f49" + ); + let proof_bytes = hex!( + "0000000000000000000000000000000000000000000000000000000001d6792a000000000000000000000000000000000000000000000000000000000000127500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d67929e1dbc67b9da4b90227fb3dc2e7ffdce4e120d583502399e4bd083c02651ca5eb00000000000000000000000000000000000000000000000000000000000012760000000000000000000000000000000000000000000000000000000000000257a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f4963bc2eb07f9c83afe64eb8815b626cd0a7d2a1bbb4630a44a1896af297d0135d00000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000340000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000d2700000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000139739e9bd7f1addf87db9b6a762bd0e1713baa895c3b82b4595080e5ba02fb5b3cf2915702b49122c32b822e6a11384074d8902d5ea5f79c7cb0d7804e49501b8b532298f49e38d3f7140ce1ba61c243152e4e380b37eb628e08d5270d8b2c5e4ebedd84bb14066175726120fbc4d208000000000452505352902a869d4e00b3bb93f1e88e41a2b5f51fc637626b4ce1da15749ef2d79de4797a9ae459070449534d50010118a13886ac93d163a1d22cdef94e018eba5189424a66b7bd03a5ac232beb46bf08b0f9d2b979fff833d7e21a64a5183c61e2630c0b452236baba3c1b4ff41821044953544d20ca3be169000000000561757261010152d45dea4dcf058b0610e12981e0e4c97ad153f26481510c0b78beedf1848b4dd2abd37b8c6b800b72fa12199898eca7651471b49e38d6167a84fb6e2df7c7840000000000000000000000000000000000000000000000000000000000000000000000000001644388a21c0000000000000000000000000000000000000000000000000000000000000000002f850ee998974d6cc00e50cd0814b098c05bfade466d28573240d057f2535200000000000000000000000000000000000000000000000000000000000000002ac5e596c552ee76353c176f0870e47a0aa765ceafc4c65b03dbf434e27fa9062f185bdc40f7aae982c1c8c6b766dd491a1e1cd60128efbc58da965e5be96320287f4ce1b04538f0c8287c8eff096c36df67dc17970032546c9b3d4dd5510c5c25e880e13469e1e1aca1b41c367f2ecf04da65f7602fb53ec212b03d0148157b2cd9a79a9779f350d240e6d4c980848302fca8c7447c5fa7ac8d3c6eefcd0c640acff8b27ea316db978652553e3d054765094cf0dab6085a616489cdb973c42b258e22f346ac3ceb3e2e6750c37dad1f98f6ca15d1f70659343caa52dbbcad150b75dd2dcf0ba0a664ea4605b291df54ab1aa5b4c55034b9425ba29cc87eca7b00000000000000000000000000000000000000000000000000000000" + ); + + let sol_state = + ::abi_decode(&state_bytes).expect("decode state"); + let trusted: ConsensusState = sol_state.into(); + let trusted_scale = trusted.encode(); + + // The solidity side encodes the SP1 proof as a tuple of top-level params + // (matches `abi.decode(proof, (MiniCommitment, PartialBeefyMmrLeaf, + // ParachainHeader[], bytes))` in SP1Beefy.sol). Decode as a sequence, not a + // struct, and assemble `Sp1BeefyProof` by hand. + type ProofTuple = sol! { (MiniCommitment, PartialBeefyMmrLeaf, ParachainHeader[], bytes) }; + let (commitment, leaf, headers, plonk_proof) = + ::abi_decode_sequence(&proof_bytes).expect("decode proof tuple"); + let sp1_proof = Sp1BeefyProof { + block_number: commitment + .blockNumber + .try_into() + .expect("block number out of bounds"), + validator_set_id: commitment + .validatorSetId + .try_into() + .expect("validator set id out of bounds"), + mmr_leaf: leaf.into(), + headers: headers.into_iter().map(Into::into).collect(), + proof: plonk_proof.to_vec(), + }; + + let mut wire = vec![PROOF_TYPE_SP1]; + sp1_proof.encode_to(&mut wire); + + println!("TRUSTED_STATE_SCALE_HEX_LEN = {}", trusted_scale.len()); + println!("TRUSTED_STATE_SCALE_HEX = \"{}\"", hex::encode(&trusted_scale)); + println!("WIRE_PROOF_HEX_LEN = {}", wire.len()); + println!("WIRE_PROOF_HEX = \"{}\"", hex::encode(&wire)); +} + /// One-off SP1 verifier smoke test using the same Groth16 fixture that drives /// `SP1BeefyTest.testVerifySp1Optional` in `evm/test/SP1BeefyTest.sol`. /// @@ -227,28 +287,43 @@ async fn test_verify_consensus() { #[test] fn test_sp1_verify_consensus_accepts_solidity_fixture() { use alloy_sol_types::{SolType, SolValue, sol}; - use beefy_verifier_primitives::ConsensusState; + use beefy_verifier_primitives::{ConsensusState, Sp1BeefyProof}; use ismp_solidity_abi::{ beefy::Beefy::BeefyConsensusState, sp1_beefy::SP1Beefy::{MiniCommitment, ParachainHeader, PartialBeefyMmrLeaf}, - sp1_beefy_proof_from_solidity, }; // Fixtures copied verbatim from SP1BeefyTest.testVerifySp1Optional() in // evm/test/SP1BeefyTest.sol. - let state_bytes = hex!("0000000000000000000000000000000000000000000000000000000001d6792200000000000000000000000000000000000000000000000000000000012a531800000000000000000000000000000000000000000000000000000000000012750000000000000000000000000000000000000000000000000000000000000257a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f4900000000000000000000000000000000000000000000000000000000000012760000000000000000000000000000000000000000000000000000000000000257a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f49"); - let proof_bytes = hex!("0000000000000000000000000000000000000000000000000000000001d6792a000000000000000000000000000000000000000000000000000000000000127500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d67929e1dbc67b9da4b90227fb3dc2e7ffdce4e120d583502399e4bd083c02651ca5eb00000000000000000000000000000000000000000000000000000000000012760000000000000000000000000000000000000000000000000000000000000257a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f4963bc2eb07f9c83afe64eb8815b626cd0a7d2a1bbb4630a44a1896af297d0135d00000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000340000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000d2700000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000139739e9bd7f1addf87db9b6a762bd0e1713baa895c3b82b4595080e5ba02fb5b3cf2915702b49122c32b822e6a11384074d8902d5ea5f79c7cb0d7804e49501b8b532298f49e38d3f7140ce1ba61c243152e4e380b37eb628e08d5270d8b2c5e4ebedd84bb14066175726120fbc4d208000000000452505352902a869d4e00b3bb93f1e88e41a2b5f51fc637626b4ce1da15749ef2d79de4797a9ae459070449534d50010118a13886ac93d163a1d22cdef94e018eba5189424a66b7bd03a5ac232beb46bf08b0f9d2b979fff833d7e21a64a5183c61e2630c0b452236baba3c1b4ff41821044953544d20ca3be169000000000561757261010152d45dea4dcf058b0610e12981e0e4c97ad153f26481510c0b78beedf1848b4dd2abd37b8c6b800b72fa12199898eca7651471b49e38d6167a84fb6e2df7c7840000000000000000000000000000000000000000000000000000000000000000000000000001644388a21c0000000000000000000000000000000000000000000000000000000000000000002f850ee998974d6cc00e50cd0814b098c05bfade466d28573240d057f2535200000000000000000000000000000000000000000000000000000000000000002ac5e596c552ee76353c176f0870e47a0aa765ceafc4c65b03dbf434e27fa9062f185bdc40f7aae982c1c8c6b766dd491a1e1cd60128efbc58da965e5be96320287f4ce1b04538f0c8287c8eff096c36df67dc17970032546c9b3d4dd5510c5c25e880e13469e1e1aca1b41c367f2ecf04da65f7602fb53ec212b03d0148157b2cd9a79a9779f350d240e6d4c980848302fca8c7447c5fa7ac8d3c6eefcd0c640acff8b27ea316db978652553e3d054765094cf0dab6085a616489cdb973c42b258e22f346ac3ceb3e2e6750c37dad1f98f6ca15d1f70659343caa52dbbcad150b75dd2dcf0ba0a664ea4605b291df54ab1aa5b4c55034b9425ba29cc87eca7b00000000000000000000000000000000000000000000000000000000"); + let state_bytes = hex!( + "0000000000000000000000000000000000000000000000000000000001d6792200000000000000000000000000000000000000000000000000000000012a531800000000000000000000000000000000000000000000000000000000000012750000000000000000000000000000000000000000000000000000000000000257a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f4900000000000000000000000000000000000000000000000000000000000012760000000000000000000000000000000000000000000000000000000000000257a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f49" + ); + let proof_bytes = hex!( + "0000000000000000000000000000000000000000000000000000000001d6792a000000000000000000000000000000000000000000000000000000000000127500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d67929e1dbc67b9da4b90227fb3dc2e7ffdce4e120d583502399e4bd083c02651ca5eb00000000000000000000000000000000000000000000000000000000000012760000000000000000000000000000000000000000000000000000000000000257a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f4963bc2eb07f9c83afe64eb8815b626cd0a7d2a1bbb4630a44a1896af297d0135d00000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000340000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000d2700000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000139739e9bd7f1addf87db9b6a762bd0e1713baa895c3b82b4595080e5ba02fb5b3cf2915702b49122c32b822e6a11384074d8902d5ea5f79c7cb0d7804e49501b8b532298f49e38d3f7140ce1ba61c243152e4e380b37eb628e08d5270d8b2c5e4ebedd84bb14066175726120fbc4d208000000000452505352902a869d4e00b3bb93f1e88e41a2b5f51fc637626b4ce1da15749ef2d79de4797a9ae459070449534d50010118a13886ac93d163a1d22cdef94e018eba5189424a66b7bd03a5ac232beb46bf08b0f9d2b979fff833d7e21a64a5183c61e2630c0b452236baba3c1b4ff41821044953544d20ca3be169000000000561757261010152d45dea4dcf058b0610e12981e0e4c97ad153f26481510c0b78beedf1848b4dd2abd37b8c6b800b72fa12199898eca7651471b49e38d6167a84fb6e2df7c7840000000000000000000000000000000000000000000000000000000000000000000000000001644388a21c0000000000000000000000000000000000000000000000000000000000000000002f850ee998974d6cc00e50cd0814b098c05bfade466d28573240d057f2535200000000000000000000000000000000000000000000000000000000000000002ac5e596c552ee76353c176f0870e47a0aa765ceafc4c65b03dbf434e27fa9062f185bdc40f7aae982c1c8c6b766dd491a1e1cd60128efbc58da965e5be96320287f4ce1b04538f0c8287c8eff096c36df67dc17970032546c9b3d4dd5510c5c25e880e13469e1e1aca1b41c367f2ecf04da65f7602fb53ec212b03d0148157b2cd9a79a9779f350d240e6d4c980848302fca8c7447c5fa7ac8d3c6eefcd0c640acff8b27ea316db978652553e3d054765094cf0dab6085a616489cdb973c42b258e22f346ac3ceb3e2e6750c37dad1f98f6ca15d1f70659343caa52dbbcad150b75dd2dcf0ba0a664ea4605b291df54ab1aa5b4c55034b9425ba29cc87eca7b00000000000000000000000000000000000000000000000000000000" + ); let sol_state = ::abi_decode(&state_bytes).expect("decode state"); let trusted: ConsensusState = sol_state.into(); - // Proof payload matches SP1Beefy.sol:verifyConsensus's ABI decode signature. + // Proof payload matches SP1Beefy.sol:verifyConsensus's `abi.decode(...)` call: + // a sequence of four top-level types, not a struct wrapper. type ProofTuple = sol! { (MiniCommitment, PartialBeefyMmrLeaf, ParachainHeader[], bytes) }; let (commitment, leaf, headers, plonk_proof) = ::abi_decode_sequence(&proof_bytes).expect("decode proof tuple"); - - let sp1_proof = sp1_beefy_proof_from_solidity(commitment, leaf, headers, plonk_proof); + let sp1_proof = Sp1BeefyProof { + block_number: commitment + .blockNumber + .try_into() + .expect("block number out of bounds"), + validator_set_id: commitment + .validatorSetId + .try_into() + .expect("validator set id out of bounds"), + mmr_leaf: leaf.into(), + headers: headers.into_iter().map(Into::into).collect(), + proof: plonk_proof.to_vec(), + }; // Matches the `verificationKey` in SP1BeefyTest.sol. let vkey_hash = "0x0059fd0bff44da77999bb7974cbcf2ac7dc89e5869352f20a2f3cd46c9f53d5c"; diff --git a/modules/consensus/bsc/prover/src/test.rs b/modules/consensus/bsc/prover/src/test.rs index 5c5b2970d..cf6b13c5c 100644 --- a/modules/consensus/bsc/prover/src/test.rs +++ b/modules/consensus/bsc/prover/src/test.rs @@ -47,34 +47,30 @@ async fn setup_prover() -> BscPosProver { /// End-to-end test that mirrors the two-phase consensus-update loop used by /// `tesseract::consensus::bsc::host::start_consensus`: /// -/// 1. **Sync phase** — starting at the first candidate attested block after an -/// epoch boundary (`epoch_block + 2`), walk block-by-block up to the latest -/// position from which the previous validator set can still sign -/// (`rotation_block - 1 + epoch_length / 2`). For each header, ask the -/// prover for a `BscClientUpdate` that carries the new validator set -/// (`fetch_val_set_change: true`), drop updates with insufficient BLS -/// participation, verify under the **current** validator set, and accept -/// the first one that either contains the epoch header ancestry or whose -/// source header *is* the epoch block. That update's `next_validators` -/// becomes the pending set. +/// 1. **Sync phase** — starting at the first candidate attested block after an epoch boundary +/// (`epoch_block + 2`), walk block-by-block up to the latest position from which the previous +/// validator set can still sign (`rotation_block - 1 + epoch_length / 2`). For each header, ask +/// the prover for a `BscClientUpdate` that carries the new validator set (`fetch_val_set_change: +/// true`), drop updates with insufficient BLS participation, verify under the **current** +/// validator set, and accept the first one that either contains the epoch header ancestry or +/// whose source header *is* the epoch block. That update's `next_validators` becomes the pending +/// set. /// -/// 2. **Enactment phase** — starting at `get_rotation_block(...)`, walk up to -/// `epoch_block + epoch_length - 1` and pull an update with -/// `fetch_val_set_change: false`. Drop low-participation ones against the -/// **next** validator set, then verify under the next set. The first update -/// that verifies proves rotation has taken effect. +/// 2. **Enactment phase** — starting at `get_rotation_block(...)`, walk up to `epoch_block + +/// epoch_length - 1` and pull an update with `fetch_val_set_change: false`. Drop +/// low-participation ones against the **next** validator set, then verify under the next set. +/// The first update that verifies proves rotation has taken effect. /// /// Relative to the previous implementation which polled `latest_header` at /// 750 ms and guessed at rotation, this version: -/// - walks specific deterministic block numbers rather than racing the chain -/// tip, so it can't accidentally skip or double-count blocks; -/// - waits for each header to materialize (sleeps 3 s and retries if the -/// tip hasn't caught up), which is the expected failure mode on a live -/// chain rather than a reason to bail; -/// - uses the same `get_rotation_block` helper the production host uses, -/// instead of re-deriving the rotation boundary inline; -/// - distinguishes "update source header too old" from "update fails -/// verification" so the sync phase can fail fast on a truly bad update. +/// - walks specific deterministic block numbers rather than racing the chain tip, so it can't +/// accidentally skip or double-count blocks; +/// - waits for each header to materialize (sleeps 3 s and retries if the tip hasn't caught up), +/// which is the expected failure mode on a live chain rather than a reason to bail; +/// - uses the same `get_rotation_block` helper the production host uses, instead of re-deriving +/// the rotation boundary inline; +/// - distinguishes "update source header too old" from "update fails verification" so the sync +/// phase can fail fast on a truly bad update. #[tokio::test] #[ignore] async fn verify_bsc_pos_headers() { @@ -90,11 +86,9 @@ async fn verify_bsc_pos_headers() { // ── Sync phase ────────────────────────────────────────────────────────── let next_epoch = current_epoch + 1; let epoch_block_number = next_epoch * EPOCH_LENGTH; - let sync_end = get_rotation_block( - epoch_block_number, - current_validators.len() as u64, - EPOCH_LENGTH, - ) - 1 + EPOCH_LENGTH / 2; + let sync_end = + get_rotation_block(epoch_block_number, current_validators.len() as u64, EPOCH_LENGTH) - 1 + + EPOCH_LENGTH / 2; println!( "Sync phase: walking [{}, {}] against {}-validator set from epoch {}", @@ -140,8 +134,7 @@ async fn verify_bsc_pos_headers() { extra_data.vote_address_set.to_le_bytes().to_vec().as_slice(), ) .expect("infallible: prover already parsed extra data"); - if validators_bit_set.iter().as_bitslice().count_ones() < - (2 * current_validators.len() / 3) + if validators_bit_set.iter().as_bitslice().count_ones() < (2 * current_validators.len() / 3) { println!("sync: not enough participants at block {block}, skipping"); block += 1; @@ -181,9 +174,7 @@ async fn verify_bsc_pos_headers() { continue; } - let next = result - .next_validators - .expect("sync update must carry next validator set"); + let next = result.next_validators.expect("sync update must carry next validator set"); finalized_height = update.source_header.number.low_u64(); println!( "Sync accepted at block {block}: new {}-validator set staged, rotation at {}", @@ -197,11 +188,8 @@ async fn verify_bsc_pos_headers() { // Walk from the actual rotation block up to the end of the new epoch. // Verification must succeed under the *next* validator set before we // declare the rotation enacted. - let rotation_start = get_rotation_block( - epoch_block_number, - current_validators.len() as u64, - EPOCH_LENGTH, - ); + let rotation_start = + get_rotation_block(epoch_block_number, current_validators.len() as u64, EPOCH_LENGTH); let enact_end = epoch_block_number + EPOCH_LENGTH - 1; println!( @@ -274,8 +262,7 @@ async fn verify_bsc_pos_headers() { println!( "enact: verified update at block {block} against next set \ (source_header={}, target_header={})", - update.source_header.number, - update.target_header.number, + update.source_header.number, update.target_header.number, ); println!( "VALIDATOR SET ROTATED SUCCESSFULLY at block {block} \ diff --git a/modules/ismp/testsuite/src/lib.rs b/modules/ismp/testsuite/src/lib.rs index 556d3c6fc..5e48c25e1 100644 --- a/modules/ismp/testsuite/src/lib.rs +++ b/modules/ismp/testsuite/src/lib.rs @@ -264,7 +264,7 @@ pub fn missing_state_commitment_check(host: &H) -> Result<(), &'sta assert!(matches!( res, Err(ismp::error::Error::StateCommitmentNotFound { .. }) | - Err(ismp::error::Error::Custom(_)) + Err(ismp::error::Error::Custom(_)) )); let response_message = RequestResponse::Response(vec![Response::Post(PostResponse { @@ -285,7 +285,7 @@ pub fn missing_state_commitment_check(host: &H) -> Result<(), &'sta assert!(matches!( res, Err(ismp::error::Error::StateCommitmentNotFound { .. }) | - Err(ismp::error::Error::Custom(_)) + Err(ismp::error::Error::Custom(_)) )); // Timeout mesaage handling check @@ -298,7 +298,7 @@ pub fn missing_state_commitment_check(host: &H) -> Result<(), &'sta assert!(matches!( res, Err(ismp::error::Error::StateCommitmentNotFound { .. }) | - Err(ismp::error::Error::Custom(_)) + Err(ismp::error::Error::Custom(_)) )); Ok(()) diff --git a/modules/pallets/beefy-consensus-proofs/Cargo.toml b/modules/pallets/beefy-consensus-proofs/Cargo.toml new file mode 100644 index 000000000..2bec80c8a --- /dev/null +++ b/modules/pallets/beefy-consensus-proofs/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "pallet-beefy-consensus-proofs" +version = "0.1.0" +edition = "2021" +authors = ["Polytope Labs "] +license = "Apache-2.0" +description = "Pallet that verifies BEEFY consensus proofs and rewards submitters for useful work" +publish = false + +[dependencies] +codec = { workspace = true } +scale-info = { workspace = true } +log = { workspace = true } + +# ismp +ismp = { workspace = true, default-features = false } +pallet-ismp = { workspace = true } +ismp-beefy = { workspace = true, default-features = false } + +# beefy +beefy-verifier = { workspace = true, default-features = false } +beefy-verifier-primitives = { workspace = true, default-features = false } + +# ABI +alloy-sol-types = { workspace = true, default-features = false } +ismp-solidity-abi = { workspace = true, default-features = false } + +# misc +anyhow = { workspace = true, default-features = false } +primitive-types = { workspace = true, default-features = false } +sp-core = { workspace = true, default-features = false } +sp-io = { workspace = true, default-features = false } +hex-literal = { workspace = true, optional = true } + +[dependencies.polkadot-sdk] +workspace = true +features = [ + "frame-support", + "frame-system", + "sp-runtime", + "sp-consensus-beefy", + "sp-mmr-primitives", +] + +[features] +default = ["std"] +std = [ + "polkadot-sdk/std", + "codec/std", + "scale-info/std", + "log/std", + "ismp/std", + "pallet-ismp/std", + "ismp-beefy/std", + "beefy-verifier/std", + "beefy-verifier-primitives/std", + "alloy-sol-types/std", + "ismp-solidity-abi/std", + "anyhow/std", + "primitive-types/std", + "sp-core/std", + "sp-io/std", +] +runtime-benchmarks = [ + "polkadot-sdk/frame-benchmarking", + "polkadot-sdk/runtime-benchmarks", + "dep:hex-literal", +] +try-runtime = ["polkadot-sdk/try-runtime"] diff --git a/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs new file mode 100644 index 000000000..424b5abb9 --- /dev/null +++ b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs @@ -0,0 +1,111 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use codec::Encode; +use frame_benchmarking::v2::*; +use frame_system::RawOrigin; +use polkadot_sdk::*; +use sp_core::crypto::KeyTypeId; + +/// SCALE-encoded `beefy_verifier_primitives::ConsensusState` and wire-format proof +/// (`[PROOF_TYPE_SP1] ++ SCALE(Sp1BeefyProof)`) for the SP1 Groth16 fixture used in +/// `evm/test/SP1BeefyTest.sol::testVerifySp1Optional`. Produced by the ignored helper +/// `beefy_verifier::test::dump_sp1_fixture_scale_bytes`. +/// +/// The fixture's original `next_authorities.id` is 0x1276 (same as the leaf's +/// `beefy_next_authority_set.id`), which would keep `current_authorities.id` unchanged +/// across the update — i.e. not a rotation, a messaging-only update. We rewrite +/// `next_authorities.id` to 0x1275 so the update is a rotation (new_current = +/// prev_next > prev_current). SP1 public inputs only commit to +/// `authority.keyset_commitment` and `authority.len`, not `id`, so the proof still +/// verifies. +const TRUSTED_STATE_SCALE: [u8; 128] = hex_literal::hex!("2279d60118532a010000000000000000000000000000000000000000000000000000000000000000751200000000000057020000a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f49751200000000000057020000a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f49"); + +const WIRE_PROOF: [u8; 808] = hex_literal::hex!("012a79d6017512000000000000002979d601e1dbc67b9da4b90227fb3dc2e7ffdce4e120d583502399e4bd083c02651ca5eb761200000000000057020000a7161e52f2f4249039441385a41c6c8e36207a9b6a65d9bfae4272156ec31f4963bc2eb07f9c83afe64eb8815b626cd0a7d2a1bbb4630a44a1896af297d0135d04e504739e9bd7f1addf87db9b6a762bd0e1713baa895c3b82b4595080e5ba02fb5b3cf2915702b49122c32b822e6a11384074d8902d5ea5f79c7cb0d7804e49501b8b532298f49e38d3f7140ce1ba61c243152e4e380b37eb628e08d5270d8b2c5e4ebedd84bb14066175726120fbc4d208000000000452505352902a869d4e00b3bb93f1e88e41a2b5f51fc637626b4ce1da15749ef2d79de4797a9ae459070449534d50010118a13886ac93d163a1d22cdef94e018eba5189424a66b7bd03a5ac232beb46bf08b0f9d2b979fff833d7e21a64a5183c61e2630c0b452236baba3c1b4ff41821044953544d20ca3be169000000000561757261010152d45dea4dcf058b0610e12981e0e4c97ad153f26481510c0b78beedf1848b4dd2abd37b8c6b800b72fa12199898eca7651471b49e38d6167a84fb6e2df7c78400000000270d000091054388a21c0000000000000000000000000000000000000000000000000000000000000000002f850ee998974d6cc00e50cd0814b098c05bfade466d28573240d057f2535200000000000000000000000000000000000000000000000000000000000000002ac5e596c552ee76353c176f0870e47a0aa765ceafc4c65b03dbf434e27fa9062f185bdc40f7aae982c1c8c6b766dd491a1e1cd60128efbc58da965e5be96320287f4ce1b04538f0c8287c8eff096c36df67dc17970032546c9b3d4dd5510c5c25e880e13469e1e1aca1b41c367f2ecf04da65f7602fb53ec212b03d0148157b2cd9a79a9779f350d240e6d4c980848302fca8c7447c5fa7ac8d3c6eefcd0c640acff8b27ea316db978652553e3d054765094cf0dab6085a616489cdb973c42b258e22f346ac3ceb3e2e6750c37dad1f98f6ca15d1f70659343caa52dbbcad150b75dd2dcf0ba0a664ea4605b291df54ab1aa5b4c55034b9425ba29cc87eca7b"); + +const FIXTURE_VKEY: &[u8] = b"0x0059fd0bff44da77999bb7974cbcf2ac7dc89e5869352f20a2f3cd46c9f53d5c"; + +/// Benchmark-only key type id for the submitter keypair. +const BENCH_KEY: KeyTypeId = KeyTypeId(*b"bnch"); + +#[benchmarks( + where + T::AccountId: From<[u8; 32]> + Into<[u8; 32]>, + <::Currency as frame_support::traits::fungible::Inspect>::Balance: From, +)] +mod benchmarks { + use super::*; + + #[benchmark] + fn submit_proof() { + // Seed the consensus state and SP1 vkey the verifier will read. + pallet_ismp::ConsensusStates::::insert( + pallet::BEEFY_CONSENSUS_ID, + TRUSTED_STATE_SCALE.to_vec(), + ); + pallet::Sp1VkeyHash::::put(FIXTURE_VKEY.to_vec()); + + // Generate a deterministic SR25519 keypair via the benchmark keystore. + // `sr25519_generate` stores the keypair keyed by (BENCH_KEY, public) so + // `sr25519_sign` can look it up. The pubkey bytes double as the AccountId. + let public = sp_io::crypto::sr25519_generate(BENCH_KEY, None); + let submitter: T::AccountId = public.0.into(); + + // Sign the canonical message exactly as `verify_and_apply` computes it. + let proof = WIRE_PROOF.to_vec(); + let proof_digest = sp_io::hashing::keccak_256(&proof); + let msg_preimage = (crate::types::SIGNATURE_DOMAIN, &submitter, proof_digest).encode(); + let signed_msg = sp_io::hashing::keccak_256(&msg_preimage); + let signature = sp_io::crypto::sr25519_sign(BENCH_KEY, &public, &signed_msg) + .expect("keystore has the just-generated keypair"); + + let payload = crate::types::SubmitProofPayload { submitter, proof }; + + #[extrinsic_call] + _(RawOrigin::None, payload, signature); + + // Fixture rewrites `next_authorities.id` to force the rotation path, so a + // rotation metadata entry is always recorded. Messaging side may or may not + // fire depending on whether the fixture carries a coprocessor-height update. + assert_eq!(pallet::RotationProofs::::get().len(), 1); + } + + #[benchmark] + fn set_proof_reward() { + let reward: <::Currency as frame_support::traits::fungible::Inspect< + T::AccountId, + >>::Balance = 1_000u128.into(); + #[extrinsic_call] + _(RawOrigin::Root, reward); + + assert_eq!(pallet::ProofReward::::get(), reward); + } + + #[benchmark] + fn set_sp1_vkey_hash() { + let vkey = FIXTURE_VKEY.to_vec(); + #[extrinsic_call] + _(RawOrigin::Root, vkey.clone()); + + assert_eq!(pallet::Sp1VkeyHash::::get(), vkey); + } + + // NOTE: `initialize_state` still has no benchmark because it requires an ABI-encoded + // solidity `BeefyConsensusState` fixture. Add alongside the SP1 fixture once we need + // its weight to be accurate. +} diff --git a/modules/pallets/beefy-consensus-proofs/src/lib.rs b/modules/pallets/beefy-consensus-proofs/src/lib.rs new file mode 100644 index 000000000..1b3af111e --- /dev/null +++ b/modules/pallets/beefy-consensus-proofs/src/lib.rs @@ -0,0 +1,628 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Pallet BEEFY Consensus Proofs +//! +//! Verifies BEEFY consensus proofs (primarily SP1 ZK) submitted by off-chain provers and +//! feeds the finalized parachain state commitments into `pallet-ismp`. Rewards submitters a +//! fixed amount from the treasury when a proof does useful work — either carries the +//! expected next authority-set rotation, or advances the latest proven parachain height +//! past a block in which new ISMP requests were dispatched. +//! +//! Proofs are submitted via **authenticated unsigned** extrinsics: the payload carries an +//! SR25519 signature over `(domain, submitter, keccak256(proof))`. The submitter account +//! is both the reward payee and the claimed signer. Full proof verification runs in +//! `ValidateUnsigned` so the tx pool only ever retains valid proofs. Replay is prevented +//! by the monotonic advance of `LastProvenHeight` and the BEEFY authority set id +//! (tracked in `pallet-ismp`'s consensus state): resubmitting +//! the same bytes after a proof is applied trips `StaleProof` or `UnexpectedAuthoritySet`. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +mod benchmarking; +pub mod types; +pub mod weights; + +use polkadot_sdk::*; + +pub use pallet::*; +pub use types::{Signature, SubmitProofPayload}; +pub use weights::WeightInfo; + +/// Offchain-storage key for the rotation proof that advanced the authority set to +/// `set_id`. Relayers reconstruct this key off of a [`RotationProofs`](crate::pallet::RotationProofs) +/// entry's key and read the raw ABI-encoded proof bytes from node-local offchain storage. +pub fn rotation_offchain_key(set_id: u64) -> alloc::vec::Vec { + let mut key = alloc::vec::Vec::with_capacity( + types::OFFCHAIN_PREFIX.len() + types::OFFCHAIN_ROT.len() + 8, + ); + key.extend_from_slice(types::OFFCHAIN_PREFIX); + key.extend_from_slice(types::OFFCHAIN_ROT); + key.extend_from_slice(&set_id.to_be_bytes()); + key +} + +/// Offchain-storage key for the messaging proof that advanced the proven parachain +/// height to `proven_height`. Relayers reconstruct this key off of a +/// [`MessagingProofs`](crate::pallet::MessagingProofs) entry's key. +pub fn messaging_offchain_key(proven_height: u64) -> alloc::vec::Vec { + let mut key = alloc::vec::Vec::with_capacity( + types::OFFCHAIN_PREFIX.len() + types::OFFCHAIN_MSG.len() + 8, + ); + key.extend_from_slice(types::OFFCHAIN_PREFIX); + key.extend_from_slice(types::OFFCHAIN_MSG); + key.extend_from_slice(&proven_height.to_be_bytes()); + key +} + +/// BEEFY host-function backed crypto used by `beefy-verifier`. +pub struct SubstrateCrypto; + +impl ismp::messaging::Keccak256 for SubstrateCrypto { + fn keccak256(bytes: &[u8]) -> primitive_types::H256 { + sp_io::hashing::keccak_256(bytes).into() + } +} + +impl beefy_verifier::EcdsaRecover for SubstrateCrypto { + fn secp256k1_recover(prehash: &[u8; 32], signature: &[u8; 65]) -> anyhow::Result<[u8; 64]> { + sp_io::crypto::secp256k1_ecdsa_recover(signature, prehash) + .map_err(|_| anyhow::anyhow!("Failed to recover secp256k1 public key")) + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use alloc::{vec, vec::Vec}; + use alloy_sol_types::SolType; + use codec::{Decode, Encode}; + use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Inspect, Mutate}, + tokens::Preservation, + }, + PalletId, + }; + use frame_system::pallet_prelude::*; + use ismp::{ + consensus::{ConsensusClientId, ConsensusStateId}, + events::StateMachineUpdated, + handlers, + host::IsmpHost, + messaging::{ConsensusMessage as IsmpConsensusMessage, Message}, + }; + use ismp_solidity_abi::beefy::BeefyConsensusState as SolBeefyConsensusState; + use sp_core::sr25519; + use sp_runtime::{ + traits::AccountIdConversion, + transaction_validity::{ + InvalidTransaction, TransactionLongevity, TransactionPriority, TransactionSource, + TransactionValidity, TransactionValidityError, ValidTransaction, + }, + }; + + use crate::types::{ + Signature, SubmitProofPayload, MSG_TAG, PROOF_TYPE_SP1, ROT_TAG, SIGNATURE_DOMAIN, + }; + + /// BEEFY consensus client id. Matches the solidity constant. + pub const BEEFY_CONSENSUS_ID: ConsensusClientId = *b"BEEF"; + + /// Longevity for both messaging and rotation proofs, in blocks. + const PROOF_LONGEVITY: TransactionLongevity = 5; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: polkadot_sdk::frame_system::Config + pallet_ismp::Config { + /// Origin permitted to run privileged calls (`initialize_state`, `set_proof_reward`, + /// `set_sp1_vkey_hash`). + type AdminOrigin: EnsureOrigin; + + /// Currency used for treasury reward payouts. + type Currency: Mutate; + + /// Treasury account derivation (rewards are transferred from here). + #[pallet::constant] + type TreasuryPalletId: Get; + + /// Maximum SCALE-encoded size of a single `SubmitProofPayload`. + #[pallet::constant] + type MaxProofSize: Get; + + /// Shared cap on the `RotationProofs` and `MessagingProofs` on-chain ring buffers + /// (and, transitively, on the number of offchain proof blobs retained per stream). + #[pallet::constant] + type MaxStoredProofs: Get; + + /// The `ConsensusStateId` used for BEEFY in `pallet-ismp`. + #[pallet::constant] + type ConsensusStateId: Get; + + /// Unbonding period passed to `pallet-ismp` on first `initialize_state`, in seconds. + #[pallet::constant] + type UnbondingPeriod: Get; + + /// Weight info. + type WeightInfo: crate::weights::WeightInfo; + } + + /// Highest parachain height proven so far. + #[pallet::storage] + pub type LastProvenHeight = StorageValue<_, u64, ValueQuery>; + + /// `ChildTrieRoot` snapshot at the last messaging reward — dirty-bit for "new dispatches + /// exist since we last paid". + #[pallet::storage] + pub type LastRewardedDispatchRoot = + StorageValue<_, ::Hash, OptionQuery>; + + /// Fixed reward amount per eligible proof. + #[pallet::storage] + pub type ProofReward = + StorageValue<_, <::Currency as Inspect>::Balance, ValueQuery>; + + /// SP1 verification key hash (ASCII hex), consumed by + /// `beefy_verifier::sp1::verify_sp1_consensus`. + #[pallet::storage] + pub type Sp1VkeyHash = StorageValue<_, Vec, ValueQuery>; + + /// Bounded map of `set_id → block number` for the most recent accepted rotation + /// proofs. The raw ABI-encoded proof bytes live in offchain storage under + /// [`rotation_offchain_key(set_id)`](crate::rotation_offchain_key); keys here and + /// in offchain storage move in lock-step (oldest evicted from both when the map + /// reaches `T::MaxStoredProofs`). BEEFY set ids are monotone, so `pop_first` gives + /// FIFO eviction for free. + #[pallet::storage] + pub type RotationProofs = StorageValue< + _, + BoundedBTreeMap, T::MaxStoredProofs>, + ValueQuery, + >; + + /// Bounded map of `proven_height → block number` for the most recent accepted + /// messaging proofs. See [`messaging_offchain_key`](crate::messaging_offchain_key) + /// for the matching offchain-storage lookup. + #[pallet::storage] + pub type MessagingProofs = StorageValue< + _, + BoundedBTreeMap, T::MaxStoredProofs>, + ValueQuery, + >; + + #[pallet::error] + pub enum Error { + /// Consensus state has not been initialized yet. + NotInitialized, + /// Payload exceeds `MaxProofSize`. + ProofTooLarge, + /// `submitter` could not be interpreted as an SR25519 public key. + InvalidAccountId, + /// Signature did not verify against the signed message. + BadSignature, + /// Proof is stale (height ≤ `LastProvenHeight` for messaging, or not the expected + /// rotation). + StaleProof, + /// First proof byte is not a recognized proof type. + UnknownProofType, + /// ABI decoding or conversion failed. + AbiDecodeFailed, + /// The BEEFY verifier rejected the proof. + VerificationFailed, + /// Rotation proof did not rotate to `NextAuthoritySetId`. + UnexpectedAuthoritySet, + /// Failed to transfer the reward from the treasury. + RewardTransferFailed, + /// `pallet-ismp` rejected the consensus message. + IsmpUpdateFailed, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A proof was accepted and state advanced. + ProofAccepted { + submitter: T::AccountId, + height: u64, + new_set_id: Option, + rewarded: <::Currency as Inspect>::Balance, + }, + /// Consensus state was (re)initialized by admin. + StateInitialized { current_set_id: u64, next_set_id: u64, latest_beefy_height: u32 }, + /// Reward amount updated. + ProofRewardUpdated { + new_reward: <::Currency as Inspect>::Balance, + }, + /// SP1 verification key hash updated. + Sp1VkeyHashUpdated, + } + + #[pallet::call] + impl Pallet + where + T::AccountId: Into<[u8; 32]>, + { + /// Initialize or reset the BEEFY consensus state from its solidity-ABI encoding. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::initialize_state())] + pub fn initialize_state(origin: OriginFor, abi_state: Vec) -> DispatchResult { + ::AdminOrigin::ensure_origin(origin)?; + + let state: beefy_verifier_primitives::ConsensusState = + SolBeefyConsensusState::abi_decode(&abi_state) + .map_err(|e| { + log::warn!( + target: "ismp", + "[beefy-consensus-proofs]: abi_decode(BeefyConsensusState) failed: {e}", + ); + Error::::AbiDecodeFailed + })? + .into(); + let current_set_id = state.current_authorities.id; + let next_set_id = state.next_authorities.id; + let latest_beefy_height = state.latest_beefy_height; + + pallet_ismp::Pallet::::create_consensus_client( + frame_system::RawOrigin::Root.into(), + ismp::messaging::CreateConsensusState { + consensus_state: state.encode(), + consensus_client_id: BEEFY_CONSENSUS_ID, + consensus_state_id: T::ConsensusStateId::get(), + unbonding_period: T::UnbondingPeriod::get(), + challenge_periods: Default::default(), + state_machine_commitments: Default::default(), + }, + ) + .map_err(|e| { + log::warn!( + target: "ismp", + "[beefy-consensus-proofs]: pallet_ismp::create_consensus_client failed: {e:?}", + ); + Error::::IsmpUpdateFailed + })?; + + LastProvenHeight::::kill(); + LastRewardedDispatchRoot::::kill(); + + Self::deposit_event(Event::StateInitialized { + current_set_id, + next_set_id, + latest_beefy_height, + }); + Ok(()) + } + + /// Submit a BEEFY consensus proof. Unsigned; authenticated via the payload's + /// SR25519 signature. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::submit_proof())] + pub fn submit_proof( + origin: OriginFor, + payload: SubmitProofPayload, + signature: Signature, + ) -> DispatchResult { + ensure_none(origin)?; + + // Single verification path — same helper as `validate_unsigned`. Here the + // writes (new consensus state, parachain commitments) persist. + let outcome = Self::verify_and_apply(&payload, &signature)?; + + // Determine reward eligibility. + let child_trie_root = pallet_ismp::ChildTrieRoot::::get(); + let last_rewarded = LastRewardedDispatchRoot::::get(); + let prev_proven = LastProvenHeight::::get(); + + let messaging_reward = + Some(child_trie_root) != last_rewarded && outcome.proven_height > prev_proven; + let should_reward = outcome.rotated || messaging_reward; + + if !should_reward { + return Ok(()); + } + + if messaging_reward { + LastRewardedDispatchRoot::::put(child_trie_root); + } + if outcome.proven_height > prev_proven { + LastProvenHeight::::put(outcome.proven_height); + } + + let zero = <::Currency as Inspect>::Balance::default(); + let reward = ProofReward::::get(); + let reward_paid = if should_reward && reward > zero { + let treasury: T::AccountId = + ::TreasuryPalletId::get().into_account_truncating(); + ::Currency::transfer( + &treasury, + &payload.submitter, + reward, + Preservation::Preserve, + ) + .map_err(|e| { + log::warn!( + target: "ismp", + "[beefy-consensus-proofs] treasury reward transfer failed: {e:?}", + ); + Error::::RewardTransferFailed + })?; + reward + } else { + zero + }; + + // Fan out to offchain storage + on-chain metadata. Rotation and messaging + // are disjoint streams: a proof that rotates the authority set is only + // recorded on the rotation stream even if it also advances proven height. + // Matches the `validate_unsigned` classification (rotation preempts + // messaging in the pool), avoids storing the same proof bytes twice. + // + // BEEFY set ids and parachain heights are both strictly monotone, so the + // smallest key in each BoundedBTreeMap is always the oldest entry — + // `iter().next()` + `remove` gives FIFO eviction without an explicit + // insertion-order index. + let at = frame_system::Pallet::::block_number(); + + if outcome.rotated { + let key = crate::rotation_offchain_key(outcome.current_set_id); + sp_io::offchain_index::set(&key, &payload.proof); + + RotationProofs::::mutate(|map| { + if map.len() as u32 == T::MaxStoredProofs::get() { + if let Some(evicted_set_id) = map.iter().next().map(|(k, _)| *k) { + let _ = map.remove(&evicted_set_id); + sp_io::offchain_index::clear( + &crate::rotation_offchain_key(evicted_set_id), + ); + } + } + let _ = map.try_insert(outcome.current_set_id, at); + }); + } else if outcome.proven_height > prev_proven { + let key = crate::messaging_offchain_key(outcome.proven_height); + sp_io::offchain_index::set(&key, &payload.proof); + + MessagingProofs::::mutate(|map| { + if map.len() as u32 == T::MaxStoredProofs::get() { + if let Some(evicted_height) = map.iter().next().map(|(k, _)| *k) { + let _ = map.remove(&evicted_height); + sp_io::offchain_index::clear( + &crate::messaging_offchain_key(evicted_height), + ); + } + } + let _ = map.try_insert(outcome.proven_height, at); + }); + } + + Self::deposit_event(Event::ProofAccepted { + submitter: payload.submitter.clone(), + height: outcome.proven_height, + new_set_id: outcome.rotated.then_some(outcome.current_set_id), + rewarded: reward_paid, + }); + + Ok(()) + } + + /// Update the fixed reward amount. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::set_proof_reward())] + pub fn set_proof_reward( + origin: OriginFor, + reward: <::Currency as Inspect>::Balance, + ) -> DispatchResult { + ::AdminOrigin::ensure_origin(origin)?; + ProofReward::::put(reward); + Self::deposit_event(Event::ProofRewardUpdated { new_reward: reward }); + Ok(()) + } + + /// Update the SP1 verification key hash. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::set_sp1_vkey_hash())] + pub fn set_sp1_vkey_hash(origin: OriginFor, vkey_hash: Vec) -> DispatchResult { + ::AdminOrigin::ensure_origin(origin)?; + Sp1VkeyHash::::put(vkey_hash); + Self::deposit_event(Event::Sp1VkeyHashUpdated); + Ok(()) + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet + where + T::AccountId: Into<[u8; 32]>, + { + type Call = Call; + + // empty pre-dispatch so we don't modify storage + fn pre_dispatch(_call: &Self::Call) -> Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + let Call::submit_proof { payload, signature } = call else { + return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)); + }; + + // Single verification path: signature + handle_incoming_message (which itself + // runs the full BEEFY / SP1 check and persists state). In `validate_unsigned` + // the persistence happens in a discarded overlay; `submit_proof` re-runs this + // and the writes stick. + let outcome = Self::verify_and_apply(payload, signature).map_err(|e| { + log::debug!(target: "ismp", "validate_unsigned rejected: {e:?}"); + // Discriminate reject reasons with distinct Custom codes so tooling can + // tell why a proof was dropped without relying on log scraping. + let code: u8 = match e { + Error::::ProofTooLarge => 1, + Error::::InvalidAccountId => 2, + Error::::BadSignature => 3, + Error::::NotInitialized => 4, + Error::::UnknownProofType => 5, + Error::::AbiDecodeFailed => 6, + Error::::VerificationFailed => 7, + Error::::UnexpectedAuthoritySet => 8, + Error::::StaleProof => 9, + _ => 0, + }; + TransactionValidityError::Invalid(InvalidTransaction::Custom(code)) + })?; + + let builder = ValidTransaction::with_tag_prefix("BeefyConsensusProofs") + .longevity(PROOF_LONGEVITY) + .propagate(true); + + let tx = if outcome.rotated { + // One slot per pending rotation target. + builder + .priority(TransactionPriority::MAX) + .and_provides((ROT_TAG, outcome.current_set_id).encode()) + } else { + // Single fixed slot — highest `proven_height` wins. + builder.priority(outcome.proven_height).and_provides(MSG_TAG.encode()) + }; + + tx.build() + } + } + + /// Outcome of a successful [`Pallet::verify_and_apply`] call. + pub struct VerifyOutcome { + /// Highest parachain height finalized by this proof (0 if none). + pub proven_height: u64, + /// `current_authorities.id` of the consensus state *after* the update. + pub current_set_id: u64, + /// True iff the proof rotated the current authority set. + pub rotated: bool, + } + + impl Pallet + where + T::AccountId: Into<[u8; 32]>, + { + /// Single verification path shared by `validate_unsigned` and `submit_proof`: + /// + /// 1. SR25519 signature check over the payload. + /// 2. ABI-decode the proof into the SCALE shape `ismp-beefy` consumes. + /// 3. Dispatch `Message::Consensus` through `ismp::handlers::handle_incoming_message` — + /// `pallet-ismp` routes to `BeefyConsensusClient::verify_consensus` which runs the full + /// BEEFY / SP1 check and persists consensus state + parachain commitments. + /// 4. Extract the proven parachain height from the returned `StateMachineUpdated` events + /// and the new authority-set id from the stored consensus state so the caller can + /// classify the proof as rotation / messaging. + /// + /// Staleness rejection (messaging proofs must push height forward; rotation must + /// target the expected next set id) is enforced here so both `validate_unsigned` + /// (which runs this in a discarded overlay) and `submit_proof` (which persists) + /// share the same accept/reject decision. + pub fn verify_and_apply( + payload: &SubmitProofPayload, + signature: &Signature, + ) -> Result> { + // Size check. + if (payload.proof.len() as u32) > T::MaxProofSize::get() { + Err(Error::::ProofTooLarge)? + } + + // Signature. + let public = sr25519::Public::from(payload.submitter.clone().into()); + let proof_digest = sp_io::hashing::keccak_256(&payload.proof); + let msg_preimage = (SIGNATURE_DOMAIN, &payload.submitter, proof_digest).encode(); + let signed_msg = sp_io::hashing::keccak_256(&msg_preimage); + if !sp_io::crypto::sr25519_verify(signature, &signed_msg, &public) { + Err(Error::::BadSignature)? + } + + // expects (proof type byte || SCALE-encoded proof). + let proof_type = *payload.proof.first().ok_or(Error::::UnknownProofType)?; + if proof_type != PROOF_TYPE_SP1 { + Err(Error::::UnknownProofType)? + } + + // Hand off to pallet-ismp. + let host = pallet_ismp::Pallet::::default(); + let prev_state_bytes = host + .consensus_state(BEEFY_CONSENSUS_ID) + .map_err(|_| Error::::NotInitialized)?; + let prev_state: beefy_verifier_primitives::ConsensusState = + Decode::decode(&mut &prev_state_bytes[..]) + .map_err(|_| Error::::NotInitialized)?; + let result = handlers::handle_incoming_message( + &host, + Message::Consensus(IsmpConsensusMessage { + consensus_proof: payload.proof.clone(), + consensus_state_id: T::ConsensusStateId::get(), + signer: public.to_vec(), + }), + ) + .map_err(|e| { + log::warn!( + target: "ismp", + "[beefy-consensus-proofs] handle_incoming_message failed: {e}", + ); + Error::::VerificationFailed + })?; + + // Highest parachain height finalized by this proof + let ismp::handlers::MessageResult::ConsensusMessage(events) = result else { + Err(Error::::StaleProof)? + }; + let coprocessor = T::Coprocessor::get().unwrap(); + let proven_height = events + .into_iter() + .filter_map(|ev| match ev { + ismp::events::Event::StateMachineUpdated(StateMachineUpdated { + latest_height, + state_machine_id, + }) if state_machine_id.state_id == coprocessor => Some(latest_height), + _ => None, + }) + .max() + .unwrap_or(0); + + // Read post-update consensus state to derive the new set id. + let new_state_bytes = host + .consensus_state(BEEFY_CONSENSUS_ID) + .map_err(|_| Error::::VerificationFailed)?; + let new_state: beefy_verifier_primitives::ConsensusState = + Decode::decode(&mut &new_state_bytes[..]) + .map_err(|_| Error::::VerificationFailed)?; + let rotated = new_state.current_authorities.id > prev_state.current_authorities.id; + + // BEEFY invariant: `next` is always `current + 1`. This also subsumes the + // N → N + 1 rotation check: when rotated, `new_current == prev_next == + // prev_current + 1`, and `new_next == new_current + 1` is the same rule. + if new_state.next_authorities.id != new_state.current_authorities.id.saturating_add(1) { + Err(Error::::UnexpectedAuthoritySet)?; + } + // Messaging-only proofs must push height forward, otherwise it's a replay. + if !rotated && proven_height <= LastProvenHeight::::get() { + Err(Error::::StaleProof)? + } + + Ok(VerifyOutcome { + proven_height, + current_set_id: new_state.current_authorities.id, + rotated, + }) + } + } +} diff --git a/modules/pallets/beefy-consensus-proofs/src/types.rs b/modules/pallets/beefy-consensus-proofs/src/types.rs new file mode 100644 index 000000000..0d42d2aa2 --- /dev/null +++ b/modules/pallets/beefy-consensus-proofs/src/types.rs @@ -0,0 +1,66 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types for `pallet-beefy-consensus-proofs`. + +use codec::{Decode, DecodeWithMemTracking, Encode}; +use scale_info::TypeInfo; +use sp_core::sr25519; + +/// Payload submitted via the `submit_proof` unsigned extrinsic. +/// +/// The signed message is `keccak256(("beefy_consensus_proof_v1", submitter, +/// keccak256(proof)).encode())`; the signature in the outer extrinsic is expected to +/// verify against `submitter` interpreted as an SR25519 public key. +/// +/// No nonce: replay is prevented by on-chain state progression. Once a proof is applied +/// `LastProvenHeight` / the BEEFY authority set id advance, and `verify_and_apply` then +/// rejects any resubmission of the same bytes with `StaleProof` or +/// `UnexpectedAuthoritySet`. +#[derive(Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Eq)] +pub struct SubmitProofPayload { + /// The account that signed this payload and that will receive the reward (if any). + pub submitter: AccountId, + /// `bytes1 proof_type || abi-encoded proof body`, matching the wire format consumed by + /// `ConsensusRouter.verify` on the EVM side. + pub proof: alloc::vec::Vec, +} + +/// Domain separator for the signed message. +pub const SIGNATURE_DOMAIN: &[u8] = b"pallet_beefy_consensus_proofs"; + +/// Offchain-storage prefix for raw verified proof bytes written by `submit_proof`. +/// Combined with a stream discriminator and a `u64` (set_id or proven_height) to form +/// the actual offchain key via blake2_128. +pub const OFFCHAIN_PREFIX: &[u8] = b"beefy_consensus_proofs::"; + +/// Offchain-storage discriminator for rotation proofs. +pub const OFFCHAIN_ROT: &[u8] = b"rot"; + +/// Offchain-storage discriminator for messaging proofs. +pub const OFFCHAIN_MSG: &[u8] = b"msg"; + +/// Proof type byte: naive BEEFY proof. +pub const PROOF_TYPE_NAIVE: u8 = 0x00; +/// Proof type byte: SP1 ZK BEEFY proof. +pub const PROOF_TYPE_SP1: u8 = 0x01; + +/// `provides` tag for messaging proofs (fixed — at most one in the pool). +pub const MSG_TAG: &[u8] = b"beefy_message_proof"; +/// `provides` tag prefix for rotation proofs (`(prefix, next_set_id).encode()`). +pub const ROT_TAG: &[u8] = b"beefy_rotation_proof"; + +/// Signature type expected alongside [`SubmitProofPayload`]. +pub type Signature = sr25519::Signature; diff --git a/modules/pallets/beefy-consensus-proofs/src/weights.rs b/modules/pallets/beefy-consensus-proofs/src/weights.rs new file mode 100644 index 000000000..ed55a2197 --- /dev/null +++ b/modules/pallets/beefy-consensus-proofs/src/weights.rs @@ -0,0 +1,51 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Weight trait for `pallet-beefy-consensus-proofs`. +//! +//! Runtimes pick an implementation of [`WeightInfo`] (benchmarked or stub) and wire it +//! into [`Config::WeightInfo`](crate::pallet::Config::WeightInfo). + +use polkadot_sdk::*; + +use frame_support::weights::Weight; + +/// Weight functions needed by `pallet-beefy-consensus-proofs`. +pub trait WeightInfo { + /// Weight of `submit_proof`. + fn submit_proof() -> Weight; + /// Weight of `initialize_state`. + fn initialize_state() -> Weight; + /// Weight of `set_proof_reward`. + fn set_proof_reward() -> Weight; + /// Weight of `set_sp1_vkey_hash`. + fn set_sp1_vkey_hash() -> Weight; +} + +/// No-op [`WeightInfo`] for tests and genesis bootstrap. +impl WeightInfo for () { + fn submit_proof() -> Weight { + Weight::zero() + } + fn initialize_state() -> Weight { + Weight::zero() + } + fn set_proof_reward() -> Weight { + Weight::zero() + } + fn set_sp1_vkey_hash() -> Weight { + Weight::zero() + } +} diff --git a/modules/pallets/ismp/src/impls.rs b/modules/pallets/ismp/src/impls.rs index 760e2dd65..f5f2da4c6 100644 --- a/modules/pallets/ismp/src/impls.rs +++ b/modules/pallets/ismp/src/impls.rs @@ -157,6 +157,7 @@ impl Pallet { }, ); Responded::::insert(req_commitment, true); + Ok(commitment) } diff --git a/modules/pallets/ismp/src/lib.rs b/modules/pallets/ismp/src/lib.rs index d922f3a66..8324abbd5 100644 --- a/modules/pallets/ismp/src/lib.rs +++ b/modules/pallets/ismp/src/lib.rs @@ -379,6 +379,7 @@ pub mod pallet { let child_trie_root = H256::from_slice(&child_trie_root); ChildTrieRoot::::put::(child_trie_root.into()); + let root = match T::OffchainDB::finalize() { Ok(root) => root, Err(e) => { diff --git a/modules/pallets/testsuite/src/runtime.rs b/modules/pallets/testsuite/src/runtime.rs index 829560787..ccf61fe23 100644 --- a/modules/pallets/testsuite/src/runtime.rs +++ b/modules/pallets/testsuite/src/runtime.rs @@ -767,6 +767,9 @@ pub fn new_test_ext() -> sp_io::TestExternalities { call_dispatcher: H160::random(), }; pallet_token_governor::TokenGatewayParams::::insert(StateMachine::Evm(1), params); + + // Initialize BEEFY consensus state in pallet-ismp storage for outbound proofs + pallet_ismp::ConsensusStates::::insert(*b"BEEF", vec![0u8; 32]); }); ext } diff --git a/modules/pallets/testsuite/src/tests/mod.rs b/modules/pallets/testsuite/src/tests/mod.rs index b30f21173..7aae9bf7d 100644 --- a/modules/pallets/testsuite/src/tests/mod.rs +++ b/modules/pallets/testsuite/src/tests/mod.rs @@ -9,9 +9,9 @@ mod pallet_ismp_relayer; mod common; mod ismp_pharos; mod pallet_bridge_airdrop; -mod pallet_ismp_beefy; mod pallet_collator_manager; mod pallet_consensus_incentives; +mod pallet_ismp_beefy; mod pallet_messaging_fees; mod pallet_token_gateway; mod pharos_state_machine; diff --git a/modules/pallets/testsuite/src/tests/pallet_ismp_beefy.rs b/modules/pallets/testsuite/src/tests/pallet_ismp_beefy.rs index 4a7a8cf6c..2814fecaf 100644 --- a/modules/pallets/testsuite/src/tests/pallet_ismp_beefy.rs +++ b/modules/pallets/testsuite/src/tests/pallet_ismp_beefy.rs @@ -5,17 +5,17 @@ use polkadot_sdk::{ sp_consensus_beefy::VersionedFinalityProof, sp_core::H256, sp_io::hashing::keccak_256, *, }; use sp_consensus_beefy::ecdsa_crypto::Signature; -use subxt::{PolkadotConfig, backend::legacy::LegacyRpcMethods, ext::subxt_rpcs::rpc_params}; +use subxt::{backend::legacy::LegacyRpcMethods, ext::subxt_rpcs::rpc_params, PolkadotConfig}; use beefy_prover::{ - Prover, relay::{fetch_mmr_proof, paras_parachains}, rs_merkle::MerkleTree, - util::{MerkleHasher, hash_authority_addresses}, + util::{hash_authority_addresses, MerkleHasher}, + Prover, }; use beefy_verifier_primitives::{ - ConsensusMessage, ConsensusState, MmrProof, PROOF_TYPE_NAIVE, ParachainHeader, ParachainProof, - SignatureWithAuthorityIndex, + ConsensusMessage, ConsensusState, MmrProof, ParachainHeader, ParachainProof, + SignatureWithAuthorityIndex, PROOF_TYPE_NAIVE, }; use ismp::{ consensus::{ConsensusClient, StateMachineId}, diff --git a/parachain/runtimes/gargantua/Cargo.toml b/parachain/runtimes/gargantua/Cargo.toml index 73d746a5c..9fa8b7e76 100644 --- a/parachain/runtimes/gargantua/Cargo.toml +++ b/parachain/runtimes/gargantua/Cargo.toml @@ -27,6 +27,7 @@ ismp = { workspace = true } pallet-ismp = { workspace = true } pallet-fishermen = { workspace = true } pallet-ismp-demo = { workspace = true } +pallet-beefy-consensus-proofs = { workspace = true } pallet-ismp-runtime-api = { workspace = true } ismp-sync-committee = { workspace = true } ismp-bsc = { workspace = true } @@ -132,6 +133,7 @@ std = [ "pallet-ismp/std", "pallet-ismp-runtime-api/std", "pallet-ismp-demo/std", + "pallet-beefy-consensus-proofs/std", "ismp-sync-committee/std", "ismp-bsc/std", "ismp-grandpa/std", @@ -171,6 +173,7 @@ runtime-benchmarks = [ "ismp-parachain/runtime-benchmarks", "pallet-messaging-fees/runtime-benchmarks", "pallet-intents-coprocessor/runtime-benchmarks", + "pallet-beefy-consensus-proofs/runtime-benchmarks", ] try-runtime = [ "polkadot-sdk/try-runtime", @@ -178,6 +181,7 @@ try-runtime = [ "pallet-ismp/try-runtime", "ismp-sync-committee/try-runtime", "pallet-ismp-demo/try-runtime", + "pallet-beefy-consensus-proofs/try-runtime", "pallet-ismp-relayer/try-runtime", "pallet-ismp-host-executive/try-runtime", "pallet-mmr-tree/try-runtime", diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index b46e65539..dee29a69e 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -754,6 +754,31 @@ impl pallet_vesting::Config for Runtime { type BlockNumberProvider = System; } +parameter_types! { + /// `ConsensusStateId` used by `pallet-beefy-consensus-proofs`. Matches the solidity + /// `BEEFY_CONSENSUS_ID`. + pub const BeefyConsensusStateId: ::ismp::consensus::ConsensusStateId = *b"BEEF"; + /// Unbonding period handed to `pallet-ismp` on first `initialize_state` (21 days in + /// seconds), aligning with other BEEFY clients in the runtime. + pub const BeefyUnbondingPeriod: u64 = 21 * 24 * 60 * 60; + /// Maximum SCALE-encoded size of a `SubmitProofPayload`. + pub const MaxBeefyProofSize: u32 = 1_048_576; + /// Shared ring-buffer size for `RotationProofs` and `MessagingProofs`. Also caps the + /// number of offchain proof blobs retained per stream. + pub const MaxStoredBeefyProofs: u32 = 512; +} + +impl pallet_beefy_consensus_proofs::Config for Runtime { + type AdminOrigin = EnsureRoot; + type Currency = Balances; + type TreasuryPalletId = TreasuryPalletId; + type MaxProofSize = MaxBeefyProofSize; + type MaxStoredProofs = MaxStoredBeefyProofs; + type ConsensusStateId = BeefyConsensusStateId; + type UnbondingPeriod = BeefyUnbondingPeriod; + type WeightInfo = weights::pallet_beefy_consensus_proofs::WeightInfo; +} + // Create the runtime by composing the FRAME pallets that were previously configured. #[frame_support::runtime] mod runtime { @@ -873,6 +898,8 @@ mod runtime { pub type IsmpTendermint = ismp_tendermint::pallet; #[runtime::pallet_index(86)] pub type TxPause = pallet_tx_pause; + #[runtime::pallet_index(90)] + pub type BeefyConsensusProofs = pallet_beefy_consensus_proofs; #[runtime::pallet_index(255)] pub type IsmpGrandpa = ismp_grandpa; } @@ -907,6 +934,7 @@ mod benches { [pallet_transaction_payment, TransactionPayment] [pallet_vesting, Vesting] [pallet_tx_pause, TxPause] + [pallet_beefy_consensus_proofs, BeefyConsensusProofs] ); } diff --git a/parachain/runtimes/gargantua/src/weights/mod.rs b/parachain/runtimes/gargantua/src/weights/mod.rs index acb9eba0a..ea1f54010 100644 --- a/parachain/runtimes/gargantua/src/weights/mod.rs +++ b/parachain/runtimes/gargantua/src/weights/mod.rs @@ -31,6 +31,7 @@ pub mod ismp_parachain; pub mod pallet_asset_rate; pub mod pallet_assets; pub mod pallet_balances; +pub mod pallet_beefy_consensus_proofs; pub mod pallet_collective; pub mod pallet_intents_coprocessor; pub mod pallet_ismp; diff --git a/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs b/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs new file mode 100644 index 000000000..68cb7be98 --- /dev/null +++ b/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs @@ -0,0 +1,86 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +//! Weights for `pallet_beefy_consensus_proofs`. +//! +//! `submit_proof`, `set_proof_reward` and `set_sp1_vkey_hash` use numbers ported from +//! the `pallet_outbound_proofs` benchmark run on an AMD Ryzen Threadripper PRO +//! 5995WX (2026-04-18, wasm-execution=compiled, steps=50, repeat=20). The pallet was +//! subsequently renamed and its extrinsic surface redesigned (submit_proof is now +//! unsigned + SR25519-authed, `initialize_state` was added), so these numbers are a +//! close-but-not-exact starting point — regenerate once benchmarks are wired into CI. +//! +//! Original per-bench numbers: +//! submit_proof ~669ms 5r/2w (dominated by SP1 verification) +//! set_proof_reward ~8.7µs 0r/1w +//! set_sp1_vkey_hash ~4.8µs 0r/1w + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use polkadot_sdk::*; +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_beefy_consensus_proofs`. +pub struct WeightInfo(PhantomData); +impl pallet_beefy_consensus_proofs::WeightInfo for WeightInfo { + /// Storage: `Ismp::ConsensusStates` (r:1 w:1) + /// Storage: `BeefyConsensusProofs::Sp1VkeyHash` (r:1 w:0) + /// Storage: `BeefyConsensusProofs::LastProvenHeight` (r:1 w:1) + /// Storage: `BeefyConsensusProofs::LastRewardedDispatchRoot` (r:1 w:1) + /// Storage: `BeefyConsensusProofs::RecentProofs` (r:1 w:1) + /// Storage: `BeefyConsensusProofs::ProofReward` (r:1 w:0) + fn submit_proof() -> Weight { + // Proof Size summary in bytes: + // Measured: `547` + // Estimated: `4012` + // Minimum execution time: 669_751_633_000 picoseconds. + Weight::from_parts(694_774_527_000, 0) + .saturating_add(Weight::from_parts(0, 4012)) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `BeefyConsensusProofs::ProofReward` (r:0 w:1) + fn set_proof_reward() -> Weight { + // Minimum execution time: 8_696_000 picoseconds. + Weight::from_parts(9_027_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `BeefyConsensusProofs::Sp1VkeyHash` (r:0 w:1) + fn set_sp1_vkey_hash() -> Weight { + // Minimum execution time: 4_779_000 picoseconds. + Weight::from_parts(5_490_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Ismp::ConsensusStateClient` (r:1 w:1) + /// Storage: `Ismp::ConsensusStates` (r:0 w:1) + /// Storage: `Ismp::UnbondingPeriod` (r:0 w:1) + /// Storage: `Ismp::ConsensusClientUpdateTime` (r:0 w:1) + /// Storage: `BeefyConsensusProofs::LastProvenHeight` (r:0 w:1) + /// Storage: `BeefyConsensusProofs::LastRewardedDispatchRoot` (r:0 w:1) + fn initialize_state() -> Weight { + // Approximated: similar cost class to `set_sp1_vkey_hash`, plus several writes. + Weight::from_parts(20_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(6)) + } +} diff --git a/parachain/runtimes/nexus/src/governance/origins.rs b/parachain/runtimes/nexus/src/governance/origins.rs index e8dd80fac..ef010b437 100644 --- a/parachain/runtimes/nexus/src/governance/origins.rs +++ b/parachain/runtimes/nexus/src/governance/origins.rs @@ -82,12 +82,7 @@ pub mod custom_origins { }; () => {} } - decl_unit_ensures!( - ReferendumCanceller, - ReferendumKiller, - WhitelistedCaller, - TreasurySpend, - ); + decl_unit_ensures!(ReferendumCanceller, ReferendumKiller, WhitelistedCaller, TreasurySpend,); macro_rules! decl_ensure { ( diff --git a/tesseract/consensus/beefy/src/backend/mod.rs b/tesseract/consensus/beefy/src/backend/mod.rs index fb4099654..cec2e7967 100644 --- a/tesseract/consensus/beefy/src/backend/mod.rs +++ b/tesseract/consensus/beefy/src/backend/mod.rs @@ -31,8 +31,10 @@ use std::pin::Pin; /// Consensus proof message exchanged between prover and host #[derive(Clone, Debug, Encode, Decode)] pub struct ConsensusProof { - /// The height that is now finalized by this consensus message + /// The relay chain height finalized by this consensus message pub finalized_height: u32, + /// The parachain height finalized by this consensus message + pub finalized_parachain_height: u64, /// The validator set id responsible for signing this message pub set_id: u64, /// The consensus message in question diff --git a/tesseract/consensus/beefy/src/host.rs b/tesseract/consensus/beefy/src/host.rs index d640c974d..c9c895b92 100644 --- a/tesseract/consensus/beefy/src/host.rs +++ b/tesseract/consensus/beefy/src/host.rs @@ -265,7 +265,7 @@ where let QueueMessage { id, - proof: ConsensusProof { message, finalized_height, set_id }, + proof: ConsensusProof { message, finalized_height, set_id, .. }, } = match item { Ok(Some(message)) => message, Ok(None) => break, // no new items in the queue diff --git a/tesseract/consensus/beefy/src/prover.rs b/tesseract/consensus/beefy/src/prover.rs index 052627fec..1c54c1666 100644 --- a/tesseract/consensus/beefy/src/prover.rs +++ b/tesseract/consensus/beefy/src/prover.rs @@ -422,8 +422,21 @@ where .consensus_proof(commitment.clone(), self.consensus_state.inner.clone()) .await?; + let finalized_hash = relay_rpc + .chain_get_block_hash(Some(commitment.commitment.block_number.into())) + .await? + .expect("Epoch change header exists"); + let para_header = query_parachain_header( + &self.prover.inner().relay_rpc, + finalized_hash, + para_id, + ) + .await?; + let finalized_parachain_height: u64 = para_header.number.into(); + let message = ConsensusProof { finalized_height: commitment.commitment.block_number, + finalized_parachain_height, set_id: next_set_id, message: ConsensusMessage { consensus_proof, @@ -441,19 +454,8 @@ where .await?; } - let finalized_hash = relay_rpc - .chain_get_block_hash(Some(commitment.commitment.block_number.into())) - .await? - .expect("Epoch change header exists"); - let para_header = query_parachain_header( - &self.prover.inner().relay_rpc, - finalized_hash, - para_id, - ) - .await?; - self.consensus_state.finalized_parachain_height = - para_header.number.into(); + finalized_parachain_height; self.consensus_state.inner.latest_beefy_height = commitment.commitment.block_number; self.rotate_authorities(epoch_change_block_hash).await?; @@ -544,6 +546,7 @@ where let message = ConsensusProof { finalized_height: commitment.commitment.block_number, + finalized_parachain_height: latest_parachain_height, set_id, message: ConsensusMessage { consensus_proof, diff --git a/tesseract/consensus/beefy/zk/src/lib.rs b/tesseract/consensus/beefy/zk/src/lib.rs index e1c555d4b..40f93a823 100644 --- a/tesseract/consensus/beefy/zk/src/lib.rs +++ b/tesseract/consensus/beefy/zk/src/lib.rs @@ -108,7 +108,12 @@ where let leaf_hashes = paras.iter().map(|l| keccak_256(&l.encode())).collect::>(); let tree = MerkleTree::::from_leaves(&leaf_hashes); - let indices = message.parachain.parachains.iter().map(|i| i.index as usize).collect::>(); + let indices = message + .parachain + .parachains + .iter() + .map(|i| i.index as usize) + .collect::>(); let proof = tree.proof(&indices); let witness = proof.proof_hashes().iter().map(|item| item.clone().into()).collect(); diff --git a/tesseract/consensus/op-host/src/host.rs b/tesseract/consensus/op-host/src/host.rs index 7d4b90580..f10d69ca2 100644 --- a/tesseract/consensus/op-host/src/host.rs +++ b/tesseract/consensus/op-host/src/host.rs @@ -14,7 +14,7 @@ use ismp_optimism::{ ConsensusState, OptimismConsensusProof, OptimismConsensusType, OptimismUpdate, OPTIMISM_CONSENSUS_CLIENT_ID, }; -use op_verifier::{calculate_output_root, CANNON, _PERMISSIONED}; +use op_verifier::{calculate_output_root, _PERMISSIONED, CANNON}; use reqwest::Url; use sp_core::{bytes::from_hex, Encode, H160, H256, U256}; use sync_committee_primitives::consensus_types::{BeaconBlockHeader, Checkpoint}; diff --git a/tesseract/consensus/sync-committee/src/host.rs b/tesseract/consensus/sync-committee/src/host.rs index 3b2dda0f8..9106ddc4e 100644 --- a/tesseract/consensus/sync-committee/src/host.rs +++ b/tesseract/consensus/sync-committee/src/host.rs @@ -28,7 +28,7 @@ use std::{collections::BTreeMap, sync::Arc}; use sync_committee_primitives::{constants::Config, util::compute_sync_committee_period}; use crate::notification::consensus_notification; -use op_verifier::{CANNON, _PERMISSIONED}; +use op_verifier::{_PERMISSIONED, CANNON}; use tesseract_primitives::{IsmpHost, IsmpProvider}; #[async_trait::async_trait] diff --git a/tesseract/messaging/fees/src/db.rs b/tesseract/messaging/fees/src/db.rs index c31485de4..32d6556c5 100644 --- a/tesseract/messaging/fees/src/db.rs +++ b/tesseract/messaging/fees/src/db.rs @@ -19,8 +19,8 @@ pub mod deliveries { pub const NAME: &str = "Deliveries"; pub mod id { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "id"; pub struct Set(pub i32); @@ -89,8 +89,8 @@ pub mod deliveries { } pub mod hash { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "hash"; pub struct Set(pub String); @@ -150,8 +150,8 @@ pub mod deliveries { } pub mod source_chain { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "source_chain"; pub struct Set(pub String); @@ -215,8 +215,8 @@ pub mod deliveries { } pub mod dest_chain { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "dest_chain"; pub struct Set(pub String); @@ -280,8 +280,8 @@ pub mod deliveries { } pub mod delivery_type { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "delivery_type"; pub struct Set(pub i32); @@ -354,8 +354,8 @@ pub mod deliveries { } pub mod created_at { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "created_at"; pub struct Set(pub i32); @@ -428,8 +428,8 @@ pub mod deliveries { } pub mod height { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "height"; pub struct Set(pub i32); @@ -1073,8 +1073,8 @@ pub mod pending_withdrawal { pub const NAME: &str = "PendingWithdrawal"; pub mod id { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "id"; pub struct Set(pub i32); @@ -1143,8 +1143,8 @@ pub mod pending_withdrawal { } pub mod dest { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "dest"; pub struct Set(pub String); @@ -1204,8 +1204,8 @@ pub mod pending_withdrawal { } pub mod encoded { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "encoded"; pub struct Set(pub Vec); @@ -1614,8 +1614,8 @@ pub mod unprofitable_messages { pub const NAME: &str = "UnprofitableMessages"; pub mod id { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "id"; pub struct Set(pub i32); @@ -1684,8 +1684,8 @@ pub mod unprofitable_messages { } pub mod dest { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "dest"; pub struct Set(pub String); @@ -1745,8 +1745,8 @@ pub mod unprofitable_messages { } pub mod encoded { use super::{ - super::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, WhereParam, - WithParam, _prisma::*, + super::*, _prisma::*, OrderByParam, SetParam, UncheckedSetParam, UniqueWhereParam, + WhereParam, WithParam, }; pub const NAME: &str = "encoded"; pub struct Set(pub Vec); diff --git a/tesseract/messaging/substrate/src/provider.rs b/tesseract/messaging/substrate/src/provider.rs index 86470398a..88a7dfaca 100644 --- a/tesseract/messaging/substrate/src/provider.rs +++ b/tesseract/messaging/substrate/src/provider.rs @@ -846,15 +846,10 @@ where .chain_get_block_hash(None) .await? .ok_or_else(|| anyhow!("Failed to query latest block hash"))?; - let raw_value = self - .client - .storage() - .at(block_hash) - .fetch_raw(key.clone()) - .await? - .ok_or_else(|| { - anyhow!("State commitment not present for state machine {:?}", height) - })?; + let raw_value = + self.client.storage().at(block_hash).fetch_raw(key.clone()).await?.ok_or_else( + || anyhow!("State commitment not present for state machine {:?}", height), + )?; let commitment = Decode::decode(&mut &*raw_value)?; Ok(commitment)