diff --git a/Cargo.lock b/Cargo.lock index 732e36b4..823b3dce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12157,6 +12157,7 @@ dependencies = [ "secrecy", "serde", "serde_json", + "thiserror 2.0.12", "time", "tokio", "tracing", diff --git a/lib/batch_types/src/batch_signature.rs b/lib/batch_types/src/batch_signature.rs index 5970057c..30a87c5e 100644 --- a/lib/batch_types/src/batch_signature.rs +++ b/lib/batch_types/src/batch_signature.rs @@ -1,10 +1,15 @@ -use alloy::primitives::{Address, Signature as AlloySignature, SignatureError}; +use alloy::primitives::{ + Address, B256, Signature as AlloySignature, SignatureError, U256, keccak256, +}; use alloy::signers::Signer; use alloy::signers::local::PrivateKeySigner; -use alloy::sol_types::SolValue; +use alloy::sol_types::{SolValue, eip712_domain}; use serde::{Deserialize, Serialize}; -use zksync_os_contract_interface::IExecutor::CommitBatchInfoZKsyncOS; -use zksync_os_contract_interface::models::CommitBatchInfo; +use zksync_os_contract_interface::calldata::encode_commit_batch_data; +use zksync_os_contract_interface::models::StoredBatchInfo; +use zksync_os_types::ProtocolSemanticVersion; + +use crate::BatchInfo; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BatchSignatureSet(Vec); @@ -39,29 +44,63 @@ impl BatchSignatureSet { pub fn is_empty(&self) -> bool { self.0.is_empty() } + + pub fn to_vec(&self) -> &Vec { + &self.0 + } + + /// Remove signatures not found on allowed list + pub fn filter(mut self, allowed_signers: &[Address]) -> Self { + self.0.retain(|s| allowed_signers.contains(&s.signer)); + self + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct BatchSignature(AlloySignature); impl BatchSignature { - pub async fn sign_batch(batch_info: &CommitBatchInfo, private_key: &PrivateKeySigner) -> Self { - let encoded = encode_batch_for_signing(batch_info); - let signature = private_key.sign_message(&encoded).await.unwrap(); + /// Sign a batch for `commitBatchesMultisig` + pub async fn sign_batch( + prev_batch_info: &StoredBatchInfo, + batch_info: &BatchInfo, + l1_chain_id: u64, + multisig_committer: Address, + protocol_version: &ProtocolSemanticVersion, + private_key: &PrivateKeySigner, + ) -> Self { + let digest = eip712_multisig_digest( + prev_batch_info, + batch_info, + l1_chain_id, + multisig_committer, + protocol_version, + ); + let signature = private_key.sign_hash(&digest).await.unwrap(); BatchSignature(signature) } pub fn verify_signature( self, - batch_info: &CommitBatchInfo, + prev_batch_info: &StoredBatchInfo, + batch_info: &BatchInfo, + l1_chain_id: u64, + multisig_committer: Address, + protocol_version: &ProtocolSemanticVersion, ) -> Result { - let encoded = encode_batch_for_signing(batch_info); Ok(ValidatedBatchSignature { - signer: self.0.recover_address_from_msg(encoded)?, + signer: self + .0 + .recover_address_from_prehash(&eip712_multisig_digest( + prev_batch_info, + batch_info, + l1_chain_id, + multisig_committer, + protocol_version, + ))?, signature: self, }) } - pub fn into_raw(self) -> [u8; 65] { self.0.as_bytes() } @@ -72,9 +111,54 @@ impl BatchSignature { } } -fn encode_batch_for_signing(batch_info: &CommitBatchInfo) -> Vec { - let alloy_batch_info = CommitBatchInfoZKsyncOS::from(batch_info.clone()); - alloy_batch_info.abi_encode_params() +/// Compute the full EIP-712 digest used by the `MultisigCommitter` contract +/// for the `commitBatchesMultisig` typed data, based on the given batch info +/// and L1 domain parameters. +fn eip712_multisig_digest( + prev_batch_info: &StoredBatchInfo, + batch_info: &BatchInfo, + l1_chain_id: u64, + multisig_committer: Address, + protocol_version: &ProtocolSemanticVersion, +) -> B256 { + const TYPEHASH_BYTES: &[u8] = b"CommitBatchesMultisig(address chainAddress,uint256 processBatchFrom,uint256 processBatchTo,bytes batchData)"; + let typehash = keccak256(TYPEHASH_BYTES); + + let batch_data = encode_commit_batch_data( + prev_batch_info, + batch_info.commit_info.clone(), + protocol_version.minor, + ); + + let batch_data_hash = keccak256(batch_data); + + let encoded = ( + typehash, + batch_info.chain_address, + U256::from(batch_info.batch_number), // processBatchFrom + U256::from(batch_info.batch_number), // processBatchTo + batch_data_hash, + ) + .abi_encode_params(); + + let struct_hash = keccak256(encoded); + + let domain = eip712_domain! { + name: "MultisigCommitter", + version: "1", + chain_id: l1_chain_id, + verifying_contract: multisig_committer, + }; + let domain_separator = domain.separator(); + + keccak256( + [ + &[0x19, 0x01], + domain_separator.as_slice(), + struct_hash.as_slice(), + ] + .concat(), + ) } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/lib/batch_verification/src/client/mod.rs b/lib/batch_verification/src/client/mod.rs index 704abc62..88763d66 100644 --- a/lib/batch_verification/src/client/mod.rs +++ b/lib/batch_verification/src/client/mod.rs @@ -16,6 +16,7 @@ use tokio::sync::mpsc; use tokio_util::codec::{FramedRead, FramedWrite}; use zksync_os_batch_types::BlockMerkleTreeData; use zksync_os_batch_types::{BatchInfo, BatchSignature}; +use zksync_os_contract_interface::l1_discovery::{BatchVerificationL1, L1State}; use zksync_os_interface::types::BlockOutput; use zksync_os_merkle_tree::TreeBatchOutput; use zksync_os_observability::ComponentStateHandle; @@ -37,6 +38,7 @@ pub struct BatchVerificationClient { chain_id: u64, diamond_proxy: Address, server_address: String, + l1_state: L1State, signer: PrivateKeySigner, block_cache: BlockCache, } @@ -59,19 +61,31 @@ type VerificationInput = ( impl BatchVerificationClient { pub fn new( - finality: Finality, - private_key: SecretString, chain_id: u64, diamond_proxy: Address, server_address: String, + private_key: SecretString, + finality: Finality, + l1_state: L1State, ) -> Self { + let signer = PrivateKeySigner::from_str(private_key.expose_secret()) + .expect("Invalid batch verification private key"); + if let BatchVerificationL1::Enabled(l1_config) = l1_state.batch_verification.clone() { + if !l1_config.validators.contains(&signer.address()) { + tracing::warn!( + "Your address {} is not authorized to verify batches on L1", + signer.address() + ); + } + } + Self { - signer: PrivateKeySigner::from_str(private_key.expose_secret()) - .expect("Invalid batch verification private key"), chain_id, diamond_proxy, - block_cache: BlockCache::new(finality), server_address, + l1_state, + signer, + block_cache: BlockCache::new(finality), } } @@ -186,7 +200,7 @@ impl BatchVerificationClient { }) .collect::, BatchVerificationError>>()?; - let commit_batch_info = BatchInfo::new( + let batch_info = BatchInfo::new( blocks .iter() .map(|(block_output, replay_record, tree)| { @@ -202,18 +216,25 @@ impl BatchVerificationClient { self.diamond_proxy, request.batch_number, request.pubdata_mode, - ) - .commit_info; + ); - if commit_batch_info != request.commit_data { - let diff = request.commit_data.diff(&commit_batch_info); + if batch_info.commit_info != request.commit_data { + let diff = request.commit_data.diff(&batch_info.commit_info); return Err(BatchVerificationError::BatchDataMismatch(format!( "Batch data mismatch: {diff:?}", ))); } - let signature = BatchSignature::sign_batch(&request.commit_data, &self.signer).await; + let signature = BatchSignature::sign_batch( + &request.prev_commit_data, + &batch_info, + self.l1_state.l1_chain_id, + self.l1_state.validator_timelock, + &blocks.first().unwrap().1.protocol_version, + &self.signer, + ) + .await; Ok(signature) } diff --git a/lib/batch_verification/src/config.rs b/lib/batch_verification/src/config.rs index e986964e..44e5a253 100644 --- a/lib/batch_verification/src/config.rs +++ b/lib/batch_verification/src/config.rs @@ -10,7 +10,7 @@ pub struct BatchVerificationConfig { pub listen_address: String, pub client_enabled: bool, pub connect_address: String, - pub threshold: usize, + pub threshold: u64, pub accepted_signers: Vec, pub request_timeout: Duration, pub retry_delay: Duration, diff --git a/lib/batch_verification/src/request.rs b/lib/batch_verification/src/request.rs index 07bb3f49..fe65d965 100644 --- a/lib/batch_verification/src/request.rs +++ b/lib/batch_verification/src/request.rs @@ -1,6 +1,6 @@ use tokio_util::bytes::BytesMut; use tokio_util::codec::{self, LengthDelimitedCodec}; -use zksync_os_contract_interface::models::CommitBatchInfo; +use zksync_os_contract_interface::models::{CommitBatchInfo, StoredBatchInfo}; use zksync_os_types::PubdataMode; /// Request sent from main sequencer to external nodes for batch verification @@ -12,6 +12,7 @@ pub struct BatchVerificationRequest { pub pubdata_mode: PubdataMode, pub request_id: u64, pub commit_data: CommitBatchInfo, + pub prev_commit_data: StoredBatchInfo, } impl std::fmt::Debug for BatchVerificationRequest { diff --git a/lib/batch_verification/src/sequencer/component.rs b/lib/batch_verification/src/sequencer/component.rs index d72d0f03..4fc82e36 100644 --- a/lib/batch_verification/src/sequencer/component.rs +++ b/lib/batch_verification/src/sequencer/component.rs @@ -13,7 +13,7 @@ use std::time::Duration; use tokio::sync::mpsc::{self, Sender}; use tokio::time::Instant; use zksync_os_batch_types::{BatchSignatureSet, ValidatedBatchSignature}; -use zksync_os_contract_interface::models::CommitBatchInfo; +use zksync_os_contract_interface::l1_discovery::{BatchVerificationL1, L1State}; use zksync_os_l1_sender::batcher_metrics::BatchExecutionStage; use zksync_os_l1_sender::batcher_model::{ BatchForSigning, BatchSignatureData, SignedBatchEnvelope, @@ -29,15 +29,46 @@ fn report_exit(name: &'static str) -> impl Fn(Result { config: BatchVerificationConfig, + threshold: u64, + validators: Vec
, last_committed_batch_number: u64, + l1_state: L1State, _phantom: std::marker::PhantomData, } impl BatchVerificationPipelineStep { - pub fn new(config: BatchVerificationConfig, last_committed_batch_number: u64) -> Self { + pub fn new( + config: BatchVerificationConfig, + l1_state: L1State, + last_committed_batch_number: u64, + ) -> Self { + let config_validators = config + .accepted_signers + .clone() + .into_iter() + .map(|s| s.parse().unwrap()) + .collect(); + // If on L1 batch verifiers re configured, we use that configuration instead + let (threshold, validators) = match l1_state.batch_verification.clone() { + BatchVerificationL1::Enabled(l1_config) => { + if !l1_config.validators.is_empty() || l1_config.threshold > 0 { + ( + config.threshold.max(l1_config.threshold), + l1_config.validators, + ) + } else { + (config.threshold, config_validators) + } + } + BatchVerificationL1::Disabled => (config.threshold, config_validators), + }; + Self { config, + threshold, + validators, last_committed_batch_number, + l1_state, _phantom: std::marker::PhantomData, } } @@ -79,8 +110,12 @@ impl PipelineComponent for BatchVerificationPipelineSt let verifier = BatchVerifier::new( self.config, + self.validators, + self.threshold, response_channels, server, + self.l1_state.l1_chain_id, + self.l1_state.validator_timelock, self.last_committed_batch_number, ); let verifier_fut = verifier @@ -145,9 +180,12 @@ async fn run_batch_response_processor( struct BatchVerifier { config: BatchVerificationConfig, accepted_signers: Vec
, + threshold: u64, request_id_counter: AtomicU64, server: Arc, response_channels: Arc>>, + l1_chain_id: u64, + multisig_committer: Address, last_committed_batch_number: u64, } @@ -156,7 +194,7 @@ enum BatchVerificationError { #[error("Timeout")] Timeout, #[error("Not enough signers: {0} < {1}")] - NotEnoughSigners(usize, usize), + NotEnoughSigners(u64, u64), #[error("Internal error: {0}")] Internal(String), } @@ -183,22 +221,23 @@ impl BatchVerificationError { impl BatchVerifier { pub fn new( config: BatchVerificationConfig, + accepted_signers: Vec
, + threshold: u64, response_channels: Arc>>, server: Arc, + l1_chain_id: u64, + multisig_committer: Address, last_committed_batch_number: u64, ) -> Self { - let accepted_signers = config - .accepted_signers - .clone() - .into_iter() - .map(|s| s.parse().unwrap()) - .collect(); Self { config, request_id_counter: AtomicU64::new(1), response_channels, server, accepted_signers, + threshold, + l1_chain_id, + multisig_committer, last_committed_batch_number, } } @@ -313,18 +352,16 @@ impl BatchVerifier { // Create a channel for collecting responses for this request let (response_sender, mut response_receiver) = - mpsc::channel::(self.config.threshold); + mpsc::channel::(self.threshold.try_into().unwrap()); // Register the channel for this request_id self.response_channels.insert(request_id, response_sender); // Send verification request to all connected clients self.server - .send_verification_request(batch_envelope, request_id, self.config.threshold) + .send_verification_request(batch_envelope, request_id, self.threshold) .await?; - let commit_data = batch_envelope.batch.batch_info.commit_info.clone(); - // Collect responses with timeout let mut responses = BatchSignatureSet::new(); let start_time = Instant::now(); @@ -348,7 +385,7 @@ impl BatchVerifier { }; let Some(validated_signature) = - self.process_response(&commit_data, request_id, response) + self.process_response(batch_envelope, request_id, response) else { continue; }; @@ -376,10 +413,10 @@ impl BatchVerifier { response_latency_ms = latency.as_millis() as u64, "Validated response {} of {}", responses.len(), - self.config.threshold + self.threshold ); - if responses.len() >= self.config.threshold { + if u64::try_from(responses.len()).unwrap() >= self.threshold { break; } } @@ -401,9 +438,9 @@ impl BatchVerifier { /// Processes BatchVerificationResponse, on any error logs and returns None /// - extracts & validates signature /// - checks against list of accepted signers - fn process_response( + fn process_response( &self, - commit_data: &CommitBatchInfo, + batch_envelope: &BatchForSigning, request_id: u64, response: BatchVerificationResponse, ) -> Option { @@ -417,7 +454,7 @@ impl BatchVerifier { .. } => { tracing::info!( - batch_number = commit_data.batch_number, + batch_number = batch_envelope.batch_number(), request_id = request_id, "Verification refused: {}", reason @@ -426,9 +463,15 @@ impl BatchVerifier { } }; - let Ok(validated_signature) = signature.verify_signature(commit_data) else { + let Ok(validated_signature) = signature.verify_signature( + &batch_envelope.batch.previous_stored_batch_info, + &batch_envelope.batch.batch_info, + self.l1_chain_id, + self.multisig_committer, + &batch_envelope.batch.protocol_version, + ) else { tracing::warn!( - batch_number = commit_data.batch_number, + batch_number = batch_envelope.batch_number(), request_id = request_id, "Invalid signature", ); @@ -437,7 +480,7 @@ impl BatchVerifier { if !self.accepted_signers.contains(validated_signature.signer()) { tracing::warn!( - batch_number = commit_data.batch_number, + batch_number = batch_envelope.batch_number(), request_id = request_id, signer = validated_signature.signer().to_string(), "Signature from unknown signer", diff --git a/lib/batch_verification/src/sequencer/server.rs b/lib/batch_verification/src/sequencer/server.rs index 0d2d0a36..69da0ed7 100644 --- a/lib/batch_verification/src/sequencer/server.rs +++ b/lib/batch_verification/src/sequencer/server.rs @@ -27,7 +27,7 @@ pub(super) struct BatchVerificationServer { #[allow(clippy::large_enum_variant)] pub enum BatchVerificationRequestError { #[error("Not enough clients connected: {0} < {1}")] - NotEnoughClients(usize, usize), + NotEnoughClients(u64, u64), #[error("Failed to send batch verification request: {0}")] SendError(#[from] broadcast::error::SendError), } @@ -142,7 +142,7 @@ impl BatchVerificationServer { &self, batch_envelope: &BatchForSigning, request_id: u64, - required_clients: usize, + required_clients: u64, ) -> Result<(), BatchVerificationRequestError> { let request = BatchVerificationRequest { batch_number: batch_envelope.batch_number(), @@ -150,10 +150,12 @@ impl BatchVerificationServer { last_block_number: batch_envelope.batch.last_block_number, pubdata_mode: batch_envelope.batch.pubdata_mode, commit_data: batch_envelope.batch.batch_info.commit_info.clone(), + prev_commit_data: batch_envelope.batch.previous_stored_batch_info.clone(), request_id, }; - let clients_count = self.verification_request_broadcast.receiver_count(); + let clients_count = + u64::try_from(self.verification_request_broadcast.receiver_count()).unwrap(); if clients_count < required_clients { return Err(BatchVerificationRequestError::NotEnoughClients( diff --git a/lib/batch_verification/src/wire_format/conversion.rs b/lib/batch_verification/src/wire_format/conversion.rs index 224c4920..d9f9f3b0 100644 --- a/lib/batch_verification/src/wire_format/conversion.rs +++ b/lib/batch_verification/src/wire_format/conversion.rs @@ -5,7 +5,10 @@ use crate::{ }; use alloy::sol_types::SolValue; use zksync_os_batch_types::BatchSignature; -use zksync_os_contract_interface::{IExecutor::CommitBatchInfoZKsyncOS, models::CommitBatchInfo}; +use zksync_os_contract_interface::{ + IExecutor::{self, CommitBatchInfoZKsyncOS}, + models::{CommitBatchInfo, StoredBatchInfo}, +}; use zksync_os_types::PubdataMode; impl From for BatchVerificationRequest { @@ -17,10 +20,15 @@ impl From for BatchVerificationRequest { pubdata_mode, request_id, commit_data, + prev_commit_data, } = value; let decoded_commit_data_alloy = CommitBatchInfoZKsyncOS::abi_decode(&commit_data) .expect("Failed to decode commit data"); let decoded_commit_data = CommitBatchInfo::from(decoded_commit_data_alloy); + let decoded_prev_commit_data_alloy = + IExecutor::StoredBatchInfo::abi_decode(&prev_commit_data) + .expect("Failed to decode prev commit data"); + let decoded_prev_commit_data = StoredBatchInfo::from(decoded_prev_commit_data_alloy); Self { batch_number, first_block_number, @@ -29,6 +37,7 @@ impl From for BatchVerificationRequest { .expect("Failed to decode pubdata mode"), request_id, commit_data: decoded_commit_data, + prev_commit_data: decoded_prev_commit_data, } } } @@ -42,9 +51,12 @@ impl From for BatchVerificationRequestWireFormatV1 { pubdata_mode, request_id, commit_data, + prev_commit_data, } = value; let commit_data_alloy = CommitBatchInfoZKsyncOS::from(commit_data); let encoded_commit_data = commit_data_alloy.abi_encode(); + let prev_commit_data_alloy = IExecutor::StoredBatchInfo::from(&prev_commit_data); + let encoded_prev_commit_data = prev_commit_data_alloy.abi_encode(); Self { batch_number, first_block_number, @@ -52,6 +64,7 @@ impl From for BatchVerificationRequestWireFormatV1 { pubdata_mode: pubdata_mode.to_u8(), request_id, commit_data: encoded_commit_data, + prev_commit_data: encoded_prev_commit_data, } } } diff --git a/lib/batch_verification/src/wire_format/tests/mod.rs b/lib/batch_verification/src/wire_format/tests/mod.rs index 87d23900..3a1cd16b 100644 --- a/lib/batch_verification/src/wire_format/tests/mod.rs +++ b/lib/batch_verification/src/wire_format/tests/mod.rs @@ -3,7 +3,7 @@ use crate::{ BatchVerificationResult, }; use zksync_os_batch_types::BatchSignature; -use zksync_os_contract_interface::models::{CommitBatchInfo, DACommitmentScheme}; +use zksync_os_contract_interface::models::{CommitBatchInfo, DACommitmentScheme, StoredBatchInfo}; use zksync_os_types::PubdataMode; fn create_sample_request() -> BatchVerificationRequest { @@ -31,6 +31,16 @@ fn create_sample_request() -> BatchVerificationRequest { chain_id: 6565, operator_da_input: vec![], }, + prev_commit_data: StoredBatchInfo { + batch_number: 41, + state_commitment: B256::ZERO, + number_of_layer1_txs: 0, + priority_operations_hash: B256::ZERO, + dependency_roots_rolling_hash: B256::ZERO, + l2_to_l1_logs_root_hash: B256::ZERO, + commitment: B256::ZERO, + last_block_timestamp: 1234567880, + }, } } diff --git a/lib/batch_verification/src/wire_format/v1.rs b/lib/batch_verification/src/wire_format/v1.rs index 5d1a7387..e7996240 100644 --- a/lib/batch_verification/src/wire_format/v1.rs +++ b/lib/batch_verification/src/wire_format/v1.rs @@ -16,6 +16,7 @@ pub struct BatchVerificationRequestWireFormatV1 { pub pubdata_mode: u8, pub request_id: u64, pub commit_data: Vec, + pub prev_commit_data: Vec, } #[derive(Encode, Decode)] diff --git a/lib/contract_interface/src/calldata.rs b/lib/contract_interface/src/calldata.rs index 47b858c7..78e1fe33 100644 --- a/lib/contract_interface/src/calldata.rs +++ b/lib/contract_interface/src/calldata.rs @@ -1,8 +1,9 @@ -use crate::IExecutor; use crate::models::{CommitBatchInfo, StoredBatchInfo}; +use crate::{IExecutor, IExecutorV29}; use alloy::primitives::Address; use alloy::sol_types::{SolCall, SolValue}; +const V29_ENCODING_VERSION: u8 = 2; const V30_ENCODING_VERSION: u8 = 3; pub struct CommitCalldata { @@ -43,3 +44,43 @@ impl CommitCalldata { }) } } + +/// This function encodes only the last argument for commitBatchesSharedBridgeCall! +/// Implemented outside of struct to allow only passing necessary arguments +pub fn encode_commit_batch_data( + prev_batch_info: &StoredBatchInfo, + commit_info: CommitBatchInfo, + protocol_version_minor: u64, +) -> Vec { + let stored_batch_info = IExecutor::StoredBatchInfo::from(prev_batch_info); + match protocol_version_minor { + 29 => { + let commit_batch_info = IExecutorV29::CommitBatchInfoZKsyncOS::from(commit_info); + tracing::debug!( + last_batch_hash = ?prev_batch_info.hash(), + last_batch_number = ?prev_batch_info.batch_number, + new_batch_number = ?commit_batch_info.batchNumber, + "preparing commit calldata" + ); + let encoded_data = (stored_batch_info, vec![commit_batch_info]).abi_encode_params(); + + // Prefixed by current encoding version as expected by protocol + [[V29_ENCODING_VERSION].to_vec(), encoded_data].concat() + } + // 31 needed for upgrade integration test + 30 | 31 => { + let commit_batch_info = IExecutor::CommitBatchInfoZKsyncOS::from(commit_info.clone()); + tracing::debug!( + last_batch_hash = ?prev_batch_info.hash(), + last_batch_number = ?prev_batch_info.batch_number, + new_batch_number = ?commit_batch_info.batchNumber, + "preparing commit calldata" + ); + let encoded_data = (stored_batch_info, vec![commit_batch_info]).abi_encode_params(); + + // Prefixed by current encoding version as expected by protocol + [[V30_ENCODING_VERSION].to_vec(), encoded_data].concat() + } + _ => panic!("Unsupported protocol version: {protocol_version_minor}"), + } +} diff --git a/lib/contract_interface/src/l1_discovery.rs b/lib/contract_interface/src/l1_discovery.rs index 5d2f5b81..54df9f3d 100644 --- a/lib/contract_interface/src/l1_discovery.rs +++ b/lib/contract_interface/src/l1_discovery.rs @@ -1,23 +1,38 @@ use crate::metrics::L1_STATE_METRICS; use crate::models::BatchDaInputMode; -use crate::{Bridgehub, PubdataPricingMode, ZkChain}; +use crate::{Bridgehub, MultisigCommitter, PubdataPricingMode, ZkChain}; use alloy::eips::BlockId; use alloy::primitives::{Address, U256}; use alloy::providers::DynProvider; +use alloy::providers::Provider; use anyhow::Context; use backon::{ConstantBuilder, Retryable}; use std::fmt::{Debug, Display}; use std::time::Duration; +#[derive(Clone, Debug)] +pub struct BatchVerificationL1Config { + pub threshold: u64, + pub validators: Vec
, +} + +#[derive(Clone, Debug)] +pub enum BatchVerificationL1 { + Disabled, + Enabled(BatchVerificationL1Config), +} + #[derive(Clone, Debug)] pub struct L1State { pub bridgehub: Bridgehub, pub diamond_proxy: ZkChain, pub validator_timelock: Address, + pub batch_verification: BatchVerificationL1, pub last_committed_batch: u64, pub last_proved_batch: u64, pub last_executed_batch: u64, pub da_input_mode: BatchDaInputMode, + pub l1_chain_id: u64, } impl L1State { @@ -48,14 +63,43 @@ impl L1State { v => panic!("unexpected pubdata pricing mode: {}", v as u8), }; + let batch_verification = match MultisigCommitter::try_new( + validator_timelock_address, + diamond_proxy.provider().clone(), + *diamond_proxy.address(), + ) + .await + .context("failed to check MultisigCommitter interface")? + { + Some(multisig_committer) => { + let threshold = multisig_committer + .get_signing_threshold() + .await + .context("failed to get signing threshold")?; + let validators = multisig_committer + .get_validators() + .await + .context("failed to get validators")?; + BatchVerificationL1::Enabled(BatchVerificationL1Config { + threshold, + validators, + }) + } + None => BatchVerificationL1::Disabled, + }; + + let l1_chain_id = diamond_proxy.provider().get_chain_id().await?; + Ok(Self { bridgehub, diamond_proxy, validator_timelock: validator_timelock_address, + batch_verification, last_committed_batch, last_proved_batch, last_executed_batch, da_input_mode, + l1_chain_id, }) } @@ -88,7 +132,9 @@ impl L1State { bridgehub: this.bridgehub, diamond_proxy: this.diamond_proxy, validator_timelock: this.validator_timelock, + batch_verification: this.batch_verification, last_committed_batch, + l1_chain_id: this.l1_chain_id, last_proved_batch, last_executed_batch, da_input_mode: this.da_input_mode, diff --git a/lib/contract_interface/src/lib.rs b/lib/contract_interface/src/lib.rs index ad570f5e..89764769 100644 --- a/lib/contract_interface/src/lib.rs +++ b/lib/contract_interface/src/lib.rs @@ -7,6 +7,7 @@ use crate::IBridgehub::{ IBridgehubInstance, L2TransactionRequestDirect, L2TransactionRequestTwoBridgesOuter, requestL2TransactionDirectCall, requestL2TransactionTwoBridgesCall, }; +use crate::IMultisigCommitter::IMultisigCommitterInstance; use crate::IZKChain::IZKChainInstance; use alloy::contract::SolCallBuilder; use alloy::eips::BlockId; @@ -290,6 +291,27 @@ alloy::sol! { interface IBytecodeSupplier { event BytecodePublished(bytes32 indexed bytecodeHash, bytes bytecode); } + + #[sol(rpc)] + interface IMultisigCommitter { + + function commitBatchesMultisig( + address chainAddress, + uint256 _processBatchFrom, + uint256 _processBatchTo, + bytes calldata _batchData, + address[] calldata signers, + bytes[] calldata signatures + ) external; + + function getSigningThreshold(address chainAddress) external view returns (uint64); + + function isValidator(address chainAddress, address validator) external view returns (bool); + + function getValidatorsCount(address chainAddress) external view returns (uint256); + + function getValidatorsMember(address chainAddress, uint256 index) external view returns (address); + } } #[derive(Clone, Debug)] @@ -415,6 +437,87 @@ impl Bridgehub

{ } } +#[derive(Clone, Debug)] +pub struct MultisigCommitter { + instance: IMultisigCommitterInstance, + chain_address: Address, +} + +impl MultisigCommitter

{ + pub fn new(address: Address, provider: P, chain_address: Address) -> Self { + let instance = IMultisigCommitter::new(address, provider); + Self { + instance, + chain_address, + } + } + + /// Checks if the contract at the given address implements the `IMultisigCommitter` interface + /// by calling `getSigningThreshold`. Returns `Some(Self)` if successful, `None` if the call + /// reverts (indicating the contract doesn't implement the interface), or an error for other + /// failures (e.g., network errors). + pub async fn try_new( + address: Address, + provider: P, + chain_address: Address, + ) -> core::result::Result, alloy::contract::Error> { + let instance = IMultisigCommitter::new(address, provider); + let result = instance.getSigningThreshold(chain_address).call().await; + match result { + Ok(_) => Ok(Some(Self { + instance, + chain_address, + })), + Err(e) if e.as_revert_data().is_some() => Ok(None), + Err(e) => Err(e), + } + } + + pub async fn get_signing_threshold(&self) -> Result { + self.instance + .getSigningThreshold(self.chain_address) + .call() + .await + .enrich("getSigningThreshold", None) + } + + pub async fn is_validator(&self, validator: Address) -> Result { + self.instance + .isValidator(self.chain_address, validator) + .call() + .await + .enrich("isValidator", None) + } + + pub async fn get_validators_count(&self) -> Result { + self.instance + .getValidatorsCount(self.chain_address) + .call() + .await + .enrich("getValidatorsCount", None) + } + + pub async fn get_validator(&self, index: U256) -> Result

{ + self.instance + .getValidatorsMember(self.chain_address, index) + .call() + .await + .enrich("getValidatorsMember", None) + } + + /// Returns the list of all validators for the chain. + pub async fn get_validators(&self) -> Result> { + let count = self.get_validators_count().await?; + let count: u64 = count.saturating_to(); + let mut validators = Vec::with_capacity(count as usize); + for i in 0..count { + let validator = self.get_validator(U256::from(i)).await?; + validators.push(validator); + } + Ok(validators) + } +} + #[derive(Clone, Debug)] pub struct ZkChain { instance: IZKChainInstance, diff --git a/lib/contract_interface/src/models.rs b/lib/contract_interface/src/models.rs index 836930b5..ae6f7486 100644 --- a/lib/contract_interface/src/models.rs +++ b/lib/contract_interface/src/models.rs @@ -33,7 +33,7 @@ pub enum BatchDaInputMode { /// User-friendly version of [`IExecutor::StoredBatchInfo`] containing /// fields that are relevant for ZKsync OS. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct StoredBatchInfo { pub batch_number: u64, pub state_commitment: B256, diff --git a/lib/l1_sender/Cargo.toml b/lib/l1_sender/Cargo.toml index 26834ab8..d7661aa8 100644 --- a/lib/l1_sender/Cargo.toml +++ b/lib/l1_sender/Cargo.toml @@ -43,6 +43,7 @@ tracing.workspace = true time = { workspace = true, features = ["parsing", "formatting"] } itertools.workspace = true vise.workspace = true +thiserror.workspace = true [dev-dependencies] serde_json.workspace = true diff --git a/lib/l1_sender/src/batcher_model.rs b/lib/l1_sender/src/batcher_model.rs index cb43d051..07e7caf5 100644 --- a/lib/l1_sender/src/batcher_model.rs +++ b/lib/l1_sender/src/batcher_model.rs @@ -75,7 +75,7 @@ fn default_protocol_version() -> ProtocolSemanticVersion { #[derive(Debug)] pub struct MissingSignature; -#[derive(Debug, Serialize, Deserialize, Default)] +#[derive(Debug, Serialize, Deserialize, Default, Clone)] pub enum BatchSignatureData { Signed { signatures: BatchSignatureSet, diff --git a/lib/l1_sender/src/commands/commit.rs b/lib/l1_sender/src/commands/commit.rs index 6bfa0d0f..2317912c 100644 --- a/lib/l1_sender/src/commands/commit.rs +++ b/lib/l1_sender/src/commands/commit.rs @@ -1,20 +1,72 @@ use crate::batcher_metrics::BatchExecutionStage; -use crate::batcher_model::{FriProof, SignedBatchEnvelope}; +use crate::batcher_model::{BatchSignatureData, FriProof, SignedBatchEnvelope}; use crate::commands::SendToL1; use alloy::consensus::BlobTransactionSidecar; -use alloy::primitives::U256; -use alloy::sol_types::{SolCall, SolValue}; +use alloy::primitives::{Bytes, U256}; +use alloy::sol_types::SolCall; use std::fmt::Display; -use zksync_os_contract_interface::{IExecutor, IExecutorV29}; +use zksync_os_batch_types::BatchSignatureSet; +use zksync_os_contract_interface::calldata::encode_commit_batch_data; +use zksync_os_contract_interface::l1_discovery::BatchVerificationL1; +use zksync_os_contract_interface::{IExecutor, IMultisigCommitter}; #[derive(Debug)] pub struct CommitCommand { - input: SignedBatchEnvelope, + pub(super) input: SignedBatchEnvelope, + pub(super) signatures: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum BatchVerificationError { + #[error("Batch was not signed")] + BatchNotSigned, + #[error("Not enough signatures, we have {} but need {}", .0, .1)] + NotEnoughSignatures(u64, u64), } impl CommitCommand { - pub fn new(input: SignedBatchEnvelope) -> Self { - Self { input } + /// This function should not error normally, however if the signatures + /// attached to batch do not allow for submission to L1 it will error + /// instead of causing a reverted transaction. + pub fn try_new( + l1_config: &BatchVerificationL1, + input: SignedBatchEnvelope, + ) -> Result { + match (l1_config, input.signature_data.clone()) { + (BatchVerificationL1::Disabled, _) => Ok(Self { + input, + signatures: None, + }), + ( + BatchVerificationL1::Enabled(l1_config), + BatchSignatureData::Signed { signatures }, + ) => { + let allowed_signers = &l1_config.validators; + let filtered_signatures = signatures.filter(allowed_signers); + // edge case: if threshold is 0 it is safe to submit 0 signatures + if u64::try_from(filtered_signatures.len()).unwrap() < l1_config.threshold { + return Err(BatchVerificationError::NotEnoughSignatures( + u64::try_from(filtered_signatures.len()).unwrap(), //its fairly safe to convert usize into u64 + l1_config.threshold, + )); + } + Ok(Self { + input, + signatures: Some(filtered_signatures), + }) + } + (BatchVerificationL1::Enabled(l1_config), _) => { + // actually if threshold is 0 its still ok without signing enabled + if l1_config.threshold == 0 { + Ok(Self { + input, + signatures: None, + }) + } else { + Err(BatchVerificationError::BatchNotSigned) + } + } + } } pub(crate) fn input(&self) -> &SignedBatchEnvelope { @@ -28,14 +80,50 @@ impl SendToL1 for CommitCommand { const MINED_STAGE: BatchExecutionStage = BatchExecutionStage::CommitL1TxMined; const PASSTHROUGH_STAGE: BatchExecutionStage = BatchExecutionStage::CommitL1Passthrough; - fn solidity_call(&self) -> impl SolCall { - // todo: encode through `CommitCalldata` instead - IExecutor::commitBatchesSharedBridgeCall::new(( - self.input.batch.batch_info.chain_address, - U256::from(self.input.batch_number()), - U256::from(self.input.batch_number()), - self.to_calldata_suffix().into(), - )) + fn solidity_call(&self) -> Bytes { + if let Some(signatures_set) = &self.signatures { + let mut signatures = signatures_set.to_vec().clone(); + signatures.sort_by(|a, b| a.signer().cmp(b.signer())); + let (signers, signatures): (Vec<_>, Vec) = signatures + .into_iter() + .map(|s| { + let signer = *s.signer(); + let signature_bytes: Bytes = s.signature().clone().into_raw().to_vec().into(); + (signer, signature_bytes) + }) + .unzip(); + + IMultisigCommitter::commitBatchesMultisigCall::new(( + self.input.batch.batch_info.chain_address, + U256::from(self.input.batch_number()), + U256::from(self.input.batch_number()), + encode_commit_batch_data( + &self.input.batch.previous_stored_batch_info, + self.input.batch.batch_info.commit_info.clone(), + self.input.batch.protocol_version.minor, + ) + .into(), + signers, + signatures, + )) + .abi_encode() + .into() + } else { + // todo: encode through `CommitCalldata` instead + IExecutor::commitBatchesSharedBridgeCall::new(( + self.input.batch.batch_info.chain_address, + U256::from(self.input.batch_number()), + U256::from(self.input.batch_number()), + encode_commit_batch_data( + &self.input.batch.previous_stored_batch_info, + self.input.batch.batch_info.commit_info.clone(), + self.input.batch.protocol_version.minor, + ) + .into(), + )) + .abi_encode() + .into() + } } fn blob_sidecar(&self) -> Option { @@ -63,58 +151,21 @@ impl From for Vec> { impl Display for CommitCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "commit batch {}", self.input.batch_number())?; - Ok(()) - } -} - -impl CommitCommand { - /// `commitBatchesSharedBridge` expects the rest of calldata to be of very specific form. This - /// function makes sure last committed batch and new batch are encoded correctly. - fn to_calldata_suffix(&self) -> Vec { - let stored_batch_info = - IExecutor::StoredBatchInfo::from(&self.input.batch.previous_stored_batch_info); - - match self.input.batch.protocol_version.minor { - 29 => { - const V29_ENCODING_VERSION: u8 = 2; - - let commit_batch_info = IExecutorV29::CommitBatchInfoZKsyncOS::from( - self.input.batch.batch_info.commit_info.clone(), - ); - tracing::debug!( - last_batch_hash = ?self.input.batch.previous_stored_batch_info.hash(), - last_batch_number = ?self.input.batch.previous_stored_batch_info.batch_number, - new_batch_number = ?commit_batch_info.batchNumber, - "preparing commit calldata" - ); - let encoded_data = (stored_batch_info, vec![commit_batch_info]).abi_encode_params(); - - // Prefixed by current encoding version as expected by protocol - [[V29_ENCODING_VERSION].to_vec(), encoded_data].concat() - } - // 31 needed for upgrade integration test - 30 | 31 => { - const V30_ENCODING_VERSION: u8 = 3; - - let commit_batch_info = IExecutor::CommitBatchInfoZKsyncOS::from( - self.input.batch.batch_info.commit_info.clone(), - ); - tracing::debug!( - last_batch_hash = ?self.input.batch.previous_stored_batch_info.hash(), - last_batch_number = ?self.input.batch.previous_stored_batch_info.batch_number, - new_batch_number = ?commit_batch_info.batchNumber, - "preparing commit calldata" - ); - let encoded_data = (stored_batch_info, vec![commit_batch_info]).abi_encode_params(); - - // Prefixed by current encoding version as expected by protocol - [[V30_ENCODING_VERSION].to_vec(), encoded_data].concat() - } - _ => panic!( - "Unsupported protocol version: {}", - self.input.batch.protocol_version - ), + if let Some(signatures_set) = &self.signatures { + write!( + f, + "signed commit batch {}, signatures: {}", + self.input.batch_number(), + signatures_set + .to_vec() + .iter() + .map(|s| s.signer().to_string()) + .collect::>() + .join(", "), + )?; + } else { + write!(f, "commit batch {}", self.input.batch_number())?; } + Ok(()) } } diff --git a/lib/l1_sender/src/commands/execute.rs b/lib/l1_sender/src/commands/execute.rs index af5c44ff..658fdd97 100644 --- a/lib/l1_sender/src/commands/execute.rs +++ b/lib/l1_sender/src/commands/execute.rs @@ -1,7 +1,7 @@ use crate::batcher_metrics::BatchExecutionStage; use crate::batcher_model::{FriProof, SignedBatchEnvelope}; use crate::commands::SendToL1; -use alloy::primitives::U256; +use alloy::primitives::{Bytes, U256}; use alloy::sol_types::{SolCall, SolValue}; use std::fmt::Display; use zksync_os_contract_interface::models::PriorityOpsBatchInfo; @@ -33,13 +33,15 @@ impl SendToL1 for ExecuteCommand { const PASSTHROUGH_STAGE: BatchExecutionStage = BatchExecutionStage::ExecuteL1Passthrough; - fn solidity_call(&self) -> impl SolCall { + fn solidity_call(&self) -> Bytes { IExecutor::executeBatchesSharedBridgeCall::new(( self.batches.first().unwrap().batch.batch_info.chain_address, U256::from(self.batches.first().unwrap().batch_number()), U256::from(self.batches.last().unwrap().batch_number()), self.to_calldata_suffix().into(), )) + .abi_encode() + .into() } } diff --git a/lib/l1_sender/src/commands/mod.rs b/lib/l1_sender/src/commands/mod.rs index 68fec364..1cc7cca3 100644 --- a/lib/l1_sender/src/commands/mod.rs +++ b/lib/l1_sender/src/commands/mod.rs @@ -1,7 +1,7 @@ use crate::batcher_metrics::BatchExecutionStage; use crate::batcher_model::{FriProof, SignedBatchEnvelope}; use alloy::consensus::BlobTransactionSidecar; -use alloy::sol_types::SolCall; +use alloy::primitives::Bytes; use itertools::Itertools; use std::fmt::Display; @@ -43,7 +43,8 @@ pub trait SendToL1: const SENT_STAGE: BatchExecutionStage; const MINED_STAGE: BatchExecutionStage; const PASSTHROUGH_STAGE: BatchExecutionStage; - fn solidity_call(&self) -> impl SolCall; + /// We use `Bytes` instead of `SolCall`, because SolCall is a trait that cannot be dyn + fn solidity_call(&self) -> Bytes; fn blob_sidecar(&self) -> Option { None diff --git a/lib/l1_sender/src/commands/prove.rs b/lib/l1_sender/src/commands/prove.rs index 6e6289e4..4dc24ac1 100644 --- a/lib/l1_sender/src/commands/prove.rs +++ b/lib/l1_sender/src/commands/prove.rs @@ -1,7 +1,7 @@ use crate::batcher_metrics::BatchExecutionStage; use crate::batcher_model::{FriProof, SignedBatchEnvelope, SnarkProof}; use crate::commands::SendToL1; -use alloy::primitives::{B256, U256, keccak256}; +use alloy::primitives::{B256, Bytes, U256, keccak256}; use alloy::sol_types::SolCall; use std::collections::HashMap; use std::fmt::Display; @@ -31,13 +31,15 @@ impl SendToL1 for ProofCommand { const MINED_STAGE: BatchExecutionStage = BatchExecutionStage::ProveL1TxMined; const PASSTHROUGH_STAGE: BatchExecutionStage = BatchExecutionStage::ProveL1Passthrough; - fn solidity_call(&self) -> impl SolCall { + fn solidity_call(&self) -> Bytes { proveBatchesSharedBridgeCall::new(( self.batches.first().unwrap().batch.batch_info.chain_address, U256::from(self.batches.first().unwrap().batch_number()), U256::from(self.batches.last().unwrap().batch_number()), self.to_calldata_suffix().into(), )) + .abi_encode() + .into() } } diff --git a/lib/l1_sender/src/lib.rs b/lib/l1_sender/src/lib.rs index 476b0b01..9b337c24 100644 --- a/lib/l1_sender/src/lib.rs +++ b/lib/l1_sender/src/lib.rs @@ -137,7 +137,7 @@ pub async fn run_l1_sender( ) .await? .with_to(to_address) - .with_call(&cmd.solidity_call()); + .with_input(cmd.solidity_call()); if let Some(blob_sidecar) = cmd.blob_sidecar() { let fee_per_blob_gas = provider.get_blob_base_fee().await?; diff --git a/node/bin/src/config.rs b/node/bin/src/config.rs index 14a14efa..b74bce84 100644 --- a/node/bin/src/config.rs +++ b/node/bin/src/config.rs @@ -615,7 +615,7 @@ pub struct BatchVerificationConfig { pub connect_address: String, /// [server] Threshold (number of needed signatures) #[config(default_t = 1)] - pub threshold: usize, + pub threshold: u64, /// [server] Accepted signer pubkeys #[config(default_t = vec!["0x36615Cf349d7F6344891B1e7CA7C72883F5dc049".into()], with = Delimited(","))] pub accepted_signers: Vec, diff --git a/node/bin/src/lib.rs b/node/bin/src/lib.rs index a74ea79c..8105741a 100644 --- a/node/bin/src/lib.rs +++ b/node/bin/src/lib.rs @@ -726,6 +726,7 @@ async fn run_main_node_pipeline( }) .pipe(BatchVerificationPipelineStep::new( config.batch_verification_config.into(), + node_state_on_startup.l1_state.clone(), node_state_on_startup.l1_state.last_committed_batch, )) .pipe(fri_proving_step) @@ -733,6 +734,7 @@ async fn run_main_node_pipeline( next_expected_batch_number: node_state_on_startup.l1_state.last_executed_batch + 1, last_committed_batch_number: node_state_on_startup.l1_state.last_committed_batch, proof_storage: batch_storage.clone(), + batch_verification_l1_config: node_state_on_startup.l1_state.batch_verification.clone(), }) .pipe(UpgradeGatekeeper::new( node_state_on_startup.l1_state.diamond_proxy.clone(), @@ -832,11 +834,12 @@ async fn run_en_pipeline( .pipe_if( config.batch_verification_config.client_enabled, BatchVerificationClient::new( - finality.clone(), - config.batch_verification_config.signing_key.clone(), config.genesis_config.chain_id.unwrap(), *node_state_on_startup.l1_state.diamond_proxy.address(), config.batch_verification_config.connect_address, + config.batch_verification_config.signing_key.clone(), + finality.clone(), + node_state_on_startup.l1_state.clone(), ), NoOpSink::new(), ) diff --git a/node/bin/src/prover_api/gapless_committer.rs b/node/bin/src/prover_api/gapless_committer.rs index 8b15d7a6..5b5093cc 100644 --- a/node/bin/src/prover_api/gapless_committer.rs +++ b/node/bin/src/prover_api/gapless_committer.rs @@ -1,7 +1,9 @@ use crate::prover_api::proof_storage::{ProofStorage, StoredBatch}; +use anyhow::Context; use async_trait::async_trait; use std::collections::BTreeMap; use tokio::sync::mpsc; +use zksync_os_contract_interface::l1_discovery::BatchVerificationL1; use zksync_os_l1_sender::batcher_metrics::BatchExecutionStage; use zksync_os_l1_sender::batcher_model::{FriProof, SignedBatchEnvelope}; use zksync_os_l1_sender::commands::L1SenderCommand; @@ -20,6 +22,7 @@ pub struct GaplessCommitter { pub next_expected_batch_number: u64, pub last_committed_batch_number: u64, pub proof_storage: ProofStorage, + pub batch_verification_l1_config: BatchVerificationL1, } #[async_trait] @@ -76,9 +79,12 @@ impl PipelineComponent for GaplessCommitter { stored_batch.batch_envelope(), )) } else { - L1SenderCommand::SendToL1(CommitCommand::new( + CommitCommand::try_new( + &self.batch_verification_l1_config, stored_batch.batch_envelope(), - )) + ) + .map(L1SenderCommand::SendToL1) + .context("Committer batch signature failure")? }; latency_tracker.enter_state(GenericComponentState::WaitingSend); output.send(result).await?;