Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ZKS-02] Introduce Committee IDs #2374

Merged
merged 8 commits into from Mar 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions ledger/block/src/verify.rs
Expand Up @@ -233,6 +233,29 @@ impl<N: Network> Block<N> {
Authority::Quorum(subdag) => subdag.timestamp(previous_committee_lookback),
};

// Check that the committee IDs are correct.
if let Authority::Quorum(subdag) = &self.authority {
// Check that the committee ID of the leader certificate is correct.
ensure!(
subdag.leader_certificate().committee_id() == current_committee_lookback.id(),
"Leader certificate has an incorrect committee ID"
);

// Check that all all certificates on each round have the same committee ID.
cfg_iter!(subdag).try_for_each(|(round, certificates)| {
// Check that every certificate for a given round shares the same committee ID.
let expected_committee_id = certificates
.first()
.map(|certificate| certificate.committee_id())
.ok_or(anyhow!("No certificates found for subdag round {round}"))?;
ensure!(
certificates.iter().skip(1).all(|certificate| certificate.committee_id() == expected_committee_id),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK so I think that makes sense to me, most of the DAG will likely be the same committee id, but any vertex that's on the same round (or previous round) as the last committed anchor will have a different committee id

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the intent is to reject those out of date cases during the proposal phase.

Do you see any concern that an out of date vertex could get 2f+1 signatures, be included in block production here, and cause a halting concern?

"Certificates on round {round} do not all have the same committee ID",
);
Ok(())
})?;
}

// Return success.
Ok((
expected_round,
Expand Down
8 changes: 8 additions & 0 deletions ledger/committee/src/bytes.rs
Expand Up @@ -24,6 +24,8 @@ impl<N: Network> FromBytes for Committee<N> {
return Err(error("Invalid committee version"));
}

// Read the committee ID.
let id = Field::read_le(&mut reader)?;
// Read the starting round.
let starting_round = u64::read_le(&mut reader)?;
// Read the number of members.
Expand Down Expand Up @@ -51,6 +53,10 @@ impl<N: Network> FromBytes for Committee<N> {
let total_stake = u64::read_le(&mut reader)?;
// Construct the committee.
let committee = Self::new(starting_round, members).map_err(|e| error(e.to_string()))?;
// Ensure the committee ID matches.
if committee.id() != id {
return Err(error("Invalid committee ID during deserialization"));
}
// Ensure the total stake matches.
match committee.total_stake() == total_stake {
true => Ok(committee),
Expand All @@ -64,6 +70,8 @@ impl<N: Network> ToBytes for Committee<N> {
fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
// Write the version.
1u8.write_le(&mut writer)?;
// Write the committee ID.
self.id().write_le(&mut writer)?;
// Write the starting round.
self.starting_round.write_le(&mut writer)?;
// Write the number of members.
Expand Down
12 changes: 11 additions & 1 deletion ledger/committee/src/lib.rs
Expand Up @@ -18,6 +18,7 @@
mod bytes;
mod serialize;
mod string;
mod to_id;

#[cfg(any(test, feature = "prop-tests"))]
pub mod prop_tests;
Expand All @@ -41,6 +42,8 @@ pub const MAX_DELEGATORS: u32 = 100_000u32;

#[derive(Clone, PartialEq, Eq)]
pub struct Committee<N: Network> {
/// The committee ID, defined as the hash of the starting round, members, and total stake.
id: Field<N>,
/// The starting round number for this committee.
starting_round: u64,
/// A map of `address` to `(stake, is_open)` state.
Expand Down Expand Up @@ -78,14 +81,21 @@ impl<N: Network> Committee<N> {
);
// Compute the total stake of the committee for this round.
let total_stake = Self::compute_total_stake(&members)?;
// Compute the committee ID.
let id = Self::compute_committee_id(starting_round, &members, total_stake)?;
#[cfg(feature = "metrics")]
metrics::gauge(metrics::committee::TOTAL_STAKE, total_stake as f64);
// Return the new committee.
Ok(Self { starting_round, members, total_stake })
Ok(Self { id, starting_round, members, total_stake })
}
}

impl<N: Network> Committee<N> {
/// Returns the committee ID.
pub const fn id(&self) -> Field<N> {
self.id
}

/// Returns the starting round number for this committee.
pub const fn starting_round(&self) -> u64 {
self.starting_round
Expand Down
8 changes: 7 additions & 1 deletion ledger/committee/src/serialize.rs
Expand Up @@ -19,7 +19,8 @@ impl<N: Network> Serialize for Committee<N> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match serializer.is_human_readable() {
true => {
let mut certificate = serializer.serialize_struct("Committee", 3)?;
let mut certificate = serializer.serialize_struct("Committee", 4)?;
certificate.serialize_field("id", &self.id)?;
certificate.serialize_field("starting_round", &self.starting_round)?;
certificate.serialize_field("members", &self.members)?;
certificate.serialize_field("total_stake", &self.total_stake)?;
Expand All @@ -36,12 +37,17 @@ impl<'de, N: Network> Deserialize<'de> for Committee<N> {
match deserializer.is_human_readable() {
true => {
let mut value = serde_json::Value::deserialize(deserializer)?;
let id: Field<N> = DeserializeExt::take_from_value::<D>(&mut value, "id")?;
let total_stake: u64 = DeserializeExt::take_from_value::<D>(&mut value, "total_stake")?;
let committee = Self::new(
DeserializeExt::take_from_value::<D>(&mut value, "starting_round")?,
DeserializeExt::take_from_value::<D>(&mut value, "members")?,
)
.map_err(de::Error::custom)?;

if committee.id != id {
return Err(de::Error::custom("committee ID mismatch"));
}
match committee.total_stake == total_stake {
true => Ok(committee),
false => Err(de::Error::custom("total stake mismatch")),
Expand Down
50 changes: 50 additions & 0 deletions ledger/committee/src/to_id.rs
@@ -0,0 +1,50 @@
// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the snarkVM library.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use super::*;

impl<N: Network> Committee<N> {
/// Returns the committee ID.
pub fn to_id(&self) -> Result<Field<N>> {
Self::compute_committee_id(self.starting_round, &self.members, self.total_stake)
}
}

impl<N: Network> Committee<N> {
/// Returns the commmitee ID.
pub fn compute_committee_id(
starting_round: u64,
members: &IndexMap<Address<N>, (u64, bool)>,
total_stake: u64,
) -> Result<Field<N>> {
let mut preimage = Vec::new();
// Insert the starting_round.
starting_round.write_le(&mut preimage)?;
// Write the number of members.
u16::try_from(members.len())?.write_le(&mut preimage)?;
// Write the members.
for (address, (stake, is_open)) in members {
// Write the address.
address.write_le(&mut preimage)?;
// Write the stake.
stake.write_le(&mut preimage)?;
// Write the is_open flag.
is_open.write_le(&mut preimage)?;
}
// Insert the total stake.
total_stake.write_le(&mut preimage)?;
// Hash the preimage.
N::hash_bhp1024(&preimage.to_bits_le())
}
}
15 changes: 10 additions & 5 deletions ledger/narwhal/batch-certificate/src/lib.rs
Expand Up @@ -121,6 +121,16 @@ impl<N: Network> BatchCertificate<N> {
self.batch_header().round()
}

/// Returns the timestamp of the batch header.
pub fn timestamp(&self) -> i64 {
self.batch_header().timestamp()
}

/// Returns the committee ID.
pub const fn committee_id(&self) -> Field<N> {
self.batch_header().committee_id()
}

/// Returns the transmission IDs.
pub const fn transmission_ids(&self) -> &IndexSet<TransmissionID<N>> {
self.batch_header().transmission_ids()
Expand All @@ -131,11 +141,6 @@ impl<N: Network> BatchCertificate<N> {
self.batch_header().previous_certificate_ids()
}

/// Returns the timestamp of the batch header.
pub fn timestamp(&self) -> i64 {
self.batch_header().timestamp()
}

/// Returns the signatures of the batch ID from the committee.
pub fn signatures(&self) -> Box<dyn '_ + ExactSizeIterator<Item = &Signature<N>>> {
Box::new(self.signatures.iter())
Expand Down
9 changes: 7 additions & 2 deletions ledger/narwhal/batch-header/src/bytes.rs
Expand Up @@ -32,6 +32,8 @@ impl<N: Network> FromBytes for BatchHeader<N> {
let round = u64::read_le(&mut reader)?;
// Read the timestamp.
let timestamp = i64::read_le(&mut reader)?;
// Read the committee ID.
let committee_id = Field::read_le(&mut reader)?;

// Read the number of transmission IDs.
let num_transmission_ids = u32::read_le(&mut reader)?;
Expand Down Expand Up @@ -69,8 +71,9 @@ impl<N: Network> FromBytes for BatchHeader<N> {
let signature = Signature::read_le(&mut reader)?;

// Construct the batch.
let batch = Self::from(author, round, timestamp, transmission_ids, previous_certificate_ids, signature)
.map_err(error)?;
let batch =
Self::from(author, round, timestamp, committee_id, transmission_ids, previous_certificate_ids, signature)
.map_err(error)?;

// Return the batch.
match batch.batch_id == batch_id {
Expand All @@ -93,6 +96,8 @@ impl<N: Network> ToBytes for BatchHeader<N> {
self.round.write_le(&mut writer)?;
// Write the timestamp.
self.timestamp.write_le(&mut writer)?;
// Write the committee ID.
self.committee_id.write_le(&mut writer)?;
// Write the number of transmission IDs.
u32::try_from(self.transmission_ids.len()).map_err(|e| error(e.to_string()))?.write_le(&mut writer)?;
// Write the transmission IDs.
Expand Down
56 changes: 50 additions & 6 deletions ledger/narwhal/batch-header/src/lib.rs
Expand Up @@ -32,14 +32,16 @@ use narwhal_transmission_id::TransmissionID;
#[derive(Clone, PartialEq, Eq)]
pub struct BatchHeader<N: Network> {
/// The batch ID, defined as the hash of the author, round number, timestamp, transmission IDs,
/// previous batch certificate IDs, and last election certificate IDs.
/// committee ID, previous batch certificate IDs, and last election certificate IDs.
batch_id: Field<N>,
/// The author of the batch.
author: Address<N>,
/// The round number.
round: u64,
/// The timestamp.
timestamp: i64,
/// The committee ID.
committee_id: Field<N>,
/// The set of `transmission IDs`.
transmission_ids: IndexSet<TransmissionID<N>>,
/// The batch certificate IDs of the previous round.
Expand All @@ -66,6 +68,7 @@ impl<N: Network> BatchHeader<N> {
private_key: &PrivateKey<N>,
round: u64,
timestamp: i64,
committee_id: Field<N>,
transmission_ids: IndexSet<TransmissionID<N>>,
previous_certificate_ids: IndexSet<Field<N>>,
rng: &mut R,
Expand Down Expand Up @@ -95,18 +98,35 @@ impl<N: Network> BatchHeader<N> {
// Retrieve the address.
let author = Address::try_from(private_key)?;
// Compute the batch ID.
let batch_id = Self::compute_batch_id(author, round, timestamp, &transmission_ids, &previous_certificate_ids)?;
let batch_id = Self::compute_batch_id(
author,
round,
timestamp,
committee_id,
&transmission_ids,
&previous_certificate_ids,
)?;
// Sign the preimage.
let signature = private_key.sign(&[batch_id], rng)?;
// Return the batch header.
Ok(Self { author, batch_id, round, timestamp, transmission_ids, previous_certificate_ids, signature })
Ok(Self {
batch_id,
author,
round,
timestamp,
committee_id,
transmission_ids,
previous_certificate_ids,
signature,
})
}

/// Initializes a new batch header.
pub fn from(
author: Address<N>,
round: u64,
timestamp: i64,
committee_id: Field<N>,
transmission_ids: IndexSet<TransmissionID<N>>,
previous_certificate_ids: IndexSet<Field<N>>,
signature: Signature<N>,
Expand Down Expand Up @@ -134,13 +154,29 @@ impl<N: Network> BatchHeader<N> {
);

// Compute the batch ID.
let batch_id = Self::compute_batch_id(author, round, timestamp, &transmission_ids, &previous_certificate_ids)?;
let batch_id = Self::compute_batch_id(
author,
round,
timestamp,
committee_id,
&transmission_ids,
&previous_certificate_ids,
)?;
// Verify the signature.
if !signature.verify(&author, &[batch_id]) {
bail!("Invalid signature for the batch header");
}
// Return the batch header.
Ok(Self { author, batch_id, round, timestamp, transmission_ids, previous_certificate_ids, signature })
Ok(Self {
author,
batch_id,
round,
timestamp,
committee_id,
transmission_ids,
previous_certificate_ids,
signature,
})
}
}

Expand All @@ -165,6 +201,11 @@ impl<N: Network> BatchHeader<N> {
self.timestamp
}

/// Returns the committee ID.
pub const fn committee_id(&self) -> Field<N> {
self.committee_id
}

/// Returns the transmission IDs.
pub const fn transmission_ids(&self) -> &IndexSet<TransmissionID<N>> {
&self.transmission_ids
Expand Down Expand Up @@ -228,13 +269,16 @@ pub mod test_helpers {
) -> BatchHeader<CurrentNetwork> {
// Sample a private key.
let private_key = PrivateKey::new(rng).unwrap();
// Sample the committee ID.
let committee_id = Field::<CurrentNetwork>::rand(rng);
// Sample transmission IDs.
let transmission_ids =
narwhal_transmission_id::test_helpers::sample_transmission_ids(rng).into_iter().collect::<IndexSet<_>>();
// Checkpoint the timestamp for the batch.
let timestamp = OffsetDateTime::now_utc().unix_timestamp();
// Return the batch header.
BatchHeader::new(&private_key, round, timestamp, transmission_ids, previous_certificate_ids, rng).unwrap()
BatchHeader::new(&private_key, round, timestamp, committee_id, transmission_ids, previous_certificate_ids, rng)
.unwrap()
}

/// Returns a list of sample batch headers, sampled at random.
Expand Down
4 changes: 3 additions & 1 deletion ledger/narwhal/batch-header/src/serialize.rs
Expand Up @@ -19,11 +19,12 @@ impl<N: Network> Serialize for BatchHeader<N> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match serializer.is_human_readable() {
true => {
let mut header = serializer.serialize_struct("BatchHeader", 7)?;
let mut header = serializer.serialize_struct("BatchHeader", 8)?;
header.serialize_field("batch_id", &self.batch_id)?;
header.serialize_field("author", &self.author)?;
header.serialize_field("round", &self.round)?;
header.serialize_field("timestamp", &self.timestamp)?;
header.serialize_field("committee_id", &self.committee_id)?;
header.serialize_field("transmission_ids", &self.transmission_ids)?;
header.serialize_field("previous_certificate_ids", &self.previous_certificate_ids)?;
header.serialize_field("signature", &self.signature)?;
Expand All @@ -47,6 +48,7 @@ impl<'de, N: Network> Deserialize<'de> for BatchHeader<N> {
DeserializeExt::take_from_value::<D>(&mut header, "author")?,
DeserializeExt::take_from_value::<D>(&mut header, "round")?,
DeserializeExt::take_from_value::<D>(&mut header, "timestamp")?,
DeserializeExt::take_from_value::<D>(&mut header, "committee_id")?,
DeserializeExt::take_from_value::<D>(&mut header, "transmission_ids")?,
DeserializeExt::take_from_value::<D>(&mut header, "previous_certificate_ids")?,
DeserializeExt::take_from_value::<D>(&mut header, "signature")?,
Expand Down