try-runtime

The try-runtime tool is built to query a snapshot of runtime storage, using an in-memory-externalities to store state. By using the in-memory storage, you can write tests for a specified runtime state that enables you to test against real chain state before going to production. With the try-runtime command-line interface, you can specify the block you want to query against.

In its simplest form, try-runtime is a tool that enables you to:

  1. Connect to a remote node.
  2. Call into some runtime API.
  3. Retrieve state from a node at a given block.
  4. Write tests for the data retrieved.

Motivation

The initial motivation for try-runtime came from the need to test runtime changes against state from a real chain. Prior TestExternalities and BasicExternalities existed for writing unit and integrated tests with mock data, but lacked the ability to test against a chain's actual state. The try-runtime tool extends TestExternalities and BasicExternalities by retrieving state using the following RPC endpoints for the node:

After using the key-value database to retrieve state, try-runtime inserts the data into TestExternalities.

How it works

The try-runtime tool has its own implementation of externalities called remote_externalities which is just a wrapper around TestExternalities that uses a generic key-value store where data is type encoded.

The diagram below illustrates the way externalities sits outside a compiled runtime as a means to capture the storage of that runtime.

Storage externalities

With remote_externalities, you can capture some chain state and run tests on it. Essentially, RemoteExternalities will populate a TestExternalities with a real chain's data.

Testing with externalities

To query state, try-runtime uses the RPC methods provided by StateApiClient. In particular:

  • storage This method returns the storage value for the key that represents the block you specify.
  • storage_key_paged This method returns the keys that match a prefix you specify with pagination support.

Usage

The most common use case for try-runtime is to help you prepare for storage migration and runtime upgrades.

Because the RPC calls that query storage are computationally expensive, there are a number of command-line options you should set for a running node before you use the try-runtime command. To prepare a node for try-runtime testing, set the following options:

  • Set --rpc-max-payload 1000 to ensure large RPC queries can work.
  • Set --rpc-cors all to ensure WebSocket connections can come through.

You can combine try-runtime with fork-off-substrate to test your chain before production. Use try-runtime to test your chain's migration and its pre and post states. Then, use fork-off-substrate if you want to check that block production continues after the migration.

Runtime upgrade hooks

By default, runtime upgrade hooks—which can be defined inside of the runtime or inside pallets—specify what should happen when there's been a runtime upgrade. That is, the default on_runtime_upgrade method only describes runtime state after the upgrade. However, it is possible to use methods provided by try-runtime to inspect and compare the runtime state before and after a runtime upgrade for testing purposes.

If you enable the try-runtime feature for the runtime, you can define pre-upgrade and post-upgrade hooks for the runtime as follows:

#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, &'static str> {
		Ok(Vec::new())
}

#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), &'static str> {
		Ok(())
}

With these function, you can use the pre_upgrade hook to retrieve the runtime state and return it as a Vec result. You can the pass the Vec as input parameter to the post_upgrade hook.

In addition to the pre_upgrade and post_upgrade methods, OnRuntimeUpgradeHelpersExt provides a set of helper functions to use with try-runtime for testing storage migrations. These helper functions include the following:

  • storage_key: Generates a storage key unique to this runtime upgrade. This can be used to communicate data from pre-upgrade to post-upgrade state and check them.
  • set_temp_storage Writes some temporary data to a specific storage that can be read (potentially in the post-upgrade hook).
  • get_temp_storage: Gets temporary storage data written by set_temp_storage.

Using the frame_executive::Executive struct, these helper functions in action would look like:

pub struct CheckTotalIssuance;
impl OnRuntimeUpgrade for CheckTotalIssuance {
	#[cfg(feature = "try-runtime")]
	fn post_upgrade() {
		// iterate over all accounts, sum their balance and ensure that sum is correct.
	}
}

pub struct EnsureAccountsWontDie;
impl OnRuntimeUpgrade for EnsureAccountsWontDie {
	#[cfg(feature = "try-runtime")]
	fn pre_upgrade() {
		let account_count = frame_system::Accounts::<Runtime>::iter().count();
		Self::set_temp_storage(account_count, "account_count");
	}

	#[cfg(feature = "try-runtime")]
	fn post_upgrade() {
		// ensure that this migration doesn't kill any account.
		let post_migration = frame_system::Accounts::<Runtime>::iter().count();
		let pre_migration = Self::get_temp_storage::<u32>("account_count");
		ensure!(post_migration == pre_migration, "error ...");
	}
}

pub type CheckerMigrations = (EnsureAccountsWontDie, CheckTotalIssuance);
pub type Executive = Executive<_, _, _, _, (CheckerMigrations)>;

Command-line examples

To use try-runtime from the command line, run your node with the --features=try-runtime flag. For example:

cargo run --release --features=try-runtime try-runtime

You can use the following subcommands with try-runtime:

  • on-runtime-upgrade: Executes tryRuntime_on_runtime_upgrade against the given runtime state.
  • offchain-worker: Executes offchainWorkerApi_offchain_worker against the given runtime state.
  • execute-block: Executes core_execute_block using the given block and the runtime state of the parent block.
  • follow-chain: Follows a given chain's finalized blocks and applies to all its extrinsics. This allows the behavior of a new runtime to be inspected over a long period of time, with real transactions coming as input.

To view usage information for a specific try-runtime subcommand, specify the subcommand and the --help flag. For example, to see usage information for try-runtime on-runtime-upgrade, you can run the following command:

cargo run --release --features=try-runtime try-runtime on-runtime-upgrade --help

For example, you can run try-runtime with the on-runtime-upgrade subcommand for a chain running locally with a command like this:

cargo run --release --features=try-runtime try-runtime on-runtime-upgrade live ws://localhost:9944

You can use try-runtime to re-execute code from the ElectionProviderMultiPhase offchain worker on localhost:9944 with a command like this:

cargo run -- --release \
   --features=try-runtime \
   try-runtime \
   --execution Wasm \
   --wasm-execution Compiled \
   offchain-worker \
   --header-at 0x491d09f313c707b5096650d76600f063b09835fd820e2916d3f8b0f5b45bec30 \
   live \
   -b 0x491d09f313c707b5096650d76600f063b09835fd820e2916d3f8b0f5b45bec30 \
   -m ElectionProviderMultiPhase \
   --uri wss://localhost:9944

You can run the migrations of the local runtime on the state of SomeChain with a command like this:

RUST_LOG=runtime=trace,try-runtime::cli=trace,executor=trace \
   cargo run try-runtime \
   --execution Native \
   --chain somechain-dev \
   on-runtime-upgrade \
   live \
   --uri wss://rpc.polkadot.io

You can run try-runtime against the state for a specific block number with a command like this:

RUST_LOG=runtime=trace,try-runtime::cli=trace,executor=trace \
   cargo run try-runtime \
   --execution Native \
   --chain dev \
   --no-spec-name-check \
   on-runtime-upgrade \
   live \
   --uri wss://rpc.polkadot.io \
   --at <block-hash>

Notice that this command requires the --no-spec-name-check command-line option.

Where to go next