Skip to content

Commit fbfd9bc

Browse files
feat(anvil): implement anvil_rollback (#9783)
* implement anvil_rollback * PR improvements --------- Co-authored-by: grandizzy <[email protected]>
1 parent 1904216 commit fbfd9bc

File tree

4 files changed

+94
-23
lines changed

4 files changed

+94
-23
lines changed

crates/anvil/core/src/eth/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,10 @@ pub enum EthRequest {
777777
#[cfg_attr(feature = "serde", serde(rename = "anvil_reorg",))]
778778
Reorg(ReorgOptions),
779779

780+
/// Rollback the chain
781+
#[cfg_attr(feature = "serde", serde(rename = "anvil_rollback",))]
782+
Rollback(Option<u64>),
783+
780784
/// Wallet
781785
#[cfg_attr(feature = "serde", serde(rename = "wallet_getCapabilities", with = "empty_params"))]
782786
WalletGetCapabilities(()),

crates/anvil/src/eth/api.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ impl EthApi {
456456
EthRequest::Reorg(reorg_options) => {
457457
self.anvil_reorg(reorg_options).await.to_rpc_result()
458458
}
459+
EthRequest::Rollback(depth) => self.anvil_rollback(depth).await.to_rpc_result(),
459460
EthRequest::WalletGetCapabilities(()) => self.get_capabilities().to_rpc_result(),
460461
EthRequest::WalletSendTransaction(tx) => {
461462
self.wallet_send_transaction(*tx).await.to_rpc_result()
@@ -2067,6 +2068,36 @@ impl EthApi {
20672068
Ok(())
20682069
}
20692070

2071+
/// Rollback the chain to a specific depth.
2072+
///
2073+
/// e.g depth = 3
2074+
/// A -> B -> C -> D -> E
2075+
/// A -> B
2076+
///
2077+
/// Depth specifies the height to rollback the chain back to. Depth must not exceed the current
2078+
/// chain height, i.e. can't rollback past the genesis block.
2079+
///
2080+
/// Handler for RPC call: `anvil_rollback`
2081+
pub async fn anvil_rollback(&self, depth: Option<u64>) -> Result<()> {
2082+
node_info!("anvil_rollback");
2083+
let depth = depth.unwrap_or(1);
2084+
2085+
// Check reorg depth doesn't exceed current chain height
2086+
let current_height = self.backend.best_number();
2087+
let common_height = current_height.checked_sub(depth).ok_or(BlockchainError::RpcError(
2088+
RpcError::invalid_params(format!(
2089+
"Rollback depth must not exceed current chain height: current height {current_height}, depth {depth}"
2090+
)),
2091+
))?;
2092+
2093+
// Get the common ancestor block
2094+
let common_block =
2095+
self.backend.get_block(common_height).ok_or(BlockchainError::BlockNotFound)?;
2096+
2097+
self.backend.rollback(common_block).await?;
2098+
Ok(())
2099+
}
2100+
20702101
/// Snapshot the state of the blockchain at the current block.
20712102
///
20722103
/// Handler for RPC call: `evm_snapshot`

crates/anvil/src/eth/backend/mem/mod.rs

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2631,6 +2631,27 @@ impl Backend {
26312631
tx_pairs: HashMap<u64, Vec<Arc<PoolTransaction>>>,
26322632
common_block: Block,
26332633
) -> Result<(), BlockchainError> {
2634+
self.rollback(common_block).await?;
2635+
// Create the new reorged chain, filling the blocks with transactions if supplied
2636+
for i in 0..depth {
2637+
let to_be_mined = tx_pairs.get(&i).cloned().unwrap_or_else(Vec::new);
2638+
let outcome = self.do_mine_block(to_be_mined).await;
2639+
node_info!(
2640+
" Mined reorg block number {}. With {} valid txs and with invalid {} txs",
2641+
outcome.block_number,
2642+
outcome.included.len(),
2643+
outcome.invalid.len()
2644+
);
2645+
}
2646+
2647+
Ok(())
2648+
}
2649+
2650+
/// Rollback the chain to a common height.
2651+
///
2652+
/// The state of the chain is rewound using `rewind` to the common block, including the db,
2653+
/// storage, and env.
2654+
pub async fn rollback(&self, common_block: Block) -> Result<(), BlockchainError> {
26342655
// Get the database at the common block
26352656
let common_state = {
26362657
let mut state = self.states.write();
@@ -2661,31 +2682,14 @@ impl Backend {
26612682

26622683
// Set environment back to common block
26632684
let mut env = self.env.write();
2664-
env.block = BlockEnv {
2665-
number: U256::from(common_block.header.number),
2666-
timestamp: U256::from(common_block.header.timestamp),
2667-
gas_limit: U256::from(common_block.header.gas_limit),
2668-
difficulty: common_block.header.difficulty,
2669-
prevrandao: Some(common_block.header.mix_hash),
2670-
coinbase: env.block.coinbase,
2671-
basefee: env.block.basefee,
2672-
..env.block.clone()
2673-
};
2674-
self.time.reset(env.block.timestamp.to::<u64>());
2675-
}
2685+
env.block.number = U256::from(common_block.header.number);
2686+
env.block.timestamp = U256::from(common_block.header.timestamp);
2687+
env.block.gas_limit = U256::from(common_block.header.gas_limit);
2688+
env.block.difficulty = common_block.header.difficulty;
2689+
env.block.prevrandao = Some(common_block.header.mix_hash);
26762690

2677-
// Create the new reorged chain, filling the blocks with transactions if supplied
2678-
for i in 0..depth {
2679-
let to_be_mined = tx_pairs.get(&i).cloned().unwrap_or_else(Vec::new);
2680-
let outcome = self.do_mine_block(to_be_mined).await;
2681-
node_info!(
2682-
" Mined reorg block number {}. With {} valid txs and with invalid {} txs",
2683-
outcome.block_number,
2684-
outcome.included.len(),
2685-
outcome.invalid.len()
2686-
);
2691+
self.time.reset(env.block.timestamp.to::<u64>());
26872692
}
2688-
26892693
Ok(())
26902694
}
26912695
}

crates/anvil/tests/it/anvil_api.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,38 @@ async fn test_reorg() {
805805
assert!(res.is_err());
806806
}
807807

808+
#[tokio::test(flavor = "multi_thread")]
809+
async fn test_rollback() {
810+
let (api, handle) = spawn(NodeConfig::test()).await;
811+
let provider = handle.http_provider();
812+
813+
// Mine 5 blocks
814+
for _ in 0..5 {
815+
api.mine_one().await;
816+
}
817+
818+
// Get block 4 for later comparison
819+
let block4 = provider.get_block(4.into(), false.into()).await.unwrap().unwrap();
820+
821+
// Rollback with None should rollback 1 block
822+
api.anvil_rollback(None).await.unwrap();
823+
824+
// Assert we're at block 4 and the block contents are kept the same
825+
let head = provider.get_block(BlockId::latest(), false.into()).await.unwrap().unwrap();
826+
assert_eq!(head, block4);
827+
828+
// Get block 1 for comparison
829+
let block1 = provider.get_block(1.into(), false.into()).await.unwrap().unwrap();
830+
831+
// Rollback to block 1
832+
let depth = 3; // from block 4 to block 1
833+
api.anvil_rollback(Some(depth)).await.unwrap();
834+
835+
// Assert we're at block 1 and the block contents are kept the same
836+
let head = provider.get_block(BlockId::latest(), false.into()).await.unwrap().unwrap();
837+
assert_eq!(head, block1);
838+
}
839+
808840
// === wallet endpoints === //
809841
#[tokio::test(flavor = "multi_thread")]
810842
async fn can_get_wallet_capabilities() {

0 commit comments

Comments
 (0)