diff --git a/Cargo.lock b/Cargo.lock index dcb41a6fa931..8a62f541fb69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8109,8 +8109,7 @@ dependencies = [ [[package]] name = "zksync_concurrency" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28279a743cd2ec5a0e3f0fec31b2e4fdd509d0b513e0aaeb000200ce464123e5" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "once_cell", @@ -8143,8 +8142,7 @@ dependencies = [ [[package]] name = "zksync_consensus_bft" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011210cdeb207516fe95ec2c8a77b3c36e444e2cd17e7db57afdc55a263025d6" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "async-trait", @@ -8165,8 +8163,7 @@ dependencies = [ [[package]] name = "zksync_consensus_crypto" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dbbc36ff78548f022192f20fb76909b1b0a460fc85289ccc54ce0ce54263165" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "blst", @@ -8189,8 +8186,7 @@ dependencies = [ [[package]] name = "zksync_consensus_executor" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f6811105b9b0fffb5983382c504d466a415f41f4a3b0f6743837bcbfc0b332" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "rand 0.8.5", @@ -8209,8 +8205,7 @@ dependencies = [ [[package]] name = "zksync_consensus_network" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79538ef206af7006c94c8d047582cf214ac493f7dd8340d40cace4f248d8c35" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "async-trait", @@ -8244,8 +8239,7 @@ dependencies = [ [[package]] name = "zksync_consensus_roles" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0070c54eed2f5cf26e76d9ec3ccdf05fdafb18c0712c8d97ef4987634972396" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "bit-vec", @@ -8266,8 +8260,7 @@ dependencies = [ [[package]] name = "zksync_consensus_storage" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d221fbd8e22f49175132c252a4923a945c1fa4a548ad66c3fc0366789cc9e53" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "async-trait", @@ -8277,6 +8270,7 @@ dependencies = [ "tracing", "vise", "zksync_concurrency", + "zksync_consensus_crypto", "zksync_consensus_roles", "zksync_protobuf", "zksync_protobuf_build", @@ -8285,8 +8279,7 @@ dependencies = [ [[package]] name = "zksync_consensus_utils" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c3d9b3b6b795ce16e0ead2b8813a2f7a1a01c9a9e3fb50993d6ecbfcdbca98" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "rand 0.8.5", @@ -9229,8 +9222,7 @@ dependencies = [ [[package]] name = "zksync_protobuf" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe77d262206bb22f4bc26e75b68466b2e7043baa4963fe97190ce8540a5d700" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "bit-vec", @@ -9250,8 +9242,7 @@ dependencies = [ [[package]] name = "zksync_protobuf_build" version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1205d607aa7291e3e016ce202d97cd7eb7d232913076dd873cbe48d564bf656" +source = "git+https://github.com/matter-labs/era-consensus.git?rev=78ade978b38480cab2b4335d9f41155e24d5c9a1#78ade978b38480cab2b4335d9f41155e24d5c9a1" dependencies = [ "anyhow", "heck 0.5.0", diff --git a/Cargo.toml b/Cargo.toml index 8b1be4471707..b77a046331d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -285,3 +285,15 @@ zksync_contract_verification_server = { path = "core/node/contract_verification_ zksync_node_api_server = { path = "core/node/api_server" } zksync_tee_verifier_input_producer = { path = "core/node/tee_verifier_input_producer" } zksync_base_token_adjuster = { path = "core/node/base_token_adjuster" } + +[patch.crates-io] +zksync_concurrency = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } +zksync_consensus_bft = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } +zksync_consensus_crypto = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } +zksync_consensus_executor = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } +zksync_consensus_network = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } +zksync_consensus_roles = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } +zksync_consensus_storage = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } +zksync_consensus_utils = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } +zksync_protobuf = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } +zksync_protobuf_build = { version = "0.1.0-rc.1", git = "https://github.com/matter-labs/era-consensus.git", rev = "78ade978b38480cab2b4335d9f41155e24d5c9a1" } diff --git a/core/lib/dal/.sqlx/query-5b780fdc69905a9dd9c9acc25f59154165381937952e3edd0d9d23a6d5a31e7a.json b/core/lib/dal/.sqlx/query-5b780fdc69905a9dd9c9acc25f59154165381937952e3edd0d9d23a6d5a31e7a.json new file mode 100644 index 000000000000..e700f4337a29 --- /dev/null +++ b/core/lib/dal/.sqlx/query-5b780fdc69905a9dd9c9acc25f59154165381937952e3edd0d9d23a6d5a31e7a.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n b.number\n FROM\n l1_batches b\n WHERE\n b.number >= $1\n AND NOT EXISTS (\n SELECT\n 1\n FROM\n l1_batches_consensus c\n WHERE\n c.l1_batch_number = b.number\n )\n ORDER BY\n b.number\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "number", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "5b780fdc69905a9dd9c9acc25f59154165381937952e3edd0d9d23a6d5a31e7a" +} diff --git a/core/lib/dal/src/consensus_dal.rs b/core/lib/dal/src/consensus_dal.rs index 3efdf5ee577b..f1beba6eed51 100644 --- a/core/lib/dal/src/consensus_dal.rs +++ b/core/lib/dal/src/consensus_dal.rs @@ -7,7 +7,7 @@ use zksync_db_connection::{ error::{DalError, DalResult, SqlxContext}, instrument::{InstrumentExt, Instrumented}, }; -use zksync_types::{L1BatchNumber, L2BlockNumber}; +use zksync_types::L2BlockNumber; pub use crate::consensus::Payload; use crate::{Core, CoreDal}; @@ -409,29 +409,12 @@ impl ConsensusDal<'_, '_> { /// /// Insertion is allowed even if it creates gaps in the L1 batch history. /// - /// It fails if the batch payload is missing or it's not consistent with the QC. + /// This method assumes that all payload validation has been carried out by the caller. pub async fn insert_batch_certificate( &mut self, cert: &attester::BatchQC, ) -> Result<(), InsertCertificateError> { - use InsertCertificateError as E; - let mut txn = self.storage.start_transaction().await?; - - let l1_batch_number = L1BatchNumber(cert.message.number.0 as u32); - let _l1_batch_header = txn - .blocks_dal() - .get_l1_batch_header(l1_batch_number) - .await? - .ok_or(E::MissingPayload)?; - - // TODO: Verify that the certificate matches the stored batch: - // * add the hash of the batch to the `BatchQC` - // * find out which field in the `l1_batches` table contains the hash we need to match - // * ideally move the responsibility of validation outside this method - - // if header.payload != want_payload.encode().hash() { - // return Err(E::PayloadMismatch); - // } + let l1_batch_number = cert.message.number.0 as i64; let res = sqlx::query!( r#" @@ -441,20 +424,18 @@ impl ConsensusDal<'_, '_> { ($1, $2, NOW(), NOW()) ON CONFLICT (l1_batch_number) DO NOTHING "#, - i64::from(l1_batch_number.0), + l1_batch_number, zksync_protobuf::serde::serialize(cert, serde_json::value::Serializer).unwrap(), ) .instrument("insert_batch_certificate") .report_latency() - .execute(&mut txn) + .execute(self.storage) .await?; if res.rows_affected().is_zero() { - tracing::debug!(%l1_batch_number, "duplicate batch certificate"); + tracing::debug!(l1_batch_number, "duplicate batch certificate"); } - txn.commit().await.context("commit")?; - Ok(()) } @@ -480,6 +461,47 @@ impl ConsensusDal<'_, '_> { .number .map(|number| attester::BatchNumber(number as u64))) } + + /// Get the numbers of L1 batches which do not have a corresponding L1 quorum certificate + /// and need signatures to be gossiped and collected. + /// + /// On the main node this means every L1 batch, because we need QC over all of them to be + /// able to submit them to L1. Replicas don't necessarily have to have the QC, because once + /// the batch is on L1 and it's final, they can get the batch from there and don't need the + /// attestations. + pub async fn unsigned_batch_numbers( + &mut self, + min_batch_number: attester::BatchNumber, + ) -> DalResult> { + Ok(sqlx::query!( + r#" + SELECT + b.number + FROM + l1_batches b + WHERE + b.number >= $1 + AND NOT EXISTS ( + SELECT + 1 + FROM + l1_batches_consensus c + WHERE + c.l1_batch_number = b.number + ) + ORDER BY + b.number + "#, + min_batch_number.0 as i64 + ) + .instrument("unsigned_batch_numbers") + .report_latency() + .fetch_all(self.storage) + .await? + .into_iter() + .map(|row| attester::BatchNumber(row.number as u64)) + .collect()) + } } #[cfg(test)] @@ -551,7 +573,8 @@ mod tests { // Insert some mock L2 blocks and L1 batches let mut block_number = 0; let mut batch_number = 0; - for _ in 0..3 { + let num_batches = 3; + for _ in 0..num_batches { for _ in 0..3 { block_number += 1; let l2_block = create_l2_block_header(block_number); @@ -612,5 +635,32 @@ mod tests { .insert_batch_certificate(&cert3) .await .expect_err("missing payload"); + + // Insert one more L1 batch without a certificate. + conn.blocks_dal() + .insert_mock_l1_batch(&create_l1_batch_header(batch_number + 1)) + .await + .unwrap(); + + let num_batches = num_batches + 1; + let num_unsigned = num_batches - 1; + + // Check how many unsigned L1 batches we can find. + for (i, (min_l1_batch_number, exp_num_unsigned)) in [ + (0, num_unsigned), + (batch_number, 1), // This one has the corresponding cert + (batch_number + 1, 1), // This is the one we inserted later + (batch_number + 2, 0), // Querying beyond the last one + ] + .into_iter() + .enumerate() + { + let unsigned = conn + .consensus_dal() + .unsigned_batch_numbers(attester::BatchNumber(u64::from(min_l1_batch_number))) + .await + .unwrap(); + assert_eq!(unsigned.len(), exp_num_unsigned, "test case {i}"); + } } } diff --git a/core/node/consensus/src/storage/connection.rs b/core/node/consensus/src/storage/connection.rs index 1d8dfc3aed57..2bb6a5c674fb 100644 --- a/core/node/consensus/src/storage/connection.rs +++ b/core/node/consensus/src/storage/connection.rs @@ -3,6 +3,7 @@ use zksync_concurrency::{ctx, error::Wrap as _, time}; use zksync_consensus_roles::{attester, validator}; use zksync_consensus_storage::{self as storage, BatchStoreState}; use zksync_dal::{consensus_dal::Payload, Core, CoreDal, DalError}; +use zksync_l1_contract_interface::i_executor::structures::StoredBatchInfo; use zksync_node_sync::{fetcher::IoCursorExt as _, ActionQueueSender, SyncState}; use zksync_state_keeper::io::common::IoCursor; use zksync_types::{commitment::L1BatchWithMetadata, L1BatchNumber}; @@ -120,6 +121,26 @@ impl<'a> Connection<'a> { ctx: &ctx::Ctx, cert: &attester::BatchQC, ) -> Result<(), InsertCertificateError> { + use crate::storage::consensus_dal::InsertCertificateError as E; + + let l1_batch_number = L1BatchNumber(cert.message.number.0 as u32); + + let Some(l1_batch) = self + .0 + .blocks_dal() + .get_l1_batch_metadata(l1_batch_number) + .await + .map_err(E::Dal)? + else { + return Err(E::MissingPayload.into()); + }; + + let l1_batch_info = StoredBatchInfo::from(&l1_batch); + + if l1_batch_info.hash().0 != *cert.message.hash.0.as_bytes() { + return Err(E::PayloadMismatch.into()); + } + Ok(ctx .wait(self.0.consensus_dal().insert_batch_certificate(cert)) .await??) @@ -344,8 +365,8 @@ impl<'a> Connection<'a> { // TODO: Fill out the proof when we have the stateless L1 batch validation story finished. // It is supposed to be a Merkle proof that the rolling hash of the batch has been included - // in the L1 state tree. The state root hash of L1 won't be available in the DB, it requires - // an API client. + // in the L1 system contract state tree. It is *not* the Ethereum state root hash, so producing + // it can be done without an L1 client, which is only required for validation. let batch = attester::SyncBatch { number, payloads, @@ -409,4 +430,20 @@ impl<'a> Connection<'a> { last, }) } + + /// Wrapper for `consensus_dal().unsigned_batch_numbers()`. + pub async fn unsigned_batch_numbers( + &mut self, + ctx: &ctx::Ctx, + min_batch_number: attester::BatchNumber, + ) -> ctx::Result> { + Ok(ctx + .wait( + self.0 + .consensus_dal() + .unsigned_batch_numbers(min_batch_number), + ) + .await? + .context("unsigned_batch_numbers()")?) + } } diff --git a/core/node/consensus/src/storage/store.rs b/core/node/consensus/src/storage/store.rs index c196989c300b..5721e543738b 100644 --- a/core/node/consensus/src/storage/store.rs +++ b/core/node/consensus/src/storage/store.rs @@ -3,11 +3,13 @@ use std::sync::Arc; use anyhow::Context as _; use zksync_concurrency::{ctx, error::Wrap as _, scope, sync, time}; use zksync_consensus_bft::PayloadManager; +use zksync_consensus_crypto::keccak256::Keccak256; use zksync_consensus_roles::{attester, validator}; use zksync_consensus_storage::{self as storage, BatchStoreState}; use zksync_dal::consensus_dal::{self, Payload}; +use zksync_l1_contract_interface::i_executor::structures::StoredBatchInfo; use zksync_node_sync::fetcher::{FetchedBlock, FetchedTransaction}; -use zksync_types::L2BlockNumber; +use zksync_types::{L1BatchNumber, L2BlockNumber}; use super::{Connection, PayloadQueue}; use crate::storage::{ConnectionPool, InsertCertificateError}; @@ -457,6 +459,28 @@ impl storage::PersistentBatchStore for Store { .1 } + /// Get the numbers of L1 batches which are missing the corresponding L1 batch quorum certificates + /// and potentially need to be signed by attesters. + async fn unsigned_batch_numbers( + &self, + ctx: &ctx::Ctx, + ) -> ctx::Result> { + // TODO: In the future external nodes will be able to ask the main node which L1 batch should be considered final. + // Later when we're fully decentralized the nodes will have to look at L1 instead. + // For now we make a best effort at gossiping votes, and have no way to tell what has been finalized, so we can + // just pick a reasonable maximum number of batches for which we might have to re-submit our signatures. + let Some(last_batch_number) = self.last_batch(ctx).await? else { + return Ok(Vec::new()); + }; + let min_batch_number = attester::BatchNumber(last_batch_number.0.saturating_sub(10)); + + self.conn(ctx) + .await? + .unsigned_batch_numbers(ctx, min_batch_number) + .await + .wrap("unsigned_batch_numbers") + } + /// Get the highest L1 batch number from storage. async fn last_batch(&self, ctx: &ctx::Ctx) -> ctx::Result> { self.conn(ctx) @@ -498,6 +522,36 @@ impl storage::PersistentBatchStore for Store { .wrap("get_batch") } + /// Returns the [attester::Batch] with the given number, which is the `message` that + /// appears in [attester::BatchQC], and represents the content that needs to be signed + /// by the attesters. + async fn get_batch_to_sign( + &self, + ctx: &ctx::Ctx, + number: attester::BatchNumber, + ) -> ctx::Result> { + let Some(batch) = self + .conn(ctx) + .await? + .batch( + ctx, + L1BatchNumber(u32::try_from(number.0).context("number")?), + ) + .await + .wrap("batch")? + else { + return Ok(None); + }; + + let info = StoredBatchInfo::from(&batch); + let hash = Keccak256::from_bytes(info.hash().0); + + Ok(Some(attester::Batch { + number, + hash: attester::BatchHash(hash), + })) + } + /// Returns the QC of the batch with the given number. async fn get_batch_qc( &self,