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:
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user