fix: Complete snowbridge pezpallet rebrand and critical bug fixes

- snowbridge-pezpallet-* → pezsnowbridge-pezpallet-* (201 refs)
- pallet/ directories → pezpallet/ (4 locations)
- Fixed pezpallet.rs self-include recursion bug
- Fixed sc-chain-spec hardcoded crate name in derive macro
- Reverted .pezpallet_by_name() to .pallet_by_name() (subxt API)
- Added BizinikiwiConfig type alias for zombienet tests
- Deleted obsolete session state files

Verified: pezsnowbridge-pezpallet-*, pezpallet-staking,
pezpallet-staking-async, pezframe-benchmarking-cli all pass cargo check
This commit is contained in:
2025-12-16 09:57:23 +03:00
parent eea003e14d
commit 3139ffa25e
3022 changed files with 42157 additions and 23579 deletions
@@ -0,0 +1,37 @@
// Copyright (C) Parity Technologies (UK) 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.
//! Custom digest items
use codec::{Decode, Encode};
use pezsp_core::{RuntimeDebug, H256};
use pezsp_runtime::generic::DigestItem;
/// Custom header digest items, inserted as DigestItem::Other
#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, RuntimeDebug)]
pub enum SnowbridgeDigestItem {
#[codec(index = 0)]
/// Merkle root of outbound Snowbridge messages.
Snowbridge(H256),
#[codec(index = 1)]
/// Merkle root of outbound Snowbridge V2 messages.
SnowbridgeV2(H256),
}
/// Convert custom application digest item into a concrete digest item
impl From<SnowbridgeDigestItem> for DigestItem {
fn from(val: SnowbridgeDigestItem) -> Self {
DigestItem::Other(val.encode())
}
}
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! # Core
//!
//! Common traits and types
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod tests;
pub mod digest_item;
pub mod location;
pub mod operating_mode;
pub mod pricing;
pub mod reward;
pub mod ringbuffer;
pub mod sparse_bitmap;
pub use location::{AgentId, AgentIdOf, TokenId, TokenIdOf};
pub use pezkuwi_teyrchain_primitives::primitives::{
Id as ParaId, IsSystem, Sibling as SiblingParaId,
};
pub use ringbuffer::{RingBufferMap, RingBufferMapImpl};
pub use pezsp_core::U256;
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use pezframe_support::{traits::Contains, BoundedVec};
use hex_literal::hex;
use scale_info::TypeInfo;
use pezsp_core::{ConstU32, H256};
use pezsp_io::hashing::keccak_256;
use pezsp_runtime::{traits::AccountIdConversion, RuntimeDebug};
use pezsp_std::prelude::*;
use xcm::latest::{Asset, Junction::Teyrchain, Location, Result as XcmResult, XcmContext};
use xcm_executor::traits::TransactAsset;
/// The ID of an agent contract
pub use operating_mode::BasicOperatingMode;
pub use pricing::{PricingParameters, Rewards};
pub fn sibling_sovereign_account<T>(para_id: ParaId) -> T::AccountId
where
T: pezframe_system::Config,
{
SiblingParaId::from(para_id).into_account_truncating()
}
pub struct AllowSiblingsOnly;
impl Contains<Location> for AllowSiblingsOnly {
fn contains(location: &Location) -> bool {
matches!(location.unpack(), (1, [Teyrchain(_)]))
}
}
pub fn gwei(x: u128) -> U256 {
U256::from(1_000_000_000u128).saturating_mul(x.into())
}
pub fn meth(x: u128) -> U256 {
U256::from(1_000_000_000_000_000u128).saturating_mul(x.into())
}
pub fn eth(x: u128) -> U256 {
U256::from(1_000_000_000_000_000_000u128).saturating_mul(x.into())
}
pub const TYR: u128 = 1_000_000_000_000;
/// Identifier for a message channel
#[derive(
Clone,
Copy,
Encode,
Decode,
DecodeWithMemTracking,
PartialEq,
Eq,
Default,
RuntimeDebug,
MaxEncodedLen,
TypeInfo,
)]
pub struct ChannelId([u8; 32]);
/// Deterministically derive a ChannelId for a sibling teyrchain
/// Generator: keccak256("para" + big_endian_bytes(para_id))
///
/// The equivalent generator on the Solidity side is in
/// contracts/src/Types.sol:into().
fn derive_channel_id_for_sibling(para_id: ParaId) -> ChannelId {
let para_id: u32 = para_id.into();
let para_id_bytes: [u8; 4] = para_id.to_be_bytes();
let prefix: [u8; 4] = *b"para";
let preimage: Vec<u8> = prefix.into_iter().chain(para_id_bytes).collect();
keccak_256(&preimage).into()
}
impl ChannelId {
pub const fn new(id: [u8; 32]) -> Self {
ChannelId(id)
}
}
impl From<ParaId> for ChannelId {
fn from(value: ParaId) -> Self {
derive_channel_id_for_sibling(value)
}
}
impl From<[u8; 32]> for ChannelId {
fn from(value: [u8; 32]) -> Self {
ChannelId(value)
}
}
impl From<ChannelId> for [u8; 32] {
fn from(value: ChannelId) -> Self {
value.0
}
}
impl<'a> From<&'a [u8; 32]> for ChannelId {
fn from(value: &'a [u8; 32]) -> Self {
ChannelId(*value)
}
}
impl From<H256> for ChannelId {
fn from(value: H256) -> Self {
ChannelId(value.into())
}
}
impl AsRef<[u8]> for ChannelId {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
#[derive(Clone, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo)]
pub struct Channel {
/// ID of the agent contract deployed on Ethereum
pub agent_id: AgentId,
/// ID of the teyrchain who will receive or send messages using this channel
pub para_id: ParaId,
}
pub trait StaticLookup {
/// Type to lookup from.
type Source;
/// Type to lookup into.
type Target;
/// Attempt a lookup.
fn lookup(s: Self::Source) -> Option<Self::Target>;
}
/// Channel for high-priority governance commands
pub const PRIMARY_GOVERNANCE_CHANNEL: ChannelId =
ChannelId::new(hex!("0000000000000000000000000000000000000000000000000000000000000001"));
/// Channel for lower-priority governance commands
pub const SECONDARY_GOVERNANCE_CHANNEL: ChannelId =
ChannelId::new(hex!("0000000000000000000000000000000000000000000000000000000000000002"));
/// Metadata to include in the instantiated ERC20 token contract
#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, RuntimeDebug, TypeInfo)]
pub struct AssetMetadata {
pub name: BoundedVec<u8, ConstU32<METADATA_FIELD_MAX_LEN>>,
pub symbol: BoundedVec<u8, ConstU32<METADATA_FIELD_MAX_LEN>>,
pub decimals: u8,
}
#[cfg(any(test, feature = "std", feature = "runtime-benchmarks"))]
impl Default for AssetMetadata {
fn default() -> Self {
AssetMetadata {
name: BoundedVec::truncate_from(vec![]),
symbol: BoundedVec::truncate_from(vec![]),
decimals: 0,
}
}
}
/// Maximum length of a string field in ERC20 token metada
const METADATA_FIELD_MAX_LEN: u32 = 32;
/// Helper function that validates `fee` can be burned, then withdraws it from `origin` and burns
/// it.
/// Note: Make sure this is called from a transactional storage context so that side-effects
/// are rolled back on errors.
pub fn burn_for_teleport<AssetTransactor>(origin: &Location, fee: &Asset) -> XcmResult
where
AssetTransactor: TransactAsset,
{
let dummy_context = XcmContext { origin: None, message_id: Default::default(), topic: None };
AssetTransactor::can_check_out(origin, fee, &dummy_context)?;
AssetTransactor::check_out(origin, fee, &dummy_context);
AssetTransactor::withdraw_asset(fee, origin, None)?;
Ok(())
}
@@ -0,0 +1,228 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! # Location
//!
//! Location helpers for dealing with Tokens and Agents
pub use pezkuwi_teyrchain_primitives::primitives::{
Id as ParaId, IsSystem, Sibling as SiblingParaId,
};
pub use pezsp_core::U256;
use codec::Encode;
use pezsp_core::H256;
use pezsp_std::prelude::*;
use xcm::prelude::{
AccountId32, AccountKey20, GeneralIndex, GeneralKey, GlobalConsensus, Location, PalletInstance,
};
use xcm_builder::{
DescribeAllTerminal, DescribeFamily, DescribeLocation, DescribeTerminus, HashedDescription,
};
pub type AgentId = H256;
/// Creates an AgentId from a Location. An AgentId is a unique mapping to an Agent contract on
/// Ethereum which acts as the sovereign account for the Location.
/// Resolves Pezkuwi locations (as seen by Ethereum) to unique `AgentId` identifiers.
pub type AgentIdOf = HashedDescription<
AgentId,
(
DescribeHere,
DescribeFamily<DescribeAllTerminal>,
DescribeGlobalPrefix<(DescribeTerminus, DescribeFamily<DescribeTokenTerminal>)>,
),
>;
pub type TokenId = H256;
/// Convert a token location (relative to Ethereum) to a stable ID that can be used on the Ethereum
/// side
pub type TokenIdOf = HashedDescription<
TokenId,
DescribeGlobalPrefix<(DescribeTerminus, DescribeFamily<DescribeTokenTerminal>)>,
>;
/// This looks like DescribeTerminus that was added to xcm-builder. However this does an extra
/// `encode` to the Vector producing a different output to DescribeTerminus. `DescribeHere`
/// should NOT be used for new code. This is left here for backwards compatibility of channels and
/// agents.
pub struct DescribeHere;
#[allow(deprecated)]
impl DescribeLocation for DescribeHere {
fn describe_location(l: &Location) -> Option<Vec<u8>> {
match l.unpack() {
(0, []) => Some(Vec::<u8>::new().encode()),
_ => None,
}
}
}
pub struct DescribeGlobalPrefix<DescribeInterior>(pezsp_std::marker::PhantomData<DescribeInterior>);
impl<Suffix: DescribeLocation> DescribeLocation for DescribeGlobalPrefix<Suffix> {
fn describe_location(l: &Location) -> Option<Vec<u8>> {
match (l.parent_count(), l.first_interior()) {
(1, Some(GlobalConsensus(network))) => {
let mut tail = l.clone().split_first_interior().0;
tail.dec_parent();
let interior = Suffix::describe_location(&tail)?;
Some((b"GlobalConsensus", network, interior).encode())
},
_ => None,
}
}
}
pub struct DescribeTokenTerminal;
impl DescribeLocation for DescribeTokenTerminal {
fn describe_location(l: &Location) -> Option<Vec<u8>> {
match l.unpack().1 {
[] => Some(Vec::<u8>::new().encode()),
[GeneralIndex(index)] => Some((b"GeneralIndex", *index).encode()),
[GeneralKey { data, .. }] => Some((b"GeneralKey", *data).encode()),
[AccountKey20 { key, .. }] => Some((b"AccountKey20", *key).encode()),
[AccountId32 { id, .. }] => Some((b"AccountId32", *id).encode()),
// Pezpallet
[PalletInstance(instance)] => Some((b"PalletInstance", *instance).encode()),
[PalletInstance(instance), GeneralIndex(index)] =>
Some((b"PalletInstance", *instance, b"GeneralIndex", *index).encode()),
[PalletInstance(instance), GeneralKey { data, .. }] =>
Some((b"PalletInstance", *instance, b"GeneralKey", *data).encode()),
[PalletInstance(instance), AccountKey20 { key, .. }] =>
Some((b"PalletInstance", *instance, b"AccountKey20", *key).encode()),
[PalletInstance(instance), AccountId32 { id, .. }] =>
Some((b"PalletInstance", *instance, b"AccountId32", *id).encode()),
// Reject all other locations
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use crate::TokenIdOf;
use xcm::{
latest::ZAGROS_GENESIS_HASH,
prelude::{
GeneralIndex, GeneralKey, GlobalConsensus, Junction::*, Location, NetworkId::ByGenesis,
PalletInstance, Teyrchain,
},
};
use xcm_executor::traits::ConvertLocation;
#[test]
fn test_token_of_id() {
let token_locations = [
// Relay Chain cases
// Relay Chain relative to Ethereum
Location::new(1, [GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH))]),
// Teyrchain cases
// Teyrchain relative to Ethereum
Location::new(1, [GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)), Teyrchain(2000)]),
// Teyrchain general index
Location::new(
1,
[GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)), Teyrchain(2000), GeneralIndex(1)],
),
// Teyrchain general key
Location::new(
1,
[
GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)),
Teyrchain(2000),
GeneralKey { length: 32, data: [0; 32] },
],
),
// Teyrchain account key 20
Location::new(
1,
[
GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)),
Teyrchain(2000),
AccountKey20 { network: None, key: [0; 20] },
],
),
// Teyrchain account id 32
Location::new(
1,
[
GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)),
Teyrchain(2000),
AccountId32 { network: None, id: [0; 32] },
],
),
// Parchain Pezpallet instance cases
// Teyrchain pezpallet instance
Location::new(
1,
[
GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)),
Teyrchain(2000),
PalletInstance(8),
],
),
// Teyrchain Pezpallet general index
Location::new(
1,
[
GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)),
Teyrchain(2000),
PalletInstance(8),
GeneralIndex(1),
],
),
// Teyrchain Pezpallet general key
Location::new(
1,
[
GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)),
Teyrchain(2000),
PalletInstance(8),
GeneralKey { length: 32, data: [0; 32] },
],
),
// Teyrchain Pezpallet account key 20
Location::new(
1,
[
GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)),
Teyrchain(2000),
PalletInstance(8),
AccountKey20 { network: None, key: [0; 20] },
],
),
// Teyrchain Pezpallet account id 32
Location::new(
1,
[
GlobalConsensus(ByGenesis(ZAGROS_GENESIS_HASH)),
Teyrchain(2000),
PalletInstance(8),
AccountId32 { network: None, id: [0; 32] },
],
),
];
for token in token_locations {
assert!(
TokenIdOf::convert_location(&token).is_some(),
"Valid token = {token:?} yields no TokenId."
);
}
let non_token_locations = [
// Relative location for a token should fail.
Location::new(1, []),
// Relative location for a token should fail.
Location::new(1, [Teyrchain(1000)]),
];
for token in non_token_locations {
assert!(
TokenIdOf::convert_location(&token).is_none(),
"Invalid token = {token:?} yields a TokenId."
);
}
}
}
@@ -0,0 +1,41 @@
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use pezsp_runtime::RuntimeDebug;
/// Basic operating modes for a bridges module (Normal/Halted).
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
Clone,
Copy,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BasicOperatingMode {
/// Normal mode, when all operations are allowed.
Normal,
/// The pezpallet is halted. All non-governance operations are disabled.
Halted,
}
impl Default for BasicOperatingMode {
fn default() -> Self {
Self::Normal
}
}
impl BasicOperatingMode {
pub fn is_halted(&self) -> bool {
*self == BasicOperatingMode::Halted
}
}
/// Check whether the export message is paused based on the status of the basic operating mode.
pub trait ExportPausedQuery {
fn is_paused() -> bool;
}
@@ -0,0 +1,76 @@
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use pezsp_arithmetic::traits::{BaseArithmetic, Unsigned, Zero};
use pezsp_core::U256;
use pezsp_runtime::{FixedU128, RuntimeDebug};
use pezsp_std::prelude::*;
#[derive(
Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo,
)]
pub struct PricingParameters<Balance> {
/// ETH/HEZ exchange rate
pub exchange_rate: FixedU128,
/// Relayer rewards
pub rewards: Rewards<Balance>,
/// Ether (wei) fee per gas unit
pub fee_per_gas: U256,
/// Fee multiplier
pub multiplier: FixedU128,
}
#[derive(
Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo,
)]
pub struct Rewards<Balance> {
/// Local reward in HEZ
pub local: Balance,
/// Remote reward in ETH (wei)
pub remote: U256,
}
#[derive(RuntimeDebug)]
pub struct InvalidPricingParameters;
impl<Balance> PricingParameters<Balance>
where
Balance: BaseArithmetic + Unsigned + Copy,
{
pub fn validate(&self) -> Result<(), InvalidPricingParameters> {
if self.exchange_rate == FixedU128::zero() {
return Err(InvalidPricingParameters);
}
if self.fee_per_gas == U256::zero() {
return Err(InvalidPricingParameters);
}
if self.rewards.local.is_zero() {
return Err(InvalidPricingParameters);
}
if self.rewards.remote.is_zero() {
return Err(InvalidPricingParameters);
}
if self.multiplier == FixedU128::zero() {
return Err(InvalidPricingParameters);
}
Ok(())
}
}
/// Holder for fixed point number implemented in <https://github.com/PaulRBerg/prb-math>
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(PartialEq))]
pub struct UD60x18(U256);
impl From<FixedU128> for UD60x18 {
fn from(value: FixedU128) -> Self {
// Both FixedU128 and UD60x18 have 18 decimal places
let inner: u128 = value.into_inner();
UD60x18(inner.into())
}
}
impl UD60x18 {
pub fn into_inner(self) -> U256 {
self.0
}
}
@@ -0,0 +1,401 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
extern crate alloc;
use crate::reward::RewardPaymentError::{ChargeFeesFailure, XcmSendFailure};
use bp_relayers::PaymentProcedure;
use codec::DecodeWithMemTracking;
use pezframe_support::{dispatch::GetDispatchInfo, PalletError};
use scale_info::TypeInfo;
use pezsp_runtime::{
codec::{Decode, Encode},
traits::Get,
DispatchError,
};
use pezsp_std::{fmt::Debug, marker::PhantomData};
use xcm::{
opaque::latest::prelude::Xcm,
prelude::{ExecuteXcm, Junction::*, Location, SendXcm, *},
};
/// Describes the message that the tip should be added to (either Inbound or Outbound message) and
/// the message nonce.
#[derive(Debug, Clone, PartialEq, Encode, Decode, DecodeWithMemTracking, TypeInfo)]
pub enum MessageId {
/// Message from Ethereum
Inbound(u64),
/// Message to Ethereum
Outbound(u64),
}
#[derive(Debug, Encode, PartialEq, DecodeWithMemTracking, Decode, TypeInfo, PalletError)]
pub enum AddTipError {
NonceConsumed,
UnknownMessage,
AmountZero,
}
/// Trait to add a tip for a nonce.
pub trait AddTip {
/// Add a relayer reward tip to a pezpallet.
fn add_tip(nonce: u64, amount: u128) -> Result<(), AddTipError>;
}
/// Error related to paying out relayer rewards.
#[derive(Debug, Encode, Decode)]
pub enum RewardPaymentError {
/// The XCM to mint the reward on AssetHub could not be sent.
XcmSendFailure,
/// The delivery fee to send the XCM could not be charged.
ChargeFeesFailure,
}
impl From<RewardPaymentError> for DispatchError {
fn from(e: RewardPaymentError) -> DispatchError {
match e {
XcmSendFailure => DispatchError::Other("xcm send failure"),
ChargeFeesFailure => DispatchError::Other("charge fees error"),
}
}
}
/// Reward payment procedure that sends a XCM to AssetHub to mint the reward (foreign asset)
/// into the provided beneficiary account.
pub struct PayAccountOnLocation<
Relayer,
RewardBalance,
EthereumNetwork,
AssetHubLocation,
InboundQueueLocation,
XcmSender,
XcmExecutor,
Call,
>(
PhantomData<(
Relayer,
RewardBalance,
EthereumNetwork,
AssetHubLocation,
InboundQueueLocation,
XcmSender,
XcmExecutor,
Call,
)>,
);
impl<
Relayer,
RewardBalance,
EthereumNetwork,
AssetHubLocation,
InboundQueueLocation,
XcmSender,
XcmExecutor,
Call,
> PaymentProcedure<Relayer, (), RewardBalance>
for PayAccountOnLocation<
Relayer,
RewardBalance,
EthereumNetwork,
AssetHubLocation,
InboundQueueLocation,
XcmSender,
XcmExecutor,
Call,
>
where
Relayer: Clone
+ Debug
+ Decode
+ Encode
+ Eq
+ TypeInfo
+ Into<pezsp_runtime::AccountId32>
+ Into<Location>,
EthereumNetwork: Get<NetworkId>,
InboundQueueLocation: Get<InteriorLocation>,
AssetHubLocation: Get<Location>,
XcmSender: SendXcm,
RewardBalance: Into<u128> + Clone,
XcmExecutor: ExecuteXcm<Call>,
Call: Decode + GetDispatchInfo,
{
type Error = DispatchError;
type Beneficiary = Location;
fn pay_reward(
relayer: &Relayer,
_: (),
reward: RewardBalance,
beneficiary: Self::Beneficiary,
) -> Result<(), Self::Error> {
let ethereum_location = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]);
let assets: Asset = (ethereum_location.clone(), reward.into()).into();
let xcm: Xcm<()> = alloc::vec![
UnpaidExecution { weight_limit: Unlimited, check_origin: None },
DescendOrigin(InboundQueueLocation::get().into()),
UniversalOrigin(GlobalConsensus(EthereumNetwork::get())),
ReserveAssetDeposited(assets.into()),
DepositAsset { assets: AllCounted(1).into(), beneficiary },
]
.into();
let (ticket, fee) =
validate_send::<XcmSender>(AssetHubLocation::get(), xcm).map_err(|_| XcmSendFailure)?;
XcmExecutor::charge_fees(relayer.clone(), fee).map_err(|_| ChargeFeesFailure)?;
XcmSender::deliver(ticket).map_err(|_| XcmSendFailure)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use pezframe_support::parameter_types;
use pezsp_runtime::AccountId32;
#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)]
pub struct MockRelayer(pub AccountId32);
impl From<MockRelayer> for AccountId32 {
fn from(m: MockRelayer) -> Self {
m.0
}
}
impl From<MockRelayer> for Location {
fn from(_m: MockRelayer) -> Self {
// For simplicity, return a dummy location
Location::new(1, Here)
}
}
pub enum BridgeReward {
#[allow(dead_code)]
Snowbridge,
}
parameter_types! {
pub AssetHubLocation: Location = Location::new(1,[Teyrchain(1000)]);
pub InboundQueueLocation: InteriorLocation = [PalletInstance(84)].into();
pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 };
pub const DefaultMyRewardKind: BridgeReward = BridgeReward::Snowbridge;
}
pub enum Weightless {}
impl PreparedMessage for Weightless {
fn weight_of(&self) -> Weight {
unreachable!();
}
}
pub struct MockXcmExecutor;
impl<C> ExecuteXcm<C> for MockXcmExecutor {
type Prepared = Weightless;
fn prepare(_: Xcm<C>, _: Weight) -> Result<Self::Prepared, InstructionError> {
Err(InstructionError { index: 0, error: XcmError::Unimplemented })
}
fn execute(
_: impl Into<Location>,
_: Self::Prepared,
_: &mut XcmHash,
_: Weight,
) -> Outcome {
unreachable!()
}
fn charge_fees(_: impl Into<Location>, _: Assets) -> xcm::latest::Result {
Ok(())
}
}
#[derive(Debug, Decode, Default)]
pub struct MockCall;
impl GetDispatchInfo for MockCall {
fn get_dispatch_info(&self) -> pezframe_support::dispatch::DispatchInfo {
Default::default()
}
}
pub struct MockXcmSender;
impl SendXcm for MockXcmSender {
type Ticket = Xcm<()>;
fn validate(
dest: &mut Option<Location>,
xcm: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
if let Some(location) = dest {
match location.unpack() {
(_, [Teyrchain(1001)]) => return Err(SendError::NotApplicable),
_ => Ok((xcm.clone().unwrap(), Assets::default())),
}
} else {
Ok((xcm.clone().unwrap(), Assets::default()))
}
}
fn deliver(xcm: Self::Ticket) -> core::result::Result<XcmHash, SendError> {
let hash = xcm.using_encoded(pezsp_io::hashing::blake2_256);
Ok(hash)
}
}
#[test]
fn pay_reward_success() {
let relayer = MockRelayer(AccountId32::new([1u8; 32]));
let beneficiary = Location::new(1, Here);
let reward = 1_000u128;
type TestedPayAccountOnLocation = PayAccountOnLocation<
MockRelayer,
u128,
EthereumNetwork,
AssetHubLocation,
InboundQueueLocation,
MockXcmSender,
MockXcmExecutor,
MockCall,
>;
let result = TestedPayAccountOnLocation::pay_reward(&relayer, (), reward, beneficiary);
assert!(result.is_ok());
}
#[test]
fn pay_reward_fails_on_xcm_validate_xcm() {
struct FailingXcmValidator;
impl SendXcm for FailingXcmValidator {
type Ticket = ();
fn validate(
_dest: &mut Option<Location>,
_xcm: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
Err(SendError::NotApplicable)
}
fn deliver(xcm: Self::Ticket) -> core::result::Result<XcmHash, SendError> {
let hash = xcm.using_encoded(pezsp_io::hashing::blake2_256);
Ok(hash)
}
}
type FailingSenderPayAccount = PayAccountOnLocation<
MockRelayer,
u128,
EthereumNetwork,
AssetHubLocation,
InboundQueueLocation,
FailingXcmValidator,
MockXcmExecutor,
MockCall,
>;
let relayer = MockRelayer(AccountId32::new([1u8; 32]));
let reward = 1_000u128;
let beneficiary = Location::new(1, Here);
let result = FailingSenderPayAccount::pay_reward(&relayer, (), reward, beneficiary);
assert!(result.is_err());
let err_str = format!("{:?}", result.err().unwrap());
assert!(
err_str.contains("xcm send failure"),
"Expected xcm send failure error, got {:?}",
err_str
);
}
#[test]
fn pay_reward_fails_on_charge_fees() {
struct FailingXcmExecutor;
impl<C> ExecuteXcm<C> for FailingXcmExecutor {
type Prepared = Weightless;
fn prepare(_: Xcm<C>, _: Weight) -> Result<Self::Prepared, InstructionError> {
Err(InstructionError { index: 0, error: XcmError::Unimplemented })
}
fn execute(
_: impl Into<Location>,
_: Self::Prepared,
_: &mut XcmHash,
_: Weight,
) -> Outcome {
unreachable!()
}
fn charge_fees(_: impl Into<Location>, _: Assets) -> xcm::latest::Result {
Err(crate::reward::SendError::Fees.into())
}
}
type FailingExecutorPayAccount = PayAccountOnLocation<
MockRelayer,
u128,
EthereumNetwork,
AssetHubLocation,
InboundQueueLocation,
MockXcmSender,
FailingXcmExecutor,
MockCall,
>;
let relayer = MockRelayer(AccountId32::new([3u8; 32]));
let beneficiary = Location::new(1, Here);
let reward = 500u128;
let result = FailingExecutorPayAccount::pay_reward(&relayer, (), reward, beneficiary);
assert!(result.is_err());
let err_str = format!("{:?}", result.err().unwrap());
assert!(
err_str.contains("charge fees error"),
"Expected 'charge fees error', got {:?}",
err_str
);
}
#[test]
fn pay_reward_fails_on_delivery() {
#[derive(Default)]
struct FailingDeliveryXcmSender;
impl SendXcm for FailingDeliveryXcmSender {
type Ticket = ();
fn validate(
_dest: &mut Option<Location>,
_xcm: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
Ok(((), Assets::from(vec![])))
}
fn deliver(_xcm: Self::Ticket) -> core::result::Result<XcmHash, SendError> {
Err(SendError::NotApplicable)
}
}
type FailingDeliveryPayAccount = PayAccountOnLocation<
MockRelayer,
u128,
EthereumNetwork,
AssetHubLocation,
InboundQueueLocation,
FailingDeliveryXcmSender,
MockXcmExecutor,
MockCall,
>;
let relayer = MockRelayer(AccountId32::new([4u8; 32]));
let beneficiary = Location::new(1, Here);
let reward = 123u128;
let result = FailingDeliveryPayAccount::pay_reward(&relayer, (), reward, beneficiary);
assert!(result.is_err());
let err_str = format!("{:?}", result.err().unwrap());
assert!(
err_str.contains("xcm send failure"),
"Expected 'xcm delivery failure', got {:?}",
err_str
);
}
}
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
use codec::FullCodec;
use core::{cmp::Ord, marker::PhantomData, ops::Add};
use pezframe_support::storage::{types::QueryKindTrait, StorageMap, StorageValue};
use pezsp_core::{Get, GetDefault};
use pezsp_runtime::traits::{One, Zero};
/// Trait object presenting the ringbuffer interface.
pub trait RingBufferMap<Key, Value, QueryKind>
where
Key: FullCodec,
Value: FullCodec,
QueryKind: QueryKindTrait<Value, GetDefault>,
{
/// Insert a map entry.
fn insert(k: Key, v: Value);
/// Check if map contains a key
fn contains_key(k: Key) -> bool;
/// Get the value of the key
fn get(k: Key) -> QueryKind::Query;
}
pub struct RingBufferMapImpl<Index, B, CurrentIndex, Intermediate, M, QueryKind>(
PhantomData<(Index, B, CurrentIndex, Intermediate, M, QueryKind)>,
);
/// Ringbuffer implementation based on `RingBufferTransient`
impl<Key, Value, Index, B, CurrentIndex, Intermediate, M, QueryKind>
RingBufferMap<Key, Value, QueryKind>
for RingBufferMapImpl<Index, B, CurrentIndex, Intermediate, M, QueryKind>
where
Key: FullCodec + Clone,
Value: FullCodec,
Index: Ord + One + Zero + Add<Output = Index> + Copy + FullCodec + Eq,
B: Get<Index>,
CurrentIndex: StorageValue<Index, Query = Index>,
Intermediate: StorageMap<Index, Key, Query = Key>,
M: StorageMap<Key, Value, Query = QueryKind::Query>,
QueryKind: QueryKindTrait<Value, GetDefault>,
{
/// Insert a map entry.
fn insert(k: Key, v: Value) {
let bound = B::get();
let mut current_index = CurrentIndex::get();
// Adding one here as bound denotes number of items but our index starts with zero.
if (current_index + Index::one()) >= bound {
current_index = Index::zero();
} else {
current_index = current_index + Index::one();
}
// Deleting earlier entry if it exists
if Intermediate::contains_key(current_index) {
let older_key = Intermediate::get(current_index);
M::remove(older_key);
}
Intermediate::insert(current_index, k.clone());
CurrentIndex::set(current_index);
M::insert(k, v);
}
/// Check if map contains a key
fn contains_key(k: Key) -> bool {
M::contains_key(k)
}
/// Get the value associated with key
fn get(k: Key) -> M::Query {
M::get(k)
}
}
@@ -0,0 +1,339 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! # Sparse Bitmap
//!
//! A module that provides an efficient way to track message nonces using a sparse bitmap.
//!
//! ## Overview
//!
//! The `SparseBitmap` uses a `StorageMap<u64, u128>` to store bit flags for a large range of
//! nonces. Each key (bucket) in the storage map contains a 128-bit value that can track 128
//! individual nonces.
//!
//! The implementation efficiently maps a u64 index (nonce) to:
//! 1. A bucket - calculated as `index >> 7` (dividing by 128)
//! 2. A bit position - calculated as `index & 127` (remainder when dividing by 128)
//!
//! ## Example
//!
//! For nonce 300:
//! - Bucket = 300 >> 7 = 2 (third bucket)
//! - Bit position = 300 & 127 = 44 (45th bit in the bucket)
//! - Corresponding bit mask = 1 << 44
//!
//! This approach allows tracking up to 2^64 nonces while only storing buckets that actually contain
//! data, making it suitable for sparse sets of nonces across a wide range.
use pezframe_support::storage::StorageMap;
use pezsp_std::marker::PhantomData;
/// Sparse bitmap interface.
pub trait SparseBitmap<BitMap>
where
BitMap: StorageMap<u64, u128, Query = u128>,
{
/// Get the bool at the provided index.
fn get(index: u64) -> bool;
/// Set the bool at the given index to true.
fn set(index: u64);
}
/// Sparse bitmap implementation.
pub struct SparseBitmapImpl<BitMap>(PhantomData<BitMap>);
impl<BitMap> SparseBitmapImpl<BitMap>
where
BitMap: StorageMap<u64, u128, Query = u128>,
{
/// Computes the bucket index and the bit mask for a given bit index.
/// Each bucket contains 128 bits.
fn compute_bucket_and_mask(index: u64) -> (u64, u128) {
(index >> 7, 1u128 << (index & 127))
}
}
impl<BitMap> SparseBitmap<BitMap> for SparseBitmapImpl<BitMap>
where
BitMap: StorageMap<u64, u128, Query = u128>,
{
/// Checks if the bit at the specified index is set.
/// Returns `true` if the bit is set, `false` otherwise.
/// * `index`: The index (nonce) to check.
fn get(index: u64) -> bool {
// Calculate bucket and mask
let (bucket, mask) = Self::compute_bucket_and_mask(index);
// Retrieve bucket and check bit
let bucket_value = BitMap::get(bucket);
bucket_value & mask != 0
}
/// Sets the bit at the specified index.
/// This marks the nonce as processed by setting its corresponding bit in the bitmap.
/// * `index`: The index (nonce) to set.
fn set(index: u64) {
// Calculate bucket and mask
let (bucket, mask) = Self::compute_bucket_and_mask(index);
// Mutate the storage to set the bit
BitMap::mutate(bucket, |value| {
*value |= mask; // Set the bit in the bucket
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use pezframe_support::{
storage::{generator::StorageMap as StorageMapHelper, storage_prefix},
Twox64Concat,
};
use pezsp_io::TestExternalities;
pub struct MockStorageMap;
impl StorageMapHelper<u64, u128> for MockStorageMap {
type Query = u128;
type Hasher = Twox64Concat;
fn pezpallet_prefix() -> &'static [u8] {
b"MyModule"
}
fn storage_prefix() -> &'static [u8] {
b"MyStorageMap"
}
fn prefix_hash() -> [u8; 32] {
storage_prefix(Self::pezpallet_prefix(), Self::storage_prefix())
}
fn from_optional_value_to_query(v: Option<u128>) -> Self::Query {
v.unwrap_or_default()
}
fn from_query_to_optional_value(v: Self::Query) -> Option<u128> {
Some(v)
}
}
type TestSparseBitmap = SparseBitmapImpl<MockStorageMap>;
#[test]
fn test_sparse_bitmap_set_and_get() {
TestExternalities::default().execute_with(|| {
let index = 300u64;
let (bucket, mask) = TestSparseBitmap::compute_bucket_and_mask(index);
// Test initial state
assert_eq!(MockStorageMap::get(bucket), 0);
assert!(!TestSparseBitmap::get(index));
// Set the bit
TestSparseBitmap::set(index);
// Test after setting
assert_eq!(MockStorageMap::get(bucket), mask);
assert!(TestSparseBitmap::get(index));
});
}
#[test]
fn test_sparse_bitmap_multiple_sets() {
TestExternalities::default().execute_with(|| {
let index1 = 300u64;
let index2 = 305u64; // Same bucket, different bit
let (bucket, _) = TestSparseBitmap::compute_bucket_and_mask(index1);
let (_, mask1) = TestSparseBitmap::compute_bucket_and_mask(index1);
let (_, mask2) = TestSparseBitmap::compute_bucket_and_mask(index2);
// Test initial state
assert_eq!(MockStorageMap::get(bucket), 0);
assert!(!TestSparseBitmap::get(index1));
assert!(!TestSparseBitmap::get(index2));
// Set the first bit
TestSparseBitmap::set(index1);
// Test after first set
assert_eq!(MockStorageMap::get(bucket), mask1);
assert!(TestSparseBitmap::get(index1));
assert!(!TestSparseBitmap::get(index2));
// Set the second bit
TestSparseBitmap::set(index2);
// Test after second set
assert_eq!(MockStorageMap::get(bucket), mask1 | mask2); // Bucket should contain both masks
assert!(TestSparseBitmap::get(index1));
assert!(TestSparseBitmap::get(index2));
})
}
#[test]
fn test_sparse_bitmap_different_buckets() {
TestExternalities::default().execute_with(|| {
let index1 = 300u64; // Bucket 1
let index2 = 300u64 + (1 << 7); // Bucket 2 (128 bits apart)
let (bucket1, _) = TestSparseBitmap::compute_bucket_and_mask(index1);
let (bucket2, _) = TestSparseBitmap::compute_bucket_and_mask(index2);
let (_, mask1) = TestSparseBitmap::compute_bucket_and_mask(index1);
let (_, mask2) = TestSparseBitmap::compute_bucket_and_mask(index2);
// Test initial state
assert_eq!(MockStorageMap::get(bucket1), 0);
assert_eq!(MockStorageMap::get(bucket2), 0);
// Set bits in different buckets
TestSparseBitmap::set(index1);
TestSparseBitmap::set(index2);
// Test after setting
assert_eq!(MockStorageMap::get(bucket1), mask1); // Bucket 1 should contain mask1
assert_eq!(MockStorageMap::get(bucket2), mask2); // Bucket 2 should contain mask2
assert!(TestSparseBitmap::get(index1));
assert!(TestSparseBitmap::get(index2));
})
}
#[test]
fn test_sparse_bitmap_wide_range() {
TestExternalities::default().execute_with(|| {
// Test wide range of values across u64 spectrum
let test_indices = [
0u64, // Smallest possible value
1u64, // Early value
127u64, // Last value in first bucket
128u64, // First value in second bucket
255u64, // End of second bucket
1000u64, // Medium-small value
123456u64, // Medium value
(1u64 << 32) - 1, // Max u32 value
1u64 << 32, // First value after max u32
(1u64 << 32) + 1, // Just after u32 max
(1u64 << 40) - 1, // Large value near a power of 2
(1u64 << 40), // Power of 2 value
(1u64 << 40) + 1, // Just after power of 2
u64::MAX / 2, // Middle of u64 range
u64::MAX - 128, // Near the end
u64::MAX - 1, // Second-to-last possible value
u64::MAX, // Largest possible value
];
// Verify each bit can be set and read correctly
for &index in &test_indices {
// Verify initial state - bit should be unset
assert!(!TestSparseBitmap::get(index), "Index {} should initially be unset", index);
// Set the bit
TestSparseBitmap::set(index);
// Verify bit was set
assert!(
TestSparseBitmap::get(index),
"Index {} should be set after setting",
index
);
// Calculate bucket and mask for verification
let (bucket, mask) = TestSparseBitmap::compute_bucket_and_mask(index);
// Verify the storage contains the bit
let value = MockStorageMap::get(bucket);
assert!(value & mask != 0, "Storage for index {} should have bit set", index);
}
// Verify all set bits can still be read correctly
for &index in &test_indices {
assert!(TestSparseBitmap::get(index), "Index {} should still be set", index);
}
})
}
#[test]
fn test_sparse_bitmap_bucket_boundaries() {
TestExternalities::default().execute_with(|| {
// Test adjacent indices on bucket boundaries
let boundary_pairs = [
(127u64, 128u64), // End of bucket 0, start of bucket 1
(255u64, 256u64), // End of bucket 1, start of bucket 2
(1023u64, 1024u64), // End of bucket 7, start of bucket 8
];
for (i1, i2) in boundary_pairs {
// Calculate buckets - should be different
let (b1, m1) = TestSparseBitmap::compute_bucket_and_mask(i1);
let (b2, m2) = TestSparseBitmap::compute_bucket_and_mask(i2);
// Ensure they're in different buckets
assert_ne!(b1, b2, "Indices {} and {} should be in different buckets", i1, i2);
// Set both bits
TestSparseBitmap::set(i1);
TestSparseBitmap::set(i2);
// Verify both are set
assert!(TestSparseBitmap::get(i1), "Boundary index {} should be set", i1);
assert!(TestSparseBitmap::get(i2), "Boundary index {} should be set", i2);
// Verify storage contains correct masks
let stored_b1_value = MockStorageMap::get(b1);
let stored_b2_value = MockStorageMap::get(b2);
// Just verify the bits are set in the masks (not checking exact mask values)
assert_ne!(stored_b1_value, 0, "Storage for bucket {} should not be 0", b1);
assert_ne!(stored_b2_value, 0, "Storage for bucket {} should not be 0", b2);
assert!(
stored_b1_value & m1 != 0,
"Bit for index {} should be set in bucket {}",
i1,
b1
);
assert!(
stored_b2_value & m2 != 0,
"Bit for index {} should be set in bucket {}",
i2,
b2
);
}
})
}
#[test]
fn test_sparse_bitmap_large_buckets() {
TestExternalities::default().execute_with(|| {
// Test indices that produce large bucket numbers (near u64::MAX)
let large_indices = [u64::MAX - 1, u64::MAX];
for &index in &large_indices {
let (bucket, mask) = TestSparseBitmap::compute_bucket_and_mask(index);
// Verify bucket calculation is as expected
assert_eq!(
bucket,
u64::from(index) >> 7,
"Bucket calculation incorrect for {}",
index
);
// Set and verify the bit
TestSparseBitmap::set(index);
assert!(TestSparseBitmap::get(index), "Large index {} should be set", index);
// Verify the bit is set in storage
let stored_value = MockStorageMap::get(bucket);
assert_ne!(stored_value, 0, "Storage for bucket {} should not be 0", bucket);
assert!(
stored_value & mask != 0,
"Bit for index {} should be set in bucket {}",
index,
bucket
);
}
})
}
}
@@ -0,0 +1,13 @@
use crate::{ChannelId, ParaId};
use hex_literal::hex;
const EXPECT_CHANNEL_ID: [u8; 32] =
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539");
// The Solidity equivalent code is tested in Gateway.t.sol:testDeriveChannelID
#[test]
fn generate_channel_id() {
let para_id: ParaId = 1000.into();
let channel_id: ChannelId = para_id.into();
assert_eq!(channel_id, EXPECT_CHANNEL_ID.into());
}