Expand description

How to enable metadata hash verification in the runtime.

§Enable metadata hash verification

This guide will teach you how to enable the metadata hash verification in your runtime.

§What is metadata hash verification?

Each FRAME based runtime exposes metadata about itself. This metadata is used by consumers of the runtime to interpret the state, to construct transactions etc. Part of this metadata are the type information. These type information can be used to e.g. decode storage entries or to decode a transaction. So, the metadata is quite useful for wallets to interact with a FRAME based chain. Online wallets can fetch the metadata directly from any node of the chain they are connected to, but offline wallets can not do this. So, for the offline wallet to have access to the metadata it needs to be transferred and stored on the device. The problem is that the metadata has a size of several hundreds of kilobytes, which takes quite a while to transfer to these offline wallets and the internal storage of these devices is also not big enough to store the metadata for one or more networks. The next problem is that the offline wallet/user can not trust the metadata to be correct. It is very important for the metadata to be correct or otherwise an attacker could change them in a way that the offline wallet decodes a transaction in a different way than what it will be decoded to on chain. So, the user may sign an incorrect transaction leading to unexpected behavior.

The metadata hash verification circumvents the issues of the huge metadata and the need to trust some metadata blob to be correct. To generate a hash for the metadata, the metadata is chunked, these chunks are put into a merkle tree and then the root of this merkle tree is the “metadata hash”. For a more technical explanation on how it works, see RFC78. At compile time the metadata hash is generated and “baked” into the runtime. This makes it extremely cheap for the runtime to verify on chain that the metadata hash is correct. By having the runtime verify the hash on chain, the user also doesn’t need to trust the offchain metadata. If the metadata hash doesn’t match the on chain metadata hash the transaction will be rejected. The metadata hash itself is added to the data of the transaction that is signed, this means the actual hash does not appear in the transaction. On chain the same procedure is repeated with the metadata hash that is known by the runtime and if the metadata hash doesn’t match the signature verification will fail. As the metadata hash is actually the root of a merkle tree, the offline wallet can get proofs of individual types to decode a transaction. This means that the offline wallet does not require the entire metadata to be present on the device.

§Integrating metadata hash verification into your runtime

The integration of the metadata hash verification is split into two parts, first the actual integration into the runtime and secondly the enabling of the metadata hash generation at compile time.

§Runtime integration

From the runtime side only the CheckMetadataHash needs to be added to the list of signed extension:

pub type SignedExtra = (
	frame_system::CheckNonZeroSender<Runtime>,
	frame_system::CheckSpecVersion<Runtime>,
	frame_system::CheckTxVersion<Runtime>,
	frame_system::CheckGenesis<Runtime>,
	frame_system::CheckEra<Runtime>,
	frame_system::CheckNonce<Runtime>,
	frame_system::CheckWeight<Runtime>,
	pallet_transaction_payment::ChargeTransactionPayment<Runtime>,
	cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim<Runtime>,
	frame_metadata_hash_extension::CheckMetadataHash<Runtime>,
);

Note:

Adding the signed extension changes the encoding of the transaction and adds one extra byte per transaction!

This signed extension will make sure to decode the requested mode and will add the metadata hash to the signed data depending on the requested mode. The mode gives the user/wallet control over deciding if the metadata hash should be verified or not. The metadata hash itself is drawn from the RUNTIME_METADATA_HASH environment variable. If the environment variable is not set, any transaction that requires the metadata hash is rejected with the error CannotLookup. This is a security measurement to prevent including invalid transactions.

The extension does not work with the native runtime, because the RUNTIME_METADATA_HASH environment variable is not set when building the frame-metadata-hash-extension crate.

§Enable metadata hash generation

The metadata hash generation needs to be enabled when building the wasm binary. The substrate-wasm-builder supports this out of the box:

#[cfg(all(feature = "std", feature = "metadata-hash"))]
fn main() {
	substrate_wasm_builder::WasmBuilder::init_with_defaults()
		.enable_metadata_hash("UNIT", 12)
		.build();
}

Note:

The metadata-hash feature needs to be enabled for the substrate-wasm-builder to enable the code for being able to generate the metadata hash. It is also recommended to put the metadata hash generation behind a feature in the runtime as shown above. The reason behind is that it adds a lot of code which increases the compile time and the generation itself also increases the compile time. Thus, it is recommended to enable the feature only when the metadata hash is required (e.g. for an on-chain build).

The two parameters to enable_metadata_hash are the token symbol and the number of decimals of the primary token of the chain. These information are included for the wallets to show token related operations in a more user friendly way.