referrerpolicy=no-referrer-when-downgrade
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:

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.