From ac5726d1310ad2e91fa57cd02a9d80530928a8e3 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 16 Dec 2025 14:08:54 -0300 Subject: [PATCH 01/47] timelock first steps --- .../l2/contracts/src/l1/OnChainProposer.sol | 25 +--- crates/l2/contracts/src/l1/Timelock.sol | 116 ++++++++++++++++++ 2 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 crates/l2/contracts/src/l1/Timelock.sol diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index eb819106bc..ec6a4ae094 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -64,10 +64,6 @@ contract OnChainProposer is /// @dev This is crucial for ensuring that only subsequents batches are committed in the contract. uint256 public lastCommittedBatch; - /// @dev The sequencer addresses that are authorized to commit and verify batches. - mapping(address _authorizedAddress => bool) - public authorizedSequencerAddresses; - address public BRIDGE; /// @dev Deprecated variable. address public PICO_VERIFIER_ADDRESS; @@ -101,14 +97,6 @@ contract OnChainProposer is /// @notice True if verification is done through Aligned Layer instead of smart contract verifiers. bool public ALIGNED_MODE; - modifier onlySequencer() { - require( - authorizedSequencerAddresses[msg.sender], - "000" // OnChainProposer: caller is not the sequencer - ); - _; - } - /// @notice Initializes the contract. /// @dev This method is called only once after the contract is deployed. /// @dev It sets the bridge address. @@ -130,7 +118,6 @@ contract OnChainProposer is bytes32 sp1Vk, bytes32 risc0Vk, bytes32 genesisStateRoot, - address[] calldata sequencerAddresses, uint256 chainId ) public initializer { VALIDIUM = _validium; @@ -164,10 +151,6 @@ contract OnChainProposer is new ICommonBridge.L2MessageRollingHash[](0) ); - for (uint256 i = 0; i < sequencerAddresses.length; i++) { - authorizedSequencerAddresses[sequencerAddresses[i]] = true; - } - CHAIN_ID = chainId; OwnableUpgradeable.__Ownable_init(owner); @@ -204,7 +187,7 @@ contract OnChainProposer is uint256 nonPrivilegedTransactions, ICommonBridge.BalanceDiff[] calldata balanceDiffs, ICommonBridge.L2MessageRollingHash[] calldata l2MessageRollingHashes - ) external override onlySequencer whenNotPaused { + ) external override onlyOwner whenNotPaused { // TODO: Refactor validation require( batchNumber == lastCommittedBatch + 1, @@ -297,7 +280,7 @@ contract OnChainProposer is //tdx bytes calldata tdxPublicValues, bytes memory tdxSignature - ) external override onlySequencer whenNotPaused { + ) external override onlyOwner whenNotPaused { require( !ALIGNED_MODE, "008" // Batch verification should be done via Aligned Layer. Call verifyBatchesAligned() instead. @@ -440,7 +423,7 @@ contract OnChainProposer is bytes[] calldata publicInputsList, bytes32[][] calldata sp1MerkleProofsList, bytes32[][] calldata risc0MerkleProofsList - ) external override onlySequencer whenNotPaused { + ) external override onlyOwner whenNotPaused { require( ALIGNED_MODE, "00h" // Batch verification should be done via smart contract verifiers. Call verifyBatch() instead. @@ -683,7 +666,7 @@ contract OnChainProposer is /// @inheritdoc IOnChainProposer function revertBatch( uint256 batchNumber - ) external override onlySequencer whenPaused { + ) external override onlyOwner whenPaused { require( batchNumber >= lastVerifiedBatch, "010" // OnChainProposer: can't revert verified batch diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol new file mode 100644 index 0000000000..369eb5a608 --- /dev/null +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.29; + +import "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {IOnChainProposer} from "./interfaces/IOnChainProposer.sol"; +import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; + +contract Timelock is + TimelockControllerUpgradeable, + UUPSUpgradeable, + Ownable2StepUpgradeable +{ + bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); + bytes32 public constant SECURITY_COUNCIL = keccak256("SECURITY_COUNCIL"); + + IOnChainProposer public onChainProposer; + + function initialize( + uint256 minDelay, + address[] memory sequencers, + address owner, + address securityCouncil, + address _onChainProposer + ) public initializer { + for (uint256 i = 0; i < sequencers.length; ++i) { + _grantRole(SEQUENCER, sequencers[i]); + } + + _grantRole(SECURITY_COUNCIL, securityCouncil); + + address[] memory owners = new address[](1); + owners[0] = owner; + + TimelockControllerUpgradeable.__TimelockController_init( + minDelay, + owners, // proposers + owners, // executors + securityCouncil // admin + ); + OwnableUpgradeable.__Ownable_init(owner); + onChainProposer = IOnChainProposer(_onChainProposer); + } + + function commitBatch( + uint256 batchNumber, + bytes32 newStateRoot, + bytes32 withdrawalsLogsMerkleRoot, + bytes32 processedPrivilegedTransactionsRollingHash, + bytes32 lastBlockHash, + uint256 nonPrivilegedTransactions, + ICommonBridge.BalanceDiff[] calldata balanceDiffs, + ICommonBridge.L2MessageRollingHash[] calldata l2MessageRollingHashes + ) external onlyRole(SEQUENCER) { + onChainProposer.commitBatch( + batchNumber, + newStateRoot, + withdrawalsLogsMerkleRoot, + processedPrivilegedTransactionsRollingHash, + lastBlockHash, + nonPrivilegedTransactions, + balanceDiffs, + l2MessageRollingHashes + ); + } + + function verifyBatch( + uint256 batchNumber, + bytes memory risc0BlockProof, + bytes calldata risc0Journal, + bytes calldata sp1PublicValues, + bytes memory sp1ProofBytes, + bytes calldata tdxPublicValues, + bytes memory tdxSignature + ) external onlyRole(SEQUENCER) { + onChainProposer.verifyBatch( + batchNumber, + risc0BlockProof, + risc0Journal, + sp1PublicValues, + sp1ProofBytes, + tdxPublicValues, + tdxSignature + ); + } + + function verifyBatchesAligned( + uint256 firstBatchNumber, + bytes[] calldata publicInputsList, + bytes32[][] calldata sp1MerkleProofsList, + bytes32[][] calldata risc0MerkleProofsList + ) external onlyRole(SEQUENCER) { + onChainProposer.verifyBatchesAligned( + firstBatchNumber, + publicInputsList, + sp1MerkleProofsList, + risc0MerkleProofsList + ); + } + + function revertBatch( + uint256 batchNumber + ) external onlyRole(SECURITY_COUNCIL) { + onChainProposer.revertBatch(batchNumber); + } + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner { + address sender = _msgSender(); + if (sender != address(this)) { + revert TimelockUnauthorizedCaller(sender); + } + } +} From 5e341dd4df78e4e247c8abbd9db77fae5d83f26b Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 16 Dec 2025 15:00:00 -0300 Subject: [PATCH 02/47] improve some stuff and add comments --- .../l2/contracts/src/l1/OnChainProposer.sol | 2 -- crates/l2/contracts/src/l1/Timelock.sol | 27 +++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index ec6a4ae094..526e2b4c26 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -11,8 +11,6 @@ import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; import {IRiscZeroVerifier} from "./interfaces/IRiscZeroVerifier.sol"; import {ISP1Verifier} from "./interfaces/ISP1Verifier.sol"; import {ITDXVerifier} from "./interfaces/ITDXVerifier.sol"; -import "./interfaces/ICommonBridge.sol"; -import "./interfaces/IOnChainProposer.sol"; import "../l2/interfaces/ICommonBridgeL2.sol"; /// @title OnChainProposer contract. diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 369eb5a608..895eb2ae53 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -7,22 +7,20 @@ import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import {IOnChainProposer} from "./interfaces/IOnChainProposer.sol"; import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; -contract Timelock is - TimelockControllerUpgradeable, - UUPSUpgradeable, - Ownable2StepUpgradeable -{ +contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { + error TimelockUnauthorizedCaller(address caller); + bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); bytes32 public constant SECURITY_COUNCIL = keccak256("SECURITY_COUNCIL"); IOnChainProposer public onChainProposer; function initialize( - uint256 minDelay, - address[] memory sequencers, - address owner, - address securityCouncil, - address _onChainProposer + uint256 minDelay, // This should be the minimum delay for contract upgrades in seconds (e.g. 7 days = 604800 sec). + address[] memory sequencers, // Will be able to commit and verify batches. + address owner, // Will be able to propose and execute functions, respecting the delay. + address securityCouncil, // It will have admin role, which means no delay. + address _onChainProposer // deployed OnChainProposer contract. ) public initializer { for (uint256 i = 0; i < sequencers.length; ++i) { _grantRole(SEQUENCER, sequencers[i]); @@ -39,10 +37,12 @@ contract Timelock is owners, // executors securityCouncil // admin ); - OwnableUpgradeable.__Ownable_init(owner); + onChainProposer = IOnChainProposer(_onChainProposer); } + // TODO: In commit and verify we should probably modify logic so that we have a time window between commit and verify, + // or if we want to do it better we can have commit -> verify -> execute and the time window has to be between commit and execute. function commitBatch( uint256 batchNumber, bytes32 newStateRoot, @@ -105,9 +105,8 @@ contract Timelock is onChainProposer.revertBatch(batchNumber); } - function _authorizeUpgrade( - address newImplementation - ) internal override onlyOwner { + // Logic for updating Timelock contract. Should be triggered by the timelock itself so that it respects min time. + function _authorizeUpgrade(address newImplementation) internal override { address sender = _msgSender(); if (sender != address(this)) { revert TimelockUnauthorizedCaller(sender); From 6138e2933a29e4bface94a7c8b7b191827a5afd3 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 16 Dec 2025 15:02:49 -0300 Subject: [PATCH 03/47] add one more comment about security council --- crates/l2/contracts/src/l1/Timelock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 895eb2ae53..b8ad8a3156 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -19,7 +19,7 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { uint256 minDelay, // This should be the minimum delay for contract upgrades in seconds (e.g. 7 days = 604800 sec). address[] memory sequencers, // Will be able to commit and verify batches. address owner, // Will be able to propose and execute functions, respecting the delay. - address securityCouncil, // It will have admin role, which means no delay. + address securityCouncil, // TODO: Admin role -> Can manage roles. But it can't schedule/execute by itself, maybe we should add that address _onChainProposer // deployed OnChainProposer contract. ) public initializer { for (uint256 i = 0; i < sequencers.length; ++i) { From a2b631c6a2042418bcafbb38bc50a193da019743 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 16 Dec 2025 15:50:15 -0300 Subject: [PATCH 04/47] Deploy timelock contract and make it compile (not tested) --- cmd/ethrex/build_l2.rs | 5 + cmd/ethrex/l2/deployer.rs | 156 +++++++++++++----- .../l2/contracts/src/l1/OnChainProposer.sol | 10 +- crates/l2/contracts/src/l1/Timelock.sol | 2 - .../src/l1/interfaces/IOnChainProposer.sol | 7 - 5 files changed, 128 insertions(+), 52 deletions(-) diff --git a/cmd/ethrex/build_l2.rs b/cmd/ethrex/build_l2.rs index d1f9acbc58..de7510d726 100644 --- a/cmd/ethrex/build_l2.rs +++ b/cmd/ethrex/build_l2.rs @@ -107,6 +107,10 @@ pub fn download_script() { &Path::new("../../crates/l2/contracts/src/l1/Router.sol"), "Router", ), + ( + &Path::new("../../crates/l2/contracts/src/l1/Timelock.sol"), + "Timelock", + ), ]; for (path, name) in l1_contracts { compile_contract_to_bytecode( @@ -192,6 +196,7 @@ fn write_empty_bytecode_files(output_contracts_path: &Path) { "UpgradeableSystemContract", "SequencerRegistry", "OnChainProposerBased", + "Timelock", ]; for name in &contract_names { diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 378e6b9ba8..58d89aa0ce 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -532,6 +532,13 @@ const SEQUENCER_REGISTRY_BYTECODE: &[u8] = include_bytes!(concat!( "/contracts/solc_out/SequencerRegistry.bytecode" )); +/// Bytecode of the Timelock contract. +/// This is generated by the [build script](./build.rs). +const TIMELOCK_BYTECODE: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/contracts/solc_out/Timelock.bytecode" +)); + /// Bytecode of the SP1Verifier contract. /// This is generated by the [build script](./build.rs). const SP1_VERIFIER_BYTECODE: &[u8] = include_bytes!(concat!( @@ -540,7 +547,8 @@ const SP1_VERIFIER_BYTECODE: &[u8] = include_bytes!(concat!( )); const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE_BASED: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,address,uint256)"; -const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,address[],uint256)"; +const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,uint256,address)"; +const INITIALIZE_TIMELOCK_SIGNATURE: &str = "initialize(uint256,address[],address,address,address)"; const INITIALIZE_BRIDGE_ADDRESS_SIGNATURE: &str = "initializeBridgeAddress(address)"; const TRANSFER_OWNERSHIP_SIGNATURE: &str = "transferOwnership(address)"; @@ -564,6 +572,7 @@ pub struct ContractAddresses { pub sequencer_registry_address: Address, pub aligned_aggregator_address: Address, pub router: Option
, + pub timelock_address: Address, } pub async fn deploy_l1_contracts( @@ -715,6 +724,33 @@ async fn deploy_contracts( Default::default() }; + info!("Deploying Timelock"); + + let timelock_deployment = deploy_with_proxy_from_bytecode_no_wait( + deployer, + eth_client, + TIMELOCK_BYTECODE, + &salt, + Overrides { + nonce: Some(nonce), + gas_limit: Some(TRANSACTION_GAS_LIMIT), + max_fee_per_gas: Some(gas_price), + max_priority_fee_per_gas: Some(gas_price), + ..Default::default() + }, + ) + .await?; + + nonce += 2; + + info!( + "Timelock deployed:\n Proxy -> address={:#x}, tx_hash={:#x}\n Impl -> address={:#x}, tx_hash={:#x}", + timelock_deployment.proxy_address, + timelock_deployment.proxy_tx_hash, + timelock_deployment.implementation_address, + timelock_deployment.implementation_tx_hash, + ); + info!("Deploying OnChainProposer"); trace!("Attempting to deploy OnChainProposer contract"); @@ -888,6 +924,8 @@ async fn deploy_contracts( "Contracts deployed" ); let receipts = vec![ + timelock_deployment.implementation_tx_hash, + timelock_deployment.proxy_tx_hash, on_chain_proposer_deployment.implementation_tx_hash, on_chain_proposer_deployment.proxy_tx_hash, bridge_deployment.implementation_tx_hash, @@ -909,6 +947,7 @@ async fn deploy_contracts( sequencer_registry_address: sequencer_registry_deployment.proxy_address, aligned_aggregator_address, router: opts.router.or(Some(router_deployment.proxy_address)), + timelock_address: timelock_deployment.proxy_address, }, receipts, )) @@ -1031,6 +1070,44 @@ async fn initialize_contracts( let deployer_address = get_address_from_secret_key(&opts.private_key.secret_bytes()) .map_err(DeployerError::InternalError)?; + info!("Initializing Timelock"); + let initialize_tx_hash = { + let deployer = Signer::Local(LocalSigner::new(opts.private_key)); + let deployer_nonce = eth_client + .get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending)) + .await?; + let calldata_values = vec![ + Value::Uint(U256::from(120)), // TODO: change minDelay for testing: 120s + Value::Array(vec![ + // sequencers + Value::Address(opts.committer_l1_address), + Value::Address(opts.proof_sender_l1_address), + ]), + Value::Address(opts.on_chain_proposer_owner), // owner + Value::Address(opts.on_chain_proposer_owner), // securityCouncil + Value::Address(contract_addresses.on_chain_proposer_address), // onChainProposer + ]; + let timelock_initialization_calldata = + encode_calldata(INITIALIZE_TIMELOCK_SIGNATURE, &calldata_values)?; + + initialize_contract_no_wait( + contract_addresses.timelock_address, + timelock_initialization_calldata, + &deployer, + eth_client, + Overrides { + nonce: Some(deployer_nonce), + gas_limit: Some(TRANSACTION_GAS_LIMIT), + max_fee_per_gas: Some(gas_price), + max_priority_fee_per_gas: Some(gas_price), + ..Default::default() + }, + ) + .await? + }; + info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "Timelock initialized"); + tx_hashes.push(initialize_tx_hash); + info!("Initializing OnChainProposer"); if opts.deploy_based_contracts { @@ -1146,7 +1223,7 @@ async fn initialize_contracts( // Initialize only OnChainProposer without Based config let calldata_values = vec![ Value::Bool(opts.validium), - Value::Address(deployer_address), + Value::Address(contract_addresses.timelock_address), // Owner set to Timelock Value::Bool(opts.risc0), Value::Bool(opts.sp1), Value::Bool(opts.tdx), @@ -1158,11 +1235,9 @@ async fn initialize_contracts( Value::FixedBytes(sp1_vk), Value::FixedBytes(risc0_vk), Value::FixedBytes(genesis.compute_state_root().0.to_vec().into()), - Value::Array(vec![ - Value::Address(opts.committer_l1_address), - Value::Address(opts.proof_sender_l1_address), - ]), + // Removed sequencers array Value::Uint(genesis.config.chain_id.into()), + Value::Address(contract_addresses.bridge_address), ]; trace!(calldata_values = ?calldata_values, "OnChainProposer initialization calldata values"); let on_chain_proposer_initialization_calldata = @@ -1192,41 +1267,43 @@ async fn initialize_contracts( tx_hashes.push(initialize_tx_hash); } - let initialize_bridge_address_tx_hash = { - let initializer_nonce = eth_client - .get_nonce( - initializer.address(), - BlockIdentifier::Tag(BlockTag::Pending), - ) - .await?; - let calldata_values = vec![Value::Address(contract_addresses.bridge_address)]; - let on_chain_proposer_initialization_calldata = - encode_calldata(INITIALIZE_BRIDGE_ADDRESS_SIGNATURE, &calldata_values)?; + if opts.deploy_based_contracts { + let initialize_bridge_address_tx_hash = { + let initializer_nonce = eth_client + .get_nonce( + initializer.address(), + BlockIdentifier::Tag(BlockTag::Pending), + ) + .await?; + let calldata_values = vec![Value::Address(contract_addresses.bridge_address)]; + let on_chain_proposer_initialization_calldata = + encode_calldata(INITIALIZE_BRIDGE_ADDRESS_SIGNATURE, &calldata_values)?; - initialize_contract_no_wait( - contract_addresses.on_chain_proposer_address, - on_chain_proposer_initialization_calldata, - initializer, - eth_client, - Overrides { - nonce: Some(initializer_nonce), - gas_limit: Some(TRANSACTION_GAS_LIMIT), - max_fee_per_gas: Some(gas_price), - max_priority_fee_per_gas: Some(gas_price), - ..Default::default() - }, - ) - .await? - }; + initialize_contract_no_wait( + contract_addresses.on_chain_proposer_address, + on_chain_proposer_initialization_calldata, + initializer, + eth_client, + Overrides { + nonce: Some(initializer_nonce), + gas_limit: Some(TRANSACTION_GAS_LIMIT), + max_fee_per_gas: Some(gas_price), + max_priority_fee_per_gas: Some(gas_price), + ..Default::default() + }, + ) + .await? + }; - info!( - tx_hash = %format!("{initialize_bridge_address_tx_hash:#x}"), - "OnChainProposer bridge address initialized" - ); + info!( + tx_hash = %format!("{initialize_bridge_address_tx_hash:#x}"), + "OnChainProposer bridge address initialized" + ); - tx_hashes.push(initialize_bridge_address_tx_hash); + tx_hashes.push(initialize_bridge_address_tx_hash); + } - if opts.on_chain_proposer_owner != initializer.address() { + if opts.deploy_based_contracts && opts.on_chain_proposer_owner != initializer.address() { let initializer_nonce = eth_client .get_nonce( initializer.address(), @@ -1577,6 +1654,11 @@ fn write_contract_addresses_to_env( "ETHREX_COMMITTER_ON_CHAIN_PROPOSER_ADDRESS={:#x}", contract_addresses.on_chain_proposer_address )?; + writeln!( + writer, + "ETHREX_TIMELOCK_ADDRESS={:#x}", + contract_addresses.timelock_address + )?; writeln!( writer, "ETHREX_WATCHER_BRIDGE_ADDRESS={:#x}", diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 526e2b4c26..02ff36c3ad 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -116,7 +116,8 @@ contract OnChainProposer is bytes32 sp1Vk, bytes32 risc0Vk, bytes32 genesisStateRoot, - uint256 chainId + uint256 chainId, + address bridge ) public initializer { VALIDIUM = _validium; @@ -151,16 +152,13 @@ contract OnChainProposer is CHAIN_ID = chainId; - OwnableUpgradeable.__Ownable_init(owner); - } - - /// @inheritdoc IOnChainProposer - function initializeBridgeAddress(address bridge) public onlyOwner { require( bridge != address(0), "001" // OnChainProposer: bridge is the zero address ); BRIDGE = bridge; + + OwnableUpgradeable.__Ownable_init(owner); } /// @inheritdoc IOnChainProposer diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index b8ad8a3156..e5490544e4 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -8,8 +8,6 @@ import {IOnChainProposer} from "./interfaces/IOnChainProposer.sol"; import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { - error TimelockUnauthorizedCaller(address caller); - bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); bytes32 public constant SECURITY_COUNCIL = keccak256("SECURITY_COUNCIL"); diff --git a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol index 9f972ea65e..ec799af8a1 100644 --- a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol @@ -35,13 +35,6 @@ interface IOnChainProposer { /// @param newVerificationKey The new verification key. event VerificationKeyUpgraded(string verifier, bytes32 newVerificationKey); - /// @notice Set the bridge address for the first time. - /// @dev This method is separated from initialize because both the CommonBridge - /// and the OnChainProposer need to know the address of the other. This solves - /// the circular dependency while allowing to initialize the proxy with the deploy. - /// @param bridge the address of the bridge contract. - function initializeBridgeAddress(address bridge) external; - /// @notice Upgrades the SP1 verification key that represents the sequencer's code. /// @param new_vk new verification key for SP1 verifier function upgradeSP1VerificationKey(bytes32 new_vk) external; From 06429f5092de147cecdfb06e5159c37d47270df1 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 16 Dec 2025 18:36:22 -0300 Subject: [PATCH 05/47] remove comments --- cmd/ethrex/l2/deployer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 58d89aa0ce..0f8a52afe7 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -1223,7 +1223,7 @@ async fn initialize_contracts( // Initialize only OnChainProposer without Based config let calldata_values = vec![ Value::Bool(opts.validium), - Value::Address(contract_addresses.timelock_address), // Owner set to Timelock + Value::Address(contract_addresses.timelock_address), Value::Bool(opts.risc0), Value::Bool(opts.sp1), Value::Bool(opts.tdx), @@ -1235,7 +1235,6 @@ async fn initialize_contracts( Value::FixedBytes(sp1_vk), Value::FixedBytes(risc0_vk), Value::FixedBytes(genesis.compute_state_root().0.to_vec().into()), - // Removed sequencers array Value::Uint(genesis.config.chain_id.into()), Value::Address(contract_addresses.bridge_address), ]; From 75968ed9708c98d329c53962bb699c96791af3bd Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 16 Dec 2025 19:51:17 -0300 Subject: [PATCH 06/47] commit and verify batches to Timelock instead of OnChainProposer --- cmd/ethrex/l2/command.rs | 4 ++++ cmd/ethrex/l2/options.rs | 10 ++++++++++ crates/l2/sequencer/configs.rs | 1 + crates/l2/sequencer/l1_committer.rs | 13 +++++++++++-- crates/l2/sequencer/l1_proof_sender.rs | 8 +++++++- crates/l2/sequencer/l1_proof_verifier.rs | 8 +++++++- 6 files changed, 40 insertions(+), 4 deletions(-) diff --git a/cmd/ethrex/l2/command.rs b/cmd/ethrex/l2/command.rs index 9680c7c524..96683dfe07 100644 --- a/cmd/ethrex/l2/command.rs +++ b/cmd/ethrex/l2/command.rs @@ -95,6 +95,10 @@ impl L2Command { .sequencer_opts .committer_opts .on_chain_proposer_address = Some(contract_addresses.on_chain_proposer_address); + l2_options + .sequencer_opts + .committer_opts + .timelock_address = Some(contract_addresses.timelock_address); l2_options.sequencer_opts.watcher_opts.bridge_address = Some(contract_addresses.bridge_address); println!("Initializing L2"); diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index 34642f8ca5..73938cb2b6 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -172,6 +172,7 @@ impl TryFrom for SequencerConfig { .committer_opts .on_chain_proposer_address .ok_or(SequencerOptionsError::NoOnChainProposerAddress)?, + timelock_address: opts.committer_opts.timelock_address, first_wake_up_time_ms: opts.committer_opts.first_wake_up_time_ms.unwrap_or(0), commit_time_ms: opts.committer_opts.commit_time_ms, batch_gas_limit: opts.committer_opts.batch_gas_limit, @@ -605,6 +606,13 @@ pub struct CommitterOptions { required_unless_present = "dev" )] pub on_chain_proposer_address: Option
, + #[arg( + long = "l1.timelock-address", + value_name = "ADDRESS", + env = "ETHREX_TIMELOCK_ADDRESS", + help_heading = "L1 Committer options" + )] + pub timelock_address: Option
, #[arg( long = "committer.commit-time", default_value = "60000", @@ -648,6 +656,7 @@ impl Default for CommitterOptions { ) .ok(), on_chain_proposer_address: None, + timelock_address: None, commit_time_ms: 60000, batch_gas_limit: None, first_wake_up_time_ms: None, @@ -675,6 +684,7 @@ impl CommitterOptions { self.on_chain_proposer_address = self .on_chain_proposer_address .or(defaults.on_chain_proposer_address); + self.timelock_address = self.timelock_address.or(defaults.timelock_address); self.batch_gas_limit = self.batch_gas_limit.or(defaults.batch_gas_limit); self.first_wake_up_time_ms = self .first_wake_up_time_ms diff --git a/crates/l2/sequencer/configs.rs b/crates/l2/sequencer/configs.rs index 159ae16423..f641bfffa1 100644 --- a/crates/l2/sequencer/configs.rs +++ b/crates/l2/sequencer/configs.rs @@ -32,6 +32,7 @@ pub struct BlockProducerConfig { #[derive(Clone, Debug)] pub struct CommitterConfig { pub on_chain_proposer_address: Address, + pub timelock_address: Option
, pub first_wake_up_time_ms: u64, pub commit_time_ms: u64, pub batch_gas_limit: Option, diff --git a/crates/l2/sequencer/l1_committer.rs b/crates/l2/sequencer/l1_committer.rs index 2144a16b11..7e37a6fea3 100644 --- a/crates/l2/sequencer/l1_committer.rs +++ b/crates/l2/sequencer/l1_committer.rs @@ -101,6 +101,7 @@ pub struct L1Committer { eth_client: EthClient, blockchain: Arc, on_chain_proposer_address: Address, + timelock_address: Option
, store: Store, rollup_store: StoreRollup, commit_time_ms: u64, @@ -192,6 +193,7 @@ impl L1Committer { eth_client, blockchain, on_chain_proposer_address: committer_config.on_chain_proposer_address, + timelock_address: committer_config.timelock_address, store, rollup_store, commit_time_ms: committer_config.commit_time_ms, @@ -1194,6 +1196,13 @@ impl L1Committer { CommitterError::ConversionError("Failed to convert gas_price to a u64".to_owned()) })?; + let target_address = if !self.based { + self.timelock_address + .unwrap_or(self.on_chain_proposer_address) + } else { + self.on_chain_proposer_address + }; + // Validium: EIP1559 Transaction. // Rollup: EIP4844 Transaction -> For on-chain Data Availability. let tx = if !self.validium { @@ -1211,7 +1220,7 @@ impl L1Committer { build_generic_tx( &self.eth_client, TxType::EIP4844, - self.on_chain_proposer_address, + target_address, self.signer.address(), calldata.into(), Overrides { @@ -1231,7 +1240,7 @@ impl L1Committer { build_generic_tx( &self.eth_client, TxType::EIP1559, - self.on_chain_proposer_address, + target_address, self.signer.address(), calldata.into(), Overrides { diff --git a/crates/l2/sequencer/l1_proof_sender.rs b/crates/l2/sequencer/l1_proof_sender.rs index d68a220b93..b4ca750404 100644 --- a/crates/l2/sequencer/l1_proof_sender.rs +++ b/crates/l2/sequencer/l1_proof_sender.rs @@ -68,6 +68,7 @@ pub struct L1ProofSender { eth_client: EthClient, signer: ethrex_l2_rpc::signer::Signer, on_chain_proposer_address: Address, + timelock_address: Option
, needed_proof_types: Vec, proof_send_interval_ms: u64, sequencer_state: SequencerState, @@ -124,6 +125,7 @@ impl L1ProofSender { eth_client, signer: cfg.signer.clone(), on_chain_proposer_address: committer_cfg.on_chain_proposer_address, + timelock_address: committer_cfg.timelock_address, needed_proof_types, proof_send_interval_ms: cfg.proof_send_interval_ms, sequencer_state, @@ -397,10 +399,14 @@ impl L1ProofSender { let calldata = encode_calldata(VERIFY_FUNCTION_SIGNATURE, &calldata_values)?; + let target_address = self + .timelock_address + .unwrap_or(self.on_chain_proposer_address); + let send_verify_tx_result = send_verify_tx( calldata, &self.eth_client, - self.on_chain_proposer_address, + target_address, &self.signer, ) .await; diff --git a/crates/l2/sequencer/l1_proof_verifier.rs b/crates/l2/sequencer/l1_proof_verifier.rs index 6c5d2192bf..afcde0439e 100644 --- a/crates/l2/sequencer/l1_proof_verifier.rs +++ b/crates/l2/sequencer/l1_proof_verifier.rs @@ -59,6 +59,7 @@ struct L1ProofVerifier { beacon_urls: Vec, l1_signer: Signer, on_chain_proposer_address: Address, + timelock_address: Option
, proof_verify_interval_ms: u64, network: Network, rollup_store: StoreRollup, @@ -96,6 +97,7 @@ impl L1ProofVerifier { network: aligned_cfg.network.clone(), l1_signer: proof_coordinator_cfg.signer, on_chain_proposer_address: committer_cfg.on_chain_proposer_address, + timelock_address: committer_cfg.timelock_address, proof_verify_interval_ms: aligned_cfg.aligned_verifier_interval_ms, rollup_store, sp1_vk, @@ -238,10 +240,14 @@ impl L1ProofVerifier { let calldata = encode_calldata(ALIGNED_VERIFY_FUNCTION_SIGNATURE, &calldata_values)?; + let target_address = self + .timelock_address + .unwrap_or(self.on_chain_proposer_address); + let send_verify_tx_result = send_verify_tx( calldata, &self.eth_client, - self.on_chain_proposer_address, + target_address, &self.l1_signer, ) .await; From ea206cbf893fa7334a68a1f307b83cd4ccc97099 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 17 Dec 2025 11:32:01 -0300 Subject: [PATCH 07/47] add pause and unpause functions --- crates/l2/contracts/src/l1/Timelock.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index e5490544e4..b2ce1f8cd0 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -103,6 +103,14 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { onChainProposer.revertBatch(batchNumber); } + function pause() external onlyRole(SECURITY_COUNCIL) { + onChainProposer.pause(); + } + + function unpause() external onlyRole(SECURITY_COUNCIL) { + onChainProposer.unpause(); + } + // Logic for updating Timelock contract. Should be triggered by the timelock itself so that it respects min time. function _authorizeUpgrade(address newImplementation) internal override { address sender = _msgSender(); From 4e2ab15181005e403984940edcbee41fdcc02964 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 17 Dec 2025 16:14:10 -0300 Subject: [PATCH 08/47] change mindelay to 30 seconds for testing --- cmd/ethrex/l2/deployer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 0f8a52afe7..489f94f02c 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -1077,7 +1077,7 @@ async fn initialize_contracts( .get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending)) .await?; let calldata_values = vec![ - Value::Uint(U256::from(120)), // TODO: change minDelay for testing: 120s + Value::Uint(U256::from(30)), // TODO: change minDelay for testing: 30s Value::Array(vec![ // sequencers Value::Address(opts.committer_l1_address), From d390d9737d0e1d334b87256b0ff9eed1e24f2004 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 17 Dec 2025 16:58:02 -0300 Subject: [PATCH 09/47] add emergency execution for security council --- crates/l2/contracts/src/l1/Timelock.sol | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index b2ce1f8cd0..7b87e0d6ad 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -11,6 +11,8 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); bytes32 public constant SECURITY_COUNCIL = keccak256("SECURITY_COUNCIL"); + event EmergencyExecution(address indexed target, uint256 value, bytes data); + IOnChainProposer public onChainProposer; function initialize( @@ -39,8 +41,7 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { onChainProposer = IOnChainProposer(_onChainProposer); } - // TODO: In commit and verify we should probably modify logic so that we have a time window between commit and verify, - // or if we want to do it better we can have commit -> verify -> execute and the time window has to be between commit and execute. + //TODO: In the future commit and verify will have timelock logic incorporated in case there are any zkVM bugs and we want to avoid applying the changes in the L1. Probably the Security Council would act upon those changes. function commitBatch( uint256 batchNumber, bytes32 newStateRoot, @@ -111,6 +112,17 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { onChainProposer.unpause(); } + // In case a bug is detected the Security Council can act immediately. + // Ideally in the future this should be possible only if the bug is detected on-chain with a proper mechanism. + function emergencyExecute( + address target, + uint256 value, + bytes calldata data + ) external payable onlyRole(SECURITY_COUNCIL) { + _execute(target, value, data); + emit EmergencyExecution(target, value, data); + } + // Logic for updating Timelock contract. Should be triggered by the timelock itself so that it respects min time. function _authorizeUpgrade(address newImplementation) internal override { address sender = _msgSender(); From 6516788bfc1189fd41112750c39f593cb66bf088 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 17 Dec 2025 17:45:53 -0300 Subject: [PATCH 10/47] remove initialization of bridge address and transfer of ownership --- cmd/ethrex/l2/deployer.rs | 110 +----------------- .../src/l1/based/OnChainProposer.sol | 18 +-- .../l1/based/interfaces/IOnChainProposer.sol | 7 -- 3 files changed, 7 insertions(+), 128 deletions(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 489f94f02c..fb30a38768 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -546,11 +546,10 @@ const SP1_VERIFIER_BYTECODE: &[u8] = include_bytes!(concat!( "/contracts/solc_out/SP1Verifier.bytecode" )); -const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE_BASED: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,address,uint256)"; +const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE_BASED: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,address,uint256,address)"; const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,uint256,address)"; const INITIALIZE_TIMELOCK_SIGNATURE: &str = "initialize(uint256,address[],address,address,address)"; -const INITIALIZE_BRIDGE_ADDRESS_SIGNATURE: &str = "initializeBridgeAddress(address)"; const TRANSFER_OWNERSHIP_SIGNATURE: &str = "transferOwnership(address)"; const ACCEPT_OWNERSHIP_SIGNATURE: &str = "acceptOwnership()"; const BRIDGE_INITIALIZER_SIGNATURE: &str = "initialize(address,address,uint256,address, uint256)"; @@ -1114,7 +1113,7 @@ async fn initialize_contracts( // Initialize OnChainProposer with Based config and SequencerRegistry let calldata_values = vec![ Value::Bool(opts.validium), - Value::Address(deployer_address), + Value::Address(opts.on_chain_proposer_owner), Value::Bool(opts.risc0), Value::Bool(opts.sp1), Value::Bool(opts.tdx), @@ -1128,6 +1127,7 @@ async fn initialize_contracts( Value::FixedBytes(genesis.compute_state_root().0.to_vec().into()), Value::Address(contract_addresses.sequencer_registry_address), Value::Uint(genesis.config.chain_id.into()), + Value::Address(contract_addresses.bridge_address), ]; trace!(calldata_values = ?calldata_values, "OnChainProposer initialization calldata values"); @@ -1266,110 +1266,6 @@ async fn initialize_contracts( tx_hashes.push(initialize_tx_hash); } - if opts.deploy_based_contracts { - let initialize_bridge_address_tx_hash = { - let initializer_nonce = eth_client - .get_nonce( - initializer.address(), - BlockIdentifier::Tag(BlockTag::Pending), - ) - .await?; - let calldata_values = vec![Value::Address(contract_addresses.bridge_address)]; - let on_chain_proposer_initialization_calldata = - encode_calldata(INITIALIZE_BRIDGE_ADDRESS_SIGNATURE, &calldata_values)?; - - initialize_contract_no_wait( - contract_addresses.on_chain_proposer_address, - on_chain_proposer_initialization_calldata, - initializer, - eth_client, - Overrides { - nonce: Some(initializer_nonce), - gas_limit: Some(TRANSACTION_GAS_LIMIT), - max_fee_per_gas: Some(gas_price), - max_priority_fee_per_gas: Some(gas_price), - ..Default::default() - }, - ) - .await? - }; - - info!( - tx_hash = %format!("{initialize_bridge_address_tx_hash:#x}"), - "OnChainProposer bridge address initialized" - ); - - tx_hashes.push(initialize_bridge_address_tx_hash); - } - - if opts.deploy_based_contracts && opts.on_chain_proposer_owner != initializer.address() { - let initializer_nonce = eth_client - .get_nonce( - initializer.address(), - BlockIdentifier::Tag(BlockTag::Pending), - ) - .await?; - let transfer_ownership_tx_hash = { - let owner_transfer_calldata = encode_calldata( - TRANSFER_OWNERSHIP_SIGNATURE, - &[Value::Address(opts.on_chain_proposer_owner)], - )?; - - initialize_contract_no_wait( - contract_addresses.on_chain_proposer_address, - owner_transfer_calldata, - initializer, - eth_client, - Overrides { - nonce: Some(initializer_nonce), - gas_limit: Some(TRANSACTION_GAS_LIMIT), - max_fee_per_gas: Some(gas_price), - max_priority_fee_per_gas: Some(gas_price), - ..Default::default() - }, - ) - .await? - }; - - tx_hashes.push(transfer_ownership_tx_hash); - - if let Some(owner_pk) = opts.on_chain_proposer_owner_pk { - let signer = Signer::Local(LocalSigner::new(owner_pk)); - let owner_nonce = eth_client - .get_nonce(signer.address(), BlockIdentifier::Tag(BlockTag::Pending)) - .await?; - let accept_ownership_calldata = encode_calldata(ACCEPT_OWNERSHIP_SIGNATURE, &[])?; - let accept_tx = build_generic_tx( - eth_client, - TxType::EIP1559, - contract_addresses.on_chain_proposer_address, - opts.on_chain_proposer_owner, - accept_ownership_calldata.into(), - Overrides { - nonce: Some(owner_nonce), - gas_limit: Some(TRANSACTION_GAS_LIMIT), - max_fee_per_gas: Some(gas_price), - max_priority_fee_per_gas: Some(gas_price), - ..Default::default() - }, - ) - .await?; - let accept_tx_hash = send_generic_transaction(eth_client, accept_tx, &signer).await?; - tx_hashes.push(accept_tx_hash); - - info!( - transfer_tx_hash = %format!("{transfer_ownership_tx_hash:#x}"), - accept_tx_hash = %format!("{accept_tx_hash:#x}"), - "OnChainProposer ownership transfered" - ); - } else { - info!( - transfer_tx_hash = %format!("{transfer_ownership_tx_hash:#x}"), - "OnChainProposer ownership transfered but not accepted yet" - ); - } - } - info!("Initializing CommonBridge"); let initialize_tx_hash = { let initializer_nonce = eth_client diff --git a/crates/l2/contracts/src/l1/based/OnChainProposer.sol b/crates/l2/contracts/src/l1/based/OnChainProposer.sol index b407a7f150..a29ccf5204 100644 --- a/crates/l2/contracts/src/l1/based/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/based/OnChainProposer.sol @@ -123,7 +123,8 @@ contract OnChainProposer is bytes32 risc0Vk, bytes32 genesisStateRoot, address sequencer_registry, - uint256 chainId + uint256 chainId, + address bridge ) public initializer { VALIDIUM = _validium; @@ -172,24 +173,13 @@ contract OnChainProposer is CHAIN_ID = chainId; - OwnableUpgradeable.__Ownable_init(owner); - } - - /// @inheritdoc IOnChainProposer - function initializeBridgeAddress(address bridge) public onlyOwner { - require( - BRIDGE == address(0), - "OnChainProposer: bridge already initialized" - ); require( bridge != address(0), "OnChainProposer: bridge is the zero address" ); - require( - bridge != address(this), - "OnChainProposer: bridge is the contract address" - ); BRIDGE = bridge; + + OwnableUpgradeable.__Ownable_init(owner); } /// @inheritdoc IOnChainProposer diff --git a/crates/l2/contracts/src/l1/based/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/based/interfaces/IOnChainProposer.sol index 0b4d6ca0ce..ef4071ccdb 100644 --- a/crates/l2/contracts/src/l1/based/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/based/interfaces/IOnChainProposer.sol @@ -29,13 +29,6 @@ interface IOnChainProposer { /// @param newVerificationKey The new verification key. event VerificationKeyUpgraded(string verifier, bytes32 newVerificationKey); - /// @notice Set the bridge address for the first time. - /// @dev This method is separated from initialize because both the CommonBridge - /// and the OnChainProposer need to know the address of the other. This solves - /// the circular dependency while allowing to initialize the proxy with the deploy. - /// @param bridge the address of the bridge contract. - function initializeBridgeAddress(address bridge) external; - /// @notice Upgrades the SP1 verification key that represents the sequencer's code. /// @param new_vk new verification key for SP1 verifier function upgradeSP1VerificationKey(bytes32 new_vk) external; From 35454091a5e736d2764e7958c65f80d188ddf505 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 17 Dec 2025 17:46:42 -0300 Subject: [PATCH 11/47] run cargo fmt --- cmd/ethrex/l2/command.rs | 6 ++---- crates/l2/sequencer/l1_proof_sender.rs | 9 ++------- crates/l2/sequencer/l1_proof_verifier.rs | 9 ++------- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/cmd/ethrex/l2/command.rs b/cmd/ethrex/l2/command.rs index 96683dfe07..414bfa4393 100644 --- a/cmd/ethrex/l2/command.rs +++ b/cmd/ethrex/l2/command.rs @@ -95,10 +95,8 @@ impl L2Command { .sequencer_opts .committer_opts .on_chain_proposer_address = Some(contract_addresses.on_chain_proposer_address); - l2_options - .sequencer_opts - .committer_opts - .timelock_address = Some(contract_addresses.timelock_address); + l2_options.sequencer_opts.committer_opts.timelock_address = + Some(contract_addresses.timelock_address); l2_options.sequencer_opts.watcher_opts.bridge_address = Some(contract_addresses.bridge_address); println!("Initializing L2"); diff --git a/crates/l2/sequencer/l1_proof_sender.rs b/crates/l2/sequencer/l1_proof_sender.rs index b4ca750404..e35a5f3105 100644 --- a/crates/l2/sequencer/l1_proof_sender.rs +++ b/crates/l2/sequencer/l1_proof_sender.rs @@ -403,13 +403,8 @@ impl L1ProofSender { .timelock_address .unwrap_or(self.on_chain_proposer_address); - let send_verify_tx_result = send_verify_tx( - calldata, - &self.eth_client, - target_address, - &self.signer, - ) - .await; + let send_verify_tx_result = + send_verify_tx(calldata, &self.eth_client, target_address, &self.signer).await; if let Err(EthClientError::EstimateGasError(EstimateGasError::RPCError(error))) = send_verify_tx_result.as_ref() diff --git a/crates/l2/sequencer/l1_proof_verifier.rs b/crates/l2/sequencer/l1_proof_verifier.rs index afcde0439e..0ac2e607a3 100644 --- a/crates/l2/sequencer/l1_proof_verifier.rs +++ b/crates/l2/sequencer/l1_proof_verifier.rs @@ -244,13 +244,8 @@ impl L1ProofVerifier { .timelock_address .unwrap_or(self.on_chain_proposer_address); - let send_verify_tx_result = send_verify_tx( - calldata, - &self.eth_client, - target_address, - &self.l1_signer, - ) - .await; + let send_verify_tx_result = + send_verify_tx(calldata, &self.eth_client, target_address, &self.l1_signer).await; if let Err(EthClientError::EstimateGasError(EstimateGasError::RPCError(error))) = send_verify_tx_result.as_ref() From 6a41d67859c9d8997f329af1853e6c2dd25d3272 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 17 Dec 2025 18:14:31 -0300 Subject: [PATCH 12/47] fix solidity lint in Timelock --- crates/l2/contracts/src/l1/Timelock.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 7b87e0d6ad..3d8c5d6f79 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -124,7 +124,9 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { } // Logic for updating Timelock contract. Should be triggered by the timelock itself so that it respects min time. - function _authorizeUpgrade(address newImplementation) internal override { + function _authorizeUpgrade( + address /*newImplementation*/ + ) internal view override { address sender = _msgSender(); if (sender != address(this)) { revert TimelockUnauthorizedCaller(sender); From ad29f8a77d3b20aa09dfd1d957152f17fca4388f Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 10:28:51 -0300 Subject: [PATCH 13/47] use onlySelf modifier instead --- crates/l2/contracts/src/l1/Timelock.sol | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 3d8c5d6f79..ab83fdb43e 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -15,6 +15,15 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { IOnChainProposer public onChainProposer; + // Used for functions in the timelock that should be triggered + modifier onlySelf() { + require( + msg.sender == address(this), + "Timelock: caller is not the timelock itself" + ); + _; + } + function initialize( uint256 minDelay, // This should be the minimum delay for contract upgrades in seconds (e.g. 7 days = 604800 sec). address[] memory sequencers, // Will be able to commit and verify batches. @@ -123,13 +132,9 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { emit EmergencyExecution(target, value, data); } - // Logic for updating Timelock contract. Should be triggered by the timelock itself so that it respects min time. + /// @notice Allow timelock itself to upgrade the contract in order to respect min time. + /// @param newImplementation the address of the new implementation function _authorizeUpgrade( - address /*newImplementation*/ - ) internal view override { - address sender = _msgSender(); - if (sender != address(this)) { - revert TimelockUnauthorizedCaller(sender); - } - } + address newImplementation + ) internal virtual override onlySelf {} } From 8a8df36bb886a96875fe69402291fc45a19b4f73 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 11:34:00 -0300 Subject: [PATCH 14/47] rename error in based contract --- crates/l2/contracts/src/l1/based/OnChainProposer.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/l2/contracts/src/l1/based/OnChainProposer.sol b/crates/l2/contracts/src/l1/based/OnChainProposer.sol index ed7a6ddd84..0010a46683 100644 --- a/crates/l2/contracts/src/l1/based/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/based/OnChainProposer.sol @@ -202,7 +202,7 @@ contract OnChainProposer is require( bridge != address(0), - "001" // OnChainProposer: bridge is the zero address + "OnChainProposer: bridge is the zero address" ); BRIDGE = bridge; From a5c12c701b07c39fe09a3295a50be263cf015cd1 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 11:48:33 -0300 Subject: [PATCH 15/47] require timelock address to be set if it's not based --- crates/l2/sequencer/l1_committer.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/l2/sequencer/l1_committer.rs b/crates/l2/sequencer/l1_committer.rs index 157ee71f4a..fb6819126c 100644 --- a/crates/l2/sequencer/l1_committer.rs +++ b/crates/l2/sequencer/l1_committer.rs @@ -1175,7 +1175,9 @@ impl L1Committer { let target_address = if !self.based { self.timelock_address - .unwrap_or(self.on_chain_proposer_address) + .ok_or(CommitterError::UnexpectedError( + "Timelock address is not set".to_string(), + ))? } else { self.on_chain_proposer_address }; From 9e740c40f317a9f8e1f5c0aad7d5fea7a6104f39 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 11:55:50 -0300 Subject: [PATCH 16/47] make tiny changes --- cmd/ethrex/l2/deployer.rs | 2 +- crates/l2/contracts/src/l1/OnChainProposer.sol | 2 +- crates/l2/sequencer/l1_committer.rs | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 91cabc1f13..1377a9e716 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -1078,7 +1078,7 @@ async fn initialize_contracts( .get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending)) .await?; let calldata_values = vec![ - Value::Uint(U256::from(30)), // TODO: change minDelay for testing: 30s + Value::Uint(U256::from(30)), // TODO: Make minDelay parametrizable. For now this is for testing purposes. Value::Array(vec![ // sequencers Value::Address(opts.committer_l1_address), diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 5f20c9783c..0c1e174cca 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -76,7 +76,7 @@ contract OnChainProposer is /// @dev Deprecated variable. bytes32 public SP1_VERIFICATION_KEY; - /// @notice Indicates whether the contract operates in validium mode.Add commentMore actions + /// @notice Indicates whether the contract operates in validium mode. /// @dev This value is immutable and can only be set during contract deployment. bool public VALIDIUM; diff --git a/crates/l2/sequencer/l1_committer.rs b/crates/l2/sequencer/l1_committer.rs index fb6819126c..769b11426e 100644 --- a/crates/l2/sequencer/l1_committer.rs +++ b/crates/l2/sequencer/l1_committer.rs @@ -70,8 +70,7 @@ use spawned_concurrency::tasks::{ const COMMIT_FUNCTION_SIGNATURE_BASED: &str = "commitBatch(uint256,bytes32,bytes32,bytes32,bytes32,uint256,bytes32,bytes[])"; -const COMMIT_FUNCTION_SIGNATURE: &str = "commitBatch(uint256,bytes32,bytes32,bytes32,bytes32,uint256,bytes32,(uint256,uint256,bytes32[])[],(uint256,bytes32)[]) -"; +const COMMIT_FUNCTION_SIGNATURE: &str = "commitBatch(uint256,bytes32,bytes32,bytes32,bytes32,uint256,bytes32,(uint256,uint256,bytes32[])[],(uint256,bytes32)[])"; /// Default wake up time for the committer to check if it should send a commit tx const COMMITTER_DEFAULT_WAKE_TIME_MS: u64 = 60_000; From f697e2310e5d1bdb41ba63b3a2f9af9c67fcc295 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 12:48:29 -0300 Subject: [PATCH 17/47] deploy timelock only on non-based --- cmd/ethrex/l2/deployer.rs | 76 ++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 1377a9e716..2ad6acac07 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -1071,43 +1071,47 @@ async fn initialize_contracts( let deployer_address = get_address_from_secret_key(&opts.private_key.secret_bytes()) .map_err(DeployerError::InternalError)?; - info!("Initializing Timelock"); - let initialize_tx_hash = { - let deployer = Signer::Local(LocalSigner::new(opts.private_key)); - let deployer_nonce = eth_client - .get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending)) - .await?; - let calldata_values = vec![ - Value::Uint(U256::from(30)), // TODO: Make minDelay parametrizable. For now this is for testing purposes. - Value::Array(vec![ - // sequencers - Value::Address(opts.committer_l1_address), - Value::Address(opts.proof_sender_l1_address), - ]), - Value::Address(opts.on_chain_proposer_owner), // owner - Value::Address(opts.on_chain_proposer_owner), // securityCouncil - Value::Address(contract_addresses.on_chain_proposer_address), // onChainProposer - ]; - let timelock_initialization_calldata = - encode_calldata(INITIALIZE_TIMELOCK_SIGNATURE, &calldata_values)?; + if !opts.deploy_based_contracts { + info!("Initializing Timelock"); + let initialize_tx_hash = { + let deployer = Signer::Local(LocalSigner::new(opts.private_key)); + let deployer_nonce = eth_client + .get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending)) + .await?; + let calldata_values = vec![ + Value::Uint(U256::from(30)), // TODO: Make minDelay parametrizable. For now this is for testing purposes. + Value::Array(vec![ + // sequencers + Value::Address(opts.committer_l1_address), + Value::Address(opts.proof_sender_l1_address), + ]), + Value::Address(opts.on_chain_proposer_owner), // owner + Value::Address(opts.on_chain_proposer_owner), // securityCouncil + Value::Address(contract_addresses.on_chain_proposer_address), // onChainProposer + ]; + let timelock_initialization_calldata = + encode_calldata(INITIALIZE_TIMELOCK_SIGNATURE, &calldata_values)?; - initialize_contract_no_wait( - contract_addresses.timelock_address, - timelock_initialization_calldata, - &deployer, - eth_client, - Overrides { - nonce: Some(deployer_nonce), - gas_limit: Some(TRANSACTION_GAS_LIMIT), - max_fee_per_gas: Some(gas_price), - max_priority_fee_per_gas: Some(gas_price), - ..Default::default() - }, - ) - .await? - }; - info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "Timelock initialized"); - tx_hashes.push(initialize_tx_hash); + initialize_contract_no_wait( + contract_addresses.timelock_address, + timelock_initialization_calldata, + &deployer, + eth_client, + Overrides { + nonce: Some(deployer_nonce), + gas_limit: Some(TRANSACTION_GAS_LIMIT), + max_fee_per_gas: Some(gas_price), + max_priority_fee_per_gas: Some(gas_price), + ..Default::default() + }, + ) + .await? + }; + info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "Timelock initialized"); + tx_hashes.push(initialize_tx_hash); + } else { + info!("Skipping Timelock initialization (based enabled)"); + } info!("Initializing OnChainProposer"); From e4dd641902b883d0f6c0069c73630d37be8a413e Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 12:48:42 -0300 Subject: [PATCH 18/47] improve comments in timelock contract --- crates/l2/contracts/src/l1/Timelock.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index a6102b975c..2a3871a4ba 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -3,7 +3,6 @@ pragma solidity =0.8.31; import "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import {IOnChainProposer} from "./interfaces/IOnChainProposer.sol"; import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; @@ -28,7 +27,7 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { uint256 minDelay, // This should be the minimum delay for contract upgrades in seconds (e.g. 7 days = 604800 sec). address[] memory sequencers, // Will be able to commit and verify batches. address owner, // Will be able to propose and execute functions, respecting the delay. - address securityCouncil, // TODO: Admin role -> Can manage roles. But it can't schedule/execute by itself, maybe we should add that + address securityCouncil, // Can manage roles (admin) and also can bypass delay times thanks to the SECURITY_COUNCIL role. address _onChainProposer // deployed OnChainProposer contract. ) public initializer { for (uint256 i = 0; i < sequencers.length; ++i) { @@ -50,7 +49,7 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { onChainProposer = IOnChainProposer(_onChainProposer); } - //TODO: In the future commit and verify will have timelock logic incorporated in case there are any zkVM bugs and we want to avoid applying the changes in the L1. Probably the Security Council would act upon those changes. + // NOTE: In the future commit and verify will have timelock logic incorporated in case there are any zkVM bugs and we want to avoid applying the changes in the L1. Probably the Security Council would act upon those changes. function commitBatch( uint256 batchNumber, bytes32 newStateRoot, From 43589a80d43aa85527cc6186fbcf5e529ed0ba9e Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 13:16:05 -0300 Subject: [PATCH 19/47] add comments to Timelock contract --- crates/l2/contracts/src/l1/Timelock.sol | 40 ++++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 2a3871a4ba..e735450a22 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -6,15 +6,29 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {IOnChainProposer} from "./interfaces/IOnChainProposer.sol"; import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; +/// @title Timelock contract. +/// @author LambdaClass +/// @notice The Timelock contract is the owner of the OnChainProposer contract, it gates access to it by managing roles +/// and adding delay to specific operations for some roles (e.g. updating the contract, in order to provide an exit window). contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { + /// @notice Role identifier for sequencers. + /// @dev Accounts with this role can commit and verify batches. bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); + + /// @notice Role identifier for the Security Council. + /// @dev Accounts with this role can manage roles and bypass the timelock delay. bytes32 public constant SECURITY_COUNCIL = keccak256("SECURITY_COUNCIL"); + /// @notice Emitted when the Security Council executes a call bypassing the delay. + /// @param target The address that was called. + /// @param value The ETH value that was sent. + /// @param data The calldata that was forwarded to `target`. event EmergencyExecution(address indexed target, uint256 value, bytes data); + /// @notice The OnChainProposer contract controlled by this timelock. IOnChainProposer public onChainProposer; - // Used for functions in the timelock that should be triggered + /// @dev Restricts calls to the timelock itself. modifier onlySelf() { require( msg.sender == address(this), @@ -23,12 +37,19 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { _; } + /// @notice Initializes the timelock contract. + /// @dev Called once after proxy deployment. + /// @param minDelay The minimum delay (in seconds) for scheduled operations. + /// @param sequencers Accounts that can commit and verify batches. + /// @param owner The account that can propose and execute operations, respecting the delay. + /// @param securityCouncil The Security Council account that can manage roles and bypass the delay. + /// @param _onChainProposer The deployed `OnChainProposer` contract address. function initialize( - uint256 minDelay, // This should be the minimum delay for contract upgrades in seconds (e.g. 7 days = 604800 sec). - address[] memory sequencers, // Will be able to commit and verify batches. - address owner, // Will be able to propose and execute functions, respecting the delay. - address securityCouncil, // Can manage roles (admin) and also can bypass delay times thanks to the SECURITY_COUNCIL role. - address _onChainProposer // deployed OnChainProposer contract. + uint256 minDelay, + address[] memory sequencers, + address owner, + address securityCouncil, + address _onChainProposer ) public initializer { for (uint256 i = 0; i < sequencers.length; ++i) { _grantRole(SEQUENCER, sequencers[i]); @@ -122,8 +143,11 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { onChainProposer.unpause(); } - // In case a bug is detected the Security Council can act immediately. - // Ideally in the future this should be possible only if the bug is detected on-chain with a proper mechanism. + /// @notice Executes an operation immediately, bypassing the timelock delay. + /// @dev Intended for emergency use by the Security Council. + /// @param target The address to call. + /// @param value The ETH value to send with the call. + /// @param data The calldata to forward to `target`. function emergencyExecute( address target, uint256 value, From 1e6c0b2a1f5e79ec542a1417a5d6ace9f4a6d9f2 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 13:19:39 -0300 Subject: [PATCH 20/47] rename owner to governance --- crates/l2/contracts/src/l1/Timelock.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index e735450a22..f6f3f378a8 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -41,13 +41,13 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { /// @dev Called once after proxy deployment. /// @param minDelay The minimum delay (in seconds) for scheduled operations. /// @param sequencers Accounts that can commit and verify batches. - /// @param owner The account that can propose and execute operations, respecting the delay. + /// @param governance The account that can propose and execute operations, respecting the delay. /// @param securityCouncil The Security Council account that can manage roles and bypass the delay. /// @param _onChainProposer The deployed `OnChainProposer` contract address. function initialize( uint256 minDelay, address[] memory sequencers, - address owner, + address governance, address securityCouncil, address _onChainProposer ) public initializer { @@ -57,13 +57,13 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { _grantRole(SECURITY_COUNCIL, securityCouncil); - address[] memory owners = new address[](1); - owners[0] = owner; + address[] memory governanceAccounts = new address[](1); + governanceAccounts[0] = governance; TimelockControllerUpgradeable.__TimelockController_init( minDelay, - owners, // proposers - owners, // executors + governanceAccounts, // proposers + governanceAccounts, // executors securityCouncil // admin ); From 1369951869c5e35cdbe9283d1f6b14eeb2a7079e Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 17:09:55 -0300 Subject: [PATCH 21/47] disable initialize function from TimelockControllerUpgradeable --- crates/l2/contracts/src/l1/Timelock.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index f6f3f378a8..cfdfc990f2 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -37,6 +37,16 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { _; } + /// @notice Disables the parent initialize function to prevent accidental misuse. + function initialize( + uint256, + address[] memory, + address[] memory, + address + ) public pure override { + revert("Timelock: use the custom initialize function"); + } + /// @notice Initializes the timelock contract. /// @dev Called once after proxy deployment. /// @param minDelay The minimum delay (in seconds) for scheduled operations. From 7edc1e0e15f3f111e9a4ec5e3e0e9532a5a5637f Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 19:02:16 -0300 Subject: [PATCH 22/47] Make Timelock inherit IOnChainProposer --- crates/l2/contracts/src/l1/Timelock.sol | 42 ++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index cfdfc990f2..7cde509261 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -10,7 +10,7 @@ import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; /// @author LambdaClass /// @notice The Timelock contract is the owner of the OnChainProposer contract, it gates access to it by managing roles /// and adding delay to specific operations for some roles (e.g. updating the contract, in order to provide an exit window). -contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { +contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, IOnChainProposer { /// @notice Role identifier for sequencers. /// @dev Accounts with this role can commit and verify batches. bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); @@ -80,7 +80,37 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { onChainProposer = IOnChainProposer(_onChainProposer); } + /// @inheritdoc IOnChainProposer + function lastCommittedBatch() external view returns (uint256) { + return onChainProposer.lastCommittedBatch(); + } + + /// @inheritdoc IOnChainProposer + function lastVerifiedBatch() external view returns (uint256) { + return onChainProposer.lastVerifiedBatch(); + } + + /// @inheritdoc IOnChainProposer + /// @custom:access Only callable by the timelock itself. + function upgradeSP1VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) external onlySelf { + onChainProposer.upgradeSP1VerificationKey(commit_hash, new_vk); + } + + /// @inheritdoc IOnChainProposer + /// @custom:access Only callable by the timelock itself. + function upgradeRISC0VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) external onlySelf { + onChainProposer.upgradeRISC0VerificationKey(commit_hash, new_vk); + } + // NOTE: In the future commit and verify will have timelock logic incorporated in case there are any zkVM bugs and we want to avoid applying the changes in the L1. Probably the Security Council would act upon those changes. + /// @inheritdoc IOnChainProposer + /// @custom:access Restricted to accounts with the `SEQUENCER` role. function commitBatch( uint256 batchNumber, bytes32 newStateRoot, @@ -105,6 +135,8 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { ); } + /// @inheritdoc IOnChainProposer + /// @custom:access Restricted to accounts with the `SEQUENCER` role. function verifyBatch( uint256 batchNumber, bytes memory risc0BlockProof, @@ -125,6 +157,8 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { ); } + /// @inheritdoc IOnChainProposer + /// @custom:access Restricted to accounts with the `SEQUENCER` role. function verifyBatchesAligned( uint256 firstBatchNumber, bytes[] calldata publicInputsList, @@ -139,16 +173,22 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable { ); } + /// @inheritdoc IOnChainProposer + /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. function revertBatch( uint256 batchNumber ) external onlyRole(SECURITY_COUNCIL) { onChainProposer.revertBatch(batchNumber); } + /// @inheritdoc IOnChainProposer + /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. function pause() external onlyRole(SECURITY_COUNCIL) { onChainProposer.pause(); } + /// @inheritdoc IOnChainProposer + /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. function unpause() external onlyRole(SECURITY_COUNCIL) { onChainProposer.unpause(); } From f68872f1327681d8d76bac12ac34c70eea2e368c Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 18 Dec 2025 19:11:55 -0300 Subject: [PATCH 23/47] let security council upgrade vks --- crates/l2/contracts/src/l1/Timelock.sol | 42 ++++++++++++++----------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 7cde509261..b80f60240d 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -10,7 +10,11 @@ import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; /// @author LambdaClass /// @notice The Timelock contract is the owner of the OnChainProposer contract, it gates access to it by managing roles /// and adding delay to specific operations for some roles (e.g. updating the contract, in order to provide an exit window). -contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, IOnChainProposer { +contract Timelock is + TimelockControllerUpgradeable, + UUPSUpgradeable, + IOnChainProposer +{ /// @notice Role identifier for sequencers. /// @dev Accounts with this role can commit and verify batches. bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); @@ -90,24 +94,6 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, IOnChainPro return onChainProposer.lastVerifiedBatch(); } - /// @inheritdoc IOnChainProposer - /// @custom:access Only callable by the timelock itself. - function upgradeSP1VerificationKey( - bytes32 commit_hash, - bytes32 new_vk - ) external onlySelf { - onChainProposer.upgradeSP1VerificationKey(commit_hash, new_vk); - } - - /// @inheritdoc IOnChainProposer - /// @custom:access Only callable by the timelock itself. - function upgradeRISC0VerificationKey( - bytes32 commit_hash, - bytes32 new_vk - ) external onlySelf { - onChainProposer.upgradeRISC0VerificationKey(commit_hash, new_vk); - } - // NOTE: In the future commit and verify will have timelock logic incorporated in case there are any zkVM bugs and we want to avoid applying the changes in the L1. Probably the Security Council would act upon those changes. /// @inheritdoc IOnChainProposer /// @custom:access Restricted to accounts with the `SEQUENCER` role. @@ -193,6 +179,24 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, IOnChainPro onChainProposer.unpause(); } + /// @inheritdoc IOnChainProposer + /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. + function upgradeSP1VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) external onlyRole(SECURITY_COUNCIL) { + onChainProposer.upgradeSP1VerificationKey(commit_hash, new_vk); + } + + /// @inheritdoc IOnChainProposer + /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. + function upgradeRISC0VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) external onlyRole(SECURITY_COUNCIL) { + onChainProposer.upgradeRISC0VerificationKey(commit_hash, new_vk); + } + /// @notice Executes an operation immediately, bypassing the timelock delay. /// @dev Intended for emergency use by the Security Council. /// @param target The address to call. From 90d7add24d3ca4c8892ce5aebd5b39d6f2f72ae5 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 12:01:32 -0300 Subject: [PATCH 24/47] try fix timelock bug --- cmd/ethrex/l2/command.rs | 2 +- cmd/ethrex/l2/deployer.rs | 84 +++++++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/cmd/ethrex/l2/command.rs b/cmd/ethrex/l2/command.rs index a70478a3ec..07c8e9cef6 100644 --- a/cmd/ethrex/l2/command.rs +++ b/cmd/ethrex/l2/command.rs @@ -96,7 +96,7 @@ impl L2Command { .committer_opts .on_chain_proposer_address = Some(contract_addresses.on_chain_proposer_address); l2_options.sequencer_opts.committer_opts.timelock_address = - Some(contract_addresses.timelock_address); + contract_addresses.timelock_address; l2_options.sequencer_opts.watcher_opts.bridge_address = Some(contract_addresses.bridge_address); println!("Initializing L2"); diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 2ad6acac07..7fc4064639 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -572,7 +572,7 @@ pub struct ContractAddresses { pub sequencer_registry_address: Address, pub aligned_aggregator_address: Address, pub router: Option
, - pub timelock_address: Address, + pub timelock_address: Option
, } pub async fn deploy_l1_contracts( @@ -724,32 +724,40 @@ async fn deploy_contracts( Default::default() }; - info!("Deploying Timelock"); + let (timelock_deployment, timelock_address) = if !opts.deploy_based_contracts { + info!("Deploying Timelock"); - let timelock_deployment = deploy_with_proxy_from_bytecode_no_wait( - deployer, - eth_client, - TIMELOCK_BYTECODE, - &salt, - Overrides { - nonce: Some(nonce), - gas_limit: Some(TRANSACTION_GAS_LIMIT), - max_fee_per_gas: Some(gas_price), - max_priority_fee_per_gas: Some(gas_price), - ..Default::default() - }, - ) - .await?; + let timelock_deployment = deploy_with_proxy_from_bytecode_no_wait( + deployer, + eth_client, + TIMELOCK_BYTECODE, + &salt, + Overrides { + nonce: Some(nonce), + gas_limit: Some(TRANSACTION_GAS_LIMIT), + max_fee_per_gas: Some(gas_price), + max_priority_fee_per_gas: Some(gas_price), + ..Default::default() + }, + ) + .await?; - nonce += 2; + nonce += 2; - info!( - "Timelock deployed:\n Proxy -> address={:#x}, tx_hash={:#x}\n Impl -> address={:#x}, tx_hash={:#x}", - timelock_deployment.proxy_address, - timelock_deployment.proxy_tx_hash, - timelock_deployment.implementation_address, - timelock_deployment.implementation_tx_hash, - ); + info!( + "Timelock deployed:\n Proxy -> address={:#x}, tx_hash={:#x}\n Impl -> address={:#x}, tx_hash={:#x}", + timelock_deployment.proxy_address, + timelock_deployment.proxy_tx_hash, + timelock_deployment.implementation_address, + timelock_deployment.implementation_tx_hash, + ); + ( + Some(timelock_deployment.clone()), + Some(timelock_deployment.proxy_address), + ) + } else { + (None, None) + }; info!("Deploying OnChainProposer"); @@ -923,9 +931,7 @@ async fn deploy_contracts( tdx_verifier_address = ?tdx_verifier_address, "Contracts deployed" ); - let receipts = vec![ - timelock_deployment.implementation_tx_hash, - timelock_deployment.proxy_tx_hash, + let mut receipts = vec![ on_chain_proposer_deployment.implementation_tx_hash, on_chain_proposer_deployment.proxy_tx_hash, bridge_deployment.implementation_tx_hash, @@ -937,6 +943,11 @@ async fn deploy_contracts( router_deployment.proxy_tx_hash, ]; + if let Some(timelock_deployment) = timelock_deployment { + receipts.push(timelock_deployment.implementation_tx_hash); + receipts.push(timelock_deployment.proxy_tx_hash); + } + Ok(( ContractAddresses { on_chain_proposer_address: on_chain_proposer_deployment.proxy_address, @@ -947,7 +958,7 @@ async fn deploy_contracts( sequencer_registry_address: sequencer_registry_deployment.proxy_address, aligned_aggregator_address, router: opts.router.or(Some(router_deployment.proxy_address)), - timelock_address: timelock_deployment.proxy_address, + timelock_address, }, receipts, )) @@ -1072,6 +1083,9 @@ async fn initialize_contracts( .map_err(DeployerError::InternalError)?; if !opts.deploy_based_contracts { + let timelock_address = contract_addresses.timelock_address.ok_or( + DeployerError::InternalError("Timelock address missing".to_string()), + )?; info!("Initializing Timelock"); let initialize_tx_hash = { let deployer = Signer::Local(LocalSigner::new(opts.private_key)); @@ -1093,7 +1107,7 @@ async fn initialize_contracts( encode_calldata(INITIALIZE_TIMELOCK_SIGNATURE, &calldata_values)?; initialize_contract_no_wait( - contract_addresses.timelock_address, + timelock_address, timelock_initialization_calldata, &deployer, eth_client, @@ -1230,7 +1244,9 @@ async fn initialize_contracts( // Initialize only OnChainProposer without Based config let calldata_values = vec![ Value::Bool(opts.validium), - Value::Address(contract_addresses.timelock_address), + Value::Address(contract_addresses.timelock_address.ok_or( + DeployerError::InternalError("Timelock address missing".to_string()), + )?), Value::Bool(opts.risc0), Value::Bool(opts.sp1), Value::Bool(opts.tdx), @@ -1557,11 +1573,9 @@ fn write_contract_addresses_to_env( "ETHREX_COMMITTER_ON_CHAIN_PROPOSER_ADDRESS={:#x}", contract_addresses.on_chain_proposer_address )?; - writeln!( - writer, - "ETHREX_TIMELOCK_ADDRESS={:#x}", - contract_addresses.timelock_address - )?; + if let Some(timelock_address) = contract_addresses.timelock_address { + writeln!(writer, "ETHREX_TIMELOCK_ADDRESS={:#x}", timelock_address)?; + } writeln!( writer, "ETHREX_WATCHER_BRIDGE_ADDRESS={:#x}", From 21a4d5dc361e97ae17c957cce896ee14052e6be1 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 12:32:59 -0300 Subject: [PATCH 25/47] add Clone --- crates/l2/sdk/src/sdk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/l2/sdk/src/sdk.rs b/crates/l2/sdk/src/sdk.rs index 8c7d963493..724ac43ca1 100644 --- a/crates/l2/sdk/src/sdk.rs +++ b/crates/l2/sdk/src/sdk.rs @@ -378,7 +378,7 @@ pub const CREATE2DEPLOYER_ADDRESS: Address = H160([ 0x1a, 0xe9, 0xbe, 0xf2, ]); -#[derive(Default)] +#[derive(Default, Clone)] pub struct ProxyDeployment { pub proxy_address: Address, pub proxy_tx_hash: H256, From f8bce2a0e5bf7eb6a11f38cc37998669755e0f82 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 12:35:37 -0300 Subject: [PATCH 26/47] run cargo fmt --- cmd/ethrex/l2/deployer.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 7fc4064639..f28cab9177 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -1083,9 +1083,12 @@ async fn initialize_contracts( .map_err(DeployerError::InternalError)?; if !opts.deploy_based_contracts { - let timelock_address = contract_addresses.timelock_address.ok_or( - DeployerError::InternalError("Timelock address missing".to_string()), - )?; + let timelock_address = + contract_addresses + .timelock_address + .ok_or(DeployerError::InternalError( + "Timelock address missing".to_string(), + ))?; info!("Initializing Timelock"); let initialize_tx_hash = { let deployer = Signer::Local(LocalSigner::new(opts.private_key)); From fa3c723b47041a2069f29128a13d1886361749d1 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 16:51:37 -0300 Subject: [PATCH 27/47] make tdx contract point to the timelock and this one implements authorizedSequencerAddresses --- cmd/ethrex/l2/deployer.rs | 5 ++++- crates/l2/contracts/src/l1/Timelock.sol | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index f28cab9177..274e56fe7e 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -897,13 +897,16 @@ async fn deploy_contracts( _ => Address::zero(), }; + let tdx_controller_address = + timelock_address.unwrap_or(on_chain_proposer_deployment.proxy_address); + // if it's a required proof type, but no address has been specified, deploy it. let tdx_verifier_address = match opts.tdx_verifier_address { Some(addr) if opts.tdx => addr, None if opts.tdx => { info!("Deploying TDXVerifier (if tdx_deploy_verifier is true)"); let tdx_verifier_address = - deploy_tdx_contracts(opts, on_chain_proposer_deployment.proxy_address)?; + deploy_tdx_contracts(opts, tdx_controller_address)?; info!(address = %format!("{tdx_verifier_address:#x}"), "TDXVerifier deployed"); tdx_verifier_address diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index b80f60240d..eac79a6ad8 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -94,6 +94,14 @@ contract Timelock is return onChainProposer.lastVerifiedBatch(); } + /// @notice Returns whether an address has the sequencer role. + /// @dev This matches the legacy OnChainProposer mapping used by TDXVerifier. + function authorizedSequencerAddresses( + address addr + ) external view returns (bool) { + return hasRole(SEQUENCER, addr); + } + // NOTE: In the future commit and verify will have timelock logic incorporated in case there are any zkVM bugs and we want to avoid applying the changes in the L1. Probably the Security Council would act upon those changes. /// @inheritdoc IOnChainProposer /// @custom:access Restricted to accounts with the `SEQUENCER` role. From 3e6b14b51f4a6717a13cc6960758abef027522e6 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 17:04:33 -0300 Subject: [PATCH 28/47] cargo fmt --- cmd/ethrex/l2/deployer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 274e56fe7e..c90f885f6a 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -905,8 +905,7 @@ async fn deploy_contracts( Some(addr) if opts.tdx => addr, None if opts.tdx => { info!("Deploying TDXVerifier (if tdx_deploy_verifier is true)"); - let tdx_verifier_address = - deploy_tdx_contracts(opts, tdx_controller_address)?; + let tdx_verifier_address = deploy_tdx_contracts(opts, tdx_controller_address)?; info!(address = %format!("{tdx_verifier_address:#x}"), "TDXVerifier deployed"); tdx_verifier_address From 4442465028b8bd58adf7c38fbaf1226b3622f0b1 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 17:35:55 -0300 Subject: [PATCH 29/47] Add authorizedSequencerAddresses to interface and implement it as delegation in the OnChainProposer --- crates/l2/contracts/src/l1/OnChainProposer.sol | 8 ++++++++ .../l2/contracts/src/l1/interfaces/IOnChainProposer.sol | 6 ++++++ crates/l2/tee/contracts/Makefile | 2 +- crates/l2/tee/contracts/src/TDXVerifier.sol | 9 +++------ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 0c1e174cca..82d0e21024 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -102,6 +102,14 @@ contract OnChainProposer is /// @notice True if verification is done through Aligned Layer instead of smart contract verifiers. bool public ALIGNED_MODE; + /// @inheritdoc IOnChainProposer + function authorizedSequencerAddresses( + address addr + ) external view returns (bool) { + return + IOnChainProposer(owner()).authorizedSequencerAddresses(addr); + } + /// @notice Verification keys keyed by git commit hash (keccak of the commit SHA string) and verifier type. mapping(bytes32 commitHash => mapping(uint8 verifierId => bytes32 vk)) public verificationKeys; diff --git a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol index 6d4ebec4b2..43a006ca7e 100644 --- a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol @@ -16,6 +16,12 @@ interface IOnChainProposer { /// @return The latest verified batch number as a uint256. function lastVerifiedBatch() external view returns (uint256); + /// @notice Returns whether an address is authorized to commit and verify batches. + /// @param addr The address to query. + function authorizedSequencerAddresses( + address addr + ) external view returns (bool); + /// @notice A batch has been committed. /// @dev Event emitted when a batch is committed. /// @param newStateRoot The new state root of the batch that was committed. diff --git a/crates/l2/tee/contracts/Makefile b/crates/l2/tee/contracts/Makefile index b6d6afe09e..1db181ca23 100644 --- a/crates/l2/tee/contracts/Makefile +++ b/crates/l2/tee/contracts/Makefile @@ -42,7 +42,7 @@ lib/openzeppelin-contracts: solc_out/TDXVerifier.bin: src/TDXVerifier.sol lib/openzeppelin-contracts mkdir -p solc_out - solc src/TDXVerifier.sol --bin --allow-paths lib/ -o solc_out/ --overwrite + solc src/TDXVerifier.sol --bin --allow-paths lib/,../../.. @openzeppelin/=lib/openzeppelin-contracts/ -o solc_out/ --overwrite deploy: solc_out/TDXVerifier.bin $(eval CONTRACT_BIN := $(shell cat solc_out/TDXVerifier.bin)) diff --git a/crates/l2/tee/contracts/src/TDXVerifier.sol b/crates/l2/tee/contracts/src/TDXVerifier.sol index c8f9bdac9f..805280b75f 100644 --- a/crates/l2/tee/contracts/src/TDXVerifier.sol +++ b/crates/l2/tee/contracts/src/TDXVerifier.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -import "../lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import "../lib/openzeppelin-contracts/contracts/utils/cryptography/MessageHashUtils.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {IOnChainProposer} from "../../../contracts/src/l1/interfaces/IOnChainProposer.sol"; interface IAttestation { function verifyAndAttestOnChain(bytes calldata rawQuote) @@ -11,10 +12,6 @@ interface IAttestation { returns (bool success, bytes memory output); } -interface IOnChainProposer { - function authorizedSequencerAddresses(address addr) external returns (bool isAuthorized); -} - contract TDXVerifier { IAttestation public quoteVerifier = IAttestation(address(0)); IOnChainProposer public onChainProposer = IOnChainProposer(address(0)); From f3b40dd37f06b22ba8819bf7c43c07cfea8597a6 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 17:43:30 -0300 Subject: [PATCH 30/47] rename owner to timelock_owner --- crates/l2/contracts/src/l1/OnChainProposer.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 82d0e21024..fa659e64b4 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -116,14 +116,14 @@ contract OnChainProposer is /// @notice Initializes the contract. /// @dev This method is called only once after the contract is deployed. - /// @dev It sets the bridge address. - /// @param owner the address of the owner who can perform upgrades. + /// @dev The owner is expected to be the Timelock contract. + /// @param timelock_owner the Timelock address that can perform upgrades. /// @param alignedProofAggregator the address of the alignedProofAggregatorService contract. /// @param r0verifier the address of the risc0 groth16 verifier. /// @param sp1verifier the address of the sp1 groth16 verifier. function initialize( bool _validium, - address owner, + address timelock_owner, bool requireRisc0Proof, bool requireSp1Proof, bool requireTdxProof, @@ -192,7 +192,7 @@ contract OnChainProposer is ); BRIDGE = bridge; - OwnableUpgradeable.__Ownable_init(owner); + OwnableUpgradeable.__Ownable_init(timelock_owner); } /// @inheritdoc IOnChainProposer From da63fe66bf0a2f80186b4998d39c27d422bd1b1c Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 18:06:18 -0300 Subject: [PATCH 31/47] tidy and improve Timelock contract, create interface --- .../l2/contracts/src/l1/OnChainProposer.sol | 8 -- crates/l2/contracts/src/l1/Timelock.sol | 68 +--------------- .../src/l1/interfaces/IOnChainProposer.sol | 6 -- .../contracts/src/l1/interfaces/ITimelock.sol | 78 +++++++++++++++++++ crates/l2/tee/contracts/src/TDXVerifier.sol | 14 ++-- 5 files changed, 89 insertions(+), 85 deletions(-) create mode 100644 crates/l2/contracts/src/l1/interfaces/ITimelock.sol diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index fa659e64b4..96648f2d3a 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -102,14 +102,6 @@ contract OnChainProposer is /// @notice True if verification is done through Aligned Layer instead of smart contract verifiers. bool public ALIGNED_MODE; - /// @inheritdoc IOnChainProposer - function authorizedSequencerAddresses( - address addr - ) external view returns (bool) { - return - IOnChainProposer(owner()).authorizedSequencerAddresses(addr); - } - /// @notice Verification keys keyed by git commit hash (keccak of the commit SHA string) and verifier type. mapping(bytes32 commitHash => mapping(uint8 verifierId => bytes32 vk)) public verificationKeys; diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index eac79a6ad8..97b1604bf6 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -5,16 +5,13 @@ import "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgrade import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {IOnChainProposer} from "./interfaces/IOnChainProposer.sol"; import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; +import {ITimelock} from "./interfaces/ITimelock.sol"; /// @title Timelock contract. /// @author LambdaClass /// @notice The Timelock contract is the owner of the OnChainProposer contract, it gates access to it by managing roles /// and adding delay to specific operations for some roles (e.g. updating the contract, in order to provide an exit window). -contract Timelock is - TimelockControllerUpgradeable, - UUPSUpgradeable, - IOnChainProposer -{ +contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, ITimelock { /// @notice Role identifier for sequencers. /// @dev Accounts with this role can commit and verify batches. bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); @@ -23,12 +20,6 @@ contract Timelock is /// @dev Accounts with this role can manage roles and bypass the timelock delay. bytes32 public constant SECURITY_COUNCIL = keccak256("SECURITY_COUNCIL"); - /// @notice Emitted when the Security Council executes a call bypassing the delay. - /// @param target The address that was called. - /// @param value The ETH value that was sent. - /// @param data The calldata that was forwarded to `target`. - event EmergencyExecution(address indexed target, uint256 value, bytes data); - /// @notice The OnChainProposer contract controlled by this timelock. IOnChainProposer public onChainProposer; @@ -47,7 +38,7 @@ contract Timelock is address[] memory, address[] memory, address - ) public pure override { + ) public pure override(TimelockControllerUpgradeable) { revert("Timelock: use the custom initialize function"); } @@ -64,7 +55,7 @@ contract Timelock is address governance, address securityCouncil, address _onChainProposer - ) public initializer { + ) public override(ITimelock) initializer { for (uint256 i = 0; i < sequencers.length; ++i) { _grantRole(SEQUENCER, sequencers[i]); } @@ -84,16 +75,6 @@ contract Timelock is onChainProposer = IOnChainProposer(_onChainProposer); } - /// @inheritdoc IOnChainProposer - function lastCommittedBatch() external view returns (uint256) { - return onChainProposer.lastCommittedBatch(); - } - - /// @inheritdoc IOnChainProposer - function lastVerifiedBatch() external view returns (uint256) { - return onChainProposer.lastVerifiedBatch(); - } - /// @notice Returns whether an address has the sequencer role. /// @dev This matches the legacy OnChainProposer mapping used by TDXVerifier. function authorizedSequencerAddresses( @@ -103,7 +84,6 @@ contract Timelock is } // NOTE: In the future commit and verify will have timelock logic incorporated in case there are any zkVM bugs and we want to avoid applying the changes in the L1. Probably the Security Council would act upon those changes. - /// @inheritdoc IOnChainProposer /// @custom:access Restricted to accounts with the `SEQUENCER` role. function commitBatch( uint256 batchNumber, @@ -129,7 +109,6 @@ contract Timelock is ); } - /// @inheritdoc IOnChainProposer /// @custom:access Restricted to accounts with the `SEQUENCER` role. function verifyBatch( uint256 batchNumber, @@ -151,7 +130,6 @@ contract Timelock is ); } - /// @inheritdoc IOnChainProposer /// @custom:access Restricted to accounts with the `SEQUENCER` role. function verifyBatchesAligned( uint256 firstBatchNumber, @@ -167,44 +145,6 @@ contract Timelock is ); } - /// @inheritdoc IOnChainProposer - /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. - function revertBatch( - uint256 batchNumber - ) external onlyRole(SECURITY_COUNCIL) { - onChainProposer.revertBatch(batchNumber); - } - - /// @inheritdoc IOnChainProposer - /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. - function pause() external onlyRole(SECURITY_COUNCIL) { - onChainProposer.pause(); - } - - /// @inheritdoc IOnChainProposer - /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. - function unpause() external onlyRole(SECURITY_COUNCIL) { - onChainProposer.unpause(); - } - - /// @inheritdoc IOnChainProposer - /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. - function upgradeSP1VerificationKey( - bytes32 commit_hash, - bytes32 new_vk - ) external onlyRole(SECURITY_COUNCIL) { - onChainProposer.upgradeSP1VerificationKey(commit_hash, new_vk); - } - - /// @inheritdoc IOnChainProposer - /// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role. - function upgradeRISC0VerificationKey( - bytes32 commit_hash, - bytes32 new_vk - ) external onlyRole(SECURITY_COUNCIL) { - onChainProposer.upgradeRISC0VerificationKey(commit_hash, new_vk); - } - /// @notice Executes an operation immediately, bypassing the timelock delay. /// @dev Intended for emergency use by the Security Council. /// @param target The address to call. diff --git a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol index 43a006ca7e..6d4ebec4b2 100644 --- a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol @@ -16,12 +16,6 @@ interface IOnChainProposer { /// @return The latest verified batch number as a uint256. function lastVerifiedBatch() external view returns (uint256); - /// @notice Returns whether an address is authorized to commit and verify batches. - /// @param addr The address to query. - function authorizedSequencerAddresses( - address addr - ) external view returns (bool); - /// @notice A batch has been committed. /// @dev Event emitted when a batch is committed. /// @param newStateRoot The new state root of the batch that was committed. diff --git a/crates/l2/contracts/src/l1/interfaces/ITimelock.sol b/crates/l2/contracts/src/l1/interfaces/ITimelock.sol new file mode 100644 index 0000000000..1d5d8bee28 --- /dev/null +++ b/crates/l2/contracts/src/l1/interfaces/ITimelock.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.31; + +import {IOnChainProposer} from "./IOnChainProposer.sol"; +import {ICommonBridge} from "./ICommonBridge.sol"; + +/// @title Interface for the Timelock contract. +/// @author LambdaClass +/// @notice Timelock owner for OnChainProposer, manages roles and forwards sequencer actions. +interface ITimelock { + /// @notice Emitted when the Security Council executes a call bypassing the delay. + /// @param target The address that was called. + /// @param value The ETH value that was sent. + /// @param data The calldata that was forwarded to `target`. + event EmergencyExecution(address indexed target, uint256 value, bytes data); + + /// @notice The OnChainProposer contract controlled by this timelock. + function onChainProposer() external view returns (IOnChainProposer); + + /// @notice Initializes the timelock contract. + /// @dev Called once after proxy deployment. + /// @param minDelay The minimum delay (in seconds) for scheduled operations. + /// @param sequencers Accounts that can commit and verify batches. + /// @param governance The account that can propose and execute operations, respecting the delay. + /// @param securityCouncil The Security Council account that can manage roles and bypass the delay. + /// @param _onChainProposer The deployed `OnChainProposer` contract address. + function initialize( + uint256 minDelay, + address[] memory sequencers, + address governance, + address securityCouncil, + address _onChainProposer + ) external; + + /// @notice Returns whether an address has the sequencer role. + function authorizedSequencerAddresses( + address addr + ) external view returns (bool); + + /// @notice Commits a batch through the timelock. + function commitBatch( + uint256 batchNumber, + bytes32 newStateRoot, + bytes32 withdrawalsLogsMerkleRoot, + bytes32 processedPrivilegedTransactionsRollingHash, + bytes32 lastBlockHash, + uint256 nonPrivilegedTransactions, + bytes32 commitHash, + ICommonBridge.BalanceDiff[] calldata balanceDiffs, + ICommonBridge.L2MessageRollingHash[] calldata l2MessageRollingHashes + ) external; + + /// @notice Verifies a single batch through the timelock. + function verifyBatch( + uint256 batchNumber, + bytes memory risc0BlockProof, + bytes calldata risc0Journal, + bytes calldata sp1PublicValues, + bytes memory sp1ProofBytes, + bytes calldata tdxPublicValues, + bytes memory tdxSignature + ) external; + + /// @notice Verifies multiple batches through the timelock using aligned proofs. + function verifyBatchesAligned( + uint256 firstBatchNumber, + bytes[] calldata publicInputsList, + bytes32[][] calldata sp1MerkleProofsList, + bytes32[][] calldata risc0MerkleProofsList + ) external; + + /// @notice Executes an operation immediately, bypassing the timelock delay. + function emergencyExecute( + address target, + uint256 value, + bytes calldata data + ) external payable; +} diff --git a/crates/l2/tee/contracts/src/TDXVerifier.sol b/crates/l2/tee/contracts/src/TDXVerifier.sol index 805280b75f..16b7e10b88 100644 --- a/crates/l2/tee/contracts/src/TDXVerifier.sol +++ b/crates/l2/tee/contracts/src/TDXVerifier.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {IOnChainProposer} from "../../../contracts/src/l1/interfaces/IOnChainProposer.sol"; +import {ITimelock} from "../../../contracts/src/l1/interfaces/ITimelock.sol"; interface IAttestation { function verifyAndAttestOnChain(bytes calldata rawQuote) @@ -14,7 +14,7 @@ interface IAttestation { contract TDXVerifier { IAttestation public quoteVerifier = IAttestation(address(0)); - IOnChainProposer public onChainProposer = IOnChainProposer(address(0)); + ITimelock public timelock = ITimelock(address(0)); address public authorizedSignature = address(0); bool public isDevMode = false; @@ -26,14 +26,14 @@ contract TDXVerifier { /// @notice Initializes the contract /// @param _dcap DCAP contract. - /// @param _ocp OnChainProposer contract, used for permission checks + /// @param _timelock Timelock contract, used for permission checks /// @param _isDevMode Disables quote verification - constructor(address _dcap, address _ocp, bool _isDevMode) { + constructor(address _dcap, address _timelock, bool _isDevMode) { require(_dcap != address(0), "TDXVerifier: DCAP address can't be null"); - require(_ocp != address(0), "TDXVerifier: OnChainPropser address can't be null"); + require(_timelock != address(0), "TDXVerifier: Timelock address can't be null"); quoteVerifier = IAttestation(_dcap); - onChainProposer = IOnChainProposer(_ocp); + timelock = ITimelock(_timelock); isDevMode = _isDevMode; } @@ -57,7 +57,7 @@ contract TDXVerifier { bytes calldata quote ) external { require( - onChainProposer.authorizedSequencerAddresses(msg.sender), + timelock.authorizedSequencerAddresses(msg.sender), "TDXVerifier: only sequencer can update keys" ); // TODO: only allow the owner to update the key, to avoid DoS From 1c33ced1c1ec9e623a5493cc3784e1e68272c471 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 18:08:19 -0300 Subject: [PATCH 32/47] use proper errors in timelock --- crates/l2/contracts/src/l1/Timelock.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 97b1604bf6..057be3c0e6 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -12,6 +12,9 @@ import {ITimelock} from "./interfaces/ITimelock.sol"; /// @notice The Timelock contract is the owner of the OnChainProposer contract, it gates access to it by managing roles /// and adding delay to specific operations for some roles (e.g. updating the contract, in order to provide an exit window). contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, ITimelock { + error TimelockCallerNotSelf(); + error TimelockUseCustomInitialize(); + /// @notice Role identifier for sequencers. /// @dev Accounts with this role can commit and verify batches. bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); @@ -25,10 +28,9 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, ITimelock { /// @dev Restricts calls to the timelock itself. modifier onlySelf() { - require( - msg.sender == address(this), - "Timelock: caller is not the timelock itself" - ); + if (msg.sender != address(this)) { + revert TimelockCallerNotSelf(); + } _; } @@ -38,8 +40,8 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, ITimelock { address[] memory, address[] memory, address - ) public pure override(TimelockControllerUpgradeable) { - revert("Timelock: use the custom initialize function"); + ) public pure override(ITimelock, TimelockControllerUpgradeable) { + revert TimelockUseCustomInitialize(); } /// @notice Initializes the timelock contract. From 2681bb2b3398085427c43be1b55860758e3a20eb Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 18:17:23 -0300 Subject: [PATCH 33/47] use let some instead --- cmd/ethrex/l2/deployer.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index c90f885f6a..c62cd10948 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -1084,13 +1084,7 @@ async fn initialize_contracts( let deployer_address = get_address_from_secret_key(&opts.private_key.secret_bytes()) .map_err(DeployerError::InternalError)?; - if !opts.deploy_based_contracts { - let timelock_address = - contract_addresses - .timelock_address - .ok_or(DeployerError::InternalError( - "Timelock address missing".to_string(), - ))?; + if let Some(timelock_address) = contract_addresses.timelock_address { info!("Initializing Timelock"); let initialize_tx_hash = { let deployer = Signer::Local(LocalSigner::new(opts.private_key)); From a56ece8fbcb26d75f734820742a6c22434c00033 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 18:35:27 -0300 Subject: [PATCH 34/47] Keep track of the nonce after deploying --- cmd/ethrex/l2/deployer.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index c62cd10948..22f9a3d099 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -1083,14 +1083,14 @@ async fn initialize_contracts( let deployer_address = get_address_from_secret_key(&opts.private_key.secret_bytes()) .map_err(DeployerError::InternalError)?; + let deployer = Signer::Local(LocalSigner::new(opts.private_key)); + let mut deployer_nonce = eth_client + .get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending)) + .await?; if let Some(timelock_address) = contract_addresses.timelock_address { info!("Initializing Timelock"); let initialize_tx_hash = { - let deployer = Signer::Local(LocalSigner::new(opts.private_key)); - let deployer_nonce = eth_client - .get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending)) - .await?; let calldata_values = vec![ Value::Uint(U256::from(30)), // TODO: Make minDelay parametrizable. For now this is for testing purposes. Value::Array(vec![ @@ -1120,6 +1120,7 @@ async fn initialize_contracts( ) .await? }; + deployer_nonce += 1; info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "Timelock initialized"); tx_hashes.push(initialize_tx_hash); } else { @@ -1156,11 +1157,6 @@ async fn initialize_contracts( &calldata_values, )?; - let deployer = Signer::Local(LocalSigner::new(opts.private_key)); - let deployer_nonce = eth_client - .get_nonce(deployer.address(), BlockIdentifier::Tag(BlockTag::Pending)) - .await?; - let initialize_tx_hash = initialize_contract_no_wait( contract_addresses.on_chain_proposer_address, on_chain_proposer_initialization_calldata, @@ -1175,6 +1171,7 @@ async fn initialize_contracts( }, ) .await?; + deployer_nonce += 1; info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "OnChainProposer initialized"); @@ -1182,9 +1179,6 @@ async fn initialize_contracts( info!("Initializing SequencerRegistry"); let initialize_tx_hash = { - let deployer_nonce = eth_client - .get_nonce(deployer.address(), BlockIdentifier::Tag(BlockTag::Pending)) - .await?; let calldata_values = vec![ Value::Address(opts.sequencer_registry_owner.ok_or( DeployerError::ConfigValueNotSet("--sequencer-registry-owner".to_string()), @@ -1209,6 +1203,7 @@ async fn initialize_contracts( ) .await? }; + deployer_nonce += 1; info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "SequencerRegistry initialized"); tx_hashes.push(initialize_tx_hash); } else { From 5d8747bb3aed9cbdcf8963e4ff38063509988532 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 18:39:21 -0300 Subject: [PATCH 35/47] deprecate variable in comment --- crates/l2/contracts/src/l1/OnChainProposer.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 96648f2d3a..6f0ad60d9c 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -67,6 +67,10 @@ contract OnChainProposer is /// @dev This is crucial for ensuring that only subsequents batches are committed in the contract. uint256 public lastCommittedBatch; + /// @dev Deprecated variable. This is managed inside the Timelock. + mapping(address _authorizedAddress => bool) + public authorizedSequencerAddresses; + address public BRIDGE; /// @dev Deprecated variable. address public PICO_VERIFIER_ADDRESS; From 97f99c09626b8d3423e4bba18f32d604ddbad72a Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 18:52:47 -0300 Subject: [PATCH 36/47] remove invalid override --- crates/l2/contracts/src/l1/Timelock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 057be3c0e6..34fb3752bc 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -40,7 +40,7 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, ITimelock { address[] memory, address[] memory, address - ) public pure override(ITimelock, TimelockControllerUpgradeable) { + ) public pure override(TimelockControllerUpgradeable) { revert TimelockUseCustomInitialize(); } From 0bde3ebc5e5487441de61d7ca4c87c0d100a82e5 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 18:53:24 -0300 Subject: [PATCH 37/47] add comment to ocp initialize --- crates/l2/contracts/src/l1/OnChainProposer.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 6f0ad60d9c..74691b9e80 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -113,6 +113,7 @@ contract OnChainProposer is /// @notice Initializes the contract. /// @dev This method is called only once after the contract is deployed. /// @dev The owner is expected to be the Timelock contract. + /// @dev It sets the bridge address. /// @param timelock_owner the Timelock address that can perform upgrades. /// @param alignedProofAggregator the address of the alignedProofAggregatorService contract. /// @param r0verifier the address of the risc0 groth16 verifier. From d35c56be7361cab76cf52f3478ac7654fb7afc5b Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Dec 2025 19:15:03 -0300 Subject: [PATCH 38/47] Revert "Keep track of the nonce after deploying" This reverts commit a56ece8fbcb26d75f734820742a6c22434c00033. --- cmd/ethrex/l2/deployer.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 22f9a3d099..c62cd10948 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -1083,14 +1083,14 @@ async fn initialize_contracts( let deployer_address = get_address_from_secret_key(&opts.private_key.secret_bytes()) .map_err(DeployerError::InternalError)?; - let deployer = Signer::Local(LocalSigner::new(opts.private_key)); - let mut deployer_nonce = eth_client - .get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending)) - .await?; if let Some(timelock_address) = contract_addresses.timelock_address { info!("Initializing Timelock"); let initialize_tx_hash = { + let deployer = Signer::Local(LocalSigner::new(opts.private_key)); + let deployer_nonce = eth_client + .get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending)) + .await?; let calldata_values = vec![ Value::Uint(U256::from(30)), // TODO: Make minDelay parametrizable. For now this is for testing purposes. Value::Array(vec![ @@ -1120,7 +1120,6 @@ async fn initialize_contracts( ) .await? }; - deployer_nonce += 1; info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "Timelock initialized"); tx_hashes.push(initialize_tx_hash); } else { @@ -1157,6 +1156,11 @@ async fn initialize_contracts( &calldata_values, )?; + let deployer = Signer::Local(LocalSigner::new(opts.private_key)); + let deployer_nonce = eth_client + .get_nonce(deployer.address(), BlockIdentifier::Tag(BlockTag::Pending)) + .await?; + let initialize_tx_hash = initialize_contract_no_wait( contract_addresses.on_chain_proposer_address, on_chain_proposer_initialization_calldata, @@ -1171,7 +1175,6 @@ async fn initialize_contracts( }, ) .await?; - deployer_nonce += 1; info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "OnChainProposer initialized"); @@ -1179,6 +1182,9 @@ async fn initialize_contracts( info!("Initializing SequencerRegistry"); let initialize_tx_hash = { + let deployer_nonce = eth_client + .get_nonce(deployer.address(), BlockIdentifier::Tag(BlockTag::Pending)) + .await?; let calldata_values = vec![ Value::Address(opts.sequencer_registry_owner.ok_or( DeployerError::ConfigValueNotSet("--sequencer-registry-owner".to_string()), @@ -1203,7 +1209,6 @@ async fn initialize_contracts( ) .await? }; - deployer_nonce += 1; info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "SequencerRegistry initialized"); tx_hashes.push(initialize_tx_hash); } else { From f6e02d77ed04a72a8da91d9fb796599685f3d15e Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 22 Dec 2025 11:31:22 -0300 Subject: [PATCH 39/47] rename authorizedSequencerAddresses to isSequencer --- crates/l2/contracts/src/l1/Timelock.sol | 4 +- .../contracts/src/l1/interfaces/ITimelock.sol | 4 +- crates/l2/tee/contracts/src/TDXVerifier.sol | 81 +++++++++++++------ 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 34fb3752bc..17422503d1 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -79,9 +79,7 @@ contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, ITimelock { /// @notice Returns whether an address has the sequencer role. /// @dev This matches the legacy OnChainProposer mapping used by TDXVerifier. - function authorizedSequencerAddresses( - address addr - ) external view returns (bool) { + function isSequencer(address addr) external view returns (bool) { return hasRole(SEQUENCER, addr); } diff --git a/crates/l2/contracts/src/l1/interfaces/ITimelock.sol b/crates/l2/contracts/src/l1/interfaces/ITimelock.sol index 1d5d8bee28..2258fd68b0 100644 --- a/crates/l2/contracts/src/l1/interfaces/ITimelock.sol +++ b/crates/l2/contracts/src/l1/interfaces/ITimelock.sol @@ -33,9 +33,7 @@ interface ITimelock { ) external; /// @notice Returns whether an address has the sequencer role. - function authorizedSequencerAddresses( - address addr - ) external view returns (bool); + function isSequencer(address addr) external view returns (bool); /// @notice Commits a batch through the timelock. function commitBatch( diff --git a/crates/l2/tee/contracts/src/TDXVerifier.sol b/crates/l2/tee/contracts/src/TDXVerifier.sol index 16b7e10b88..e752271ffa 100644 --- a/crates/l2/tee/contracts/src/TDXVerifier.sol +++ b/crates/l2/tee/contracts/src/TDXVerifier.sol @@ -6,10 +6,9 @@ import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ITimelock} from "../../../contracts/src/l1/interfaces/ITimelock.sol"; interface IAttestation { - function verifyAndAttestOnChain(bytes calldata rawQuote) - external - payable - returns (bool success, bytes memory output); + function verifyAndAttestOnChain( + bytes calldata rawQuote + ) external payable returns (bool success, bytes memory output); } contract TDXVerifier { @@ -19,10 +18,14 @@ contract TDXVerifier { address public authorizedSignature = address(0); bool public isDevMode = false; - bytes public RTMR0 = hex'4f3d617a1c89bd9a89ea146c15b04383b7db7318f41a851802bba8eace5a6cf71050e65f65fd50176e4f006764a42643'; - bytes public RTMR1 = hex'53827a034d1e4c7f13fd2a12aee4497e7097f15a04794553e12fe73e2ffb8bd57585e771951115a13ec4d7e6bc193038'; - bytes public RTMR2 = hex'2ca1a728ff13c36195ad95e8f725bf00d7f9c5d6ed730fb8f50cccad692ab81aefc83d594819375649be934022573528'; - bytes public MRTD = hex'91eb2b44d141d4ece09f0c75c2c53d247a3c68edd7fafe8a3520c942a604a407de03ae6dc5f87f27428b2538873118b7'; + bytes public RTMR0 = + hex"4f3d617a1c89bd9a89ea146c15b04383b7db7318f41a851802bba8eace5a6cf71050e65f65fd50176e4f006764a42643"; + bytes public RTMR1 = + hex"53827a034d1e4c7f13fd2a12aee4497e7097f15a04794553e12fe73e2ffb8bd57585e771951115a13ec4d7e6bc193038"; + bytes public RTMR2 = + hex"2ca1a728ff13c36195ad95e8f725bf00d7f9c5d6ed730fb8f50cccad692ab81aefc83d594819375649be934022573528"; + bytes public MRTD = + hex"91eb2b44d141d4ece09f0c75c2c53d247a3c68edd7fafe8a3520c942a604a407de03ae6dc5f87f27428b2538873118b7"; /// @notice Initializes the contract /// @param _dcap DCAP contract. @@ -30,7 +33,10 @@ contract TDXVerifier { /// @param _isDevMode Disables quote verification constructor(address _dcap, address _timelock, bool _isDevMode) { require(_dcap != address(0), "TDXVerifier: DCAP address can't be null"); - require(_timelock != address(0), "TDXVerifier: Timelock address can't be null"); + require( + _timelock != address(0), + "TDXVerifier: Timelock address can't be null" + ); quoteVerifier = IAttestation(_dcap); timelock = ITimelock(_timelock); @@ -45,19 +51,23 @@ contract TDXVerifier { bytes calldata payload, bytes memory signature ) external view { - require(authorizedSignature != address(0), "TDXVerifier: authorized signer not registered"); + require( + authorizedSignature != address(0), + "TDXVerifier: authorized signer not registered" + ); bytes32 signedHash = MessageHashUtils.toEthSignedMessageHash(payload); - require(ECDSA.recover(signedHash, signature) == authorizedSignature, "TDXVerifier: invalid signature"); + require( + ECDSA.recover(signedHash, signature) == authorizedSignature, + "TDXVerifier: invalid signature" + ); } /// @notice Registers the quote /// @dev The data required to verify the quote must be loaded to the PCCS contracts beforehand /// @param quote The TDX quote, which includes the address being registered - function register( - bytes calldata quote - ) external { + function register(bytes calldata quote) external { require( - timelock.authorizedSequencerAddresses(msg.sender), + timelock.isSequencer(msg.sender), "TDXVerifier: only sequencer can update keys" ); // TODO: only allow the owner to update the key, to avoid DoS @@ -65,25 +75,44 @@ contract TDXVerifier { authorizedSignature = _getAddress(quote, 0); return; } - (bool success, bytes memory report) = quoteVerifier.verifyAndAttestOnChain(quote); + (bool success, bytes memory report) = quoteVerifier + .verifyAndAttestOnChain(quote); require(success, "TDXVerifier: quote verification failed"); _validateReport(report); authorizedSignature = _getAddress(report, 533); } - function _validateReport(bytes memory report) view internal { - require(_rangeEquals(report, 0, hex'0004'), "TDXVerifier: Unsupported quote version"); + function _validateReport(bytes memory report) internal view { + require( + _rangeEquals(report, 0, hex"0004"), + "TDXVerifier: Unsupported quote version" + ); require(report[2] == 0x81, "TDXVerifier: Quote is not of type TDX"); require(report[6] == 0, "TDXVerifier: TCB_STATUS != OK"); - require(uint8(report[133]) & 15 == 0, "TDXVerifier: debug attributes are set"); + require( + uint8(report[133]) & 15 == 0, + "TDXVerifier: debug attributes are set" + ); require(_rangeEquals(report, 149, MRTD), "TDXVerifier: MRTD mismatch"); - require(_rangeEquals(report, 341, RTMR0), "TDXVerifier: RTMR0 mismatch"); - require(_rangeEquals(report, 389, RTMR1), "TDXVerifier: RTMR1 mismatch"); - require(_rangeEquals(report, 437, RTMR2), "TDXVerifier: RTMR2 mismatch"); + require( + _rangeEquals(report, 341, RTMR0), + "TDXVerifier: RTMR0 mismatch" + ); + require( + _rangeEquals(report, 389, RTMR1), + "TDXVerifier: RTMR1 mismatch" + ); + require( + _rangeEquals(report, 437, RTMR2), + "TDXVerifier: RTMR2 mismatch" + ); // RTMR3 is ignored } - function _getAddress(bytes memory report, uint256 offset) pure public returns (address) { + function _getAddress( + bytes memory report, + uint256 offset + ) public pure returns (address) { uint256 addr; for (uint8 i = 0; i < 20; i++) { addr = (addr << 8) | uint8(report[offset + i]); @@ -91,7 +120,11 @@ contract TDXVerifier { return address(uint160(addr)); } - function _rangeEquals(bytes memory report, uint256 offset, bytes memory other) pure internal returns (bool) { + function _rangeEquals( + bytes memory report, + uint256 offset, + bytes memory other + ) internal pure returns (bool) { for (uint256 i; i < other.length; i++) { if (report[offset + i] != other[i]) return false; } From 0b63415af8568da9980cf72e6aa194a6cc0bb682 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 22 Dec 2025 11:33:39 -0300 Subject: [PATCH 40/47] move errors to timelock interface --- crates/l2/contracts/src/l1/Timelock.sol | 3 --- crates/l2/contracts/src/l1/interfaces/ITimelock.sol | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/l2/contracts/src/l1/Timelock.sol b/crates/l2/contracts/src/l1/Timelock.sol index 17422503d1..6af92bf1f4 100644 --- a/crates/l2/contracts/src/l1/Timelock.sol +++ b/crates/l2/contracts/src/l1/Timelock.sol @@ -12,9 +12,6 @@ import {ITimelock} from "./interfaces/ITimelock.sol"; /// @notice The Timelock contract is the owner of the OnChainProposer contract, it gates access to it by managing roles /// and adding delay to specific operations for some roles (e.g. updating the contract, in order to provide an exit window). contract Timelock is TimelockControllerUpgradeable, UUPSUpgradeable, ITimelock { - error TimelockCallerNotSelf(); - error TimelockUseCustomInitialize(); - /// @notice Role identifier for sequencers. /// @dev Accounts with this role can commit and verify batches. bytes32 public constant SEQUENCER = keccak256("SEQUENCER"); diff --git a/crates/l2/contracts/src/l1/interfaces/ITimelock.sol b/crates/l2/contracts/src/l1/interfaces/ITimelock.sol index 2258fd68b0..975bfd0b79 100644 --- a/crates/l2/contracts/src/l1/interfaces/ITimelock.sol +++ b/crates/l2/contracts/src/l1/interfaces/ITimelock.sol @@ -14,6 +14,12 @@ interface ITimelock { /// @param data The calldata that was forwarded to `target`. event EmergencyExecution(address indexed target, uint256 value, bytes data); + // Used for functions that can only be called by the Timelock itself. + error TimelockCallerNotSelf(); + + // Used for other initialize() from contracts that the Timelock inherits from + error TimelockUseCustomInitialize(); + /// @notice The OnChainProposer contract controlled by this timelock. function onChainProposer() external view returns (IOnChainProposer); From a02e8e589e048f9f26d29b7d8dc8a49f8c20b95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jerem=C3=ADas=20Salom=C3=B3n?= <48994069+JereSalo@users.noreply.github.com> Date: Mon, 22 Dec 2025 12:54:11 -0300 Subject: [PATCH 41/47] ci(l2): fix TDX CI for timelock PR (#5698) **Motivation** **Description** --- crates/l2/tee/contracts/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/l2/tee/contracts/Makefile b/crates/l2/tee/contracts/Makefile index 1db181ca23..2755c193e9 100644 --- a/crates/l2/tee/contracts/Makefile +++ b/crates/l2/tee/contracts/Makefile @@ -42,7 +42,7 @@ lib/openzeppelin-contracts: solc_out/TDXVerifier.bin: src/TDXVerifier.sol lib/openzeppelin-contracts mkdir -p solc_out - solc src/TDXVerifier.sol --bin --allow-paths lib/,../../.. @openzeppelin/=lib/openzeppelin-contracts/ -o solc_out/ --overwrite + solc src/TDXVerifier.sol --bin --allow-paths lib/,../../.. --base-path . --include-path ../../ @openzeppelin/=lib/openzeppelin-contracts/ -o solc_out/ --overwrite deploy: solc_out/TDXVerifier.bin $(eval CONTRACT_BIN := $(shell cat solc_out/TDXVerifier.bin)) From 44344bcec581279ed06657bc3122a93c55144109 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 23 Dec 2025 11:32:56 -0300 Subject: [PATCH 42/47] add notice --- crates/l2/contracts/src/l1/interfaces/ITimelock.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/l2/contracts/src/l1/interfaces/ITimelock.sol b/crates/l2/contracts/src/l1/interfaces/ITimelock.sol index 975bfd0b79..7679aacbcf 100644 --- a/crates/l2/contracts/src/l1/interfaces/ITimelock.sol +++ b/crates/l2/contracts/src/l1/interfaces/ITimelock.sol @@ -14,10 +14,10 @@ interface ITimelock { /// @param data The calldata that was forwarded to `target`. event EmergencyExecution(address indexed target, uint256 value, bytes data); - // Used for functions that can only be called by the Timelock itself. + // @notice Used for functions that can only be called by the Timelock itself. error TimelockCallerNotSelf(); - // Used for other initialize() from contracts that the Timelock inherits from + // @notice Used for other initialize() from contracts that the Timelock inherits from error TimelockUseCustomInitialize(); /// @notice The OnChainProposer contract controlled by this timelock. From 73b8c1cbfcd2156d6e552b61d288b91cadb66d22 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 23 Dec 2025 11:33:05 -0300 Subject: [PATCH 43/47] add address check for bridge --- crates/l2/contracts/src/l1/OnChainProposer.sol | 4 ++++ crates/l2/contracts/src/l1/based/OnChainProposer.sol | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 74691b9e80..c70ffa87bf 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -187,6 +187,10 @@ contract OnChainProposer is bridge != address(0), "001" // OnChainProposer: bridge is the zero address ); + require( + bridge != address(this), + "000" // OnChainProposer: bridge is the contract address + ); BRIDGE = bridge; OwnableUpgradeable.__Ownable_init(timelock_owner); diff --git a/crates/l2/contracts/src/l1/based/OnChainProposer.sol b/crates/l2/contracts/src/l1/based/OnChainProposer.sol index 0010a46683..cd78131f0d 100644 --- a/crates/l2/contracts/src/l1/based/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/based/OnChainProposer.sol @@ -204,6 +204,10 @@ contract OnChainProposer is bridge != address(0), "OnChainProposer: bridge is the zero address" ); + require( + bridge != address(this), + "OnChainProposer: bridge is the contract address" + ); BRIDGE = bridge; emit VerificationKeyUpgraded("SP1", commitHash, sp1Vk); From 36282afb4b5870bfdbe1ec2c726e3b01891af22b Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 23 Dec 2025 13:47:41 -0300 Subject: [PATCH 44/47] add comment in proof sender and verifier --- crates/l2/sequencer/l1_proof_sender.rs | 1 + crates/l2/sequencer/l1_proof_verifier.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/l2/sequencer/l1_proof_sender.rs b/crates/l2/sequencer/l1_proof_sender.rs index e35a5f3105..9098e079a1 100644 --- a/crates/l2/sequencer/l1_proof_sender.rs +++ b/crates/l2/sequencer/l1_proof_sender.rs @@ -399,6 +399,7 @@ impl L1ProofSender { let calldata = encode_calldata(VERIFY_FUNCTION_SIGNATURE, &calldata_values)?; + // Based won't have timelock address until we implement it on it. For the meantime if it's None (only happens in based) we use the OCP let target_address = self .timelock_address .unwrap_or(self.on_chain_proposer_address); diff --git a/crates/l2/sequencer/l1_proof_verifier.rs b/crates/l2/sequencer/l1_proof_verifier.rs index 0ac2e607a3..4621fb870f 100644 --- a/crates/l2/sequencer/l1_proof_verifier.rs +++ b/crates/l2/sequencer/l1_proof_verifier.rs @@ -240,6 +240,7 @@ impl L1ProofVerifier { let calldata = encode_calldata(ALIGNED_VERIFY_FUNCTION_SIGNATURE, &calldata_values)?; + // Based won't have timelock address until we implement it on it. For the meantime if it's None (only happens in based) we use the OCP let target_address = self .timelock_address .unwrap_or(self.on_chain_proposer_address); From ec979464aae0d32cd5ca51bb8e94d77817210c5c Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 23 Dec 2025 13:59:04 -0300 Subject: [PATCH 45/47] fix stack too deep OnChainProposer --- .../l2/contracts/src/l1/OnChainProposer.sol | 81 ++++++++++++------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 595ade387d..a42feb0576 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -137,22 +137,16 @@ contract OnChainProposer is address bridge ) public initializer { VALIDIUM = _validium; - - // Risc0 constants - REQUIRE_RISC0_PROOF = requireRisc0Proof; - RISC0_VERIFIER_ADDRESS = r0verifier; - - // SP1 constants - REQUIRE_SP1_PROOF = requireSp1Proof; - SP1_VERIFIER_ADDRESS = sp1verifier; - - // TDX constants - REQUIRE_TDX_PROOF = requireTdxProof; - TDX_VERIFIER_ADDRESS = tdxverifier; - - // Aligned Layer constants - ALIGNED_MODE = aligned; - ALIGNEDPROOFAGGREGATOR = alignedProofAggregator; + _setVerificationConfig( + requireRisc0Proof, + requireSp1Proof, + requireTdxProof, + aligned, + r0verifier, + sp1verifier, + tdxverifier, + alignedProofAggregator + ); require( commitHash != bytes32(0), @@ -169,17 +163,7 @@ contract OnChainProposer is verificationKeys[commitHash][SP1_VERIFIER_ID] = sp1Vk; verificationKeys[commitHash][RISC0_VERIFIER_ID] = risc0Vk; - batchCommitments[0] = BatchCommitmentInfo( - genesisStateRoot, - bytes32(0), - bytes32(0), - bytes32(0), - bytes32(0), - 0, - new ICommonBridge.BalanceDiff[](0), - commitHash, - new ICommonBridge.L2MessageRollingHash[](0) - ); + _initGenesisCommitment(genesisStateRoot, commitHash); CHAIN_ID = chainId; @@ -196,6 +180,49 @@ contract OnChainProposer is OwnableUpgradeable.__Ownable_init(timelock_owner); } + // Auxiliary initializer helper to avoid stack-too-deep. + function _setVerificationConfig( + bool requireRisc0Proof, + bool requireSp1Proof, + bool requireTdxProof, + bool aligned, + address r0verifier, + address sp1verifier, + address tdxverifier, + address alignedProofAggregator + ) internal { + REQUIRE_RISC0_PROOF = requireRisc0Proof; + REQUIRE_SP1_PROOF = requireSp1Proof; + REQUIRE_TDX_PROOF = requireTdxProof; + + RISC0_VERIFIER_ADDRESS = r0verifier; + SP1_VERIFIER_ADDRESS = sp1verifier; + TDX_VERIFIER_ADDRESS = tdxverifier; + + ALIGNED_MODE = aligned; + ALIGNEDPROOFAGGREGATOR = alignedProofAggregator; + } + + // Auxiliary initializer helper to avoid stack-too-deep. + function _initGenesisCommitment( + bytes32 genesisStateRoot, + bytes32 commitHash + ) internal { + BatchCommitmentInfo storage commitment = batchCommitments[0]; + commitment.newStateRoot = genesisStateRoot; + commitment.blobKZGVersionedHash = bytes32(0); + commitment.processedPrivilegedTransactionsRollingHash = bytes32(0); + commitment.withdrawalsLogsMerkleRoot = bytes32(0); + commitment.lastBlockHash = bytes32(0); + commitment.nonPrivilegedTransactions = 0; + commitment.balanceDiffs = new ICommonBridge.BalanceDiff[](0); + commitment.commitHash = commitHash; + commitment + .l2InMessageRollingHashes = new ICommonBridge.L2MessageRollingHash[]( + 0 + ); + } + /// @inheritdoc IOnChainProposer function upgradeSP1VerificationKey( bytes32 commit_hash, From eefbc733bba74bc26c8fd652c82edbd8242ca34b Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 23 Dec 2025 14:04:50 -0300 Subject: [PATCH 46/47] add dev to comment in OCP --- crates/l2/contracts/src/l1/OnChainProposer.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index a42feb0576..5b692e60ad 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -180,7 +180,7 @@ contract OnChainProposer is OwnableUpgradeable.__Ownable_init(timelock_owner); } - // Auxiliary initializer helper to avoid stack-too-deep. + /// @dev Auxiliary initializer helper to avoid stack-too-deep. function _setVerificationConfig( bool requireRisc0Proof, bool requireSp1Proof, @@ -203,7 +203,7 @@ contract OnChainProposer is ALIGNEDPROOFAGGREGATOR = alignedProofAggregator; } - // Auxiliary initializer helper to avoid stack-too-deep. + /// @dev Auxiliary initializer helper to avoid stack-too-deep. function _initGenesisCommitment( bytes32 genesisStateRoot, bytes32 commitHash From 16b54cc9022878dc5474e2b9dbafe53ed95e9bce Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 23 Dec 2025 14:48:25 -0300 Subject: [PATCH 47/47] remove auxiliary initializers --- .../l2/contracts/src/l1/OnChainProposer.sol | 78 ++++++------------- 1 file changed, 24 insertions(+), 54 deletions(-) diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 5b692e60ad..5edeba82b6 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -137,16 +137,17 @@ contract OnChainProposer is address bridge ) public initializer { VALIDIUM = _validium; - _setVerificationConfig( - requireRisc0Proof, - requireSp1Proof, - requireTdxProof, - aligned, - r0verifier, - sp1verifier, - tdxverifier, - alignedProofAggregator - ); + + REQUIRE_RISC0_PROOF = requireRisc0Proof; + REQUIRE_SP1_PROOF = requireSp1Proof; + REQUIRE_TDX_PROOF = requireTdxProof; + + RISC0_VERIFIER_ADDRESS = r0verifier; + SP1_VERIFIER_ADDRESS = sp1verifier; + TDX_VERIFIER_ADDRESS = tdxverifier; + + ALIGNED_MODE = aligned; + ALIGNEDPROOFAGGREGATOR = alignedProofAggregator; require( commitHash != bytes32(0), @@ -163,7 +164,19 @@ contract OnChainProposer is verificationKeys[commitHash][SP1_VERIFIER_ID] = sp1Vk; verificationKeys[commitHash][RISC0_VERIFIER_ID] = risc0Vk; - _initGenesisCommitment(genesisStateRoot, commitHash); + BatchCommitmentInfo storage commitment = batchCommitments[0]; + commitment.newStateRoot = genesisStateRoot; + commitment.blobKZGVersionedHash = bytes32(0); + commitment.processedPrivilegedTransactionsRollingHash = bytes32(0); + commitment.withdrawalsLogsMerkleRoot = bytes32(0); + commitment.lastBlockHash = bytes32(0); + commitment.nonPrivilegedTransactions = 0; + commitment.balanceDiffs = new ICommonBridge.BalanceDiff[](0); + commitment.commitHash = commitHash; + commitment + .l2InMessageRollingHashes = new ICommonBridge.L2MessageRollingHash[]( + 0 + ); CHAIN_ID = chainId; @@ -180,49 +193,6 @@ contract OnChainProposer is OwnableUpgradeable.__Ownable_init(timelock_owner); } - /// @dev Auxiliary initializer helper to avoid stack-too-deep. - function _setVerificationConfig( - bool requireRisc0Proof, - bool requireSp1Proof, - bool requireTdxProof, - bool aligned, - address r0verifier, - address sp1verifier, - address tdxverifier, - address alignedProofAggregator - ) internal { - REQUIRE_RISC0_PROOF = requireRisc0Proof; - REQUIRE_SP1_PROOF = requireSp1Proof; - REQUIRE_TDX_PROOF = requireTdxProof; - - RISC0_VERIFIER_ADDRESS = r0verifier; - SP1_VERIFIER_ADDRESS = sp1verifier; - TDX_VERIFIER_ADDRESS = tdxverifier; - - ALIGNED_MODE = aligned; - ALIGNEDPROOFAGGREGATOR = alignedProofAggregator; - } - - /// @dev Auxiliary initializer helper to avoid stack-too-deep. - function _initGenesisCommitment( - bytes32 genesisStateRoot, - bytes32 commitHash - ) internal { - BatchCommitmentInfo storage commitment = batchCommitments[0]; - commitment.newStateRoot = genesisStateRoot; - commitment.blobKZGVersionedHash = bytes32(0); - commitment.processedPrivilegedTransactionsRollingHash = bytes32(0); - commitment.withdrawalsLogsMerkleRoot = bytes32(0); - commitment.lastBlockHash = bytes32(0); - commitment.nonPrivilegedTransactions = 0; - commitment.balanceDiffs = new ICommonBridge.BalanceDiff[](0); - commitment.commitHash = commitHash; - commitment - .l2InMessageRollingHashes = new ICommonBridge.L2MessageRollingHash[]( - 0 - ); - } - /// @inheritdoc IOnChainProposer function upgradeSP1VerificationKey( bytes32 commit_hash,