diff --git a/Cargo.lock b/Cargo.lock index 89602ed51..8ff33513c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8199,6 +8199,7 @@ dependencies = [ "pallet-messaging-fees", "pallet-mmr-runtime-api", "pallet-mmr-tree", + "pallet-outbound-proofs", "pallet-state-coprocessor", "pallet-token-gateway", "pallet-token-gateway-inspector", @@ -15242,6 +15243,7 @@ dependencies = [ "pallet-ismp-rpc", "pallet-messaging-fees", "pallet-mmr-tree", + "pallet-outbound-proofs", "pallet-token-gateway", "pallet-token-gateway-inspector", "pallet-token-governor", @@ -15678,6 +15680,17 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-outbound-proofs" +version = "0.1.0" +dependencies = [ + "pallet-ismp", + "parity-scale-codec", + "polkadot-sdk", + "scale-info", + "sp1-verifier", +] + [[package]] name = "pallet-paged-list" version = "0.23.0" diff --git a/Cargo.toml b/Cargo.toml index a44a3826a..cb860bb35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ members = [ "modules/ismp/state-machines/hyperbridge", "modules/pallets/consensus-incentives", "modules/pallets/messaging-fees", + "modules/pallets/outbound-proofs", # evm stuff # "evm/integration-tests", @@ -119,7 +120,6 @@ members = [ "tesseract/consensus/polygon", "tesseract/consensus/tendermint", - # Airdrop "modules/pallets/bridge-drop", ] @@ -303,6 +303,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-outbound-proofs = { path = "modules/pallets/outbound-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 } diff --git a/modules/pallets/intents-coprocessor/src/tests.rs b/modules/pallets/intents-coprocessor/src/tests.rs index 08a3f757a..7a631ebe2 100644 --- a/modules/pallets/intents-coprocessor/src/tests.rs +++ b/modules/pallets/intents-coprocessor/src/tests.rs @@ -131,6 +131,7 @@ impl pallet_ismp::Config for Test { type ConsensusClients = (); type OffchainDB = (); type FeeHandler = (); + type OnDispatch = (); } parameter_types! { diff --git a/modules/pallets/ismp/src/impls.rs b/modules/pallets/ismp/src/impls.rs index 760e2dd65..f9850b772 100644 --- a/modules/pallets/ismp/src/impls.rs +++ b/modules/pallets/ismp/src/impls.rs @@ -21,7 +21,7 @@ use crate::{ dispatcher::{FeeMetadata, RequestMetadata}, fee_handler::FeeHandler, offchain::{self, ForkIdentifier, Leaf, LeafIndexAndPos, OffchainDBProvider}, - Config, Error, Event, Pallet, Responded, + Config, Error, Event, OnDispatch, Pallet, Responded, }; use alloc::{string::ToString, vec, vec::Vec}; use codec::Decode; @@ -117,6 +117,8 @@ impl Pallet { }, ); + T::OnDispatch::on_dispatch(); + Ok(commitment) } @@ -157,6 +159,9 @@ impl Pallet { }, ); Responded::::insert(req_commitment, true); + + T::OnDispatch::on_dispatch(); + Ok(commitment) } diff --git a/modules/pallets/ismp/src/lib.rs b/modules/pallets/ismp/src/lib.rs index 98e3b3e9c..13bceb56b 100644 --- a/modules/pallets/ismp/src/lib.rs +++ b/modules/pallets/ismp/src/lib.rs @@ -79,6 +79,15 @@ pub mod pallet { use sp_std::prelude::*; pub use utils::*; + /// Hook called when an outgoing message is dispatched + pub trait OnDispatch { + fn on_dispatch(); + } + + impl OnDispatch for () { + fn on_dispatch() {} + } + /// [`PalletId`] where relayer fees will be collected pub const RELAYER_FEE_ACCOUNT: PalletId = PalletId(*b"ISMPFEES"); @@ -160,6 +169,10 @@ pub mod pallet { /// This offchain DB is also allowed to "merkelize" and "generate proofs" for messages. /// Most state machines will likey not need this and can just provide `()` type OffchainDB: OffchainDBProvider; + + /// Hook called when an outgoing request is dispatched. + /// Use `()` for a no-op implementation. + type OnDispatch: OnDispatch; } // Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and diff --git a/modules/pallets/outbound-proofs/Cargo.toml b/modules/pallets/outbound-proofs/Cargo.toml new file mode 100644 index 000000000..c54c842ba --- /dev/null +++ b/modules/pallets/outbound-proofs/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "pallet-outbound-proofs" +version = "0.1.0" +edition = "2021" +authors = ["Polytope Labs "] +license = "Apache-2.0" +description = "Pallet for storing and rewarding outbound consensus proofs on Hyperbridge" +publish = false + +[dependencies] +codec = { workspace = true } +pallet-ismp = { workspace = true } +scale-info = { workspace = true } +sp1-verifier = { version = "6.0.2", default-features = false, optional = true } + +[dependencies.polkadot-sdk] +workspace = true +features = ["frame-support", "frame-system", "sp-io", "sp-runtime"] + +[features] +default = ["std"] +std = [ + "polkadot-sdk/std", + "codec/std", + "pallet-ismp/std", + "scale-info/std", +] +sp1 = ["sp1-verifier"] +runtime-benchmarks = [ + "polkadot-sdk/frame-benchmarking", + "polkadot-sdk/runtime-benchmarks", +] +try-runtime = ["polkadot-sdk/try-runtime"] diff --git a/modules/pallets/outbound-proofs/src/benchmarking.rs b/modules/pallets/outbound-proofs/src/benchmarking.rs new file mode 100644 index 000000000..c83448d01 --- /dev/null +++ b/modules/pallets/outbound-proofs/src/benchmarking.rs @@ -0,0 +1,160 @@ +// 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 alloc::vec; +use frame_benchmarking::v2::*; +use frame_support::BoundedVec; +use frame_system::RawOrigin; +use pallet::{ + ConsensusState, CurrentEpoch, LatestMessageBlock, LatestProvenParachainHeight, ProvenHeights, + Sp1VkeyHash, +}; +use types::BeefyConsensusState; + +#[benchmarks( + where + T::AccountId: From<[u8; 32]>, + >::Balance: From, +)] +mod benchmarks { + use super::*; + + #[benchmark] + fn submit_proof() { + let caller: T::AccountId = whitelisted_caller(); + + CurrentEpoch::::put(0u64); + ConsensusState::::put(BeefyConsensusState::default()); + + let proof: BoundedVec = + vec![0u8; 100].try_into().expect("fits in bounds"); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), proof, 1000u64, 500u64, 1u64); + + assert!(ProvenHeights::::contains_key(1000u64)); + assert_eq!(CurrentEpoch::::get(), 1u64); + } + + #[benchmark] + fn set_proof_reward() { + let reward: >::Balance = 1000u128.into(); + #[extrinsic_call] + _(RawOrigin::Root, reward); + + assert_eq!(pallet::ProofReward::::get(), reward); + } + + #[benchmark] + fn set_sp1_vkey_hash() { + let vkey = vec![0xabu8; 32]; + #[extrinsic_call] + _(RawOrigin::Root, vkey.clone()); + + assert_eq!(Sp1VkeyHash::::get(), vkey); + } + +} + +// Minimal test runtime for benchmark tests +#[cfg(test)] +use polkadot_sdk::*; + +#[cfg(test)] +type Block = frame_system::mocking::MockBlock; + +#[cfg(test)] +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + OutboundProofs: crate::pallet, + } +); + +#[cfg(test)] +#[frame_support::derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountData = pallet_balances::AccountData; +} + +#[cfg(test)] +#[frame_support::derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +#[cfg(test)] +pub struct DummyVerifier; +#[cfg(test)] +impl ProofVerifier for DummyVerifier { + fn verify( + trusted_state: &BeefyConsensusState, + _proof: &[u8], + ) -> Result { + Ok(trusted_state.clone()) + } +} + +#[cfg(test)] +pub struct DummyOnDispatch; +#[cfg(test)] +impl pallet_ismp::OnDispatch for DummyOnDispatch { + fn on_dispatch() {} +} + +#[cfg(test)] +frame_support::parameter_types! { + pub const TreasuryId: frame_support::PalletId = frame_support::PalletId(*b"hb/trsry"); +} + +#[cfg(test)] +pub struct TestWeights; +#[cfg(test)] +impl pallet::WeightInfo for TestWeights { + fn submit_proof() -> frame_support::weights::Weight { + frame_support::weights::Weight::zero() + } + fn set_proof_reward() -> frame_support::weights::Weight { + frame_support::weights::Weight::zero() + } + fn set_sp1_vkey_hash() -> frame_support::weights::Weight { + frame_support::weights::Weight::zero() + } +} + +#[cfg(test)] +impl crate::pallet::Config for Test { + type AdminOrigin = frame_system::EnsureRoot; + type ProofVerifier = DummyVerifier; + type Currency = Balances; + type TreasuryPalletId = TreasuryId; + type MaxProofSize = frame_support::traits::ConstU32<100_000>; + type MaxStoredProofs = frame_support::traits::ConstU32<100>; + type WeightInfo = TestWeights; +} + +#[cfg(test)] +pub fn new_test_ext() -> sp_io::TestExternalities { + use sp_runtime::BuildStorage; + let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| frame_system::Pallet::::set_block_number(1)); + ext +} diff --git a/modules/pallets/outbound-proofs/src/lib.rs b/modules/pallets/outbound-proofs/src/lib.rs new file mode 100644 index 000000000..8349f11f9 --- /dev/null +++ b/modules/pallets/outbound-proofs/src/lib.rs @@ -0,0 +1,304 @@ +// 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_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod types; +pub mod verifier; + +mod benchmarking; + +use polkadot_sdk::*; + +pub use pallet::*; +pub use types::{BeefyAuthoritySet, BeefyConsensusState, EpochInfo, PendingProofInfo, ProofMetadata}; + +pub trait ProofVerifier { + fn verify( + trusted_state: &BeefyConsensusState, + proof: &[u8], + ) -> Result; +} + +/// Production SP1 BEEFY proof verifier. +/// Reads the verification key hash from pallet storage and performs +/// full verification mirroring SP1Beefy.sol. +#[cfg(feature = "sp1")] +pub struct Sp1ProofVerifier(core::marker::PhantomData); + +#[cfg(feature = "sp1")] +impl ProofVerifier for Sp1ProofVerifier { + fn verify( + trusted_state: &BeefyConsensusState, + proof: &[u8], + ) -> Result { + let vkey_bytes = pallet::Sp1VkeyHash::::get(); + let vkey_hash = core::str::from_utf8(&vkey_bytes) + .map_err(|_| frame_support::pallet_prelude::DispatchError::Other("invalid vkey hash encoding"))?; + + let result = verifier::verify_beefy_proof(trusted_state, proof, vkey_hash) + .map_err(|e| frame_support::pallet_prelude::DispatchError::Other(match e { + verifier::VerificationError::DecodeFailed => "proof decode failed", + verifier::VerificationError::StaleHeight => "stale proof height", + verifier::VerificationError::UnknownAuthoritySet => "unknown authority set", + verifier::VerificationError::InvalidProof => "SP1 proof verification failed", + }))?; + + Ok(result.new_state) + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Inspect, Mutate}, + tokens::Preservation, + }, + PalletId, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::AccountIdConversion; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: polkadot_sdk::frame_system::Config { + type AdminOrigin: EnsureOrigin; + + type ProofVerifier: ProofVerifier; + + type Currency: Mutate; + + #[pallet::constant] + type TreasuryPalletId: Get; + + #[pallet::constant] + type MaxProofSize: Get; + + #[pallet::constant] + type MaxStoredProofs: Get; + + type WeightInfo: WeightInfo; + } + + pub trait WeightInfo { + fn submit_proof() -> Weight; + fn set_proof_reward() -> Weight; + fn set_sp1_vkey_hash() -> Weight; + } + + #[pallet::storage] + pub type UnprovenEpochs = + StorageMap<_, Blake2_128Concat, u64, EpochInfo, OptionQuery>; + + #[pallet::storage] + pub type UnprovenHeights = + StorageMap<_, Blake2_128Concat, u64, PendingProofInfo>, OptionQuery>; + + #[pallet::storage] + pub type ProvenHeights = StorageMap< + _, + Blake2_128Concat, + u64, + ProofMetadata>, + OptionQuery, + >; + + #[pallet::storage] + pub type RecentProofs = StorageValue< + _, + BoundedVec>, T::MaxStoredProofs>, + ValueQuery, + >; + + /// The last parachain block number where an ISMP message was dispatched + #[pallet::storage] + pub type LatestMessageBlock = StorageValue<_, u64, ValueQuery>; + + /// The last proven parachain block height + #[pallet::storage] + pub type LatestProvenParachainHeight = StorageValue<_, u64, ValueQuery>; + + /// Current BEEFY authority set epoch + #[pallet::storage] + pub type CurrentEpoch = StorageValue<_, u64, ValueQuery>; + + /// BEEFY consensus state — tracks authority sets and latest proven relay height + #[pallet::storage] + pub type ConsensusState = StorageValue<_, BeefyConsensusState, ValueQuery>; + + /// Reward amount per valid proof — updatable via governance + #[pallet::storage] + pub type ProofReward = + StorageValue<_, >::Balance, ValueQuery>; + + /// SP1 verification key hash — updatable via governance when the SP1 program changes + #[pallet::storage] + pub type Sp1VkeyHash = + StorageValue<_, alloc::vec::Vec, ValueQuery>; + + #[pallet::error] + pub enum Error { + ProofNotNeeded, + AlreadyProven, + RingBufferFull, + RewardFailed, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + ProofSubmitted { + prover: T::AccountId, + relay_chain_height: u64, + parachain_height: u64, + validator_set_id: u64, + mandatory: bool, + }, + ProofRewardUpdated { + new_reward: >::Balance, + }, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::submit_proof())] + pub fn submit_proof( + origin: OriginFor, + consensus_proof: BoundedVec, + relay_chain_height: u64, + parachain_height: u64, + validator_set_id: u64, + ) -> DispatchResult { + let prover = ensure_signed(origin)?; + + let current_epoch = CurrentEpoch::::get(); + let is_mandatory = validator_set_id > current_epoch; + + if !is_mandatory { + let last_message = LatestMessageBlock::::get(); + let last_proven = LatestProvenParachainHeight::::get(); + + ensure!( + last_message > last_proven && parachain_height >= last_message, + Error::::ProofNotNeeded, + ); + } + + ensure!( + !ProvenHeights::::contains_key(relay_chain_height), + Error::::AlreadyProven, + ); + + let trusted_state = ConsensusState::::get(); + let new_state = T::ProofVerifier::verify(&trusted_state, &consensus_proof)?; + ConsensusState::::put(new_state); + + let offchain_key = Self::offchain_proof_key(relay_chain_height, validator_set_id); + sp_io::offchain_index::set(&offchain_key, &consensus_proof); + + let metadata = ProofMetadata { + finalized_height: relay_chain_height, + validator_set_id, + prover: prover.clone(), + created_at: frame_system::Pallet::::block_number(), + }; + ProvenHeights::::insert(relay_chain_height, metadata.clone()); + + RecentProofs::::try_mutate(|proofs| -> DispatchResult { + if proofs.len() as u32 == T::MaxStoredProofs::get() { + proofs.remove(0); + } + proofs.try_push(metadata).map_err(|_| Error::::RingBufferFull)?; + Ok(()) + })?; + + LatestProvenParachainHeight::::put(parachain_height); + if is_mandatory { + CurrentEpoch::::put(validator_set_id); + } + + UnprovenEpochs::::remove(validator_set_id); + UnprovenHeights::::remove(relay_chain_height); + + let reward = ProofReward::::get(); + if reward > Default::default() { + let treasury: T::AccountId = T::TreasuryPalletId::get().into_account_truncating(); + T::Currency::transfer(&treasury, &prover, reward, Preservation::Preserve) + .map_err(|_| Error::::RewardFailed)?; + } + + Self::deposit_event(Event::ProofSubmitted { + prover, + relay_chain_height, + parachain_height, + validator_set_id, + mandatory: is_mandatory, + }); + + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::set_proof_reward())] + pub fn set_proof_reward( + origin: OriginFor, + reward: >::Balance, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + ProofReward::::put(reward); + Self::deposit_event(Event::ProofRewardUpdated { new_reward: reward }); + Ok(()) + } + + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::set_sp1_vkey_hash())] + pub fn set_sp1_vkey_hash( + origin: OriginFor, + vkey_hash: alloc::vec::Vec, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + Sp1VkeyHash::::put(vkey_hash); + Ok(()) + } + } + + impl pallet_ismp::OnDispatch for Pallet + where + BlockNumberFor: Into, + { + fn on_dispatch() { + let block: u64 = frame_system::Pallet::::block_number().into(); + LatestMessageBlock::::put(block); + } + } + + impl Pallet { + fn offchain_proof_key(finalized_height: u64, validator_set_id: u64) -> alloc::vec::Vec { + let mut key = b"outbound_proofs::".to_vec(); + key.extend_from_slice(&finalized_height.to_be_bytes()); + key.extend_from_slice(&validator_set_id.to_be_bytes()); + key + } + } +} diff --git a/modules/pallets/outbound-proofs/src/types.rs b/modules/pallets/outbound-proofs/src/types.rs new file mode 100644 index 000000000..7bb9d9b6d --- /dev/null +++ b/modules/pallets/outbound-proofs/src/types.rs @@ -0,0 +1,55 @@ +// 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. + +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Clone, Debug, PartialEq, Eq)] +pub struct EpochInfo { + pub validator_set_id: u64, + pub relay_block_number: u32, +} + +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Clone, Debug, PartialEq, Eq)] +pub struct PendingProofInfo { + pub finalized_height: u64, + pub request_count: u32, + pub created_at: BlockNumber, +} + +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Clone, Debug, PartialEq, Eq)] +pub struct ProofMetadata { + pub finalized_height: u64, + pub validator_set_id: u64, + pub prover: AccountId, + pub created_at: BlockNumber, +} + +/// On-chain BEEFY authority set commitment +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Clone, Debug, PartialEq, Eq, Default)] +pub struct BeefyAuthoritySet { + pub id: u64, + pub len: u32, + pub root: [u8; 32], +} + +/// On-chain BEEFY consensus state +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Clone, Debug, PartialEq, Eq, Default)] +pub struct BeefyConsensusState { + pub latest_height: u32, + pub beefy_activation_block: u32, + pub current_authority_set: BeefyAuthoritySet, + pub next_authority_set: BeefyAuthoritySet, +} diff --git a/modules/pallets/outbound-proofs/src/verifier.rs b/modules/pallets/outbound-proofs/src/verifier.rs new file mode 100644 index 000000000..8bfd02be2 --- /dev/null +++ b/modules/pallets/outbound-proofs/src/verifier.rs @@ -0,0 +1,159 @@ +// 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. + +//! SP1 BEEFY proof verifier +use alloc::vec::Vec; +use codec::{Decode, Encode}; + +use crate::types::{BeefyAuthoritySet, BeefyConsensusState}; + +/// Decoded SP1 BEEFY proof +#[derive(Debug, Clone, Encode, Decode)] +pub struct Sp1BeefyProof { + pub commitment: MiniCommitment, + pub mmr_leaf: PartialBeefyMmrLeaf, + pub headers: Vec, + pub proof: Vec, +} + +#[derive(Debug, Clone, Encode, Decode)] +pub struct MiniCommitment { + pub block_number: u32, + pub validator_set_id: u64, +} + +#[derive(Debug, Clone, Encode, Decode)] +pub struct PartialBeefyMmrLeaf { + pub version: u32, + pub parent_number: u32, + pub parent_hash: [u8; 32], + pub next_authority_set: AuthoritySetCommitment, + pub extra: [u8; 32], + pub k_index: u32, + pub leaf_index: u32, +} + +#[derive(Debug, Clone, Encode, Decode)] +pub struct AuthoritySetCommitment { + pub id: u64, + pub len: u32, + pub root: [u8; 32], +} + +#[derive(Debug, Clone, Encode, Decode)] +pub struct ParachainHeader { + pub id: u32, + pub header: Vec, +} + +/// Result of verifying a BEEFY proof, the updated consensus state +pub struct VerificationResult { + pub new_state: BeefyConsensusState, +} + +/// Errors from proof verification +#[derive(Debug)] +pub enum VerificationError { + DecodeFailed, + StaleHeight, + UnknownAuthoritySet, + InvalidProof, +} + +impl core::fmt::Display for VerificationError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + Self::DecodeFailed => write!(f, "Failed to decode proof"), + Self::StaleHeight => write!(f, "Proof height is stale"), + Self::UnknownAuthoritySet => write!(f, "Unknown authority set"), + Self::InvalidProof => write!(f, "SP1 proof verification failed"), + } + } +} + +pub fn verify_beefy_proof( + trusted_state: &BeefyConsensusState, + raw_proof: &[u8], + vkey_hash: &str, +) -> Result { + let proof = + Sp1BeefyProof::decode(&mut &raw_proof[..]).map_err(|_| VerificationError::DecodeFailed)?; + + if trusted_state.latest_height >= proof.commitment.block_number { + return Err(VerificationError::StaleHeight); + } + + let authority = if proof.commitment.validator_set_id == trusted_state.next_authority_set.id { + &trusted_state.next_authority_set + } else if proof.commitment.validator_set_id == trusted_state.current_authority_set.id { + &trusted_state.current_authority_set + } else { + return Err(VerificationError::UnknownAuthoritySet); + }; + + let public_inputs = build_public_inputs(&proof, authority.root, authority.len); + + #[cfg(feature = "sp1")] + { + sp1_verifier::PlonkVerifier::verify( + &proof.proof, + &public_inputs, + vkey_hash, + &sp1_verifier::PLONK_VK_BYTES, + ) + .map_err(|_| VerificationError::InvalidProof)?; + } + + let mut new_state = trusted_state.clone(); + + if proof.mmr_leaf.next_authority_set.id > trusted_state.next_authority_set.id { + new_state.current_authority_set = trusted_state.next_authority_set.clone(); + new_state.next_authority_set = BeefyAuthoritySet { + id: proof.mmr_leaf.next_authority_set.id, + len: proof.mmr_leaf.next_authority_set.len, + root: proof.mmr_leaf.next_authority_set.root, + }; + } + + new_state.latest_height = proof.commitment.block_number; + + Ok(VerificationResult { new_state }) +} + +fn build_public_inputs( + proof: &Sp1BeefyProof, + authority_root: [u8; 32], + authority_len: u32, +) -> Vec { + let leaf_hash = polkadot_sdk::sp_io::hashing::keccak_256(&proof.mmr_leaf.encode()); + + let headers: Vec<[u8; 32]> = proof + .headers + .iter() + .map(|h| polkadot_sdk::sp_io::hashing::keccak_256(&h.header)) + .collect(); + + let mut encoded = Vec::new(); + encoded.extend_from_slice(&authority_root); + encoded.extend_from_slice(&{ + let mut buf = [0u8; 32]; + buf[28..32].copy_from_slice(&authority_len.to_be_bytes()); + buf + }); + encoded.extend_from_slice(&leaf_hash); + headers.iter().for_each(|h| encoded.extend_from_slice(h)); + + encoded +} diff --git a/modules/pallets/testsuite/Cargo.toml b/modules/pallets/testsuite/Cargo.toml index bfd238473..baa3d465f 100644 --- a/modules/pallets/testsuite/Cargo.toml +++ b/modules/pallets/testsuite/Cargo.toml @@ -47,6 +47,7 @@ token-gateway-primitives = { workspace = true, default-features = true } pallet-bridge-airdrop = { workspace = true, default-features = true } pallet-consensus-incentives = { workspace = true, default-features = true } pallet-messaging-fees = { workspace = true, default-features = true } +pallet-outbound-proofs = { workspace = true, default-features = true } pallet-collator-manager = { workspace = true, default-features = true } hyperbridge-client-machine = {workspace = true, default-features = true } evm-state-machine = { workspace = true, default-features = true } diff --git a/modules/pallets/testsuite/src/runtime.rs b/modules/pallets/testsuite/src/runtime.rs index 73ca10fb2..c3b398eca 100644 --- a/modules/pallets/testsuite/src/runtime.rs +++ b/modules/pallets/testsuite/src/runtime.rs @@ -112,6 +112,7 @@ frame_support::construct_runtime!( CollatorManager: pallet_collator_manager, MsgQueue: mock_message_queue, Authorship: pallet_authorship, + OutboundProofs: pallet_outbound_proofs, } ); @@ -271,6 +272,7 @@ impl pallet_ismp::Config for Test { true, >, ); + type OnDispatch = pallet_outbound_proofs::Pallet; } impl pallet_hyperbridge::Config for Test { @@ -498,6 +500,39 @@ impl pallet_vesting::Config for Test { type BlockNumberProvider = System; } +pub struct DummyProofVerifier; +impl pallet_outbound_proofs::ProofVerifier for DummyProofVerifier { + fn verify( + trusted_state: &pallet_outbound_proofs::BeefyConsensusState, + _proof: &[u8], + ) -> Result { + Ok(trusted_state.clone()) + } +} + +pub struct OutboundProofsWeights; +impl pallet_outbound_proofs::WeightInfo for OutboundProofsWeights { + fn submit_proof() -> Weight { + Weight::zero() + } + fn set_proof_reward() -> Weight { + Weight::zero() + } + fn set_sp1_vkey_hash() -> Weight { + Weight::zero() + } +} + +impl pallet_outbound_proofs::Config for Test { + type AdminOrigin = EnsureRoot; + type ProofVerifier = DummyProofVerifier; + type Currency = Balances; + type TreasuryPalletId = TreasuryAccount; + type MaxProofSize = ConstU32<100_000>; + type MaxStoredProofs = ConstU32<3>; + type WeightInfo = OutboundProofsWeights; +} + #[derive(Default)] pub struct ErrorModule; diff --git a/modules/pallets/testsuite/src/tests/mod.rs b/modules/pallets/testsuite/src/tests/mod.rs index aa4ac8f2a..7d228b061 100644 --- a/modules/pallets/testsuite/src/tests/mod.rs +++ b/modules/pallets/testsuite/src/tests/mod.rs @@ -14,5 +14,6 @@ mod pallet_bridge_airdrop; mod pallet_collator_manager; mod pallet_consensus_incentives; mod pallet_messaging_fees; +mod pallet_outbound_proofs; mod pallet_token_gateway; mod substrate_evm_state_machine; diff --git a/parachain/runtimes/gargantua/Cargo.toml b/parachain/runtimes/gargantua/Cargo.toml index 94f7e3705..d507ee654 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-outbound-proofs = { workspace = true } pallet-ismp-runtime-api = { workspace = true } ismp-sync-committee = { workspace = true } ismp-bsc = { workspace = true } @@ -129,6 +130,7 @@ std = [ "pallet-ismp/std", "pallet-ismp-runtime-api/std", "pallet-ismp-demo/std", + "pallet-outbound-proofs/std", "ismp-sync-committee/std", "ismp-bsc/std", "ismp-grandpa/std", @@ -167,6 +169,7 @@ runtime-benchmarks = [ "ismp-parachain/runtime-benchmarks", "pallet-messaging-fees/runtime-benchmarks", "pallet-intents-coprocessor/runtime-benchmarks", + "pallet-outbound-proofs/runtime-benchmarks", ] try-runtime = [ "polkadot-sdk/try-runtime", @@ -174,10 +177,12 @@ try-runtime = [ "pallet-ismp/try-runtime", "ismp-sync-committee/try-runtime", "pallet-ismp-demo/try-runtime", + "pallet-outbound-proofs/try-runtime", "pallet-ismp-relayer/try-runtime", "pallet-ismp-host-executive/try-runtime", "pallet-mmr-tree/try-runtime", "cumulus-pallet-parachain-system/try-runtime", ] +sp1-verifier = ["pallet-outbound-proofs/sp1"] # This must be used when buiding for a runtime upgrade so metadata hash verification is possible metadata-hash = ["substrate-wasm-builder/metadata-hash"] diff --git a/parachain/runtimes/gargantua/src/ismp.rs b/parachain/runtimes/gargantua/src/ismp.rs index aebc07f77..bb6717daa 100644 --- a/parachain/runtimes/gargantua/src/ismp.rs +++ b/parachain/runtimes/gargantua/src/ismp.rs @@ -172,6 +172,7 @@ impl pallet_ismp::Config for Runtime { TreasuryPalletId, false, >; + type OnDispatch = pallet_outbound_proofs::Pallet; } impl ismp_grandpa::Config for Runtime { diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index 2a08a719f..a90a642d7 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -713,6 +713,32 @@ impl pallet_vesting::Config for Runtime { type BlockNumberProvider = System; } + +#[cfg(not(feature = "sp1-verifier"))] +pub struct DummyProofVerifier; +#[cfg(not(feature = "sp1-verifier"))] +impl pallet_outbound_proofs::ProofVerifier for DummyProofVerifier { + fn verify( + trusted_state: &pallet_outbound_proofs::BeefyConsensusState, + _proof: &[u8], + ) -> Result { + Ok(trusted_state.clone()) + } +} + +impl pallet_outbound_proofs::pallet::Config for Runtime { + type AdminOrigin = EnsureRoot; + #[cfg(feature = "sp1-verifier")] + type ProofVerifier = pallet_outbound_proofs::Sp1ProofVerifier; + #[cfg(not(feature = "sp1-verifier"))] + type ProofVerifier = DummyProofVerifier; + type Currency = Balances; + type TreasuryPalletId = TreasuryPalletId; + type MaxProofSize = ConstU32<100_000>; + type MaxStoredProofs = ConstU32<100>; + type WeightInfo = crate::weights::pallet_outbound_proofs::WeightInfo; +} + // Create the runtime by composing the FRAME pallets that were previously configured. #[frame_support::runtime] mod runtime { @@ -834,6 +860,8 @@ mod runtime { pub type IsmpTendermint = ismp_tendermint::pallet; #[runtime::pallet_index(255)] pub type IsmpGrandpa = ismp_grandpa; + #[runtime::pallet_index(90)] + pub type OutboundProofs = pallet_outbound_proofs; } #[cfg(feature = "runtime-benchmarks")] @@ -865,6 +893,7 @@ mod benches { [pallet_intents_coprocessor, IntentsCoprocessor] [pallet_transaction_payment, TransactionPayment] [pallet_vesting, Vesting] + [pallet_outbound_proofs, OutboundProofs] ); } diff --git a/parachain/runtimes/gargantua/src/weights/mod.rs b/parachain/runtimes/gargantua/src/weights/mod.rs index 0b23c2dad..d9345023e 100644 --- a/parachain/runtimes/gargantua/src/weights/mod.rs +++ b/parachain/runtimes/gargantua/src/weights/mod.rs @@ -33,6 +33,7 @@ pub mod pallet_assets; pub mod pallet_balances; pub mod pallet_collective; pub mod pallet_intents_coprocessor; +pub mod pallet_outbound_proofs; pub mod pallet_message_queue; pub mod pallet_session; pub mod pallet_sudo; diff --git a/parachain/runtimes/nexus/src/ismp.rs b/parachain/runtimes/nexus/src/ismp.rs index 51a570271..87f9c492f 100644 --- a/parachain/runtimes/nexus/src/ismp.rs +++ b/parachain/runtimes/nexus/src/ismp.rs @@ -177,6 +177,7 @@ impl pallet_ismp::Config for Runtime { type OffchainDB = Mmr; type FeeHandler = (pallet_consensus_incentives::Pallet, pallet_messaging_fees::Pallet); + type OnDispatch = (); } impl pallet_ismp_relayer::Config for Runtime { 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..5627e0775 100644 --- a/tesseract/consensus/beefy/src/host.rs +++ b/tesseract/consensus/beefy/src/host.rs @@ -117,6 +117,7 @@ where Ok(()) } + } #[async_trait::async_trait] @@ -182,7 +183,10 @@ where let item = self.backend.receive_mandatory_proof(&counterparty_state_machine).await; - let QueueMessage { id, proof: ConsensusProof { message, set_id, .. } } = + let QueueMessage { + id, + proof: ConsensusProof { message, set_id, .. }, + } = match item { Ok(Some(message)) => message, // no new items in the queue, continue to process messages queue @@ -265,7 +269,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 49167cb82..c7999fa6c 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, @@ -572,6 +575,7 @@ where } } } + } /// Beefy prover, can either produce zk proofs or naive proofs