From 0699023b9161b529367d400a0dc0a3b7b06ed284 Mon Sep 17 00:00:00 2001 From: muharem Date: Wed, 10 Apr 2024 17:40:19 +0200 Subject: [PATCH 1/6] meta tx pallet gen --- Cargo.lock | 19 ++ Cargo.toml | 1 + substrate/frame/meta-tx/Cargo.toml | 53 +++++ substrate/frame/meta-tx/src/lib.rs | 182 ++++++++++++++++++ substrate/frame/meta-tx/src/mock.rs | 109 +++++++++++ substrate/frame/meta-tx/src/tests.rs | 141 ++++++++++++++ .../primitives/runtime/src/traits/mod.rs | 11 +- 7 files changed, 513 insertions(+), 3 deletions(-) create mode 100644 substrate/frame/meta-tx/Cargo.toml create mode 100644 substrate/frame/meta-tx/src/lib.rs create mode 100644 substrate/frame/meta-tx/src/mock.rs create mode 100644 substrate/frame/meta-tx/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 087d5ad3cec3..b83dd75f55be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10147,6 +10147,25 @@ dependencies = [ "sp-weights", ] +[[package]] +name = "pallet-meta-tx" +version = "0.0.1" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-transaction-payment", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std 14.0.0", +] + [[package]] name = "pallet-migrations" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 5eeac5978270..59f431079bbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -346,6 +346,7 @@ members = [ "substrate/frame/membership", "substrate/frame/merkle-mountain-range", "substrate/frame/message-queue", + "substrate/frame/meta-tx", "substrate/frame/migrations", "substrate/frame/mixnet", "substrate/frame/multisig", diff --git a/substrate/frame/meta-tx/Cargo.toml b/substrate/frame/meta-tx/Cargo.toml new file mode 100644 index 000000000000..3a637087c8a3 --- /dev/null +++ b/substrate/frame/meta-tx/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "pallet-meta-tx" +description = "Dispatch Meta Transaction" +license = "Apache-2.0" +version = "0.0.1" +edition.workspace = true +authors.workspace = true +repository.workspace = true + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["max-encoded-len"] } +scale-info = { version = "2.1.2", default-features = false, features = ["derive"] } +serde = { features = ["derive"], optional = true, workspace = true, default-features = true } + +frame-support = { path = "../support", default-features = false } +frame-system = { path = "../system", default-features = false } +sp-core = { path = "../../primitives/core", default-features = false } +sp-runtime = { path = "../../primitives/runtime", default-features = false } +sp-std = { path = "../../primitives/std", default-features = false } +frame-benchmarking = { path = "../benchmarking", default-features = false, optional = true } + +[dev-dependencies] +pallet-balances = { path = "../balances", features = ["std"] } +sp-io = { path = "../../primitives/io", features = ["std"] } +keyring = { package = "sp-keyring", path = "../../primitives/keyring" } +pallet-transaction-payment = { path = "../../frame/transaction-payment" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "serde", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "pallet-balances/runtime-benchmarks", + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "pallet-balances/try-runtime", + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/substrate/frame/meta-tx/src/lib.rs b/substrate/frame/meta-tx/src/lib.rs new file mode 100644 index 000000000000..f2e1a125d2f1 --- /dev/null +++ b/substrate/frame/meta-tx/src/lib.rs @@ -0,0 +1,182 @@ +// This file is part of Substrate. + +// 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. + +//! Meta Tx +//! +//! TODO docs + +#![cfg_attr(not(feature = "std"), not_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; +pub use pallet::*; + +use frame_support::{ + dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}, + pallet_prelude::*, + traits::OriginTrait, +}; +use frame_system::pallet_prelude::*; +use sp_runtime::traits::TransactionExtension; +use sp_runtime::traits::TransactionExtensionBase; +use sp_runtime::traits::{Dispatchable, IdentifyAccount, Verify}; + +/// Meta transaction type. +// TODO: The `MetaTx` type is similar to `sp_runtime::generic::UncheckedExtrinsic`. However, +// `MetaTx` cannot replace generic::UncheckedExtrinsic because we need to box the call type, +// given that `MetaTx` is used as an argument type for a call. +#[derive(Encode, Decode, PartialEq, Eq, TypeInfo, Clone, RuntimeDebug)] +pub struct MetaTx { + proof: Proof, + call: Box, + tx_ext: TxExtension, +} + +impl MetaTx { + pub fn new_signed( + address: Address, + signature: Signature, + call: Call, + tx_ext: TxExtension, + ) -> Self { + Self { proof: Proof::Signed(address, signature), call: Box::new(call), tx_ext } + } +} + +#[derive(Encode, Decode, PartialEq, Eq, TypeInfo, Clone, RuntimeDebug)] +pub enum Proof { + Signed(Address, Signature), + // TODO `General` as in `sp_runtime::generic::Preamble`. +} + +pub type MetaTxFor = MetaTx< + <::PublicKey as IdentifyAccount>::AccountId, + ::Signature, + ::RuntimeCall, + ::TxExtension, +>; + +pub type SignedPayloadFor = + sp_runtime::generic::SignedPayload<::RuntimeCall, ::TxExtension>; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The overarching call type. + type RuntimeCall: Parameter + + GetDispatchInfo + + Dispatchable< + Info = DispatchInfo, + PostInfo = PostDispatchInfo, + RuntimeOrigin = Self::RuntimeOrigin, + > + IsType<::RuntimeCall>; + /// Signature type for meta transactions. + type Signature: Parameter + Verify; + /// Public key type used for signature verification. + type PublicKey: IdentifyAccount; + /// The context type of `Self::TxExtension` + type TxContext: Member + Default; + /// Transaction extension/s for meta transactions. + type TxExtension: TransactionExtension<::RuntimeCall, Self::TxContext>; + } + + #[pallet::error] + pub enum Error { + /// TODO + TODO, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// A call was dispatched. + Dispatched { result: DispatchResultWithPostInfo }, + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::call] + impl Pallet { + /// Dispatch meta transaction. + /// + /// origin must be signed + #[pallet::call_index(0)] + #[pallet::weight({ + let dispatch_info = meta_tx.call.get_dispatch_info(); + // TODO: plus T::WeightInfo::dispatch() which must include the weight of T::TxExtension + ( + dispatch_info.weight, + dispatch_info.class, + ) + })] + pub fn dispatch(origin: OriginFor, meta_tx: MetaTxFor) -> DispatchResultWithPostInfo { + let _who = ensure_signed(origin)?; + + let (signer, signature) = match meta_tx.proof { + Proof::Signed(signer, signature) => (signer, signature), + }; + + let signed_payload = SignedPayloadFor::::new(*meta_tx.call, meta_tx.tx_ext) + .map_err(|_| Error::::TODO)?; + + if !signed_payload.using_encoded(|payload| signature.verify(payload, &signer)) { + return Err(Error::::TODO.into()); + } + + let origin = T::RuntimeOrigin::signed(signer); + let (call, tx_ext, _) = signed_payload.deconstruct(); + let info = call.get_dispatch_info(); + // TODO: to get the len we have to encode the original `meta_tx`. + let len = 0; + let mut ctx = T::TxContext::default(); + + let (_, val, origin) = T::TxExtension::validate( + &tx_ext, + origin, + &call, + &info, + len, + &mut ctx, + tx_ext.implicit().map_err(|_| Error::::TODO)?, + &call, + ) + .map_err(|_| Error::::TODO)?; + + let pre = T::TxExtension::prepare(tx_ext, val, &origin, &call, &info, len, &ctx) + .map_err(|_| Error::::TODO)?; + + let res = call.dispatch(origin); + let post_info = res.unwrap_or_else(|err| err.post_info); + let pd_res = res.map(|_| ()).map_err(|e| e.error); + + T::TxExtension::post_dispatch(pre, &info, &post_info, len, &pd_res, &ctx) + .map_err(|_| Error::::TODO)?; + + Self::deposit_event(Event::Dispatched { result: res }); + + res + } + } +} diff --git a/substrate/frame/meta-tx/src/mock.rs b/substrate/frame/meta-tx/src/mock.rs new file mode 100644 index 000000000000..a4367d82e70d --- /dev/null +++ b/substrate/frame/meta-tx/src/mock.rs @@ -0,0 +1,109 @@ +// This file is part of Substrate. +// 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. + +//! Mock setup for tests. + +#![cfg(any(test, feature = "runtime-benchmarks"))] + +use crate as pallet_meta_tx; +use crate::*; +use frame_support::{ + construct_runtime, derive_impl, + weights::{FixedFee, NoFee}, +}; +use sp_core::ConstU8; +use sp_runtime::{traits::IdentityLookup, MultiSignature}; + +pub type Balance = u64; + +pub type Signature = MultiSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; + +pub type TxExtension = ( + frame_system::CheckNonZeroSender, + frame_system::CheckSpecVersion, + frame_system::CheckTxVersion, + frame_system::CheckGenesis, + frame_system::CheckMortality, + frame_system::CheckNonce, + frame_system::CheckWeight, + pallet_transaction_payment::ChargeTransactionPayment, +); + +pub type UncheckedExtrinsic = + sp_runtime::generic::UncheckedExtrinsic; + +pub type MetaTxExtension = ( + frame_system::CheckNonZeroSender, + frame_system::CheckSpecVersion, + frame_system::CheckTxVersion, + frame_system::CheckGenesis, + frame_system::CheckMortality, + frame_system::CheckNonce, +); + +impl Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Signature = Signature; + type PublicKey = ::Signer; + type TxContext = (); + type TxExtension = MetaTxExtension; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Runtime { + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = frame_system::mocking::MockBlock; + type AccountData = pallet_balances::AccountData<::Balance>; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Runtime { + type ReserveIdentifier = [u8; 8]; + type AccountStore = System; +} + +pub const TX_FEE: u32 = 10; + +impl pallet_transaction_payment::Config for Runtime { + type WeightInfo = (); + type RuntimeEvent = RuntimeEvent; + type OnChargeTransaction = pallet_transaction_payment::CurrencyAdapter; + type OperationalFeeMultiplier = ConstU8<1>; + type WeightToFee = FixedFee; + type LengthToFee = NoFee; + type FeeMultiplierUpdate = (); +} + +construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + MetaTx: pallet_meta_tx, + TxPayment: pallet_transaction_payment, + } +); + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut ext = sp_io::TestExternalities::new(Default::default()); + ext.execute_with(|| { + frame_system::GenesisConfig::::default().build(); + System::set_block_number(1); + }); + ext +} diff --git a/substrate/frame/meta-tx/src/tests.rs b/substrate/frame/meta-tx/src/tests.rs new file mode 100644 index 000000000000..bc44e3b06c34 --- /dev/null +++ b/substrate/frame/meta-tx/src/tests.rs @@ -0,0 +1,141 @@ +// This file is part of Substrate. + +// 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. + +use crate::*; +use frame_support::traits::tokens::fungible::Inspect; +use keyring::AccountKeyring; +use mock::*; +use sp_io::hashing::blake2_256; +use sp_runtime::traits::Hash; +use sp_runtime::traits::{Applyable, Checkable, IdentityLookup}; +use sp_runtime::MultiSignature; + +#[test] +fn meta_tx_works() { + new_test_ext().execute_with(|| { + // meta tx signer + let alice_keyring = AccountKeyring::Alice; + // meta tx relayer + let bob_keyring = AccountKeyring::Bob; + + let alice_account = AccountId::from(alice_keyring.public()); + let bob_account = AccountId::from(bob_keyring.public()); + + let ed = Balances::minimum_balance(); + let tx_fee: Balance = (2 * TX_FEE).into(); // base tx fee + weight fee + let alice_balance = ed * 100; + let bob_balance = ed * 100; + + { + // setup initial balances for alice and bob + Balances::force_set_balance( + RuntimeOrigin::root(), + alice_account.clone().into(), + alice_balance, + ) + .unwrap(); + Balances::force_set_balance( + RuntimeOrigin::root(), + bob_account.clone().into(), + bob_balance, + ) + .unwrap(); + } + + // Alice builds a meta transaction. + + let remark_call = + RuntimeCall::System(frame_system::Call::remark_with_event { remark: vec![1] }); + let meta_tx_ext: MetaTxExtension = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckMortality::::from(sp_runtime::generic::Era::immortal()), + frame_system::CheckNonce::::from( + frame_system::Pallet::::account(&alice_account).nonce, + ), + ); + + let meta_tx_sig = MultiSignature::Sr25519( + (remark_call.clone(), meta_tx_ext.clone(), meta_tx_ext.implicit().unwrap()) + .using_encoded(|e| alice_keyring.sign(&blake2_256(e))), + ); + + let meta_tx = MetaTxFor::::new_signed( + alice_account.clone(), + meta_tx_sig, + remark_call.clone(), + meta_tx_ext.clone(), + ); + + // Encode and share with the world. + let meta_tx_encoded = meta_tx.encode(); + + // Bob acts as meta transaction relayer. + + let meta_tx = MetaTxFor::::decode(&mut &meta_tx_encoded[..]).unwrap(); + let call = RuntimeCall::MetaTx(Call::dispatch { meta_tx: meta_tx.clone() }); + let tx_ext: TxExtension = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckMortality::::from(sp_runtime::generic::Era::immortal()), + frame_system::CheckNonce::::from( + frame_system::Pallet::::account(&bob_account).nonce, + ), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(0), + ); + + let tx_sig = MultiSignature::Sr25519( + (call.clone(), tx_ext.clone(), tx_ext.implicit().unwrap()) + .using_encoded(|e| bob_keyring.sign(&blake2_256(e))), + ); + + let uxt = UncheckedExtrinsic::new_signed(call, bob_account.clone(), tx_sig, tx_ext); + + // Check Extrinsic validity and apply it. + + let uxt_info = uxt.get_dispatch_info(); + let uxt_len = uxt.using_encoded(|e| e.len()); + + let xt = >>::check( + uxt, + &Default::default(), + ) + .unwrap(); + + let res = xt.apply::(&uxt_info, uxt_len).unwrap(); + + // Asserting the results. + + assert!(res.is_ok()); + + System::assert_has_event(RuntimeEvent::MetaTx(crate::Event::Dispatched { result: res })); + + System::assert_has_event(RuntimeEvent::System(frame_system::Event::Remarked { + sender: alice_account.clone(), + hash: ::Hashing::hash(&[1]), + })); + + // Alice balance is unchanged, Bob paid the transaction fee. + assert_eq!(alice_balance, Balances::free_balance(alice_account)); + assert_eq!(bob_balance - tx_fee, Balances::free_balance(bob_account)); + }); +} diff --git a/substrate/primitives/runtime/src/traits/mod.rs b/substrate/primitives/runtime/src/traits/mod.rs index c26b0c5650ff..0acf054afe7e 100644 --- a/substrate/primitives/runtime/src/traits/mod.rs +++ b/substrate/primitives/runtime/src/traits/mod.rs @@ -232,8 +232,13 @@ pub trait StaticLookup { } /// A lookup implementation returning the input value. -#[derive(Default, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct IdentityLookup(PhantomData); +impl Default for IdentityLookup { + fn default() -> Self { + Self(Default::default()) + } +} impl StaticLookup for IdentityLookup { type Source = T; type Target = T; @@ -1803,7 +1808,7 @@ impl<'a, T: codec::Input> codec::Input for AppendZerosInput<'a, T> { into[i] = b; i += 1; } else { - break + break; } } i @@ -1920,7 +1925,7 @@ impl AccountIdConversion fo fn try_from_sub_account(x: &T) -> Option<(Self, S)> { x.using_encoded(|d| { if d[0..4] != Id::TYPE_ID { - return None + return None; } let mut cursor = &d[4..]; let result = Decode::decode(&mut cursor).ok()?; From 4e0fa2d3094441711e6eb88bcbc76a9fb015d7e5 Mon Sep 17 00:00:00 2001 From: muharem Date: Tue, 7 May 2024 16:08:56 +0200 Subject: [PATCH 2/6] resolve todos and docs --- Cargo.lock | 1 + substrate/frame/meta-tx/Cargo.toml | 1 + substrate/frame/meta-tx/src/lib.rs | 167 ++++++++++++++++++++------- substrate/frame/meta-tx/src/mock.rs | 8 +- substrate/frame/meta-tx/src/tests.rs | 12 +- 5 files changed, 136 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b83dd75f55be..e95372ba0e70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10151,6 +10151,7 @@ dependencies = [ name = "pallet-meta-tx" version = "0.0.1" dependencies = [ + "docify 0.2.7", "frame-benchmarking", "frame-support", "frame-system", diff --git a/substrate/frame/meta-tx/Cargo.toml b/substrate/frame/meta-tx/Cargo.toml index 3a637087c8a3..4a5a0cf2e6c4 100644 --- a/substrate/frame/meta-tx/Cargo.toml +++ b/substrate/frame/meta-tx/Cargo.toml @@ -9,6 +9,7 @@ repository.workspace = true [dependencies] codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["max-encoded-len"] } +docify = "0.2.7" scale-info = { version = "2.1.2", default-features = false, features = ["derive"] } serde = { features = ["derive"], optional = true, workspace = true, default-features = true } diff --git a/substrate/frame/meta-tx/src/lib.rs b/substrate/frame/meta-tx/src/lib.rs index f2e1a125d2f1..620f78260834 100644 --- a/substrate/frame/meta-tx/src/lib.rs +++ b/substrate/frame/meta-tx/src/lib.rs @@ -15,9 +15,44 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Meta Tx +//! # Meta Tx or Meta Transaction pallet. //! -//! TODO docs +//! The pallet provides a way to dispatch a transaction authorized by one party (the signer) and +//! executed by an untrusted third party (the relayer) that covers the transaction fees. +//! +//! ## Pallet API +//! +//! See the [`pallet`] module for more information about the interfaces this pallet exposes, +//! including its configuration trait, dispatchables, storage items, events and errors. +//! +//! ## Overview +//! +//! The pallet exposes a client level API which usually not meant to be used directly by the end +//! user. Meta transaction constructed with a wallet help will contain a target call, required +//! extensions and a signer signature then will be gossiped with the world and can be picked up by +//! anyone who is interested in relaying the transaction. The relayer will publish a regular +//! transaction with the [`dispatch`](`Pallet::dispatch`) call and the meta transaction as an +//! argument to execute the target call on behalf of the signer and cover the fees. +//! +//! The pallet exposes a client-level API, which is usually not meant to be used directly by the +//! end-user. A meta transaction constructed with a wallet's help will contain a target call, +//! required extensions, and a signer's signature. It will then be shared with the world and can +//! be picked up by anyone interested in relaying the transaction. The relayer will publish a +//! regular transaction with the [`dispatch`](`Pallet::dispatch`) call and the meta transaction as +//! an argument to execute the target call on behalf of the signer and cover the fees. +//! +//! ### Example +#![doc = docify::embed!("src/tests.rs", sign_and_execute_meta_tx)] +//! +//! ## Low Level / Implementation Details +//! +//! The layout of the Meta Transaction is identical to the regular transaction. It contains the +//! signer's address, the signature, the target call, and a configurable set of extensions. The +//! signed payload concatenates the call, the extensions, and the implicit data of the extensions +//! and can be represented as the [sp_runtime::generic::SignedPayload] type. The extensions are +//! presented under the same [TransactionExtension] contract, and types like +//! [frame_system::CheckGenesis], [frame_system::CheckMortality], [frame_system::CheckNonce], etc., +//! can be used and are generally relevant in the context of meta transactions. #![cfg_attr(not(feature = "std"), not_std)] @@ -33,47 +68,57 @@ use frame_support::{ traits::OriginTrait, }; use frame_system::pallet_prelude::*; -use sp_runtime::traits::TransactionExtension; -use sp_runtime::traits::TransactionExtensionBase; -use sp_runtime::traits::{Dispatchable, IdentifyAccount, Verify}; - -/// Meta transaction type. -// TODO: The `MetaTx` type is similar to `sp_runtime::generic::UncheckedExtrinsic`. However, -// `MetaTx` cannot replace generic::UncheckedExtrinsic because we need to box the call type, -// given that `MetaTx` is used as an argument type for a call. +use sp_runtime::traits::{ + Dispatchable, IdentifyAccount, TransactionExtension, TransactionExtensionBase, Verify, +}; + +/// Meta Transaction type. +/// +/// The data that is provided and signed by the signer and shared with the relayer. #[derive(Encode, Decode, PartialEq, Eq, TypeInfo, Clone, RuntimeDebug)] -pub struct MetaTx { +pub struct MetaTx { + /// The proof of the authenticity of the meta transaction. proof: Proof, + /// The target call to be executed on behalf of the signer. call: Box, - tx_ext: TxExtension, + /// The required extension/s. + /// + /// This might include the nonce check, expiration, etc. + extension: Extension, } -impl MetaTx { +impl MetaTx { + /// Create a new meta transaction. pub fn new_signed( address: Address, signature: Signature, call: Call, - tx_ext: TxExtension, + extension: Extension, ) -> Self { - Self { proof: Proof::Signed(address, signature), call: Box::new(call), tx_ext } + Self { proof: Proof::Signed(address, signature), call: Box::new(call), extension } } } +/// Proof of the authenticity of the meta transaction. +// It could potentially be extended to support additional types of proofs, similar to the +// sp_runtime::generic::Preamble::Bare transaction type. #[derive(Encode, Decode, PartialEq, Eq, TypeInfo, Clone, RuntimeDebug)] pub enum Proof { + /// Signature of the meta transaction payload and the signer's address. Signed(Address, Signature), - // TODO `General` as in `sp_runtime::generic::Preamble`. } +/// The [`MetaTx`] for the given config. pub type MetaTxFor = MetaTx< <::PublicKey as IdentifyAccount>::AccountId, ::Signature, ::RuntimeCall, - ::TxExtension, + ::Extension, >; +/// The [`sp_runtime::generic::SignedPayload`] for the given config. pub type SignedPayloadFor = - sp_runtime::generic::SignedPayload<::RuntimeCall, ::TxExtension>; + sp_runtime::generic::SignedPayload<::RuntimeCall, ::Extension>; #[frame_support::pallet(dev_mode)] pub mod pallet { @@ -95,16 +140,29 @@ pub mod pallet { type Signature: Parameter + Verify; /// Public key type used for signature verification. type PublicKey: IdentifyAccount; - /// The context type of `Self::TxExtension` - type TxContext: Member + Default; + /// The context type of `Self::Extension`. + type Context: Member + Default; /// Transaction extension/s for meta transactions. - type TxExtension: TransactionExtension<::RuntimeCall, Self::TxContext>; + /// + /// The extensions that must be present in every meta transaction. This + /// generally includes extensions like [frame_system::CheckSpecVersion], + /// [frame_system::CheckTxVersion], [frame_system::CheckGenesis], + /// [frame_system::CheckMortality], [frame_system::CheckNonce], etc. + type Extension: TransactionExtension<::RuntimeCall, Self::Context>; } #[pallet::error] pub enum Error { - /// TODO - TODO, + /// Invalid proof (e.g. signature). + BadProof, + /// The meta transaction is not yet valid (e.g. nonce too high). + Future, + /// The meta transaction is outdated (e.g. nonce too low). + Stale, + /// The meta transactions's birth block is ancient. + AncientBirthBlock, + /// The meta transaction is invalid. + Invalid, } #[pallet::event] @@ -119,64 +177,85 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// Dispatch meta transaction. + /// Dispatch a given meta transaction. /// - /// origin must be signed + /// - `_origin`: Can be any kind of origin. + /// - `meta_tx`: Meta Transaction with a target call to be dispatched. #[pallet::call_index(0)] #[pallet::weight({ let dispatch_info = meta_tx.call.get_dispatch_info(); - // TODO: plus T::WeightInfo::dispatch() which must include the weight of T::TxExtension + // TODO: plus T::WeightInfo::dispatch() which must include the weight of T::Extension ( dispatch_info.weight, dispatch_info.class, ) })] - pub fn dispatch(origin: OriginFor, meta_tx: MetaTxFor) -> DispatchResultWithPostInfo { - let _who = ensure_signed(origin)?; + pub fn dispatch( + _origin: OriginFor, + meta_tx: MetaTxFor, + ) -> DispatchResultWithPostInfo { + let meta_tx_size = meta_tx.encoded_size(); let (signer, signature) = match meta_tx.proof { Proof::Signed(signer, signature) => (signer, signature), }; - let signed_payload = SignedPayloadFor::::new(*meta_tx.call, meta_tx.tx_ext) - .map_err(|_| Error::::TODO)?; + let signed_payload = SignedPayloadFor::::new(*meta_tx.call, meta_tx.extension) + .map_err(|_| Error::::Invalid)?; if !signed_payload.using_encoded(|payload| signature.verify(payload, &signer)) { - return Err(Error::::TODO.into()); + return Err(Error::::BadProof.into()); } let origin = T::RuntimeOrigin::signed(signer); - let (call, tx_ext, _) = signed_payload.deconstruct(); + let (call, extension, _) = signed_payload.deconstruct(); let info = call.get_dispatch_info(); - // TODO: to get the len we have to encode the original `meta_tx`. - let len = 0; - let mut ctx = T::TxContext::default(); + let mut ctx = T::Context::default(); - let (_, val, origin) = T::TxExtension::validate( - &tx_ext, + let (_, val, origin) = T::Extension::validate( + &extension, origin, &call, &info, - len, + meta_tx_size, &mut ctx, - tx_ext.implicit().map_err(|_| Error::::TODO)?, + extension.implicit().map_err(|_| Error::::Invalid)?, &call, ) - .map_err(|_| Error::::TODO)?; + .map_err(|err| Error::::from(err))?; - let pre = T::TxExtension::prepare(tx_ext, val, &origin, &call, &info, len, &ctx) - .map_err(|_| Error::::TODO)?; + let pre = + T::Extension::prepare(extension, val, &origin, &call, &info, meta_tx_size, &ctx) + .map_err(|err| Error::::from(err))?; let res = call.dispatch(origin); let post_info = res.unwrap_or_else(|err| err.post_info); let pd_res = res.map(|_| ()).map_err(|e| e.error); - T::TxExtension::post_dispatch(pre, &info, &post_info, len, &pd_res, &ctx) - .map_err(|_| Error::::TODO)?; + T::Extension::post_dispatch(pre, &info, &post_info, meta_tx_size, &pd_res, &ctx) + .map_err(|err| Error::::from(err))?; Self::deposit_event(Event::Dispatched { result: res }); res } } + + /// Implements [`From`] for [`Error`] by mapping the relevant error + /// variants. + impl From for Error { + fn from(err: TransactionValidityError) -> Self { + use TransactionValidityError::*; + match err { + Unknown(_) => Error::::Invalid, + Invalid(err) => match err { + InvalidTransaction::BadProof => Error::::BadProof, + InvalidTransaction::Future => Error::::Future, + InvalidTransaction::Stale => Error::::Stale, + InvalidTransaction::AncientBirthBlock => Error::::AncientBirthBlock, + _ => Error::::Invalid, + }, + } + } + } } diff --git a/substrate/frame/meta-tx/src/mock.rs b/substrate/frame/meta-tx/src/mock.rs index a4367d82e70d..70e8d9d220a4 100644 --- a/substrate/frame/meta-tx/src/mock.rs +++ b/substrate/frame/meta-tx/src/mock.rs @@ -32,7 +32,7 @@ pub type Balance = u64; pub type Signature = MultiSignature; pub type AccountId = <::Signer as IdentifyAccount>::AccountId; -pub type TxExtension = ( +pub type Extension = ( frame_system::CheckNonZeroSender, frame_system::CheckSpecVersion, frame_system::CheckTxVersion, @@ -44,7 +44,7 @@ pub type TxExtension = ( ); pub type UncheckedExtrinsic = - sp_runtime::generic::UncheckedExtrinsic; + sp_runtime::generic::UncheckedExtrinsic; pub type MetaTxExtension = ( frame_system::CheckNonZeroSender, @@ -60,8 +60,8 @@ impl Config for Runtime { type RuntimeCall = RuntimeCall; type Signature = Signature; type PublicKey = ::Signer; - type TxContext = (); - type TxExtension = MetaTxExtension; + type Context = (); + type Extension = MetaTxExtension; } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] diff --git a/substrate/frame/meta-tx/src/tests.rs b/substrate/frame/meta-tx/src/tests.rs index bc44e3b06c34..b3292eda5df1 100644 --- a/substrate/frame/meta-tx/src/tests.rs +++ b/substrate/frame/meta-tx/src/tests.rs @@ -20,12 +20,14 @@ use frame_support::traits::tokens::fungible::Inspect; use keyring::AccountKeyring; use mock::*; use sp_io::hashing::blake2_256; -use sp_runtime::traits::Hash; -use sp_runtime::traits::{Applyable, Checkable, IdentityLookup}; -use sp_runtime::MultiSignature; +use sp_runtime::{ + traits::{Applyable, Checkable, Hash, IdentityLookup}, + MultiSignature, +}; +#[docify::export] #[test] -fn meta_tx_works() { +fn sign_and_execute_meta_tx() { new_test_ext().execute_with(|| { // meta tx signer let alice_keyring = AccountKeyring::Alice; @@ -90,7 +92,7 @@ fn meta_tx_works() { let meta_tx = MetaTxFor::::decode(&mut &meta_tx_encoded[..]).unwrap(); let call = RuntimeCall::MetaTx(Call::dispatch { meta_tx: meta_tx.clone() }); - let tx_ext: TxExtension = ( + let tx_ext: Extension = ( frame_system::CheckNonZeroSender::::new(), frame_system::CheckSpecVersion::::new(), frame_system::CheckTxVersion::::new(), From 7a104ef3f19a0a8032f53369ee3fb106d73f75f7 Mon Sep 17 00:00:00 2001 From: muharem Date: Tue, 7 May 2024 16:41:08 +0200 Subject: [PATCH 3/6] fix --- substrate/frame/meta-tx/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/substrate/frame/meta-tx/src/lib.rs b/substrate/frame/meta-tx/src/lib.rs index 620f78260834..4c5489d02e19 100644 --- a/substrate/frame/meta-tx/src/lib.rs +++ b/substrate/frame/meta-tx/src/lib.rs @@ -54,7 +54,7 @@ //! [frame_system::CheckGenesis], [frame_system::CheckMortality], [frame_system::CheckNonce], etc., //! can be used and are generally relevant in the context of meta transactions. -#![cfg_attr(not(feature = "std"), not_std)] +#![cfg_attr(not(feature = "std"), no_std)] #[cfg(test)] mod mock; @@ -71,6 +71,7 @@ use frame_system::pallet_prelude::*; use sp_runtime::traits::{ Dispatchable, IdentifyAccount, TransactionExtension, TransactionExtensionBase, Verify, }; +use sp_std::boxed::Box; /// Meta Transaction type. /// From 59a66fa0ec859c4c78edc12bfb5e55cf324cc2c3 Mon Sep 17 00:00:00 2001 From: muharem Date: Tue, 7 May 2024 16:41:59 +0200 Subject: [PATCH 4/6] include meta-tx instance into kitchensink runtime --- Cargo.lock | 1 + substrate/bin/node/runtime/Cargo.toml | 4 ++++ substrate/bin/node/runtime/src/lib.rs | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e95372ba0e70..30fedaa13317 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7090,6 +7090,7 @@ dependencies = [ "pallet-lottery", "pallet-membership", "pallet-message-queue", + "pallet-meta-tx", "pallet-migrations", "pallet-mixnet", "pallet-mmr", diff --git a/substrate/bin/node/runtime/Cargo.toml b/substrate/bin/node/runtime/Cargo.toml index 09c8fb4ed3d4..943a992c7f82 100644 --- a/substrate/bin/node/runtime/Cargo.toml +++ b/substrate/bin/node/runtime/Cargo.toml @@ -97,6 +97,7 @@ pallet-identity = { path = "../../../frame/identity", default-features = false } pallet-lottery = { path = "../../../frame/lottery", default-features = false } pallet-membership = { path = "../../../frame/membership", default-features = false } pallet-message-queue = { path = "../../../frame/message-queue", default-features = false } +pallet-meta-tx = { path = "../../../frame/meta-tx", default-features = false } pallet-mixnet = { path = "../../../frame/mixnet", default-features = false } pallet-mmr = { path = "../../../frame/merkle-mountain-range", default-features = false } pallet-multisig = { path = "../../../frame/multisig", default-features = false } @@ -199,6 +200,7 @@ std = [ "pallet-lottery/std", "pallet-membership/std", "pallet-message-queue/std", + "pallet-meta-tx/std", "pallet-migrations/std", "pallet-mixnet/std", "pallet-mmr/std", @@ -305,6 +307,7 @@ runtime-benchmarks = [ "pallet-lottery/runtime-benchmarks", "pallet-membership/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", + "pallet-meta-tx/runtime-benchmarks", "pallet-migrations/runtime-benchmarks", "pallet-mixnet/runtime-benchmarks", "pallet-mmr/runtime-benchmarks", @@ -386,6 +389,7 @@ try-runtime = [ "pallet-lottery/try-runtime", "pallet-membership/try-runtime", "pallet-message-queue/try-runtime", + "pallet-meta-tx/try-runtime", "pallet-migrations/try-runtime", "pallet-mixnet/try-runtime", "pallet-mmr/try-runtime", diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index c48cfa9c2dec..af3b3b9abb5f 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -2196,6 +2196,24 @@ impl pallet_parameters::Config for Runtime { type WeightInfo = (); } +pub type MetaTxExtension = ( + frame_system::CheckNonZeroSender, + frame_system::CheckSpecVersion, + frame_system::CheckTxVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, +); + +impl pallet_meta_tx::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Signature = Signature; + type PublicKey = ::Signer; + type Context = (); + type Extension = MetaTxExtension; +} + #[frame_support::runtime] mod runtime { #[runtime::runtime] @@ -2449,6 +2467,9 @@ mod runtime { #[runtime::pallet_index(77)] pub type SkipFeelessPayment = pallet_skip_feeless_payment; + + #[runtime::pallet_index(78)] + pub type MetaTransacion = pallet_meta_tx; } /// The address format for describing accounts. From ec0d6681388c6f87ccbbf078b84ca17e1b263000 Mon Sep 17 00:00:00 2001 From: muharem Date: Tue, 7 May 2024 17:25:45 +0200 Subject: [PATCH 5/6] clippy fixes --- substrate/frame/meta-tx/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/substrate/frame/meta-tx/src/lib.rs b/substrate/frame/meta-tx/src/lib.rs index 4c5489d02e19..dea857284f56 100644 --- a/substrate/frame/meta-tx/src/lib.rs +++ b/substrate/frame/meta-tx/src/lib.rs @@ -71,7 +71,7 @@ use frame_system::pallet_prelude::*; use sp_runtime::traits::{ Dispatchable, IdentifyAccount, TransactionExtension, TransactionExtensionBase, Verify, }; -use sp_std::boxed::Box; +use sp_std::prelude::*; /// Meta Transaction type. /// @@ -223,18 +223,18 @@ pub mod pallet { extension.implicit().map_err(|_| Error::::Invalid)?, &call, ) - .map_err(|err| Error::::from(err))?; + .map_err(Error::::from)?; let pre = T::Extension::prepare(extension, val, &origin, &call, &info, meta_tx_size, &ctx) - .map_err(|err| Error::::from(err))?; + .map_err(Error::::from)?; let res = call.dispatch(origin); let post_info = res.unwrap_or_else(|err| err.post_info); let pd_res = res.map(|_| ()).map_err(|e| e.error); T::Extension::post_dispatch(pre, &info, &post_info, meta_tx_size, &pd_res, &ctx) - .map_err(|err| Error::::from(err))?; + .map_err(Error::::from)?; Self::deposit_event(Event::Dispatched { result: res }); From 8407cd0f3090976b1aa928ea9a2983d90a378ee7 Mon Sep 17 00:00:00 2001 From: muharem Date: Tue, 7 May 2024 17:30:30 +0200 Subject: [PATCH 6/6] remove unnecessary fmt --- substrate/primitives/runtime/src/traits/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/substrate/primitives/runtime/src/traits/mod.rs b/substrate/primitives/runtime/src/traits/mod.rs index 0acf054afe7e..44860b620891 100644 --- a/substrate/primitives/runtime/src/traits/mod.rs +++ b/substrate/primitives/runtime/src/traits/mod.rs @@ -1808,7 +1808,7 @@ impl<'a, T: codec::Input> codec::Input for AppendZerosInput<'a, T> { into[i] = b; i += 1; } else { - break; + break } } i @@ -1925,7 +1925,7 @@ impl AccountIdConversion fo fn try_from_sub_account(x: &T) -> Option<(Self, S)> { x.using_encoded(|d| { if d[0..4] != Id::TYPE_ID { - return None; + return None } let mut cursor = &d[4..]; let result = Decode::decode(&mut cursor).ok()?;