Substrate introduces some unique concepts such as unsigned transactions and signed extensions. What are the steps for encoding an extrinsic in Substrate? What are the specific details that relate to encoding signed transactions for chains that may use one or more signed extensions?
2 Answers
More generally, if you were to SCALE encode a signed transaction, you'd follow these steps:
- Combine the
call
,extra
andadditional
data and SCALE encode it:call
data: the pallet index, call index and other fields relevant to this call (e.g. if this is a transfer call from the Balances pallet, this would beaddress
andbalance
).extra
data (signed extensions): e.g. era, nonce and some tip value.additional
data: the spec version, transaction version and genesis hash of the target chain.
- If the resulting payload is more than 256 bytes, hash it then sign the result (e.g. using Alice). If not, just sign the SCALE encoded bytes from (1).
- Format the signature by combining the sender's account ID + the signature from (2) + the
extra
data. - Encode the formatted signature.
- Encode the
call
itself. - Construct the extrinsic by creating an array of bytes whereby:
- The first byte contains the first bit as 1 (denoting that the transaction is signed) and 7 bits containing the transaction version (currently 4 for all Substrate based chains).
- The remaining bytes contain the result from (4) and (5) (in that order).
- Prefix the encoded data from (6) with its compact encoded length.
Have a look at this example to see these steps in action. If you were to encode an unsigned transaction, you'd follow the same procedure except:
- The first bit of the first byte set in (6) is 0 (the bit following the compact length added in (7)), denoting that the transaction is unsigned.
- There are no signature bytes following this first byte encoded in (6); we append the call data from (5) next.
For encoding an extrinsic with different signed extensions, you'll need to know what they are and add them to the extra
data you SCALE encode.
A couple clarifications on key terms:
An unsigned transaction in Substrate is just a type of transaction which instead of requiring a signature, requires a set of conditions to be met in order for the transaction to be validated. This implies that developers must carefully implement their own protection mechanisms and economic deterrents against bad actors.
A signed extension is a way to extend the information packed into a transaction (signed or unsigned) so that the transaction pool can perform certain validation checks on it. Examples of these checks include: what's the weight for this transaction and will it fit in the block? Has this transaction already expired? Is the nonce for the same account unique? Is there a tip attached to this transaction? See the list of signed extensions in FRAME here.
As most Substrate builders are Rust natives, the best examples of extrinsic formation and submission can be found in subxt.
For your specific example, sp_runtime::traits::SignedExtension
can be implemented in a manner such as in subxt/src/extrinsic/extra.rs where SignedExtra
trait is implimented for various types like DefaultExtraWithTxPayment
.
The encoding is implemented in subxt/src/lib.rs quite simply using the codec Encode
/Decode
traits implemented on things like Call
set here:
/// Call trait.
pub trait Call: Encode {
/// Pallet name.
const PALLET: &'static str;
/// Function name.
const FUNCTION: &'static str;
/// Returns true if the given pallet and function names match this call.
fn is_call(pallet: &str, function: &str) -> bool {
Self::PALLET == pallet && Self::FUNCTION == function
}
}