Collection of advice on how to review (and write) Substrate based runtimes.
- Fail fast (to reduce computation done on-chain).
- Verify any assumptions about the rest of the runtime made in your custom pallets and logic.
- Ensure math is explicit about how to handle overflows. (Using e.g.
checked_addorsaturating_addinstead of+, alternatively adding comments why a bare+is safe.) - Avoid panics (e.g.
unwrap) at almost any cost and add "proofs" forexpect()calls on why they cannot fail. The only reason to panic should be in case a block should not be built when that code path is triggered. - When reviewing runtime tests ensure that there's at least one
assume_noop!for each place in the code that returns an error.
- Make sure the origin is being checked (either none, root, signed or custom origin) even if the sender is not important.
Nonefor unsigned transactions.Rootfor privileged operation only executable by "the chain itself".Signed(account)for signed transactions by "regular users" of the chain.- custom origin when you want to combine the above or have custom origin logic (e.g. for extrinsics dispatched via XCM).
- Pay attention to efficient usage of storage (avoid duplicate keys, be aware of the trade offs of how often you want to read/write something).
- Make sure to keep atomicity in basically all cases. It's incredibly rare to want a non-atomic extrinsic. Ensure writes happen after reads/checks ("verify first, write last"). Alternatively: use
with_transactionor#[transactional]wrapper to roll back unwanted changes. - Make sure that hashers in storage are appropriate.
blake2_concatas secure defaulttwox_concatfor cheaper hashing, keys need to be safe (i.e. not user-controlled; e.g. continuous counter controlled by the chain)identityis cheapest, only use for already random (i.e. usually hashed) data (e.g. the hash of a utxo used as a key to store it)
- Watch out for anything that changes the storage layout -- sometimes it is more subtle than simply adding or removing a field to
decl_storage/#[pallet::storage]. Reasons the key or value might change:- Key: changing the name. Use
#[pallet::storage_prefix]renaming to avoid the underlying key changing. - Key: changing the hasher (e.g.
Blake2_128ConcattoTwox64Concat) - Key/Value: changing a type (that serializes differently) (e.g.
type MyKey = u16totype MyKey = u32) - Value: adding a field to a struct (e.g.
MyStruct(u8)toMyStruct(u8, u8))
- Key: changing the name. Use
- Avoid depending directly on other pallets and adhere to separation of concerns.
- Don't reorder the
Callenum. If you do, you need to bump the transaction version in the chains using the pallet.
- Set explicit indices in
construct_runtimeand avoid reordering the pallets as it will:- change the
Callindices (in case indices were not set explicitly) necessitating a transaction version bump. - change the execution order of hooks (e.g.
on_initialize) which can lead to subtle errors.
- change the
- Triple check every
validate_unsigned,pre_dispatchor customSignedExtensionto make sure it doesn't open up a DoS vector. - If there are any Offchain Workers, make sure the results they generate are validated on-chain (and not assumed to be valid).
- Limit the size of dynamic data passed into your dispatchables (e.g.
Vecs) via (constant) limits or economics. - Check that
on_finalizeweight is added toon_initializeweight (and both are determined correctly). Also keepon_finalizeas cheap as possible. Consider usingon_idlefor things you don't want to happen at the start of the block. - Benchmarks used to determine the weight need to measure the worst case. This can mean covering all relevant branches with a benchmark each.