referrerpolicy=no-referrer-when-downgrade

Module polkadot_sdk_docs::guides::enable_pov_reclaim

source ·
Expand description

How to enhance a given runtime and node to be cumulus-enabled, run it as a parachain and connect it to a relay-chain. How to enable storage weight reclaiming in a parachain node and runtime.

§Enable storage weight reclaiming

This guide will teach you how to enable storage weight reclaiming for a parachain. The explanations in this guide assume a project structure similar to the one detailed in the substrate documentation. Full technical details are available in the original pull request.

§What is PoV reclaim?

When a parachain submits a block to a relay chain like Polkadot or Kusama, it sends the block itself and a storage proof. Together they form the Proof-of-Validity (PoV). The PoV allows the relay chain to validate the parachain block by re-executing it. Relay chain validators distribute this PoV among themselves over the network. This distribution is costly and limits the size of the storage proof. The storage weight dimension of FRAME weights reflects this cost and limits the size of the storage proof. However, the storage weight determined during benchmarking represents the worst case. In reality, runtime operations often consume less space in the storage proof. PoV reclaim offers a mechanism to reclaim the difference between the benchmarked worst-case and the real proof-size consumption.

§How to enable PoV reclaim

§1. Add the host function to your node

To reclaim excess storage weight, a parachain runtime needs the ability to fetch the size of the storage proof from the node. The reclaim mechanism uses the storage_proof_size host function for this purpose. For convenience, cumulus provides ParachainHostFunctions, a set of host functions typically used by cumulus-based parachains. In the binary crate of your parachain, find the instantiation of the WasmExecutor and set the correct generic type.

This example from the parachain-template shows a type definition that includes the correct host functions.

type ParachainExecutor = WasmExecutor<ParachainHostFunctions>;

Note:

If you see error runtime requires function imports which are not present on the host: 'env:ext_storage_proof_size_storage_proof_size_version_1', it is likely that this step in the guide was not set up correctly.

§2. Enable storage proof recording during import

The reclaim mechanism reads the size of the currently recorded storage proof multiple times during block authoring and block import. Proof recording during authoring is already enabled on parachains. You must also ensure that storage proof recording is enabled during block import. Find where your node builds the fundamental substrate components by calling new_full_parts. Replace this with new_full_parts_record_import and pass true as the last parameter to enable import recording.

pub fn new_partial(config: &Configuration) -> Result<Service, sc_service::Error> {
	let telemetry = config
		.telemetry_endpoints
		.clone()
		.filter(|x| !x.is_empty())
		.map(|endpoints| -> Result<_, sc_telemetry::Error> {
			let worker = TelemetryWorker::new(16)?;
			let telemetry = worker.handle().new_telemetry(endpoints);
			Ok((worker, telemetry))
		})
		.transpose()?;

	let heap_pages = config
		.executor
		.default_heap_pages
		.map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |h| HeapAllocStrategy::Static { extra_pages: h as _ });

	let executor = ParachainExecutor::builder()
		.with_execution_method(config.executor.wasm_method)
		.with_onchain_heap_alloc_strategy(heap_pages)
		.with_offchain_heap_alloc_strategy(heap_pages)
		.with_max_runtime_instances(config.executor.max_runtime_instances)
		.with_runtime_cache_size(config.executor.runtime_cache_size)
		.build();

	let (client, backend, keystore_container, task_manager) =
		sc_service::new_full_parts_record_import::<Block, RuntimeApi, _>(
			config,
			telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()),
			executor,
			true,
		)?;
	let client = Arc::new(client);

	let telemetry_worker_handle = telemetry.as_ref().map(|(worker, _)| worker.handle());

	let telemetry = telemetry.map(|(worker, telemetry)| {
		task_manager.spawn_handle().spawn("telemetry", None, worker.run());
		telemetry
	});

	let transaction_pool = Arc::from(
		sc_transaction_pool::Builder::new(
			task_manager.spawn_essential_handle(),
			client.clone(),
			config.role.is_authority().into(),
		)
		.with_options(config.transaction_pool.clone())
		.with_prometheus(config.prometheus_registry())
		.build(),
	);

	let block_import = ParachainBlockImport::new(client.clone(), backend.clone());

	let import_queue = build_import_queue(
		client.clone(),
		block_import.clone(),
		config,
		telemetry.as_ref().map(|telemetry| telemetry.handle()),
		&task_manager,
	);

	Ok(PartialComponents {
		backend,
		client,
		import_queue,
		keystore_container,
		task_manager,
		transaction_pool,
		select_chain: (),
		other: (block_import, telemetry, telemetry_worker_handle),
	})
}

Note:

If you see error Storage root must match that calculated. during block import, it is likely that this step in the guide was not set up correctly.

§3. Add the TransactionExtension to your runtime

In your runtime, you will find a list of TransactionExtensions. To enable the reclaiming, add StorageWeightReclaim to that list. For maximum efficiency, make sure that StorageWeightReclaim is last in the list. The extension will check the size of the storage proof before and after an extrinsic execution. It reclaims the difference between the calculated size and the benchmarked size.

pub type TxExtension = (
	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>,
);

§Optional: Verify that reclaim works

Start your node with the log target runtime::storage_reclaim set to trace to enable full logging for StorageWeightReclaim. The following log is an example from a local testnet. To trigger the log, execute any extrinsic on the network.

...
2024-04-22 17:31:48.014 TRACE runtime::storage_reclaim: [ferdie] Reclaiming storage weight. benchmarked: 3593, consumed: 265 unspent: 0
...

In the above example we see a benchmarked size of 3593 bytes, while the extrinsic only consumed 265 bytes of proof size. This results in 3328 bytes of reclaim.