diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index 1c1e5240c02..38ab2d59100 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -9,11 +9,12 @@ use std::{ use bytes::Bytes; use clap::Parser; use ethrex_common::H256; +use ethrex_common::utils::keccak; use ethrex_common::{ Address, U256, types::{Genesis, TxType}, }; -use ethrex_l2::utils::test_data_io::read_genesis_file; +use ethrex_l2::{sequencer::utils::get_git_commit_hash, utils::test_data_io::read_genesis_file}; use ethrex_l2_common::{calldata::Value, prover::ProverType, utils::get_address_from_secret_key}; use ethrex_l2_rpc::signer::{LocalSigner, Signer}; use ethrex_l2_sdk::{ @@ -539,8 +540,8 @@ 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: &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,bytes32,address,uint256)"; +const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,bytes32,address[],uint256)"; const INITIALIZE_BRIDGE_ADDRESS_SIGNATURE: &str = "initializeBridgeAddress(address)"; const TRANSFER_OWNERSHIP_SIGNATURE: &str = "transferOwnership(address)"; @@ -1027,6 +1028,7 @@ async fn initialize_contracts( let risc0_vk = get_vk(ProverType::RISC0, opts)?; info!("Risc0 vk read"); + let commit_hash = keccak(get_git_commit_hash()); let deployer_address = get_address_from_secret_key(&opts.private_key.secret_bytes()) .map_err(DeployerError::InternalError)?; @@ -1048,6 +1050,7 @@ async fn initialize_contracts( Value::Address(contract_addresses.aligned_aggregator_address), Value::FixedBytes(sp1_vk), Value::FixedBytes(risc0_vk), + Value::FixedBytes(commit_hash.0.to_vec().into()), Value::FixedBytes(genesis.compute_state_root().0.to_vec().into()), Value::Address(contract_addresses.sequencer_registry_address), Value::Uint(genesis.config.chain_id.into()), @@ -1157,6 +1160,7 @@ async fn initialize_contracts( Value::Address(contract_addresses.aligned_aggregator_address), Value::FixedBytes(sp1_vk), Value::FixedBytes(risc0_vk), + Value::FixedBytes(commit_hash.0.to_vec().into()), Value::FixedBytes(genesis.compute_state_root().0.to_vec().into()), Value::Array(vec![ 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 88d43e6bbdc..b4f6d6b68af 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -40,8 +40,13 @@ contract OnChainProposer is bytes32 lastBlockHash; bytes32 l2MessagesMerkleRoot; ICommonBridge.BalanceDiff[] balanceDiffs; + /// @dev git commit hash that produced the proof/verification key used for this batch + bytes32 commitHash; } + uint8 internal constant VK_SP1 = 1; + uint8 internal constant VK_RISC0 = 2; + /// @notice The commitments of the committed batches. /// @dev If a batch is committed, the commitment is stored here. /// @dev If a batch was not committed yet, it won't be here. @@ -73,6 +78,7 @@ contract OnChainProposer is address public RISC0_VERIFIER_ADDRESS; address public SP1_VERIFIER_ADDRESS; + /// @dev Deprecated variable. bytes32 public SP1_VERIFICATION_KEY; /// @notice Indicates whether the contract operates in validium mode.Add commentMore actions @@ -85,6 +91,7 @@ contract OnChainProposer is /// @dev This address is set during contract initialization and is used to verify aligned proofs. address public ALIGNEDPROOFAGGREGATOR; + /// @dev Deprecated variable. bytes32 public RISC0_VERIFICATION_KEY; /// @notice Chain ID of the network @@ -100,6 +107,10 @@ contract OnChainProposer is /// @notice True if verification is done through Aligned Layer instead of smart contract verifiers. bool public ALIGNED_MODE; + /// @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; + modifier onlySequencer() { require( authorizedSequencerAddresses[msg.sender], @@ -128,6 +139,7 @@ contract OnChainProposer is address alignedProofAggregator, bytes32 sp1Vk, bytes32 risc0Vk, + bytes32 commitHash, bytes32 genesisStateRoot, address[] calldata sequencerAddresses, uint256 chainId @@ -137,12 +149,10 @@ contract OnChainProposer is // Risc0 constants REQUIRE_RISC0_PROOF = requireRisc0Proof; RISC0_VERIFIER_ADDRESS = r0verifier; - RISC0_VERIFICATION_KEY = risc0Vk; // SP1 constants REQUIRE_SP1_PROOF = requireSp1Proof; SP1_VERIFIER_ADDRESS = sp1verifier; - SP1_VERIFICATION_KEY = sp1Vk; // TDX constants REQUIRE_TDX_PROOF = requireTdxProof; @@ -152,6 +162,21 @@ contract OnChainProposer is ALIGNED_MODE = aligned; ALIGNEDPROOFAGGREGATOR = alignedProofAggregator; + require( + commitHash != bytes32(0), + "OnChainProposer: commit hash is zero" + ); + require( + !REQUIRE_SP1_PROOF || sp1Vk != bytes32(0), + "OnChainProposer: missing SP1 verification key" + ); + require( + !REQUIRE_RISC0_PROOF || risc0Vk != bytes32(0), + "OnChainProposer: missing RISC0 verification key" + ); + verificationKeys[commitHash][VK_SP1] = sp1Vk; + verificationKeys[commitHash][VK_RISC0] = risc0Vk; + batchCommitments[0] = BatchCommitmentInfo( genesisStateRoot, bytes32(0), @@ -159,7 +184,8 @@ contract OnChainProposer is bytes32(0), bytes32(0), bytes32(0), - new ICommonBridge.BalanceDiff[](0) + new ICommonBridge.BalanceDiff[](0), + commitHash ); for (uint256 i = 0; i < sequencerAddresses.length; i++) { @@ -169,6 +195,9 @@ contract OnChainProposer is CHAIN_ID = chainId; OwnableUpgradeable.__Ownable_init(owner); + + emit VerificationKeyUpgraded("SP1", commitHash, sp1Vk); + emit VerificationKeyUpgraded("RISC0", commitHash, risc0Vk); } /// @inheritdoc IOnChainProposer @@ -181,15 +210,33 @@ contract OnChainProposer is } /// @inheritdoc IOnChainProposer - function upgradeSP1VerificationKey(bytes32 new_vk) public onlyOwner { - SP1_VERIFICATION_KEY = new_vk; - emit VerificationKeyUpgraded("SP1", new_vk); + function upgradeSP1VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) public onlyOwner { + require( + commit_hash != bytes32(0), + "OnChainProposer: commit hash is zero" + ); + // we don't want to restrict setting the vk to zero + // as we may want to disable the version + verificationKeys[commit_hash][VK_SP1] = new_vk; + emit VerificationKeyUpgraded("SP1", commit_hash, new_vk); } /// @inheritdoc IOnChainProposer - function upgradeRISC0VerificationKey(bytes32 new_vk) public onlyOwner { - RISC0_VERIFICATION_KEY = new_vk; - emit VerificationKeyUpgraded("RISC0", new_vk); + function upgradeRISC0VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) public onlyOwner { + require( + commit_hash != bytes32(0), + "OnChainProposer: commit hash is zero" + ); + // we don't want to restrict setting the vk to zero + // as we may want to disable the version + verificationKeys[commit_hash][VK_RISC0] = new_vk; + emit VerificationKeyUpgraded("RISC0", commit_hash, new_vk); } /// @inheritdoc IOnChainProposer @@ -200,6 +247,7 @@ contract OnChainProposer is bytes32 processedPrivilegedTransactionsRollingHash, bytes32 lastBlockHash, bytes32 l2MessagesMerkleRoot, + bytes32 commitHash, ICommonBridge.BalanceDiff[] calldata balanceDiffs ) external override onlySequencer whenNotPaused { // TODO: Refactor validation @@ -248,6 +296,16 @@ contract OnChainProposer is ); } + // Validate commit hash and corresponding verification keys are valid + require(commitHash != bytes32(0), "012"); + require( + (!REQUIRE_SP1_PROOF || + verificationKeys[commitHash][VK_SP1] != bytes32(0)) && + (!REQUIRE_RISC0_PROOF || + verificationKeys[commitHash][VK_RISC0] != bytes32(0)), + "013" // missing verification key for commit hash + ); + batchCommitments[batchNumber] = BatchCommitmentInfo( newStateRoot, blobVersionedHash, @@ -255,7 +313,8 @@ contract OnChainProposer is withdrawalsLogsMerkleRoot, lastBlockHash, l2MessagesMerkleRoot, - balanceDiffs + balanceDiffs, + commitHash ); emit BatchCommitted(newStateRoot); @@ -313,15 +372,18 @@ contract OnChainProposer is if (bytes(reason).length != 0) { revert( string.concat( - "00b", // OnChainProposer: Invalid RISC0 proof: + "00b", // OnChainProposer: Invalid RISC0 proof: reason ) ); } + bytes32 batchCommitHash = batchCommitments[batchNumber].commitHash; + bytes32 risc0Vk = verificationKeys[batchCommitHash][VK_RISC0]; try IRiscZeroVerifier(RISC0_VERIFIER_ADDRESS).verify( risc0BlockProof, - RISC0_VERIFICATION_KEY, + // we use the same vk as the one set for the commit of the batch + risc0Vk, sha256(risc0Journal) ) {} catch { @@ -340,14 +402,16 @@ contract OnChainProposer is if (bytes(reason).length != 0) { revert( string.concat( - "00d", // OnChainProposer: Invalid SP1 proof: + "00d", // OnChainProposer: Invalid SP1 proof: reason ) ); } + bytes32 batchCommitHash = batchCommitments[batchNumber].commitHash; + bytes32 sp1Vk = verificationKeys[batchCommitHash][VK_SP1]; try ISP1Verifier(SP1_VERIFIER_ADDRESS).verifyProof( - SP1_VERIFICATION_KEY, + sp1Vk, sp1PublicValues, sp1ProofBytes ) @@ -367,7 +431,7 @@ contract OnChainProposer is if (bytes(reason).length != 0) { revert( string.concat( - "00f", // OnChainProposer: Invalid TDX proof: + "00f", // OnChainProposer: Invalid TDX proof: reason ) ); @@ -458,7 +522,7 @@ contract OnChainProposer is if (bytes(reason).length != 0) { revert( string.concat( - "00m", // OnChainProposer: Invalid ALIGNED proof: + "00m", // OnChainProposer: Invalid ALIGNED proof: reason ) ); @@ -467,7 +531,9 @@ contract OnChainProposer is if (REQUIRE_SP1_PROOF) { _verifyProofInclusionAligned( sp1MerkleProofsList[i], - SP1_VERIFICATION_KEY, + verificationKeys[batchCommitments[batchNumber].commitHash][ + VK_SP1 + ], publicInputsList[i] ); } @@ -475,7 +541,9 @@ contract OnChainProposer is if (REQUIRE_RISC0_PROOF) { _verifyProofInclusionAligned( risc0MerkleProofsList[i], - RISC0_VERIFICATION_KEY, + verificationKeys[batchCommitments[batchNumber].commitHash][ + VK_RISC0 + ], publicInputsList[i] ); } @@ -494,7 +562,9 @@ contract OnChainProposer is uint256 batchNumber, bytes calldata publicData ) internal view returns (string memory) { - uint256 registered_chains = batchCommitments[batchNumber].balanceDiffs.length; + uint256 registered_chains = batchCommitments[batchNumber] + .balanceDiffs + .length; if (publicData.length != 288 + 64 * registered_chains) { return "00n"; // invalid public data length } @@ -502,21 +572,18 @@ contract OnChainProposer is if ( batchCommitments[lastVerifiedBatch].newStateRoot != initialStateRoot ) { - return - "00o"; // initial state root public inputs don't match with initial state root + return "00o"; // initial state root public inputs don't match with initial state root } bytes32 finalStateRoot = bytes32(publicData[32:64]); if (batchCommitments[batchNumber].newStateRoot != finalStateRoot) { - return - "00p"; // final state root public inputs don't match with final state root + return "00p"; // final state root public inputs don't match with final state root } bytes32 withdrawalsMerkleRoot = bytes32(publicData[64:96]); if ( batchCommitments[batchNumber].withdrawalsLogsMerkleRoot != withdrawalsMerkleRoot ) { - return - "00q"; // withdrawals public inputs don't match with committed withdrawals + return "00q"; // withdrawals public inputs don't match with committed withdrawals } bytes32 privilegedTransactionsHash = bytes32(publicData[96:128]); if ( @@ -524,21 +591,18 @@ contract OnChainProposer is .processedPrivilegedTransactionsRollingHash != privilegedTransactionsHash ) { - return - "00r"; // privileged transactions hash public input does not match with committed transactions + return "00r"; // privileged transactions hash public input does not match with committed transactions } bytes32 blobVersionedHash = bytes32(publicData[128:160]); if ( batchCommitments[batchNumber].blobKZGVersionedHash != blobVersionedHash ) { - return - "00s"; // blob versioned hash public input does not match with committed hash + return "00s"; // blob versioned hash public input does not match with committed hash } bytes32 lastBlockHash = bytes32(publicData[160:192]); if (batchCommitments[batchNumber].lastBlockHash != lastBlockHash) { - return - "00t"; // last block hash public inputs don't match with last block hash + return "00t"; // last block hash public inputs don't match with last block hash } uint256 chainId = uint256(bytes32(publicData[192:224])); if (chainId != CHAIN_ID) { @@ -551,28 +615,28 @@ contract OnChainProposer is ICommonBridge(BRIDGE).hasExpiredPrivilegedTransactions() && nonPrivilegedTransactions != 0 ) { - return - "00v"; // exceeded privileged transaction inclusion deadline, can't include non-privileged transactions + return "00v"; // exceeded privileged transaction inclusion deadline, can't include non-privileged transactions } bytes32 l2MessagesMerkleRoot = bytes32(publicData[256:288]); if ( batchCommitments[batchNumber].l2MessagesMerkleRoot != l2MessagesMerkleRoot ) { - return - "00w"; // l2 messages merkle root public inputs don't match with committed l2 messages merkle root + return "00w"; // l2 messages merkle root public inputs don't match with committed l2 messages merkle root } for (uint256 i = 0; i < registered_chains; i++) { uint256 offset = 288 + i * 64; uint256 chainId = uint256(bytes32(publicData[offset:offset + 32])); - uint256 value = uint256(bytes32(publicData[offset + 32:offset + 64])); + uint256 value = uint256( + bytes32(publicData[offset + 32:offset + 64]) + ); if ( - batchCommitments[batchNumber].balanceDiffs[i].chainId != chainId || + batchCommitments[batchNumber].balanceDiffs[i].chainId != + chainId || batchCommitments[batchNumber].balanceDiffs[i].value != value ) { - return - "00x"; // balance diffs public inputs don't match with committed balance diffs + return "00x"; // balance diffs public inputs don't match with committed balance diffs } } return ""; diff --git a/crates/l2/contracts/src/l1/based/OnChainProposer.sol b/crates/l2/contracts/src/l1/based/OnChainProposer.sol index 5f0d94944d4..1629128b2ca 100644 --- a/crates/l2/contracts/src/l1/based/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/based/OnChainProposer.sol @@ -34,8 +34,13 @@ contract OnChainProposer is bytes32 processedPrivilegedTransactionsRollingHash; bytes32 withdrawalsLogsMerkleRoot; bytes32 lastBlockHash; + /// @dev git commit hash that produced the proof/verification key used for this batch + bytes32 commitHash; } + uint8 internal constant VK_SP1 = 1; + uint8 internal constant VK_RISC0 = 2; + /// @notice The commitments of the committed batches. /// @dev If a batch is committed, the commitment is stored here. /// @dev If a batch was not committed yet, it won't be here. @@ -65,6 +70,7 @@ contract OnChainProposer is address public TDX_VERIFIER_ADDRESS; address public SEQUENCER_REGISTRY; + /// @dev Deprecated variable. bytes32 public SP1_VERIFICATION_KEY; /// @notice Indicates whether the contract operates in validium mode. @@ -75,6 +81,7 @@ contract OnChainProposer is /// @dev This address is set during contract initialization and is used to verify aligned proofs. address public ALIGNEDPROOFAGGREGATOR; + /// @dev Deprecated variable. bytes32 public RISC0_VERIFICATION_KEY; /// @notice True if a Risc0 proof is required for batch verification. @@ -90,6 +97,10 @@ contract OnChainProposer is /// @notice Chain ID of the network uint256 public CHAIN_ID; + /// @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; + modifier onlyLeaderSequencer() { require( msg.sender == @@ -120,6 +131,7 @@ contract OnChainProposer is address alignedProofAggregator, bytes32 sp1Vk, bytes32 risc0Vk, + bytes32 commitHash, bytes32 genesisStateRoot, address sequencer_registry, uint256 chainId @@ -129,13 +141,10 @@ contract OnChainProposer is // Risc0 constants REQUIRE_RISC0_PROOF = requireRisc0Proof; RISC0_VERIFIER_ADDRESS = r0verifier; - RISC0_VERIFICATION_KEY = risc0Vk; // SP1 constants REQUIRE_SP1_PROOF = requireSp1Proof; SP1_VERIFIER_ADDRESS = sp1verifier; - SP1_VERIFICATION_KEY = sp1Vk; - RISC0_VERIFICATION_KEY = risc0Vk; // TDX constants REQUIRE_TDX_PROOF = requireTdxProof; @@ -145,12 +154,28 @@ contract OnChainProposer is ALIGNED_MODE = aligned; ALIGNEDPROOFAGGREGATOR = alignedProofAggregator; + require( + commitHash != bytes32(0), + "OnChainProposer: commit hash is zero" + ); + require( + !REQUIRE_SP1_PROOF || sp1Vk != bytes32(0), + "OnChainProposer: missing SP1 verification key" + ); + require( + !REQUIRE_RISC0_PROOF || risc0Vk != bytes32(0), + "OnChainProposer: missing RISC0 verification key" + ); + verificationKeys[commitHash][VK_SP1] = sp1Vk; + verificationKeys[commitHash][VK_RISC0] = risc0Vk; + batchCommitments[0] = BatchCommitmentInfo( genesisStateRoot, bytes32(0), bytes32(0), bytes32(0), - bytes32(0) + bytes32(0), + commitHash ); // Set the SequencerRegistry address @@ -171,6 +196,9 @@ contract OnChainProposer is CHAIN_ID = chainId; OwnableUpgradeable.__Ownable_init(owner); + + emit VerificationKeyUpgraded("SP1", commitHash, sp1Vk); + emit VerificationKeyUpgraded("RISC0", commitHash, risc0Vk); } /// @inheritdoc IOnChainProposer @@ -191,15 +219,33 @@ contract OnChainProposer is } /// @inheritdoc IOnChainProposer - function upgradeSP1VerificationKey(bytes32 new_vk) public onlyOwner { - SP1_VERIFICATION_KEY = new_vk; - emit VerificationKeyUpgraded("SP1", new_vk); + function upgradeSP1VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) public onlyOwner { + require( + commit_hash != bytes32(0), + "OnChainProposer: commit hash is zero" + ); + // we don't want to restrict setting the vk to zero + // as we may want to disable the version + verificationKeys[commit_hash][VK_SP1] = new_vk; + emit VerificationKeyUpgraded("SP1", commit_hash, new_vk); } /// @inheritdoc IOnChainProposer - function upgradeRISC0VerificationKey(bytes32 new_vk) public onlyOwner { - RISC0_VERIFICATION_KEY = new_vk; - emit VerificationKeyUpgraded("RISC0", new_vk); + function upgradeRISC0VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) public onlyOwner { + require( + commit_hash != bytes32(0), + "OnChainProposer: commit hash is zero" + ); + // we don't want to restrict setting the vk to zero + // as we may want to disable the version + verificationKeys[commit_hash][VK_RISC0] = new_vk; + emit VerificationKeyUpgraded("RISC0", commit_hash, new_vk); } /// @inheritdoc IOnChainProposer @@ -209,6 +255,7 @@ contract OnChainProposer is bytes32 withdrawalsLogsMerkleRoot, bytes32 processedPrivilegedTransactionsRollingHash, bytes32 lastBlockHash, + bytes32 commitHash, bytes[] calldata //rlpEncodedBlocks ) external override onlyLeaderSequencer { // TODO: Refactor validation @@ -245,6 +292,16 @@ contract OnChainProposer is ); } + // Validate commit hash and corresponding verification keys are valid + require(commitHash != bytes32(0), "012"); + require( + (!REQUIRE_SP1_PROOF || + verificationKeys[commitHash][VK_SP1] != bytes32(0)) && + (!REQUIRE_RISC0_PROOF || + verificationKeys[commitHash][VK_RISC0] != bytes32(0)), + "013" // missing verification key for commit hash + ); + // Blob is published in the (EIP-4844) transaction that calls this function. bytes32 blobVersionedHash = blobhash(0); if (VALIDIUM) { @@ -264,7 +321,8 @@ contract OnChainProposer is blobVersionedHash, processedPrivilegedTransactionsRollingHash, withdrawalsLogsMerkleRoot, - lastBlockHash + lastBlockHash, + commitHash ); emit BatchCommitted(batchNumber, newStateRoot); @@ -293,7 +351,10 @@ contract OnChainProposer is bytes calldata tdxPublicValues, bytes memory tdxSignature ) external { - require(!ALIGNED_MODE, "Batch verification should be done via Aligned Layer. Call verifyBatchesAligned() instead."); + require( + !ALIGNED_MODE, + "Batch verification should be done via Aligned Layer. Call verifyBatchesAligned() instead." + ); require( batchCommitments[batchNumber].newStateRoot != bytes32(0), @@ -324,10 +385,12 @@ contract OnChainProposer is ) ); } + bytes32 batchCommitHash = batchCommitments[batchNumber].commitHash; + bytes32 risc0Vk = verificationKeys[batchCommitHash][VK_RISC0]; try IRiscZeroVerifier(RISC0_VERIFIER_ADDRESS).verify( risc0BlockProof, - RISC0_VERIFICATION_KEY, + risc0Vk, sha256(risc0Journal) ) {} catch { @@ -351,9 +414,11 @@ contract OnChainProposer is ) ); } + bytes32 batchCommitHash = batchCommitments[batchNumber].commitHash; + bytes32 sp1Vk = verificationKeys[batchCommitHash][VK_SP1]; try ISP1Verifier(SP1_VERIFIER_ADDRESS).verifyProof( - SP1_VERIFICATION_KEY, + sp1Vk, sp1PublicValues, sp1ProofBytes ) @@ -379,7 +444,10 @@ contract OnChainProposer is ); } try - ITDXVerifier(TDX_VERIFIER_ADDRESS).verify(tdxPublicValues, tdxSignature) + ITDXVerifier(TDX_VERIFIER_ADDRESS).verify( + tdxPublicValues, + tdxSignature + ) {} catch { revert( "OnChainProposer: Invalid TDX proof failed proof verification" @@ -462,7 +530,9 @@ contract OnChainProposer is if (REQUIRE_SP1_PROOF) { _verifyProofInclusionAligned( sp1MerkleProofsList[i], - SP1_VERIFICATION_KEY, + verificationKeys[batchCommitments[batchNumber].commitHash][ + VK_SP1 + ], publicInputsList[i] ); } @@ -470,7 +540,9 @@ contract OnChainProposer is if (REQUIRE_RISC0_PROOF) { _verifyProofInclusionAligned( risc0MerkleProofsList[i], - RISC0_VERIFICATION_KEY, + verificationKeys[batchCommitments[batchNumber].commitHash][ + VK_RISC0 + ], publicInputsList[i] ); } diff --git a/crates/l2/contracts/src/l1/based/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/based/interfaces/IOnChainProposer.sol index f37079d127f..29830e753e6 100644 --- a/crates/l2/contracts/src/l1/based/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/based/interfaces/IOnChainProposer.sol @@ -26,8 +26,13 @@ interface IOnChainProposer { /// @notice A verification key has been upgraded. /// @dev Event emitted when a verification key is upgraded. /// @param verifier The name of the verifier whose key was upgraded. + /// @param commitHash The git commit hash associated to the verification key. /// @param newVerificationKey The new verification key. - event VerificationKeyUpgraded(string verifier, bytes32 newVerificationKey); + event VerificationKeyUpgraded( + string verifier, + bytes32 commitHash, + bytes32 newVerificationKey + ); /// @notice Set the bridge address for the first time. /// @dev This method is separated from initialize because both the CommonBridge @@ -37,12 +42,20 @@ interface IOnChainProposer { function initializeBridgeAddress(address bridge) external; /// @notice Upgrades the SP1 verification key that represents the sequencer's code. + /// @param commitHash git commit hash that produced the verifier keys for this batch. /// @param new_vk new verification key for SP1 verifier - function upgradeSP1VerificationKey(bytes32 new_vk) external; + function upgradeSP1VerificationKey( + bytes32 commitHash, + bytes32 new_vk + ) external; /// @notice Upgrades the RISC0 verification key that represents the sequencer's code. + /// @param commitHash git commit hash that produced the verifier keys for this batch. /// @param new_vk new verification key for RISC0 verifier - function upgradeRISC0VerificationKey(bytes32 new_vk) external; + function upgradeRISC0VerificationKey( + bytes32 commitHash, + bytes32 new_vk + ) external; /// @notice Commits to a batch of L2 blocks. /// @dev Committing to an L2 batch means to store the batch's commitment @@ -54,6 +67,7 @@ interface IOnChainProposer { /// @param processedDepositLogsRollingHash the rolling hash of the processed /// deposits logs of the batch to be committed. /// @param lastBlockHash the hash of the last block of the batch to be committed. + /// @param commitHash git commit hash that produced the verifier keys for this batch. /// @param _rlpEncodedBlocks the list of RLP-encoded blocks in the batch. function commitBatch( uint256 batchNumber, @@ -61,6 +75,7 @@ interface IOnChainProposer { bytes32 withdrawalsLogsMerkleRoot, bytes32 processedDepositLogsRollingHash, bytes32 lastBlockHash, + bytes32 commitHash, bytes[] calldata _rlpEncodedBlocks ) external; diff --git a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol index 9b3a85d190a..822241a2f35 100644 --- a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol @@ -32,8 +32,13 @@ interface IOnChainProposer { /// @notice A verification key has been upgraded. /// @dev Event emitted when a verification key is upgraded. /// @param verifier The name of the verifier whose key was upgraded. + /// @param commitHash The git commit hash associated to the verification key. /// @param newVerificationKey The new verification key. - event VerificationKeyUpgraded(string verifier, bytes32 newVerificationKey); + event VerificationKeyUpgraded( + string verifier, + bytes32 commitHash, + bytes32 newVerificationKey + ); /// @notice Set the bridge address for the first time. /// @dev This method is separated from initialize because both the CommonBridge @@ -44,11 +49,19 @@ interface IOnChainProposer { /// @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; + /// @param commit_hash git commit hash that produced the new verification key + function upgradeSP1VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) external; /// @notice Upgrades the RISC0 verification key that represents the sequencer's code. /// @param new_vk new verification key for RISC0 verifier - function upgradeRISC0VerificationKey(bytes32 new_vk) external; + /// @param commit_hash git commit hash that produced the new verification key + function upgradeRISC0VerificationKey( + bytes32 commit_hash, + bytes32 new_vk + ) external; /// @notice Commits to a batch of L2 blocks. /// @dev Committing to an L2 batch means to store the batch's commitment @@ -57,19 +70,21 @@ interface IOnChainProposer { /// @param newStateRoot the new state root of the batch to be committed. /// @param withdrawalsLogsMerkleRoot the merkle root of the withdrawal logs /// of the batch to be committed. - /// @param l2MessagesMerkleRoot the merkle root of the l2 messages - /// of the batch to be committed. /// @param processedPrivilegedTransactionsRollingHash the rolling hash of the processed /// privileged transactions of the batch to be committed. /// @param lastBlockHash the hash of the last block of the batch to be committed. + /// @param l2MessagesMerkleRoot the merkle root of the l2 messages + /// of the batch to be committed. + /// @param commitHash git commit hash that produced the verifier keys for this batch. /// @param balanceDiffs the balance diffs of the batch to be committed. function commitBatch( uint256 batchNumber, bytes32 newStateRoot, bytes32 withdrawalsLogsMerkleRoot, - bytes32 l2MessagesMerkleRoot, bytes32 processedPrivilegedTransactionsRollingHash, bytes32 lastBlockHash, + bytes32 l2MessagesMerkleRoot, + bytes32 commitHash, ICommonBridge.BalanceDiff[] calldata balanceDiffs ) external; diff --git a/crates/l2/prover/src/prover.rs b/crates/l2/prover/src/prover.rs index 0ed3366c719..74df19047c4 100644 --- a/crates/l2/prover/src/prover.rs +++ b/crates/l2/prover/src/prover.rs @@ -102,11 +102,12 @@ impl Prover { input, format, } => (batch_number, input, format), - ProofData::InvalidCodeVersion { commit_hash } => { - return Err(format!( - "Invalid code version received. Server commit_hash: {}, Prover commit_hash: {}", - commit_hash, self.commit_hash - )); + ProofData::NoBatchForVersion { commit_hash } => { + warn!( + "Received no batch available to prove for current version: {}. The prover may be older or newer to the next batch to prove", + commit_hash, + ); + return Ok(None); } _ => return Err("Expecting ProofData::Response".to_owned()), }; diff --git a/crates/l2/sequencer/l1_committer.rs b/crates/l2/sequencer/l1_committer.rs index 4e352300477..fe7ce2c92c1 100644 --- a/crates/l2/sequencer/l1_committer.rs +++ b/crates/l2/sequencer/l1_committer.rs @@ -13,6 +13,7 @@ use bytes::Bytes; use ethrex_blockchain::{ Blockchain, BlockchainOptions, BlockchainType, L2Config, error::ChainError, vm::StoreVmDatabase, }; +use ethrex_common::utils::keccak; use ethrex_common::{ Address, H256, U256, types::{ @@ -68,9 +69,9 @@ use spawned_concurrency::tasks::{ }; const COMMIT_FUNCTION_SIGNATURE_BASED: &str = - "commitBatch(uint256,bytes32,bytes32,bytes32,bytes32,bytes[])"; + "commitBatch(uint256,bytes32,bytes32,bytes32,bytes32,bytes32,bytes[])"; const COMMIT_FUNCTION_SIGNATURE: &str = - "commitBatch(uint256,bytes32, bytes32,bytes32,bytes32,bytes32,(uint256,uint256)[])"; + "commitBatch(uint256,bytes32,bytes32,bytes32,bytes32,bytes32,bytes32,(uint256,uint256)[])"; /// 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; @@ -1090,6 +1091,7 @@ impl L1Committer { debug!("l2 messages merkle root: {l2_messages_merkle_root:#x}"); debug!("l2 messages hashes len: {}", batch.l2_message_hashes.len()); let last_block_hash = get_last_block_hash(&self.store, batch.last_block)?; + let commit_hash_bytes = keccak(self.git_commit_hash.as_bytes()); let balance_diffs: Vec = batch .balance_diffs .iter() @@ -1118,6 +1120,7 @@ impl L1Committer { encoded_blocks.push(block.encode_to_vec().into()); } + calldata_values.push(Value::FixedBytes(commit_hash_bytes.0.to_vec().into())); calldata_values.push(Value::Array( encoded_blocks.into_iter().map(Value::Bytes).collect(), )); @@ -1125,6 +1128,7 @@ impl L1Committer { (COMMIT_FUNCTION_SIGNATURE_BASED, calldata_values) } else { calldata_values.push(Value::FixedBytes(l2_messages_merkle_root.0.to_vec().into())); + calldata_values.push(Value::FixedBytes(commit_hash_bytes.0.to_vec().into())); calldata_values.push(Value::Array(balance_diffs)); (COMMIT_FUNCTION_SIGNATURE, calldata_values) }; diff --git a/crates/l2/sequencer/proof_coordinator.rs b/crates/l2/sequencer/proof_coordinator.rs index 51023b50fea..f00bb2f78e5 100644 --- a/crates/l2/sequencer/proof_coordinator.rs +++ b/crates/l2/sequencer/proof_coordinator.rs @@ -50,9 +50,10 @@ pub enum ProofData { BatchRequest { commit_hash: String }, /// 4. - /// The Server responds with an InvalidCodeVersion if the code version is not compatible. - /// The Client should then update its code to match the server's version. - InvalidCodeVersion { commit_hash: String }, + /// The Server responds with a NoBatchForVersion if the code version is not the same as the one + /// generated in the batch. + /// The Client can only prove batches of its own version. + NoBatchForVersion { commit_hash: String }, /// 5. /// The Server responds with a BatchResponse containing the ProverInputData. @@ -95,9 +96,9 @@ impl ProofData { ProofData::BatchRequest { commit_hash } } - /// Builder function for creating a InvalidCodeVersion - pub fn invalid_code_version(commit_hash: String) -> Self { - ProofData::InvalidCodeVersion { commit_hash } + /// Builder function for creating a NoBatchForVersion + pub fn no_batch_for_version(commit_hash: String) -> Self { + ProofData::NoBatchForVersion { commit_hash } } /// Builder function for creating a BatchResponse @@ -251,15 +252,10 @@ impl ProofCoordinator { let batch_to_prove = 1 + self.rollup_store.get_latest_sent_batch_proof().await?; if commit_hash != self.git_commit_hash { - error!( - "Code version mismatch: expected {}, got {}", + debug!( + "Mismatch on prover version. Expected: {}, got: {}. Looking for batches left to prove", self.git_commit_hash, commit_hash ); - - let response = ProofData::invalid_code_version(self.git_commit_hash.clone()); - send_response(stream, &response).await?; - info!("InvalidCodeVersion sent"); - return Ok(()); } let mut all_proofs_exist = true; @@ -282,13 +278,13 @@ impl ProofCoordinator { } else { let Some(input) = self .rollup_store - .get_prover_input_by_batch_and_version(batch_to_prove, &self.git_commit_hash) + .get_prover_input_by_batch_and_version(batch_to_prove, &commit_hash) .await? else { - return Err(ProofCoordinatorError::MissingBatchProverInput( - batch_to_prove, - self.git_commit_hash.clone(), - )); + let response = ProofData::no_batch_for_version(commit_hash); + send_response(stream, &response).await?; + info!("No batch for version sent"); + return Ok(()); }; debug!("Sending BatchResponse for block_number: {batch_to_prove}"); let format = if self.aligned { diff --git a/crates/l2/tee/quote-gen/src/sender.rs b/crates/l2/tee/quote-gen/src/sender.rs index 393f8b02a23..0f48d211b1e 100644 --- a/crates/l2/tee/quote-gen/src/sender.rs +++ b/crates/l2/tee/quote-gen/src/sender.rs @@ -39,10 +39,10 @@ pub async fn get_batch(commit_hash: String) -> Result<(u64, ProgramInput), Strin )), _ => Err("No blocks to prove.".to_owned()), }, - ProofData::InvalidCodeVersion { + ProofData::NoBatchForVersion { commit_hash: server_code_version, } => Err(format!( - "Invalid code version received. Server code: {}, Prover code: {}", + "Next batch does not match with the current version. Server code: {}, Prover code: {}", server_code_version, commit_hash )), _ => Err("Expecting ProofData::Response".to_owned()), diff --git a/crates/l2/tests/tests.rs b/crates/l2/tests/tests.rs index 2e1363e6300..3d16be23314 100644 --- a/crates/l2/tests/tests.rs +++ b/crates/l2/tests/tests.rs @@ -90,10 +90,10 @@ const DEFAULT_PROPOSER_COINBASE_ADDRESS: Address = H160([ 0xad, 0x62, 0x0c, 0x8d, ]); -// 0x44669840b8f0aedaa707636272031b5e8d67516c +// 0xfb1b0f56d95052f1f3540cbbd53e903febdbb128 const DEFAULT_ON_CHAIN_PROPOSER_ADDRESS: Address = H160([ - 0x44, 0x66, 0x98, 0x40, 0xb8, 0xf0, 0xae, 0xda, 0xa7, 0x07, 0x63, 0x62, 0x72, 0x03, 0x1b, 0x5e, - 0x8d, 0x67, 0x51, 0x6c, + 0xfb, 0x1b, 0x0f, 0x56, 0xd9, 0x50, 0x52, 0xf1, 0xf3, 0x54, 0x0c, 0xbb, 0xd5, 0x3e, 0x90, 0x3f, + 0xeb, 0xdb, 0xb1, 0x28, ]); const DEFAULT_RICH_KEYS_FILE_PATH: &str = "../../fixtures/keys/private_keys_l1.txt"; diff --git a/docs/l2/fundamentals/upgrades.md b/docs/l2/fundamentals/upgrades.md new file mode 100644 index 00000000000..79c68b98e80 --- /dev/null +++ b/docs/l2/fundamentals/upgrades.md @@ -0,0 +1,33 @@ +# Upgrades + +## Sequencer and prover versions + +Each committed batch stores the git commit hash of the sequencer build that produced it. The OnChainProposer uses that commit hash to look up the verifier key in its `verificationKeys` mapping. When you upgrade the sequencer, all batches committed before the upgrade must be proved with the prover matching the old version, and all batches committed after the upgrade must be proved with a prover built from the new version. + +## Registering a new verification key + +To allow proofs from a new sequencer/prover build, register its verification key against the commit hash: + +1. Compute the commit hash as the Keccak-256 of the (reduced) git commit. For example, the commit `9219410` produces `b9105485bc4ba523201eaaf76478a47b259fa7399bbed795cf19294861b7fc57`. +2. From the OnChainProposer owner account, send the upgrade transaction. Example (replace addresses and keys with your values): + ``` + rex send \ + "upgradeSP1VerificationKey(bytes32,bytes32)" \ + \ + \ + --private-key + ``` +3. (Optional) Verify the mapping entry: + ``` + rex call \ + "verificationKeys(bytes32,uint8)(bytes32)" \ + \ + + ``` + `1` is the SP1 verifier ID (`2` is RISC0). + +### Verification key artifacts + +- The SP1 verification key that goes on-chain is obtained when you build the prover. It is stored at `crates/l2/prover/src/guest_program/src/sp1/out/riscv32im-succinct-zkvm-vk-bn254`. + +- For Aligned proving, use the `u32` form generated alongside it at `crates/l2/prover/src/guest_program/src/sp1/out/riscv32im-succinct-zkvm-vk-u32`.