diff --git a/ledger/block/src/verify.rs b/ledger/block/src/verify.rs index b7afba54df..74a3e86e2e 100644 --- a/ledger/block/src/verify.rs +++ b/ledger/block/src/verify.rs @@ -233,6 +233,29 @@ impl Block { 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), + "Certificates on round {round} do not all have the same committee ID", + ); + Ok(()) + })?; + } + // Return success. Ok(( expected_round, diff --git a/ledger/committee/src/bytes.rs b/ledger/committee/src/bytes.rs index 2c3eba9771..52910b99e9 100644 --- a/ledger/committee/src/bytes.rs +++ b/ledger/committee/src/bytes.rs @@ -24,6 +24,8 @@ impl FromBytes for Committee { 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. @@ -51,6 +53,10 @@ impl FromBytes for Committee { 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), @@ -64,6 +70,8 @@ impl ToBytes for Committee { fn write_le(&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. diff --git a/ledger/committee/src/lib.rs b/ledger/committee/src/lib.rs index e198b08e71..37940efc16 100644 --- a/ledger/committee/src/lib.rs +++ b/ledger/committee/src/lib.rs @@ -18,6 +18,7 @@ mod bytes; mod serialize; mod string; +mod to_id; #[cfg(any(test, feature = "prop-tests"))] pub mod prop_tests; @@ -41,6 +42,8 @@ pub const MAX_DELEGATORS: u32 = 100_000u32; #[derive(Clone, PartialEq, Eq)] pub struct Committee { + /// The committee ID, defined as the hash of the starting round, members, and total stake. + id: Field, /// The starting round number for this committee. starting_round: u64, /// A map of `address` to `(stake, is_open)` state. @@ -78,14 +81,21 @@ impl Committee { ); // 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 Committee { + /// Returns the committee ID. + pub const fn id(&self) -> Field { + self.id + } + /// Returns the starting round number for this committee. pub const fn starting_round(&self) -> u64 { self.starting_round diff --git a/ledger/committee/src/serialize.rs b/ledger/committee/src/serialize.rs index df121bd75a..3964cf7597 100644 --- a/ledger/committee/src/serialize.rs +++ b/ledger/committee/src/serialize.rs @@ -19,7 +19,8 @@ impl Serialize for Committee { fn serialize(&self, serializer: S) -> Result { 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)?; @@ -36,12 +37,17 @@ impl<'de, N: Network> Deserialize<'de> for Committee { match deserializer.is_human_readable() { true => { let mut value = serde_json::Value::deserialize(deserializer)?; + let id: Field = DeserializeExt::take_from_value::(&mut value, "id")?; let total_stake: u64 = DeserializeExt::take_from_value::(&mut value, "total_stake")?; let committee = Self::new( DeserializeExt::take_from_value::(&mut value, "starting_round")?, DeserializeExt::take_from_value::(&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")), diff --git a/ledger/committee/src/to_id.rs b/ledger/committee/src/to_id.rs new file mode 100644 index 0000000000..b0a57335d7 --- /dev/null +++ b/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 Committee { + /// Returns the committee ID. + pub fn to_id(&self) -> Result> { + Self::compute_committee_id(self.starting_round, &self.members, self.total_stake) + } +} + +impl Committee { + /// Returns the commmitee ID. + pub fn compute_committee_id( + starting_round: u64, + members: &IndexMap, (u64, bool)>, + total_stake: u64, + ) -> Result> { + 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()) + } +} diff --git a/ledger/narwhal/batch-certificate/src/lib.rs b/ledger/narwhal/batch-certificate/src/lib.rs index 292bed5366..a99166c383 100644 --- a/ledger/narwhal/batch-certificate/src/lib.rs +++ b/ledger/narwhal/batch-certificate/src/lib.rs @@ -121,6 +121,16 @@ impl BatchCertificate { 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 { + self.batch_header().committee_id() + } + /// Returns the transmission IDs. pub const fn transmission_ids(&self) -> &IndexSet> { self.batch_header().transmission_ids() @@ -131,11 +141,6 @@ impl BatchCertificate { 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>> { Box::new(self.signatures.iter()) diff --git a/ledger/narwhal/batch-header/src/bytes.rs b/ledger/narwhal/batch-header/src/bytes.rs index 37474eb118..ceb455192e 100644 --- a/ledger/narwhal/batch-header/src/bytes.rs +++ b/ledger/narwhal/batch-header/src/bytes.rs @@ -32,6 +32,8 @@ impl FromBytes for BatchHeader { 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)?; @@ -69,8 +71,9 @@ impl FromBytes for BatchHeader { 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 { @@ -93,6 +96,8 @@ impl ToBytes for BatchHeader { 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. diff --git a/ledger/narwhal/batch-header/src/lib.rs b/ledger/narwhal/batch-header/src/lib.rs index d50ea0f7f3..ce79fc6c6a 100644 --- a/ledger/narwhal/batch-header/src/lib.rs +++ b/ledger/narwhal/batch-header/src/lib.rs @@ -32,7 +32,7 @@ use narwhal_transmission_id::TransmissionID; #[derive(Clone, PartialEq, Eq)] pub struct BatchHeader { /// 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, /// The author of the batch. author: Address, @@ -40,6 +40,8 @@ pub struct BatchHeader { round: u64, /// The timestamp. timestamp: i64, + /// The committee ID. + committee_id: Field, /// The set of `transmission IDs`. transmission_ids: IndexSet>, /// The batch certificate IDs of the previous round. @@ -66,6 +68,7 @@ impl BatchHeader { private_key: &PrivateKey, round: u64, timestamp: i64, + committee_id: Field, transmission_ids: IndexSet>, previous_certificate_ids: IndexSet>, rng: &mut R, @@ -95,11 +98,27 @@ impl BatchHeader { // 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. @@ -107,6 +126,7 @@ impl BatchHeader { author: Address, round: u64, timestamp: i64, + committee_id: Field, transmission_ids: IndexSet>, previous_certificate_ids: IndexSet>, signature: Signature, @@ -134,13 +154,29 @@ impl BatchHeader { ); // 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, + }) } } @@ -165,6 +201,11 @@ impl BatchHeader { self.timestamp } + /// Returns the committee ID. + pub const fn committee_id(&self) -> Field { + self.committee_id + } + /// Returns the transmission IDs. pub const fn transmission_ids(&self) -> &IndexSet> { &self.transmission_ids @@ -228,13 +269,16 @@ pub mod test_helpers { ) -> BatchHeader { // Sample a private key. let private_key = PrivateKey::new(rng).unwrap(); + // Sample the committee ID. + let committee_id = Field::::rand(rng); // Sample transmission IDs. let transmission_ids = narwhal_transmission_id::test_helpers::sample_transmission_ids(rng).into_iter().collect::>(); // 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. diff --git a/ledger/narwhal/batch-header/src/serialize.rs b/ledger/narwhal/batch-header/src/serialize.rs index 6acddaead2..6e86f014c8 100644 --- a/ledger/narwhal/batch-header/src/serialize.rs +++ b/ledger/narwhal/batch-header/src/serialize.rs @@ -19,11 +19,12 @@ impl Serialize for BatchHeader { fn serialize(&self, serializer: S) -> Result { 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)?; @@ -47,6 +48,7 @@ impl<'de, N: Network> Deserialize<'de> for BatchHeader { DeserializeExt::take_from_value::(&mut header, "author")?, DeserializeExt::take_from_value::(&mut header, "round")?, DeserializeExt::take_from_value::(&mut header, "timestamp")?, + DeserializeExt::take_from_value::(&mut header, "committee_id")?, DeserializeExt::take_from_value::(&mut header, "transmission_ids")?, DeserializeExt::take_from_value::(&mut header, "previous_certificate_ids")?, DeserializeExt::take_from_value::(&mut header, "signature")?, diff --git a/ledger/narwhal/batch-header/src/to_id.rs b/ledger/narwhal/batch-header/src/to_id.rs index 1d99cbda77..c783d2e4ff 100644 --- a/ledger/narwhal/batch-header/src/to_id.rs +++ b/ledger/narwhal/batch-header/src/to_id.rs @@ -21,6 +21,7 @@ impl BatchHeader { self.author, self.round, self.timestamp, + self.committee_id, &self.transmission_ids, &self.previous_certificate_ids, ) @@ -33,6 +34,7 @@ impl BatchHeader { author: Address, round: u64, timestamp: i64, + committee_id: Field, transmission_ids: &IndexSet>, previous_certificate_ids: &IndexSet>, ) -> Result> { @@ -43,6 +45,8 @@ impl BatchHeader { round.write_le(&mut preimage)?; // Insert the timestamp. timestamp.write_le(&mut preimage)?; + // Insert the committee ID. + committee_id.write_le(&mut preimage)?; // Insert the number of transmissions. u32::try_from(transmission_ids.len())?.write_le(&mut preimage)?; // Insert the transmission IDs. diff --git a/parameters/src/mainnet/genesis.rs b/parameters/src/mainnet/genesis.rs index c89d73e889..4aa500db6e 100644 --- a/parameters/src/mainnet/genesis.rs +++ b/parameters/src/mainnet/genesis.rs @@ -27,6 +27,6 @@ mod tests { #[test] fn test_genesis_block() { let bytes = GenesisBytes::load_bytes(); - assert_eq!(15133, bytes.len() as u64, "Update me if serialization has changed"); + assert_eq!(15165, bytes.len() as u64, "Update me if serialization has changed"); } } diff --git a/parameters/src/mainnet/resources/block.genesis b/parameters/src/mainnet/resources/block.genesis index 8f1f7992e2..f7853a6f5b 100644 Binary files a/parameters/src/mainnet/resources/block.genesis and b/parameters/src/mainnet/resources/block.genesis differ diff --git a/synthesizer/tests/expectations/vm/execute_and_finalize/test_rand.out b/synthesizer/tests/expectations/vm/execute_and_finalize/test_rand.out index 43b2ae5b12..eb372e7191 100644 --- a/synthesizer/tests/expectations/vm/execute_and_finalize/test_rand.out +++ b/synthesizer/tests/expectations/vm/execute_and_finalize/test_rand.out @@ -19,14 +19,14 @@ outputs: test_rand.aleo/rand_chacha_check: outputs: - '{"type":"future","id":"5655280628674362392666464396476127281186411758739892961608160465159658100070field","value":"{\n program_id: test_rand.aleo,\n function_name: rand_chacha_check,\n arguments: [\n 0field,\n false\n ]\n}"}' - speculate: the execution was accepted + speculate: the execution was rejected add_next_block: succeeded. - verified: true execute: test_rand.aleo/rand_chacha_check: outputs: - '{"type":"future","id":"4722955375376605065432422068493514420613444220803270858286854080465504837613field","value":"{\n program_id: test_rand.aleo,\n function_name: rand_chacha_check,\n arguments: [\n 1field,\n true\n ]\n}"}' - speculate: the execution was rejected + speculate: the execution was accepted add_next_block: succeeded. additional: - child_outputs: