From 3f5b4f63a44c78e2386eb53abce12f54f9bdaa0e Mon Sep 17 00:00:00 2001 From: Nacho Avecilla Date: Tue, 21 May 2024 10:17:05 -0300 Subject: [PATCH] Broadcast validators signature and collect QC (BFT-414) (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ Implement a mechanism for validators to broadcast their signatures to other nodes in the network and collect them in a new certificate. ## Why ❔ This is essential for signing L1 batches and sending them to L1 for verification. Validators need to broadcast their signatures and gather them in a certificate in case the majority signs the new batch. --------- Co-authored-by: Grzegorz Prusak Co-authored-by: Ignacio Avecilla Co-authored-by: Bruno França --- .github/workflows/protobuf.yaml | 15 +- node/Cargo.toml | 4 +- node/actors/bft/src/leader/replica_commit.rs | 6 +- node/actors/bft/src/leader/replica_prepare.rs | 6 +- node/actors/bft/src/leader/tests.rs | 2 +- node/actors/bft/src/testonly/ut_harness.rs | 14 +- node/actors/executor/src/lib.rs | 13 +- node/actors/network/src/config.rs | 6 + node/actors/network/src/consensus/mod.rs | 2 +- node/actors/network/src/consensus/tests.rs | 20 +- node/actors/network/src/gossip/batch_votes.rs | 102 +++++++ node/actors/network/src/gossip/mod.rs | 84 +++++- node/actors/network/src/gossip/runner.rs | 54 +++- node/actors/network/src/gossip/testonly.rs | 1 - node/actors/network/src/gossip/tests/mod.rs | 122 ++++++++- .../network/src/gossip/tests/syncing.rs | 6 +- node/actors/network/src/lib.rs | 16 +- node/actors/network/src/proto/gossip.proto | 6 + node/actors/network/src/rpc/mod.rs | 1 + .../network/src/rpc/push_batch_votes.rs | 42 +++ node/actors/network/src/rpc/tests.rs | 1 + node/actors/network/src/testonly.rs | 8 +- node/libs/crypto/src/bls12_381/mod.rs | 6 + node/libs/crypto/src/bn254/mod.rs | 11 +- node/libs/crypto/src/ed25519/mod.rs | 6 + node/libs/roles/src/attester/conv.rs | 164 +++++++++++ .../src/attester/keys/aggregate_signature.rs | 72 +++++ node/libs/roles/src/attester/keys/mod.rs | 11 + .../roles/src/attester/keys/public_key.rs | 33 +++ .../roles/src/attester/keys/secret_key.rs | 74 +++++ .../libs/roles/src/attester/keys/signature.rs | 50 ++++ .../libs/roles/src/attester/messages/batch.rs | 142 ++++++++++ node/libs/roles/src/attester/messages/mod.rs | 6 + node/libs/roles/src/attester/messages/msg.rs | 259 ++++++++++++++++++ node/libs/roles/src/attester/mod.rs | 12 + node/libs/roles/src/attester/testonly.rs | 99 +++++++ node/libs/roles/src/attester/tests.rs | 241 ++++++++++++++++ node/libs/roles/src/lib.rs | 3 + node/libs/roles/src/node/keys.rs | 2 +- node/libs/roles/src/node/tests.rs | 12 +- node/libs/roles/src/proto/attester.proto | 49 ++++ node/libs/roles/src/proto/validator.proto | 2 + node/libs/roles/src/validator/conv.rs | 28 +- .../roles/src/validator/keys/signature.rs | 8 +- .../roles/src/validator/messages/consensus.rs | 24 +- .../src/validator/messages/leader_commit.rs | 12 +- .../src/validator/messages/leader_prepare.rs | 16 +- .../roles/src/validator/messages/tests.rs | 101 +++++-- node/libs/roles/src/validator/testonly.rs | 54 +++- node/libs/roles/src/validator/tests.rs | 95 ++++--- node/libs/storage/src/batch_store.rs | 16 ++ node/libs/storage/src/lib.rs | 2 + node/tools/src/bin/deployer.rs | 2 +- node/tools/src/bin/localnet_config.rs | 2 +- node/tools/src/config.rs | 1 - node/tools/src/tests.rs | 4 +- 56 files changed, 1970 insertions(+), 180 deletions(-) create mode 100644 node/actors/network/src/gossip/batch_votes.rs create mode 100644 node/actors/network/src/rpc/push_batch_votes.rs create mode 100644 node/libs/roles/src/attester/conv.rs create mode 100644 node/libs/roles/src/attester/keys/aggregate_signature.rs create mode 100644 node/libs/roles/src/attester/keys/mod.rs create mode 100644 node/libs/roles/src/attester/keys/public_key.rs create mode 100644 node/libs/roles/src/attester/keys/secret_key.rs create mode 100644 node/libs/roles/src/attester/keys/signature.rs create mode 100644 node/libs/roles/src/attester/messages/batch.rs create mode 100644 node/libs/roles/src/attester/messages/mod.rs create mode 100644 node/libs/roles/src/attester/messages/msg.rs create mode 100644 node/libs/roles/src/attester/mod.rs create mode 100644 node/libs/roles/src/attester/testonly.rs create mode 100644 node/libs/roles/src/attester/tests.rs create mode 100644 node/libs/roles/src/proto/attester.proto create mode 100644 node/libs/storage/src/batch_store.rs diff --git a/.github/workflows/protobuf.yaml b/.github/workflows/protobuf.yaml index 2ce0e6f0..128764b4 100644 --- a/.github/workflows/protobuf.yaml +++ b/.github/workflows/protobuf.yaml @@ -2,7 +2,7 @@ name: protobuf_compatibility on: pull_request: - branches: [ "*" ] + branches: ["*"] push: # protobuf compatibility is a transitive property, # but it requires all the transitions to be checked. @@ -11,7 +11,7 @@ on: # (unless we improve our github setup). # Therefore on post-merge we will execute the # compatibility check as well (TODO: alerting). - branches: [ "main" ] + branches: ["main"] permissions: id-token: write @@ -33,8 +33,8 @@ jobs: compatibility: runs-on: [ubuntu-22.04-github-hosted-16core] steps: - - uses: mozilla-actions/sccache-action@v0.0.3 - + - uses: mozilla-actions/sccache-action@v0.0.3 + # before - uses: actions/checkout@v4 with: @@ -42,8 +42,7 @@ jobs: path: before fetch-depth: 0 # fetches all branches and tags, which is needed to compute the LCA. - name: checkout LCA - run: - git checkout $(git merge-base $BASE $HEAD) + run: git checkout $(git merge-base $BASE $HEAD) working-directory: ./before - name: compile before run: cargo build --all-targets @@ -53,7 +52,7 @@ jobs: perl -ne 'print "$1\n" if /PROTOBUF_DESCRIPTOR="(.*)"/' `find ./before/node/target/debug/build/*/output` | xargs cat > ./before.binpb - + # after - uses: actions/checkout@v4 with: @@ -67,7 +66,7 @@ jobs: perl -ne 'print "$1\n" if /PROTOBUF_DESCRIPTOR="(.*)"/' `find ./after/node/target/debug/build/*/output` | xargs cat > ./after.binpb - + # compare - uses: bufbuild/buf-setup-action@v1 with: diff --git a/node/Cargo.toml b/node/Cargo.toml index 67d9bf2e..a931d402 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -9,7 +9,7 @@ members = [ "libs/protobuf_build", "libs/roles", "libs/storage", - "libs/utils", + "libs/utils", "tests", "tools", ] @@ -55,7 +55,7 @@ ff_ce = "0.14.3" heck = "0.5.0" hex = "0.4.3" im = "15.1.0" -jsonrpsee = { version = "0.21.0", features = ["server", "http-client"] } +jsonrpsee = { version = "0.21.0", features = ["server", "http-client"] } k8s-openapi = { version = "0.21.0", features = ["latest"] } kube = { version = "0.88.1", features = ["runtime", "derive"] } num-bigint = "0.4.4" diff --git a/node/actors/bft/src/leader/replica_commit.rs b/node/actors/bft/src/leader/replica_commit.rs index 959215ab..e44d7a5d 100644 --- a/node/actors/bft/src/leader/replica_commit.rs +++ b/node/actors/bft/src/leader/replica_commit.rs @@ -51,7 +51,7 @@ impl StateMachine { let author = &signed_message.key; // Check that the message signer is in the validator committee. - if !self.config.genesis().committee.contains(author) { + if !self.config.genesis().validators.contains(author) { return Err(Error::NonValidatorSigner { signer: author.clone(), }); @@ -103,7 +103,7 @@ impl StateMachine { .expect("Could not add message to CommitQC"); // Calculate the CommitQC signers weight. - let weight = self.config.genesis().committee.weight(&commit_qc.signers); + let weight = self.config.genesis().validators.weight(&commit_qc.signers); // Update commit message current view number for author self.replica_commit_views @@ -118,7 +118,7 @@ impl StateMachine { .retain(|view_number, _| active_views.contains(view_number)); // Now we check if we have enough weight to continue. - if weight < self.config.genesis().committee.threshold() { + if weight < self.config.genesis().validators.threshold() { return Ok(()); }; diff --git a/node/actors/bft/src/leader/replica_prepare.rs b/node/actors/bft/src/leader/replica_prepare.rs index bd0a414b..a9ab65a5 100644 --- a/node/actors/bft/src/leader/replica_prepare.rs +++ b/node/actors/bft/src/leader/replica_prepare.rs @@ -63,7 +63,7 @@ impl StateMachine { let author = &signed_message.key; // Check that the message signer is in the validator set. - if !self.config.genesis().committee.contains(author) { + if !self.config.genesis().validators.contains(author) { return Err(Error::NonValidatorSigner { signer: author.clone(), }); @@ -114,7 +114,7 @@ impl StateMachine { .expect("Could not add message to PrepareQC"); // Calculate the PrepareQC signers weight. - let weight = prepare_qc.weight(&self.config.genesis().committee); + let weight = prepare_qc.weight(&self.config.genesis().validators); // Update prepare message current view number for author self.replica_prepare_views @@ -129,7 +129,7 @@ impl StateMachine { .retain(|view_number, _| active_views.contains(view_number)); // Now we check if we have enough weight to continue. - if weight < self.config.genesis().committee.threshold() { + if weight < self.config.genesis().validators.threshold() { return Ok(()); } diff --git a/node/actors/bft/src/leader/tests.rs b/node/actors/bft/src/leader/tests.rs index 3bb6de2c..9d22885b 100644 --- a/node/actors/bft/src/leader/tests.rs +++ b/node/actors/bft/src/leader/tests.rs @@ -374,7 +374,7 @@ async fn replica_prepare_different_messages() { let mut replica_commit_result = None; // The rest of the validators until threshold sign other_replica_prepare - for i in validators / 2..util.genesis().committee.threshold() as usize { + for i in validators / 2..util.genesis().validators.threshold() as usize { replica_commit_result = util .process_replica_prepare(ctx, util.keys[i].sign_msg(other_replica_prepare.clone())) .await diff --git a/node/actors/bft/src/testonly/ut_harness.rs b/node/actors/bft/src/testonly/ut_harness.rs index d8793413..cc2cbb49 100644 --- a/node/actors/bft/src/testonly/ut_harness.rs +++ b/node/actors/bft/src/testonly/ut_harness.rs @@ -61,7 +61,7 @@ impl UTHarness { let (send, recv) = ctx::channel::unbounded(); let cfg = Arc::new(Config { - secret_key: setup.keys[0].clone(), + secret_key: setup.validator_keys[0].clone(), block_store: block_store.clone(), replica_store: Box::new(in_memory::ReplicaStore::default()), payload_manager, @@ -75,7 +75,7 @@ impl UTHarness { leader, replica, pipe: recv, - keys: setup.keys.clone(), + keys: setup.validator_keys.clone(), leader_send, }; let _: Signed = this.try_recv().unwrap(); @@ -86,7 +86,7 @@ impl UTHarness { pub(crate) async fn new_many(ctx: &ctx::Ctx) -> (UTHarness, BlockStoreRunner) { let num_validators = 6; let (util, runner) = UTHarness::new(ctx, num_validators).await; - assert!(util.genesis().committee.max_faulty_weight() > 0); + assert!(util.genesis().validators.max_faulty_weight() > 0); (util, runner) } @@ -223,8 +223,8 @@ impl UTHarness { for (i, msg) in msgs.into_iter().enumerate() { let res = self.process_replica_prepare(ctx, msg).await; match ( - (i + 1) as u64 * self.genesis().committee.iter().next().unwrap().weight - < self.genesis().committee.threshold(), + (i + 1) as u64 * self.genesis().validators.iter().next().unwrap().weight + < self.genesis().validators.threshold(), first_match, ) { (true, _) => assert!(res.unwrap().is_none()), @@ -258,8 +258,8 @@ impl UTHarness { .leader .process_replica_commit(ctx, key.sign_msg(msg.clone())); match ( - (i + 1) as u64 * self.genesis().committee.iter().next().unwrap().weight - < self.genesis().committee.threshold(), + (i + 1) as u64 * self.genesis().validators.iter().next().unwrap().weight + < self.genesis().validators.threshold(), first_match, ) { (true, _) => res.unwrap(), diff --git a/node/actors/executor/src/lib.rs b/node/actors/executor/src/lib.rs index b5abe2f7..c346cf96 100644 --- a/node/actors/executor/src/lib.rs +++ b/node/actors/executor/src/lib.rs @@ -3,7 +3,6 @@ use crate::io::Dispatcher; use anyhow::Context as _; use std::{ collections::{HashMap, HashSet}, - fmt, sync::Arc, }; use zksync_concurrency::{ctx, limiter, net, scope, time}; @@ -19,6 +18,7 @@ mod io; mod tests; /// Validator-related part of [`Executor`]. +#[derive(Debug)] pub struct Validator { /// Consensus network configuration. pub key: validator::SecretKey, @@ -28,14 +28,6 @@ pub struct Validator { pub payload_manager: Box, } -impl fmt::Debug for Validator { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_struct("ValidatorExecutor") - .field("key", &self.key) - .finish() - } -} - /// Config of the node executor. #[derive(Clone, Debug)] pub struct Config { @@ -47,7 +39,6 @@ pub struct Config { pub public_addr: net::Host, /// Maximal size of the block payload. pub max_payload_size: usize, - /// Key of this node. It uniquely identifies the node. /// It should match the secret key provided in the `node_key` file. pub node_key: node::SecretKey, @@ -129,7 +120,7 @@ impl Executor { if !self .block_store .genesis() - .committee + .validators .contains(&validator.key.public()) { tracing::warn!( diff --git a/node/actors/network/src/config.rs b/node/actors/network/src/config.rs index e757a8fc..c7bf8a3e 100644 --- a/node/actors/network/src/config.rs +++ b/node/actors/network/src/config.rs @@ -20,6 +20,8 @@ pub struct RpcConfig { pub get_block_timeout: Option, /// Max rate of sending/receiving consensus messages. pub consensus_rate: limiter::Rate, + /// Max rate of sending/receiving l1 batch votes messages. + pub push_batch_votes_rate: limiter::Rate, } impl Default for RpcConfig { @@ -42,6 +44,10 @@ impl Default for RpcConfig { burst: 10, refresh: time::Duration::ZERO, }, + push_batch_votes_rate: limiter::Rate { + burst: 2, + refresh: time::Duration::milliseconds(500), + }, } } } diff --git a/node/actors/network/src/consensus/mod.rs b/node/actors/network/src/consensus/mod.rs index 0e20a609..f3ded417 100644 --- a/node/actors/network/src/consensus/mod.rs +++ b/node/actors/network/src/consensus/mod.rs @@ -151,7 +151,7 @@ impl Network { /// Constructs a new consensus network state. pub(crate) fn new(gossip: Arc) -> Option> { let key = gossip.cfg.validator_key.clone()?; - let validators: HashSet<_> = gossip.genesis().committee.keys().cloned().collect(); + let validators: HashSet<_> = gossip.genesis().validators.keys().cloned().collect(); Some(Arc::new(Self { key, inbound: PoolWatch::new(validators.clone(), 0), diff --git a/node/actors/network/src/consensus/tests.rs b/node/actors/network/src/consensus/tests.rs index 6883ee9b..4c87be33 100644 --- a/node/actors/network/src/consensus/tests.rs +++ b/node/actors/network/src/consensus/tests.rs @@ -166,12 +166,14 @@ async fn test_genesis_mismatch() { .gossip .validator_addrs .update( - &setup.genesis.committee, - &[Arc::new(setup.keys[1].sign_msg(validator::NetAddress { - addr: *cfgs[1].server_addr, - version: 0, - timestamp: ctx.now_utc(), - }))], + &setup.genesis.validators, + &[Arc::new(setup.validator_keys[1].sign_msg( + validator::NetAddress { + addr: *cfgs[1].server_addr, + version: 0, + timestamp: ctx.now_utc(), + }, + ))], ) .await .unwrap(); @@ -185,7 +187,7 @@ async fn test_genesis_mismatch() { .context("preface::accept()")?; assert_eq!(endpoint, preface::Endpoint::ConsensusNet); tracing::info!("Expect the handshake to fail"); - let res = handshake::inbound(ctx, &setup.keys[1], rng.gen(), &mut stream).await; + let res = handshake::inbound(ctx, &setup.validator_keys[1], rng.gen(), &mut stream).await; assert_matches!(res, Err(handshake::Error::GenesisMismatch)); tracing::info!("Try to connect to a node with a mismatching genesis."); @@ -195,10 +197,10 @@ async fn test_genesis_mismatch() { .context("preface::connect")?; let res = handshake::outbound( ctx, - &setup.keys[1], + &setup.validator_keys[1], rng.gen(), &mut stream, - &setup.keys[0].public(), + &setup.validator_keys[0].public(), ) .await; tracing::info!( diff --git a/node/actors/network/src/gossip/batch_votes.rs b/node/actors/network/src/gossip/batch_votes.rs new file mode 100644 index 00000000..7fb0100f --- /dev/null +++ b/node/actors/network/src/gossip/batch_votes.rs @@ -0,0 +1,102 @@ +//! Global state distributed by active attesters, observed by all the nodes in the network. +use crate::watch::Watch; +use std::{collections::HashSet, sync::Arc}; +use zksync_concurrency::sync; +use zksync_consensus_roles::attester::{self, Batch}; + +/// Mapping from attester::PublicKey to a signed attester::Batch message. +/// Represents the currents state of node's knowledge about the attester votes. +#[derive(Clone, Default, PartialEq, Eq)] +pub(crate) struct BatchVotes( + pub(super) im::HashMap>>, +); + +impl BatchVotes { + /// Returns a set of entries of `self` which are newer than the entries in `b`. + pub(super) fn get_newer(&self, b: &Self) -> Vec>> { + let mut newer = vec![]; + for (k, v) in &self.0 { + if let Some(bv) = b.0.get(k) { + if v.msg <= bv.msg { + continue; + } + } + newer.push(v.clone()); + } + newer + } + + /// Updates the discovery map with entries from `data`. + /// It exits as soon as an invalid entry is found. + /// `self` might get modified even if an error is returned + /// (all entries verified so far are added). + /// Returns true iff some new entry was added. + pub(super) fn update( + &mut self, + attesters: &attester::Committee, + data: &[Arc>], + ) -> anyhow::Result { + let mut changed = false; + + let mut done = HashSet::new(); + for d in data { + // Disallow multiple entries for the same key: + // it is important because a malicious attester may spam us with + // new versions and verifying signatures is expensive. + if done.contains(&d.key) { + anyhow::bail!("duplicate entry for {:?}", d.key); + } + done.insert(d.key.clone()); + if !attesters.contains(&d.key) { + // We just skip the entries we are not interested in. + // For now the set of attesters is static, so we could treat this as an error, + // however we eventually want the attester set to be dynamic. + continue; + } + if let Some(x) = self.0.get(&d.key) { + if d.msg <= x.msg { + continue; + } + } + d.verify()?; + self.0.insert(d.key.clone(), d.clone()); + changed = true; + } + Ok(changed) + } +} + +/// Watch wrapper of BatchVotes, +/// which supports subscribing to BatchVotes updates. +pub(crate) struct BatchVotesWatch(Watch); + +impl Default for BatchVotesWatch { + fn default() -> Self { + Self(Watch::new(BatchVotes::default())) + } +} + +impl BatchVotesWatch { + /// Subscribes to BatchVotes updates. + pub(crate) fn subscribe(&self) -> sync::watch::Receiver { + self.0.subscribe() + } + + /// Inserts data to BatchVotes. + /// Subscribers are notified iff at least 1 new entry has + /// been inserted. Returns an error iff an invalid + /// entry in `data` has been found. The provider of the + /// invalid entry should be banned. + pub(crate) async fn update( + &self, + attesters: &attester::Committee, + data: &[Arc>], + ) -> anyhow::Result<()> { + let this = self.0.lock().await; + let mut votes = this.borrow().clone(); + if votes.update(attesters, data)? { + this.send_replace(votes); + } + Ok(()) + } +} diff --git a/node/actors/network/src/gossip/mod.rs b/node/actors/network/src/gossip/mod.rs index e8bf588c..7242f4a0 100644 --- a/node/actors/network/src/gossip/mod.rs +++ b/node/actors/network/src/gossip/mod.rs @@ -13,12 +13,17 @@ //! eclipse attack. Dynamic connections are supposed to improve the properties of the gossip //! network graph (minimize its diameter, increase connectedness). use crate::{gossip::ValidatorAddrsWatch, io, pool::PoolWatch, Config}; +use anyhow::Context as _; +use im::HashMap; use std::sync::{atomic::AtomicUsize, Arc}; pub(crate) use validator_addrs::*; use zksync_concurrency::{ctx, ctx::channel, scope, sync}; -use zksync_consensus_roles::{node, validator}; +use zksync_consensus_roles::{attester, node, validator}; use zksync_consensus_storage::BlockStore; +use self::batch_votes::BatchVotesWatch; + +mod batch_votes; mod fetch; mod handshake; mod runner; @@ -38,12 +43,18 @@ pub(crate) struct Network { pub(crate) outbound: PoolWatch, /// Current state of knowledge about validators' endpoints. pub(crate) validator_addrs: ValidatorAddrsWatch, + /// Current state of knowledge about batch votes. + pub(crate) batch_votes: BatchVotesWatch, /// Block store to serve `get_block` requests from. pub(crate) block_store: Arc, /// Output pipe of the network actor. pub(crate) sender: channel::UnboundedSender, /// Queue of block fetching requests. pub(crate) fetch_queue: fetch::Queue, + /// Last viewed QC. + pub(crate) last_viewed_qc: Option, + /// L1 batch qc. + pub(crate) batch_qc: HashMap, /// TESTONLY: how many time push_validator_addrs rpc was called by the peers. pub(crate) push_validator_addrs_calls: AtomicUsize, } @@ -63,6 +74,9 @@ impl Network { ), outbound: PoolWatch::new(cfg.gossip.static_outbound.keys().cloned().collect(), 0), validator_addrs: ValidatorAddrsWatch::default(), + batch_votes: BatchVotesWatch::default(), + batch_qc: HashMap::new(), + last_viewed_qc: None, cfg, fetch_queue: fetch::Queue::default(), block_store, @@ -103,4 +117,72 @@ impl Network { }) .await; } + + /// Task that keeps hearing about new votes and updates the L1 batch qc. + /// It will propagate the QC if there's enough votes. + pub(crate) async fn update_batch_qc(&self, ctx: &ctx::Ctx) -> anyhow::Result<()> { + // TODO This is not a good way to do this, we shouldn't be verifying the QC every time + // Can we get only the latest votes? + let attesters = self.genesis().attesters.as_ref().context("attesters")?; + loop { + let mut sub = self.batch_votes.subscribe(); + let votes = sync::changed(ctx, &mut sub) + .await + .context("batch votes")? + .clone(); + + // Check next QC to collect votes for. + let new_qc = self + .last_viewed_qc + .clone() + .map(|qc| { + attester::BatchQC::new( + attester::Batch { + number: qc.message.number.next(), + }, + self.genesis(), + ) + }) + .unwrap_or_else(|| { + attester::BatchQC::new( + attester::Batch { + number: attester::BatchNumber(0), + }, + self.genesis(), + ) + }) + .context("new qc")?; + + // Check votes for the correct QC. + for (_, sig) in votes.0 { + if self + .batch_qc + .clone() + .entry(new_qc.message.number.clone()) + .or_insert_with(|| { + attester::BatchQC::new(new_qc.message.clone(), self.genesis()).expect("qc") + }) + .add(&sig, self.genesis()) + .is_err() + { + // TODO: Should we ban the peer somehow? + continue; + } + } + + let weight = attesters.weight( + &self + .batch_qc + .get(&new_qc.message.number) + .context("last qc")? + .signers, + ); + + if weight < attesters.threshold() { + return Ok(()); + }; + + // If we have enough weight, we can update the last viewed QC and propagate it. + } + } } diff --git a/node/actors/network/src/gossip/runner.rs b/node/actors/network/src/gossip/runner.rs index b16e8452..af727905 100644 --- a/node/actors/network/src/gossip/runner.rs +++ b/node/actors/network/src/gossip/runner.rs @@ -1,4 +1,4 @@ -use super::{handshake, Network, ValidatorAddrs}; +use super::{batch_votes::BatchVotes, handshake, Network, ValidatorAddrs}; use crate::{noise, preface, rpc}; use anyhow::Context as _; use async_trait::async_trait; @@ -26,7 +26,28 @@ impl rpc::Handler for PushValidatorAddrsServer<' .fetch_add(1, Ordering::SeqCst); self.0 .validator_addrs - .update(&self.0.genesis().committee, &req.0[..]) + .update(&self.0.genesis().validators, &req.0) + .await?; + Ok(()) + } +} + +struct PushBatchVotesServer<'a>(&'a Network); + +#[async_trait::async_trait] +impl rpc::Handler for PushBatchVotesServer<'_> { + /// Here we bound the buffering of incoming batch messages. + fn max_req_size(&self) -> usize { + 100 * kB + } + + async fn handle(&self, _ctx: &ctx::Ctx, req: rpc::push_batch_votes::Req) -> anyhow::Result<()> { + self.0 + .batch_votes + .update( + self.0.genesis().attesters.as_ref().context("attesters")?, + &req.0, + ) .await?; Ok(()) } @@ -112,6 +133,35 @@ impl Network { .add_client(&get_block_client) .add_server(ctx, &*self.block_store, self.cfg.rpc.get_block_rate) .add_server(ctx, rpc::ping::Server, rpc::ping::RATE); + if self.genesis().attesters.as_ref().is_some() { + let push_signature_client = rpc::Client::::new( + ctx, + self.cfg.rpc.push_batch_votes_rate, + ); + let push_signature_server = PushBatchVotesServer(self); + service = service.add_client(&push_signature_client).add_server( + ctx, + push_signature_server, + self.cfg.rpc.push_batch_votes_rate, + ); + // Push L1 batch votes updates to peer. + s.spawn::<()>(async { + let push_signature_client = push_signature_client; + let mut old = BatchVotes::default(); + let mut sub = self.batch_votes.subscribe(); + sub.mark_changed(); + loop { + let new = sync::changed(ctx, &mut sub).await?.clone(); + let diff = new.get_newer(&old); + if diff.is_empty() { + continue; + } + old = new; + let req = rpc::push_batch_votes::Req(diff); + push_signature_client.call(ctx, &req, kB).await?; + } + }); + } if let Some(ping_timeout) = &self.cfg.ping_timeout { let ping_client = rpc::Client::::new(ctx, rpc::ping::RATE); diff --git a/node/actors/network/src/gossip/testonly.rs b/node/actors/network/src/gossip/testonly.rs index fcf8bee4..18e544e0 100644 --- a/node/actors/network/src/gossip/testonly.rs +++ b/node/actors/network/src/gossip/testonly.rs @@ -1,7 +1,6 @@ #![allow(dead_code)] use super::*; use crate::{frame, mux, noise, preface, rpc, Config, GossipConfig}; -use anyhow::Context as _; use rand::Rng as _; use std::collections::BTreeMap; use zksync_concurrency::{ctx, limiter}; diff --git a/node/actors/network/src/gossip/tests/mod.rs b/node/actors/network/src/gossip/tests/mod.rs index b2d20553..b16eefee 100644 --- a/node/actors/network/src/gossip/tests/mod.rs +++ b/node/actors/network/src/gossip/tests/mod.rs @@ -1,6 +1,5 @@ use super::*; use crate::{metrics, preface, rpc, testonly}; -use anyhow::Context as _; use assert_matches::assert_matches; use pretty_assertions::assert_eq; use rand::Rng; @@ -14,7 +13,7 @@ use zksync_concurrency::{ testonly::{abort_on_panic, set_timeout}, time, }; -use zksync_consensus_roles::validator; +use zksync_consensus_roles::{attester, validator}; use zksync_consensus_storage::testonly::new_store; mod fetch; @@ -86,6 +85,9 @@ fn mk_version(rng: &mut R) -> u64 { #[derive(Default)] struct View(im::HashMap>>); +#[derive(Default)] +struct Signatures(im::HashMap>>); + fn mk_netaddr( key: &validator::SecretKey, addr: std::net::SocketAddr, @@ -99,6 +101,13 @@ fn mk_netaddr( }) } +fn mk_batch( + key: &attester::SecretKey, + number: attester::BatchNumber, +) -> attester::Signed { + key.sign_msg(attester::Batch { number }) +} + fn random_netaddr( rng: &mut R, key: &validator::SecretKey, @@ -111,6 +120,16 @@ fn random_netaddr( )) } +fn random_batch_votes( + rng: &mut R, + key: &attester::SecretKey, +) -> Arc> { + let batch = attester::Batch { + number: attester::BatchNumber(rng.gen_range(0..1000)), + }; + Arc::new(key.sign_msg(batch.to_owned())) +} + fn update_netaddr( rng: &mut R, addr: &validator::NetAddress, @@ -126,6 +145,18 @@ fn update_netaddr( )) } +fn update_signature( + _rng: &mut R, + batch: &attester::Batch, + key: &attester::SecretKey, + batch_number_diff: i64, +) -> Arc> { + let batch = attester::Batch { + number: attester::BatchNumber((batch.number.0 as i64 + batch_number_diff) as u64), + }; + Arc::new(key.sign_msg(batch.to_owned())) +} + impl View { fn insert(&mut self, entry: Arc>) { self.0.insert(entry.key.clone(), entry); @@ -140,6 +171,20 @@ impl View { } } +impl Signatures { + fn insert(&mut self, entry: Arc>) { + self.0.insert(entry.key.clone(), entry); + } + + fn get(&mut self, key: &attester::SecretKey) -> Arc> { + self.0.get(&key.public()).unwrap().clone() + } + + fn as_vec(&self) -> Vec>> { + self.0.values().cloned().collect() + } +} + #[tokio::test] async fn test_validator_addrs() { abort_on_panic(); @@ -375,7 +420,9 @@ async fn validator_node_restart() { let sub = &mut node1.net.gossip.validator_addrs.subscribe(); let want = Some(*cfgs[0].server_addr); sync::wait_for(ctx, sub, |got| { - got.get(&setup.keys[0].public()).map(|x| x.msg.addr) == want + got.get(&setup.validator_keys[0].public()) + .map(|x| x.msg.addr) + == want }) .await?; Ok(()) @@ -463,3 +510,72 @@ async fn rate_limiting() { assert!((1..=2).contains(&got), "got {got} want 1 or 2"); } } + +#[tokio::test] +async fn test_batch_votes() { + abort_on_panic(); + let rng = &mut ctx::test_root(&ctx::RealClock).rng(); + + let keys: Vec = (0..8).map(|_| rng.gen()).collect(); + let attesters = attester::Committee::new(keys.iter().map(|k| attester::WeightedAttester { + key: k.public(), + weight: 1250, + })) + .unwrap(); + let votes = BatchVotesWatch::default(); + let mut sub = votes.subscribe(); + + // Initial values. + let mut want = Signatures::default(); + for k in &keys[0..6] { + want.insert(random_batch_votes(rng, k)); + } + votes.update(&attesters, &want.as_vec()).await.unwrap(); + assert_eq!(want.0, sub.borrow_and_update().0); + + // newer batch number + let k0v2 = update_signature(rng, &want.get(&keys[0]).msg, &keys[0], 1); + // same batch number + let k1v2 = update_signature(rng, &want.get(&keys[1]).msg, &keys[1], 0); + // older batch number + let k4v2 = update_signature(rng, &want.get(&keys[4]).msg, &keys[4], -1); + // first entry for a key in the config + let k6v1 = random_batch_votes(rng, &keys[6]); + // entry for a key outside of the config + let k8 = rng.gen(); + let k8v1 = random_batch_votes(rng, &k8); + + want.insert(k0v2.clone()); + want.insert(k1v2.clone()); + want.insert(k6v1.clone()); + let update = [ + k0v2, + k1v2, + k4v2, + // no new entry for keys[5] + k6v1, + // no entry at all for keys[7] + k8v1.clone(), + ]; + votes.update(&attesters, &update).await.unwrap(); + assert_eq!(want.0, sub.borrow_and_update().0); + + // Invalid signature. + let mut k0v3 = mk_batch(&keys[1], attester::BatchNumber(rng.gen_range(0..1000))); + k0v3.sig = rng.gen(); + assert!(votes.update(&attesters, &[Arc::new(k0v3)]).await.is_err()); + assert_eq!(want.0, sub.borrow_and_update().0); + + // Duplicate entry in the update. + assert!(votes + .update(&attesters, &[k8v1.clone(), k8v1]) + .await + .is_err()); + assert_eq!(want.0, sub.borrow_and_update().0); +} + +// TODO: This test is disabled because the logic for attesters to receive and sign batches is not implemented yet. +// It should be re-enabled once the logic is implemented. +// #[tokio::test(flavor = "multi_thread")] +// async fn test_batch_votes_propagation() { +// } diff --git a/node/actors/network/src/gossip/tests/syncing.rs b/node/actors/network/src/gossip/tests/syncing.rs index 44922ada..a9455781 100644 --- a/node/actors/network/src/gossip/tests/syncing.rs +++ b/node/actors/network/src/gossip/tests/syncing.rs @@ -136,7 +136,7 @@ async fn test_switching_on_nodes() { // It is important that all nodes will connect to each other, // because we spawn the nodes gradually and we want the network // to be connected at all times. - let cfgs = testonly::new_configs(rng, &setup, setup.keys.len()); + let cfgs = testonly::new_configs(rng, &setup, setup.validator_keys.len()); setup.push_blocks(rng, cfgs.len()); scope::run!(ctx, |ctx, s| async { let mut nodes = vec![]; @@ -191,7 +191,7 @@ async fn test_switching_off_nodes() { // It is important that all nodes will connect to each other, // because we spawn the nodes gradually and we want the network // to be connected at all times. - let cfgs = testonly::new_configs(rng, &setup, setup.keys.len()); + let cfgs = testonly::new_configs(rng, &setup, setup.validator_keys.len()); setup.push_blocks(rng, cfgs.len()); scope::run!(ctx, |ctx, s| async { let mut nodes = vec![]; @@ -253,7 +253,7 @@ async fn test_different_first_block() { // It is important that all nodes will connect to each other, // because we spawn the nodes gradually and we want the network // to be connected at all times. - let cfgs = testonly::new_configs(rng, &setup, setup.keys.len()); + let cfgs = testonly::new_configs(rng, &setup, setup.validator_keys.len()); scope::run!(ctx, |ctx, s| async { let mut nodes = vec![]; for (i, mut cfg) in cfgs.into_iter().enumerate() { diff --git a/node/actors/network/src/lib.rs b/node/actors/network/src/lib.rs index 0f6e3e69..e7fc7e59 100644 --- a/node/actors/network/src/lib.rs +++ b/node/actors/network/src/lib.rs @@ -2,7 +2,10 @@ use anyhow::Context as _; use std::sync::Arc; use tracing::Instrument as _; -use zksync_concurrency::{ctx, ctx::channel, limiter, scope}; +use zksync_concurrency::{ + ctx::{self, channel}, + limiter, scope, +}; use zksync_consensus_storage::BlockStore; use zksync_consensus_utils::pipe::ActorPipe; @@ -119,6 +122,13 @@ impl Runner { Ok(()) }); + // Update QC batches in the background. + s.spawn(async { + // TODO: Handle this correctly. + let _ = self.net.gossip.update_batch_qc(ctx).await; + Ok(()) + }); + // Maintain static gossip connections. for (peer, addr) in &self.net.gossip.cfg.gossip.static_outbound { s.spawn::<()>(async { @@ -139,7 +149,7 @@ impl Runner { } if let Some(c) = &self.net.consensus { - let validators = &c.gossip.genesis().committee; + let validators = &c.gossip.genesis().validators; // If we are active validator ... if validators.contains(&c.key.public()) { // Maintain outbound connections. @@ -152,6 +162,8 @@ impl Runner { } } + // TODO: check if we are active attester to get new L1 Batches, sign them and broadcast the signature + let accept_limiter = limiter::Limiter::new(ctx, self.net.gossip.cfg.tcp_accept_rate); loop { accept_limiter.acquire(ctx, 1).await?; diff --git a/node/actors/network/src/proto/gossip.proto b/node/actors/network/src/proto/gossip.proto index db219151..f2bd6e0f 100644 --- a/node/actors/network/src/proto/gossip.proto +++ b/node/actors/network/src/proto/gossip.proto @@ -4,6 +4,7 @@ package zksync.network.gossip; import "zksync/roles/node.proto"; import "zksync/roles/validator.proto"; +import "zksync/roles/attester.proto"; // First message exchanged in the encrypted session. message Handshake { @@ -17,6 +18,11 @@ message PushValidatorAddrs { repeated roles.validator.Signed net_addresses = 1; } +message PushBatchVotes { + // Signed roles.validator.Msg.votes + repeated roles.attester.Signed votes = 1; +} + // State of the local block store. // A node is expected to store a continuous range of blocks at all times // and actively fetch newest blocks. diff --git a/node/actors/network/src/rpc/mod.rs b/node/actors/network/src/rpc/mod.rs index 2780e005..7f339a4f 100644 --- a/node/actors/network/src/rpc/mod.rs +++ b/node/actors/network/src/rpc/mod.rs @@ -25,6 +25,7 @@ pub(crate) mod consensus; pub(crate) mod get_block; mod metrics; pub(crate) mod ping; +pub(crate) mod push_batch_votes; pub(crate) mod push_block_store_state; pub(crate) mod push_validator_addrs; #[cfg(test)] diff --git a/node/actors/network/src/rpc/push_batch_votes.rs b/node/actors/network/src/rpc/push_batch_votes.rs new file mode 100644 index 00000000..35c3762c --- /dev/null +++ b/node/actors/network/src/rpc/push_batch_votes.rs @@ -0,0 +1,42 @@ +//! Defines RPC for passing consensus messages. +use std::sync::Arc; + +use crate::{mux, proto::gossip as proto}; +use anyhow::Context as _; +use zksync_consensus_roles::attester::{self, Batch}; +use zksync_protobuf::ProtoFmt; + +/// PushBatchVotes RPC. +pub(crate) struct Rpc; + +impl super::Rpc for Rpc { + const CAPABILITY_ID: mux::CapabilityId = 5; + const INFLIGHT: u32 = 1; + const METHOD: &'static str = "push_batch_votes"; + type Req = Req; + type Resp = (); +} + +/// Signed batch message that the receiving peer should process. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Req(pub(crate) Vec>>); + +impl ProtoFmt for Req { + type Proto = proto::PushBatchVotes; + + fn read(r: &Self::Proto) -> anyhow::Result { + let mut votes = vec![]; + for (i, e) in r.votes.iter().enumerate() { + votes.push(Arc::new( + ProtoFmt::read(e).with_context(|| format!("votes[{i}]"))?, + )); + } + Ok(Self(votes)) + } + + fn build(&self) -> Self::Proto { + Self::Proto { + votes: self.0.iter().map(|a| ProtoFmt::build(a.as_ref())).collect(), + } + } +} diff --git a/node/actors/network/src/rpc/tests.rs b/node/actors/network/src/rpc/tests.rs index b3490406..4ac242f3 100644 --- a/node/actors/network/src/rpc/tests.rs +++ b/node/actors/network/src/rpc/tests.rs @@ -17,6 +17,7 @@ fn test_capability_rpc_correspondence() { push_block_store_state::Rpc::CAPABILITY_ID, get_block::Rpc::CAPABILITY_ID, ping::Rpc::CAPABILITY_ID, + push_batch_votes::Rpc::CAPABILITY_ID, ]; assert_eq!(ids.len(), HashSet::from(ids).len()); } diff --git a/node/actors/network/src/testonly.rs b/node/actors/network/src/testonly.rs index fff81eb7..5e08dbbb 100644 --- a/node/actors/network/src/testonly.rs +++ b/node/actors/network/src/testonly.rs @@ -77,7 +77,7 @@ pub fn new_configs( setup: &validator::testonly::Setup, gossip_peers: usize, ) -> Vec { - let configs = setup.keys.iter().map(|key| { + let configs = setup.validator_keys.iter().map(|validator_key| { let addr = net::tcp::testonly::reserve_listener(); Config { server_addr: addr, @@ -85,7 +85,7 @@ pub fn new_configs( // Pings are disabled in tests by default to avoid dropping connections // due to timeouts. ping_timeout: None, - validator_key: Some(key.clone()), + validator_key: Some(validator_key.clone()), gossip: GossipConfig { key: rng.gen(), dynamic_inbound_limit: usize::MAX, @@ -220,7 +220,7 @@ impl Instance { pub async fn wait_for_consensus_connections(&self) { let consensus_state = self.net.consensus.as_ref().unwrap(); - let want: HashSet<_> = self.genesis().committee.keys().cloned().collect(); + let want: HashSet<_> = self.genesis().validators.keys().cloned().collect(); consensus_state .inbound .subscribe() @@ -298,7 +298,7 @@ pub async fn instant_network( node.net .gossip .validator_addrs - .update(&node.genesis().committee, &addrs) + .update(&node.genesis().validators, &addrs) .await .unwrap(); } diff --git a/node/libs/crypto/src/bls12_381/mod.rs b/node/libs/crypto/src/bls12_381/mod.rs index 248dfc99..3ae37d17 100644 --- a/node/libs/crypto/src/bls12_381/mod.rs +++ b/node/libs/crypto/src/bls12_381/mod.rs @@ -168,6 +168,12 @@ impl Ord for Signature { } } +impl std::hash::Hash for Signature { + fn hash(&self, state: &mut H) { + ByteFmt::encode(self).hash(state) + } +} + /// Type safety wrapper around a `blst` aggregate signature #[derive(Clone, Debug)] pub struct AggregateSignature(bls::AggregateSignature); diff --git a/node/libs/crypto/src/bn254/mod.rs b/node/libs/crypto/src/bn254/mod.rs index b8d45ac6..730313ce 100644 --- a/node/libs/crypto/src/bn254/mod.rs +++ b/node/libs/crypto/src/bn254/mod.rs @@ -15,7 +15,6 @@ use pairing::{ use std::{ collections::HashMap, fmt::{Debug, Formatter}, - hash::{Hash, Hasher}, io::Cursor, }; @@ -119,8 +118,8 @@ impl PublicKey { } } -impl Hash for PublicKey { - fn hash(&self, state: &mut H) { +impl std::hash::Hash for PublicKey { + fn hash(&self, state: &mut H) { state.write(&self.encode()); } } @@ -199,6 +198,12 @@ impl ByteFmt for Signature { } } +impl std::hash::Hash for Signature { + fn hash(&self, state: &mut H) { + ByteFmt::encode(self).hash(state) + } +} + impl PartialOrd for Signature { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) diff --git a/node/libs/crypto/src/ed25519/mod.rs b/node/libs/crypto/src/ed25519/mod.rs index 284869fc..4122ec04 100644 --- a/node/libs/crypto/src/ed25519/mod.rs +++ b/node/libs/crypto/src/ed25519/mod.rs @@ -123,6 +123,12 @@ impl ByteFmt for Signature { } } +impl std::hash::Hash for Signature { + fn hash(&self, state: &mut H) { + ByteFmt::encode(self).hash(state) + } +} + /// Error returned when an invalid signature is detected. #[derive(Debug, thiserror::Error)] #[error("invalid signature")] diff --git a/node/libs/roles/src/attester/conv.rs b/node/libs/roles/src/attester/conv.rs new file mode 100644 index 00000000..b636fcc9 --- /dev/null +++ b/node/libs/roles/src/attester/conv.rs @@ -0,0 +1,164 @@ +use crate::proto::attester::{self as proto}; +use anyhow::Context as _; +use zksync_consensus_crypto::ByteFmt; +use zksync_consensus_utils::enum_util::Variant; +use zksync_protobuf::{read_required, required, ProtoFmt}; + +use super::{ + AggregateSignature, Batch, BatchNumber, BatchQC, Msg, MsgHash, PublicKey, Signature, Signed, + Signers, WeightedAttester, +}; + +impl ProtoFmt for Batch { + type Proto = proto::Batch; + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self { + number: BatchNumber(*required(&r.number).context("number")?), + }) + } + fn build(&self) -> Self::Proto { + Self::Proto { + number: Some(self.number.0), + } + } +} + +impl + Clone> ProtoFmt for Signed { + type Proto = proto::Signed; + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self { + msg: V::extract(read_required::(&r.msg).context("msg")?)?, + key: read_required(&r.key).context("key")?, + sig: read_required(&r.sig).context("sig")?, + }) + } + fn build(&self) -> Self::Proto { + Self::Proto { + msg: Some(self.msg.clone().insert().build()), + key: Some(self.key.build()), + sig: Some(self.sig.build()), + } + } +} + +impl ProtoFmt for Msg { + type Proto = proto::Msg; + + fn read(r: &Self::Proto) -> anyhow::Result { + use proto::msg::T; + Ok(match r.t.as_ref().context("missing")? { + T::Batch(r) => Self::Batch(ProtoFmt::read(r).context("Batch")?), + }) + } + + fn build(&self) -> Self::Proto { + use proto::msg::T; + + let t = match self { + Self::Batch(x) => T::Batch(x.build()), + }; + + Self::Proto { t: Some(t) } + } +} + +impl ProtoFmt for PublicKey { + type Proto = proto::PublicKey; + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self(ByteFmt::decode(required(&r.bn254)?)?)) + } + fn build(&self) -> Self::Proto { + Self::Proto { + bn254: Some(self.0.encode()), + } + } +} + +impl ProtoFmt for Signature { + type Proto = proto::Signature; + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self(ByteFmt::decode(required(&r.bn254)?)?)) + } + fn build(&self) -> Self::Proto { + Self::Proto { + bn254: Some(self.0.encode()), + } + } +} + +impl ProtoFmt for WeightedAttester { + type Proto = proto::WeightedAttester; + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self { + key: read_required(&r.key).context("key")?, + weight: *required(&r.weight).context("weight")?, + }) + } + + fn build(&self) -> Self::Proto { + Self::Proto { + key: Some(self.key.build()), + weight: Some(self.weight), + } + } +} + +impl ProtoFmt for Signers { + type Proto = zksync_protobuf::proto::std::BitVector; + + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self(ProtoFmt::read(r)?)) + } + + fn build(&self) -> Self::Proto { + self.0.build() + } +} + +impl ProtoFmt for AggregateSignature { + type Proto = proto::AggregateSignature; + + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self(ByteFmt::decode(required(&r.bn254)?)?)) + } + + fn build(&self) -> Self::Proto { + Self::Proto { + bn254: Some(self.0.encode()), + } + } +} + +impl ProtoFmt for MsgHash { + type Proto = proto::MsgHash; + + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self(ByteFmt::decode(required(&r.keccak256)?)?)) + } + + fn build(&self) -> Self::Proto { + Self::Proto { + keccak256: Some(self.0.encode()), + } + } +} + +impl ProtoFmt for BatchQC { + type Proto = proto::BatchQc; + + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self { + message: read_required(&r.msg).context("message")?, + signers: read_required(&r.signers).context("signers")?, + signature: read_required(&r.sig).context("signature")?, + }) + } + + fn build(&self) -> Self::Proto { + Self::Proto { + msg: Some(self.message.build()), + signers: Some(self.signers.build()), + sig: Some(self.signature.build()), + } + } +} diff --git a/node/libs/roles/src/attester/keys/aggregate_signature.rs b/node/libs/roles/src/attester/keys/aggregate_signature.rs new file mode 100644 index 00000000..95ab6e0e --- /dev/null +++ b/node/libs/roles/src/attester/keys/aggregate_signature.rs @@ -0,0 +1,72 @@ +use crate::attester::{Batch, MsgHash}; + +use super::{PublicKey, Signature}; +use std::fmt; +use zksync_consensus_crypto::{bn254, ByteFmt, Text, TextFmt}; +use zksync_consensus_utils::enum_util::Variant; + +/// An aggregate signature from an attester. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Default)] +pub struct AggregateSignature(pub(crate) bn254::AggregateSignature); + +impl AggregateSignature { + /// Add a signature to the aggregation. + pub fn add(&mut self, sig: &Signature) { + self.0.add(&sig.0) + } + + /// Verify a list of messages against a list of public keys. + pub(crate) fn verify_messages<'a>( + &self, + messages_and_keys: impl Iterator, + ) -> anyhow::Result<()> { + let hashes_and_keys = + messages_and_keys.map(|(message, key)| (message.insert().hash(), key)); + self.verify_hash(hashes_and_keys) + } + + /// Verify a message hash against a list of public keys. + pub(crate) fn verify_hash<'a>( + &self, + hashes_and_keys: impl Iterator, + ) -> anyhow::Result<()> { + let bytes_and_pks: Vec<_> = hashes_and_keys + .map(|(hash, pk)| (hash.0.as_bytes().to_owned(), &pk.0)) + .collect(); + + let bytes_and_pks = bytes_and_pks.iter().map(|(bytes, pk)| (&bytes[..], *pk)); + + self.0.verify(bytes_and_pks) + } +} + +impl ByteFmt for AggregateSignature { + fn decode(bytes: &[u8]) -> anyhow::Result { + ByteFmt::decode(bytes).map(Self) + } + + fn encode(&self) -> Vec { + ByteFmt::encode(&self.0) + } +} + +impl TextFmt for AggregateSignature { + fn decode(text: Text) -> anyhow::Result { + text.strip("attester:aggregate_signature:bn254:")? + .decode_hex() + .map(Self) + } + + fn encode(&self) -> String { + format!( + "attester:aggregate_signature:bn254:{}", + hex::encode(ByteFmt::encode(&self.0)) + ) + } +} + +impl fmt::Debug for AggregateSignature { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&TextFmt::encode(self)) + } +} diff --git a/node/libs/roles/src/attester/keys/mod.rs b/node/libs/roles/src/attester/keys/mod.rs new file mode 100644 index 00000000..5e1d558b --- /dev/null +++ b/node/libs/roles/src/attester/keys/mod.rs @@ -0,0 +1,11 @@ +//! Keys and signatures used by the attester. + +mod aggregate_signature; +mod public_key; +mod secret_key; +mod signature; + +pub use aggregate_signature::AggregateSignature; +pub use public_key::PublicKey; +pub use secret_key::SecretKey; +pub use signature::Signature; diff --git a/node/libs/roles/src/attester/keys/public_key.rs b/node/libs/roles/src/attester/keys/public_key.rs new file mode 100644 index 00000000..9f23d620 --- /dev/null +++ b/node/libs/roles/src/attester/keys/public_key.rs @@ -0,0 +1,33 @@ +use std::fmt; +use zksync_consensus_crypto::{bn254, ByteFmt, Text, TextFmt}; + +/// A public key for an attester used in L1 batch signing. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PublicKey(pub(crate) bn254::PublicKey); + +impl ByteFmt for PublicKey { + fn encode(&self) -> Vec { + ByteFmt::encode(&self.0) + } + fn decode(bytes: &[u8]) -> anyhow::Result { + ByteFmt::decode(bytes).map(Self) + } +} + +impl TextFmt for PublicKey { + fn encode(&self) -> String { + format!( + "attester:public:bn254:{}", + hex::encode(ByteFmt::encode(&self.0)) + ) + } + fn decode(text: Text) -> anyhow::Result { + text.strip("attester:public:bn254:")?.decode_hex().map(Self) + } +} + +impl fmt::Debug for PublicKey { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(&TextFmt::encode(self)) + } +} diff --git a/node/libs/roles/src/attester/keys/secret_key.rs b/node/libs/roles/src/attester/keys/secret_key.rs new file mode 100644 index 00000000..eb911fbb --- /dev/null +++ b/node/libs/roles/src/attester/keys/secret_key.rs @@ -0,0 +1,74 @@ +use super::{PublicKey, Signature}; +use crate::attester::{Batch, Msg, MsgHash, Signed}; +use std::{fmt, sync::Arc}; +use zksync_consensus_crypto::{bn254, ByteFmt, Text, TextFmt}; +use zksync_consensus_utils::enum_util::Variant; + +/// A secret key for the attester role to sign L1 batches. +/// SecretKey is put into an Arc, so that we can clone it, +/// without copying the secret all over the RAM. +#[derive(Clone, PartialEq)] +pub struct SecretKey(pub(crate) Arc); + +impl SecretKey { + /// Generates a batch secret key from a cryptographically-secure entropy source. + pub fn generate() -> Self { + Self(Arc::new(bn254::SecretKey::generate())) + } + + /// Public key corresponding to this secret key. + pub fn public(&self) -> PublicKey { + PublicKey(self.0.public()) + } + + /// Signs a batch message. + pub fn sign_msg(&self, msg: Batch) -> Signed + where + V: Variant, + { + let msg = msg.insert(); + Signed { + sig: self.sign_hash(&msg.hash()), + key: self.public(), + msg: V::extract(msg).unwrap(), + } + } + + /// Sign a message hash. + pub fn sign_hash(&self, msg_hash: &MsgHash) -> Signature { + Signature(self.0.sign(&ByteFmt::encode(msg_hash))) + } +} + +impl ByteFmt for SecretKey { + fn encode(&self) -> Vec { + ByteFmt::encode(&*self.0) + } + + fn decode(bytes: &[u8]) -> anyhow::Result { + ByteFmt::decode(bytes).map(Arc::new).map(Self) + } +} + +impl TextFmt for SecretKey { + fn encode(&self) -> String { + format!( + "attester:secret:bn254:{}", + hex::encode(ByteFmt::encode(&*self.0)) + ) + } + + fn decode(text: Text) -> anyhow::Result { + text.strip("attester:secret:bn254:")? + .decode_hex() + .map(Arc::new) + .map(Self) + } +} + +impl fmt::Debug for SecretKey { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + // The secret itself should never be logged. + write!(fmt, "", TextFmt::encode(&self.public())) + } +} diff --git a/node/libs/roles/src/attester/keys/signature.rs b/node/libs/roles/src/attester/keys/signature.rs new file mode 100644 index 00000000..22583ddf --- /dev/null +++ b/node/libs/roles/src/attester/keys/signature.rs @@ -0,0 +1,50 @@ +use crate::attester::{Msg, MsgHash}; + +use super::PublicKey; +use std::fmt; +use zksync_consensus_crypto::{bn254, ByteFmt, Text, TextFmt}; + +/// A signature of an L1 batch from an attester. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Signature(pub(crate) bn254::Signature); + +impl Signature { + /// Verify a message against a public key. + pub fn verify_msg(&self, msg: &Msg, pk: &PublicKey) -> anyhow::Result<()> { + self.verify_hash(&msg.hash(), pk) + } + + /// Verify a message hash against a public key. + pub fn verify_hash(&self, msg_hash: &MsgHash, pk: &PublicKey) -> anyhow::Result<()> { + self.0.verify(&ByteFmt::encode(msg_hash), &pk.0) + } +} + +impl ByteFmt for Signature { + fn encode(&self) -> Vec { + ByteFmt::encode(&self.0) + } + fn decode(bytes: &[u8]) -> anyhow::Result { + ByteFmt::decode(bytes).map(Self) + } +} + +impl TextFmt for Signature { + fn encode(&self) -> String { + format!( + "attester:signature:bn254:{}", + hex::encode(ByteFmt::encode(&self.0)) + ) + } + fn decode(text: Text) -> anyhow::Result { + text.strip("attester:signature:bn254:")? + .decode_hex() + .map(Self) + } +} + +impl fmt::Debug for Signature { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(&TextFmt::encode(self)) + } +} diff --git a/node/libs/roles/src/attester/messages/batch.rs b/node/libs/roles/src/attester/messages/batch.rs new file mode 100644 index 00000000..b6af94d6 --- /dev/null +++ b/node/libs/roles/src/attester/messages/batch.rs @@ -0,0 +1,142 @@ +use crate::{attester, validator::Genesis}; + +use super::{Signed, Signers}; +use anyhow::{ensure, Context as _}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, PartialOrd)] +/// A batch number. +pub struct BatchNumber(pub u64); + +impl BatchNumber { + /// Increment the batch number. + pub fn next(&self) -> BatchNumber { + BatchNumber(self.0.checked_add(1).unwrap()) + } +} + +/// A message containing information about a batch of blocks. +/// It is signed by the attesters and then propagated through the gossip network. +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd)] +pub struct Batch { + /// The number of the batch. + pub number: BatchNumber, + // TODO: add the hash of the L1 batch as a field +} + +/// A certificate for a batch of L2 blocks to be sent to L1. +/// It contains the signatures of the attesters that signed the batch. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct BatchQC { + /// The aggregate signature of the signed L1 batches. + pub signature: attester::AggregateSignature, + /// The attesters that signed this message. + pub signers: Signers, + /// The message that was signed. + pub message: Batch, +} + +/// Error returned by `BatchQC::verify()` if the signature is invalid. +#[derive(thiserror::Error, Debug)] +pub enum BatchQCVerifyError { + /// Bad signature. + #[error("bad signature: {0:#}")] + BadSignature(#[source] anyhow::Error), + /// Not enough signers. + #[error("not enough signers: got {got}, want {want}")] + NotEnoughSigners { + /// Got signers. + got: u64, + /// Want signers. + want: u64, + }, + /// Bad signer set. + #[error("signers set doesn't match genesis")] + BadSignersSet, + /// No attester committee in genesis. + #[error("No attester committee in genesis")] + AttestersNotInGenesis, +} + +/// Error returned by `BatchQC::add()` if the signature is invalid. +#[derive(thiserror::Error, Debug)] +pub enum BatchQCAddError { + /// Inconsistent messages. + #[error("Trying to add signature for a different message")] + InconsistentMessages, + /// Signer not present in the committee. + #[error("Signer not in committee: {signer:?}")] + SignerNotInCommittee { + /// Signer of the message. + signer: Box, + }, + /// Message already present in BatchQC. + #[error("Message already signed for BatchQC")] + Exists, +} + +impl BatchQC { + /// Create a new empty instance for a given `Batch` message. + pub fn new(message: Batch, genesis: &Genesis) -> anyhow::Result { + Ok(Self { + message, + signers: Signers::new( + genesis + .attesters + .as_ref() + .context("no attester committee in genesis")? + .len(), + ), + signature: attester::AggregateSignature::default(), + }) + } + + /// Add a attester's signature. + /// Signature is assumed to be already verified. + pub fn add(&mut self, msg: &Signed, genesis: &Genesis) -> anyhow::Result<()> { + use BatchQCAddError as Error; + ensure!(self.message == msg.msg, Error::InconsistentMessages); + let i = genesis + .attesters + .as_ref() + .context("no attester committee in genesis")? + .index(&msg.key) + .ok_or(Error::SignerNotInCommittee { + signer: Box::new(msg.key.clone()), + })?; + ensure!(!self.signers.0[i], Error::Exists); + self.signers.0.set(i, true); + self.signature.add(&msg.sig); + Ok(()) + } + + /// Verifies the signature of the BatchQC. + pub fn verify(&self, genesis: &Genesis) -> Result<(), BatchQCVerifyError> { + use BatchQCVerifyError as Error; + let attesters = genesis + .attesters + .as_ref() + .ok_or(Error::AttestersNotInGenesis)?; + if self.signers.len() != attesters.len() { + return Err(Error::BadSignersSet); + } + // Verify that the signer's weight is sufficient. + let weight = attesters.weight(&self.signers); + let threshold = attesters.threshold(); + if weight < threshold { + return Err(Error::NotEnoughSigners { + got: weight, + want: threshold, + }); + } + + let messages_and_keys = attesters + .keys() + .enumerate() + .filter(|(i, _)| self.signers.0[*i]) + .map(|(_, pk)| (self.message.clone(), pk)); + + self.signature + .verify_messages(messages_and_keys) + .map_err(Error::BadSignature) + } +} diff --git a/node/libs/roles/src/attester/messages/mod.rs b/node/libs/roles/src/attester/messages/mod.rs new file mode 100644 index 00000000..bd68233d --- /dev/null +++ b/node/libs/roles/src/attester/messages/mod.rs @@ -0,0 +1,6 @@ +//! Attester messages. +mod batch; +mod msg; + +pub use batch::*; +pub use msg::*; diff --git a/node/libs/roles/src/attester/messages/msg.rs b/node/libs/roles/src/attester/messages/msg.rs new file mode 100644 index 00000000..5501cae9 --- /dev/null +++ b/node/libs/roles/src/attester/messages/msg.rs @@ -0,0 +1,259 @@ +use std::{collections::BTreeMap, fmt}; + +use crate::{attester, validator}; +use anyhow::Context as _; +use bit_vec::BitVec; +use zksync_consensus_crypto::{keccak256, ByteFmt, Text, TextFmt}; +use zksync_consensus_utils::enum_util::{BadVariantError, Variant}; + +/// Message that is sent by an attester. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Msg { + /// L1 batch message. + Batch(attester::Batch), +} + +impl Msg { + /// Returns the hash of the message. + pub fn hash(&self) -> MsgHash { + MsgHash(keccak256::Keccak256::new(&zksync_protobuf::canonical(self))) + } +} + +impl Variant for attester::Batch { + fn insert(self) -> Msg { + Msg::Batch(self) + } + fn extract(msg: Msg) -> Result { + let Msg::Batch(this) = msg; + Ok(this) + } +} + +/// Strongly typed signed l1 batch message. +/// WARNING: signature is not guaranteed to be valid. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Signed> { + /// The message that was signed. + pub msg: V, + /// The public key of the signer. + pub key: attester::PublicKey, + /// The signature. + pub sig: attester::Signature, +} + +/// Struct that represents a bit map of attesters. We use it to compactly store +/// which attesters signed a given Batch message. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Signers(pub BitVec); + +impl Signers { + /// Constructs an empty Signers set. + pub fn new(n: usize) -> Self { + Self(BitVec::from_elem(n, false)) + } + + /// Returns the number of signers, i.e. the number of attesters that signed + /// the particular message that this signer bitmap refers to. + pub fn count(&self) -> usize { + self.0.iter().filter(|b| *b).count() + } + + /// Size of the corresponding attester::Committee. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns true if there are no signers. + pub fn is_empty(&self) -> bool { + self.0.none() + } +} + +/// A struct that represents a set of attesters. It is used to store the current attester set. +/// We represent each attester by its attester public key. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct Committee { + vec: Vec, + indexes: BTreeMap, + total_weight: u64, +} + +impl std::ops::Deref for Committee { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.vec + } +} + +impl Committee { + /// Creates a new Committee from a list of attester public keys. + pub fn new(attesters: impl IntoIterator) -> anyhow::Result { + let mut weighted_attester = BTreeMap::new(); + let mut total_weight: u64 = 0; + for attester in attesters { + anyhow::ensure!( + !weighted_attester.contains_key(&attester.key), + "Duplicated attester in attester Committee" + ); + anyhow::ensure!( + attester.weight > 0, + "Attester weight has to be a positive value" + ); + total_weight = total_weight + .checked_add(attester.weight) + .context("Sum of weights overflows in attester Committee")?; + weighted_attester.insert(attester.key.clone(), attester); + } + anyhow::ensure!( + !weighted_attester.is_empty(), + "Attester Committee must contain at least one attester" + ); + Ok(Self { + vec: weighted_attester.values().cloned().collect(), + indexes: weighted_attester + .values() + .enumerate() + .map(|(i, v)| (v.key.clone(), i)) + .collect(), + total_weight, + }) + } + + /// Iterates over attester keys. + pub fn keys(&self) -> impl Iterator { + self.vec.iter().map(|v| &v.key) + } + + /// Returns the number of attesters. + #[allow(clippy::len_without_is_empty)] // a valid `Committee` is always non-empty by construction + pub fn len(&self) -> usize { + self.vec.len() + } + + /// Returns true if the given attester is in the attester committee. + pub fn contains(&self, attester: &attester::PublicKey) -> bool { + self.indexes.contains_key(attester) + } + + /// Get attester by its index in the committee. + pub fn get(&self, index: usize) -> Option<&WeightedAttester> { + self.vec.get(index) + } + + /// Get the index of a attester in the committee. + pub fn index(&self, attester: &attester::PublicKey) -> Option { + self.indexes.get(attester).copied() + } + + /// Signature weight threshold for this attester committee. + pub fn threshold(&self) -> u64 { + threshold(self.total_weight()) + } + + /// Compute the sum of signers weights. + pub fn weight(&self, signers: &Signers) -> u64 { + assert_eq!(self.vec.len(), signers.len()); + self.vec + .iter() + .enumerate() + .filter(|(i, _)| signers.0[*i]) + .map(|(_, v)| v.weight) + .sum() + } + + /// Sum of all attesters weight in the committee + pub fn total_weight(&self) -> u64 { + self.total_weight + } +} + +/// Calculate the attester threshold, that is the minimum votes weight for any attesters action to be valid, +/// for a given committee total weight. +/// Technically we need just n > f+1, but for now we use a threshold consistent with the validator committee. +pub fn threshold(total_weight: u64) -> u64 { + total_weight - validator::max_faulty_weight(total_weight) +} + +impl std::ops::BitOrAssign<&Self> for Signers { + fn bitor_assign(&mut self, other: &Self) { + self.0.or(&other.0); + } +} + +impl std::ops::BitAndAssign<&Self> for Signers { + fn bitand_assign(&mut self, other: &Self) { + self.0.and(&other.0); + } +} + +impl std::ops::BitAnd for &Signers { + type Output = Signers; + fn bitand(self, other: Self) -> Signers { + let mut this = self.clone(); + this &= other; + this + } +} + +/// The hash of a message. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MsgHash(pub(crate) keccak256::Keccak256); + +impl ByteFmt for MsgHash { + fn decode(bytes: &[u8]) -> anyhow::Result { + ByteFmt::decode(bytes).map(Self) + } + + fn encode(&self) -> Vec { + ByteFmt::encode(&self.0) + } +} + +impl TextFmt for MsgHash { + fn decode(text: Text) -> anyhow::Result { + text.strip("attester_msg:keccak256:")? + .decode_hex() + .map(Self) + } + + fn encode(&self) -> String { + format!( + "attester_msg:keccak256:{}", + hex::encode(ByteFmt::encode(&self.0)) + ) + } +} + +impl fmt::Debug for MsgHash { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(&TextFmt::encode(self)) + } +} + +impl + Clone> Signed { + /// Verify the signature on the message. + pub fn verify(&self) -> anyhow::Result<()> { + self.sig.verify_msg(&self.msg.clone().insert(), &self.key) + } + + /// Casts a signed message variant to sub/super variant. + /// It is an equivalent of constructing/deconstructing enum values. + pub fn cast(self) -> Result, BadVariantError> { + Ok(Signed { + msg: V::extract(self.msg.insert())?, + key: self.key, + sig: self.sig, + }) + } +} + +/// Attester representation inside a Committee. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WeightedAttester { + /// Attester key + pub key: attester::PublicKey, + /// Attester weight inside the Committee. + pub weight: u64, +} diff --git a/node/libs/roles/src/attester/mod.rs b/node/libs/roles/src/attester/mod.rs new file mode 100644 index 00000000..5bc5b466 --- /dev/null +++ b/node/libs/roles/src/attester/mod.rs @@ -0,0 +1,12 @@ +//! Attester role implementation. + +#[cfg(test)] +mod tests; + +mod conv; +mod keys; +mod messages; +mod testonly; + +pub use self::keys::*; +pub use self::messages::*; diff --git a/node/libs/roles/src/attester/testonly.rs b/node/libs/roles/src/attester/testonly.rs new file mode 100644 index 00000000..502b24a4 --- /dev/null +++ b/node/libs/roles/src/attester/testonly.rs @@ -0,0 +1,99 @@ +use super::{ + AggregateSignature, Batch, BatchNumber, BatchQC, Committee, Msg, MsgHash, PublicKey, SecretKey, + Signature, Signed, Signers, WeightedAttester, +}; +use bit_vec::BitVec; +use rand::{ + distributions::{Distribution, Standard}, + Rng, +}; +use std::sync::Arc; +use zksync_consensus_utils::enum_util::Variant; + +impl AggregateSignature { + /// Generate a new aggregate signature from a list of signatures. + pub fn aggregate<'a>(sigs: impl IntoIterator) -> Self { + let mut agg = Self::default(); + for sig in sigs { + agg.add(sig); + } + agg + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> SecretKey { + SecretKey(Arc::new(rng.gen())) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> PublicKey { + PublicKey(rng.gen()) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> AggregateSignature { + AggregateSignature(rng.gen()) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Committee { + let count = rng.gen_range(1..11); + let public_keys = (0..count).map(|_| WeightedAttester { + key: rng.gen(), + weight: 1, + }); + Committee::new(public_keys).unwrap() + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Batch { + Batch { + number: BatchNumber(rng.gen()), + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> BatchQC { + BatchQC { + message: rng.gen(), + signers: rng.gen(), + signature: rng.gen(), + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Msg { + Msg::Batch(rng.gen()) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Signers { + Signers(BitVec::from_bytes(&rng.gen::<[u8; 4]>())) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Signature { + Signature(rng.gen()) + } +} + +impl> Distribution> for Standard { + fn sample(&self, rng: &mut R) -> Signed { + rng.gen::().sign_msg(rng.gen()) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> MsgHash { + MsgHash(rng.gen()) + } +} diff --git a/node/libs/roles/src/attester/tests.rs b/node/libs/roles/src/attester/tests.rs new file mode 100644 index 00000000..f29ed464 --- /dev/null +++ b/node/libs/roles/src/attester/tests.rs @@ -0,0 +1,241 @@ +use crate::validator::testonly::Setup; + +use super::*; +use assert_matches::assert_matches; +use rand::Rng; +use zksync_concurrency::ctx; +use zksync_consensus_crypto::{ByteFmt, Text, TextFmt}; +use zksync_protobuf::testonly::test_encode_random; + +#[test] +fn test_byte_encoding() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let sk: SecretKey = rng.gen(); + assert_eq!(sk, ByteFmt::decode(&ByteFmt::encode(&sk)).unwrap()); + + let pk: PublicKey = rng.gen(); + assert_eq!(pk, ByteFmt::decode(&ByteFmt::encode(&pk)).unwrap()); + + let sig: Signature = rng.gen(); + assert_eq!(sig, ByteFmt::decode(&ByteFmt::encode(&sig)).unwrap()); + + let agg_sig: AggregateSignature = rng.gen(); + assert_eq!( + agg_sig, + ByteFmt::decode(&ByteFmt::encode(&agg_sig)).unwrap() + ); +} + +#[test] +fn test_text_encoding() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let sk: SecretKey = rng.gen(); + let t = TextFmt::encode(&sk); + assert_eq!(sk, Text::new(&t).decode::().unwrap()); + + let pk: PublicKey = rng.gen(); + let t = TextFmt::encode(&pk); + assert_eq!(pk, Text::new(&t).decode::().unwrap()); + + let sig: Signature = rng.gen(); + let t = TextFmt::encode(&sig); + assert_eq!(sig, Text::new(&t).decode::().unwrap()); + + let agg_sig: AggregateSignature = rng.gen(); + let t = TextFmt::encode(&agg_sig); + assert_eq!( + agg_sig, + Text::new(&t).decode::().unwrap() + ); + + let msg_hash: MsgHash = rng.gen(); + let t = TextFmt::encode(&msg_hash); + assert_eq!(msg_hash, Text::new(&t).decode::().unwrap()); +} + +#[test] +fn test_schema_encoding() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + test_encode_random::>(rng); + test_encode_random::(rng); + test_encode_random::(rng); + test_encode_random::(rng); + test_encode_random::(rng); + test_encode_random::(rng); + test_encode_random::(rng); + test_encode_random::(rng); +} + +#[test] +fn test_signature_verify() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let msg1: MsgHash = rng.gen(); + let msg2: MsgHash = rng.gen(); + + let key1: SecretKey = rng.gen(); + let key2: SecretKey = rng.gen(); + + let sig1 = key1.sign_hash(&msg1); + + // Matching key and message. + sig1.verify_hash(&msg1, &key1.public()).unwrap(); + + // Mismatching message. + assert!(sig1.verify_hash(&msg2, &key1.public()).is_err()); + + // Mismatching key. + assert!(sig1.verify_hash(&msg1, &key2.public()).is_err()); +} + +#[test] +fn test_agg_signature_verify() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let msg1: MsgHash = rng.gen(); + let msg2: MsgHash = rng.gen(); + + let key1: SecretKey = rng.gen(); + let key2: SecretKey = rng.gen(); + + let sig1 = key1.sign_hash(&msg1); + let sig2 = key2.sign_hash(&msg2); + + let agg_sig = AggregateSignature::aggregate(vec![&sig1, &sig2]); + + // Matching key and message. + agg_sig + .verify_hash([(msg1, &key1.public()), (msg2, &key2.public())].into_iter()) + .unwrap(); + + // Mismatching message. + assert!(agg_sig + .verify_hash([(msg2, &key1.public()), (msg1, &key2.public())].into_iter()) + .is_err()); + + // Mismatching key. + assert!(agg_sig + .verify_hash([(msg1, &key2.public()), (msg2, &key1.public())].into_iter()) + .is_err()); +} + +fn make_batch_msg(rng: &mut impl Rng) -> Batch { + Batch { + number: BatchNumber(rng.gen()), + } +} + +#[test] +fn test_batch_qc() { + use BatchQCVerifyError as Error; + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let setup1 = Setup::new(rng, 6); + let setup2 = Setup::new(rng, 6); + let mut genesis3 = (*setup1.genesis).clone(); + genesis3.attesters = Committee::new( + setup1 + .genesis + .attesters + .as_ref() + .unwrap() + .iter() + .take(3) + .cloned(), + ) + .unwrap() + .into(); + let genesis3 = genesis3.with_hash(); + let attesters = setup1.genesis.attesters.as_ref().unwrap(); + + for i in 0..setup1.attester_keys.len() + 1 { + let mut qc = BatchQC::new(make_batch_msg(rng), &setup1.genesis).unwrap(); + for key in &setup1.attester_keys[0..i] { + qc.add(&key.sign_msg(qc.message.clone()), &setup1.genesis) + .unwrap(); + } + + let expected_weight: u64 = attesters.iter().take(i).map(|w| w.weight).sum(); + if expected_weight >= attesters.threshold() { + assert!(qc.verify(&setup1.genesis).is_ok()); + } else { + assert_matches!( + qc.verify(&setup1.genesis), + Err(Error::NotEnoughSigners { .. }) + ); + } + + // Mismatching attesters sets. + assert!(qc.verify(&setup2.genesis).is_err()); + assert!(qc.verify(&genesis3).is_err()); + } +} + +#[test] +fn test_attester_committee_weights() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + // Attesters with non-uniform weights + let setup = Setup::new_with_weights(rng, vec![1000, 600, 800, 6000, 900, 700]); + // Expected sum of the attesters weights + let sums = [1000, 1600, 2400, 8400, 9300, 10000]; + + let msg = make_batch_msg(rng); + let mut qc = BatchQC::new(msg.clone(), &setup.genesis).unwrap(); + for (n, weight) in sums.iter().enumerate() { + let key = &setup.attester_keys[n]; + qc.add(&key.sign_msg(msg.clone()), &setup.genesis).unwrap(); + assert_eq!( + setup + .genesis + .attesters + .as_ref() + .unwrap() + .weight(&qc.signers), + *weight + ); + } +} + +#[test] +fn test_committee_weights_overflow_check() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let attesters: Vec = [u64::MAX / 5; 6] + .iter() + .map(|w| WeightedAttester { + key: rng.gen::().public(), + weight: *w, + }) + .collect(); + + // Creation should overflow + assert_matches!(Committee::new(attesters), Err(_)); +} + +#[test] +fn test_committee_with_zero_weights() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + + let attesters: Vec = [1000, 0, 800, 6000, 0, 700] + .iter() + .map(|w| WeightedAttester { + key: rng.gen::().public(), + weight: *w, + }) + .collect(); + + // Committee creation should error on zero weight attesters + assert_matches!(Committee::new(attesters), Err(_)); +} diff --git a/node/libs/roles/src/lib.rs b/node/libs/roles/src/lib.rs index 8f836ca7..38457858 100644 --- a/node/libs/roles/src/lib.rs +++ b/node/libs/roles/src/lib.rs @@ -6,7 +6,10 @@ //! - `Validator`: a node that participates in the consensus protocol, so it votes for blocks and produces blocks. //! It also participates in the validator network, which is a mesh network just for validators. Not //! every node has this role. +//! - `Attester`: a node that signs the L1 batches and broadcasts the signatures known as votes to the gossip network. +//! Not every node has this role. +pub mod attester; pub mod node; pub mod proto; pub mod validator; diff --git a/node/libs/roles/src/node/keys.rs b/node/libs/roles/src/node/keys.rs index b3fb0441..107ff538 100644 --- a/node/libs/roles/src/node/keys.rs +++ b/node/libs/roles/src/node/keys.rs @@ -99,7 +99,7 @@ impl fmt::Debug for PublicKey { } /// A signature of a message. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Signature(pub(super) ed25519::Signature); impl ByteFmt for Signature { diff --git a/node/libs/roles/src/node/tests.rs b/node/libs/roles/src/node/tests.rs index 493d4ce8..9814b16f 100644 --- a/node/libs/roles/src/node/tests.rs +++ b/node/libs/roles/src/node/tests.rs @@ -7,12 +7,7 @@ use zksync_protobuf::testonly::{test_encode, test_encode_random}; #[test] fn test_byte_encoding() { let key = SecretKey::generate(); - assert_eq!( - key.public(), - ::decode(&ByteFmt::encode(&key)) - .unwrap() - .public() - ); + assert_eq!(key, ByteFmt::decode(&ByteFmt::encode(&key)).unwrap()); assert_eq!( key.public(), ByteFmt::decode(&ByteFmt::encode(&key.public())).unwrap() @@ -24,10 +19,7 @@ fn test_text_encoding() { let key = SecretKey::generate(); let t1 = TextFmt::encode(&key); let t2 = TextFmt::encode(&key.public()); - assert_eq!( - key.public(), - Text::new(&t1).decode::().unwrap().public() - ); + assert_eq!(key, Text::new(&t1).decode::().unwrap()); assert_eq!(key.public(), Text::new(&t2).decode().unwrap()); assert!(Text::new(&t1).decode::().is_err()); assert!(Text::new(&t2).decode::().is_err()); diff --git a/node/libs/roles/src/proto/attester.proto b/node/libs/roles/src/proto/attester.proto new file mode 100644 index 00000000..4c5e2c73 --- /dev/null +++ b/node/libs/roles/src/proto/attester.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +package zksync.roles.attester; + +import "zksync/std.proto"; + +message Batch { + optional uint64 number = 1; // required + // TODO: add the hash of the L1 batch as a field +} + +message BatchQC { + optional Batch msg = 1; // required + optional std.BitVector signers = 2; // required + optional AggregateSignature sig = 3; // required +} + +message Msg { + oneof t { // required + Batch batch = 4; + } +} + +message Signed { + optional Msg msg = 1; // required + optional PublicKey key = 2; // required + optional Signature sig = 3; // required +} + +message PublicKey { + optional bytes bn254 = 1; // required +} + +message Signature { + optional bytes bn254 = 1; // required +} + +message WeightedAttester { + optional PublicKey key = 1; // required + optional uint64 weight = 2; // required +} + +message AggregateSignature { + optional bytes bn254 = 1; // required +} + +message MsgHash { + optional bytes keccak256 = 1; // required +} diff --git a/node/libs/roles/src/proto/validator.proto b/node/libs/roles/src/proto/validator.proto index ad8dfa9c..87600b30 100644 --- a/node/libs/roles/src/proto/validator.proto +++ b/node/libs/roles/src/proto/validator.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package zksync.roles.validator; import "zksync/std.proto"; +import "zksync/roles/attester.proto"; message Genesis { reserved 1,2; @@ -15,6 +16,7 @@ message Genesis { // We will either remove them entirely, or keep them for the initial epoch. optional uint32 protocol_version = 8; // required; ProtocolVersion repeated WeightedValidator validators_v1 = 3; + repeated attester.WeightedAttester attesters = 9; // optional optional LeaderSelectionMode leader_selection = 4; // required } diff --git a/node/libs/roles/src/validator/conv.rs b/node/libs/roles/src/validator/conv.rs index db65093e..0cef59da 100644 --- a/node/libs/roles/src/validator/conv.rs +++ b/node/libs/roles/src/validator/conv.rs @@ -1,10 +1,15 @@ +use crate::{ + attester::{self, WeightedAttester}, + node::SessionId, +}; + use super::{ AggregateSignature, BlockHeader, BlockNumber, ChainId, CommitQC, Committee, ConsensusMsg, FinalBlock, ForkNumber, Genesis, GenesisHash, GenesisRaw, LeaderCommit, LeaderPrepare, Msg, MsgHash, NetAddress, Payload, PayloadHash, Phase, PrepareQC, ProtocolVersion, PublicKey, ReplicaCommit, ReplicaPrepare, Signature, Signed, Signers, View, ViewNumber, WeightedValidator, }; -use crate::{node::SessionId, proto::validator as proto, validator::LeaderSelectionMode}; +use crate::{proto::validator as proto, validator::LeaderSelectionMode}; use anyhow::Context as _; use std::collections::BTreeMap; use zksync_consensus_crypto::ByteFmt; @@ -21,13 +26,25 @@ impl ProtoFmt for GenesisRaw { .map(|(i, v)| WeightedValidator::read(v).context(i)) .collect::>() .context("validators_v1")?; + let attesters: Vec<_> = r + .attesters + .iter() + .enumerate() + .map(|(i, v)| WeightedAttester::read(v).context(i)) + .collect::>() + .context("attesters")?; Ok(GenesisRaw { chain_id: ChainId(*required(&r.chain_id).context("chain_id")?), fork_number: ForkNumber(*required(&r.fork_number).context("fork_number")?), first_block: BlockNumber(*required(&r.first_block).context("first_block")?), protocol_version: ProtocolVersion(r.protocol_version.context("protocol_version")?), - committee: Committee::new(validators.into_iter()).context("validators_v1")?, + validators: Committee::new(validators.into_iter()).context("validators_v1")?, + attesters: if attesters.is_empty() { + None + } else { + Some(attester::Committee::new(attesters.into_iter()).context("attesters")?) + }, leader_selection: read_required(&r.leader_selection).context("leader_selection")?, }) } @@ -38,7 +55,12 @@ impl ProtoFmt for GenesisRaw { first_block: Some(self.first_block.0), protocol_version: Some(self.protocol_version.0), - validators_v1: self.committee.iter().map(|v| v.build()).collect(), + validators_v1: self.validators.iter().map(|v| v.build()).collect(), + attesters: self + .attesters + .as_ref() + .map(|c| c.iter().map(|v| v.build()).collect()) + .unwrap_or_default(), leader_selection: Some(self.leader_selection.build()), } } diff --git a/node/libs/roles/src/validator/keys/signature.rs b/node/libs/roles/src/validator/keys/signature.rs index deb89eea..967ae149 100644 --- a/node/libs/roles/src/validator/keys/signature.rs +++ b/node/libs/roles/src/validator/keys/signature.rs @@ -4,7 +4,7 @@ use std::fmt; use zksync_consensus_crypto::{bls12_381, ByteFmt, Text, TextFmt}; /// A signature from a validator. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, Hash)] pub struct Signature(pub(crate) bls12_381::Signature); impl Signature { @@ -47,9 +47,3 @@ impl fmt::Debug for Signature { fmt.write_str(&TextFmt::encode(self)) } } - -impl std::hash::Hash for Signature { - fn hash(&self, state: &mut H) { - ByteFmt::encode(self).hash(state) - } -} diff --git a/node/libs/roles/src/validator/messages/consensus.rs b/node/libs/roles/src/validator/messages/consensus.rs index f88a5aee..882bdcee 100644 --- a/node/libs/roles/src/validator/messages/consensus.rs +++ b/node/libs/roles/src/validator/messages/consensus.rs @@ -1,6 +1,6 @@ //! Messages related to the consensus protocol. use super::{BlockNumber, LeaderCommit, LeaderPrepare, Msg, ReplicaCommit, ReplicaPrepare}; -use crate::validator; +use crate::{attester, validator}; use anyhow::Context; use bit_vec::BitVec; use num_bigint::BigUint; @@ -86,6 +86,14 @@ pub struct Committee { total_weight: u64, } +impl std::ops::Deref for Committee { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.vec + } +} + impl Committee { /// Creates a new Committee from a list of validator public keys. pub fn new(validators: impl IntoIterator) -> anyhow::Result { @@ -118,11 +126,6 @@ impl Committee { }) } - /// Iterates over weighted validators. - pub fn iter(&self) -> impl Iterator { - self.vec.iter() - } - /// Iterates over validator keys. pub fn keys(&self) -> impl Iterator { self.vec.iter().map(|v| &v.key) @@ -232,7 +235,6 @@ pub struct ChainId(pub u64); pub struct GenesisRaw { /// ID of the blockchain. pub chain_id: ChainId, - /// Number of the fork. Should be incremented every time the genesis is updated, /// i.e. whenever a hard fork is performed. pub fork_number: ForkNumber, @@ -241,7 +243,9 @@ pub struct GenesisRaw { /// First block of a fork. pub first_block: BlockNumber, /// Set of validators of the chain. - pub committee: Committee, + pub validators: Committee, + /// Set of attesters of the chain. + pub attesters: Option, /// The mode used for selecting leader for a given view. pub leader_selection: LeaderSelectionMode, } @@ -306,7 +310,7 @@ impl Genesis { /// Verifies correctness. pub fn verify(&self) -> anyhow::Result<()> { if let LeaderSelectionMode::Sticky(pk) = &self.leader_selection { - if self.committee.index(pk).is_none() { + if self.validators.index(pk).is_none() { anyhow::bail!("leader_selection sticky mode public key is not in committee"); } } @@ -316,7 +320,7 @@ impl Genesis { /// Computes the leader for the given view. pub fn view_leader(&self, view: ViewNumber) -> validator::PublicKey { - self.committee.view_leader(view, &self.leader_selection) + self.validators.view_leader(view, &self.leader_selection) } /// Hash of the genesis. diff --git a/node/libs/roles/src/validator/messages/leader_commit.rs b/node/libs/roles/src/validator/messages/leader_commit.rs index ebd35854..bbcedf1b 100644 --- a/node/libs/roles/src/validator/messages/leader_commit.rs +++ b/node/libs/roles/src/validator/messages/leader_commit.rs @@ -86,7 +86,7 @@ impl CommitQC { pub fn new(message: ReplicaCommit, genesis: &Genesis) -> Self { Self { message, - signers: Signers::new(genesis.committee.len()), + signers: Signers::new(genesis.validators.len()), signature: validator::AggregateSignature::default(), } } @@ -102,7 +102,7 @@ impl CommitQC { if self.message != msg.msg { return Err(Error::InconsistentMessages); }; - let Some(i) = genesis.committee.index(&msg.key) else { + let Some(i) = genesis.validators.index(&msg.key) else { return Err(Error::SignerNotInCommittee { signer: Box::new(msg.key.clone()), }); @@ -121,13 +121,13 @@ impl CommitQC { self.message .verify(genesis) .map_err(Error::InvalidMessage)?; - if self.signers.len() != genesis.committee.len() { + if self.signers.len() != genesis.validators.len() { return Err(Error::BadSignersSet); } // Verify the signers' weight is enough. - let weight = genesis.committee.weight(&self.signers); - let threshold = genesis.committee.threshold(); + let weight = genesis.validators.weight(&self.signers); + let threshold = genesis.validators.threshold(); if weight < threshold { return Err(Error::NotEnoughSigners { got: weight, @@ -137,7 +137,7 @@ impl CommitQC { // Now we can verify the signature. let messages_and_keys = genesis - .committee + .validators .keys() .enumerate() .filter(|(i, _)| self.signers.0[*i]) diff --git a/node/libs/roles/src/validator/messages/leader_prepare.rs b/node/libs/roles/src/validator/messages/leader_prepare.rs index 9282da16..f452a594 100644 --- a/node/libs/roles/src/validator/messages/leader_prepare.rs +++ b/node/libs/roles/src/validator/messages/leader_prepare.rs @@ -79,11 +79,11 @@ impl PrepareQC { let mut count: HashMap<_, u64> = HashMap::new(); for (msg, signers) in &self.map { if let Some(v) = &msg.high_vote { - *count.entry(v.proposal).or_default() += genesis.committee.weight(signers); + *count.entry(v.proposal).or_default() += genesis.validators.weight(signers); } } // We only take one value from the iterator because there can only be at most one block with a quorum of 2f+1 votes. - let min = 2 * genesis.committee.max_faulty_weight() + 1; + let min = 2 * genesis.validators.max_faulty_weight() + 1; count.into_iter().find(|x| x.1 >= min).map(|x| x.0) } @@ -107,7 +107,7 @@ impl PrepareQC { if msg.msg.view != self.view { return Err(Error::InconsistentViews); } - let Some(i) = genesis.committee.index(&msg.key) else { + let Some(i) = genesis.validators.index(&msg.key) else { return Err(Error::SignerNotInCommittee { signer: Box::new(msg.key.clone()), }); @@ -118,7 +118,7 @@ impl PrepareQC { let e = self .map .entry(msg.msg.clone()) - .or_insert_with(|| Signers::new(genesis.committee.len())); + .or_insert_with(|| Signers::new(genesis.validators.len())); e.0.set(i, true); self.signature.add(&msg.sig); Ok(()) @@ -128,7 +128,7 @@ impl PrepareQC { pub fn verify(&self, genesis: &Genesis) -> Result<(), PrepareQCVerifyError> { use PrepareQCVerifyError as Error; self.view.verify(genesis).map_err(Error::View)?; - let mut sum = Signers::new(genesis.committee.len()); + let mut sum = Signers::new(genesis.validators.len()); // Check the ReplicaPrepare messages. for (i, (msg, signers)) in self.map.iter().enumerate() { @@ -156,8 +156,8 @@ impl PrepareQC { } // Verify the signers' weight is enough. - let weight = genesis.committee.weight(&sum); - let threshold = genesis.committee.threshold(); + let weight = genesis.validators.weight(&sum); + let threshold = genesis.validators.threshold(); if weight < threshold { return Err(Error::NotEnoughSigners { got: weight, @@ -167,7 +167,7 @@ impl PrepareQC { // Now we can verify the signature. let messages_and_keys = self.map.clone().into_iter().flat_map(|(msg, signers)| { genesis - .committee + .validators .keys() .enumerate() .filter(|(i, _)| signers.0[*i]) diff --git a/node/libs/roles/src/validator/messages/tests.rs b/node/libs/roles/src/validator/messages/tests.rs index 0730bc41..32ac4d98 100644 --- a/node/libs/roles/src/validator/messages/tests.rs +++ b/node/libs/roles/src/validator/messages/tests.rs @@ -1,3 +1,4 @@ +use crate::attester::{self, WeightedAttester}; use crate::validator::*; use anyhow::Context as _; use rand::{prelude::StdRng, Rng, SeedableRng}; @@ -6,7 +7,7 @@ use zksync_consensus_crypto::Text; use zksync_consensus_utils::enum_util::Variant as _; /// Hardcoded secret keys. -fn keys() -> Vec { +fn validator_keys() -> Vec { [ "validator:secret:bls12_381:27cb45b1670a1ae8d376a85821d51c7f91ebc6e32788027a84758441aaf0a987", "validator:secret:bls12_381:20132edc08a529e927f155e710ae7295a2a0d249f1b1f37726894d1d0d8f0d81", @@ -19,12 +20,41 @@ fn keys() -> Vec { .collect() } +fn attester_keys() -> Vec { + [ + "attester:secret:bn254:27cb45b1670a1ae8d376a85821d51c7f91ebc6e32788027a84758441aaf0a987", + "attester:secret:bn254:20132edc08a529e927f155e710ae7295a2a0d249f1b1f37726894d1d0d8f0d81", + "attester:secret:bn254:0946901f0a6650284726763b12de5da0f06df0016c8ec2144cf6b1903f1979a6", + ] + .iter() + .map(|raw| Text::new(raw).decode().unwrap()) + .collect() +} + /// Hardcoded committee. -fn committee() -> Committee { - Committee::new(keys().iter().enumerate().map(|(i, key)| WeightedValidator { - key: key.public(), - weight: i as u64 + 10, - })) +fn validator_committee() -> Committee { + Committee::new( + validator_keys() + .iter() + .enumerate() + .map(|(i, key)| WeightedValidator { + key: key.public(), + weight: i as u64 + 10, + }), + ) + .unwrap() +} + +fn attester_committee() -> attester::Committee { + attester::Committee::new( + attester_keys() + .iter() + .enumerate() + .map(|(i, key)| WeightedAttester { + key: key.public(), + weight: i as u64 + 10, + }), + ) .unwrap() } @@ -39,8 +69,8 @@ fn payload() -> Payload { /// Checks that the order of validators in a committee is stable. #[test] fn committee_change_detector() { - let committee = committee(); - let got: Vec = keys() + let committee = validator_committee(); + let got: Vec = validator_keys() .iter() .map(|k| committee.index(&k.public()).unwrap()) .collect(); @@ -61,7 +91,7 @@ fn payload_hash_change_detector() { fn test_sticky() { let ctx = ctx::test_root(&ctx::RealClock); let rng = &mut ctx.rng(); - let committee = committee(); + let committee = validator_committee(); let want = committee .get(rng.gen_range(0..committee.len())) .unwrap() @@ -83,7 +113,7 @@ fn views() -> impl Iterator { /// Checks that leader schedule is stable. #[test] fn roundrobin_change_detector() { - let committee = committee(); + let committee = validator_committee(); let mode = LeaderSelectionMode::RoundRobin; let got: Vec<_> = views() .map(|view| { @@ -97,7 +127,7 @@ fn roundrobin_change_detector() { /// Checks that leader schedule is stable. #[test] fn weighted_change_detector() { - let committee = committee(); + let committee = validator_committee(); let mode = LeaderSelectionMode::Weighted; let got: Vec<_> = views() .map(|view| { @@ -112,14 +142,30 @@ mod version1 { use super::*; /// Hardcoded genesis. - fn genesis() -> Genesis { + fn genesis_empty_attesters() -> Genesis { + GenesisRaw { + chain_id: ChainId(1337), + fork_number: ForkNumber(402598740274745173), + first_block: BlockNumber(8902834932452), + + protocol_version: ProtocolVersion(1), + validators: validator_committee(), + attesters: None, + leader_selection: LeaderSelectionMode::Weighted, + } + .with_hash() + } + + /// Hardcoded genesis. + fn genesis_with_attesters() -> Genesis { GenesisRaw { chain_id: ChainId(1337), fork_number: ForkNumber(402598740274745173), first_block: BlockNumber(8902834932452), protocol_version: ProtocolVersion(1), - committee: committee(), + validators: validator_committee(), + attesters: attester_committee().into(), leader_selection: LeaderSelectionMode::Weighted, } .with_hash() @@ -128,6 +174,7 @@ mod version1 { /// Note that genesis is NOT versioned by ProtocolVersion. /// Even if it was, ALL versions of genesis need to be supported FOREVER, /// unless we introduce dynamic regenesis. + /// FIXME: This fails with the new attester committee. #[test] fn genesis_hash_change_detector() { let want: GenesisHash = Text::new( @@ -135,7 +182,21 @@ mod version1 { ) .decode() .unwrap(); - assert_eq!(want, genesis().hash()); + assert_eq!(want, genesis_empty_attesters().hash()); + } + + /// Note that genesis is NOT versioned by ProtocolVersion. + /// Even if it was, ALL versions of genesis need to be supported FOREVER, + /// unless we introduce dynamic regenesis. + /// FIXME: This fails with the new attester committee. + #[test] + fn genesis_hash_change_detector_2() { + let want: GenesisHash = Text::new( + "genesis_hash:keccak256:63d6562ea2a27069e64a4005d1aef446907db945d85e06323296d2c0f8336c65", + ) + .decode() + .unwrap(); + assert_eq!(want, genesis_with_attesters().hash()); } #[test] @@ -151,7 +212,7 @@ mod version1 { /// valid signature of msg (signed by `keys()[0]`). #[track_caller] fn change_detector(msg: Msg, hash: &str, sig: &str) { - let key = keys()[0].clone(); + let key = validator_keys()[0].clone(); (|| { let hash: MsgHash = Text::new(hash).decode()?; let sig: Signature = Text::new(sig).decode()?; @@ -165,7 +226,7 @@ mod version1 { /// Hardcoded view. fn view() -> View { View { - genesis: genesis().hash(), + genesis: genesis_empty_attesters().hash(), number: ViewNumber(9136573498460759103), } } @@ -188,10 +249,10 @@ mod version1 { /// Hardcoded `CommitQC`. fn commit_qc() -> CommitQC { - let genesis = genesis(); + let genesis = genesis_empty_attesters(); let replica_commit = replica_commit(); let mut x = CommitQC::new(replica_commit.clone(), &genesis); - for k in keys() { + for k in validator_keys() { x.add(&k.sign_msg(replica_commit.clone()), &genesis) .unwrap(); } @@ -217,9 +278,9 @@ mod version1 { /// Hardcoded `PrepareQC`. fn prepare_qc() -> PrepareQC { let mut x = PrepareQC::new(view()); - let genesis = genesis(); + let genesis = genesis_empty_attesters(); let replica_prepare = replica_prepare(); - for k in keys() { + for k in validator_keys() { x.add(&k.sign_msg(replica_prepare.clone()), &genesis) .unwrap(); } diff --git a/node/libs/roles/src/validator/testonly.rs b/node/libs/roles/src/validator/testonly.rs index d6f00480..df7c09c6 100644 --- a/node/libs/roles/src/validator/testonly.rs +++ b/node/libs/roles/src/validator/testonly.rs @@ -1,4 +1,6 @@ //! Test-only utilities. +use crate::attester; + use super::{ AggregateSignature, BlockHeader, BlockNumber, ChainId, CommitQC, Committee, ConsensusMsg, FinalBlock, ForkNumber, Genesis, GenesisHash, GenesisRaw, LeaderCommit, LeaderPrepare, Msg, @@ -25,11 +27,12 @@ pub struct SetupSpec { pub fork_number: ForkNumber, /// First block. pub first_block: BlockNumber, - /// Protocol version. pub protocol_version: ProtocolVersion, /// Validator secret keys and weights. - pub weights: Vec<(SecretKey, u64)>, + pub validator_weights: Vec<(SecretKey, u64)>, + /// Attester secret keys and weights. + pub attester_weights: Vec<(attester::SecretKey, u64)>, /// Leader selection. pub leader_selection: LeaderSelectionMode, } @@ -37,6 +40,7 @@ pub struct SetupSpec { /// Test setup. #[derive(Debug, Clone)] pub struct Setup(SetupInner); + impl SetupSpec { /// New `SetupSpec`. pub fn new(rng: &mut impl Rng, validators: usize) -> Self { @@ -46,7 +50,12 @@ impl SetupSpec { /// New `SetupSpec`. pub fn new_with_weights(rng: &mut impl Rng, weights: Vec) -> Self { Self { - weights: weights.into_iter().map(|w| (rng.gen(), w)).collect(), + validator_weights: weights + .clone() + .into_iter() + .map(|w| (rng.gen(), w)) + .collect(), + attester_weights: weights.into_iter().map(|w| (rng.gen(), w)).collect(), chain_id: ChainId(1337), fork_number: ForkNumber(rng.gen_range(0..100)), first_block: BlockNumber(rng.gen_range(0..100)), @@ -98,7 +107,7 @@ impl Setup { }; let msg = ReplicaCommit { view, proposal }; let mut justification = CommitQC::new(msg, &self.0.genesis); - for key in &self.0.keys { + for key in &self.0.validator_keys { justification .add( &key.sign_msg(justification.message.clone()), @@ -124,6 +133,14 @@ impl Setup { let first = self.0.blocks.first()?.number(); self.0.blocks.get(n.0.checked_sub(first.0)? as usize) } + + /// Pushes a new L1 batch. + pub fn push_batch(&mut self, batch: attester::Batch) { + for key in &self.0.attester_keys { + let signed = key.sign_msg(batch.clone()); + self.0.signed_batches.push(signed); + } + } } impl From for Setup { @@ -135,15 +152,27 @@ impl From for Setup { first_block: spec.first_block, protocol_version: spec.protocol_version, - committee: Committee::new(spec.weights.iter().map(|(k, w)| WeightedValidator { - key: k.public(), - weight: *w, + validators: Committee::new(spec.validator_weights.iter().map(|(k, w)| { + WeightedValidator { + key: k.public(), + weight: *w, + } })) .unwrap(), + attesters: attester::Committee::new(spec.attester_weights.iter().map(|(k, w)| { + attester::WeightedAttester { + key: k.public(), + weight: *w, + } + })) + .unwrap() + .into(), leader_selection: spec.leader_selection, } .with_hash(), - keys: spec.weights.into_iter().map(|(k, _)| k).collect(), + validator_keys: spec.validator_weights.into_iter().map(|(k, _)| k).collect(), + attester_keys: spec.attester_weights.into_iter().map(|(k, _)| k).collect(), + signed_batches: vec![], blocks: vec![], }) } @@ -153,9 +182,13 @@ impl From for Setup { #[derive(Debug, Clone)] pub struct SetupInner { /// Validators' secret keys. - pub keys: Vec, + pub validator_keys: Vec, + /// Attesters' secret keys. + pub attester_keys: Vec, /// Past blocks. pub blocks: Vec, + /// L1 batches + pub signed_batches: Vec>, /// Genesis config. pub genesis: Genesis, } @@ -250,7 +283,8 @@ impl Distribution for Standard { first_block: rng.gen(), protocol_version: rng.gen(), - committee: rng.gen(), + validators: rng.gen(), + attesters: rng.gen(), leader_selection: rng.gen(), } } diff --git a/node/libs/roles/src/validator/tests.rs b/node/libs/roles/src/validator/tests.rs index f65f432b..2f0136f0 100644 --- a/node/libs/roles/src/validator/tests.rs +++ b/node/libs/roles/src/validator/tests.rs @@ -13,12 +13,7 @@ fn test_byte_encoding() { let rng = &mut ctx.rng(); let sk: SecretKey = rng.gen(); - assert_eq!( - sk.public(), - ::decode(&ByteFmt::encode(&sk)) - .unwrap() - .public() - ); + assert_eq!(sk, ByteFmt::decode(&ByteFmt::encode(&sk)).unwrap()); let pk: PublicKey = rng.gen(); assert_eq!(pk, ByteFmt::decode(&ByteFmt::encode(&pk)).unwrap()); @@ -52,10 +47,7 @@ fn test_text_encoding() { let sk: SecretKey = rng.gen(); let t = TextFmt::encode(&sk); - assert_eq!( - sk.public(), - Text::new(&t).decode::().unwrap().public() - ); + assert_eq!(sk, Text::new(&t).decode::().unwrap()); let pk: PublicKey = rng.gen(); let t = TextFmt::encode(&pk); @@ -96,18 +88,18 @@ fn test_schema_encoding() { test_encode_random::(rng); test_encode_random::(rng); test_encode_random::(rng); - test_encode_random::(rng); test_encode_random::(rng); + test_encode_random::(rng); test_encode_random::(rng); test_encode_random::(rng); } #[test] -fn test_genesis_schema_decode() { +fn test_genesis_verify() { let ctx = ctx::test_root(&ctx::RealClock); let rng = &mut ctx.rng(); - let genesis = rng.gen::(); + let genesis = Setup::new(rng, 1).genesis.clone(); assert!(genesis.verify().is_ok()); assert!(Genesis::read(&genesis.build()).is_ok()); @@ -189,7 +181,7 @@ fn make_replica_commit(rng: &mut impl Rng, view: ViewNumber, setup: &Setup) -> R fn make_commit_qc(rng: &mut impl Rng, view: ViewNumber, setup: &Setup) -> CommitQC { let mut qc = CommitQC::new(make_replica_commit(rng, view, setup), &setup.genesis); - for key in &setup.keys { + for key in &setup.validator_keys { qc.add(&key.sign_msg(qc.message.clone()), &setup.genesis) .unwrap(); } @@ -220,19 +212,25 @@ fn test_commit_qc() { let setup1 = Setup::new(rng, 6); let setup2 = Setup::new(rng, 6); let mut genesis3 = (*setup1.genesis).clone(); - genesis3.committee = Committee::new(setup1.genesis.committee.iter().take(3).cloned()).unwrap(); + genesis3.validators = + Committee::new(setup1.genesis.validators.iter().take(3).cloned()).unwrap(); let genesis3 = genesis3.with_hash(); - let validator_weight = setup1.genesis.committee.total_weight() / 6; - for i in 0..setup1.keys.len() + 1 { + for i in 0..setup1.validator_keys.len() + 1 { let view = rng.gen(); let mut qc = CommitQC::new(make_replica_commit(rng, view, &setup1), &setup1.genesis); - for key in &setup1.keys[0..i] { + for key in &setup1.validator_keys[0..i] { qc.add(&key.sign_msg(qc.message.clone()), &setup1.genesis) .unwrap(); } - let expected_weight = i as u64 * validator_weight; - if expected_weight >= setup1.genesis.committee.threshold() { + let expected_weight: u64 = setup1 + .genesis + .validators + .iter() + .take(i) + .map(|w| w.weight) + .sum(); + if expected_weight >= setup1.genesis.validators.threshold() { qc.verify(&setup1.genesis).unwrap(); } else { assert_matches!( @@ -258,7 +256,10 @@ fn test_commit_qc_add_errors() { let msg = qc.message.clone(); // Add the message assert_matches!( - qc.add(&setup.keys[0].sign_msg(msg.clone()), &setup.genesis), + qc.add( + &setup.validator_keys[0].sign_msg(msg.clone()), + &setup.genesis + ), Ok(()) ); @@ -266,7 +267,7 @@ fn test_commit_qc_add_errors() { let mut msg1 = msg.clone(); msg1.view.number = view.next(); assert_matches!( - qc.add(&setup.keys[0].sign_msg(msg1), &setup.genesis), + qc.add(&setup.validator_keys[0].sign_msg(msg1), &setup.genesis), Err(Error::InconsistentMessages { .. }) ); @@ -281,12 +282,18 @@ fn test_commit_qc_add_errors() { // Try to add the same message already added by same validator assert_matches!( - qc.add(&setup.keys[0].sign_msg(msg.clone()), &setup.genesis), + qc.add( + &setup.validator_keys[0].sign_msg(msg.clone()), + &setup.genesis + ), Err(Error::Exists { .. }) ); // Add same message signed by another validator. - assert_matches!(qc.add(&setup.keys[1].sign_msg(msg), &setup.genesis), Ok(())); + assert_matches!( + qc.add(&setup.validator_keys[1].sign_msg(msg), &setup.genesis), + Ok(()) + ); } #[test] @@ -299,7 +306,8 @@ fn test_prepare_qc() { let setup1 = Setup::new(rng, 6); let setup2 = Setup::new(rng, 6); let mut genesis3 = (*setup1.genesis).clone(); - genesis3.committee = Committee::new(setup1.genesis.committee.iter().take(3).cloned()).unwrap(); + genesis3.validators = + Committee::new(setup1.genesis.validators.iter().take(3).cloned()).unwrap(); let genesis3 = genesis3.with_hash(); let view: ViewNumber = rng.gen(); @@ -307,17 +315,23 @@ fn test_prepare_qc() { .map(|_| make_replica_prepare(rng, view, &setup1)) .collect(); - for n in 0..setup1.keys.len() + 1 { + for n in 0..setup1.validator_keys.len() + 1 { let mut qc = PrepareQC::new(msgs[0].view.clone()); - for key in &setup1.keys[0..n] { + for key in &setup1.validator_keys[0..n] { qc.add( &key.sign_msg(msgs.choose(rng).unwrap().clone()), &setup1.genesis, ) .unwrap(); } - let expected_weight = n as u64 * setup1.genesis.committee.total_weight() / 6; - if expected_weight >= setup1.genesis.committee.threshold() { + let expected_weight: u64 = setup1 + .genesis + .validators + .iter() + .take(n) + .map(|w| w.weight) + .sum(); + if expected_weight >= setup1.genesis.validators.threshold() { qc.verify(&setup1.genesis).unwrap(); } else { assert_matches!( @@ -345,7 +359,10 @@ fn test_prepare_qc_add_errors() { // Add the message assert_matches!( - qc.add(&setup.keys[0].sign_msg(msg.clone()), &setup.genesis), + qc.add( + &setup.validator_keys[0].sign_msg(msg.clone()), + &setup.genesis + ), Ok(()) ); @@ -353,7 +370,7 @@ fn test_prepare_qc_add_errors() { let mut msg1 = msg.clone(); msg1.view.number = view.next(); assert_matches!( - qc.add(&setup.keys[0].sign_msg(msg1), &setup.genesis), + qc.add(&setup.validator_keys[0].sign_msg(msg1), &setup.genesis), Err(Error::InconsistentViews { .. }) ); @@ -368,20 +385,26 @@ fn test_prepare_qc_add_errors() { // Try to add the same message already added by same validator assert_matches!( - qc.add(&setup.keys[0].sign_msg(msg.clone()), &setup.genesis), + qc.add( + &setup.validator_keys[0].sign_msg(msg.clone()), + &setup.genesis + ), Err(Error::Exists { .. }) ); // Try to add a message for a validator that already added another message let msg2 = make_replica_prepare(rng, view, &setup); assert_matches!( - qc.add(&setup.keys[0].sign_msg(msg2), &setup.genesis), + qc.add(&setup.validator_keys[0].sign_msg(msg2), &setup.genesis), Err(Error::Exists { .. }) ); // Add same message signed by another validator. assert_matches!( - qc.add(&setup.keys[1].sign_msg(msg.clone()), &setup.genesis), + qc.add( + &setup.validator_keys[1].sign_msg(msg.clone()), + &setup.genesis + ), Ok(()) ); } @@ -400,10 +423,10 @@ fn test_validator_committee_weights() { let msg = make_replica_prepare(rng, view, &setup); let mut qc = PrepareQC::new(msg.view.clone()); for (n, weight) in sums.iter().enumerate() { - let key = &setup.keys[n]; + let key = &setup.validator_keys[n]; qc.add(&key.sign_msg(msg.clone()), &setup.genesis).unwrap(); let signers = &qc.map[&msg]; - assert_eq!(setup.genesis.committee.weight(signers), *weight); + assert_eq!(setup.genesis.validators.weight(signers), *weight); } } diff --git a/node/libs/storage/src/batch_store.rs b/node/libs/storage/src/batch_store.rs new file mode 100644 index 00000000..f885f82d --- /dev/null +++ b/node/libs/storage/src/batch_store.rs @@ -0,0 +1,16 @@ +//! Defines storage layer for batches of blocks. +use zksync_consensus_roles::attester; + +/// Trait for the shared state of batches between the consensus and the execution layer. +pub trait PersistentBatchStore { + /// Get the L1 batch from storage with the highest number. + fn last_batch(&self) -> attester::BatchNumber; + /// Get the L1 batch QC from storage with the highest number. + fn last_batch_qc(&self) -> attester::BatchNumber; + /// Returns the batch with the given number. + fn get_batch(&self, number: attester::BatchNumber) -> Option; + /// Returns the QC of the batch with the given number. + fn get_batch_qc(&self, number: attester::BatchNumber) -> Option; + /// Store the given QC in the storage. + fn store_qc(&self, qc: attester::BatchQC); +} diff --git a/node/libs/storage/src/lib.rs b/node/libs/storage/src/lib.rs index ee017752..33497156 100644 --- a/node/libs/storage/src/lib.rs +++ b/node/libs/storage/src/lib.rs @@ -1,5 +1,6 @@ //! Abstraction for persistent data storage. //! It provides schema-aware type-safe database access. +mod batch_store; mod block_store; pub mod proto; mod replica_store; @@ -8,6 +9,7 @@ pub mod testonly; mod tests; pub use crate::{ + batch_store::PersistentBatchStore, block_store::{BlockStore, BlockStoreRunner, BlockStoreState, PersistentBlockStore}, replica_store::{Proposal, ReplicaState, ReplicaStore}, }; diff --git a/node/tools/src/bin/deployer.rs b/node/tools/src/bin/deployer.rs index 49bea272..609f2628 100644 --- a/node/tools/src/bin/deployer.rs +++ b/node/tools/src/bin/deployer.rs @@ -29,7 +29,7 @@ fn generate_consensus_nodes(nodes: usize, seed_nodes_amount: Option) -> V let rng = &mut rand::thread_rng(); let setup = validator::testonly::Setup::new(rng, nodes); - let validator_keys = setup.keys.clone(); + let validator_keys = setup.validator_keys.clone(); // Each node will have `gossip_peers` outbound peers. let peers = 2; diff --git a/node/tools/src/bin/localnet_config.rs b/node/tools/src/bin/localnet_config.rs index 38a22bc9..18d3c00e 100644 --- a/node/tools/src/bin/localnet_config.rs +++ b/node/tools/src/bin/localnet_config.rs @@ -58,7 +58,7 @@ fn main() -> anyhow::Result<()> { let rng = &mut rand::thread_rng(); let setup = validator::testonly::Setup::new(rng, validator_count); - let validator_keys = setup.keys.clone(); + let validator_keys = setup.validator_keys.clone(); // Each node will have `gossip_peers` outbound peers. let nodes = addrs.len(); diff --git a/node/tools/src/config.rs b/node/tools/src/config.rs index 498916c8..cd199a2c 100644 --- a/node/tools/src/config.rs +++ b/node/tools/src/config.rs @@ -133,7 +133,6 @@ impl ProtoFmt for AppConfig { // TODO: read secret. validator_key: read_optional_secret_text(&r.validator_secret_key) .context("validator_secret_key")?, - node_key: read_required_secret_text(&r.node_secret_key).context("node_secret_key")?, gossip_dynamic_inbound_limit: required(&r.gossip_dynamic_inbound_limit) .and_then(|x| Ok((*x).try_into()?)) diff --git a/node/tools/src/tests.rs b/node/tools/src/tests.rs index a3bccaeb..79f6a56c 100644 --- a/node/tools/src/tests.rs +++ b/node/tools/src/tests.rs @@ -12,9 +12,9 @@ impl Distribution for EncodeDist { let mut genesis: validator::GenesisRaw = rng.gen(); // In order for the genesis to be valid, the sticky leader needs to be in the validator committee. if let LeaderSelectionMode::Sticky(_) = genesis.leader_selection { - let i = rng.gen_range(0..genesis.committee.len()); + let i = rng.gen_range(0..genesis.validators.len()); genesis.leader_selection = - LeaderSelectionMode::Sticky(genesis.committee.get(i).unwrap().key.clone()); + LeaderSelectionMode::Sticky(genesis.validators.get(i).unwrap().key.clone()); } AppConfig { server_addr: self.sample(rng),