Skip to content

Conversation

re-gius
Copy link

@re-gius re-gius commented Sep 9, 2025

Support genesis configuration customization for anvil-polkadot.
This is implemented through a wrapper struct around ChainSpec, called DevelopmentChainSpec, which re-implements assimilate_storage for the BuildStorage trait to override genesis storage for customizable items: chain id, block number, and timestamp.

For reference, also see #260 (comment)

Notice that there are still some missing genesis storage items to override: alloc (accounts with code/balances), coinbase (block author), gas settings.

@iulianbarbu iulianbarbu linked an issue Sep 15, 2025 that may be closed by this pull request
@re-gius re-gius marked this pull request as ready for review September 17, 2025 15:17
@re-gius
Copy link
Author

re-gius commented Sep 17, 2025

EDIT: actually, I don't see it's producing blocks, what I was seeing was probably the anvil config printed as string, not the genesis block storage. So I would say it's not working yet

@alindima
Copy link

alindima commented Sep 18, 2025

EDIT: actually, I don't see it's producing blocks, what I was seeing was probably the anvil config printed as string, not the genesis block storage. So I would say it's not working yet

After Alex's block production PR, we no longer start producing blocks by default. You either need to configure it via RPC or CLI. A simple way is to use the -b <block_interval> CLI param, which will produce a block every block_interval seconds

}

#[cfg(test)]
mod tests {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these tests are not really useful

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 2ff0438 to test for key-value storage translation, can they be useful now?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be using the hardcoded variants as defined in this module. After all, this is what we use in reality

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's also do this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e1abde2

@alindima
Copy link

I tried customising the genesis block number but without success. When querying the numer from polkadot.js, I get <unknown>. Could it be because you are encoding it as u64 instead of u32?

Anyway, setting it in the genesis state is not going to be enough. The block builder uses the block number from the header of the genesis block (not from the runtime state). I think we need to implement another GenesisBlockBuilder (by implementing BuildGenesisBlock on a newtype), which uses the configured block number. See the construct_genesis_block function, this is the place where we set the block number to 0.

@alindima
Copy link

And for timestamp, we also need to modify the timestamp of the mining engine (similar to what the setTime RPC is doing), so that the next blocks will be based on this timestamp (otherwise they'll just jump right back to the present moment on the next block). CC: @AlexandruCihodaru

@iulianbarbu
Copy link

iulianbarbu commented Sep 19, 2025

And for timestamp, we also need to modify the timestamp of the mining engine (similar to what the setTime RPC is doing), so that the next blocks will be based on this timestamp (otherwise they'll just jump right back to the present moment on the next block). CC: @AlexandruCihodaru

Yeh, looks like block 1 references current time. I don't think we're able to tell what timestamp does block 0 reference, with polkadotjs (no extrinsic shows up in it, like in block 1 case, and the header doesn't seem to show this info either). Would be nice. Not sure how subscan shows it for the production networks.

@iulianbarbu
Copy link

iulianbarbu commented Sep 19, 2025

Anyway, setting it in the genesis state is not going to be enough. The block builder uses the block number from the header of the genesis block (not from the runtime state).

I am thinking we should set the genesis block number in the genesis block header too. Not sure though how doable it is.
LE: maybe that means we'll not need to reimplement the genesis block builder. On the plus side the info shown in polkadotjs will be in accordance with how the node was started.

Copy link

@iulianbarbu iulianbarbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, nice work! Would be great to have also the chain id tested like Alin mentioned offline (which IIUC should be easily doable).

@re-gius re-gius self-assigned this Sep 29, 2025
let time_manager =
Arc::new(TimeManager::new_with_milliseconds(sp_timestamp::Timestamp::current().into()));
let time_manager = Arc::new(TimeManager::new_with_milliseconds(
sp_timestamp::Timestamp::from(anvil_config.get_genesis_timestamp()).into(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_genesis_timestamp will return the current time as seconds, but we create the TimeManager based on milliseconds.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to use --block-time 3 for 3 seconds block interval. The CLI and the behavior during testing corresponds. Somewhere a seconds to milliseconds conversion must be done I think.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is done, all parameters to RPCs are converted to milliseconds by the TimeManager methods.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Changed in 90da646

Copy link
Author

@re-gius re-gius Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

& fixed accordingly in 7221e82

Copy link

@alindima alindima left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Nice work!

Approving, modulo Alex's comment and let's make test_genesis less flaky in the CI

impl<'a> From<&'a AnvilNodeConfig> for GenesisConfig {
fn from(anvil_config: &'a AnvilNodeConfig) -> Self {
Self {
chain_id: anvil_config.get_chain_id(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also update standard_rpc::test_get_chain_id

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, we should properly implement the get_chain_id RPC to read the state instead of returning the hardcoded constant

let genesis_hash = node.block_hash_by_number(genesis_block_number).await.unwrap();
let actual_genesis_timestamp = node.get_decoded_timestamp(Some(genesis_hash)).await;
// Anvil genesis timestamp is in seconds, while Substrate timestamp is in milliseconds.
assert_eq!(actual_genesis_timestamp, genesis_timestamp * 1000);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use checked_mul

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in fcf200b


#[tokio::test(flavor = "multi_thread")]
async fn test_genesis() {
let genesis_block_number: u32 = 1000;
Copy link

@AlexandruCihodaru AlexandruCihodaru Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When creating the OnlineClient in create_revive_rpc_client

let api = OnlineClient::<SrcChainConfig>::from_rpc_client(rpc_client.clone()).await?;
there is this assumption that genesis block number is 0
https://github.com/paritytech/subxt/blob/884dfe6c69753584660f53352da636f9d0706631/rpcs/src/methods/legacy.rs#L89-L94

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a problem with this? Did you run into any issues?
If not, then that's fine. We shouldn't need to call this for our use case

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we set the genesis number to 1000 as in the test, we will not be able to create the OnlineClient because it calls the get_genesis from subxt that I linked above, which assumes that the genesis number is zero

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see. I think there's an easy workaround. Instead of calling OnlineClient::<SrcChainConfig>::from_rpc_client we should call from_rpc_client_with() and pass the genesis hash as returned by our local backend

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in fcf200b

// Anvil genesis timestamp is in seconds, while Substrate timestamp is in milliseconds.
timestamp: anvil_config.get_genesis_timestamp() * 1000,
alloc: anvil_config.genesis.as_ref().map(|g| g.alloc.clone()),
number: anvil_config.get_genesis_number() as u32,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could fail 🤷

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added expect in e1abde2

Self {
chain_id: anvil_config.get_chain_id(),
// Anvil genesis timestamp is in seconds, while Substrate timestamp is in milliseconds.
timestamp: anvil_config.get_genesis_timestamp() * 1000,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use checked_mul here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in e1abde2

.client
.storage(genesis_hash, &StorageKey(CODE_KEY.to_vec()))
.expect("Runtime code not found for given genesis hash")
.unwrap();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do proper error handling

let runtime_version = substrate_service
.client
.runtime_version_at(genesis_hash)
.expect("Runtime version not found for given genesis hash");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's propagate the error instead of expect

assert_eq!(current_chain_id, chain_id);

// Manually mine two blocks and force the timestamp to be increasing with 1 second each time.
unwrap_response::<()>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timestamp of the second block will include everything that happens from when the TimeManager is created until the inherent is created, so maybe take a larger tolerance in the assert

let time_manager =
Arc::new(TimeManager::new_with_milliseconds(sp_timestamp::Timestamp::current().into()));
let time_manager = Arc::new(TimeManager::new_with_milliseconds(
sp_timestamp::Timestamp::from(1000 * anvil_config.get_genesis_timestamp()).into(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: checked_mul

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in e1abde2

Comment on lines +450 to +455
let api = OnlineClient::<SrcChainConfig>::from_rpc_client_with(
genesis_hash,
subxt_runtime_version,
subxt_metadata,
rpc_client.clone(),
)?;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move the creation of the OnlineClient in a separate method

Copy link

@alindima alindima Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it belongs here, we're after all only creating this for the purposes of the revive rpc client

// Using best_hash because genesis hash is only set if genesis block number is equal to 0, but
// Anvil allows a custom genesis block number.
// https://github.com/paritytech/polkadot-sdk/blob/62c3a80f913a272c4f5dba2c91320056d39ec68e/substrate/client/api/src/in_mem.rs#L178
let genesis_hash = substrate_service.client.chain_info().best_hash;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably work in most cases but it's a bit racy.

The genesis hash here may be different if the node is configured to mine blocks very frequently, it could get to mine a block before we retrieve the first block's hash here.

The clean solution would be to retrieve the block hash of the genesis block number (which we know).
Let's do this instead

Copy link

@alindima alindima Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the genesis_hash set in from_rpc_client_with will probably need to be equal to the zeroed hash (which is what the client returns).
AFAIK the extrinsic payload contains the genesis hash as well (being constructed by subxt), so the runtime will reject the transaction if the hash is different.

Let's add a transaction to the integration test and see what happens and work from there

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I see tests failing in CI with:

called `Result::unwrap()` on an `Err` value: "Expected success but got error: RpcError { code: InternalError, message: \"Client error: RPC error: RPC error: client error: ErrorObject { code: ServerError(1010), message: \\\"Invalid Transaction\\\", data: Some(RawValue(\\\"Transaction call is not expected\\\")) }\", data: None }"

use anvil_polkadot::config::{AnvilNodeConfig, SubstrateNodeConfig};

#[tokio::test(flavor = "multi_thread")]
async fn test_genesis() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also add a test for configuring via json file, but this can come later, in the next PR if you want

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Anvil] Genesis customisation
4 participants