## Module extrinsic_encoding

Source
Expand description

Learn about how extrinsics are encoded to be transmitted to a node and stored in blocks.

## §Constructing and Signing Extrinsics

Extrinsics are payloads that are stored in blocks which are responsible for altering the state of a blockchain via the state transition function.

Bizinikiwi is configurable enough that extrinsics can take any format. In practice, runtimes tend to use our [`pezsp_runtime::generic::UncheckedExtrinsic`] type to represent extrinsics, because it’s generic enough to cater for most (if not all) use cases. In Pezkuwi, this is configured here at the time of writing.

What follows is a description of how extrinsics based on this [`pezsp_runtime::generic::UncheckedExtrinsic`] type are encoded into bytes. Specifically, we are looking at how extrinsics with a format version of 5 are encoded. This version is itself a part of the payload, and if it changes, it indicates that something about the encoding may have changed.

## §Encoding an Extrinsic

At a high level, all extrinsics compatible with [`pezsp_runtime::generic::UncheckedExtrinsic`] are formed from concatenating some details together, as in the following pseudo-code:

    extrinsic_bytes = concat(
        compact_encoded_length,
        version_and_extrinsic_type,
    	maybe_extension_data,
        call_data
    )

For clarity, the actual implementation in Bizinikiwi looks like this:

ⓘ

    impl<Address, Call, Signature, Extension> Encode
    	for UncheckedExtrinsic<Address, Call, Signature, Extension>
    where
    	Preamble<Address, Signature, Extension>: Encode,
    	Call: Encode,
    	Extension: Encode,
    {
    	fn encode(&self) -> Vec<u8> {
    		let tmp = self.encode_without_prefix();

    		let compact_len = codec::Compact::<u32>(tmp.len() as u32);

    		// Allocate the output buffer with the correct length
    		let mut output = Vec::with_capacity(compact_len.size_hint() + tmp.len());

    		compact_len.encode_to(&mut output);
    		output.extend(tmp);

    		output
    	}
    }

ⓘ

    impl<Address, Call, Signature, Extra> Encode
    	for UncheckedExtrinsicV4<Address, Call, Signature, Extra>
    where
    	Address: Encode,
    	Signature: Encode,
    	Call: Encode,
    	Extra: Encode,
    {
    	fn encode(&self) -> Vec<u8> {
    		let mut tmp = Vec::with_capacity(core::mem::size_of::<Self>());

    		// 1 byte version id.
    		match self.signature.as_ref() {
    			Some(s) => {
    				tmp.push(4u8 | 0b1000_0000);
    				s.encode_to(&mut tmp);
    			},
    			None => {
    				tmp.push(4u8 & 0b0111_1111);
    			},
    		}
    		self.function.encode_to(&mut tmp);

    		let compact_len = codec::Compact::<u32>(tmp.len() as u32);

    		// Allocate the output buffer with the correct length
    		let mut output = Vec::with_capacity(compact_len.size_hint() + tmp.len());

    		compact_len.encode_to(&mut output);
    		output.extend(tmp);

    		output
    	}
    }

Let’s look at how each of these details is constructed:

### §compact_encoded_length

This is a [SCALE compact encoded][pezframe::deps::codec::Compact] integer which is equal to the length, in bytes, of the rest of the extrinsic details.

To obtain this value, we must encode and concatenate together the rest of the extrinsic details first, and then obtain the byte length of these. We can then compact encode that length, and prepend it to the rest of the details.

### §version_and_maybe_signature

If the extrinsic is unsigned, then `version_and_maybe_signature` will be just one byte denoting the transaction protocol version, which is 4 (or `0b0000_0100`).

If the extrinsic is signed (all extrinsics submitted from users must be signed), then `version_and_maybe_signature` is obtained by concatenating some details together, ie:

    version_and_maybe_signature = concat(
        version_and_signed,
        from_address,
        signature,
        transaction_extensions_extra,
    )

Each of the details to be concatenated together is explained below:

### §version_and_extrinsic_type

This byte has 2 components:

- the 2 most significant bits represent the extrinsic type:
  - bare - `0b00`
  - signed - `0b10`
  - general - `0b01`
- the 6 least significant bits represent the extrinsic format version (currently 5)

#### §Bare extrinsics

If the extrinsic is bare, then `version_and_extrinsic_type` will be just the transaction protocol version, which is 5 (or `0b0000_0101`). Bare extrinsics do not carry any other extension data, so `maybe_extension_data` would not be included in the payload and the `version_and_extrinsic_type` would always be followed by the encoded call bytes.

#### §Signed extrinsics

If the extrinsic is signed (all extrinsics submitted from users used to be signed up until version 4), then `version_and_extrinsic_type` is obtained by having a MSB of `1` on the transaction protocol version byte (which translates to `0b1000_0101`).

Additionally, signed extrinsics also carry with them address and signature information encoded as follows:

##### §from_address

This is the [SCALE encoded][pezframe::deps::codec] address of the sender of the extrinsic. The address is the first generic parameter of [`pezsp_runtime::generic::UncheckedExtrinsic`], and so can vary from chain to chain.

The address type used on the Pezkuwi relay chain is [`pezsp_runtime::MultiAddress<AccountId32>`], where `AccountId32` is defined [here][`pezsp_core::crypto::AccountId32`]. When constructing a signed extrinsic to be submitted to a Pezkuwi node, you’ll always use the [`pezsp_runtime::MultiAddress::Id`] variant to wrap your `AccountId32`.

##### §signature

This is the [SCALE encoded][pezframe::deps::codec] signature. The signature type is configured via the third generic parameter of [`pezsp_runtime::generic::UncheckedExtrinsic`], which determines the shape of the signature and signing algorithm that should be used.

The signature is obtained by signing the signed payload bytes (see below on how this is constructed) using the private key associated with the address and correct algorithm.

The signature type used on the Pezkuwi relay chain is [`pezsp_runtime::MultiSignature`]; the variants there are the types of signature that can be provided.

#### §General extrinsics

If the extrinsic is general (it doesn’t carry a signature in the payload, only extension data), then `version_and_extrinsic_type` is obtained by logical OR between the general transaction type bits and the transaction protocol version byte (which translates to `0b0100_0101`).

#### §transaction_extensions_extra

This is the concatenation of the [SCALE encoded][pezframe::deps::codec] bytes representing first a single byte describing the extension version (this is bumped whenever a change occurs in the transaction extension pipeline) followed by the bytes of each of the [transaction extensions][pezsp_runtime::traits::TransactionExtension], and are configured by the fourth generic parameter of [`pezsp_runtime::generic::UncheckedExtrinsic`]. Learn more about transaction extensions here.

When it comes to constructing an extrinsic, each transaction extension has two things that we are interested in here:

- The actual SCALE encoding of the transaction extension type itself; this is what will form our `transaction_extensions_extra` bytes.
- An `Implicit` type. This is SCALE encoded into the `transaction_extensions_implicit` data (see below).

Either (or both) of these can encode to zero bytes.

Each chain configures the set of transaction extensions that it uses in its runtime configuration. At the time of writing, Pezkuwi configures them here. Some of the common transaction extensions are defined [here][pezframe::deps::pezframe_system#transaction-extensions].

Information about exactly which transaction extensions are present on a chain and in what order is also a part of the metadata for the chain. For V15 metadata, it can be [found here][pezframe::deps::pezframe_support::__private::metadata::v15::ExtrinsicMetadata].

### §call_data

This is the main payload of the extrinsic, which is used to determine how the chain’s state is altered. This is defined by the second generic parameter of [`pezsp_runtime::generic::UncheckedExtrinsic`].

A call can be anything that implements [`Encode`][pezframe::deps::codec::Encode]. In FRAME-based runtimes, a call is represented as an enum of enums, where the outer enum represents the FRAME pezpallet being called, and the inner enum represents the call being made within that pezpallet, and any arguments to it. Read more about the call enum here.

FRAME `Call` enums are automatically generated, and end up looking something like this:

ⓘ

    pub mod call_data {
    	use codec::{Decode, Encode};
    	use pezsp_runtime::{traits::Dispatchable, DispatchResultWithInfo};

    	// The outer enum composes calls within
    	// different pallets together. We have two
    	// pallets, "PalletA" and "PalletB".
    	#[derive(Encode, Decode, Clone)]
    	pub enum Call {
    		#[codec(index = 0)]
    		PalletA(PalletACall),
    		#[codec(index = 7)]
    		PalletB(PalletBCall),
    	}

    	// An inner enum represents the calls within
    	// a specific pezpallet. "PalletA" has one call,
    	// "Foo".
    	#[derive(Encode, Decode, Clone)]
    	pub enum PalletACall {
    		#[codec(index = 0)]
    		Foo(String),
    	}

    	#[derive(Encode, Decode, Clone)]
    	pub enum PalletBCall {
    		#[codec(index = 0)]
    		Bar(String),
    	}

    	impl Dispatchable for Call {
    		type RuntimeOrigin = ();
    		type Config = ();
    		type Info = ();
    		type PostInfo = ();
    		fn dispatch(self, _origin: Self::RuntimeOrigin) -> DispatchResultWithInfo<Self::PostInfo> {
    			Ok(())
    		}
    	}
    }

In pseudo-code, this `Call` enum encodes equivalently to:

    call_data = concat(
        pezpallet_index,
        call_index,
        call_args
    )

- `pezpallet_index` is a single byte denoting the index of the pezpallet that we are calling into, and is what the tag of the outermost enum will encode to.
- `call_index` is a single byte denoting the index of the call that we are making the pezpallet, and is what the tag of the inner enum will encode to.
- `call_args` are the SCALE encoded bytes for each of the arguments that the call expects, and are typically provided as values to the inner enum.

Information about the pallets that exist for a chain (including their indexes), the calls available in each pezpallet (including their indexes), and the arguments required for each call can be found in the metadata for the chain. For V15 metadata, this information [is here][pezframe::deps::pezframe_support::__private::metadata::v15::PalletMetadata].

## §The Signed Payload Format

All signed extrinsics submitted to a node from the outside world (also known as transactions) need to be signed. The data that needs to be signed for some extrinsic is called the signed payload, and its shape is described by the following pseudo-code:

    signed_payload = blake2_256(
    	concat(
        	call_data,
        	transaction_extensions_extra,
        	transaction_extensions_implicit,
    	)
    )

The bytes representing `call_data` and `transaction_extensions_extra` can be obtained as descibed above. `transaction_extensions_implicit` is constructed by SCALE encoding the [“implicit” data][pezsp_runtime::traits::TransactionExtension::Implicit] for each transaction extension that the chain is using, in order.

Once we’ve concatenated those together, we hash the result using a Blake2 256bit hasher.

The [`pezsp_runtime::generic::SignedPayload`] type takes care of assembling the correct payload for us, given `call_data` and a tuple of transaction extensions.

## §The General Transaction Format

A General transaction does not have a signature method hardcoded in the check logic of the extrinsic, such as a traditionally signed transaction. Instead, general transactions should have one or more extensions in the transaction extension pipeline that auhtorize origins in some way, one of which could be the traditional signature check that happens for all signed transactions in the Checkable implementation of UncheckedExtrinsic. Therefore, it is up to each extension to define the format of the payload it will try to check and authorize the right origin type. For an example, look into the authorization example pezpallet extensions

## §Example Encoding

Using [`pezsp_runtime::generic::UncheckedExtrinsic`], we can construct and encode an extrinsic as follows:

ⓘ

    pub mod encoding_example {
    	use super::call_data::{Call, PalletACall};
    	use crate::reference_docs::transaction_extensions::transaction_extensions_example;
    	use codec::Encode;
    	use pezsp_core::crypto::AccountId32;
    	use pezsp_keyring::sr25519::Keyring;
    	use pezsp_runtime::{
    		generic::{SignedPayload, UncheckedExtrinsic},
    		MultiAddress, MultiSignature,
    	};

    	// Define some transaction extensions to use. We'll use a couple of examples
    	// from the transaction extensions reference doc.
    	type TransactionExtensions = (
    		transaction_extensions_example::AddToPayload,
    		transaction_extensions_example::AddToSignaturePayload,
    	);

    	// We'll use `UncheckedExtrinsic` to encode our extrinsic for us. We set
    	// the address and signature type to those used on Pezkuwi, use our custom
    	// `Call` type, and use our custom set of `TransactionExtensions`.
    	type Extrinsic = UncheckedExtrinsic<
    		MultiAddress<AccountId32, ()>,
    		Call,
    		MultiSignature,
    		TransactionExtensions,
    	>;

    	pub fn encode_demo_extrinsic() -> Vec<u8> {
    		// The "from" address will be our Alice dev account.
    		let from_address = MultiAddress::<AccountId32, ()>::Id(Keyring::Alice.to_account_id());

    		// We provide some values for our expected transaction extensions.
    		let transaction_extensions = (
    			transaction_extensions_example::AddToPayload(1),
    			transaction_extensions_example::AddToSignaturePayload,
    		);

    		// Construct our call data:
    		let call_data = Call::PalletA(PalletACall::Foo("Hello".to_string()));

    		// The signed payload. This takes care of encoding the call_data,
    		// transaction_extensions_extra and transaction_extensions_implicit, and hashing
    		// the result if it's > 256 bytes:
    		let signed_payload = SignedPayload::new(call_data.clone(), transaction_extensions.clone());

    		// Sign the signed payload with our Alice dev account's private key,
    		// and wrap the signature into the expected type:
    		let signature = {
    			let sig = Keyring::Alice.sign(&signed_payload.encode());
    			MultiSignature::Sr25519(sig)
    		};

    		// Now, we can build and encode our extrinsic:
    		let ext = Extrinsic::new_signed(call_data, from_address, signature, transaction_extensions);

    		let encoded_ext = ext.encode();
    		encoded_ext
    	}
    }

## Modules§

call_data
encoding_example