Expand description
§Authorization Transaction Extension Example Pallet
This pallet serves as an example and is not meant to be used in production.
FRAME Transaction Extension reference implementation, origin mutation, origin authorization and
integration in a TransactionExtension
pipeline.
The TransactionExtension used in this example is
AuthorizeCoownership. If activated, the extension will
authorize 2 signers as coowners, with a coowner origin specific to
the coownership example pallet, by validating a signature of the rest of
the transaction from each party. This means any extensions after ours in the pipeline, their
implicits and the actual call. The extension pipeline used in our example checks the genesis
hash, transaction version and mortality of the transaction after the AuthorizeCoownership
runs
as we want these transactions to run regardless of what origin passes through them and/or we
want their implicit data in any signature authorization happening earlier in the pipeline.
In this example, aside from the AuthorizeCoownership extension, we use the following pallets:
- pallet_coownership - provides a coowner origin and the functionality to authorize it.
- pallet_assets - a dummy asset pallet that tracks assets, identified by an AssetId, and their respective owners, which can be either an account or a pair of owners.
Assets are created in pallet_assets using the create_asset call, which accepts traditionally signed origins (a single account) or coowner origins, authorized through the CoownerOrigin type.
§Example runtime setup
mod example_runtime {
use super::*;
/// Our `TransactionExtension` fit for general transactions.
pub type TxExtension = (
// Validate the signature of regular account transactions (substitutes the old signed
// transaction).
VerifySignature<Runtime>,
// Nonce check (and increment) for the caller.
CheckNonce<Runtime>,
// If activated, will mutate the origin to a `pallet_coownership` origin of 2 accounts that
// own something.
AuthorizeCoownership<Runtime, MultiSigner, MultiSignature>,
// Some other extensions that we want to run for every possible origin and we want captured
// in any and all signature and authorization schemes (such as the traditional account
// signature or the double signature in `pallet_coownership`).
CheckGenesis<Runtime>,
CheckTxVersion<Runtime>,
CheckEra<Runtime>,
);
/// Convenience type to more easily construct the signature to be signed in case
/// `AuthorizeCoownership` is activated.
pub type InnerTxExtension = (CheckGenesis<Runtime>, CheckTxVersion<Runtime>, CheckEra<Runtime>);
pub type UncheckedExtrinsic =
generic::UncheckedExtrinsic<AccountId, RuntimeCall, Signature, TxExtension>;
pub type Header = generic::Header<BlockNumber, BlakeTwo256>;
pub type Block = generic::Block<Header, UncheckedExtrinsic>;
pub type AccountId = <<Signature as Verify>::Signer as IdentifyAccount>::AccountId;
pub type Signature = MultiSignature;
pub type BlockNumber = u32;
// For testing the pallet, we construct a mock runtime.
frame_support::construct_runtime!(
pub enum Runtime
{
System: frame_system,
VerifySignaturePallet: pallet_verify_signature,
Assets: pallet_assets,
Coownership: pallet_coownership,
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type AccountId = AccountId;
type Block = Block;
type Lookup = IdentityLookup<Self::AccountId>;
}
#[cfg(feature = "runtime-benchmarks")]
pub struct BenchmarkHelper;
#[cfg(feature = "runtime-benchmarks")]
impl pallet_verify_signature::BenchmarkHelper<MultiSignature, AccountId> for BenchmarkHelper {
fn create_signature(_entropy: &[u8], msg: &[u8]) -> (MultiSignature, AccountId) {
use sp_io::crypto::{sr25519_generate, sr25519_sign};
use sp_runtime::traits::IdentifyAccount;
let public = sr25519_generate(0.into(), None);
let who_account: AccountId = MultiSigner::Sr25519(public).into_account().into();
let signature = MultiSignature::Sr25519(sr25519_sign(0.into(), &public, msg).unwrap());
(signature, who_account)
}
}
impl pallet_verify_signature::Config for Runtime {
type Signature = MultiSignature;
type AccountIdentifier = MultiSigner;
type WeightInfo = ();
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = BenchmarkHelper;
}
/// Type that enables any pallet to ask for a coowner origin.
pub struct EnsureCoowner;
impl EnsureOrigin<RuntimeOrigin> for EnsureCoowner {
type Success = (AccountId, AccountId);
fn try_origin(o: RuntimeOrigin) -> Result<Self::Success, RuntimeOrigin> {
match o.clone().into() {
Ok(pallet_coownership::Origin::<Runtime>::Coowners(first, second)) =>
Ok((first, second)),
_ => Err(o),
}
}
#[cfg(feature = "runtime-benchmarks")]
fn try_successful_origin() -> Result<RuntimeOrigin, ()> {
unimplemented!()
}
}
impl pallet_assets::Config for Runtime {
type CoownerOrigin = EnsureCoowner;
}
impl pallet_coownership::Config for Runtime {
type RuntimeOrigin = RuntimeOrigin;
type PalletsOrigin = OriginCaller;
}
}
§Example usage
#[test]
fn create_coowned_asset_works() {
new_test_ext().execute_with(|| {
let alice_keyring = AccountKeyring::Alice;
let bob_keyring = AccountKeyring::Bob;
let charlie_keyring = AccountKeyring::Charlie;
let alice_account = AccountId::from(alice_keyring.public());
let bob_account = AccountId::from(bob_keyring.public());
let charlie_account = AccountId::from(charlie_keyring.public());
// Simple call to create asset with Id `42`.
let create_asset_call =
RuntimeCall::Assets(pallet_assets::Call::create_asset { asset_id: 42 });
let ext_version: ExtensionVersion = 0;
// Create the inner transaction extension, to be signed by our coowners, Alice and Bob.
let inner_ext: InnerTxExtension = (
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckTxVersion::<Runtime>::new(),
frame_system::CheckEra::<Runtime>::from(sp_runtime::generic::Era::immortal()),
);
// Create the payload Alice and Bob need to sign.
let inner_payload =
(&(ext_version, &create_asset_call), &inner_ext, inner_ext.implicit().unwrap());
// Create Alice's signature.
let alice_inner_sig = MultiSignature::Sr25519(
inner_payload.using_encoded(|e| alice_keyring.sign(&sp_io::hashing::blake2_256(e))),
);
// Create Bob's signature.
let bob_inner_sig = MultiSignature::Sr25519(
inner_payload.using_encoded(|e| bob_keyring.sign(&sp_io::hashing::blake2_256(e))),
);
// Create the transaction extension, to be signed by the submitter of the extrinsic, let's
// have it be Charlie.
let initial_nonce = 23;
let tx_ext = (
frame_system::CheckNonce::<Runtime>::from(initial_nonce),
AuthorizeCoownership::<Runtime, MultiSigner, MultiSignature>::new(
(alice_keyring.into(), alice_inner_sig.clone()),
(bob_keyring.into(), bob_inner_sig.clone()),
),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckTxVersion::<Runtime>::new(),
frame_system::CheckEra::<Runtime>::from(sp_runtime::generic::Era::immortal()),
);
// Create Charlie's transaction signature, to be used in the top level
// `VerifyMultiSignature` extension.
let tx_sign = MultiSignature::Sr25519(
(&(ext_version, &create_asset_call), &tx_ext, tx_ext.implicit().unwrap())
.using_encoded(|e| charlie_keyring.sign(&sp_io::hashing::blake2_256(e))),
);
// Add the signature to the extension.
let tx_ext = (
VerifySignature::new_with_signature(tx_sign, charlie_account.clone()),
frame_system::CheckNonce::<Runtime>::from(initial_nonce),
AuthorizeCoownership::<Runtime, MultiSigner, MultiSignature>::new(
(alice_keyring.into(), alice_inner_sig),
(bob_keyring.into(), bob_inner_sig),
),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckTxVersion::<Runtime>::new(),
frame_system::CheckEra::<Runtime>::from(sp_runtime::generic::Era::immortal()),
);
// Create the transaction and we're ready for dispatch.
let uxt = UncheckedExtrinsic::new_transaction(create_asset_call, tx_ext);
// Check Extrinsic validity and apply it.
let uxt_info = uxt.get_dispatch_info();
let uxt_len = uxt.using_encoded(|e| e.len());
// Manually pay for Charlie's nonce.
frame_system::Account::<Runtime>::mutate(&charlie_account, |info| {
info.nonce = initial_nonce;
info.providers = 1;
});
// Check should pass.
let xt = <UncheckedExtrinsic as Checkable<IdentityLookup<AccountId>>>::check(
uxt,
&Default::default(),
)
.unwrap();
// Apply the extrinsic.
let res = xt.apply::<Runtime>(&uxt_info, uxt_len).unwrap();
// Asserting the results.
assert!(res.is_ok());
assert_eq!(frame_system::Account::<Runtime>::get(charlie_account).nonce, initial_nonce + 1);
assert_eq!(
pallet_assets::AssetOwners::<Runtime>::get(42),
Some(pallet_assets::Owner::<AccountId>::Double(alice_account, bob_account))
);
});
}
This example does not focus on any pallet logic or syntax, but rather on TransactionExtension
functionality. The pallets used are just skeletons to provide storage state and custom origin
choices and requirements, as shown in the examples. Any weight and/or
transaction fee is out of scope for this example.
Modules§
- The
pallet
module in each FRAME pallet hosts the most important items needed to construct this pallet. - The
pallet
module in each FRAME pallet hosts the most important items needed to construct this pallet.