Skip to content

Commit e930184

Browse files
authored
Implementanvil_nodeInfo and anvil_metadata (paritytech#364)
* draft implementation of `anvil_nodeInfo` and `anvil_metadata` * clippy * import nit * Implement thread-safe snapshot getter * fix + add tests * CR changes
1 parent 9083e9c commit e930184

File tree

5 files changed

+196
-10
lines changed

5 files changed

+196
-10
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/anvil-polkadot/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ subxt-signer = "0.43.0"
144144
tokio-stream = "0.1.17"
145145
jsonrpsee = "0.24.9"
146146
sqlx = "0.8.6"
147+
revm.workspace = true
147148

148149
[dev-dependencies]
149150
alloy-provider = { workspace = true, features = ["txpool-api"] }

crates/anvil-polkadot/src/api_server/server.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ use crate::{
2727
use alloy_dyn_abi::TypedData;
2828
use alloy_eips::{BlockId, BlockNumberOrTag};
2929
use alloy_primitives::{Address, B256, U64, U256};
30-
use alloy_rpc_types::{Filter, TransactionRequest, anvil::MineOptions, txpool::TxpoolStatus};
30+
use alloy_rpc_types::{
31+
Filter, TransactionRequest,
32+
anvil::{Metadata as AnvilMetadata, MineOptions, NodeEnvironment, NodeInfo},
33+
txpool::TxpoolStatus,
34+
};
3135
use alloy_serde::WithOtherFields;
3236
use alloy_trie::{EMPTY_ROOT_HASH, KECCAK_EMPTY, TrieAccount};
3337
use anvil_core::eth::{EthRequest, Params as MineParams};
@@ -52,12 +56,13 @@ use polkadot_sdk::{
5256
polkadot_sdk_frame::runtime::types_common::OpaqueBlock,
5357
sc_client_api::HeaderBackend,
5458
sc_service::{InPoolTransaction, SpawnTaskHandle, TransactionPool},
55-
sp_api::{Metadata, ProvideRuntimeApi},
59+
sp_api::{Metadata as _, ProvideRuntimeApi},
5660
sp_arithmetic::Permill,
5761
sp_blockchain::Info,
5862
sp_core::{self, Hasher, keccak_256},
5963
sp_runtime::traits::BlakeTwo256,
6064
};
65+
use revm::primitives::hardfork::SpecId;
6166
use sqlx::sqlite::SqlitePoolOptions;
6267
use std::{collections::HashSet, sync::Arc, time::Duration};
6368
use substrate_runtime::{Balance, RuntimeCall, UncheckedExtrinsic};
@@ -84,6 +89,7 @@ pub struct ApiServer {
8489
snapshot_manager: SnapshotManager,
8590
impersonation_manager: ImpersonationManager,
8691
tx_pool: Arc<TransactionPoolHandle>,
92+
instance_id: B256,
8793
}
8894

8995
impl ApiServer {
@@ -122,6 +128,7 @@ impl ApiServer {
122128
impersonation_manager,
123129
tx_pool: substrate_service.tx_pool.clone(),
124130
wallet: DevSigner::new(signers)?,
131+
instance_id: B256::random(),
125132
})
126133
}
127134

@@ -344,6 +351,9 @@ impl ApiServer {
344351
node_info!("anvil_dropTransaction");
345352
self.anvil_drop_transaction(eth_hash).await.to_rpc_result()
346353
}
354+
// --- Metadata ---
355+
EthRequest::NodeInfo(_) => self.anvil_node_info().await.to_rpc_result(),
356+
EthRequest::AnvilMetadata(_) => self.anvil_metadata().await.to_rpc_result(),
347357
_ => Err::<(), _>(Error::RpcUnimplemented).to_rpc_result(),
348358
};
349359

@@ -749,6 +759,69 @@ impl ApiServer {
749759
Ok(())
750760
}
751761

762+
async fn anvil_node_info(&self) -> Result<NodeInfo> {
763+
node_info!("anvil_nodeInfo");
764+
765+
let best_hash = self.latest_block();
766+
let Some(current_block) =
767+
self.get_block_by_hash(B256::from_slice(best_hash.as_ref()), false).await?
768+
else {
769+
return Err(Error::InternalError("Latest block not found".to_string()));
770+
};
771+
let current_block_number: u64 =
772+
current_block.number.try_into().map_err(|_| EthRpcError::ConversionError)?;
773+
let current_block_timestamp: u64 =
774+
current_block.timestamp.try_into().map_err(|_| EthRpcError::ConversionError)?;
775+
// This is both gas price and base fee, since pallet-revive does not support tips
776+
// https://github.com/paritytech/polkadot-sdk/blob/227c73b5c8810c0f34e87447f00e96743234fa52/substrate/frame/revive/rpc/src/lib.rs#L269
777+
let base_fee: u128 =
778+
current_block.base_fee_per_gas.try_into().map_err(|_| EthRpcError::ConversionError)?;
779+
let gas_limit: u64 = current_block.gas_limit.try_into().unwrap_or(u64::MAX);
780+
// pallet-revive should currently support all opcodes in PRAGUE.
781+
let hard_fork: &str = SpecId::PRAGUE.into();
782+
783+
Ok(NodeInfo {
784+
current_block_number,
785+
current_block_timestamp,
786+
current_block_hash: B256::from_slice(best_hash.as_ref()),
787+
hard_fork: hard_fork.to_string(),
788+
// pallet-revive does not support tips
789+
transaction_order: "fifo".to_string(),
790+
environment: NodeEnvironment {
791+
base_fee,
792+
chain_id: self.chain_id(best_hash),
793+
gas_limit,
794+
gas_price: base_fee,
795+
},
796+
// Forking is not supported yet in anvil-polkadot
797+
fork_config: Default::default(),
798+
})
799+
}
800+
801+
async fn anvil_metadata(&self) -> Result<AnvilMetadata> {
802+
node_info!("anvil_metadata");
803+
804+
let best_hash = self.latest_block();
805+
let Some(latest_block) =
806+
self.get_block_by_hash(B256::from_slice(best_hash.as_ref()), false).await?
807+
else {
808+
return Err(Error::InternalError("Latest block not found".to_string()));
809+
};
810+
let latest_block_number: u64 =
811+
latest_block.number.try_into().map_err(|_| EthRpcError::ConversionError)?;
812+
813+
Ok(AnvilMetadata {
814+
client_version: CLIENT_VERSION.to_string(),
815+
chain_id: self.chain_id(best_hash),
816+
latest_block_hash: B256::from_slice(best_hash.as_ref()),
817+
latest_block_number,
818+
instance_id: self.instance_id,
819+
// Forking is not supported yet in anvil-polkadot
820+
forked_network: None,
821+
snapshots: self.snapshot_manager.list_snapshots(),
822+
})
823+
}
824+
752825
async fn get_block_transaction_count_by_hash(&self, block_hash: B256) -> Result<Option<U256>> {
753826
let block_hash = H256::from_slice(block_hash.as_slice());
754827
Ok(self.eth_rpc_client.receipts_count_per_block(&block_hash).await.map(U256::from))

crates/anvil-polkadot/src/substrate_node/snapshot.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use crate::substrate_node::service::{Backend, Client};
2-
use alloy_primitives::U256;
2+
use alloy_primitives::{B256, U256};
33
use polkadot_sdk::{
44
polkadot_sdk_frame::runtime::types_common::OpaqueBlock,
55
sc_client_api::Backend as BackendT,
66
sp_blockchain::{HeaderBackend, Info, Result},
77
};
88
use std::{collections::BTreeMap, sync::Arc};
99

10-
type Snapshot = u64;
10+
// The snapshot contains the block number and the block hash
11+
type Snapshot = (u64, B256);
1112

1213
pub struct RevertInfo {
1314
pub info: Info<OpaqueBlock>,
@@ -32,25 +33,26 @@ impl SnapshotManager {
3233
pub fn snapshot(&mut self) -> U256 {
3334
let current_snapshot_id = self.next_snapshot_id;
3435
self.next_snapshot_id += U256::ONE;
35-
let snapshot = self.client.info().best_number.into();
36-
self.snapshots.insert(current_snapshot_id, snapshot);
36+
let block_number = self.client.info().best_number.into();
37+
let block_hash = B256::from_slice(self.client.info().best_hash.as_ref());
38+
self.snapshots.insert(current_snapshot_id, (block_number, block_hash));
3739
current_snapshot_id
3840
}
3941

4042
/// Revert the chain to the block number represented by the snapshot `id`.
4143
pub fn revert(&mut self, snapshot_id: U256) -> Result<Option<RevertInfo>> {
4244
let maybe_snapshot = self.snapshots.remove(&snapshot_id);
43-
let Some(snap) = maybe_snapshot else {
45+
let Some((snapshot_block_number, _)) = maybe_snapshot else {
4446
return Ok(None);
4547
};
4648

4749
let current_best_number: u64 = self.client.info().best_number.into();
48-
let number_of_blocks_to_revert = current_best_number - snap;
50+
let number_of_blocks_to_revert = current_best_number - snapshot_block_number;
4951

5052
let (reverted, _) =
5153
self.backend.revert(number_of_blocks_to_revert.try_into().unwrap_or(u32::MAX), true)?;
5254

53-
self.snapshots.retain(|_, snap_to_remove| *snap_to_remove < snap);
55+
self.snapshots.retain(|_, (snap_to_remove, _)| *snap_to_remove < snapshot_block_number);
5456

5557
Ok(Some(RevertInfo { reverted: reverted.into(), info: self.client.info() }))
5658
}
@@ -61,4 +63,8 @@ impl SnapshotManager {
6163
self.backend.revert(depth.unwrap_or(1).try_into().unwrap_or(u32::MAX), true)?;
6264
Ok(RevertInfo { reverted: reverted.into(), info: self.client.info() })
6365
}
66+
67+
pub fn list_snapshots(&self) -> BTreeMap<U256, (u64, B256)> {
68+
self.snapshots.clone()
69+
}
6470
}

crates/anvil-polkadot/tests/it/standard_rpc.rs

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ use crate::{
88
},
99
};
1010
use alloy_primitives::{Address, B256, Bytes, U256, map::HashSet};
11-
use alloy_rpc_types::{Index, TransactionInput, TransactionRequest};
11+
use alloy_rpc_types::{
12+
Index, TransactionInput, TransactionRequest,
13+
anvil::{Metadata as AnvilMetadata, NodeInfo},
14+
};
1215
use alloy_serde::WithOtherFields;
1316
use alloy_sol_types::{SolCall, SolEvent};
1417
use anvil_core::eth::EthRequest;
@@ -894,3 +897,105 @@ async fn test_coinbase() {
894897
let coinbase = multicall_get_coinbase(&mut node, alith_addr, contract_address).await;
895898
assert_eq!(coinbase, new_coinbase);
896899
}
900+
901+
#[tokio::test(flavor = "multi_thread")]
902+
async fn test_anvil_node_info() {
903+
let anvil_node_config = AnvilNodeConfig::test_config();
904+
let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config);
905+
let mut node = TestNode::new(anvil_node_config.clone(), substrate_node_config).await.unwrap();
906+
907+
let node_info =
908+
unwrap_response::<NodeInfo>(node.eth_rpc(EthRequest::NodeInfo(())).await.unwrap()).unwrap();
909+
910+
// Check initial state - should be at genesis block
911+
assert_eq!(node_info.current_block_number, 0);
912+
assert_eq!(node_info.hard_fork, "Prague".to_string());
913+
assert_eq!(node_info.transaction_order, "fifo");
914+
assert_eq!(node_info.environment.chain_id, 0x7a69);
915+
916+
// Verify fork config is empty (forking not supported)
917+
assert_eq!(node_info.fork_config.fork_url, None);
918+
assert_eq!(node_info.fork_config.fork_block_number, None);
919+
assert_eq!(node_info.fork_config.fork_retry_backoff, None);
920+
921+
let genesis_block_hash = node.block_hash_by_number(0).await.unwrap();
922+
assert_eq!(node_info.current_block_hash, B256::from_slice(genesis_block_hash.as_ref()));
923+
let block = node.get_block_by_hash(genesis_block_hash).await;
924+
assert_eq!(block.gas_limit, node_info.environment.gas_limit.into());
925+
assert_eq!(block.base_fee_per_gas, node_info.environment.base_fee.into());
926+
assert_eq!(block.base_fee_per_gas, node_info.environment.gas_price.into());
927+
928+
// Mine some blocks and check that node_info updates
929+
unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(Some(U256::from(3)), None)).await.unwrap())
930+
.unwrap();
931+
tokio::time::sleep(Duration::from_millis(400)).await;
932+
933+
let node_info_after =
934+
unwrap_response::<NodeInfo>(node.eth_rpc(EthRequest::NodeInfo(())).await.unwrap()).unwrap();
935+
936+
// Block number should have increased
937+
assert_eq!(node_info_after.current_block_number, 3);
938+
939+
// Timestamp should be greater or equal (may have advanced)
940+
assert!(node_info_after.current_block_timestamp >= node_info.current_block_timestamp);
941+
assert_eq!(node_info_after.environment.chain_id, node_info.environment.chain_id);
942+
}
943+
944+
#[tokio::test(flavor = "multi_thread")]
945+
async fn test_anvil_metadata() {
946+
let anvil_node_config = AnvilNodeConfig::test_config();
947+
let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config);
948+
let mut node = TestNode::new(anvil_node_config.clone(), substrate_node_config).await.unwrap();
949+
950+
let metadata = unwrap_response::<AnvilMetadata>(
951+
node.eth_rpc(EthRequest::AnvilMetadata(())).await.unwrap(),
952+
)
953+
.unwrap();
954+
955+
assert!(metadata.client_version.contains("anvil-polkadot"));
956+
assert_eq!(metadata.latest_block_number, 0);
957+
assert_eq!(metadata.chain_id, 0x7a69);
958+
959+
// Verify forked_network is None (forking not supported)
960+
assert_eq!(metadata.forked_network, None);
961+
962+
// Initial snapshots should be empty
963+
assert!(metadata.snapshots.is_empty());
964+
965+
// Get current block hash for comparison
966+
let block_hash = node.block_hash_by_number(0).await.unwrap();
967+
assert_eq!(metadata.latest_block_hash, B256::from_slice(block_hash.as_ref()));
968+
969+
// Create a snapshot and verify it appears in metadata
970+
let snapshot_id = U256::from_str_radix(
971+
unwrap_response::<String>(node.eth_rpc(EthRequest::EvmSnapshot(())).await.unwrap())
972+
.unwrap()
973+
.trim_start_matches("0x"),
974+
16,
975+
)
976+
.unwrap();
977+
978+
let metadata_after_snapshot = unwrap_response::<AnvilMetadata>(
979+
node.eth_rpc(EthRequest::AnvilMetadata(())).await.unwrap(),
980+
)
981+
.unwrap();
982+
983+
// Should have one snapshot
984+
assert_eq!(metadata_after_snapshot.snapshots.len(), 1);
985+
assert!(metadata_after_snapshot.snapshots.contains_key(&snapshot_id));
986+
987+
// Mine some blocks and check that metadata updates
988+
unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(Some(U256::from(5)), None)).await.unwrap())
989+
.unwrap();
990+
tokio::time::sleep(Duration::from_millis(400)).await;
991+
992+
let metadata_after_mining = unwrap_response::<AnvilMetadata>(
993+
node.eth_rpc(EthRequest::AnvilMetadata(())).await.unwrap(),
994+
)
995+
.unwrap();
996+
997+
// Block number should have increased
998+
assert_eq!(metadata_after_mining.latest_block_number, 5);
999+
// Snapshot should still be present
1000+
assert!(metadata_after_mining.snapshots.contains_key(&snapshot_id));
1001+
}

0 commit comments

Comments
 (0)