feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
[package]
|
||||
name = "snowbridge-pallet-inbound-queue-v2"
|
||||
description = "Snowbridge Inbound Queue Pallet V2"
|
||||
version = "0.2.0"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
license = "Apache-2.0"
|
||||
categories = ["cryptography::cryptocurrencies"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[package.metadata.pezkuwi-sdk]
|
||||
exclude-from-umbrella = true
|
||||
|
||||
[dependencies]
|
||||
alloy-core = { workspace = true, features = ["sol-types"] }
|
||||
codec = { features = ["derive"], workspace = true }
|
||||
scale-info = { features = ["derive"], workspace = true }
|
||||
serde = { optional = true, workspace = true, default-features = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
frame-benchmarking = { optional = true, workspace = true }
|
||||
frame-support = { workspace = true }
|
||||
frame-system = { workspace = true }
|
||||
pallet-balances = { workspace = true }
|
||||
sp-core = { workspace = true }
|
||||
sp-io = { workspace = true }
|
||||
sp-runtime = { workspace = true }
|
||||
sp-std = { workspace = true }
|
||||
|
||||
xcm = { workspace = true }
|
||||
xcm-builder = { workspace = true }
|
||||
xcm-executor = { workspace = true }
|
||||
|
||||
snowbridge-beacon-primitives = { workspace = true }
|
||||
snowbridge-core = { workspace = true }
|
||||
snowbridge-inbound-queue-primitives = { workspace = true }
|
||||
snowbridge-pallet-inbound-queue-v2-fixtures = { optional = true, workspace = true }
|
||||
|
||||
bp-relayers = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
frame-benchmarking = { workspace = true, default-features = true }
|
||||
hex-literal = { workspace = true, default-features = true }
|
||||
snowbridge-test-utils = { workspace = true }
|
||||
sp-keyring = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"alloy-core/std",
|
||||
"bp-relayers/std",
|
||||
"codec/std",
|
||||
"frame-benchmarking/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"pallet-balances/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"snowbridge-beacon-primitives/std",
|
||||
"snowbridge-core/std",
|
||||
"snowbridge-inbound-queue-primitives/std",
|
||||
"snowbridge-pallet-inbound-queue-v2-fixtures?/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"tracing/std",
|
||||
"xcm-builder/std",
|
||||
"xcm-executor/std",
|
||||
"xcm/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"bp-relayers/runtime-benchmarks",
|
||||
"frame-benchmarking",
|
||||
"frame-benchmarking/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"pallet-balances/runtime-benchmarks",
|
||||
"snowbridge-beacon-primitives/runtime-benchmarks",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"snowbridge-inbound-queue-primitives/runtime-benchmarks",
|
||||
"snowbridge-pallet-inbound-queue-v2-fixtures/runtime-benchmarks",
|
||||
"snowbridge-test-utils/runtime-benchmarks",
|
||||
"sp-io/runtime-benchmarks",
|
||||
"sp-keyring/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
"xcm-executor/runtime-benchmarks",
|
||||
"xcm/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"frame-support/try-runtime",
|
||||
"frame-system/try-runtime",
|
||||
"pallet-balances/try-runtime",
|
||||
"snowbridge-inbound-queue-primitives/try-runtime",
|
||||
"sp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,73 @@
|
||||
# Ethereum Inbound Queue V2
|
||||
|
||||
Reads messages from Ethereum and sends them to intended destination on PezkuwiChain, using XCM.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Message Flow
|
||||
**1. Ethereum Gateway Event:** A message is first emitted by a GatewayProxy contract on Ethereum in an OutboundMessageAccepted
|
||||
event. This event contains:
|
||||
- A nonce (for replay protection).
|
||||
- Information about the originating address, asset(s), and XCM payload.
|
||||
- Relayer fee and execution fee (both in Ether).
|
||||
This event is emitted when the `v2_registerToken` and `v2_sendMessage` is called on Ethereum.
|
||||
|
||||
**2. Relayer Submits Proof:** A relayer gathers the event proof (containing the Ethereum event log and the proofs required:
|
||||
receipts proof and execution header proof) and calls the `submit` extrinsic of this pallet.
|
||||
|
||||
**3. Verification:** The supplied proof is verified by an on-chain Verifier (configured in the runtime as the EthereumBeaconClient).
|
||||
The verifier checks that the header containing the message is valid. If verification fails, the submission is rejected.
|
||||
|
||||
**4. Message Conversion:** Once verified, the message data is translated into XCM via a MessageConverter implementation.
|
||||
This translation includes extracting payload details, XCM instructions, and bridging asset references.
|
||||
|
||||
**5. XCM Dispatch:** The resulting XCM message is dispatched to the target AssetHub teyrchain for further processing. Depending
|
||||
on the `xcm` provided in the payload, more messages may be sent to teyrchains after AssetHub.
|
||||
|
||||
**6. Relayer Reward:** The relayer is rewarded with Ether (the relayer_fee portion), paid out by the configured RewardPayment
|
||||
handler, which accumulates rewards against a relayer account, which may be claimed.
|
||||
|
||||
### Key Components
|
||||
#### Verifier
|
||||
A trait-based component (snowbridge_inbound_queue_primitives::Verifier) responsible for verifying Ethereum events and proofs.
|
||||
The implementation for the verifier is the Ethereum client.
|
||||
|
||||
#### Message Converter
|
||||
Translates the Ethereum-provided message data (Message) into XCM instructions. The default implementation uses logic in MessageToXcm.
|
||||
|
||||
#### Reward Payment
|
||||
Handles paying out Ether-based rewards to the relayer.
|
||||
|
||||
#### Operating Mode
|
||||
A gating mechanism allowing governance to halt or resume inbound message processing.
|
||||
|
||||
### Extrinsics
|
||||
|
||||
The pallet provides the following public extrinsics:
|
||||
|
||||
**1. Message Submission: `submit`**
|
||||
|
||||
Primary extrinsic for inbound messages. Relayers call this with a proof of the Gateway event from Ethereum. The process
|
||||
is described in [message-flow](#message-flow).
|
||||
|
||||
```
|
||||
pub fn submit(
|
||||
origin: OriginFor<T>,
|
||||
event: Box<EventProof>,
|
||||
) -> DispatchResult
|
||||
```
|
||||
|
||||
**2. Governance: `set_operating_mode`**
|
||||
|
||||
Allows governance (Root origin) to set the operating mode of the pallet. This can be used to:
|
||||
|
||||
- Halt all incoming message processing (Halted state).
|
||||
- Resume normal operation or set other custom states.
|
||||
|
||||
```
|
||||
pub fn set_operating_mode(
|
||||
origin: OriginFor<T>,
|
||||
mode: BasicOperatingMode,
|
||||
) -> DispatchResult
|
||||
```
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "snowbridge-pallet-inbound-queue-v2-fixtures"
|
||||
description = "Snowbridge Inbound Queue Test Fixtures V2"
|
||||
version = "0.10.0"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
license = "Apache-2.0"
|
||||
categories = ["cryptography::cryptocurrencies"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[package.metadata.pezkuwi-sdk]
|
||||
exclude-from-umbrella = true
|
||||
|
||||
[dependencies]
|
||||
hex-literal = { workspace = true, default-features = true }
|
||||
snowbridge-beacon-primitives = { workspace = true }
|
||||
snowbridge-core = { workspace = true }
|
||||
snowbridge-inbound-queue-primitives = { workspace = true }
|
||||
sp-core = { workspace = true }
|
||||
sp-std = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"snowbridge-beacon-primitives/std",
|
||||
"snowbridge-core/std",
|
||||
"snowbridge-inbound-queue-primitives/std",
|
||||
"sp-core/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"snowbridge-beacon-primitives/runtime-benchmarks",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"snowbridge-inbound-queue-primitives/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod register_token;
|
||||
@@ -0,0 +1,93 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
// Generated, do not edit!
|
||||
// See ethereum client README.md for instructions to generate
|
||||
|
||||
use hex_literal::hex;
|
||||
use snowbridge_beacon_primitives::{
|
||||
types::deneb, AncestryProof, BeaconHeader, ExecutionProof, VersionedExecutionPayloadHeader,
|
||||
};
|
||||
use snowbridge_inbound_queue_primitives::{EventFixture, EventProof, Log, Proof};
|
||||
use sp_core::U256;
|
||||
use sp_std::vec;
|
||||
|
||||
pub fn make_register_token_message() -> EventFixture {
|
||||
EventFixture {
|
||||
event: EventProof {
|
||||
event_log: Log {
|
||||
address: hex!("b1185ede04202fe62d38f5db72f71e38ff3e8305").into(),
|
||||
topics: vec![
|
||||
hex!("550e2067494b1736ea5573f2d19cdc0ac95b410fff161bf16f11c6229655ec9c").into(),
|
||||
],
|
||||
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b1185ede04202fe62d38f5db72f71e38ff3e830500000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b8ea8cb425d85536b158d661da1ef0895bb92f1d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").into(),
|
||||
},
|
||||
proof: Proof {
|
||||
receipt_proof: (vec![
|
||||
hex!("eeff6f8a38ee0f085b3aaaecd1b1791393dba27677b7d833a619560d34848349").to_vec(),
|
||||
], vec![
|
||||
hex!("f90352822080b9034c02f903480183016e84b9010000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000001000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f9023df9023a94b1185ede04202fe62d38f5db72f71e38ff3e8305e1a0550e2067494b1736ea5573f2d19cdc0ac95b410fff161bf16f11c6229655ec9cb9020000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b1185ede04202fe62d38f5db72f71e38ff3e830500000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b8ea8cb425d85536b158d661da1ef0895bb92f1d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").to_vec(),
|
||||
]),
|
||||
execution_proof: ExecutionProof {
|
||||
header: BeaconHeader {
|
||||
slot: 275,
|
||||
proposer_index: 2,
|
||||
parent_root: hex!("e21765cf0d02d7b4a0a5805b932783f47374c063e2162aded537bb7b4dedbad4").into(),
|
||||
state_root: hex!("f07afd6c90d2f5fd119dc4bdfacc82fa71bdb31ab4459ae095b5988d94127fea").into(),
|
||||
body_root: hex!("67dc49443764050c2e3bbb207002dcce7b3b2d6947ed7e76f0529cd5d1d5d9af").into(),
|
||||
},
|
||||
ancestry_proof: Some(AncestryProof {
|
||||
header_branch: vec![
|
||||
hex!("e21765cf0d02d7b4a0a5805b932783f47374c063e2162aded537bb7b4dedbad4").into(),
|
||||
hex!("61a8478a4d86e66601fbe69b23940988918150960b539ad41ca020126d8edc92").into(),
|
||||
hex!("8957944d5ac1f347623050629a5229caa586b2e2e6a9c628f39bbb318c1a5710").into(),
|
||||
hex!("0452de7770e3461c061a4dea2a3168ad794e35e42820ef21783016a694e74c04").into(),
|
||||
hex!("845629476fdd6c1ef5398695e2604885cd996d82ce809d5a906e6bdc8c8ac075").into(),
|
||||
hex!("fc6fd46beeae6416f6ddcbee852cae193e9bc0876c4f2df59ba77ce7e46fbc62").into(),
|
||||
hex!("426e10693355dd53c2fd9baea9e886e2debaea353f795d0088908ef3b23e92e9").into(),
|
||||
hex!("6cc1c3729a81840282cb9f9f4c6114e9da9ff0b914fb2cff09772115f4b78d8f").into(),
|
||||
hex!("2392c4845e4144d1be02edce87c084fd91f8ddc7df00d7beb8e93b963ff3991b").into(),
|
||||
hex!("5ff84fb92accc3a9f576d14988d2156d71a5d9383adfa270bb5513c11212aa3a").into(),
|
||||
hex!("4b4252f5ba4a5a5237e758db965edf95be92f7173220f9f1ed8e6610c6552840").into(),
|
||||
hex!("40129fbdb90ff15790fc19632c89498daa33cb1b1959cee5e7a39889aff07faa").into(),
|
||||
hex!("037f20b36fba940608d0b159f45f8c1020d8d2573ec3137ae99a1fad2b6731e6").into(),
|
||||
],
|
||||
finalized_block_root: hex!("6433981df0a293cd29ef4f19a4e8cf032f398d3ae6d8d6f6fd92cdb9573d4e21").into(),
|
||||
}),
|
||||
execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader {
|
||||
parent_hash: hex!("de1c258e68f7cea3f46ac0827503859b1ba3457b96298cd41856ff116a72fb34").into(),
|
||||
fee_recipient: hex!("0000000000000000000000000000000000000000").into(),
|
||||
state_root: hex!("d7efbbefc22e7ff966d565361e4c66f661a93f38962b273657690c14f848609d").into(),
|
||||
receipts_root: hex!("eeff6f8a38ee0f085b3aaaecd1b1791393dba27677b7d833a619560d34848349").into(),
|
||||
logs_bloom: hex!("00000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000001000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").into(),
|
||||
prev_randao: hex!("83d6cda8782ae5d525633a621fe6969a9478a3b631f703821f8e54f9784e25e5").into(),
|
||||
block_number: 275,
|
||||
gas_limit: 61151018,
|
||||
gas_used: 93828,
|
||||
timestamp: 1741254134,
|
||||
extra_data: hex!("d983010e0c846765746888676f312e32322e358664617277696e").into(),
|
||||
base_fee_per_gas: U256::from(7u64),
|
||||
block_hash: hex!("9f13c5ebf7b00bc30fc72a0be1ae1b359f8f8fce2a68dbeb6b4c661b676917c0").into(),
|
||||
transactions_root: hex!("4ed8e0ac8f160d64b9c7029970b5285b1235ae2e0ecea6e68d87b44a20cc7ff1").into(),
|
||||
withdrawals_root: hex!("792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535").into(),
|
||||
blob_gas_used: 0,
|
||||
excess_blob_gas: 0,
|
||||
}),
|
||||
execution_branch: vec![
|
||||
hex!("904fa220e45619c2b931035be7701dd7b975ca4f4a64e9083a8b8f8534aec236").into(),
|
||||
hex!("b46f0c01805fe212e15907981b757e6c496b0cb06664224655613dcec82505bb").into(),
|
||||
hex!("db56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71").into(),
|
||||
hex!("0fcc49cc9ab2f5802bf2ce2293432a24d83636b94c48d80eefb98f92ef166fa2").into(),
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
finalized_header: BeaconHeader {
|
||||
slot: 7840,
|
||||
proposer_index: 5,
|
||||
parent_root: hex!("13d7edddc85f905cc4024114f36542531112207a69fe0d4633a406b4c25255e1").into(),
|
||||
state_root: hex!("2e6d8e14a1e351e03a55fad556d166a009e816a8870c77aeec5adef21631e5e8").into(),
|
||||
body_root: hex!("33213addd3c5198cff62976e6ae2cab54f699169df3505a175a621d31b6b079d").into(),
|
||||
},
|
||||
block_roots_root: hex!("4e457522f4a5ea3d544185f9dc34527008a784b5a748e5ae14f0822b00e255e0").into(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
use crate::Pallet as InboundQueue;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_support::assert_ok;
|
||||
use frame_system::RawOrigin;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn submit() -> Result<(), BenchmarkError> {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
let create_message = T::Helper::initialize_storage();
|
||||
|
||||
#[block]
|
||||
{
|
||||
assert_ok!(InboundQueue::<T>::submit(
|
||||
RawOrigin::Signed(caller.clone()).into(),
|
||||
Box::new(create_message.event),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(InboundQueue, crate::mock::new_tester(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Inbound Queue
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! Receives messages emitted by the Gateway contract on Ethereum, whereupon they are verified,
|
||||
//! translated to XCM, and finally sent to AssetHub for further processing.
|
||||
//!
|
||||
//! Message relayers are rewarded in wrapped Ether that is included within the message. This
|
||||
//! wrapped Ether is derived from Ether that the message origin has locked up on Ethereum.
|
||||
//!
|
||||
//! # Extrinsics
|
||||
//!
|
||||
//! ## Governance
|
||||
//!
|
||||
//! * [`Call::set_operating_mode`]: Set the operating mode of the pallet. Can be used to disable
|
||||
//! processing of inbound messages.
|
||||
//!
|
||||
//! ## Message Submission
|
||||
//!
|
||||
//! * [`Call::submit`]: Submit a message for verification and dispatch to the final destination
|
||||
//! teyrchain.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
pub mod weights;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
pub use crate::weights::WeightInfo;
|
||||
use bp_relayers::RewardLedger;
|
||||
use frame_system::ensure_signed;
|
||||
use snowbridge_core::{
|
||||
reward::{AddTip, AddTipError},
|
||||
sparse_bitmap::{SparseBitmap, SparseBitmapImpl},
|
||||
BasicOperatingMode,
|
||||
};
|
||||
use snowbridge_inbound_queue_primitives::{
|
||||
v2::{ConvertMessage, ConvertMessageError, Message},
|
||||
EventProof, VerificationError, Verifier,
|
||||
};
|
||||
use sp_core::H160;
|
||||
use sp_runtime::traits::TryConvert;
|
||||
use sp_std::prelude::*;
|
||||
use xcm::prelude::{ExecuteXcm, Junction::*, Location, SendXcm, *};
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
pub const LOG_TARGET: &str = "snowbridge-pallet-inbound-queue-v2";
|
||||
|
||||
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
|
||||
|
||||
pub type Nonce<T> = SparseBitmapImpl<crate::NonceBitmap<T>>;
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
use snowbridge_inbound_queue_primitives::EventFixture;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub trait BenchmarkHelper<T> {
|
||||
fn initialize_storage() -> EventFixture;
|
||||
}
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
/// The verifier for inbound messages from Ethereum.
|
||||
type Verifier: Verifier;
|
||||
/// XCM message sender.
|
||||
type XcmSender: SendXcm;
|
||||
/// Handler for XCM fees.
|
||||
type XcmExecutor: ExecuteXcm<Self::RuntimeCall>;
|
||||
/// Address of the Gateway contract.
|
||||
#[pallet::constant]
|
||||
type GatewayAddress: Get<H160>;
|
||||
/// AssetHub teyrchain ID.
|
||||
type AssetHubParaId: Get<u32>;
|
||||
/// Convert a command from Ethereum to an XCM message.
|
||||
type MessageConverter: ConvertMessage;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper: BenchmarkHelper<Self>;
|
||||
/// Reward discriminator type.
|
||||
type RewardKind: Parameter + MaxEncodedLen + Send + Sync + Copy + Clone;
|
||||
/// The default RewardKind discriminator for rewards allocated to relayers from this pallet.
|
||||
#[pallet::constant]
|
||||
type DefaultRewardKind: Get<Self::RewardKind>;
|
||||
/// Relayer reward payment.
|
||||
type RewardPayment: RewardLedger<Self::AccountId, Self::RewardKind, u128>;
|
||||
/// AccountId to Location converter
|
||||
type AccountToLocation: for<'a> TryConvert<&'a Self::AccountId, Location>;
|
||||
type WeightInfo: WeightInfo;
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// A message was received from Ethereum
|
||||
MessageReceived {
|
||||
/// The message nonce
|
||||
nonce: u64,
|
||||
/// ID of the XCM message which was forwarded to the final destination teyrchain
|
||||
message_id: [u8; 32],
|
||||
},
|
||||
/// Set OperatingMode
|
||||
OperatingModeChanged { mode: BasicOperatingMode },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// Message came from an invalid outbound channel on the Ethereum side.
|
||||
InvalidGateway,
|
||||
/// Account could not be converted to bytes
|
||||
InvalidAccount,
|
||||
/// Message has an invalid envelope.
|
||||
InvalidMessage,
|
||||
/// Message has an unexpected nonce.
|
||||
InvalidNonce,
|
||||
/// Fee provided is invalid.
|
||||
InvalidFee,
|
||||
/// Message has an invalid payload.
|
||||
InvalidPayload,
|
||||
/// Message channel is invalid
|
||||
InvalidChannel,
|
||||
/// The max nonce for the type has been reached
|
||||
MaxNonceReached,
|
||||
/// Cannot convert location
|
||||
InvalidAccountConversion,
|
||||
/// Invalid network specified
|
||||
InvalidNetwork,
|
||||
/// Pallet is halted
|
||||
Halted,
|
||||
/// The operation required fees to be paid which the initiator could not meet.
|
||||
FeesNotMet,
|
||||
/// The desired destination was unreachable, generally because there is a no way of routing
|
||||
/// to it.
|
||||
Unreachable,
|
||||
/// There was some other issue (i.e. not to do with routing) in sending the message.
|
||||
/// Perhaps a lack of space for buffering the message.
|
||||
SendFailure,
|
||||
/// Invalid foreign ERC-20 token ID
|
||||
InvalidAsset,
|
||||
/// Cannot reachor a foreign ERC-20 asset location.
|
||||
CannotReanchor,
|
||||
/// Message verification error
|
||||
Verification(VerificationError),
|
||||
}
|
||||
|
||||
impl<T: Config> From<SendError> for Error<T> {
|
||||
fn from(e: SendError) -> Self {
|
||||
match e {
|
||||
SendError::Fees => Error::<T>::FeesNotMet,
|
||||
SendError::NotApplicable => Error::<T>::Unreachable,
|
||||
_ => Error::<T>::SendFailure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> From<ConvertMessageError> for Error<T> {
|
||||
fn from(e: ConvertMessageError) -> Self {
|
||||
match e {
|
||||
ConvertMessageError::InvalidAsset => Error::<T>::InvalidAsset,
|
||||
ConvertMessageError::CannotReanchor => Error::<T>::CannotReanchor,
|
||||
ConvertMessageError::InvalidNetwork => Error::<T>::InvalidNetwork,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// StorageMap used for encoding a SparseBitmapImpl that tracks whether a specific nonce has
|
||||
/// been processed or not. Message nonces are unique and never repeated.
|
||||
#[pallet::storage]
|
||||
pub type NonceBitmap<T: Config> = StorageMap<_, Twox64Concat, u64, u128, ValueQuery>;
|
||||
|
||||
/// The current operating mode of the pallet.
|
||||
#[pallet::storage]
|
||||
pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
|
||||
|
||||
/// Keep track of tips added for a message as an additional relayer incentivization. The
|
||||
/// key for the storage map is the nonce of the message to which the tip should be added.
|
||||
/// The value is the tip amount, in Ether.
|
||||
#[pallet::storage]
|
||||
pub type Tips<T: Config> = StorageMap<_, Blake2_128Concat, u64, u128, OptionQuery>;
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Submit an inbound message originating from the Gateway contract on Ethereum
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::submit())]
|
||||
pub fn submit(origin: OriginFor<T>, event: Box<EventProof>) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
ensure!(!OperatingMode::<T>::get().is_halted(), Error::<T>::Halted);
|
||||
|
||||
// submit message for verification
|
||||
T::Verifier::verify(&event.event_log, &event.proof)
|
||||
.map_err(|e| Error::<T>::Verification(e))?;
|
||||
|
||||
// Decode event log into a bridge message
|
||||
let message =
|
||||
Message::try_from(&event.event_log).map_err(|_| Error::<T>::InvalidMessage)?;
|
||||
|
||||
Self::process_message(who, message)
|
||||
}
|
||||
|
||||
/// Halt or resume all pallet operations. May only be called by root.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
|
||||
pub fn set_operating_mode(
|
||||
origin: OriginFor<T>,
|
||||
mode: BasicOperatingMode,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
OperatingMode::<T>::set(mode);
|
||||
Self::deposit_event(Event::OperatingModeChanged { mode });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
pub fn process_message(relayer: T::AccountId, message: Message) -> DispatchResult {
|
||||
// Verify that the message was submitted from the known Gateway contract
|
||||
ensure!(T::GatewayAddress::get() == message.gateway, Error::<T>::InvalidGateway);
|
||||
|
||||
let (nonce, relayer_fee) = (message.nonce, message.relayer_fee);
|
||||
|
||||
// Verify the message has not been processed
|
||||
ensure!(!Nonce::<T>::get(nonce), Error::<T>::InvalidNonce);
|
||||
|
||||
let xcm =
|
||||
T::MessageConverter::convert(message).map_err(|error| Error::<T>::from(error))?;
|
||||
|
||||
// Forward XCM to AH
|
||||
let dest = Location::new(1, [Teyrchain(T::AssetHubParaId::get())]);
|
||||
|
||||
// Mark message as received
|
||||
Nonce::<T>::set(nonce);
|
||||
|
||||
let message_id =
|
||||
Self::send_xcm(dest.clone(), &relayer, xcm.clone()).map_err(|error| {
|
||||
tracing::error!(target: LOG_TARGET, ?error, ?dest, ?xcm, "XCM send failed with error");
|
||||
Error::<T>::from(error)
|
||||
})?;
|
||||
|
||||
// Pay relayer reward
|
||||
let tip = Tips::<T>::take(nonce).unwrap_or_default();
|
||||
let total_tip = relayer_fee.saturating_add(tip);
|
||||
if total_tip > 0 {
|
||||
T::RewardPayment::register_reward(&relayer, T::DefaultRewardKind::get(), total_tip);
|
||||
}
|
||||
|
||||
Self::deposit_event(Event::MessageReceived { nonce, message_id });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_xcm(
|
||||
dest: Location,
|
||||
fee_payer: &T::AccountId,
|
||||
xcm: Xcm<()>,
|
||||
) -> Result<XcmHash, SendError> {
|
||||
let (ticket, fee) = validate_send::<T::XcmSender>(dest, xcm)?;
|
||||
let fee_payer = T::AccountToLocation::try_convert(fee_payer).map_err(|err| {
|
||||
tracing::error!(
|
||||
target: LOG_TARGET,
|
||||
?err,
|
||||
"Failed to convert account to XCM location",
|
||||
);
|
||||
SendError::NotApplicable
|
||||
})?;
|
||||
T::XcmExecutor::charge_fees(fee_payer.clone(), fee.clone()).map_err(|error| {
|
||||
tracing::error!(
|
||||
target: LOG_TARGET,
|
||||
?error,
|
||||
"Charging fees failed with error",
|
||||
);
|
||||
SendError::Fees
|
||||
})?;
|
||||
T::XcmSender::deliver(ticket)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> AddTip for Pallet<T> {
|
||||
fn add_tip(nonce: u64, amount: u128) -> Result<(), AddTipError> {
|
||||
ensure!(amount > 0, AddTipError::AmountZero);
|
||||
// If the nonce is already processed, return an error
|
||||
ensure!(!Nonce::<T>::get(nonce.into()), AddTipError::NonceConsumed);
|
||||
// Otherwise add the tip.
|
||||
Tips::<T>::mutate(nonce, |tip| {
|
||||
*tip = Some(tip.unwrap_or_default().saturating_add(amount));
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
use crate::{self as inbound_queue_v2};
|
||||
use frame_support::{derive_impl, parameter_types, traits::ConstU32};
|
||||
use hex_literal::hex;
|
||||
use snowbridge_beacon_primitives::{
|
||||
types::deneb, BeaconHeader, ExecutionProof, VersionedExecutionPayloadHeader,
|
||||
};
|
||||
use snowbridge_core::{ParaId, TokenId};
|
||||
use snowbridge_inbound_queue_primitives::{
|
||||
v2::{CreateAssetCallInfo, MessageToXcm},
|
||||
Log, Proof, VerificationError,
|
||||
};
|
||||
use sp_core::H160;
|
||||
use sp_runtime::{
|
||||
traits::{IdentityLookup, MaybeConvert},
|
||||
BuildStorage,
|
||||
};
|
||||
use sp_std::{convert::From, default::Default, marker::PhantomData};
|
||||
use xcm::{opaque::latest::ZAGROS_GENESIS_HASH, prelude::*};
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
use snowbridge_test_utils::mock_rewards::{BridgeReward, MockRewardLedger};
|
||||
pub use snowbridge_test_utils::mock_xcm::{MockXcmExecutor, MockXcmSender};
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
use snowbridge_inbound_queue_primitives::EventFixture;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
use snowbridge_pallet_inbound_queue_v2_fixtures::register_token::make_register_token_message;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Storage, Event<T>},
|
||||
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
|
||||
InboundQueue: inbound_queue_v2::{Pallet, Call, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
pub(crate) const ERROR_ADDRESS: [u8; 20] = hex!("0000000000000000000000000000000000000911");
|
||||
|
||||
pub type AccountId = sp_runtime::AccountId32;
|
||||
type Balance = u128;
|
||||
|
||||
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
|
||||
impl frame_system::Config for Test {
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type AccountData = pallet_balances::AccountData<u128>;
|
||||
type Block = Block;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u128 = 1;
|
||||
}
|
||||
|
||||
#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
|
||||
impl pallet_balances::Config for Test {
|
||||
type Balance = Balance;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
// Mock verifier
|
||||
pub struct MockVerifier;
|
||||
|
||||
impl Verifier for MockVerifier {
|
||||
fn verify(log: &Log, _: &Proof) -> Result<(), VerificationError> {
|
||||
if log.address == ERROR_ADDRESS.into() {
|
||||
return Err(VerificationError::InvalidProof);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const GATEWAY_ADDRESS: [u8; 20] = hex!["b1185ede04202fe62d38f5db72f71e38ff3e8305"];
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
impl<T: Config> BenchmarkHelper<T> for Test {
|
||||
// not implemented since the MockVerifier is used for tests
|
||||
fn initialize_storage() -> EventFixture {
|
||||
make_register_token_message()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockTokenIdConvert;
|
||||
impl MaybeConvert<TokenId, Location> for MockTokenIdConvert {
|
||||
fn maybe_convert(_id: TokenId) -> Option<Location> {
|
||||
Some(Location::parent())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockAccountLocationConverter<AccountId>(PhantomData<AccountId>);
|
||||
impl<'a, AccountId: Clone + Clone> TryConvert<&'a AccountId, Location>
|
||||
for MockAccountLocationConverter<AccountId>
|
||||
{
|
||||
fn try_convert(_who: &AccountId) -> Result<Location, &AccountId> {
|
||||
Ok(Location::here())
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const EthereumNetwork: NetworkId = Ethereum { chain_id: 11155111 };
|
||||
pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS);
|
||||
pub InboundQueueLocation: InteriorLocation = [PalletInstance(84)].into();
|
||||
pub SnowbridgeReward: BridgeReward = BridgeReward::Snowbridge;
|
||||
pub const CreateAssetCallIndex: [u8;2] = [53, 0];
|
||||
pub const SetReservesCallIndex: [u8;2] = [53, 33];
|
||||
pub const CreateAssetDeposit: u128 = 10_000_000_000u128;
|
||||
pub const LocalNetwork: NetworkId = ByGenesis(ZAGROS_GENESIS_HASH);
|
||||
pub CreateAssetCall: CreateAssetCallInfo = CreateAssetCallInfo {
|
||||
create_call: CreateAssetCallIndex::get(),
|
||||
deposit: CreateAssetDeposit::get(),
|
||||
min_balance: 1,
|
||||
set_reserves_call: SetReservesCallIndex::get(),
|
||||
};
|
||||
pub AssetHubParaId: ParaId = ParaId::from(1000);
|
||||
}
|
||||
|
||||
impl inbound_queue_v2::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Verifier = MockVerifier;
|
||||
type XcmSender = MockXcmSender;
|
||||
type XcmExecutor = MockXcmExecutor;
|
||||
type GatewayAddress = GatewayAddress;
|
||||
type AssetHubParaId = ConstU32<1000>;
|
||||
type MessageConverter = MessageToXcm<
|
||||
CreateAssetCall,
|
||||
EthereumNetwork,
|
||||
LocalNetwork,
|
||||
GatewayAddress,
|
||||
InboundQueueLocation,
|
||||
AssetHubParaId,
|
||||
MockTokenIdConvert,
|
||||
AccountId,
|
||||
>;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper = Test;
|
||||
type WeightInfo = ();
|
||||
type AccountToLocation = MockAccountLocationConverter<AccountId>;
|
||||
type RewardKind = BridgeReward;
|
||||
type DefaultRewardKind = SnowbridgeReward;
|
||||
type RewardPayment = MockRewardLedger;
|
||||
}
|
||||
|
||||
pub fn setup() {
|
||||
System::set_block_number(1);
|
||||
}
|
||||
|
||||
pub fn new_tester() -> sp_io::TestExternalities {
|
||||
let storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
let mut ext: sp_io::TestExternalities = storage.into();
|
||||
ext.execute_with(setup);
|
||||
ext
|
||||
}
|
||||
|
||||
// Generated from smoketests:
|
||||
// cd smoketests
|
||||
// ./make-bindings
|
||||
// cargo test --test register_token -- --nocapture
|
||||
pub fn mock_event_log() -> Log {
|
||||
Log {
|
||||
// gateway address
|
||||
address: hex!("b1185ede04202fe62d38f5db72f71e38ff3e8305").into(),
|
||||
topics: vec![
|
||||
hex!("550e2067494b1736ea5573f2d19cdc0ac95b410fff161bf16f11c6229655ec9c").into(),
|
||||
],
|
||||
// Nonce + Payload
|
||||
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b1185ede04202fe62d38f5db72f71e38ff3e830500000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b8ea8cb425d85536b158d661da1ef0895bb92f1d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_event_log_invalid_gateway() -> Log {
|
||||
Log {
|
||||
// gateway address
|
||||
address: H160::zero(),
|
||||
topics: vec![
|
||||
hex!("550e2067494b1736ea5573f2d19cdc0ac95b410fff161bf16f11c6229655ec9c").into(),
|
||||
],
|
||||
// Nonce + Payload
|
||||
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b1185ede04202fe62d38f5db72f71e38ff3e830500000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b8ea8cb425d85536b158d661da1ef0895bb92f1d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_event_log_invalid_message() -> Log {
|
||||
Log {
|
||||
// gateway address
|
||||
address: hex!("b8ea8cb425d85536b158d661da1ef0895bb92f1d").into(),
|
||||
topics: vec![
|
||||
hex!("b61699d45635baed7500944331ea827538a50dbfef79180f2079e9185da627aa").into(),
|
||||
],
|
||||
// Nonce + Payload
|
||||
data: hex!("000000000000000000000000000000000000000000000000000000b8ea8cb425d85536b158d661da1ef0895bb92f1d000000000000000000000000000000000000000000000000001dcd6500000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000059682f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cdeadbeef774667629726ec1fabebcec0d9139bd1c8f72a23deadbeef0000000000000000000000001dcd650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_execution_proof() -> ExecutionProof {
|
||||
ExecutionProof {
|
||||
header: BeaconHeader::default(),
|
||||
ancestry_proof: None,
|
||||
execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader {
|
||||
parent_hash: Default::default(),
|
||||
fee_recipient: Default::default(),
|
||||
state_root: Default::default(),
|
||||
receipts_root: Default::default(),
|
||||
logs_bloom: vec![],
|
||||
prev_randao: Default::default(),
|
||||
block_number: 0,
|
||||
gas_limit: 0,
|
||||
gas_used: 0,
|
||||
timestamp: 0,
|
||||
extra_data: vec![],
|
||||
base_fee_per_gas: Default::default(),
|
||||
block_hash: Default::default(),
|
||||
transactions_root: Default::default(),
|
||||
withdrawals_root: Default::default(),
|
||||
blob_gas_used: 0,
|
||||
excess_blob_gas: 0,
|
||||
}),
|
||||
execution_branch: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// Generated from smoketests:
|
||||
// cd smoketests
|
||||
// ./make-bindings
|
||||
// cargo test --test register_token_v2 -- --nocapture
|
||||
pub fn mock_event_log_v2() -> Log {
|
||||
Log {
|
||||
// gateway address
|
||||
address: hex!("b1185ede04202fe62d38f5db72f71e38ff3e8305").into(),
|
||||
topics: vec![
|
||||
hex!("550e2067494b1736ea5573f2d19cdc0ac95b410fff161bf16f11c6229655ec9c").into(),
|
||||
],
|
||||
// Nonce + Payload
|
||||
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b1185ede04202fe62d38f5db72f71e38ff3e830500000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b8ea8cb425d85536b158d661da1ef0895bb92f1d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").into(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use super::*;
|
||||
|
||||
use crate::{mock::*, Error};
|
||||
use codec::Encode;
|
||||
use frame_support::{assert_noop, assert_ok};
|
||||
use snowbridge_inbound_queue_primitives::{v2::XcmPayload, EventProof, Proof};
|
||||
use snowbridge_test_utils::{
|
||||
mock_rewards::{RegisteredRewardAmount, RegisteredRewardsCount},
|
||||
mock_xcm::{set_charge_fees_override, set_sender_override},
|
||||
};
|
||||
use sp_keyring::sr25519::Keyring;
|
||||
use sp_runtime::DispatchError;
|
||||
|
||||
#[test]
|
||||
fn test_submit_happy_path() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
|
||||
let origin = RuntimeOrigin::signed(relayer.clone());
|
||||
|
||||
// Submit message
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_ok!(InboundQueue::submit(origin.clone(), Box::new(event.clone())));
|
||||
|
||||
let events = frame_system::Pallet::<Test>::events();
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event.event,
|
||||
RuntimeEvent::InboundQueue(Event::MessageReceived { nonce, ..})
|
||||
if nonce == 1
|
||||
)),
|
||||
"no message received event emitted."
|
||||
);
|
||||
|
||||
assert_eq!(RegisteredRewardsCount::get(), 1, "Relayer reward should have been registered");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_with_invalid_gateway() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = RuntimeOrigin::signed(relayer);
|
||||
|
||||
// Submit message
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log_invalid_gateway(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
assert_noop!(
|
||||
InboundQueue::submit(origin.clone(), Box::new(event.clone())),
|
||||
Error::<Test>::InvalidGateway
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_verification_fails_with_invalid_proof() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
|
||||
let origin = RuntimeOrigin::signed(relayer.clone());
|
||||
|
||||
// Submit message
|
||||
let mut event = EventProof {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
// The mock verifier will error once it matches this address.
|
||||
event.event_log.address = ERROR_ADDRESS.into();
|
||||
|
||||
assert_noop!(
|
||||
InboundQueue::submit(origin.clone(), Box::new(event.clone())),
|
||||
Error::<Test>::Verification(VerificationError::InvalidProof)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_fails_with_malformed_message() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
|
||||
let origin = RuntimeOrigin::signed(relayer.clone());
|
||||
|
||||
// Submit message
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log_invalid_message(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_noop!(
|
||||
InboundQueue::submit(origin.clone(), Box::new(event.clone())),
|
||||
Error::<Test>::InvalidMessage
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_using_same_nonce_fails() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
|
||||
let origin = RuntimeOrigin::signed(relayer.clone());
|
||||
|
||||
// Submit message
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_ok!(InboundQueue::submit(origin.clone(), Box::new(event.clone())));
|
||||
|
||||
let events = frame_system::Pallet::<Test>::events();
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event.event,
|
||||
RuntimeEvent::InboundQueue(Event::MessageReceived { nonce, ..})
|
||||
if nonce == 1
|
||||
)),
|
||||
"no event emitted."
|
||||
);
|
||||
|
||||
assert_noop!(
|
||||
InboundQueue::submit(origin.clone(), Box::new(event.clone())),
|
||||
Error::<Test>::InvalidNonce
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_operating_mode() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = RuntimeOrigin::signed(relayer);
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_ok!(InboundQueue::set_operating_mode(
|
||||
RuntimeOrigin::root(),
|
||||
snowbridge_core::BasicOperatingMode::Halted
|
||||
));
|
||||
|
||||
assert_noop!(InboundQueue::submit(origin, Box::new(event)), Error::<Test>::Halted);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_operating_mode_root_only() {
|
||||
new_tester().execute_with(|| {
|
||||
assert_noop!(
|
||||
InboundQueue::set_operating_mode(
|
||||
RuntimeOrigin::signed(Keyring::Bob.into()),
|
||||
snowbridge_core::BasicOperatingMode::Halted
|
||||
),
|
||||
DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xcm_send_failure() {
|
||||
crate::test::new_tester().execute_with(|| {
|
||||
set_sender_override(
|
||||
|dest: &mut Option<Location>, xcm: &mut Option<Xcm<()>>| {
|
||||
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()))
|
||||
}
|
||||
},
|
||||
|_| Err(SendError::DestinationUnsupported),
|
||||
);
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
|
||||
let origin = mock::RuntimeOrigin::signed(relayer.clone());
|
||||
|
||||
// Submit message
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_noop!(
|
||||
crate::test::InboundQueue::submit(origin.clone(), Box::new(event.clone())),
|
||||
Error::<Test>::SendFailure
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xcm_send_validate_failure() {
|
||||
crate::test::new_tester().execute_with(|| {
|
||||
set_sender_override(
|
||||
|_, _| return Err(SendError::NotApplicable),
|
||||
|xcm| {
|
||||
let hash = xcm.using_encoded(sp_io::hashing::blake2_256);
|
||||
Ok(hash)
|
||||
},
|
||||
);
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
|
||||
let origin = mock::RuntimeOrigin::signed(relayer.clone());
|
||||
|
||||
// Submit message
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_noop!(
|
||||
crate::test::InboundQueue::submit(origin.clone(), Box::new(event.clone())),
|
||||
Error::<Test>::Unreachable
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xcm_charge_fees_failure() {
|
||||
crate::test::new_tester().execute_with(|| {
|
||||
set_charge_fees_override(|_, _| Err(XcmError::FeesNotMet));
|
||||
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
|
||||
let origin = mock::RuntimeOrigin::signed(relayer.clone());
|
||||
|
||||
// Submit message
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_noop!(
|
||||
crate::test::InboundQueue::submit(origin.clone(), Box::new(event.clone())),
|
||||
Error::<Test>::FeesNotMet
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_token() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = RuntimeOrigin::signed(relayer);
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log_v2(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_ok!(InboundQueue::submit(origin, Box::new(event)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_operating_mode() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = RuntimeOrigin::signed(relayer);
|
||||
let event = EventProof {
|
||||
event_log: mock_event_log(),
|
||||
proof: Proof {
|
||||
receipt_proof: Default::default(),
|
||||
execution_proof: mock_execution_proof(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_ok!(InboundQueue::set_operating_mode(
|
||||
RuntimeOrigin::root(),
|
||||
snowbridge_core::BasicOperatingMode::Halted
|
||||
));
|
||||
|
||||
assert_noop!(
|
||||
InboundQueue::submit(origin.clone(), Box::new(event.clone())),
|
||||
Error::<Test>::Halted
|
||||
);
|
||||
|
||||
assert_ok!(InboundQueue::set_operating_mode(
|
||||
RuntimeOrigin::root(),
|
||||
snowbridge_core::BasicOperatingMode::Normal
|
||||
));
|
||||
|
||||
assert_ok!(InboundQueue::submit(origin, Box::new(event)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_reward_does_not_register_reward() {
|
||||
new_tester().execute_with(|| {
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
let origin = H160::random();
|
||||
assert_ok!(InboundQueue::process_message(
|
||||
relayer,
|
||||
Message {
|
||||
nonce: 0,
|
||||
assets: vec![],
|
||||
xcm: XcmPayload::Raw(vec![]),
|
||||
claimer: None,
|
||||
execution_fee: 1_000_000_000,
|
||||
relayer_fee: 0,
|
||||
gateway: GatewayAddress::get(),
|
||||
origin,
|
||||
value: 3_000_000_000,
|
||||
}
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
RegisteredRewardsCount::get(),
|
||||
0,
|
||||
"Zero relayer reward should not be registered"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tip_cumulative() {
|
||||
new_tester().execute_with(|| {
|
||||
let nonce: u64 = 10;
|
||||
let amount1: u128 = 500;
|
||||
let amount2: u128 = 300;
|
||||
|
||||
assert_eq!(Tips::<Test>::get(nonce), None);
|
||||
assert_ok!(InboundQueue::add_tip(nonce, amount1));
|
||||
assert_eq!(Tips::<Test>::get(nonce), Some(amount1));
|
||||
assert_ok!(InboundQueue::add_tip(nonce, amount2));
|
||||
assert_eq!(Tips::<Test>::get(nonce), Some(amount1 + amount2));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tip_nonce_consumed() {
|
||||
new_tester().execute_with(|| {
|
||||
let nonce: u64 = 20;
|
||||
let amount: u128 = 400;
|
||||
Nonce::<Test>::set(nonce.into());
|
||||
|
||||
assert_noop!(InboundQueue::add_tip(nonce, amount), AddTipError::NonceConsumed);
|
||||
assert_eq!(Tips::<Test>::get(nonce), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_tip_amount_zero() {
|
||||
new_tester().execute_with(|| {
|
||||
let nonce: u64 = 30;
|
||||
let amount: u128 = 0;
|
||||
|
||||
assert_noop!(InboundQueue::add_tip(nonce, amount), AddTipError::AmountZero);
|
||||
assert_eq!(Tips::<Test>::get(nonce), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inbound_tip_is_paid_out_to_relayer() {
|
||||
new_tester().execute_with(|| {
|
||||
let nonce: u64 = 77;
|
||||
let tip: u128 = 12_345;
|
||||
let relayer_fee: u128 = 2_000;
|
||||
|
||||
// Add tip for nonce before message is processed
|
||||
assert_ok!(InboundQueue::add_tip(nonce, tip));
|
||||
assert_eq!(Tips::<Test>::get(nonce), Some(tip));
|
||||
|
||||
// Process inbound message with relayer_fee
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
assert_ok!(InboundQueue::process_message(
|
||||
relayer,
|
||||
Message {
|
||||
nonce,
|
||||
assets: vec![],
|
||||
xcm: XcmPayload::Raw(vec![]),
|
||||
claimer: None,
|
||||
execution_fee: 1_000_000_000,
|
||||
relayer_fee,
|
||||
gateway: mock::GatewayAddress::get(),
|
||||
origin: H160::random(),
|
||||
value: 3_000_000_000,
|
||||
},
|
||||
));
|
||||
|
||||
// Reward should be registered from relayer_fee + tip
|
||||
assert_eq!(
|
||||
RegisteredRewardsCount::get(),
|
||||
1,
|
||||
"Reward should be registered from relayer_fee + tip"
|
||||
);
|
||||
|
||||
// Check the actual reward amount paid out (should be relayer_fee + tip)
|
||||
assert_eq!(
|
||||
RegisteredRewardAmount::get(),
|
||||
relayer_fee + tip,
|
||||
"Reward amount should equal relayer_fee + tip"
|
||||
);
|
||||
|
||||
// Tip should be consumed from storage
|
||||
assert_eq!(Tips::<Test>::get(nonce), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relayer_fee_paid_out_when_no_tip_exists() {
|
||||
new_tester().execute_with(|| {
|
||||
let nonce: u64 = 88;
|
||||
let relayer_fee: u128 = 5_000;
|
||||
|
||||
// Ensure no tip exists for this nonce
|
||||
assert_eq!(Tips::<Test>::get(nonce), None);
|
||||
|
||||
// Process inbound message with relayer_fee but no tip
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
assert_ok!(InboundQueue::process_message(
|
||||
relayer,
|
||||
Message {
|
||||
nonce,
|
||||
assets: vec![],
|
||||
xcm: XcmPayload::Raw(vec![]),
|
||||
claimer: None,
|
||||
execution_fee: 1_000_000_000,
|
||||
relayer_fee,
|
||||
gateway: mock::GatewayAddress::get(),
|
||||
origin: H160::random(),
|
||||
value: 3_000_000_000,
|
||||
},
|
||||
));
|
||||
|
||||
// Relayer fee should be paid out even without tip
|
||||
assert_eq!(
|
||||
RegisteredRewardsCount::get(),
|
||||
1,
|
||||
"Relayer fee should be paid out even when no tip exists"
|
||||
);
|
||||
|
||||
// Check the actual reward amount paid out
|
||||
assert_eq!(
|
||||
RegisteredRewardAmount::get(),
|
||||
relayer_fee,
|
||||
"Reward amount should equal relayer_fee when no tip exists"
|
||||
);
|
||||
|
||||
// Confirm no tip storage was affected
|
||||
assert_eq!(Tips::<Test>::get(nonce), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tip_paid_out_when_no_relayer_fee() {
|
||||
new_tester().execute_with(|| {
|
||||
let nonce: u64 = 99;
|
||||
let tip: u128 = 8_500;
|
||||
|
||||
// Add tip for nonce before message is processed
|
||||
assert_ok!(InboundQueue::add_tip(nonce, tip));
|
||||
assert_eq!(Tips::<Test>::get(nonce), Some(tip));
|
||||
|
||||
// Process inbound message with zero relayer_fee but with tip
|
||||
let relayer: AccountId = Keyring::Bob.into();
|
||||
assert_ok!(InboundQueue::process_message(
|
||||
relayer,
|
||||
Message {
|
||||
nonce,
|
||||
assets: vec![],
|
||||
xcm: XcmPayload::Raw(vec![]),
|
||||
claimer: None,
|
||||
execution_fee: 1_000_000_000,
|
||||
relayer_fee: 0,
|
||||
gateway: mock::GatewayAddress::get(),
|
||||
origin: H160::random(),
|
||||
value: 3_000_000_000,
|
||||
},
|
||||
));
|
||||
|
||||
// Tip should be paid out even without relayer fee
|
||||
assert_eq!(
|
||||
RegisteredRewardsCount::get(),
|
||||
1,
|
||||
"Tip should be paid out even when relayer_fee is 0"
|
||||
);
|
||||
|
||||
// Check the actual reward amount paid out (should be just the tip)
|
||||
assert_eq!(
|
||||
RegisteredRewardAmount::get(),
|
||||
tip,
|
||||
"Reward amount should equal tip when relayer_fee is 0"
|
||||
);
|
||||
|
||||
// Tip should be consumed from storage
|
||||
assert_eq!(Tips::<Test>::get(nonce), None);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Autogenerated weights for `snowbridge_inbound_queue`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
|
||||
//! DATE: 2023-07-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `macbook pro 14 m2`, CPU: `m2-arm64`
|
||||
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-pezkuwichain-dev"), DB CACHE: 1024
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for ethereum_beacon_client.
|
||||
pub trait WeightInfo {
|
||||
fn submit() -> Weight;
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
fn submit() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `309`
|
||||
// Estimated: `3774`
|
||||
// Minimum execution time: 59_000_000 picoseconds.
|
||||
Weight::from_parts(60_000_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3774))
|
||||
.saturating_add(RocksDbWeight::get().reads(7))
|
||||
.saturating_add(RocksDbWeight::get().writes(2))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user