diff --git a/pallets/acurast-xcm-receiver/Cargo.toml b/pallets/acurast-xcm-receiver/Cargo.toml new file mode 100644 index 00000000..28d5ea9d --- /dev/null +++ b/pallets/acurast-xcm-receiver/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "pallet-acurast-xcm-receiver" +authors = ["Papers AG"] +description = "Pallet for defining the receiving logic from acurast parachain." +version = "0.0.1" +license = "Unlicense" +homepage = "https://docs.acurast.com/" +edition = "2021" +publish = false +repository = "https://github.com/acurast/" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } + +# Benchmarks +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.29", optional = true, default-features = false } + +# Substrate +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.29", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.29", default-features = false } +scale-info = { version = "2.0", default-features = false, features = [ "derive" ] } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.29", default-features = false } + +[dev-dependencies] +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.29", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.29", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.29", default-features = false } +# XCM +xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.29" } +xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.29" } +pallet-xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.29" } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "frame-benchmarking/std", +] + +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/acurast-xcm-receiver/README.md b/pallets/acurast-xcm-receiver/README.md new file mode 100644 index 00000000..3fe6413d --- /dev/null +++ b/pallets/acurast-xcm-receiver/README.md @@ -0,0 +1,86 @@ +# Acurast XCM Receiver Pallet + +## 🚧🚧🚧 The project is still a work in progress 🚧🚧🚧 + +## Introduction + +The `pallet-acurast-xcm-receiver` adds support for parachains to receive [XCM](https://wiki.polkadot.network/docs/learn-xcm) messages from [Acurast parachain](https://docs.acurast.com/). + +The Pallet exposes the following extrinsics. + +### fulfill + +Allows to post the fulfillment of a registered job. The `fulfill` call will fail if the job was not previously assigned to the origin. The fulfillment structure consists of: + +## Setup + +1. Add the following dependency to your Cargo manifest: + +```toml +[dependencies] +pallet-acurast-xcm-receiver = { git = "https://github.com/Acurast/acurast-core.git" } +``` + +2. Implement `pallet_acurast_xcm_receiver::Config` for your `Runtime` and add the Pallet: + +```rust +/// Runtime example + +pub struct ParachainBarrier; +impl pallet_acurast_xcm_receiver::traits::ParachainBarrier for ParachainBarrier { + fn ensure_xcm_origin( + origin: frame_system::pallet_prelude::OriginFor, + ) -> Result<(), sp_runtime::DispatchError> { + // List of allowd parachains + let allowed_parachains = [ + // The Acurast parachain identifier + xcm::opaque::latest::Junction::Parachain(2001), + ]; + + // Ensure that the call comes from an xcm message + let location = pallet_xcm::ensure_xcm(origin)?; + + let is_valid_origin = location + .interior() + .iter() + .any(|junction| allowed_parachains.contains(junction)); + + if !is_valid_origin { + return Err(sp_runtime::DispatchError::Other( + "MultiLocation not allowed.", + )); + } + + Ok(()) + } +} + +pub struct OnAcurastFulfillment; +impl pallet_acurast_xcm_receiver::traits::OnFulfillment for OnAcurastFulfillment { + fn fulfill( + payload: &[u8], + ) -> sp_runtime::DispatchResultWithInfo { + // handle payload (e.i. Call a contract) + } +} + +impl pallet_acurast_xcm_receiver::Config for Runtime { + type Event = Event; + type Payload = sp_runtime::bounded::bounded_vec::BoundedVec>; + type OnFulfillment = OnAcurastFulfillment; + type Barrier = ParachainBarrier; +} + +// Add pallet to the runtime +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = opaque::Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + // All your other pallets + ... + AcurastReceiver: pallet_acurast_xcm_receiver::{Pallet, Storage, Call, Event}; + } +); +``` diff --git a/pallets/acurast-xcm-receiver/src/benchmarking.rs b/pallets/acurast-xcm-receiver/src/benchmarking.rs new file mode 100644 index 00000000..49d3283c --- /dev/null +++ b/pallets/acurast-xcm-receiver/src/benchmarking.rs @@ -0,0 +1,12 @@ +//! Benchmarking setup for pallet-template + +// use super::*; + +// #[allow(unused)] +// use frame_benchmarking::{benchmarks, whitelisted_caller}; +// use frame_system::RawOrigin; + +// benchmarks! { +// // TODO: add benchmarks +// impl_benchmark_test_suite!(crate::Pallet, crate::mock::new_test_ext(), crate::mock::Test); +// } diff --git a/pallets/acurast-xcm-receiver/src/lib.rs b/pallets/acurast-xcm-receiver/src/lib.rs new file mode 100644 index 00000000..6b176694 --- /dev/null +++ b/pallets/acurast-xcm-receiver/src/lib.rs @@ -0,0 +1,84 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +pub mod traits; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[frame_support::pallet] +pub mod pallet { + use crate::traits::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::OriginFor; + use sp_std::prelude::*; + + /// Configure the pallet by specifying the parameters and types on which it depends. + #[pallet::config] + pub trait Config: frame_system::Config { + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type Event: From> + IsType<::Event>; + /// The fulfillment payload. + type Payload: Parameter + Member + Clone + Into>; + /// Generic parameters + type Parameters: Parameter + Member + Clone + Into>; + /// Handler to notify the runtime when a new fulfillment is received. + type OnFulfillment: OnFulfillment; + /// Handle origin validation + type Barrier: ParachainBarrier; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + /// TODO: We may want to add a storage for job identifiers, allowing acurast parachain to send + /// the (job identifer + payload). The requester address would be indexed by job identifier. + + // Pallets use events to inform users when important changes are made. + // https://docs.substrate.io/v3/runtime/events-and-errors + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + FulfillReceived(T::Payload, Option), + } + + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error {} + + #[pallet::call] + impl Pallet { + /// Dispatchable function that notifies the runtime about a fulfilment coming from acurast parachain. + #[pallet::weight(Weight::from_ref_time(10_000).saturating_add(T::DbWeight::get().writes(1)))] + pub fn fulfill( + origin: OriginFor, + payload: T::Payload, + parameters: Option, + ) -> DispatchResult { + // Check that the extrinsic comes from a trusted xcm channel. + T::Barrier::ensure_xcm_origin(origin)?; + + // Notify the runtime about the fulfillment. + match T::OnFulfillment::fulfill( + payload.clone().into(), + parameters.clone().map(|parameters| parameters.into()), + ) { + Err(err) => Err(err.error), + Ok(_) => { + // Emit events + Self::deposit_event(Event::FulfillReceived(payload, parameters)); + + Ok(()) + } + } + } + } +} diff --git a/pallets/acurast-xcm-receiver/src/mock.rs b/pallets/acurast-xcm-receiver/src/mock.rs new file mode 100644 index 00000000..f24e11c3 --- /dev/null +++ b/pallets/acurast-xcm-receiver/src/mock.rs @@ -0,0 +1,141 @@ +use crate as pallet_acurast_xcm_receiver; +use frame_support::{ + traits::{ConstU16, ConstU64, Everything, Nothing}, + weights::{Pays, PostDispatchInfo}, +}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; +use sp_std::prelude::*; +use xcm::v2::Junction::Parachain; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + AcurastReceiver: pallet_acurast_xcm_receiver::{Pallet, Call, Storage, Event}, + PolkadotXcm: pallet_xcm::{Pallet, Storage, Call, Event, Origin, Config}, + } +); + +frame_support::parameter_types! { + /// The amount of weight an XCM operation takes. This is safe overestimate. + pub UnitWeightCost: xcm::v2::Weight = 200_000_000; + /// Maximum number of instructions in a single XCM fragment. A sanity check against + /// weight caculations getting too crazy. + pub MaxInstructions: u32 = 100; + // The ancestry, defines the multilocation describing this consensus system + pub Ancestry: xcm::v2::MultiLocation = Parachain(2001).into(); +} + +impl pallet_xcm::Config for Test { + type Event = Event; + type SendXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmRouter = (); + type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmExecuteFilter = Nothing; + type XcmExecutor = (); + type XcmTeleportFilter = Nothing; + type XcmReserveTransferFilter = Everything; + type Weigher = xcm_builder::FixedWeightBounds; + type LocationInverter = xcm_builder::LocationInverter; + type Origin = Origin; + type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; +} + +impl system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = ConstU16<42>; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +pub struct OnAcurastFulfillment; +impl crate::traits::OnFulfillment for OnAcurastFulfillment { + fn fulfill( + _payload: Vec, + _parameters: Option>, + ) -> frame_support::sp_runtime::DispatchResultWithInfo { + Ok(PostDispatchInfo { + actual_weight: None, + pays_fee: Pays::No, + }) + } +} + +pub struct ParachainBarrier; +impl crate::traits::ParachainBarrier for ParachainBarrier { + fn ensure_xcm_origin( + origin: frame_system::pallet_prelude::OriginFor, + ) -> Result<(), sp_runtime::DispatchError> { + // List of allowd parachains + let allowed_parachains = [ + // Acurast + xcm::opaque::latest::Junction::Parachain(2001), + ]; + + // Ensure that the call comes from an xcm message + let location = pallet_xcm::ensure_xcm(origin)?; + + let is_valid_origin = location + .interior() + .iter() + .any(|junction| allowed_parachains.contains(junction)); + + if !is_valid_origin { + return Err(sp_runtime::DispatchError::Other( + "MultiLocation not allowed.", + )); + } + + Ok(()) + } +} + +impl pallet_acurast_xcm_receiver::Config for Test { + type Event = Event; + type Payload = Vec; + type Parameters = Vec; + type OnFulfillment = OnAcurastFulfillment; + type Barrier = ParachainBarrier; +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + system::GenesisConfig::default() + .build_storage::() + .unwrap() + .into() +} diff --git a/pallets/acurast-xcm-receiver/src/tests.rs b/pallets/acurast-xcm-receiver/src/tests.rs new file mode 100644 index 00000000..8d69d3ba --- /dev/null +++ b/pallets/acurast-xcm-receiver/src/tests.rs @@ -0,0 +1,33 @@ +use crate::mock::*; +use frame_support::{assert_err, assert_ok}; +use xcm::v2::{Junction::Parachain, Junctions::X1, MultiLocation}; + +#[test] +fn test_fulfill() { + let payload = vec![0u8; 128]; + + new_test_ext().execute_with(|| { + // Mock and xcm origin + let xcm_origin = Origin::from(pallet_xcm::Origin::Xcm(MultiLocation { + parents: 1, + interior: X1(Parachain(2001)), + })); + + // Dispatch fulfill extrinsic with valid origin. + assert_ok!(AcurastReceiver::fulfill(xcm_origin, payload.clone(), None)); + }); + + new_test_ext().execute_with(|| { + // Mock and xcm origin + let xcm_origin = Origin::from(pallet_xcm::Origin::Xcm(MultiLocation { + parents: 1, + interior: X1(Parachain(2000)), + })); + + // Dispatch fulfill extrinsic with wrong origin. + assert_err!( + AcurastReceiver::fulfill(xcm_origin, payload.clone(), None), + "MultiLocation not allowed." + ); + }); +} diff --git a/pallets/acurast-xcm-receiver/src/traits.rs b/pallets/acurast-xcm-receiver/src/traits.rs new file mode 100644 index 00000000..8df83386 --- /dev/null +++ b/pallets/acurast-xcm-receiver/src/traits.rs @@ -0,0 +1,20 @@ +use frame_support::{ + sp_runtime::{DispatchError, DispatchResultWithInfo}, + weights::PostDispatchInfo, +}; +use frame_system::pallet_prelude::OriginFor; + +use crate::Config; + +/// Handle fulfillment messages. +pub trait OnFulfillment { + fn fulfill( + payload: Vec, + parameters: Option>, + ) -> DispatchResultWithInfo; +} + +/// Allows execution only from trusted origins. +pub trait ParachainBarrier { + fn ensure_xcm_origin(origin: OriginFor) -> Result<(), DispatchError>; +}