Skip to content

Commit fd61888

Browse files
authored
feat(l2): batch reversion (#3136)
**Motivation** As outlined in #3124, sometimes a committed batch can't be verified or the operator wants to prevent it from going though. **Description** This PR implements a `revertBatch` function that allows reverting back to any batch, as long as no verified batches are being discarded. There's also a l2 CLI subcommand, revert-batch that lets you revert a batch and remove it from the local database. Usage on local network: ``` PRIVATE_KEY=key cargo run --features l2,rollup_storage_libmdbx -- l2 revert-batch \ <batch to revert to> <OnChainProposer address> \ --datadir dev_ethrex_l2 --network test_data/genesis-l2.json ``` Closes #3124
1 parent e04ce47 commit fd61888

File tree

15 files changed

+311
-8
lines changed

15 files changed

+311
-8
lines changed

cmd/ethrex/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ethrex-rlp.workspace = true
1717
ethrex-storage-rollup = { workspace = true, optional = true }
1818
ethrex-l2 = { workspace = true, optional = true }
1919
ethrex-l2-common = { workspace = true, optional = true }
20+
ethrex-sdk = { workspace = true, optional = true }
2021

2122
bytes.workspace = true
2223
hex.workspace = true
@@ -71,6 +72,7 @@ blst = ["ethrex-vm/blst"]
7172
l2 = [
7273
"dep:ethrex-l2",
7374
"dep:ethrex-l2-common",
75+
"dep:ethrex-sdk",
7476
"ethrex-vm/l2",
7577
"ethrex-blockchain/l2",
7678
"ethrex-rpc/l2",

cmd/ethrex/l2/command.rs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use crate::{
66
init_metrics, init_network, init_rollup_store, init_rpc_api, init_store,
77
},
88
l2::options::Options,
9-
utils::{NodeConfigFile, set_datadir, store_node_config_file},
9+
networks::Network,
10+
utils::{NodeConfigFile, parse_private_key, set_datadir, store_node_config_file},
1011
};
1112
use clap::Subcommand;
1213
use ethrex_common::{
@@ -15,6 +16,8 @@ use ethrex_common::{
1516
};
1617
use ethrex_l2::SequencerConfig;
1718
use ethrex_l2_common::state_diff::StateDiff;
19+
use ethrex_l2_sdk::call_contract;
20+
use ethrex_l2_sdk::calldata::Value;
1821
use ethrex_p2p::network::peer_table;
1922
use ethrex_rpc::{
2023
EthClient,
@@ -27,6 +30,7 @@ use eyre::OptionExt;
2730
use itertools::Itertools;
2831
use keccak_hash::keccak;
2932
use reqwest::Url;
33+
use secp256k1::SecretKey;
3034
use std::{
3135
fs::{create_dir_all, read_dir},
3236
future::IntoFuture,
@@ -79,6 +83,39 @@ pub enum Command {
7983
#[arg(short = 'c', long, help = "Address of the L2 proposer coinbase")]
8084
coinbase: Address,
8185
},
86+
#[command(about = "Reverts unverified batches.")]
87+
RevertBatch {
88+
#[arg(help = "ID of the batch to revert to")]
89+
batch: u64,
90+
#[arg(help = "The address of the OnChainProposer contract")]
91+
contract_address: Address,
92+
#[arg(long, value_parser = parse_private_key, env = "PRIVATE_KEY", help = "The private key of the owner. Assumed to have sequencing permission.")]
93+
private_key: Option<SecretKey>,
94+
#[arg(
95+
long,
96+
default_value = "http://localhost:8545",
97+
env = "RPC_URL",
98+
help = "URL of the L1 RPC"
99+
)]
100+
rpc_url: Url,
101+
#[arg(
102+
long = "network",
103+
default_value_t = Network::default(),
104+
value_name = "GENESIS_FILE_PATH",
105+
help = "Receives a `Genesis` struct in json format. This is the only argument which is required. You can look at some example genesis files at `test_data/genesis*`.",
106+
env = "ETHREX_NETWORK",
107+
value_parser = clap::value_parser!(Network),
108+
)]
109+
network: Network,
110+
#[arg(
111+
long = "datadir",
112+
value_name = "DATABASE_DIRECTORY",
113+
default_value = DEFAULT_L2_DATADIR,
114+
help = "Receives the name of the directory where the Database is located.",
115+
env = "ETHREX_DATADIR"
116+
)]
117+
datadir: String,
118+
},
82119
}
83120

84121
impl Command {
@@ -429,6 +466,63 @@ impl Command {
429466
}
430467
}
431468
}
469+
Command::RevertBatch {
470+
batch,
471+
contract_address,
472+
rpc_url,
473+
private_key,
474+
datadir,
475+
network,
476+
} => {
477+
let data_dir = set_datadir(&datadir);
478+
let rollup_store_dir = data_dir.clone() + "/rollup_store";
479+
480+
let client = EthClient::new(rpc_url.as_str())?;
481+
if let Some(private_key) = private_key {
482+
info!("Pausing OnChainProposer...");
483+
call_contract(&client, &private_key, contract_address, "pause()", vec![])
484+
.await?;
485+
info!("Doing revert on OnChainProposer...");
486+
call_contract(
487+
&client,
488+
&private_key,
489+
contract_address,
490+
"revertBatch(uint256)",
491+
vec![Value::Uint(batch.into())],
492+
)
493+
.await?;
494+
} else {
495+
info!("Private key not given, not updating contract.");
496+
}
497+
info!("Updating store...");
498+
let rollup_store = init_rollup_store(&rollup_store_dir).await;
499+
let last_kept_block = rollup_store
500+
.get_block_numbers_by_batch(batch)
501+
.await?
502+
.and_then(|kept_blocks| kept_blocks.iter().max().cloned())
503+
.unwrap_or(0);
504+
505+
let genesis = network.get_genesis();
506+
let store = init_store(&data_dir, genesis).await;
507+
508+
rollup_store.revert_to_batch(batch).await?;
509+
store.update_latest_block_number(last_kept_block).await?;
510+
511+
let mut block_to_delete = last_kept_block + 1;
512+
while store
513+
.get_canonical_block_hash(block_to_delete)
514+
.await?
515+
.is_some()
516+
{
517+
store.remove_block(block_to_delete).await?;
518+
block_to_delete += 1;
519+
}
520+
if let Some(private_key) = private_key {
521+
info!("Unpausing OnChainProposer...");
522+
call_contract(&client, &private_key, contract_address, "unpause()", vec![])
523+
.await?;
524+
}
525+
}
432526
}
433527
Ok(())
434528
}

crates/l2/contracts/src/l1/OnChainProposer.sol

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity =0.8.29;
44
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
55
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
66
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
7+
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
78
import "./interfaces/IOnChainProposer.sol";
89
import {CommonBridge} from "./CommonBridge.sol";
910
import {ICommonBridge} from "./interfaces/ICommonBridge.sol";
@@ -17,7 +18,8 @@ contract OnChainProposer is
1718
IOnChainProposer,
1819
Initializable,
1920
UUPSUpgradeable,
20-
Ownable2StepUpgradeable
21+
Ownable2StepUpgradeable,
22+
PausableUpgradeable
2123
{
2224
/// @notice Committed batches data.
2325
/// @dev This struct holds the information about the committed batches.
@@ -219,7 +221,7 @@ contract OnChainProposer is
219221
bytes32 withdrawalsLogsMerkleRoot,
220222
bytes32 processedDepositLogsRollingHash,
221223
bytes32 lastBlockHash
222-
) external override onlySequencer {
224+
) external override onlySequencer whenNotPaused {
223225
// TODO: Refactor validation
224226
require(
225227
batchNumber == lastCommittedBatch + 1,
@@ -289,7 +291,7 @@ contract OnChainProposer is
289291
//tdx
290292
bytes calldata tdxPublicValues,
291293
bytes memory tdxSignature
292-
) external override onlySequencer {
294+
) external override onlySequencer whenNotPaused {
293295
// TODO: Refactor validation
294296
// TODO: imageid, programvkey and riscvvkey should be constants
295297
// TODO: organize each zkvm proof arguments in their own structs
@@ -355,7 +357,7 @@ contract OnChainProposer is
355357
bytes calldata alignedPublicInputs,
356358
bytes32 alignedProgramVKey,
357359
bytes32[] calldata alignedMerkleProof
358-
) external override onlySequencer {
360+
) external override onlySequencer whenNotPaused {
359361
require(
360362
ALIGNEDPROOFAGGREGATOR != DEV_MODE,
361363
"OnChainProposer: ALIGNEDPROOFAGGREGATOR is not set"
@@ -452,9 +454,36 @@ contract OnChainProposer is
452454
);
453455
}
454456

457+
/// @inheritdoc IOnChainProposer
458+
function revertBatch(
459+
uint256 batchNumber
460+
) external override onlySequencer whenPaused {
461+
require(batchNumber >= lastVerifiedBatch, "OnChainProposer: can't revert verified batch");
462+
require(batchNumber < lastCommittedBatch, "OnChainProposer: no batches are being reverted");
463+
464+
// Remove old batches
465+
for (uint256 i = batchNumber; i < lastCommittedBatch; i++) {
466+
delete batchCommitments[i + 1];
467+
}
468+
469+
lastCommittedBatch = batchNumber;
470+
471+
emit BatchReverted(batchCommitments[lastCommittedBatch].newStateRoot);
472+
}
473+
455474
/// @notice Allow owner to upgrade the contract.
456475
/// @param newImplementation the address of the new implementation
457476
function _authorizeUpgrade(
458477
address newImplementation
459478
) internal virtual override onlyOwner {}
479+
480+
/// @inheritdoc IOnChainProposer
481+
function pause() external override onlyOwner {
482+
_pause();
483+
}
484+
485+
/// @inheritdoc IOnChainProposer
486+
function unpause() external override onlyOwner {
487+
_unpause();
488+
}
460489
}

crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ interface IOnChainProposer {
2323
/// @dev Event emitted when a batch is verified.
2424
event BatchVerified(uint256 indexed lastVerifiedBatch);
2525

26+
/// @notice A batch has been reverted.
27+
/// @dev Event emitted when a batch is reverted.
28+
event BatchReverted(bytes32 indexed newStateRoot);
29+
2630
/// @notice Set the bridge address for the first time.
2731
/// @dev This method is separated from initialize because both the CommonBridge
2832
/// and the OnChainProposer need to know the address of the other. This solves
@@ -88,4 +92,13 @@ interface IOnChainProposer {
8892
bytes32 alignedProgramVKey,
8993
bytes32[] calldata alignedMerkleProof
9094
) external;
95+
96+
/// @notice Allows unverified batches to be reverted
97+
function revertBatch(uint256 batchNumber) external;
98+
99+
/// @notice Allows the owner to pause the contract
100+
function pause() external;
101+
102+
/// @notice Allows the owner to unpause the contract
103+
function unpause() external;
91104
}

crates/l2/sdk/src/sdk.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,22 @@ pub async fn initialize_contract(
557557

558558
Ok(initialize_tx_hash)
559559
}
560+
561+
pub async fn call_contract(
562+
client: &EthClient,
563+
private_key: &SecretKey,
564+
to: Address,
565+
signature: &str,
566+
parameters: Vec<Value>,
567+
) -> Result<H256, EthClientError> {
568+
let calldata = encode_calldata(signature, &parameters)?.into();
569+
let from = get_address_from_secret_key(private_key)?;
570+
let tx = client
571+
.build_eip1559_transaction(to, from, calldata, Default::default())
572+
.await?;
573+
574+
let tx_hash = client.send_eip1559_transaction(&tx, private_key).await?;
575+
576+
wait_for_transaction_receipt(tx_hash, client, 100).await?;
577+
Ok(tx_hash)
578+
}

crates/l2/storage/src/api.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,6 @@ pub trait StoreEngineRollup: Debug + Send + Sync + RefUnwindSafe {
9999
async fn get_lastest_sent_batch_proof(&self) -> Result<u64, StoreError>;
100100

101101
async fn set_lastest_sent_batch_proof(&self, batch_number: u64) -> Result<(), StoreError>;
102+
103+
async fn revert_to_batch(&self, batch_number: u64) -> Result<(), StoreError>;
102104
}

crates/l2/storage/src/store.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,9 @@ impl Store {
288288
pub async fn set_lastest_sent_batch_proof(&self, batch_number: u64) -> Result<(), StoreError> {
289289
self.engine.set_lastest_sent_batch_proof(batch_number).await
290290
}
291+
292+
/// Reverts to a previous batch, discarding operations in them
293+
pub async fn revert_to_batch(&self, batch_number: u64) -> Result<(), StoreError> {
294+
self.engine.revert_to_batch(batch_number).await
295+
}
291296
}

crates/l2/storage/src/store_db/in_memory.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,25 @@ impl StoreEngineRollup for Store {
199199
self.inner()?.lastest_sent_batch_proof = batch_number;
200200
Ok(())
201201
}
202+
203+
async fn revert_to_batch(&self, batch_number: u64) -> Result<(), StoreError> {
204+
let mut store = self.inner()?;
205+
store
206+
.batches_by_block
207+
.retain(|_, batch| *batch <= batch_number);
208+
store
209+
.withdrawal_hashes_by_batch
210+
.retain(|batch, _| *batch <= batch_number);
211+
store
212+
.block_numbers_by_batch
213+
.retain(|batch, _| *batch <= batch_number);
214+
store
215+
.deposit_logs_hashes
216+
.retain(|batch, _| *batch <= batch_number);
217+
store.state_roots.retain(|batch, _| *batch <= batch_number);
218+
store.blobs.retain(|batch, _| *batch <= batch_number);
219+
Ok(())
220+
}
202221
}
203222

204223
impl Debug for Store {

crates/l2/storage/src/store_db/libmdbx.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use ethrex_common::{
1111
use ethrex_rlp::encode::RLPEncode;
1212
use ethrex_storage::error::StoreError;
1313
use libmdbx::{
14-
DatabaseOptions, Mode, PageSize, ReadWriteOptions,
15-
orm::{Database, Table},
14+
DatabaseOptions, Mode, PageSize, RW, ReadWriteOptions,
15+
orm::{Database, Table, Transaction},
1616
table, table_info,
1717
};
1818

@@ -271,6 +271,38 @@ impl StoreEngineRollup for Store {
271271
async fn set_lastest_sent_batch_proof(&self, batch_number: u64) -> Result<(), StoreError> {
272272
self.write::<LastSentBatchProof>(0, batch_number).await
273273
}
274+
275+
async fn revert_to_batch(&self, batch_number: u64) -> Result<(), StoreError> {
276+
let Some(kept_blocks) = self.get_block_numbers_by_batch(batch_number).await? else {
277+
return Ok(());
278+
};
279+
let last_kept_block = *kept_blocks.iter().max().unwrap_or(&0);
280+
let txn = self
281+
.db
282+
.begin_readwrite()
283+
.map_err(StoreError::LibmdbxError)?;
284+
delete_starting_at::<BatchesByBlockNumber>(&txn, last_kept_block + 1)?;
285+
delete_starting_at::<WithdrawalHashesByBatch>(&txn, batch_number + 1)?;
286+
delete_starting_at::<BlockNumbersByBatch>(&txn, batch_number + 1)?;
287+
delete_starting_at::<DepositLogsHash>(&txn, batch_number + 1)?;
288+
delete_starting_at::<StateRoots>(&txn, batch_number + 1)?;
289+
delete_starting_at::<BlobsBundles>(&txn, batch_number + 1)?;
290+
txn.commit().map_err(StoreError::LibmdbxError)?;
291+
Ok(())
292+
}
293+
}
294+
295+
/// Deletes keys above key, assuming they are contiguous
296+
fn delete_starting_at<T: Table<Key = u64>>(
297+
txn: &Transaction<RW>,
298+
mut key: u64,
299+
) -> Result<(), StoreError> {
300+
while let Some(val) = txn.get::<T>(key).map_err(StoreError::LibmdbxError)? {
301+
txn.delete::<T>(key, Some(val))
302+
.map_err(StoreError::LibmdbxError)?;
303+
key += 1;
304+
}
305+
Ok(())
274306
}
275307

276308
table!(

0 commit comments

Comments
 (0)