Skip to content

Commit 4a0ef1c

Browse files
committed
Add initial forking integration tests
1 parent fc730dd commit 4a0ef1c

File tree

3 files changed

+256
-0
lines changed

3 files changed

+256
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
use std::time::Duration;
2+
3+
use crate::utils::{TestNode, unwrap_response};
4+
use alloy_primitives::{Address, U256};
5+
use alloy_rpc_types::TransactionRequest;
6+
use anvil_core::eth::EthRequest;
7+
use anvil_polkadot::{
8+
api_server::revive_conversions::ReviveAddress, config::{AnvilNodeConfig, SubstrateNodeConfig},
9+
};
10+
use polkadot_sdk::pallet_revive::evm::Account;
11+
12+
/// Tests that forking preserves state from the source chain and allows local modifications
13+
#[tokio::test(flavor = "multi_thread")]
14+
async fn test_fork_preserves_state_and_allows_modifications() {
15+
// Step 1: Create the "source" node
16+
let source_config = AnvilNodeConfig::test_config();
17+
let source_substrate_config = SubstrateNodeConfig::new(&source_config);
18+
let mut source_node =
19+
TestNode::new(source_config.clone(), source_substrate_config).await.unwrap();
20+
21+
let source_substrate_rpc_port = source_node.substrate_rpc_port();
22+
23+
tokio::time::sleep(Duration::from_millis(1000)).await;
24+
25+
let alith = Account::from(subxt_signer::eth::dev::alith());
26+
let baltathar = Account::from(subxt_signer::eth::dev::baltathar());
27+
let alith_address = ReviveAddress::new(alith.address());
28+
let baltathar_address = ReviveAddress::new(baltathar.address());
29+
30+
// Step 2: Perform a transaction on the source node
31+
let initial_alith_balance = source_node.get_balance(alith.address(), None).await;
32+
let initial_baltathar_balance = source_node.get_balance(baltathar.address(), None).await;
33+
34+
let transfer_amount = U256::from_str_radix("5000000000000000000", 10).unwrap(); // 5 ether
35+
let transaction = TransactionRequest::default()
36+
.value(transfer_amount)
37+
.from(Address::from(alith_address))
38+
.to(Address::from(baltathar_address));
39+
40+
source_node.send_transaction(transaction, None).await.unwrap();
41+
unwrap_response::<()>(source_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap())
42+
.unwrap();
43+
tokio::time::sleep(Duration::from_millis(500)).await;
44+
45+
// Verify the transfer happened on source node
46+
let source_alith_balance = source_node.get_balance(alith.address(), None).await;
47+
let source_baltathar_balance = source_node.get_balance(baltathar.address(), None).await;
48+
49+
assert!(
50+
source_alith_balance < initial_alith_balance, // This is to not check the gas fee
51+
"Alith balance should decrease on source node"
52+
);
53+
assert_eq!(
54+
source_baltathar_balance,
55+
initial_baltathar_balance + transfer_amount,
56+
"Baltathar should receive 5 ether on source node"
57+
);
58+
59+
// Step 3: Create a forked node pointing to the source node's Substrate RPC
60+
let source_rpc_url = format!("http://127.0.0.1:{}", source_substrate_rpc_port);
61+
62+
let fork_config = AnvilNodeConfig::test_config()
63+
.with_port(0)
64+
.with_eth_rpc_url(Some(source_rpc_url));
65+
66+
let fork_substrate_config = SubstrateNodeConfig::new(&fork_config);
67+
let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap();
68+
69+
// Step 4: Verify the forked node has the same balances as the source node at fork point
70+
let fork_alith_balance = fork_node.get_balance(alith.address(), None).await;
71+
let fork_baltathar_balance = fork_node.get_balance(baltathar.address(), None).await;
72+
73+
assert_eq!(
74+
fork_alith_balance, source_alith_balance,
75+
"Forked node should have same Alith balance as source"
76+
);
77+
assert_eq!(
78+
fork_baltathar_balance, source_baltathar_balance,
79+
"Forked node should have same Baltathar balance as source"
80+
);
81+
82+
// Step 5: Perform a transaction on the forked node (send 1 ether)
83+
let fork_transfer_amount = U256::from_str_radix("1000000000000000000", 10).unwrap(); // 1 ether
84+
let fork_transaction = TransactionRequest::default()
85+
.value(fork_transfer_amount)
86+
.from(Address::from(alith_address))
87+
.to(Address::from(baltathar_address));
88+
89+
fork_node.send_transaction(fork_transaction, None).await.unwrap();
90+
unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap();
91+
tokio::time::sleep(Duration::from_millis(500)).await;
92+
93+
// Step 6: Verify the transaction affected only the forked node
94+
let fork_alith_balance_after = fork_node.get_balance(alith.address(), None).await;
95+
let fork_baltathar_balance_after = fork_node.get_balance(baltathar.address(), None).await;
96+
97+
assert!(
98+
fork_alith_balance_after < fork_alith_balance,
99+
"Alith balance should decrease on forked node"
100+
);
101+
assert_eq!(
102+
fork_baltathar_balance_after,
103+
fork_baltathar_balance + fork_transfer_amount,
104+
"Baltathar should receive 1 ether on forked node"
105+
);
106+
107+
// Step 7: Verify the source node was NOT affected by the forked node's transaction
108+
let source_alith_balance_final = source_node.get_balance(alith.address(), None).await;
109+
let source_baltathar_balance_final = source_node.get_balance(baltathar.address(), None).await;
110+
111+
assert_eq!(
112+
source_alith_balance_final, source_alith_balance,
113+
"Source node Alith balance should remain unchanged"
114+
);
115+
assert_eq!(
116+
source_baltathar_balance_final, source_baltathar_balance,
117+
"Source node Baltathar balance should remain unchanged"
118+
);
119+
}
120+
121+
/// Tests that forking creates a new chain starting from the latest finalized block of the source chain
122+
#[tokio::test(flavor = "multi_thread")]
123+
async fn test_fork_from_latest_finalized_block() {
124+
const BLOCKS_TO_MINE: u32 = 5;
125+
// Step 1: Create the "source" node
126+
let source_config = AnvilNodeConfig::test_config();
127+
let source_substrate_config = SubstrateNodeConfig::new(&source_config);
128+
let mut source_node =
129+
TestNode::new(source_config.clone(), source_substrate_config).await.unwrap();
130+
131+
let source_substrate_rpc_port = source_node.substrate_rpc_port();
132+
133+
tokio::time::sleep(Duration::from_millis(1000)).await;
134+
135+
let alith = Account::from(subxt_signer::eth::dev::alith());
136+
let baltathar = Account::from(subxt_signer::eth::dev::baltathar());
137+
let alith_address = ReviveAddress::new(alith.address());
138+
let baltathar_address = ReviveAddress::new(baltathar.address());
139+
140+
// Step 2: Mine several blocks on the source node to create history
141+
let transfer_amount = U256::from_str_radix("1000000000000000000", 10).unwrap(); // 1 ether
142+
143+
// Mine 3 blocks with transfers
144+
for _ in 0..BLOCKS_TO_MINE {
145+
let transaction = TransactionRequest::default()
146+
.value(transfer_amount)
147+
.from(Address::from(alith_address))
148+
.to(Address::from(baltathar_address));
149+
source_node.send_transaction(transaction, None).await.unwrap();
150+
unwrap_response::<()>(source_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap())
151+
.unwrap();
152+
tokio::time::sleep(Duration::from_millis(500)).await;
153+
}
154+
155+
// Get the current block number from source (this will be the fork point)
156+
let source_best_block = source_node.best_block_number().await;
157+
let source_best_block_hash = source_node.eth_block_hash_by_number(source_best_block).await.unwrap();
158+
159+
// Verify the source node has mined the correct number of blocks
160+
assert_eq!(
161+
source_best_block, BLOCKS_TO_MINE,
162+
"Source node should have mined 5 blocks"
163+
);
164+
165+
// Step 3: Create a forked node from the latest finalized block of the source chain
166+
let source_rpc_url = format!("http://127.0.0.1:{}", source_substrate_rpc_port);
167+
168+
let fork_config = AnvilNodeConfig::test_config()
169+
.with_eth_rpc_url(Some(source_rpc_url));
170+
171+
let fork_substrate_config = SubstrateNodeConfig::new(&fork_config);
172+
let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap();
173+
174+
// Wait for fork node to be ready
175+
tokio::time::sleep(Duration::from_millis(1000)).await;
176+
177+
// Step 4: Verify the forked node starts from the same block as source best block
178+
let fork_initial_block_number = fork_node.best_block_number().await;
179+
assert_eq!(
180+
fork_initial_block_number, source_best_block,
181+
"Forked node should start from source's latest finalized block"
182+
);
183+
184+
// Step 5: Verify the genesis block hash of the fork matches the source
185+
let fork_genesis_hash = fork_node.eth_block_hash_by_number(fork_initial_block_number).await.unwrap();
186+
assert_eq!(
187+
fork_genesis_hash, source_best_block_hash,
188+
"Forked node's initial block hash should match source block hash"
189+
);
190+
191+
// Step 6: Mine a new block on the forked node
192+
let fork_transaction = TransactionRequest::default()
193+
.value(transfer_amount)
194+
.from(Address::from(alith_address))
195+
.to(Address::from(baltathar_address));
196+
fork_node.send_transaction(fork_transaction, None).await.unwrap();
197+
unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap();
198+
tokio::time::sleep(Duration::from_millis(500)).await;
199+
200+
// Step 7: Verify the forked node advanced by 1 block
201+
let fork_current_block = fork_node.best_block_number().await;
202+
assert_eq!(
203+
fork_current_block,
204+
fork_initial_block_number + 1,
205+
"Forked node should have advanced by 1 block after mining"
206+
);
207+
208+
// Step 8: Verify that the source node is still at the same block (unchanged by fork)
209+
let source_current_block = source_node.best_block_number().await;
210+
assert_eq!(
211+
source_current_block, source_best_block,
212+
"Source node should remain at the same block"
213+
);
214+
215+
// Step 9: Verify the block hashes diverged after the fork point
216+
let fork_new_block_hash = fork_node.eth_block_hash_by_number(fork_current_block).await.unwrap();
217+
218+
// Mine one more block on source to create a divergence
219+
let source_transaction = TransactionRequest::default()
220+
.value(transfer_amount)
221+
.from(Address::from(alith_address))
222+
.to(Address::from(baltathar_address));
223+
source_node.send_transaction(source_transaction, None).await.unwrap();
224+
unwrap_response::<()>(source_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap();
225+
tokio::time::sleep(Duration::from_millis(500)).await;
226+
227+
let source_new_block = source_node.best_block_number().await;
228+
let source_new_block_hash = source_node.eth_block_hash_by_number(source_new_block).await.unwrap();
229+
230+
// Both chains should be at the same height now
231+
assert_eq!(source_new_block, fork_current_block, "Both chains should be at the same height");
232+
233+
// But the hashes should be different (chains diverged)
234+
assert_ne!(
235+
fork_new_block_hash, source_new_block_hash,
236+
"Block hashes should be different (chains diverged after fork point)"
237+
);
238+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod abi;
2+
mod forking;
23
mod genesis;
34
mod impersonation;
45
mod mining;

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,23 @@ impl TestNode {
203203
u32::from_str_radix(num.trim_start_matches("0x"), 16).unwrap()
204204
}
205205

206+
pub fn substrate_rpc_port(&self) -> u16 {
207+
self.service
208+
.rpc_handlers
209+
.listen_addresses()
210+
.first()
211+
.and_then(|addr| {
212+
addr.iter().find_map(|protocol| {
213+
if let polkadot_sdk::sc_network_types::multiaddr::Protocol::Tcp(port) = protocol {
214+
Some(port)
215+
} else {
216+
None
217+
}
218+
})
219+
})
220+
.expect("Failed to get Substrate RPC port")
221+
}
222+
206223
pub async fn wait_for_block_with_timeout(
207224
&self,
208225
n: u32,

0 commit comments

Comments
 (0)