From 64e60916a245b3533216beba5c5515c82024f475 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Fri, 22 Nov 2024 19:12:58 +0000 Subject: [PATCH 01/98] WIP renaming dealings --- rs/artifact_pool/benches/load_blocks.rs | 2 +- rs/artifact_pool/src/lmdb_pool.rs | 2 +- rs/consensus/benches/validate_payload.rs | 4 +-- rs/consensus/src/consensus/block_maker.rs | 4 +-- rs/consensus/src/consensus/proptests.rs | 5 ++-- rs/consensus/src/consensus/validator.rs | 4 +-- rs/consensus/src/dkg/payload_builder.rs | 2 +- rs/consensus/src/dkg/payload_validator.rs | 8 +++--- rs/consensus/src/idkg/payload_builder.rs | 11 ++++---- .../src/fetch_stripped_artifact/test_utils.rs | 4 +-- rs/protobuf/def/types/v1/dkg.proto | 4 +-- rs/protobuf/src/gen/types/types.v1.rs | 4 +-- rs/test_utilities/consensus/src/fake.rs | 24 +++++++++------- rs/test_utilities/types/src/batch/payload.rs | 6 ++-- rs/types/types/src/consensus/dkg.rs | 28 +++++++++---------- rs/types/types/src/consensus/payload.rs | 2 +- 16 files changed, 59 insertions(+), 55 deletions(-) diff --git a/rs/artifact_pool/benches/load_blocks.rs b/rs/artifact_pool/benches/load_blocks.rs index d2fbb2e0d410..10bed69f6083 100644 --- a/rs/artifact_pool/benches/load_blocks.rs +++ b/rs/artifact_pool/benches/load_blocks.rs @@ -65,7 +65,7 @@ fn prepare(pool: &mut ConsensusPoolImpl, num: usize) { ingress, ..BatchPayload::default() }, - dealings: dkg::Dealings::new_empty( + dealings: dkg::DataPayload::new_empty( parent.payload.as_ref().dkg_interval_start_height(), ), idkg: None, diff --git a/rs/artifact_pool/src/lmdb_pool.rs b/rs/artifact_pool/src/lmdb_pool.rs index a2dc0c9dfeaf..ec238c7c48ca 100644 --- a/rs/artifact_pool/src/lmdb_pool.rs +++ b/rs/artifact_pool/src/lmdb_pool.rs @@ -1037,7 +1037,7 @@ impl PoolArtifact for ConsensusMessage { }), PayloadType::Data => BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::Dealings::new_empty(start_height), + dealings: dkg::DataPayload::new_empty(start_height), idkg: None, }), }), diff --git a/rs/consensus/benches/validate_payload.rs b/rs/consensus/benches/validate_payload.rs index f5638085b1b7..bfcf02c92b89 100644 --- a/rs/consensus/benches/validate_payload.rs +++ b/rs/consensus/benches/validate_payload.rs @@ -276,7 +276,7 @@ fn add_past_blocks( ingress, ..BatchPayload::default() }, - dealings: dkg::Dealings::new_empty( + dealings: dkg::DataPayload::new_empty( block.payload.as_ref().dkg_interval_start_height(), ), idkg: None, @@ -353,7 +353,7 @@ fn validate_payload_benchmark(criterion: &mut Criterion) { ingress, ..BatchPayload::default() }, - dealings: dkg::Dealings::new_empty( + dealings: dkg::DataPayload::new_empty( tip.payload.as_ref().dkg_interval_start_height(), ), idkg: None, diff --git a/rs/consensus/src/consensus/block_maker.rs b/rs/consensus/src/consensus/block_maker.rs index 20fe2b0ea7c2..00f6fc729228 100755 --- a/rs/consensus/src/consensus/block_maker.rs +++ b/rs/consensus/src/consensus/block_maker.rs @@ -349,7 +349,7 @@ impl BlockMaker { // Use empty payload and empty DKG dealings if the replica is halting. Status::Halting => ( BatchPayload::default(), - dkg::Dealings::new_empty(dealings.start_height), + dkg::DataPayload::new_empty(dealings.start_height), /*idkg_data=*/ None, ), Status::Running => { @@ -700,7 +700,7 @@ mod tests { let expected_payloads = PoolReader::new(&pool) .get_payloads_from_height(certified_height.increment(), start.as_ref().clone()); let returned_payload = - dkg::Payload::Dealings(dkg::Dealings::new_empty(Height::from(0))); + dkg::Payload::Dealings(dkg::DataPayload::new_empty(Height::from(0))); let pool_reader = PoolReader::new(&pool); let expected_time = expected_payloads[0].1 + get_block_maker_delay( diff --git a/rs/consensus/src/consensus/proptests.rs b/rs/consensus/src/consensus/proptests.rs index 27754004adb5..f665cb90387e 100644 --- a/rs/consensus/src/consensus/proptests.rs +++ b/rs/consensus/src/consensus/proptests.rs @@ -12,8 +12,7 @@ use ic_types::{ consensus::{ block_maker::SubnetRecords, certification::{Certification, CertificationContent}, - dkg::Dealings, - BlockPayload, DataPayload, Payload, + dkg, BlockPayload, DataPayload, Payload, }, crypto::{CryptoHash, Signed}, messages::SignedIngress, @@ -144,7 +143,7 @@ fn wrap_batch_payload(height: u64, payload: BatchPayload) -> Payload { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: payload, - dealings: Dealings::new_empty(Height::from(height)), + dealings: dkg::DataPayload::new_empty(Height::from(height)), idkg: None, }), ) diff --git a/rs/consensus/src/consensus/validator.rs b/rs/consensus/src/consensus/validator.rs index ff921d4cda01..92449f7a3ee4 100644 --- a/rs/consensus/src/consensus/validator.rs +++ b/rs/consensus/src/consensus/validator.rs @@ -3473,7 +3473,7 @@ pub mod test { ingress, ..BatchPayload::default() }, - dealings: dkg::Dealings::new_empty(Height::new(0)), + dealings: dkg::DataPayload::new_empty(Height::new(0)), idkg: None, }), ); @@ -3487,7 +3487,7 @@ pub mod test { ingress: IngressPayload::from(vec![]), ..BatchPayload::default() }, - dealings: dkg::Dealings::new_empty(Height::new(0)), + dealings: dkg::DataPayload::new_empty(Height::new(0)), idkg: None, }), ); diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 4468dae90889..47a50852462d 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -107,7 +107,7 @@ pub fn create_payload( .take(max_dealings_per_block) .cloned() .collect(); - Ok(dkg::Payload::Dealings(dkg::Dealings::new( + Ok(dkg::Payload::Dealings(dkg::DataPayload::new( last_summary_block.height, new_validated_dealings, ))) diff --git a/rs/consensus/src/dkg/payload_validator.rs b/rs/consensus/src/dkg/payload_validator.rs index cbf52f1078a3..5d3182c8f374 100644 --- a/rs/consensus/src/dkg/payload_validator.rs +++ b/rs/consensus/src/dkg/payload_validator.rs @@ -13,7 +13,7 @@ use ic_replicated_state::ReplicatedState; use ic_types::{ batch::ValidationContext, consensus::{ - dkg::{self, Dealings, Summary}, + dkg::{self, DataPayload, Summary}, Block, BlockPayload, }, crypto::{ @@ -202,7 +202,7 @@ fn validate_dealings_payload( pool_reader: &PoolReader<'_>, dkg_pool: &dyn DkgPool, last_summary: &Summary, - dealings: &Dealings, + dealings: &DataPayload, max_dealings_per_payload: usize, parent: &Block, metrics: &IntCounterVec, @@ -531,7 +531,7 @@ mod tests { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::Dealings::new(Height::from(0), parent_dealings), + dealings: dkg::DataPayload::new(Height::from(0), parent_dealings), idkg: idkg::Payload::default(), }), ); @@ -544,7 +544,7 @@ mod tests { let block_payload = BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::Dealings::new(Height::from(0), dealings_to_validate), + dealings: dkg::DataPayload::new(Height::from(0), dealings_to_validate), idkg: idkg::Payload::default(), }); diff --git a/rs/consensus/src/idkg/payload_builder.rs b/rs/consensus/src/idkg/payload_builder.rs index a96620f4c59a..2fdffba4645c 100644 --- a/rs/consensus/src/idkg/payload_builder.rs +++ b/rs/consensus/src/idkg/payload_builder.rs @@ -705,15 +705,16 @@ mod tests { use ic_test_utilities_registry::{add_subnet_record, SubnetRecordBuilder}; use ic_test_utilities_types::ids::{node_test_id, subnet_test_id, user_test_id}; use ic_types::batch::BatchPayload; - use ic_types::consensus::dkg::{Dealings, Summary}; + use ic_types::consensus::dkg::{self, Summary}; use ic_types::consensus::idkg::IDkgPayload; use ic_types::consensus::idkg::PreSigId; use ic_types::consensus::idkg::ReshareOfUnmaskedParams; use ic_types::consensus::idkg::TranscriptRef; use ic_types::consensus::idkg::UnmaskedTranscript; use ic_types::consensus::idkg::UnmaskedTranscriptWithAttributes; + use ic_types::consensus::DataPayload; use ic_types::consensus::{ - BlockPayload, BlockProposal, DataPayload, HashedBlock, Payload, Rank, SummaryPayload, + BlockPayload, BlockProposal, HashedBlock, Payload, Rank, SummaryPayload, }; use ic_types::crypto::canister_threshold_sig::idkg::IDkgTranscript; use ic_types::crypto::canister_threshold_sig::ThresholdEcdsaCombinedSignature; @@ -783,7 +784,7 @@ mod tests { } BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: Dealings::new_empty(dkg_interval_start_height), + dealings: dkg::DataPayload::new_empty(dkg_interval_start_height), idkg: Some(idkg_payload), }) } @@ -1305,7 +1306,7 @@ mod tests { idkg::KeyTranscriptCreation::Created(key_transcript_ref); let parent_block_payload = BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: Dealings::new_empty(summary_height), + dealings: dkg::DataPayload::new_empty(summary_height), idkg: Some(data_payload), }); let parent_block = add_block( @@ -1570,7 +1571,7 @@ mod tests { idkg::KeyTranscriptCreation::Begin; let parent_block_payload = BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: Dealings::new_empty(summary_height), + dealings: dkg::DataPayload::new_empty(summary_height), idkg: Some(data_payload), }); let parent_block = add_block( diff --git a/rs/p2p/artifact_downloader/src/fetch_stripped_artifact/test_utils.rs b/rs/p2p/artifact_downloader/src/fetch_stripped_artifact/test_utils.rs index c457258342a3..dec8293397ac 100644 --- a/rs/p2p/artifact_downloader/src/fetch_stripped_artifact/test_utils.rs +++ b/rs/p2p/artifact_downloader/src/fetch_stripped_artifact/test_utils.rs @@ -7,7 +7,7 @@ use ic_types::{ artifact::{ConsensusMessageId, IngressMessageId}, batch::{BatchPayload, IngressPayload}, consensus::{ - dkg::{Dealings, Summary}, + dkg::{DataPayload, Summary}, Block, BlockPayload, BlockProposal, ConsensusMessage, ConsensusMessageHash, DataPayload, Payload, Rank, }, @@ -65,7 +65,7 @@ pub(crate) fn fake_block_proposal_with_ingresses( ingress: IngressPayload::from(ingress_messages), ..BatchPayload::default() }, - dealings: Dealings::new_empty(Height::from(0)), + dealings: DataPayload::new_empty(Height::from(0)), idkg: None, }), ), diff --git a/rs/protobuf/def/types/v1/dkg.proto b/rs/protobuf/def/types/v1/dkg.proto index c6279bfd114a..de0b98e12ef4 100644 --- a/rs/protobuf/def/types/v1/dkg.proto +++ b/rs/protobuf/def/types/v1/dkg.proto @@ -15,11 +15,11 @@ message DkgMessage { message DkgPayload { oneof val { Summary summary = 1; - Dealings dealings = 2; + DataPayload data_payload = 2; } } -message Dealings { +message DataPayload { repeated DkgMessage dealings = 1; uint64 summary_height = 2; } diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index c5b5c27dea7b..0e48583b8434 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -357,11 +357,11 @@ pub mod dkg_payload { #[prost(message, tag = "1")] Summary(super::Summary), #[prost(message, tag = "2")] - Dealings(super::Dealings), + DataPayload(super::DataPayload), } } #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Dealings { +pub struct DataPayload { #[prost(message, repeated, tag = "1")] pub dealings: ::prost::alloc::vec::Vec, #[prost(uint64, tag = "2")] diff --git a/rs/test_utilities/consensus/src/fake.rs b/rs/test_utilities/consensus/src/fake.rs index 36aafcc0efed..472cf7d48a1a 100644 --- a/rs/test_utilities/consensus/src/fake.rs +++ b/rs/test_utilities/consensus/src/fake.rs @@ -8,11 +8,15 @@ use ic_interfaces::{ use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ batch::*, - consensus::certification::*, - consensus::dkg::{Dealings, Summary}, - consensus::*, - crypto::threshold_sig::ni_dkg::{NiDkgId, NiDkgTag, NiDkgTargetSubnet}, - crypto::{threshold_sig::ni_dkg::NiDkgTranscript, *}, + consensus::{ + certification::*, + dkg::{self, Summary}, + *, + }, + crypto::{ + threshold_sig::ni_dkg::{NiDkgId, NiDkgTag, NiDkgTargetSubnet, NiDkgTranscript}, + *, + }, signature::*, *, }; @@ -25,7 +29,7 @@ pub trait Fake { impl Fake for SummaryPayload { fn fake() -> Self { Self { - dkg: ic_types::consensus::dkg::Summary::fake(), + dkg: dkg::Summary::fake(), idkg: None, } } @@ -35,7 +39,7 @@ impl Fake for DataPayload { fn fake() -> Self { Self { batch: BatchPayload::default(), - dealings: dkg::Dealings::new_empty(Height::from(0)), + dealings: dkg::DataPayload::new_empty(Height::from(0)), idkg: None, } } @@ -263,7 +267,7 @@ impl FromParent for Block { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: Dealings::new_empty(dkg_start), + dealings: dkg::DataPayload::new_empty(dkg_start), idkg: parent.payload.as_ref().as_idkg().cloned(), }), ), @@ -315,7 +319,7 @@ fn test_fake_block_is_binary_compatible() { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: ic_types::consensus::dkg::Dealings::new_empty(Height::from(1)), + dealings: dkg::DataPayload::new_empty(Height::from(1)), idkg: None, }), ), @@ -342,7 +346,7 @@ fn test_fake_block() { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: ic_types::consensus::dkg::Dealings::new_empty(Height::from(1)), + dealings: dkg::DataPayload::new_empty(Height::from(1)), idkg: None, }), ), diff --git a/rs/test_utilities/types/src/batch/payload.rs b/rs/test_utilities/types/src/batch/payload.rs index 4fd25b5531c6..0b865e2ff083 100644 --- a/rs/test_utilities/types/src/batch/payload.rs +++ b/rs/test_utilities/types/src/batch/payload.rs @@ -47,7 +47,7 @@ impl PayloadBuilder { mod tests { use ic_types::{ batch::{BatchPayload, IngressPayload}, - consensus::dkg::Dealings, + consensus::dkg::DataPayload, consensus::{BlockPayload, DataPayload}, }; @@ -78,7 +78,7 @@ mod tests { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: Dealings::new_empty(Height::from(0)), + dealings: DataPayload::new_empty(Height::from(0)), idkg: None, }), ); @@ -104,7 +104,7 @@ mod tests { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: batch_payload_0, - dealings: dkg::Dealings::new_empty(Height::new(0)), + dealings: dkg::DataPayload::new_empty(Height::new(0)), idkg: None, }), ); diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index d107c51193d2..10cba04d2e3e 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -527,7 +527,7 @@ pub enum Payload { /// DKG Summary payload Summary(Summary), /// DKG Dealings payload - Dealings(Dealings), + Dealings(DataPayload), } /// DealingMessages is a vector of DKG messages @@ -537,20 +537,20 @@ pub type DealingMessages = Vec; /// started #[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)] #[cfg_attr(test, derive(ExhaustiveSet))] -pub struct Dealings { +pub struct DataPayload { /// The height of the DKG interval that this object belongs to pub start_height: Height, /// The dealing messages pub messages: DealingMessages, } -impl TryFrom for Dealings { +impl TryFrom for DataPayload { type Error = ProxyDecodeError; - fn try_from(dealings: pb::Dealings) -> Result { + fn try_from(data_payload: pb::DataPayload) -> Result { Ok(Self { - start_height: Height::from(dealings.summary_height), - messages: dealings + start_height: Height::from(data_payload.summary_height), + messages: data_payload .dealings .into_iter() .map(Message::try_from) @@ -559,7 +559,7 @@ impl TryFrom for Dealings { } } -impl Dealings { +impl DataPayload { /// Return an empty DealingsPayload using the given start_height. pub fn new_empty(start_height: Height) -> Self { Self::new(start_height, vec![]) @@ -595,18 +595,18 @@ impl From<&Summary> for pb::DkgPayload { } } -impl From<&Dealings> for pb::DkgPayload { - fn from(dealings: &Dealings) -> Self { +impl From<&DataPayload> for pb::DkgPayload { + fn from(data_payload: &DataPayload) -> Self { Self { - val: Some(pb::dkg_payload::Val::Dealings(pb::Dealings { + val: Some(pb::dkg_payload::Val::DataPayload(pb::DataPayload { // TODO do we need this clone - dealings: dealings + dealings: data_payload .messages .iter() .cloned() .map(pb::DkgMessage::from) .collect(), - summary_height: dealings.start_height.get(), + summary_height: data_payload.start_height.get(), })), } } @@ -623,8 +623,8 @@ impl TryFrom for Payload { pb::dkg_payload::Val::Summary(summary) => { Ok(Payload::Summary(Summary::try_from(summary)?)) } - pb::dkg_payload::Val::Dealings(dealings) => { - Ok(Payload::Dealings(Dealings::try_from(dealings)?)) + pb::dkg_payload::Val::DataPayload(data_payload) => { + Ok(Payload::Dealings(DataPayload::try_from(data_payload)?)) } } } diff --git a/rs/types/types/src/consensus/payload.rs b/rs/types/types/src/consensus/payload.rs index b314e22b0906..ef2cfd00396b 100644 --- a/rs/types/types/src/consensus/payload.rs +++ b/rs/types/types/src/consensus/payload.rs @@ -17,7 +17,7 @@ use std::sync::Arc; #[cfg_attr(test, derive(ExhaustiveSet))] pub struct DataPayload { pub batch: BatchPayload, - pub dealings: dkg::Dealings, + pub dealings: dkg::DataPayload, pub idkg: idkg::Payload, } From 9ced08c77d62a1b36e1987cc7d71e437256479eb Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Fri, 22 Nov 2024 19:42:47 +0000 Subject: [PATCH 02/98] More renaming --- rs/artifact_pool/benches/load_blocks.rs | 2 +- rs/artifact_pool/src/lmdb_pool.rs | 2 +- rs/consensus/benches/validate_payload.rs | 4 ++-- rs/consensus/src/consensus/block_maker.rs | 7 +++---- rs/consensus/src/consensus/proptests.rs | 2 +- rs/consensus/src/consensus/validator.rs | 4 ++-- rs/consensus/src/dkg.rs | 8 ++++---- rs/consensus/src/dkg/payload_builder.rs | 2 +- rs/consensus/src/dkg/payload_validator.rs | 6 +++--- rs/consensus/src/dkg/utils.rs | 2 +- rs/consensus/src/idkg/payload_builder.rs | 6 +++--- rs/test_utilities/artifact_pool/src/consensus_pool.rs | 4 ++-- rs/test_utilities/consensus/src/fake.rs | 8 ++++---- rs/types/types/src/consensus.rs | 6 +++--- rs/types/types/src/consensus/dkg.rs | 4 ++-- rs/types/types/src/consensus/payload.rs | 10 +++++----- 16 files changed, 38 insertions(+), 39 deletions(-) diff --git a/rs/artifact_pool/benches/load_blocks.rs b/rs/artifact_pool/benches/load_blocks.rs index 10bed69f6083..06292bf4db60 100644 --- a/rs/artifact_pool/benches/load_blocks.rs +++ b/rs/artifact_pool/benches/load_blocks.rs @@ -65,7 +65,7 @@ fn prepare(pool: &mut ConsensusPoolImpl, num: usize) { ingress, ..BatchPayload::default() }, - dealings: dkg::DataPayload::new_empty( + dkg: dkg::DataPayload::new_empty( parent.payload.as_ref().dkg_interval_start_height(), ), idkg: None, diff --git a/rs/artifact_pool/src/lmdb_pool.rs b/rs/artifact_pool/src/lmdb_pool.rs index ec238c7c48ca..32d6373bc3b9 100644 --- a/rs/artifact_pool/src/lmdb_pool.rs +++ b/rs/artifact_pool/src/lmdb_pool.rs @@ -1037,7 +1037,7 @@ impl PoolArtifact for ConsensusMessage { }), PayloadType::Data => BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new_empty(start_height), + dkg: dkg::DataPayload::new_empty(start_height), idkg: None, }), }), diff --git a/rs/consensus/benches/validate_payload.rs b/rs/consensus/benches/validate_payload.rs index bfcf02c92b89..95bca69eefa6 100644 --- a/rs/consensus/benches/validate_payload.rs +++ b/rs/consensus/benches/validate_payload.rs @@ -276,7 +276,7 @@ fn add_past_blocks( ingress, ..BatchPayload::default() }, - dealings: dkg::DataPayload::new_empty( + dkg: dkg::DataPayload::new_empty( block.payload.as_ref().dkg_interval_start_height(), ), idkg: None, @@ -353,7 +353,7 @@ fn validate_payload_benchmark(criterion: &mut Criterion) { ingress, ..BatchPayload::default() }, - dealings: dkg::DataPayload::new_empty( + dkg: dkg::DataPayload::new_empty( tip.payload.as_ref().dkg_interval_start_height(), ), idkg: None, diff --git a/rs/consensus/src/consensus/block_maker.rs b/rs/consensus/src/consensus/block_maker.rs index 00f6fc729228..9f10a41a3c87 100755 --- a/rs/consensus/src/consensus/block_maker.rs +++ b/rs/consensus/src/consensus/block_maker.rs @@ -334,7 +334,7 @@ impl BlockMaker { idkg: idkg_summary, }) } - dkg::Payload::Dealings(dealings) => { + dkg::Payload::Data(dealings) => { let (batch_payload, dealings, idkg_data) = match status::get_status( height, self.registry_client.as_ref(), @@ -391,7 +391,7 @@ impl BlockMaker { BlockPayload::Data(DataPayload { batch: batch_payload, - dealings, + dkg: dealings, idkg: idkg_data, }) } @@ -699,8 +699,7 @@ mod tests { let start_hash = start.content.get_hash(); let expected_payloads = PoolReader::new(&pool) .get_payloads_from_height(certified_height.increment(), start.as_ref().clone()); - let returned_payload = - dkg::Payload::Dealings(dkg::DataPayload::new_empty(Height::from(0))); + let returned_payload = dkg::Payload::Data(dkg::DataPayload::new_empty(Height::from(0))); let pool_reader = PoolReader::new(&pool); let expected_time = expected_payloads[0].1 + get_block_maker_delay( diff --git a/rs/consensus/src/consensus/proptests.rs b/rs/consensus/src/consensus/proptests.rs index f665cb90387e..356b4cfec098 100644 --- a/rs/consensus/src/consensus/proptests.rs +++ b/rs/consensus/src/consensus/proptests.rs @@ -143,7 +143,7 @@ fn wrap_batch_payload(height: u64, payload: BatchPayload) -> Payload { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: payload, - dealings: dkg::DataPayload::new_empty(Height::from(height)), + dkg: dkg::DataPayload::new_empty(Height::from(height)), idkg: None, }), ) diff --git a/rs/consensus/src/consensus/validator.rs b/rs/consensus/src/consensus/validator.rs index 92449f7a3ee4..ca910fc09b64 100644 --- a/rs/consensus/src/consensus/validator.rs +++ b/rs/consensus/src/consensus/validator.rs @@ -3473,7 +3473,7 @@ pub mod test { ingress, ..BatchPayload::default() }, - dealings: dkg::DataPayload::new_empty(Height::new(0)), + dkg: dkg::DataPayload::new_empty(Height::new(0)), idkg: None, }), ); @@ -3487,7 +3487,7 @@ pub mod test { ingress: IngressPayload::from(vec![]), ..BatchPayload::default() }, - dealings: dkg::DataPayload::new_empty(Height::new(0)), + dkg: dkg::DataPayload::new_empty(Height::new(0)), idkg: None, }), ); diff --git a/rs/consensus/src/dkg.rs b/rs/consensus/src/dkg.rs index 37e85399d5c0..67dc521c3570 100644 --- a/rs/consensus/src/dkg.rs +++ b/rs/consensus/src/dkg.rs @@ -692,7 +692,7 @@ mod tests { // into the block. pool.advance_round_normal_operation(); let block = pool.get_cache().finalized_block(); - let dealings = &block.payload.as_ref().as_data().dealings; + let dealings = &block.payload.as_ref().as_data().dkg; if dealings.start_height != Height::from(0) { panic!( "Expected start height in dealings {:?}, but found {:?}", @@ -711,7 +711,7 @@ mod tests { // block anymore. pool.advance_round_normal_operation(); let block = pool.get_cache().finalized_block(); - let dealings = &block.payload.as_ref().as_data().dealings; + let dealings = &block.payload.as_ref().as_data().dkg; assert_eq!(dealings.messages.len(), 0); // Now we empty the dkg pool, add new dealings from this dealer and make sure @@ -732,7 +732,7 @@ mod tests { // Advance the pool and make sure the dealing are not included. pool.advance_round_normal_operation(); let block = pool.get_cache().finalized_block(); - let dealings = &block.payload.as_ref().as_data().dealings; + let dealings = &block.payload.as_ref().as_data().dkg; assert_eq!(dealings.messages.len(), 0); // Create another dealer and add his dealings into the unvalidated pool of @@ -780,7 +780,7 @@ mod tests { // Now we create a new block and make sure, the dealings made into the payload. pool.advance_round_normal_operation(); let block = pool.get_cache().finalized_block(); - let dealings = &block.payload.as_ref().as_data().dealings; + let dealings = &block.payload.as_ref().as_data().dkg; if dealings.start_height != Height::from(0) { panic!( "Expected start height in dealings {:?}, but found {:?}", diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 47a50852462d..7b46b169d803 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -107,7 +107,7 @@ pub fn create_payload( .take(max_dealings_per_block) .cloned() .collect(); - Ok(dkg::Payload::Dealings(dkg::DataPayload::new( + Ok(dkg::Payload::Data(dkg::DataPayload::new( last_summary_block.height, new_validated_dealings, ))) diff --git a/rs/consensus/src/dkg/payload_validator.rs b/rs/consensus/src/dkg/payload_validator.rs index 5d3182c8f374..f82acc7dab9e 100644 --- a/rs/consensus/src/dkg/payload_validator.rs +++ b/rs/consensus/src/dkg/payload_validator.rs @@ -187,7 +187,7 @@ pub(crate) fn validate_payload( pool_reader, dkg_pool, last_dkg_summary, - &data_payload.dealings, + &data_payload.dkg, max_dealings_per_block, &parent, metrics, @@ -531,7 +531,7 @@ mod tests { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new(Height::from(0), parent_dealings), + dkg: dkg::DataPayload::new(Height::from(0), parent_dealings), idkg: idkg::Payload::default(), }), ); @@ -544,7 +544,7 @@ mod tests { let block_payload = BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new(Height::from(0), dealings_to_validate), + dkg: dkg::DataPayload::new(Height::from(0), dealings_to_validate), idkg: idkg::Payload::default(), }); diff --git a/rs/consensus/src/dkg/utils.rs b/rs/consensus/src/dkg/utils.rs index a04517548c5d..4b187cecd178 100644 --- a/rs/consensus/src/dkg/utils.rs +++ b/rs/consensus/src/dkg/utils.rs @@ -35,7 +35,7 @@ pub(super) fn get_dkg_dealings( .payload .as_ref() .as_data() - .dealings + .dkg .messages .iter() .for_each(|msg| { diff --git a/rs/consensus/src/idkg/payload_builder.rs b/rs/consensus/src/idkg/payload_builder.rs index 2fdffba4645c..9a19c0b17a51 100644 --- a/rs/consensus/src/idkg/payload_builder.rs +++ b/rs/consensus/src/idkg/payload_builder.rs @@ -784,7 +784,7 @@ mod tests { } BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new_empty(dkg_interval_start_height), + dkg: dkg::DataPayload::new_empty(dkg_interval_start_height), idkg: Some(idkg_payload), }) } @@ -1306,7 +1306,7 @@ mod tests { idkg::KeyTranscriptCreation::Created(key_transcript_ref); let parent_block_payload = BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new_empty(summary_height), + dkg: dkg::DataPayload::new_empty(summary_height), idkg: Some(data_payload), }); let parent_block = add_block( @@ -1571,7 +1571,7 @@ mod tests { idkg::KeyTranscriptCreation::Begin; let parent_block_payload = BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new_empty(summary_height), + dkg: dkg::DataPayload::new_empty(summary_height), idkg: Some(data_payload), }); let parent_block = add_block( diff --git a/rs/test_utilities/artifact_pool/src/consensus_pool.rs b/rs/test_utilities/artifact_pool/src/consensus_pool.rs index afa3d60d11cb..5cdf1e404fdf 100644 --- a/rs/test_utilities/artifact_pool/src/consensus_pool.rs +++ b/rs/test_utilities/artifact_pool/src/consensus_pool.rs @@ -260,9 +260,9 @@ impl TestConsensusPool { let dkg_payload = (self.dkg_payload_builder)(self, parent.clone(), &block.context); let payload = match dkg_payload { dkg::Payload::Summary(dkg) => BlockPayload::Summary(SummaryPayload { dkg, idkg }), - dkg::Payload::Dealings(dealings) => BlockPayload::Data(DataPayload { + dkg::Payload::Data(dealings) => BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings, + dkg: dealings, idkg, }), }; diff --git a/rs/test_utilities/consensus/src/fake.rs b/rs/test_utilities/consensus/src/fake.rs index 472cf7d48a1a..94121ef4021d 100644 --- a/rs/test_utilities/consensus/src/fake.rs +++ b/rs/test_utilities/consensus/src/fake.rs @@ -39,7 +39,7 @@ impl Fake for DataPayload { fn fake() -> Self { Self { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new_empty(Height::from(0)), + dkg: dkg::DataPayload::new_empty(Height::from(0)), idkg: None, } } @@ -267,7 +267,7 @@ impl FromParent for Block { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new_empty(dkg_start), + dkg: dkg::DataPayload::new_empty(dkg_start), idkg: parent.payload.as_ref().as_idkg().cloned(), }), ), @@ -319,7 +319,7 @@ fn test_fake_block_is_binary_compatible() { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new_empty(Height::from(1)), + dkg: dkg::DataPayload::new_empty(Height::from(1)), idkg: None, }), ), @@ -346,7 +346,7 @@ fn test_fake_block() { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: dkg::DataPayload::new_empty(Height::from(1)), + dkg: dkg::DataPayload::new_empty(Height::from(1)), idkg: None, }), ), diff --git a/rs/types/types/src/consensus.rs b/rs/types/types/src/consensus.rs index ec0c6a5ea2b9..c51e819ebdfb 100644 --- a/rs/types/types/src/consensus.rs +++ b/rs/types/types/src/consensus.rs @@ -1309,7 +1309,7 @@ impl From<&Block> for pb::Block { } else { let batch = &payload.as_data().batch; ( - pb::DkgPayload::from(&payload.as_data().dealings), + pb::DkgPayload::from(&payload.as_data().dkg), Some(pb::XNetPayload::from(&batch.xnet)), Some(pb::IngressPayload::from(&batch.ingress)), Some(pb::SelfValidatingPayload::from(&batch.self_validating)), @@ -1388,7 +1388,7 @@ impl TryFrom for Block { BlockPayload::Summary(SummaryPayload { dkg: summary, idkg }) } - dkg::Payload::Dealings(dealings) => { + dkg::Payload::Data(dealings) => { let idkg = block .idkg_payload .as_ref() @@ -1397,7 +1397,7 @@ impl TryFrom for Block { BlockPayload::Data(DataPayload { batch, - dealings, + dkg: dealings, idkg, }) } diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index 10cba04d2e3e..c88316b3b5e9 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -527,7 +527,7 @@ pub enum Payload { /// DKG Summary payload Summary(Summary), /// DKG Dealings payload - Dealings(DataPayload), + Data(DataPayload), } /// DealingMessages is a vector of DKG messages @@ -624,7 +624,7 @@ impl TryFrom for Payload { Ok(Payload::Summary(Summary::try_from(summary)?)) } pb::dkg_payload::Val::DataPayload(data_payload) => { - Ok(Payload::Dealings(DataPayload::try_from(data_payload)?)) + Ok(Payload::Data(DataPayload::try_from(data_payload)?)) } } } diff --git a/rs/types/types/src/consensus/payload.rs b/rs/types/types/src/consensus/payload.rs index ef2cfd00396b..17b6d814c788 100644 --- a/rs/types/types/src/consensus/payload.rs +++ b/rs/types/types/src/consensus/payload.rs @@ -17,7 +17,7 @@ use std::sync::Arc; #[cfg_attr(test, derive(ExhaustiveSet))] pub struct DataPayload { pub batch: BatchPayload, - pub dealings: dkg::DataPayload, + pub dkg: dkg::DataPayload, pub idkg: idkg::Payload, } @@ -68,7 +68,7 @@ impl BlockPayload { pub fn is_empty(&self) -> bool { match self { BlockPayload::Data(data) => { - data.batch.is_empty() && data.dealings.messages.is_empty() && data.idkg.is_none() + data.batch.is_empty() && data.dkg.messages.is_empty() && data.idkg.is_none() } _ => false, } @@ -133,7 +133,7 @@ impl BlockPayload { pub fn dkg_interval_start_height(&self) -> Height { match self { BlockPayload::Summary(summary) => summary.dkg.height, - BlockPayload::Data(data) => data.dealings.start_height, + BlockPayload::Data(data) => data.dkg.start_height, } } } @@ -244,9 +244,9 @@ impl From for BlockPayload { dkg: summary, idkg: None, }), - dkg::Payload::Dealings(dealings) => BlockPayload::Data(DataPayload { + dkg::Payload::Data(dealings) => BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings, + dkg: dealings, idkg: idkg::Payload::default(), }), } From 8a5e27500b1c6ae4d1b6718ee697218a0a1db2ba Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 25 Nov 2024 13:50:39 +0000 Subject: [PATCH 03/98] Some more renaming --- rs/consensus/src/consensus/block_maker.rs | 6 +++--- rs/test_utilities/artifact_pool/src/consensus_pool.rs | 4 ++-- rs/types/types/src/consensus.rs | 8 ++------ rs/types/types/src/consensus/payload.rs | 4 ++-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/rs/consensus/src/consensus/block_maker.rs b/rs/consensus/src/consensus/block_maker.rs index 9f10a41a3c87..7a44c87cde97 100755 --- a/rs/consensus/src/consensus/block_maker.rs +++ b/rs/consensus/src/consensus/block_maker.rs @@ -334,7 +334,7 @@ impl BlockMaker { idkg: idkg_summary, }) } - dkg::Payload::Data(dealings) => { + dkg::Payload::Data(dkg) => { let (batch_payload, dealings, idkg_data) = match status::get_status( height, self.registry_client.as_ref(), @@ -349,7 +349,7 @@ impl BlockMaker { // Use empty payload and empty DKG dealings if the replica is halting. Status::Halting => ( BatchPayload::default(), - dkg::DataPayload::new_empty(dealings.start_height), + dkg::DataPayload::new_empty(dkg.start_height), /*idkg_data=*/ None, ), Status::Running => { @@ -380,7 +380,7 @@ impl BlockMaker { .ok() .flatten(); - (batch_payload, dealings, idkg_data) + (batch_payload, dkg, idkg_data) } }; diff --git a/rs/test_utilities/artifact_pool/src/consensus_pool.rs b/rs/test_utilities/artifact_pool/src/consensus_pool.rs index 5cdf1e404fdf..2fe4c1f5b5b6 100644 --- a/rs/test_utilities/artifact_pool/src/consensus_pool.rs +++ b/rs/test_utilities/artifact_pool/src/consensus_pool.rs @@ -260,9 +260,9 @@ impl TestConsensusPool { let dkg_payload = (self.dkg_payload_builder)(self, parent.clone(), &block.context); let payload = match dkg_payload { dkg::Payload::Summary(dkg) => BlockPayload::Summary(SummaryPayload { dkg, idkg }), - dkg::Payload::Data(dealings) => BlockPayload::Data(DataPayload { + dkg::Payload::Data(dkg) => BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dealings, + dkg, idkg, }), }; diff --git a/rs/types/types/src/consensus.rs b/rs/types/types/src/consensus.rs index c51e819ebdfb..f866e2536b69 100644 --- a/rs/types/types/src/consensus.rs +++ b/rs/types/types/src/consensus.rs @@ -1388,18 +1388,14 @@ impl TryFrom for Block { BlockPayload::Summary(SummaryPayload { dkg: summary, idkg }) } - dkg::Payload::Data(dealings) => { + dkg::Payload::Data(dkg) => { let idkg = block .idkg_payload .as_ref() .map(|idkg| idkg.try_into()) .transpose()?; - BlockPayload::Data(DataPayload { - batch, - dkg: dealings, - idkg, - }) + BlockPayload::Data(DataPayload { batch, dkg, idkg }) } }; Ok(Block { diff --git a/rs/types/types/src/consensus/payload.rs b/rs/types/types/src/consensus/payload.rs index 17b6d814c788..3422af32e6a3 100644 --- a/rs/types/types/src/consensus/payload.rs +++ b/rs/types/types/src/consensus/payload.rs @@ -244,9 +244,9 @@ impl From for BlockPayload { dkg: summary, idkg: None, }), - dkg::Payload::Data(dealings) => BlockPayload::Data(DataPayload { + dkg::Payload::Data(dkg) => BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dealings, + dkg, idkg: idkg::Payload::default(), }), } From 50b3a97b9107916c9fbceccd0d0a412081ad12c1 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 25 Nov 2024 13:50:51 +0000 Subject: [PATCH 04/98] PB compatibility --- rs/protobuf/def/types/v1/dkg.proto | 5 ++++- rs/protobuf/src/gen/types/types.v1.rs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rs/protobuf/def/types/v1/dkg.proto b/rs/protobuf/def/types/v1/dkg.proto index de0b98e12ef4..84812c1e3bc0 100644 --- a/rs/protobuf/def/types/v1/dkg.proto +++ b/rs/protobuf/def/types/v1/dkg.proto @@ -15,8 +15,11 @@ message DkgMessage { message DkgPayload { oneof val { Summary summary = 1; - DataPayload data_payload = 2; + DataPayload data_payload = 3; } + + reserved 2; + reserved "dealings"; } message DataPayload { diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index 0e48583b8434..033d73445a26 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -347,7 +347,7 @@ pub struct DkgMessage { } #[derive(Clone, PartialEq, ::prost::Message)] pub struct DkgPayload { - #[prost(oneof = "dkg_payload::Val", tags = "1, 2")] + #[prost(oneof = "dkg_payload::Val", tags = "1, 3")] pub val: ::core::option::Option, } /// Nested message and enum types in `DkgPayload`. @@ -356,7 +356,7 @@ pub mod dkg_payload { pub enum Val { #[prost(message, tag = "1")] Summary(super::Summary), - #[prost(message, tag = "2")] + #[prost(message, tag = "3")] DataPayload(super::DataPayload), } } From 04d1beabc15e2842d04a37cfcb830fe6b7dec30e Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 25 Nov 2024 14:01:05 +0000 Subject: [PATCH 05/98] Fixes --- rs/artifact_pool/src/rocksdb_pool.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/artifact_pool/src/rocksdb_pool.rs b/rs/artifact_pool/src/rocksdb_pool.rs index 6ca1da28cbbf..5a57d652bd48 100755 --- a/rs/artifact_pool/src/rocksdb_pool.rs +++ b/rs/artifact_pool/src/rocksdb_pool.rs @@ -19,7 +19,7 @@ use ic_types::{ batch::BatchPayload, consensus::{ certification::{Certification, CertificationMessage, CertificationShare}, - dkg::Dealings, + dkg::DataPayload, BlockProposal, CatchUpPackage, CatchUpPackageShare, ConsensusMessage, ConsensusMessageHash, ConsensusMessageHashable, EquivocationProof, Finalization, FinalizationShare, HasHeight, Notarization, NotarizationShare, Payload, RandomBeacon, RandomBeaconShare, RandomTape, @@ -431,7 +431,7 @@ impl MutablePoolSection Box::new(move || { BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: Dealings::new_empty(start_height), + dkg: Dealings::new_empty(start_height), idkg: None, }) }), From 4f7b28a780c87a59b9751d66ba7ce4bc79362690 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 25 Nov 2024 14:36:35 +0000 Subject: [PATCH 06/98] Fixes --- rs/test_utilities/types/src/batch/payload.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rs/test_utilities/types/src/batch/payload.rs b/rs/test_utilities/types/src/batch/payload.rs index 0b865e2ff083..61b52fe8f6b2 100644 --- a/rs/test_utilities/types/src/batch/payload.rs +++ b/rs/test_utilities/types/src/batch/payload.rs @@ -47,7 +47,6 @@ impl PayloadBuilder { mod tests { use ic_types::{ batch::{BatchPayload, IngressPayload}, - consensus::dkg::DataPayload, consensus::{BlockPayload, DataPayload}, }; @@ -78,7 +77,7 @@ mod tests { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dealings: DataPayload::new_empty(Height::from(0)), + dkg: dkg::DataPayload::new_empty(Height::from(0)), idkg: None, }), ); @@ -104,7 +103,7 @@ mod tests { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: batch_payload_0, - dealings: dkg::DataPayload::new_empty(Height::new(0)), + dkg: dkg::DataPayload::new_empty(Height::new(0)), idkg: None, }), ); From 5b4b66442a5da53eacd387066031ec29e3b616b4 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 25 Nov 2024 14:56:56 +0000 Subject: [PATCH 07/98] Rename dkg::DataPayload to dkg::DkgDataPayload --- rs/artifact_pool/benches/load_blocks.rs | 7 +++---- rs/artifact_pool/src/lmdb_pool.rs | 4 ++-- rs/artifact_pool/src/rocksdb_pool.rs | 11 +++++------ rs/consensus/benches/validate_payload.rs | 7 +++---- rs/consensus/src/consensus/block_maker.rs | 15 +++++++++------ rs/consensus/src/consensus/proptests.rs | 5 +++-- rs/consensus/src/consensus/validator.rs | 10 +++++----- rs/consensus/src/dkg/payload_builder.rs | 2 +- rs/consensus/src/dkg/payload_validator.rs | 10 +++++----- rs/consensus/src/idkg/payload_builder.rs | 8 ++++---- .../src/fetch_stripped_artifact/test_utils.rs | 4 ++-- rs/protobuf/def/types/v1/dkg.proto | 4 ++-- rs/protobuf/src/gen/types/types.v1.rs | 4 ++-- rs/test_utilities/consensus/src/fake.rs | 10 +++++----- rs/test_utilities/types/src/batch/payload.rs | 6 +++--- rs/types/types/src/consensus/dkg.rs | 18 +++++++++--------- rs/types/types/src/consensus/payload.rs | 2 +- 17 files changed, 64 insertions(+), 63 deletions(-) diff --git a/rs/artifact_pool/benches/load_blocks.rs b/rs/artifact_pool/benches/load_blocks.rs index 06292bf4db60..539760c64d95 100644 --- a/rs/artifact_pool/benches/load_blocks.rs +++ b/rs/artifact_pool/benches/load_blocks.rs @@ -14,10 +14,11 @@ use ic_test_utilities_types::{ ids::{node_test_id, subnet_test_id}, messages::SignedIngressBuilder, }; +use ic_types::consensus::dkg::DkgDataPayload; use ic_types::consensus::{BlockPayload, DataPayload}; use ic_types::{ batch::{BatchPayload, IngressPayload}, - consensus::{dkg, Block, BlockProposal, ConsensusMessageHashable, HasHeight, Payload, Rank}, + consensus::{Block, BlockProposal, ConsensusMessageHashable, HasHeight, Payload, Rank}, time::UNIX_EPOCH, Height, }; @@ -65,9 +66,7 @@ fn prepare(pool: &mut ConsensusPoolImpl, num: usize) { ingress, ..BatchPayload::default() }, - dkg: dkg::DataPayload::new_empty( - parent.payload.as_ref().dkg_interval_start_height(), - ), + dkg: DkgDataPayload::new_empty(parent.payload.as_ref().dkg_interval_start_height()), idkg: None, }), ); diff --git a/rs/artifact_pool/src/lmdb_pool.rs b/rs/artifact_pool/src/lmdb_pool.rs index 32d6373bc3b9..6bce02a8aa45 100644 --- a/rs/artifact_pool/src/lmdb_pool.rs +++ b/rs/artifact_pool/src/lmdb_pool.rs @@ -23,7 +23,7 @@ use ic_types::{ batch::BatchPayload, consensus::{ certification::{Certification, CertificationMessage, CertificationShare}, - dkg, + dkg::{self, DkgDataPayload}, idkg::{ EcdsaSigShare, IDkgArtifactId, IDkgMessage, IDkgMessageType, IDkgPrefix, IDkgPrefixOf, SchnorrSigShare, SignedIDkgComplaint, SignedIDkgOpening, @@ -1037,7 +1037,7 @@ impl PoolArtifact for ConsensusMessage { }), PayloadType::Data => BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(start_height), + dkg: DkgDataPayload::new_empty(start_height), idkg: None, }), }), diff --git a/rs/artifact_pool/src/rocksdb_pool.rs b/rs/artifact_pool/src/rocksdb_pool.rs index 5a57d652bd48..ddbf984104b4 100755 --- a/rs/artifact_pool/src/rocksdb_pool.rs +++ b/rs/artifact_pool/src/rocksdb_pool.rs @@ -19,11 +19,10 @@ use ic_types::{ batch::BatchPayload, consensus::{ certification::{Certification, CertificationMessage, CertificationShare}, - dkg::DataPayload, - BlockProposal, CatchUpPackage, CatchUpPackageShare, ConsensusMessage, ConsensusMessageHash, - ConsensusMessageHashable, EquivocationProof, Finalization, FinalizationShare, HasHeight, - Notarization, NotarizationShare, Payload, RandomBeacon, RandomBeaconShare, RandomTape, - RandomTapeShare, + dkg, BlockProposal, CatchUpPackage, CatchUpPackageShare, ConsensusMessage, + ConsensusMessageHash, ConsensusMessageHashable, EquivocationProof, Finalization, + FinalizationShare, HasHeight, Notarization, NotarizationShare, Payload, RandomBeacon, + RandomBeaconShare, RandomTape, RandomTapeShare, }, crypto::CryptoHashable, Height, Time, @@ -431,7 +430,7 @@ impl MutablePoolSection Box::new(move || { BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: Dealings::new_empty(start_height), + dkg: dkg::DataPayload::new_empty(start_height), idkg: None, }) }), diff --git a/rs/consensus/benches/validate_payload.rs b/rs/consensus/benches/validate_payload.rs index 95bca69eefa6..00d463268f06 100644 --- a/rs/consensus/benches/validate_payload.rs +++ b/rs/consensus/benches/validate_payload.rs @@ -11,6 +11,7 @@ //! the message validates successfully use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use dkg::DkgDataPayload; use ic_artifact_pool::{consensus_pool::ConsensusPoolImpl, ingress_pool::IngressPoolImpl}; use ic_config::state_manager::Config as StateManagerConfig; use ic_consensus::consensus::payload_builder::PayloadBuilderImpl; @@ -276,9 +277,7 @@ fn add_past_blocks( ingress, ..BatchPayload::default() }, - dkg: dkg::DataPayload::new_empty( - block.payload.as_ref().dkg_interval_start_height(), - ), + dkg: DkgDataPayload::new_empty(block.payload.as_ref().dkg_interval_start_height()), idkg: None, }), ); @@ -353,7 +352,7 @@ fn validate_payload_benchmark(criterion: &mut Criterion) { ingress, ..BatchPayload::default() }, - dkg: dkg::DataPayload::new_empty( + dkg: DkgDataPayload::new_empty( tip.payload.as_ref().dkg_interval_start_height(), ), idkg: None, diff --git a/rs/consensus/src/consensus/block_maker.rs b/rs/consensus/src/consensus/block_maker.rs index 7a44c87cde97..863241bf16be 100755 --- a/rs/consensus/src/consensus/block_maker.rs +++ b/rs/consensus/src/consensus/block_maker.rs @@ -23,8 +23,10 @@ use ic_replicated_state::ReplicatedState; use ic_types::{ batch::{BatchPayload, ValidationContext}, consensus::{ - block_maker::SubnetRecords, dkg, hashed, Block, BlockMetadata, BlockPayload, BlockProposal, - DataPayload, HasHeight, HasRank, HashedBlock, Payload, RandomBeacon, Rank, SummaryPayload, + block_maker::SubnetRecords, + dkg::{self, DkgDataPayload}, + hashed, Block, BlockMetadata, BlockPayload, BlockProposal, DataPayload, HasHeight, HasRank, + HashedBlock, Payload, RandomBeacon, Rank, SummaryPayload, }, replica_config::ReplicaConfig, time::current_time, @@ -335,7 +337,7 @@ impl BlockMaker { }) } dkg::Payload::Data(dkg) => { - let (batch_payload, dealings, idkg_data) = match status::get_status( + let (batch_payload, dkg, idkg_data) = match status::get_status( height, self.registry_client.as_ref(), self.replica_config.subnet_id, @@ -349,7 +351,7 @@ impl BlockMaker { // Use empty payload and empty DKG dealings if the replica is halting. Status::Halting => ( BatchPayload::default(), - dkg::DataPayload::new_empty(dkg.start_height), + DkgDataPayload::new_empty(dkg.start_height), /*idkg_data=*/ None, ), Status::Running => { @@ -391,7 +393,7 @@ impl BlockMaker { BlockPayload::Data(DataPayload { batch: batch_payload, - dkg: dealings, + dkg, idkg: idkg_data, }) } @@ -699,7 +701,8 @@ mod tests { let start_hash = start.content.get_hash(); let expected_payloads = PoolReader::new(&pool) .get_payloads_from_height(certified_height.increment(), start.as_ref().clone()); - let returned_payload = dkg::Payload::Data(dkg::DataPayload::new_empty(Height::from(0))); + let returned_payload = + dkg::Payload::Data(dkg::DkgDataPayload::new_empty(Height::from(0))); let pool_reader = PoolReader::new(&pool); let expected_time = expected_payloads[0].1 + get_block_maker_delay( diff --git a/rs/consensus/src/consensus/proptests.rs b/rs/consensus/src/consensus/proptests.rs index 356b4cfec098..194c8fda2b06 100644 --- a/rs/consensus/src/consensus/proptests.rs +++ b/rs/consensus/src/consensus/proptests.rs @@ -12,7 +12,8 @@ use ic_types::{ consensus::{ block_maker::SubnetRecords, certification::{Certification, CertificationContent}, - dkg, BlockPayload, DataPayload, Payload, + dkg::DkgDataPayload, + BlockPayload, DataPayload, Payload, }, crypto::{CryptoHash, Signed}, messages::SignedIngress, @@ -143,7 +144,7 @@ fn wrap_batch_payload(height: u64, payload: BatchPayload) -> Payload { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: payload, - dkg: dkg::DataPayload::new_empty(Height::from(height)), + dkg: DkgDataPayload::new_empty(Height::from(height)), idkg: None, }), ) diff --git a/rs/consensus/src/consensus/validator.rs b/rs/consensus/src/consensus/validator.rs index ca910fc09b64..c15cb3980e20 100644 --- a/rs/consensus/src/consensus/validator.rs +++ b/rs/consensus/src/consensus/validator.rs @@ -1931,9 +1931,9 @@ pub mod test { use ic_types::{ batch::{BatchPayload, IngressPayload}, consensus::{ - dkg, idkg::PreSigId, BlockPayload, CatchUpPackageShare, DataPayload, EquivocationProof, - Finalization, FinalizationShare, HashedBlock, HashedRandomBeacon, NotarizationShare, - Payload, RandomBeaconContent, RandomTapeContent, SummaryPayload, + dkg::DkgDataPayload, idkg::PreSigId, BlockPayload, CatchUpPackageShare, DataPayload, + EquivocationProof, Finalization, FinalizationShare, HashedBlock, HashedRandomBeacon, + NotarizationShare, Payload, RandomBeaconContent, RandomTapeContent, SummaryPayload, }, crypto::{BasicSig, BasicSigOf, CombinedMultiSig, CombinedMultiSigOf, CryptoHash}, replica_config::ReplicaConfig, @@ -3473,7 +3473,7 @@ pub mod test { ingress, ..BatchPayload::default() }, - dkg: dkg::DataPayload::new_empty(Height::new(0)), + dkg: DkgDataPayload::new_empty(Height::new(0)), idkg: None, }), ); @@ -3487,7 +3487,7 @@ pub mod test { ingress: IngressPayload::from(vec![]), ..BatchPayload::default() }, - dkg: dkg::DataPayload::new_empty(Height::new(0)), + dkg: DkgDataPayload::new_empty(Height::new(0)), idkg: None, }), ); diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 7b46b169d803..ef3c392be8dc 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -107,7 +107,7 @@ pub fn create_payload( .take(max_dealings_per_block) .cloned() .collect(); - Ok(dkg::Payload::Data(dkg::DataPayload::new( + Ok(dkg::Payload::Data(dkg::DkgDataPayload::new( last_summary_block.height, new_validated_dealings, ))) diff --git a/rs/consensus/src/dkg/payload_validator.rs b/rs/consensus/src/dkg/payload_validator.rs index f82acc7dab9e..b6e489b0f103 100644 --- a/rs/consensus/src/dkg/payload_validator.rs +++ b/rs/consensus/src/dkg/payload_validator.rs @@ -13,7 +13,7 @@ use ic_replicated_state::ReplicatedState; use ic_types::{ batch::ValidationContext, consensus::{ - dkg::{self, DataPayload, Summary}, + dkg::{self, DkgDataPayload, Summary}, Block, BlockPayload, }, crypto::{ @@ -202,7 +202,7 @@ fn validate_dealings_payload( pool_reader: &PoolReader<'_>, dkg_pool: &dyn DkgPool, last_summary: &Summary, - dealings: &DataPayload, + dealings: &DkgDataPayload, max_dealings_per_payload: usize, parent: &Block, metrics: &IntCounterVec, @@ -284,7 +284,7 @@ mod tests { use ic_types::{ batch::BatchPayload, consensus::{ - dkg::{self, DealingContent, Message}, + dkg::{DealingContent, Message}, idkg, DataPayload, Payload, }, crypto::threshold_sig::ni_dkg::{NiDkgDealing, NiDkgId, NiDkgTag, NiDkgTargetSubnet}, @@ -531,7 +531,7 @@ mod tests { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new(Height::from(0), parent_dealings), + dkg: DkgDataPayload::new(Height::from(0), parent_dealings), idkg: idkg::Payload::default(), }), ); @@ -544,7 +544,7 @@ mod tests { let block_payload = BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new(Height::from(0), dealings_to_validate), + dkg: DkgDataPayload::new(Height::from(0), dealings_to_validate), idkg: idkg::Payload::default(), }); diff --git a/rs/consensus/src/idkg/payload_builder.rs b/rs/consensus/src/idkg/payload_builder.rs index 9a19c0b17a51..5b525d4b5208 100644 --- a/rs/consensus/src/idkg/payload_builder.rs +++ b/rs/consensus/src/idkg/payload_builder.rs @@ -705,7 +705,7 @@ mod tests { use ic_test_utilities_registry::{add_subnet_record, SubnetRecordBuilder}; use ic_test_utilities_types::ids::{node_test_id, subnet_test_id, user_test_id}; use ic_types::batch::BatchPayload; - use ic_types::consensus::dkg::{self, Summary}; + use ic_types::consensus::dkg::{DkgDataPayload, Summary}; use ic_types::consensus::idkg::IDkgPayload; use ic_types::consensus::idkg::PreSigId; use ic_types::consensus::idkg::ReshareOfUnmaskedParams; @@ -784,7 +784,7 @@ mod tests { } BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(dkg_interval_start_height), + dkg: DkgDataPayload::new_empty(dkg_interval_start_height), idkg: Some(idkg_payload), }) } @@ -1306,7 +1306,7 @@ mod tests { idkg::KeyTranscriptCreation::Created(key_transcript_ref); let parent_block_payload = BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(summary_height), + dkg: DkgDataPayload::new_empty(summary_height), idkg: Some(data_payload), }); let parent_block = add_block( @@ -1571,7 +1571,7 @@ mod tests { idkg::KeyTranscriptCreation::Begin; let parent_block_payload = BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(summary_height), + dkg: DkgDataPayload::new_empty(summary_height), idkg: Some(data_payload), }); let parent_block = add_block( diff --git a/rs/p2p/artifact_downloader/src/fetch_stripped_artifact/test_utils.rs b/rs/p2p/artifact_downloader/src/fetch_stripped_artifact/test_utils.rs index dec8293397ac..f6eee4a723a1 100644 --- a/rs/p2p/artifact_downloader/src/fetch_stripped_artifact/test_utils.rs +++ b/rs/p2p/artifact_downloader/src/fetch_stripped_artifact/test_utils.rs @@ -7,7 +7,7 @@ use ic_types::{ artifact::{ConsensusMessageId, IngressMessageId}, batch::{BatchPayload, IngressPayload}, consensus::{ - dkg::{DataPayload, Summary}, + dkg::{DkgDataPayload, Summary}, Block, BlockPayload, BlockProposal, ConsensusMessage, ConsensusMessageHash, DataPayload, Payload, Rank, }, @@ -65,7 +65,7 @@ pub(crate) fn fake_block_proposal_with_ingresses( ingress: IngressPayload::from(ingress_messages), ..BatchPayload::default() }, - dealings: DataPayload::new_empty(Height::from(0)), + dkg: DkgDataPayload::new_empty(Height::from(0)), idkg: None, }), ), diff --git a/rs/protobuf/def/types/v1/dkg.proto b/rs/protobuf/def/types/v1/dkg.proto index 84812c1e3bc0..415d9c422a53 100644 --- a/rs/protobuf/def/types/v1/dkg.proto +++ b/rs/protobuf/def/types/v1/dkg.proto @@ -15,14 +15,14 @@ message DkgMessage { message DkgPayload { oneof val { Summary summary = 1; - DataPayload data_payload = 3; + DkgDataPayload data_payload = 3; } reserved 2; reserved "dealings"; } -message DataPayload { +message DkgDataPayload { repeated DkgMessage dealings = 1; uint64 summary_height = 2; } diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index 033d73445a26..da9c2abe1a0f 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -357,11 +357,11 @@ pub mod dkg_payload { #[prost(message, tag = "1")] Summary(super::Summary), #[prost(message, tag = "3")] - DataPayload(super::DataPayload), + DataPayload(super::DkgDataPayload), } } #[derive(Clone, PartialEq, ::prost::Message)] -pub struct DataPayload { +pub struct DkgDataPayload { #[prost(message, repeated, tag = "1")] pub dealings: ::prost::alloc::vec::Vec, #[prost(uint64, tag = "2")] diff --git a/rs/test_utilities/consensus/src/fake.rs b/rs/test_utilities/consensus/src/fake.rs index 94121ef4021d..ed594dcf7042 100644 --- a/rs/test_utilities/consensus/src/fake.rs +++ b/rs/test_utilities/consensus/src/fake.rs @@ -10,7 +10,7 @@ use ic_types::{ batch::*, consensus::{ certification::*, - dkg::{self, Summary}, + dkg::{DkgDataPayload, Summary}, *, }, crypto::{ @@ -39,7 +39,7 @@ impl Fake for DataPayload { fn fake() -> Self { Self { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(Height::from(0)), + dkg: DkgDataPayload::new_empty(Height::from(0)), idkg: None, } } @@ -267,7 +267,7 @@ impl FromParent for Block { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(dkg_start), + dkg: DkgDataPayload::new_empty(dkg_start), idkg: parent.payload.as_ref().as_idkg().cloned(), }), ), @@ -319,7 +319,7 @@ fn test_fake_block_is_binary_compatible() { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(Height::from(1)), + dkg: DkgDataPayload::new_empty(Height::from(1)), idkg: None, }), ), @@ -346,7 +346,7 @@ fn test_fake_block() { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(Height::from(1)), + dkg: DkgDataPayload::new_empty(Height::from(1)), idkg: None, }), ), diff --git a/rs/test_utilities/types/src/batch/payload.rs b/rs/test_utilities/types/src/batch/payload.rs index 61b52fe8f6b2..180a4ef9e2a2 100644 --- a/rs/test_utilities/types/src/batch/payload.rs +++ b/rs/test_utilities/types/src/batch/payload.rs @@ -68,7 +68,7 @@ mod tests { fn payload_serialize_then_deserialize() { use ic_types::{ batch::BatchPayload, - consensus::{dkg, Payload}, + consensus::{dkg::DkgDataPayload, Payload}, Height, }; @@ -77,7 +77,7 @@ mod tests { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(Height::from(0)), + dkg: DkgDataPayload::new_empty(Height::from(0)), idkg: None, }), ); @@ -103,7 +103,7 @@ mod tests { ic_types::crypto::crypto_hash, BlockPayload::Data(DataPayload { batch: batch_payload_0, - dkg: dkg::DataPayload::new_empty(Height::new(0)), + dkg: DkgDataPayload::new_empty(Height::new(0)), idkg: None, }), ); diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index c88316b3b5e9..aaff3dfabf2d 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -527,7 +527,7 @@ pub enum Payload { /// DKG Summary payload Summary(Summary), /// DKG Dealings payload - Data(DataPayload), + Data(DkgDataPayload), } /// DealingMessages is a vector of DKG messages @@ -537,17 +537,17 @@ pub type DealingMessages = Vec; /// started #[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)] #[cfg_attr(test, derive(ExhaustiveSet))] -pub struct DataPayload { +pub struct DkgDataPayload { /// The height of the DKG interval that this object belongs to pub start_height: Height, /// The dealing messages pub messages: DealingMessages, } -impl TryFrom for DataPayload { +impl TryFrom for DkgDataPayload { type Error = ProxyDecodeError; - fn try_from(data_payload: pb::DataPayload) -> Result { + fn try_from(data_payload: pb::DkgDataPayload) -> Result { Ok(Self { start_height: Height::from(data_payload.summary_height), messages: data_payload @@ -559,7 +559,7 @@ impl TryFrom for DataPayload { } } -impl DataPayload { +impl DkgDataPayload { /// Return an empty DealingsPayload using the given start_height. pub fn new_empty(start_height: Height) -> Self { Self::new(start_height, vec![]) @@ -595,10 +595,10 @@ impl From<&Summary> for pb::DkgPayload { } } -impl From<&DataPayload> for pb::DkgPayload { - fn from(data_payload: &DataPayload) -> Self { +impl From<&DkgDataPayload> for pb::DkgPayload { + fn from(data_payload: &DkgDataPayload) -> Self { Self { - val: Some(pb::dkg_payload::Val::DataPayload(pb::DataPayload { + val: Some(pb::dkg_payload::Val::DataPayload(pb::DkgDataPayload { // TODO do we need this clone dealings: data_payload .messages @@ -624,7 +624,7 @@ impl TryFrom for Payload { Ok(Payload::Summary(Summary::try_from(summary)?)) } pb::dkg_payload::Val::DataPayload(data_payload) => { - Ok(Payload::Data(DataPayload::try_from(data_payload)?)) + Ok(Payload::Data(DkgDataPayload::try_from(data_payload)?)) } } } diff --git a/rs/types/types/src/consensus/payload.rs b/rs/types/types/src/consensus/payload.rs index 3422af32e6a3..6caf56d52cca 100644 --- a/rs/types/types/src/consensus/payload.rs +++ b/rs/types/types/src/consensus/payload.rs @@ -17,7 +17,7 @@ use std::sync::Arc; #[cfg_attr(test, derive(ExhaustiveSet))] pub struct DataPayload { pub batch: BatchPayload, - pub dkg: dkg::DataPayload, + pub dkg: dkg::DkgDataPayload, pub idkg: idkg::Payload, } From 246ca4fc327a3904c30640eb2dd39979dd834a9f Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 25 Nov 2024 15:18:29 +0000 Subject: [PATCH 08/98] Fix --- rs/artifact_pool/src/rocksdb_pool.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rs/artifact_pool/src/rocksdb_pool.rs b/rs/artifact_pool/src/rocksdb_pool.rs index ddbf984104b4..ab35775a2f04 100755 --- a/rs/artifact_pool/src/rocksdb_pool.rs +++ b/rs/artifact_pool/src/rocksdb_pool.rs @@ -19,10 +19,11 @@ use ic_types::{ batch::BatchPayload, consensus::{ certification::{Certification, CertificationMessage, CertificationShare}, - dkg, BlockProposal, CatchUpPackage, CatchUpPackageShare, ConsensusMessage, - ConsensusMessageHash, ConsensusMessageHashable, EquivocationProof, Finalization, - FinalizationShare, HasHeight, Notarization, NotarizationShare, Payload, RandomBeacon, - RandomBeaconShare, RandomTape, RandomTapeShare, + dkg::DkgDataPayload, + BlockProposal, CatchUpPackage, CatchUpPackageShare, ConsensusMessage, ConsensusMessageHash, + ConsensusMessageHashable, EquivocationProof, Finalization, FinalizationShare, HasHeight, + Notarization, NotarizationShare, Payload, RandomBeacon, RandomBeaconShare, RandomTape, + RandomTapeShare, }, crypto::CryptoHashable, Height, Time, @@ -430,7 +431,7 @@ impl MutablePoolSection Box::new(move || { BlockPayload::Data(DataPayload { batch: BatchPayload::default(), - dkg: dkg::DataPayload::new_empty(start_height), + dkg: DkgDataPayload::new_empty(start_height), idkg: None, }) }), From f815b3e51e3bd801f6c90a95f654c082b50d853c Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 25 Nov 2024 17:34:42 +0000 Subject: [PATCH 09/98] Remove werid retry logic --- rs/consensus/src/dkg/payload_builder.rs | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index ef3c392be8dc..2df1935f3076 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -196,11 +196,6 @@ pub(super) fn create_summary_payload( state_manager, validation_context, transcripts_for_remote_subnets, - &last_summary - .transcripts_for_remote_subnets - .iter() - .map(|(id, _, result)| (id.clone(), result.clone())) - .collect(), &last_summary.initial_dkg_attempts, &logger, )?; @@ -275,7 +270,6 @@ fn compute_remote_dkg_data( state_manager: &dyn StateManager, validation_context: &ValidationContext, mut new_transcripts: BTreeMap>, - previous_transcripts: &BTreeMap>, previous_attempts: &BTreeMap, logger: &ReplicaLogger, ) -> Result< @@ -308,18 +302,11 @@ fn compute_remote_dkg_data( let mut expected_configs = Vec::new(); for config in low_high_threshold_configs { let dkg_id = config.dkg_id(); - // Check if we have a transcript in the previous summary for this config, and - // if we do, move it to the new summary. - if let Some((id, transcript)) = previous_transcripts - .iter() - .find(|(id, _)| eq_sans_height(id, dkg_id)) - { - new_transcripts.insert(id.clone(), transcript.clone()); - } + // If not, we check if we computed a transcript for this config in the last round. And // if not, we move the config into the new summary so that we try again in // the next round. - else if !new_transcripts + if !new_transcripts .iter() .any(|(id, _)| eq_sans_height(id, dkg_id)) { @@ -965,7 +952,6 @@ mod tests { &validation_context, BTreeMap::new(), &BTreeMap::new(), - &BTreeMap::new(), &logger, ) .unwrap(); @@ -998,7 +984,6 @@ mod tests { state_manager.as_ref(), &validation_context, BTreeMap::new(), - &BTreeMap::new(), &initial_dkg_attempts, &logger, ) @@ -1041,7 +1026,6 @@ mod tests { state_manager.as_ref(), &validation_context, BTreeMap::new(), - &BTreeMap::new(), &initial_dkg_attempts, &logger, ) From e30b8cf9b810214196a515fc730c2fb164c4c105 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 25 Nov 2024 18:26:17 +0000 Subject: [PATCH 10/98] UPdate comment --- rs/consensus/src/dkg/payload_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 2df1935f3076..202c5eaf4361 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -303,7 +303,7 @@ fn compute_remote_dkg_data( for config in low_high_threshold_configs { let dkg_id = config.dkg_id(); - // If not, we check if we computed a transcript for this config in the last round. And + // We check if we computed a transcript for this config in the last round. And // if not, we move the config into the new summary so that we try again in // the next round. if !new_transcripts From db6d3c7330458a0f3454323930f8ffbde71d83ff Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Tue, 26 Nov 2024 14:32:04 +0000 Subject: [PATCH 11/98] Add payload --- rs/consensus/src/consensus/purger.rs | 4 ++-- rs/protobuf/def/types/v1/dkg.proto | 1 + rs/protobuf/src/gen/types/types.v1.rs | 2 ++ rs/types/types/src/consensus/dkg.rs | 22 ++++++++++++++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/rs/consensus/src/consensus/purger.rs b/rs/consensus/src/consensus/purger.rs index 915e3c944f1b..d658233d79b9 100644 --- a/rs/consensus/src/consensus/purger.rs +++ b/rs/consensus/src/consensus/purger.rs @@ -905,10 +905,10 @@ mod tests { non_finalized_notarization_2 )), ChangeAction::RemoveFromValidated(ConsensusMessage::BlockProposal( - non_finalized_block_proposal_2_1 + non_finalized_block_proposal_2_0 )), ChangeAction::RemoveFromValidated(ConsensusMessage::BlockProposal( - non_finalized_block_proposal_2_0 + non_finalized_block_proposal_2_1 )), ] ); diff --git a/rs/protobuf/def/types/v1/dkg.proto b/rs/protobuf/def/types/v1/dkg.proto index 415d9c422a53..09686ea9f44d 100644 --- a/rs/protobuf/def/types/v1/dkg.proto +++ b/rs/protobuf/def/types/v1/dkg.proto @@ -25,6 +25,7 @@ message DkgPayload { message DkgDataPayload { repeated DkgMessage dealings = 1; uint64 summary_height = 2; + repeated CallbackIdedNiDkgTranscript transcripts_for_remote_subnets = 3; } message Summary { diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index da9c2abe1a0f..74df52335f9d 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -366,6 +366,8 @@ pub struct DkgDataPayload { pub dealings: ::prost::alloc::vec::Vec, #[prost(uint64, tag = "2")] pub summary_height: u64, + #[prost(message, repeated, tag = "3")] + pub transcripts_for_remote_subnets: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct Summary { diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index aaff3dfabf2d..dd8d1808cea6 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -542,6 +542,8 @@ pub struct DkgDataPayload { pub start_height: Height, /// The dealing messages pub messages: DealingMessages, + /// Transcripts that are computed for remote subnets. + pub transcripts_for_remote_subnets: Vec<(NiDkgId, CallbackId, Result)>, } impl TryFrom for DkgDataPayload { @@ -555,6 +557,10 @@ impl TryFrom for DkgDataPayload { .into_iter() .map(Message::try_from) .collect::>()?, + transcripts_for_remote_subnets: build_transcripts_vec_from_pb( + data_payload.transcripts_for_remote_subnets, + ) + .map_err(ProxyDecodeError::Other)?, }) } } @@ -570,6 +576,19 @@ impl DkgDataPayload { Self { start_height, messages, + transcripts_for_remote_subnets: vec![], + } + } + + /// Return an new DealingsPayload. + pub fn new_with_remote_dkg_transcript( + start_height: Height, + remote_dkg_transcript: (NiDkgId, CallbackId, Result), + ) -> Self { + Self { + start_height, + messages: vec![], + transcripts_for_remote_subnets: vec![remote_dkg_transcript], } } } @@ -607,6 +626,9 @@ impl From<&DkgDataPayload> for pb::DkgPayload { .map(pb::DkgMessage::from) .collect(), summary_height: data_payload.start_height.get(), + transcripts_for_remote_subnets: build_callback_ided_transcripts_vec( + data_payload.transcripts_for_remote_subnets.as_slice(), + ), })), } } From c3d430dca49894c6593ab5b2927f75b6e296a2e1 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 27 Nov 2024 10:30:28 +0000 Subject: [PATCH 12/98] Deliver remote dkg transcripts from non-summary blocks as well --- rs/consensus/src/consensus/batch_delivery.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rs/consensus/src/consensus/batch_delivery.rs b/rs/consensus/src/consensus/batch_delivery.rs index 8448e8dfe7f8..dbb39a13fa96 100644 --- a/rs/consensus/src/consensus/batch_delivery.rs +++ b/rs/consensus/src/consensus/batch_delivery.rs @@ -275,6 +275,12 @@ pub fn generate_responses_to_subnet_calls( )) } else { let block_payload = block_payload.as_ref().as_data(); + + consensus_responses.append(&mut generate_responses_to_setup_initial_dkg_calls( + &block_payload.dkg.transcripts_for_remote_subnets, + log, + )); + if let Some(payload) = &block_payload.idkg { consensus_responses.append(&mut generate_responses_to_signature_request_contexts( payload, From d888927f566c6fdd568e80902ed313ec38441406 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 27 Nov 2024 10:31:12 +0000 Subject: [PATCH 13/98] Implement get_unused_dkg_dealings --- rs/consensus/src/dkg/utils.rs | 54 ++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/rs/consensus/src/dkg/utils.rs b/rs/consensus/src/dkg/utils.rs index 4b187cecd178..26781cc7502e 100644 --- a/rs/consensus/src/dkg/utils.rs +++ b/rs/consensus/src/dkg/utils.rs @@ -4,7 +4,7 @@ use ic_types::{ crypto::threshold_sig::ni_dkg::{NiDkgDealing, NiDkgId}, NodeId, }; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; pub(super) fn get_dealers_from_chain( pool_reader: &PoolReader<'_>, @@ -50,3 +50,55 @@ pub(super) fn get_dkg_dealings( acc }) } + +// TODO: Remove dead_code +#[allow(dead_code)] +pub(super) fn get_unused_dkg_dealings( + pool_reader: &PoolReader<'_>, + block: &Block, +) -> BTreeMap> { + let mut dealings: BTreeMap> = BTreeMap::new(); + let mut used_dealings: BTreeSet = BTreeSet::new(); + + // Note that the chain iterator is guaranteed to iterate from + // newest to oldest blocks and that transcripts can not appear before their dealings. + for block in pool_reader + .chain_iterator(block.clone()) + .take_while(|block| !block.payload.is_summary()) + { + let payload = &block.payload.as_ref().as_data().dkg; + + // Update used dealings + used_dealings.extend( + payload + .transcripts_for_remote_subnets + .iter() + .map(|transcript| transcript.0.clone()), + ); + + // Find new dealings in this payload + for (signer, ni_dkg_id, dealing) in payload + .messages + .iter() + // Filer out if they are already used + .filter(|message| !used_dealings.contains(&message.content.dkg_id)) + .map(|message| { + ( + message.signature.signer, + message.content.dkg_id.clone(), + message.content.dealing.clone(), + ) + }) + { + let entry = dealings.entry(ni_dkg_id).or_default(); + let old_entry = entry.insert(signer, dealing); + + assert!( + old_entry.is_none(), + "Dealings from the same dealers discovered." + ); + } + } + + dealings +} From 6d746dc81e72ff36e2aa511ab4adf23a33a8d577 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 27 Nov 2024 14:38:25 +0000 Subject: [PATCH 14/98] Small optimization --- rs/consensus/src/dkg.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/rs/consensus/src/dkg.rs b/rs/consensus/src/dkg.rs index 67dc521c3570..903cc53c6a58 100644 --- a/rs/consensus/src/dkg.rs +++ b/rs/consensus/src/dkg.rs @@ -362,15 +362,16 @@ impl PoolMutationsProducer for DkgImpl { let changeset = dealings .par_iter() .map(|dealings| { - self.validate_dealings_for_dealer( - dkg_pool, - &dkg_summary.configs, - start_height, - dealings.to_vec(), - ) + { + self.validate_dealings_for_dealer( + dkg_pool, + &dkg_summary.configs, + start_height, + dealings.to_vec(), + ) + } + .into_par_iter() }) - .collect::>() - .into_iter() .flatten() .collect::(); From 7d8edd405850d131748c138e54725748d0eb7444 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 27 Nov 2024 15:12:54 +0000 Subject: [PATCH 15/98] Small refactoring --- rs/consensus/src/dkg/payload_builder.rs | 20 ++++++++++---------- rs/consensus/src/dkg/utils.rs | 6 ++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 202c5eaf4361..d041eb99800f 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -535,10 +535,10 @@ fn get_dkg_interval_length( }) } -// Reads the SubnetCallContext and attempts to create DKG configs for new -// subnets for the next round. An Ok return value contains: -// * configs grouped by subnet (low and high threshold configs per subnet) -// * errors produced while generating the configs. +/// Reads the SubnetCallContext and attempts to create DKG configs for new +/// subnets for the next round. An Ok return value contains: +/// * configs grouped by subnet (low and high threshold configs per subnet) +/// * errors produced while generating the configs. #[allow(clippy::type_complexity)] fn process_subnet_call_context( this_subnet_id: SubnetId, @@ -618,9 +618,9 @@ fn get_node_list( .collect()) } -// Compares two DKG ids without considering the start block heights. This -// function is only used for DKGs for other subnets, as the start block height -// is not used to differentiate two DKGs for the same subnet. +/// Compares two DKG ids without considering the start block heights. This +/// function is only used for DKGs for other subnets, as the start block height +/// is not used to differentiate two DKGs for the same subnet. fn eq_sans_height(dkg_id1: &NiDkgId, dkg_id2: &NiDkgId) -> bool { dkg_id1.dealer_subnet == dkg_id2.dealer_subnet && dkg_id1.dkg_tag == dkg_id2.dkg_tag @@ -664,9 +664,9 @@ fn add_callback_ids_to_transcript_results( .collect() } -// This function is called for each entry on the SubnetCallContext. It returns -// either the created high and low configs for the entry or returns two errors -// identified by the NiDkgId. +/// This function is called for each entry on the SubnetCallContext. It returns +/// either the created high and low configs for the entry or returns two errors +/// identified by the NiDkgId. fn create_remote_dkg_configs( start_block_height: Height, dealer_subnet: SubnetId, diff --git a/rs/consensus/src/dkg/utils.rs b/rs/consensus/src/dkg/utils.rs index 26781cc7502e..dd867a5f4e65 100644 --- a/rs/consensus/src/dkg/utils.rs +++ b/rs/consensus/src/dkg/utils.rs @@ -52,6 +52,12 @@ pub(super) fn get_dkg_dealings( } // TODO: Remove dead_code + +/// Starts with the given block and creates a nested mapping from the DKG Id to +/// the node Id to the dealing. This function panics if multiple dealings +/// from one dealer are discovered, hence, we assume a valid block chain. +/// It also excludes dealings for ni_dkg ids, which already have a transcript in the +/// blockchain. #[allow(dead_code)] pub(super) fn get_unused_dkg_dealings( pool_reader: &PoolReader<'_>, From 4e6c8eff9abbd53127fd349641483558d3e20ae5 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 27 Nov 2024 16:00:04 +0000 Subject: [PATCH 16/98] More refactoring --- rs/consensus/src/dkg/payload_builder.rs | 49 +++++++++++++++++++------ rs/consensus/src/dkg/utils.rs | 22 ++++++----- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index d041eb99800f..477bc1991d63 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -15,7 +15,10 @@ use ic_registry_client_helpers::{ use ic_replicated_state::ReplicatedState; use ic_types::{ batch::ValidationContext, - consensus::{dkg, dkg::Summary, get_faults_tolerated, Block}, + consensus::{ + dkg::{self, DealingMessages, DkgDataPayload, Summary}, + get_faults_tolerated, Block, + }, crypto::{ threshold_sig::ni_dkg::{ config::{errors::NiDkgConfigValidationError, NiDkgConfig, NiDkgConfigData}, @@ -73,7 +76,7 @@ pub fn create_payload( if last_dkg_summary.get_next_start_height() == height { // Since `height` corresponds to the start of a new DKG interval, we create a // new summary. - return create_summary_payload( + create_summary_payload( subnet_id, registry_client, crypto, @@ -85,16 +88,34 @@ pub fn create_payload( validation_context, logger, ) - .map(dkg::Payload::Summary); + .map(dkg::Payload::Summary) + } else { + // If the height is not a start height, create a payload with new dealings. + create_data_payload( + pool_reader, + parent, + dkg_pool, + last_dkg_summary, + max_dealings_per_block, + &last_summary_block, + ) + .map(dkg::Payload::Data) } +} - // If the height is not a start height, create a payload with new dealings. - +fn create_data_payload( + pool_reader: &PoolReader<'_>, + parent: &Block, + dkg_pool: Arc>, + last_dkg_summary: &Summary, + max_dealings_per_block: usize, + last_summary_block: &Block, +) -> Result { // Get all dealer ids from the chain. let dealers_from_chain = utils::get_dealers_from_chain(pool_reader, parent); // Filter from the validated pool all dealings whose dealer has no dealing on // the chain yet. - let new_validated_dealings = dkg_pool + let new_validated_dealings: DealingMessages = dkg_pool .read() .expect("Couldn't lock DKG pool for reading.") .get_validated() @@ -107,10 +128,16 @@ pub fn create_payload( .take(max_dealings_per_block) .cloned() .collect(); - Ok(dkg::Payload::Data(dkg::DkgDataPayload::new( - last_summary_block.height, - new_validated_dealings, - ))) + + if !new_validated_dealings.is_empty() { + return Ok(DkgDataPayload::new( + last_summary_block.height, + new_validated_dealings, + )); + } + + // TODO: Try to include remote transcripts + Ok(DkgDataPayload::new_empty(last_summary_block.height)) } /// Creates a summary payload for the given parent and registry_version. @@ -151,7 +178,7 @@ pub(super) fn create_summary_payload( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { - let all_dealings = utils::get_dkg_dealings(pool_reader, parent); + let all_dealings = utils::get_dkg_dealings2(pool_reader, parent, false); let mut transcripts_for_remote_subnets = BTreeMap::new(); let mut next_transcripts = BTreeMap::new(); // Try to create transcripts from the last round. diff --git a/rs/consensus/src/dkg/utils.rs b/rs/consensus/src/dkg/utils.rs index dd867a5f4e65..6aeed71af465 100644 --- a/rs/consensus/src/dkg/utils.rs +++ b/rs/consensus/src/dkg/utils.rs @@ -10,7 +10,7 @@ pub(super) fn get_dealers_from_chain( pool_reader: &PoolReader<'_>, block: &Block, ) -> HashSet<(NiDkgId, NodeId)> { - get_dkg_dealings(pool_reader, block) + get_dkg_dealings2(pool_reader, block, false) .into_iter() .flat_map(|(dkg_id, dealings)| { dealings @@ -23,6 +23,7 @@ pub(super) fn get_dealers_from_chain( // Starts with the given block and creates a nested mapping from the DKG Id to // the node Id to the dealing. This function panics if multiple dealings // from one dealer are discovered, hence, we assume a valid block chain. +#[allow(dead_code)] pub(super) fn get_dkg_dealings( pool_reader: &PoolReader<'_>, block: &Block, @@ -59,9 +60,10 @@ pub(super) fn get_dkg_dealings( /// It also excludes dealings for ni_dkg ids, which already have a transcript in the /// blockchain. #[allow(dead_code)] -pub(super) fn get_unused_dkg_dealings( +pub(super) fn get_dkg_dealings2( pool_reader: &PoolReader<'_>, block: &Block, + exclude_used: bool, ) -> BTreeMap> { let mut dealings: BTreeMap> = BTreeMap::new(); let mut used_dealings: BTreeSet = BTreeSet::new(); @@ -74,13 +76,15 @@ pub(super) fn get_unused_dkg_dealings( { let payload = &block.payload.as_ref().as_data().dkg; - // Update used dealings - used_dealings.extend( - payload - .transcripts_for_remote_subnets - .iter() - .map(|transcript| transcript.0.clone()), - ); + if exclude_used { + // Update used dealings + used_dealings.extend( + payload + .transcripts_for_remote_subnets + .iter() + .map(|transcript| transcript.0.clone()), + ); + } // Find new dealings in this payload for (signer, ni_dkg_id, dealing) in payload From 0b553aa9370e77484217cfb0e1efaaecaa92d239 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 27 Nov 2024 16:44:46 +0000 Subject: [PATCH 17/98] Refactor callback_id acquisition --- rs/consensus/src/dkg/payload_builder.rs | 40 +++++++++++++------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 477bc1991d63..5d9f3beb5c31 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -659,38 +659,40 @@ fn add_callback_ids_to_transcript_results( state: &ReplicatedState, log: &ReplicaLogger, ) -> Vec<(NiDkgId, CallbackId, Result)> { - let setup_initial_dkg_contexts = &state - .metadata - .subnet_call_context_manager - .setup_initial_dkg_contexts; - new_transcripts .into_iter() .filter_map(|(id, result)| { - if let Some(callback_id) = setup_initial_dkg_contexts - .iter() - .filter_map(|(callback_id, context)| { - if NiDkgTargetSubnet::Remote(context.target_id) == id.target_subnet { - Some(*callback_id) - } else { - None - } - }) - .last() - { - Some((id, callback_id, result)) - } else { + let triple = get_callback_id_from_id(state, &id).map(|callback_id| (id.clone(), callback_id, result)); + if triple.is_none() { error!( log, "Unable to find callback id associated with remote dkg id {}, this should not happen", id ); - None } + triple }) .collect() } +fn get_callback_id_from_id(state: &ReplicatedState, id: &NiDkgId) -> Option { + let setup_initial_dkg_contexts = &state + .metadata + .subnet_call_context_manager + .setup_initial_dkg_contexts; + + setup_initial_dkg_contexts + .iter() + .filter_map(|(callback_id, context)| { + if NiDkgTargetSubnet::Remote(context.target_id) == id.target_subnet { + Some(*callback_id) + } else { + None + } + }) + .last() +} + /// This function is called for each entry on the SubnetCallContext. It returns /// either the created high and low configs for the entry or returns two errors /// identified by the NiDkgId. From 63fa1010c9e5388b98a745d1498cde834b19a1cd Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 27 Nov 2024 17:06:38 +0000 Subject: [PATCH 18/98] WIP implement create_early_remote_transcripts --- rs/consensus/src/dkg/payload_builder.rs | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 5d9f3beb5c31..8f1fb9745cc3 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -129,6 +129,7 @@ fn create_data_payload( .cloned() .collect(); + // If we have dealings in the payload, we will not try to make transcripts as well if !new_validated_dealings.is_empty() { return Ok(DkgDataPayload::new( last_summary_block.height, @@ -140,6 +141,51 @@ fn create_data_payload( Ok(DkgDataPayload::new_empty(last_summary_block.height)) } +fn create_early_remote_transcripts( + pool_reader: &PoolReader<'_>, + parent: &Block, + num_transcripts: usize, +) -> Vec<(CallbackId, NiDkgId, NiDkgTranscript)> { + // Get all dealings that have not been used in a transcript already + let all_dealings = utils::get_dkg_dealings2(pool_reader, parent, true); + + // Collect map of remote target_ids to ni_dkg_ids + let mut remote_contexts: BTreeMap> = BTreeMap::new(); + for (target_id, ni_dkg_id) in + all_dealings + .into_iter() + .filter_map(|(id, _)| match id.target_subnet { + NiDkgTargetSubnet::Local => None, + NiDkgTargetSubnet::Remote(target_id) => Some((target_id, id)), + }) + { + let entry = remote_contexts.entry(target_id).or_default(); + entry.push(ni_dkg_id); + } + + let x = remote_contexts + .iter() + .filter(|(_, ni_dkg_ids)| match ni_dkg_ids.len() { + 1 => { + // TODO: With vetkd, we need to check that these have a HighTresholdForMasterPublicKeyId tag + // Sould we also check that the key exists? + false + } + 2 => { + let tags = ni_dkg_ids + .iter() + .map(|id| id.dkg_tag.clone()) + .collect::>(); + let expected_tags = TAGS.iter().cloned().collect::>(); + tags == expected_tags + } + _ => false, + }) + .take(num_transcripts); + + todo!() +} + /// Creates a summary payload for the given parent and registry_version. /// /// We compute the summary from prev_summary as follows: From 26e0a2b44b24ce8773deb18fa2a212a97e5b38ac Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 27 Nov 2024 17:35:25 +0000 Subject: [PATCH 19/98] TODO --- rs/consensus/src/dkg/payload_builder.rs | 55 ++++++++++++++++++++----- rs/types/types/src/consensus/dkg.rs | 6 +-- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 8f1fb9745cc3..3c5878c8f631 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -95,9 +95,11 @@ pub fn create_payload( pool_reader, parent, dkg_pool, + crypto, last_dkg_summary, max_dealings_per_block, &last_summary_block, + logger, ) .map(dkg::Payload::Data) } @@ -107,9 +109,11 @@ fn create_data_payload( pool_reader: &PoolReader<'_>, parent: &Block, dkg_pool: Arc>, + crypto: &dyn ConsensusCrypto, last_dkg_summary: &Summary, max_dealings_per_block: usize, last_summary_block: &Block, + logger: ReplicaLogger, ) -> Result { // Get all dealer ids from the chain. let dealers_from_chain = utils::get_dealers_from_chain(pool_reader, parent); @@ -137,40 +141,49 @@ fn create_data_payload( )); } - // TODO: Try to include remote transcripts - Ok(DkgDataPayload::new_empty(last_summary_block.height)) + // Try to include remote transcripts + Ok(DkgDataPayload::new_with_remote_dkg_transcripts( + last_summary_block.height, + create_early_remote_transcripts(pool_reader, crypto, parent, 1, last_dkg_summary, logger), + )) } fn create_early_remote_transcripts( pool_reader: &PoolReader<'_>, + crypto: &dyn ConsensusCrypto, parent: &Block, num_transcripts: usize, + last_dkg_summary: &Summary, + logger: ReplicaLogger, ) -> Vec<(CallbackId, NiDkgId, NiDkgTranscript)> { // Get all dealings that have not been used in a transcript already let all_dealings = utils::get_dkg_dealings2(pool_reader, parent, true); // Collect map of remote target_ids to ni_dkg_ids let mut remote_contexts: BTreeMap> = BTreeMap::new(); - for (target_id, ni_dkg_id) in - all_dealings - .into_iter() - .filter_map(|(id, _)| match id.target_subnet { - NiDkgTargetSubnet::Local => None, - NiDkgTargetSubnet::Remote(target_id) => Some((target_id, id)), - }) + for (target_id, ni_dkg_id) in all_dealings + .iter() + .filter_map(|(id, _)| match id.target_subnet { + NiDkgTargetSubnet::Local => None, + NiDkgTargetSubnet::Remote(target_id) => Some((target_id, id)), + }) { let entry = remote_contexts.entry(target_id).or_default(); - entry.push(ni_dkg_id); + entry.push(ni_dkg_id.clone()); } let x = remote_contexts .iter() + // For inital DKG transcripts, we need a pair of .filter(|(_, ni_dkg_ids)| match ni_dkg_ids.len() { 1 => { // TODO: With vetkd, we need to check that these have a HighTresholdForMasterPublicKeyId tag // Sould we also check that the key exists? false } + // If we have two transcripts for the same ID, we check that it is one low and one high threshold transcript + // Note: We do not really need to check whether there is an actual context, since this will happen later when we map + // the transcripts to callback ids 2 => { let tags = ni_dkg_ids .iter() @@ -179,8 +192,30 @@ fn create_early_remote_transcripts( let expected_tags = TAGS.iter().cloned().collect::>(); tags == expected_tags } + // Other combinations should never happen and we discard them here _ => false, }) + // Lookup the config from the summary + .filter_map(|(_, ni_dkg_id)| { + // TODO: Find ALL configs here + last_dkg_summary + .configs + .iter() + .find(ni_dkg_id) + .map(|config| (ni_dkg_id, config)) + }) + // Generate the actual transcripts + .map(|(ni_dkg_id, config)| { + ( + ni_dkg_id, + create_transcript(crypto, config, &all_dealings, &logger), + ) + }) + .filter_map(|(ni_dkg_id, maybe_transcript)| match maybe_transcript { + Ok(_) => todo!(), + Err(_) => None, + }) + // Take only the number of transcripts .take(num_transcripts); todo!() diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index dd8d1808cea6..35bc3f1bef7d 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -581,14 +581,14 @@ impl DkgDataPayload { } /// Return an new DealingsPayload. - pub fn new_with_remote_dkg_transcript( + pub fn new_with_remote_dkg_transcripts( start_height: Height, - remote_dkg_transcript: (NiDkgId, CallbackId, Result), + remote_dkg_transcripts: Vec<(NiDkgId, CallbackId, Result)>, ) -> Self { Self { start_height, messages: vec![], - transcripts_for_remote_subnets: vec![remote_dkg_transcript], + transcripts_for_remote_subnets: remote_dkg_transcripts, } } } From 07608f29a1e97dacce55784a701079029c083bd3 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Thu, 28 Nov 2024 10:16:01 +0000 Subject: [PATCH 20/98] WIP implement PB --- rs/consensus/src/dkg/payload_builder.rs | 76 +++++++++++++------------ 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 3c5878c8f631..5e0a87f3f7f4 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -155,27 +155,42 @@ fn create_early_remote_transcripts( num_transcripts: usize, last_dkg_summary: &Summary, logger: ReplicaLogger, -) -> Vec<(CallbackId, NiDkgId, NiDkgTranscript)> { +) -> Vec<(NiDkgId, CallbackId, Result)> { // Get all dealings that have not been used in a transcript already let all_dealings = utils::get_dkg_dealings2(pool_reader, parent, true); // Collect map of remote target_ids to ni_dkg_ids let mut remote_contexts: BTreeMap> = BTreeMap::new(); - for (target_id, ni_dkg_id) in all_dealings - .iter() - .filter_map(|(id, _)| match id.target_subnet { - NiDkgTargetSubnet::Local => None, - NiDkgTargetSubnet::Remote(target_id) => Some((target_id, id)), - }) + for (target_id, dkg_id) in + all_dealings + .iter() + .filter_map(|(dkg_id, _)| match dkg_id.target_subnet { + NiDkgTargetSubnet::Local => None, + NiDkgTargetSubnet::Remote(target_id) => Some((target_id, dkg_id)), + }) { let entry = remote_contexts.entry(target_id).or_default(); - entry.push(ni_dkg_id.clone()); + entry.push(dkg_id.clone()); } let x = remote_contexts .iter() - // For inital DKG transcripts, we need a pair of - .filter(|(_, ni_dkg_ids)| match ni_dkg_ids.len() { + // Lookup the config from the summary + .map(|(_, dkg_id)| { + dkg_id + .iter() + .filter_map(|ni_dkg_id| { + last_dkg_summary + .configs + .get(ni_dkg_id) + .map(|config| (ni_dkg_id, config)) + }) + .collect::>() + }) + // For inital DKG transcripts, we need a pair of values while for VetKD we need a single config + // Here we do some matching, to check that we have the right number of configs + .filter(|ni_dkgs| match ni_dkgs.len() { + // TODO: Warn for 0? 1 => { // TODO: With vetkd, we need to check that these have a HighTresholdForMasterPublicKeyId tag // Sould we also check that the key exists? @@ -185,38 +200,27 @@ fn create_early_remote_transcripts( // Note: We do not really need to check whether there is an actual context, since this will happen later when we map // the transcripts to callback ids 2 => { - let tags = ni_dkg_ids - .iter() - .map(|id| id.dkg_tag.clone()) + let tags = ni_dkgs + .keys() + .map(|dkg_id| dkg_id.dkg_tag.clone()) .collect::>(); let expected_tags = TAGS.iter().cloned().collect::>(); tags == expected_tags } - // Other combinations should never happen and we discard them here + // Other combinations are not supported _ => false, }) - // Lookup the config from the summary - .filter_map(|(_, ni_dkg_id)| { - // TODO: Find ALL configs here - last_dkg_summary - .configs - .iter() - .find(ni_dkg_id) - .map(|config| (ni_dkg_id, config)) - }) - // Generate the actual transcripts - .map(|(ni_dkg_id, config)| { - ( - ni_dkg_id, - create_transcript(crypto, config, &all_dealings, &logger), - ) - }) - .filter_map(|(ni_dkg_id, maybe_transcript)| match maybe_transcript { - Ok(_) => todo!(), - Err(_) => None, - }) - // Take only the number of transcripts - .take(num_transcripts); + // // Generate the actual transcripts + // .filter_map(|(ni_dkg_id, config)| { + // match create_transcript(crypto, config, &all_dealings, &logger) { + // Ok(_) => todo!(), + // Err(_) => None, + // } + // }) + // // Take only the number of transcripts + // .take(num_transcripts) + .take(num_transcripts) + .collect::>(); todo!() } From 4bbcb543f188444703d8ef24b68823d234639804 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Thu, 28 Nov 2024 11:01:31 +0000 Subject: [PATCH 21/98] WIP early transcripts --- rs/consensus/src/dkg/payload_builder.rs | 107 ++++++++++++++++-------- 1 file changed, 71 insertions(+), 36 deletions(-) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 5e0a87f3f7f4..2f7b6e88b207 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -99,6 +99,8 @@ pub fn create_payload( last_dkg_summary, max_dealings_per_block, &last_summary_block, + state_manager, + validation_context, logger, ) .map(dkg::Payload::Data) @@ -113,6 +115,8 @@ fn create_data_payload( last_dkg_summary: &Summary, max_dealings_per_block: usize, last_summary_block: &Block, + state_manager: &dyn StateManager, + validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { // Get all dealer ids from the chain. @@ -144,7 +148,16 @@ fn create_data_payload( // Try to include remote transcripts Ok(DkgDataPayload::new_with_remote_dkg_transcripts( last_summary_block.height, - create_early_remote_transcripts(pool_reader, crypto, parent, 1, last_dkg_summary, logger), + create_early_remote_transcripts( + pool_reader, + crypto, + parent, + 1, + last_dkg_summary, + state_manager, + validation_context, + logger, + ), )) } @@ -154,8 +167,17 @@ fn create_early_remote_transcripts( parent: &Block, num_transcripts: usize, last_dkg_summary: &Summary, + state_manager: &dyn StateManager, + validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Vec<(NiDkgId, CallbackId, Result)> { + let state = state_manager + .get_state_at(validation_context.certified_height) + .unwrap(); + //TODO: .map_err(PayloadCreationError::StateManagerError)?; + // TODO: Since this function is relatively expensive, we should only do this if there are any outstanding + // Remote DKG contexts + // Get all dealings that have not been used in a transcript already let all_dealings = utils::get_dkg_dealings2(pool_reader, parent, true); @@ -173,56 +195,69 @@ fn create_early_remote_transcripts( entry.push(dkg_id.clone()); } - let x = remote_contexts - .iter() - // Lookup the config from the summary - .map(|(_, dkg_id)| { - dkg_id - .iter() - .filter_map(|ni_dkg_id| { - last_dkg_summary - .configs - .get(ni_dkg_id) - .map(|config| (ni_dkg_id, config)) - }) - .collect::>() - }) + let mut selected_transcripts = vec![]; + for (_, dkg_ids) in remote_contexts { + // For each target_id, try to build the necessary transcripts + let mut transcripts = dkg_ids + .iter() + // Lookup the config from the summary + .filter_map(|dkg_id| { + last_dkg_summary + .configs + .get(dkg_id) + .map(|config| (dkg_id, config)) + }) + // Lookup the callback id + .filter_map(|(dkg_id, config)| { + get_callback_id_from_id(state.get_ref(), dkg_id) + .map(|callback_id| (dkg_id, callback_id, config)) + }) + // Generate the transcripts, not that we just skip errors, they will + // be handled in the summary block, if we fail to create an early transcript + .filter_map(|(dkg_id, callback_id, config)| { + match create_transcript(crypto, config, &all_dealings, &logger) { + Ok(transcript) => { + Some((dkg_id.clone(), callback_id, Ok::<_, String>(transcript))) + } + Err(_) => None, + } + }) + .collect::>(); + // For inital DKG transcripts, we need a pair of values while for VetKD we need a single config // Here we do some matching, to check that we have the right number of configs - .filter(|ni_dkgs| match ni_dkgs.len() { + match transcripts.len() { // TODO: Warn for 0? 1 => { // TODO: With vetkd, we need to check that these have a HighTresholdForMasterPublicKeyId tag - // Sould we also check that the key exists? - false + // Sould we also check that the keys exists? + continue; } // If we have two transcripts for the same ID, we check that it is one low and one high threshold transcript // Note: We do not really need to check whether there is an actual context, since this will happen later when we map // the transcripts to callback ids 2 => { - let tags = ni_dkgs - .keys() - .map(|dkg_id| dkg_id.dkg_tag.clone()) + let tags = transcripts + .iter() + .map(|(dkg_id, _, _)| dkg_id.dkg_tag.clone()) .collect::>(); let expected_tags = TAGS.iter().cloned().collect::>(); - tags == expected_tags + + if tags != expected_tags { + continue; + } } // Other combinations are not supported - _ => false, - }) - // // Generate the actual transcripts - // .filter_map(|(ni_dkg_id, config)| { - // match create_transcript(crypto, config, &all_dealings, &logger) { - // Ok(_) => todo!(), - // Err(_) => None, - // } - // }) - // // Take only the number of transcripts - // .take(num_transcripts) - .take(num_transcripts) - .collect::>(); + _ => continue, + } + + selected_transcripts.append(&mut transcripts); + if selected_transcripts.len() >= num_transcripts { + break; + } + } - todo!() + selected_transcripts } /// Creates a summary payload for the given parent and registry_version. From b4fe0acffc571ecd136f7f7040fa3c417a6110b6 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Thu, 28 Nov 2024 12:56:12 +0000 Subject: [PATCH 22/98] Improve payload builder code --- rs/consensus/src/dkg/payload_builder.rs | 45 ++++++++++++++++--------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 2f7b6e88b207..9b2ca70401ef 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -6,7 +6,7 @@ use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; use ic_interfaces::{crypto::ErrorReproducibility, dkg::DkgPool}; use ic_interfaces_registry::RegistryClient; use ic_interfaces_state_manager::{StateManager, StateManagerError}; -use ic_logger::{error, warn, ReplicaLogger}; +use ic_logger::{error, info, warn, ReplicaLogger}; use ic_protobuf::registry::subnet::v1::CatchUpPackageContents; use ic_registry_client_helpers::{ crypto::{initial_ni_dkg_transcript_from_registry_record, DkgTranscripts}, @@ -145,19 +145,29 @@ fn create_data_payload( )); } + let remote_dkg_transcripts = create_early_remote_transcripts( + pool_reader, + crypto, + parent, + 1, + last_dkg_summary, + state_manager, + validation_context, + logger.clone(), + ); + + if !remote_dkg_transcripts.is_empty() { + info!( + logger, + "Including {} early remote DKG transcripts in rregular block payload", + remote_dkg_transcripts.len() + ); + } + // Try to include remote transcripts Ok(DkgDataPayload::new_with_remote_dkg_transcripts( last_summary_block.height, - create_early_remote_transcripts( - pool_reader, - crypto, - parent, - 1, - last_dkg_summary, - state_manager, - validation_context, - logger, - ), + remote_dkg_transcripts, )) } @@ -171,10 +181,14 @@ fn create_early_remote_transcripts( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Vec<(NiDkgId, CallbackId, Result)> { - let state = state_manager - .get_state_at(validation_context.certified_height) - .unwrap(); - //TODO: .map_err(PayloadCreationError::StateManagerError)?; + // If we cannot access the state manage, we don't return an error. + // This is because the early remote transcripts is an optimization. + // If we don't return any transcripts here, the protocol will continue anyway + // and return the transcripts in the summary block + let Ok(state) = state_manager.get_state_at(validation_context.certified_height) else { + return vec![]; + }; + // TODO: Since this function is relatively expensive, we should only do this if there are any outstanding // Remote DKG contexts @@ -227,7 +241,6 @@ fn create_early_remote_transcripts( // For inital DKG transcripts, we need a pair of values while for VetKD we need a single config // Here we do some matching, to check that we have the right number of configs match transcripts.len() { - // TODO: Warn for 0? 1 => { // TODO: With vetkd, we need to check that these have a HighTresholdForMasterPublicKeyId tag // Sould we also check that the keys exists? From 68fd82b26ec32b5b009453a8bc9fe7e60ffaf80f Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Thu, 28 Nov 2024 13:13:04 +0000 Subject: [PATCH 23/98] Remove some unused code --- rs/consensus/src/dkg.rs | 2 +- rs/consensus/src/dkg/utils.rs | 39 ++--------------------------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/rs/consensus/src/dkg.rs b/rs/consensus/src/dkg.rs index 903cc53c6a58..05cdee13fc89 100644 --- a/rs/consensus/src/dkg.rs +++ b/rs/consensus/src/dkg.rs @@ -1490,7 +1490,7 @@ mod tests { dependencies.state_manager.clone(), dependencies.registry.get_latest_version(), vec![], - Some(1), + Some(100), None, ); diff --git a/rs/consensus/src/dkg/utils.rs b/rs/consensus/src/dkg/utils.rs index 6aeed71af465..4a3065b7e98c 100644 --- a/rs/consensus/src/dkg/utils.rs +++ b/rs/consensus/src/dkg/utils.rs @@ -10,7 +10,7 @@ pub(super) fn get_dealers_from_chain( pool_reader: &PoolReader<'_>, block: &Block, ) -> HashSet<(NiDkgId, NodeId)> { - get_dkg_dealings2(pool_reader, block, false) + get_dkg_dealings(pool_reader, block, false) .into_iter() .flat_map(|(dkg_id, dealings)| { dealings @@ -20,47 +20,12 @@ pub(super) fn get_dealers_from_chain( .collect() } -// Starts with the given block and creates a nested mapping from the DKG Id to -// the node Id to the dealing. This function panics if multiple dealings -// from one dealer are discovered, hence, we assume a valid block chain. -#[allow(dead_code)] -pub(super) fn get_dkg_dealings( - pool_reader: &PoolReader<'_>, - block: &Block, -) -> BTreeMap> { - pool_reader - .chain_iterator(block.clone()) - .take_while(|block| !block.payload.is_summary()) - .fold(Default::default(), |mut acc, block| { - block - .payload - .as_ref() - .as_data() - .dkg - .messages - .iter() - .for_each(|msg| { - let collected_dealings = acc.entry(msg.content.dkg_id.clone()).or_default(); - assert!( - collected_dealings - .insert(msg.signature.signer, msg.content.dealing.clone()) - .is_none(), - "Dealings from the same dealers discovered." - ); - }); - acc - }) -} - -// TODO: Remove dead_code - /// Starts with the given block and creates a nested mapping from the DKG Id to /// the node Id to the dealing. This function panics if multiple dealings /// from one dealer are discovered, hence, we assume a valid block chain. /// It also excludes dealings for ni_dkg ids, which already have a transcript in the /// blockchain. -#[allow(dead_code)] -pub(super) fn get_dkg_dealings2( +pub(super) fn get_dkg_dealings( pool_reader: &PoolReader<'_>, block: &Block, exclude_used: bool, From 5f5e8e51bc621e8b26173e570a1497bad3ea1ca4 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Thu, 28 Nov 2024 13:13:09 +0000 Subject: [PATCH 24/98] Fix tests --- rs/consensus/src/consensus/notary.rs | 21 +++++++++++++++++++++ rs/consensus/src/dkg/payload_builder.rs | 6 +++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/rs/consensus/src/consensus/notary.rs b/rs/consensus/src/consensus/notary.rs index d471509c0679..76f5759d42e8 100644 --- a/rs/consensus/src/consensus/notary.rs +++ b/rs/consensus/src/consensus/notary.rs @@ -721,6 +721,13 @@ mod tests { .get_mut() .expect_latest_certified_height() .return_const(gap_trigger_height); + state_manager + .get_mut() + .expect_get_state_at() + .return_const(Ok(ic_interfaces_state_manager::Labeled::new( + Height::new(0), + Arc::new(ic_test_utilities_state::get_initial_state(0, 0)), + ))); assert_matches!( get_adjusted_notary_delay_from_settings( @@ -739,6 +746,13 @@ mod tests { .get_mut() .expect_latest_certified_height() .return_const(PoolReader::new(&pool).get_finalized_height()); + state_manager + .get_mut() + .expect_get_state_at() + .return_const(Ok(ic_interfaces_state_manager::Labeled::new( + Height::new(0), + Arc::new(ic_test_utilities_state::get_initial_state(0, 0)), + ))); assert_eq!( get_adjusted_notary_delay_from_settings( @@ -757,6 +771,13 @@ mod tests { .get_mut() .expect_latest_certified_height() .return_const(PoolReader::new(&pool).get_finalized_height()); + state_manager + .get_mut() + .expect_get_state_at() + .return_const(Ok(ic_interfaces_state_manager::Labeled::new( + Height::new(0), + Arc::new(ic_test_utilities_state::get_initial_state(0, 0)), + ))); pool.advance_round_normal_operation_no_cup(); diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 9b2ca70401ef..850a7644d22c 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -159,7 +159,7 @@ fn create_data_payload( if !remote_dkg_transcripts.is_empty() { info!( logger, - "Including {} early remote DKG transcripts in rregular block payload", + "Including {} early remote DKG transcripts in regular block payload", remote_dkg_transcripts.len() ); } @@ -193,7 +193,7 @@ fn create_early_remote_transcripts( // Remote DKG contexts // Get all dealings that have not been used in a transcript already - let all_dealings = utils::get_dkg_dealings2(pool_reader, parent, true); + let all_dealings = utils::get_dkg_dealings(pool_reader, parent, true); // Collect map of remote target_ids to ni_dkg_ids let mut remote_contexts: BTreeMap> = BTreeMap::new(); @@ -311,7 +311,7 @@ pub(super) fn create_summary_payload( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { - let all_dealings = utils::get_dkg_dealings2(pool_reader, parent, false); + let all_dealings = utils::get_dkg_dealings(pool_reader, parent, false); let mut transcripts_for_remote_subnets = BTreeMap::new(); let mut next_transcripts = BTreeMap::new(); // Try to create transcripts from the last round. From ec24eae12924f36283cd050cf563ca8f13685745 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Fri, 29 Nov 2024 15:05:14 +0000 Subject: [PATCH 25/98] Revert "Small optimization" This reverts commit 6d746dc81e72ff36e2aa511ab4adf23a33a8d577. --- rs/consensus/src/dkg.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/rs/consensus/src/dkg.rs b/rs/consensus/src/dkg.rs index 05cdee13fc89..cd9c5c01ae64 100644 --- a/rs/consensus/src/dkg.rs +++ b/rs/consensus/src/dkg.rs @@ -362,16 +362,15 @@ impl PoolMutationsProducer for DkgImpl { let changeset = dealings .par_iter() .map(|dealings| { - { - self.validate_dealings_for_dealer( - dkg_pool, - &dkg_summary.configs, - start_height, - dealings.to_vec(), - ) - } - .into_par_iter() + self.validate_dealings_for_dealer( + dkg_pool, + &dkg_summary.configs, + start_height, + dealings.to_vec(), + ) }) + .collect::>() + .into_iter() .flatten() .collect::(); From bbc49e12441365bc84b253f1914146adade4bd45 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 2 Dec 2024 18:05:48 +0000 Subject: [PATCH 26/98] Revert "Small optimization" This reverts commit 6d746dc81e72ff36e2aa511ab4adf23a33a8d577. --- rs/consensus/src/dkg/payload_builder.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 850a7644d22c..fda1d9617124 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -356,6 +356,11 @@ pub(super) fn create_summary_payload( state_manager, validation_context, transcripts_for_remote_subnets, + &last_summary + .transcripts_for_remote_subnets + .iter() + .map(|(id, _, result)| (id.clone(), result.clone())) + .collect(), &last_summary.initial_dkg_attempts, &logger, )?; @@ -430,6 +435,7 @@ fn compute_remote_dkg_data( state_manager: &dyn StateManager, validation_context: &ValidationContext, mut new_transcripts: BTreeMap>, + previous_transcripts: &BTreeMap>, previous_attempts: &BTreeMap, logger: &ReplicaLogger, ) -> Result< @@ -463,6 +469,15 @@ fn compute_remote_dkg_data( for config in low_high_threshold_configs { let dkg_id = config.dkg_id(); + // Check if we have a transcript in the previous summary for this config, and + // if we do, move it to the new summary. + if let Some((id, transcript)) = previous_transcripts + .iter() + .find(|(id, _)| eq_sans_height(id, dkg_id)) + { + new_transcripts.insert(id.clone(), transcript.clone()); + } + // We check if we computed a transcript for this config in the last round. And // if not, we move the config into the new summary so that we try again in // the next round. @@ -1114,6 +1129,7 @@ mod tests { &validation_context, BTreeMap::new(), &BTreeMap::new(), + &BTreeMap::new(), &logger, ) .unwrap(); @@ -1146,6 +1162,7 @@ mod tests { state_manager.as_ref(), &validation_context, BTreeMap::new(), + &BTreeMap::new(), &initial_dkg_attempts, &logger, ) @@ -1188,6 +1205,7 @@ mod tests { state_manager.as_ref(), &validation_context, BTreeMap::new(), + &BTreeMap::new(), &initial_dkg_attempts, &logger, ) From bcdafe287e53154085d82143f2a75000e1a475f3 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Mon, 2 Dec 2024 18:45:33 +0000 Subject: [PATCH 27/98] WIP implement validator --- rs/consensus/src/consensus/validator.rs | 1 + rs/consensus/src/dkg/payload_builder.rs | 28 +++++++++---- rs/consensus/src/dkg/payload_validator.rs | 50 +++++++++++++++++++++-- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/rs/consensus/src/consensus/validator.rs b/rs/consensus/src/consensus/validator.rs index c15cb3980e20..89733a355fdf 100644 --- a/rs/consensus/src/consensus/validator.rs +++ b/rs/consensus/src/consensus/validator.rs @@ -1312,6 +1312,7 @@ impl Validator { proposal.payload.as_ref(), self.state_manager.as_ref(), &proposal.context, + self.log.clone(), &self.metrics.dkg_validator, ) .map_err(|err| { diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index fda1d9617124..4eb3fd112479 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -171,7 +171,7 @@ fn create_data_payload( )) } -fn create_early_remote_transcripts( +pub(crate) fn create_early_remote_transcripts( pool_reader: &PoolReader<'_>, crypto: &dyn ConsensusCrypto, parent: &Block, @@ -181,21 +181,23 @@ fn create_early_remote_transcripts( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Vec<(NiDkgId, CallbackId, Result)> { - // If we cannot access the state manage, we don't return an error. - // This is because the early remote transcripts is an optimization. + // If we cannot access the state manager, we don't return an error. + // This is because the early remote transcripts are an optimization. // If we don't return any transcripts here, the protocol will continue anyway - // and return the transcripts in the summary block + // and return the transcripts (or their errors) in the summary block let Ok(state) = state_manager.get_state_at(validation_context.certified_height) else { return vec![]; }; - // TODO: Since this function is relatively expensive, we should only do this if there are any outstanding - // Remote DKG contexts + // Since this function is relatively expensive, we simply return, if there are no outstanding DKG constexts + if number_of_contexts(state.get_ref()) == 0 { + return vec![]; + } // Get all dealings that have not been used in a transcript already let all_dealings = utils::get_dkg_dealings(pool_reader, parent, true); - // Collect map of remote target_ids to ni_dkg_ids + // Collect map of the dealings remote target_ids to the dkg_ids let mut remote_contexts: BTreeMap> = BTreeMap::new(); for (target_id, dkg_id) in all_dealings @@ -211,7 +213,7 @@ fn create_early_remote_transcripts( let mut selected_transcripts = vec![]; for (_, dkg_ids) in remote_contexts { - // For each target_id, try to build the necessary transcripts + // For each target_id, try to build the necessary (dkg_id, callback_id transcript) triple let mut transcripts = dkg_ids .iter() // Lookup the config from the summary @@ -226,7 +228,7 @@ fn create_early_remote_transcripts( get_callback_id_from_id(state.get_ref(), dkg_id) .map(|callback_id| (dkg_id, callback_id, config)) }) - // Generate the transcripts, not that we just skip errors, they will + // Generate the transcripts. We just skip errors, they will // be handled in the summary block, if we fail to create an early transcript .filter_map(|(dkg_id, callback_id, config)| { match create_transcript(crypto, config, &all_dealings, &logger) { @@ -841,6 +843,14 @@ fn get_callback_id_from_id(state: &ReplicatedState, id: &NiDkgId) -> Option usize { + state + .metadata + .subnet_call_context_manager + .setup_initial_dkg_contexts + .len() +} + /// This function is called for each entry on the SubnetCallContext. It returns /// either the created high and low configs for the entry or returns two errors /// identified by the NiDkgId. diff --git a/rs/consensus/src/dkg/payload_validator.rs b/rs/consensus/src/dkg/payload_validator.rs index b6e489b0f103..83985ef14fd4 100644 --- a/rs/consensus/src/dkg/payload_validator.rs +++ b/rs/consensus/src/dkg/payload_validator.rs @@ -1,5 +1,4 @@ -use std::collections::HashSet; - +use self::payload_builder::create_early_remote_transcripts; use super::{payload_builder, utils, PayloadCreationError}; use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; use ic_interfaces::{ @@ -8,6 +7,7 @@ use ic_interfaces::{ }; use ic_interfaces_registry::RegistryClient; use ic_interfaces_state_manager::StateManager; +use ic_logger::{info, ReplicaLogger}; use ic_registry_client_helpers::subnet::SubnetRegistry; use ic_replicated_state::ReplicatedState; use ic_types::{ @@ -23,6 +23,7 @@ use ic_types::{ Height, NodeId, SubnetId, }; use prometheus::IntCounterVec; +use std::collections::HashSet; /// Reasons for why a dkg payload might be invalid. // The `Debug` implementation is ignored during the dead code analysis and we are getting a `field @@ -47,6 +48,8 @@ pub(crate) enum InvalidDkgPayloadReason { limit: usize, actual: usize, }, + /// The early transcripts that were included with this payload where invalid + InvalidTranscripts, } /// Possible failures which could occur while validating a dkg payload. They don't imply that the @@ -122,6 +125,7 @@ pub(crate) fn validate_payload( payload: &BlockPayload, state_manager: &dyn StateManager, validation_context: &ValidationContext, + logger: ReplicaLogger, metrics: &IntCounterVec, ) -> ValidationResult { let current_height = parent.height.increment(); @@ -155,7 +159,7 @@ pub(crate) fn validate_payload( registry_version, state_manager, validation_context, - ic_logger::replica_logger::no_op_logger(), + logger, )?; if summary_payload.dkg != expected_summary { return Err(InvalidDkgPayloadReason::MismatchedDkgSummary( @@ -190,6 +194,9 @@ pub(crate) fn validate_payload( &data_payload.dkg, max_dealings_per_block, &parent, + state_manager, + validation_context, + logger, metrics, ) } @@ -205,6 +212,9 @@ fn validate_dealings_payload( dealings: &DkgDataPayload, max_dealings_per_payload: usize, parent: &Block, + state_manager: &dyn StateManager, + validation_context: &ValidationContext, + logger: ReplicaLogger, metrics: &IntCounterVec, ) -> ValidationResult { if dealings.start_height != parent.payload.as_ref().dkg_interval_start_height() { @@ -268,6 +278,36 @@ fn validate_dealings_payload( crypto.verify_dealing(config, message.signature.signer, &message.content.dealing)?; } + // If we have early transcripts, we compare them + if !dealings.transcripts_for_remote_subnets.is_empty() { + // TODO: We should actually compare the contents of the vectors + let expected_transcripts = create_early_remote_transcripts( + pool_reader, + crypto, + parent, + 1, + last_summary, + state_manager, + validation_context, + logger.clone(), + ); + + if dealings.transcripts_for_remote_subnets != expected_transcripts { + info!( + logger, + "Failed to validate {} early remote DKG transcripts in regular block payload", + dealings.transcripts_for_remote_subnets.len() + ); + return Err(InvalidDkgPayloadReason::InvalidTranscripts.into()); + } + + info!( + logger, + "Validated {} early remote DKG transcripts in regular block payload", + dealings.transcripts_for_remote_subnets.len() + ); + } + Ok(()) } @@ -275,6 +315,7 @@ fn validate_dealings_payload( mod tests { use super::*; use ic_consensus_mocks::{dependencies_with_subnet_params, Dependencies}; + use ic_logger::no_op_logger; use ic_metrics::MetricsRegistry; use ic_test_utilities_consensus::fake::FakeContentSigner; use ic_test_utilities_registry::SubnetRecordBuilder; @@ -342,6 +383,7 @@ mod tests { block_payload, state_manager.as_ref(), &context, + no_op_logger(), &mock_metrics(), ) .is_ok()); @@ -363,6 +405,7 @@ mod tests { summary, state_manager.as_ref(), &context, + no_op_logger(), &mock_metrics(), ) .is_ok()); @@ -558,6 +601,7 @@ mod tests { &block_payload, state_manager.as_ref(), &context, + no_op_logger(), &mock_metrics(), ); From 5ff9175a6f74a16865ff3253f86509b617a43f70 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Thu, 5 Dec 2024 13:51:28 +0000 Subject: [PATCH 28/98] WIP testing --- rs/consensus/src/dkg/payload_validator.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rs/consensus/src/dkg/payload_validator.rs b/rs/consensus/src/dkg/payload_validator.rs index 83985ef14fd4..043a629df7b0 100644 --- a/rs/consensus/src/dkg/payload_validator.rs +++ b/rs/consensus/src/dkg/payload_validator.rs @@ -278,6 +278,12 @@ fn validate_dealings_payload( crypto.verify_dealing(config, message.signature.signer, &message.content.dealing)?; } + // TODO: Remove this + info!( + logger, + "This payload contains {} early remote DKG transcripts in regular block payload!!", + dealings.transcripts_for_remote_subnets.len() + ); // If we have early transcripts, we compare them if !dealings.transcripts_for_remote_subnets.is_empty() { // TODO: We should actually compare the contents of the vectors From aef69162df88f8e29dbd34a470456c64a8820857 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Thu, 5 Dec 2024 16:11:36 +0000 Subject: [PATCH 29/98] WIp implement early remote unit test --- rs/consensus/src/dkg.rs | 113 +++++++++++++++++++++- rs/consensus/src/dkg/payload_builder.rs | 3 + rs/consensus/src/dkg/payload_validator.rs | 4 + rs/consensus/src/dkg/test_utils.rs | 26 ++++- 4 files changed, 144 insertions(+), 2 deletions(-) diff --git a/rs/consensus/src/dkg.rs b/rs/consensus/src/dkg.rs index cd9c5c01ae64..b019da4df308 100644 --- a/rs/consensus/src/dkg.rs +++ b/rs/consensus/src/dkg.rs @@ -1754,7 +1754,118 @@ mod tests { block.height.get() ); } - }) + }); + } + + #[test] + fn test_early_remote_dkg_transcripts() { + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + let node_ids = (1..4).into_iter().map(node_test_id).collect::>(); + let dkg_interval_length = 99; + let subnet_id = subnet_test_id(0); + + let Dependencies { + mut pool, + registry, + state_manager, + .. + } = dependencies_with_subnet_records_with_raw_state_manager( + pool_config, + subnet_id, + vec![( + 10, + SubnetRecordBuilder::from(&node_ids) + .with_dkg_interval_length(dkg_interval_length) + .build(), + )], + ); + + let target_id = NiDkgTargetId::new([0u8; 32]); + complement_state_manager_with_remote_dkg_requests( + state_manager, + registry.get_latest_version(), + vec![10, 11, 12, 13], + None, + Some(target_id), + ); + + // Verify that the next summary block contains the configs and no transcripts. + // This also extracts the DKG ids + pool.advance_round_normal_operation_n(dkg_interval_length + 1); + let block: Block = pool + .validated() + .block_proposal() + .get_highest() + .unwrap() + .content + .into_inner(); + let remote_dkg_ids = if block.payload.as_ref().is_summary() { + let dkg_summary = &block.payload.as_ref().as_summary().dkg; + assert!(dkg_summary.transcripts_for_remote_subnets.is_empty()); + assert_eq!(dkg_summary.configs.len(), 4); + + let remote_dkg_ids = dkg_summary + .configs + .iter() + .filter(|(id, _)| id.target_subnet == NiDkgTargetSubnet::Remote(target_id)) + .map(|(id, _)| id) + .cloned() + .collect::>(); + assert_eq!(remote_dkg_ids.len(), 2); + + remote_dkg_ids + } else { + panic!( + "block at height {} is not a summary block", + block.height.get() + ); + }; + + // TODO: Advance the pool a bit and gradually put more dealings in, check that no early remote transcripts exist + // TODO: Once sufficient dealings are in the pool, check that payload contains early remote transcripts + // TODO: Check that the payload also validates + // TODO: Advance the pool a bit further, check that the early remote transcripts are not generated multiple times + // TODO: Advanve the pool until the next DKG interval and verify that it does not contain already sent remote transcripts + todo!(); + + // Verify that the next summary block contains the transcripts and not the + // configs. + // pool.advance_round_normal_operation_n(dkg_interval_length + 1); + // let block: Block = pool + // .validated() + // .block_proposal() + // .get_highest() + // .unwrap() + // .content + // .into_inner(); + // if block.payload.as_ref().is_summary() { + // let dkg_summary = &block.payload.as_ref().as_summary().dkg; + // assert_eq!(dkg_summary.configs.len(), 2); + // assert_eq!( + // dkg_summary + // .configs + // .keys() + // .filter(|id| id.target_subnet == NiDkgTargetSubnet::Remote(target_id)) + // .count(), + // 0 + // ); + // assert_eq!( + // dkg_summary + // .transcripts_for_remote_subnets + // .iter() + // .filter( + // |(id, _, _)| id.target_subnet == NiDkgTargetSubnet::Remote(target_id) + // ) + // .count(), + // 2 + // ); + // } else { + // panic!( + // "block at height {} is not a summary block", + // block.height.get() + // ); + // } + }); } /* diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 4eb3fd112479..950f5378050e 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -137,6 +137,8 @@ fn create_data_payload( .cloned() .collect(); + dbg!(new_validated_dealings.len()); + // If we have dealings in the payload, we will not try to make transcripts as well if !new_validated_dealings.is_empty() { return Ok(DkgDataPayload::new( @@ -163,6 +165,7 @@ fn create_data_payload( remote_dkg_transcripts.len() ); } + dbg!(remote_dkg_transcripts.len()); // Try to include remote transcripts Ok(DkgDataPayload::new_with_remote_dkg_transcripts( diff --git a/rs/consensus/src/dkg/payload_validator.rs b/rs/consensus/src/dkg/payload_validator.rs index 043a629df7b0..162c55063ebc 100644 --- a/rs/consensus/src/dkg/payload_validator.rs +++ b/rs/consensus/src/dkg/payload_validator.rs @@ -128,6 +128,7 @@ pub(crate) fn validate_payload( logger: ReplicaLogger, metrics: &IntCounterVec, ) -> ValidationResult { + dbg!("Validate payload is called"); let current_height = parent.height.increment(); let registry_version = pool_reader .registry_version(current_height) @@ -217,6 +218,8 @@ fn validate_dealings_payload( logger: ReplicaLogger, metrics: &IntCounterVec, ) -> ValidationResult { + dbg!("Verifying dealings"); + if dealings.start_height != parent.payload.as_ref().dkg_interval_start_height() { return Err(InvalidDkgPayloadReason::DkgStartHeightDoesNotMatchParentBlock.into()); } @@ -284,6 +287,7 @@ fn validate_dealings_payload( "This payload contains {} early remote DKG transcripts in regular block payload!!", dealings.transcripts_for_remote_subnets.len() ); + dbg!(dealings.transcripts_for_remote_subnets.len()); // If we have early transcripts, we compare them if !dealings.transcripts_for_remote_subnets.is_empty() { // TODO: We should actually compare the contents of the vectors diff --git a/rs/consensus/src/dkg/test_utils.rs b/rs/consensus/src/dkg/test_utils.rs index cdd665069283..059cd61eccdb 100644 --- a/rs/consensus/src/dkg/test_utils.rs +++ b/rs/consensus/src/dkg/test_utils.rs @@ -3,7 +3,15 @@ use ic_replicated_state::metadata_state::subnet_call_context_manager::{ }; use ic_test_utilities::state_manager::RefMockStateManager; use ic_test_utilities_types::{ids::node_test_id, messages::RequestBuilder}; -use ic_types::{crypto::threshold_sig::ni_dkg::NiDkgTargetId, Height, RegistryVersion}; +use ic_types::{ + consensus::dkg::DealingContent, + crypto::{ + threshold_sig::ni_dkg::{NiDkgDealing, NiDkgId, NiDkgTargetId}, + BasicSig, + }, + signature::{BasicSignature, BasicSigned}, + Height, RegistryVersion, ReplicaVersion, +}; use std::sync::Arc; pub(super) fn complement_state_manager_with_remote_dkg_requests( @@ -41,3 +49,19 @@ pub(super) fn complement_state_manager_with_remote_dkg_requests( expectation.times(times); } } + +pub(super) fn create_dealing(node_idx: u64, dkg_id: NiDkgId) -> BasicSigned { + let node_id = node_test_id(node_idx); + + BasicSigned { + content: DealingContent { + version: ReplicaVersion::default(), + dealing: NiDkgDealing::dummy_dealing_for_tests(node_idx as u8), + dkg_id, + }, + signature: BasicSignature { + signature: BasicSig(vec![]).into(), + signer: node_id, + }, + } +} From 9eb80920d9fde5310c09c21ba7153b113165c18b Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Thu, 5 Dec 2024 17:18:58 +0000 Subject: [PATCH 30/98] WIP implementing test --- rs/consensus/src/dkg.rs | 29 ++++++++++++++++++++++-- rs/consensus/src/dkg/test_utils.rs | 36 +++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/rs/consensus/src/dkg.rs b/rs/consensus/src/dkg.rs index b019da4df308..ea9ae3494ea8 100644 --- a/rs/consensus/src/dkg.rs +++ b/rs/consensus/src/dkg.rs @@ -612,7 +612,10 @@ fn bootstrap_idkg_summary( #[cfg(test)] mod tests { - use super::{test_utils::complement_state_manager_with_remote_dkg_requests, *}; + use super::{ + test_utils::{complement_state_manager_with_remote_dkg_requests, create_dealing}, + *, + }; use ic_artifact_pool::dkg_pool::DkgPoolImpl; use ic_consensus_mocks::{ dependencies, dependencies_with_subnet_params, @@ -643,6 +646,7 @@ mod tests { PrincipalId, RegistryVersion, ReplicaVersion, }; use std::{collections::BTreeSet, convert::TryFrom}; + use test_utils::extract_remote_dkg_transcripts_from_highest_block; #[test] // In this test we test the creation of dealing payloads. @@ -1768,6 +1772,7 @@ mod tests { mut pool, registry, state_manager, + dkg_pool, .. } = dependencies_with_subnet_records_with_raw_state_manager( pool_config, @@ -1821,7 +1826,27 @@ mod tests { ); }; - // TODO: Advance the pool a bit and gradually put more dealings in, check that no early remote transcripts exist + // TODO: Advance the pool a bit and dealings in, check that no early remote transcripts exist + // Put validated dealings into the dkg pool + dkg_pool.write().unwrap().apply(vec![ + ChangeAction::AddToValidated(create_dealing(1, remote_dkg_ids[0].clone())), + ChangeAction::AddToValidated(create_dealing(2, remote_dkg_ids[0].clone())), + ChangeAction::AddToValidated(create_dealing(3, remote_dkg_ids[0].clone())), + ChangeAction::AddToValidated(create_dealing(4, remote_dkg_ids[0].clone())), + ChangeAction::AddToValidated(create_dealing(1, remote_dkg_ids[1].clone())), + ChangeAction::AddToValidated(create_dealing(2, remote_dkg_ids[1].clone())), + ChangeAction::AddToValidated(create_dealing(3, remote_dkg_ids[1].clone())), + ChangeAction::AddToValidated(create_dealing(4, remote_dkg_ids[1].clone())), + ]); + + for _ in 0..10 { + pool.advance_round_normal_operation(); + assert_eq!( + extract_remote_dkg_transcripts_from_highest_block(&pool).len(), + 0 + ); + } + // TODO: Once sufficient dealings are in the pool, check that payload contains early remote transcripts // TODO: Check that the payload also validates // TODO: Advance the pool a bit further, check that the early remote transcripts are not generated multiple times diff --git a/rs/consensus/src/dkg/test_utils.rs b/rs/consensus/src/dkg/test_utils.rs index 059cd61eccdb..a4fe5a1911a6 100644 --- a/rs/consensus/src/dkg/test_utils.rs +++ b/rs/consensus/src/dkg/test_utils.rs @@ -1,14 +1,17 @@ +use ic_interfaces::consensus_pool::ConsensusPool; use ic_replicated_state::metadata_state::subnet_call_context_manager::{ SetupInitialDkgContext, SubnetCallContext, }; +use ic_test_artifact_pool::consensus_pool::TestConsensusPool; use ic_test_utilities::state_manager::RefMockStateManager; use ic_test_utilities_types::{ids::node_test_id, messages::RequestBuilder}; use ic_types::{ consensus::dkg::DealingContent, crypto::{ - threshold_sig::ni_dkg::{NiDkgDealing, NiDkgId, NiDkgTargetId}, + threshold_sig::ni_dkg::{NiDkgDealing, NiDkgId, NiDkgTargetId, NiDkgTranscript}, BasicSig, }, + messages::CallbackId, signature::{BasicSignature, BasicSigned}, Height, RegistryVersion, ReplicaVersion, }; @@ -50,6 +53,7 @@ pub(super) fn complement_state_manager_with_remote_dkg_requests( } } +/// Create a dealing from the node `node_idx` pub(super) fn create_dealing(node_idx: u64, dkg_id: NiDkgId) -> BasicSigned { let node_id = node_test_id(node_idx); @@ -65,3 +69,33 @@ pub(super) fn create_dealing(node_idx: u64, dkg_id: NiDkgId) -> BasicSigned Vec<(NiDkgId, CallbackId, Result)> { + let block: ic_types::consensus::Block = pool + .validated() + .block_proposal() + .get_highest() + .unwrap() + .content + .into_inner(); + + if block.payload.as_ref().is_summary() { + &block + .payload + .as_ref() + .as_summary() + .dkg + .transcripts_for_remote_subnets + } else { + &block + .payload + .as_ref() + .as_data() + .dkg + .transcripts_for_remote_subnets + } + .clone() +} From 1e03d0397f864b42d75cdbe57fd320d5e64a2178 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 11 Dec 2024 12:23:13 +0000 Subject: [PATCH 31/98] WIP implement test --- rs/consensus/src/dkg.rs | 118 ++++++++++++++--------------- rs/consensus/src/dkg/test_utils.rs | 21 ++++- 2 files changed, 77 insertions(+), 62 deletions(-) diff --git a/rs/consensus/src/dkg.rs b/rs/consensus/src/dkg.rs index ea9ae3494ea8..5a84750de6f6 100644 --- a/rs/consensus/src/dkg.rs +++ b/rs/consensus/src/dkg.rs @@ -637,7 +637,7 @@ mod tests { use ic_test_utilities_registry::{add_subnet_record, SubnetRecordBuilder}; use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ - consensus::HasVersion, + consensus::{Block, HasVersion}, crypto::threshold_sig::ni_dkg::{ NiDkgDealing, NiDkgId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet, }, @@ -646,7 +646,7 @@ mod tests { PrincipalId, RegistryVersion, ReplicaVersion, }; use std::{collections::BTreeSet, convert::TryFrom}; - use test_utils::extract_remote_dkg_transcripts_from_highest_block; + use test_utils::{extract_dealings_from_highest_block, extract_remote_dkgs_from_highest_block}; #[test] // In this test we test the creation of dealing payloads. @@ -1826,70 +1826,68 @@ mod tests { ); }; - // TODO: Advance the pool a bit and dealings in, check that no early remote transcripts exist // Put validated dealings into the dkg pool - dkg_pool.write().unwrap().apply(vec![ - ChangeAction::AddToValidated(create_dealing(1, remote_dkg_ids[0].clone())), - ChangeAction::AddToValidated(create_dealing(2, remote_dkg_ids[0].clone())), - ChangeAction::AddToValidated(create_dealing(3, remote_dkg_ids[0].clone())), - ChangeAction::AddToValidated(create_dealing(4, remote_dkg_ids[0].clone())), - ChangeAction::AddToValidated(create_dealing(1, remote_dkg_ids[1].clone())), - ChangeAction::AddToValidated(create_dealing(2, remote_dkg_ids[1].clone())), - ChangeAction::AddToValidated(create_dealing(3, remote_dkg_ids[1].clone())), - ChangeAction::AddToValidated(create_dealing(4, remote_dkg_ids[1].clone())), - ]); - - for _ in 0..10 { - pool.advance_round_normal_operation(); - assert_eq!( - extract_remote_dkg_transcripts_from_highest_block(&pool).len(), - 0 - ); - } + // dkg_pool.write().unwrap().apply(vec![ + // ChangeAction::AddToValidated(create_dealing(1, remote_dkg_ids[0].clone())), + // ChangeAction::AddToValidated(create_dealing(2, remote_dkg_ids[0].clone())), + // ChangeAction::AddToValidated(create_dealing(3, remote_dkg_ids[0].clone())), + // ChangeAction::AddToValidated(create_dealing(4, remote_dkg_ids[0].clone())), + // ChangeAction::AddToValidated(create_dealing(1, remote_dkg_ids[1].clone())), + // ChangeAction::AddToValidated(create_dealing(2, remote_dkg_ids[1].clone())), + // ChangeAction::AddToValidated(create_dealing(3, remote_dkg_ids[1].clone())), + // ChangeAction::AddToValidated(create_dealing(4, remote_dkg_ids[1].clone())), + // ]); + + // Put a dealing in the pool and check that it gets included + // Additionally check that there are no remote transcripts + dkg_pool + .write() + .unwrap() + .apply(vec![ChangeAction::AddToValidated(create_dealing( + 1, + remote_dkg_ids[0].clone(), + ))]); + pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 1); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + + // For the next round, we put nothing into the pool + // Since there are no dealings, we will try to build a remote transcript + // This will fail, however, since we need two dealings (one high one low) + pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + + // Now we put the other dealing into the pool + // The payload buukder will include the dealing + dkg_pool + .write() + .unwrap() + .apply(vec![ChangeAction::AddToValidated(create_dealing( + 1, + remote_dkg_ids[1].clone(), + ))]); + pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 1); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + + // Now sufficient dealings are in the pool, check that payload contains early remote transcripts + // NOTE: We only need one dealing each, since we are using `CryptoReturningOk`. We should consider + // using real crypto for these tests, to make them more realistic + pool.advance_round_normal_operation(); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 2); - // TODO: Once sufficient dealings are in the pool, check that payload contains early remote transcripts // TODO: Check that the payload also validates + let block: Block = pool + .validated() + .block_proposal() + .get_highest() + .unwrap() + .content + .into_inner(); // TODO: Advance the pool a bit further, check that the early remote transcripts are not generated multiple times // TODO: Advanve the pool until the next DKG interval and verify that it does not contain already sent remote transcripts todo!(); - - // Verify that the next summary block contains the transcripts and not the - // configs. - // pool.advance_round_normal_operation_n(dkg_interval_length + 1); - // let block: Block = pool - // .validated() - // .block_proposal() - // .get_highest() - // .unwrap() - // .content - // .into_inner(); - // if block.payload.as_ref().is_summary() { - // let dkg_summary = &block.payload.as_ref().as_summary().dkg; - // assert_eq!(dkg_summary.configs.len(), 2); - // assert_eq!( - // dkg_summary - // .configs - // .keys() - // .filter(|id| id.target_subnet == NiDkgTargetSubnet::Remote(target_id)) - // .count(), - // 0 - // ); - // assert_eq!( - // dkg_summary - // .transcripts_for_remote_subnets - // .iter() - // .filter( - // |(id, _, _)| id.target_subnet == NiDkgTargetSubnet::Remote(target_id) - // ) - // .count(), - // 2 - // ); - // } else { - // panic!( - // "block at height {} is not a summary block", - // block.height.get() - // ); - // } }); } diff --git a/rs/consensus/src/dkg/test_utils.rs b/rs/consensus/src/dkg/test_utils.rs index a4fe5a1911a6..7f969dafa1d2 100644 --- a/rs/consensus/src/dkg/test_utils.rs +++ b/rs/consensus/src/dkg/test_utils.rs @@ -6,7 +6,7 @@ use ic_test_artifact_pool::consensus_pool::TestConsensusPool; use ic_test_utilities::state_manager::RefMockStateManager; use ic_test_utilities_types::{ids::node_test_id, messages::RequestBuilder}; use ic_types::{ - consensus::dkg::DealingContent, + consensus::dkg::{DealingContent, DealingMessages}, crypto::{ threshold_sig::ni_dkg::{NiDkgDealing, NiDkgId, NiDkgTargetId, NiDkgTranscript}, BasicSig, @@ -71,7 +71,7 @@ pub(super) fn create_dealing(node_idx: u64, dkg_id: NiDkgId) -> BasicSigned Vec<(NiDkgId, CallbackId, Result)> { let block: ic_types::consensus::Block = pool @@ -99,3 +99,20 @@ pub(super) fn extract_remote_dkg_transcripts_from_highest_block( } .clone() } + +/// Extract the remote dkg transcripts from the current highest validated block +pub(super) fn extract_dealings_from_highest_block(pool: &TestConsensusPool) -> DealingMessages { + let block: ic_types::consensus::Block = pool + .validated() + .block_proposal() + .get_highest() + .unwrap() + .content + .into_inner(); + + if block.payload.as_ref().is_summary() { + vec![] + } else { + block.payload.as_ref().as_data().dkg.messages.clone() + } +} From 7ed6d2879e8bb5e524c56cedac326bce608baa35 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 11 Dec 2024 13:03:31 +0000 Subject: [PATCH 32/98] WIP improve test --- rs/consensus/src/dkg.rs | 50 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/rs/consensus/src/dkg.rs b/rs/consensus/src/dkg.rs index 5a84750de6f6..d7e0e1b925d2 100644 --- a/rs/consensus/src/dkg.rs +++ b/rs/consensus/src/dkg.rs @@ -637,7 +637,7 @@ mod tests { use ic_test_utilities_registry::{add_subnet_record, SubnetRecordBuilder}; use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ - consensus::{Block, HasVersion}, + consensus::{Block, HasHeight, HasVersion}, crypto::threshold_sig::ni_dkg::{ NiDkgDealing, NiDkgId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet, }, @@ -645,6 +645,7 @@ mod tests { time::UNIX_EPOCH, PrincipalId, RegistryVersion, ReplicaVersion, }; + use payload_validator::validate_payload; use std::{collections::BTreeSet, convert::TryFrom}; use test_utils::{extract_dealings_from_highest_block, extract_remote_dkgs_from_highest_block}; @@ -1773,6 +1774,7 @@ mod tests { registry, state_manager, dkg_pool, + crypto, .. } = dependencies_with_subnet_records_with_raw_state_manager( pool_config, @@ -1787,7 +1789,7 @@ mod tests { let target_id = NiDkgTargetId::new([0u8; 32]); complement_state_manager_with_remote_dkg_requests( - state_manager, + state_manager.clone(), registry.get_latest_version(), vec![10, 11, 12, 13], None, @@ -1826,18 +1828,6 @@ mod tests { ); }; - // Put validated dealings into the dkg pool - // dkg_pool.write().unwrap().apply(vec![ - // ChangeAction::AddToValidated(create_dealing(1, remote_dkg_ids[0].clone())), - // ChangeAction::AddToValidated(create_dealing(2, remote_dkg_ids[0].clone())), - // ChangeAction::AddToValidated(create_dealing(3, remote_dkg_ids[0].clone())), - // ChangeAction::AddToValidated(create_dealing(4, remote_dkg_ids[0].clone())), - // ChangeAction::AddToValidated(create_dealing(1, remote_dkg_ids[1].clone())), - // ChangeAction::AddToValidated(create_dealing(2, remote_dkg_ids[1].clone())), - // ChangeAction::AddToValidated(create_dealing(3, remote_dkg_ids[1].clone())), - // ChangeAction::AddToValidated(create_dealing(4, remote_dkg_ids[1].clone())), - // ]); - // Put a dealing in the pool and check that it gets included // Additionally check that there are no remote transcripts dkg_pool @@ -1877,7 +1867,7 @@ mod tests { pool.advance_round_normal_operation(); assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 2); - // TODO: Check that the payload also validates + // Check that the payload also validates let block: Block = pool .validated() .block_proposal() @@ -1885,8 +1875,36 @@ mod tests { .unwrap() .content .into_inner(); + let pool_reader = PoolReader::new(&pool); + + let parent = &block.parent; + let height = block.height().decrement(); + let parent = pool_reader + .get_notarized_block(parent, height) + .map(|block| block.into_inner()) + .unwrap(); + + assert!(validate_payload( + subnet_test_id(0), + registry.as_ref(), + crypto.as_ref(), + &pool_reader, + &*dkg_pool.read().unwrap(), + parent, + &block.payload.as_ref(), + state_manager.as_ref(), + &block.context, + no_op_logger(), + &MetricsRegistry::new().int_counter_vec( + "consensus_dkg_validator", + "DKG validator counter", + &["type"], + ) + ) + .is_ok()); + // TODO: Advance the pool a bit further, check that the early remote transcripts are not generated multiple times - // TODO: Advanve the pool until the next DKG interval and verify that it does not contain already sent remote transcripts + // TODO: Advance the pool until the next DKG interval and verify that it does not contain already sent remote transcripts todo!(); }); } From 2fd18458480ce4dace65cc282e412edd310e1159 Mon Sep 17 00:00:00 2001 From: Leon Tan Date: Wed, 11 Dec 2024 13:38:19 +0000 Subject: [PATCH 33/98] WIP working with tests --- rs/consensus/src/dkg.rs | 10 +++++++--- rs/consensus/src/dkg/payload_builder.rs | 10 +++++++++- rs/consensus/src/dkg/test_utils.rs | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/rs/consensus/src/dkg.rs b/rs/consensus/src/dkg.rs index d7e0e1b925d2..a68c54d1c1bd 100644 --- a/rs/consensus/src/dkg.rs +++ b/rs/consensus/src/dkg.rs @@ -1903,9 +1903,13 @@ mod tests { ) .is_ok()); - // TODO: Advance the pool a bit further, check that the early remote transcripts are not generated multiple times - // TODO: Advance the pool until the next DKG interval and verify that it does not contain already sent remote transcripts - todo!(); + // Advance the pool a until the next DKG, check that the early remote transcripts are not generated multiple times + // including that they are not included in the summary + for _ in 0..100 { + pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + } }); } diff --git a/rs/consensus/src/dkg/payload_builder.rs b/rs/consensus/src/dkg/payload_builder.rs index 950f5378050e..a1ea580420c5 100644 --- a/rs/consensus/src/dkg/payload_builder.rs +++ b/rs/consensus/src/dkg/payload_builder.rs @@ -316,11 +316,15 @@ pub(super) fn create_summary_payload( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { - let all_dealings = utils::get_dkg_dealings(pool_reader, parent, false); + dbg!("Creating summary payload"); + let all_dealings = utils::get_dkg_dealings(pool_reader, parent, true); + dbg!(&all_dealings.len()); let mut transcripts_for_remote_subnets = BTreeMap::new(); let mut next_transcripts = BTreeMap::new(); // Try to create transcripts from the last round. for (dkg_id, config) in last_summary.configs.iter() { + // TODO: Filter out configs that no longer have a matching context + match create_transcript(crypto, config, &all_dealings, &logger) { Ok(transcript) => { let previous_value_found = if dkg_id.target_subnet == NiDkgTargetSubnet::Local { @@ -353,6 +357,7 @@ pub(super) fn create_summary_payload( let height = parent.height.increment(); + dbg!(transcripts_for_remote_subnets.len()); let (mut configs, transcripts_for_remote_subnets, initial_dkg_attempts) = compute_remote_dkg_data( subnet_id, @@ -369,6 +374,7 @@ pub(super) fn create_summary_payload( &last_summary.initial_dkg_attempts, &logger, )?; + dbg!(transcripts_for_remote_subnets.len()); let interval_length = last_summary.next_interval_length; let next_interval_length = get_dkg_interval_length( @@ -428,6 +434,8 @@ fn create_transcript( let no_dealings = BTreeMap::new(); let dealings = all_dealings.get(config.dkg_id()).unwrap_or(&no_dealings); + // TODO: We need to check that we are not erroring on insufficient dealings here + ic_interfaces::crypto::NiDkgAlgorithm::create_transcript(crypto, config, dealings) } diff --git a/rs/consensus/src/dkg/test_utils.rs b/rs/consensus/src/dkg/test_utils.rs index 7f969dafa1d2..6b3f6803e431 100644 --- a/rs/consensus/src/dkg/test_utils.rs +++ b/rs/consensus/src/dkg/test_utils.rs @@ -100,7 +100,7 @@ pub(super) fn extract_remote_dkgs_from_highest_block( .clone() } -/// Extract the remote dkg transcripts from the current highest validated block +/// Extract the dealings from the current highest validated block pub(super) fn extract_dealings_from_highest_block(pool: &TestConsensusPool) -> DealingMessages { let block: ic_types::consensus::Block = pool .validated() From aee217ac0a6c1831fc0a45c87eebaa82da714bb2 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 15 Jan 2026 11:02:18 +0000 Subject: [PATCH 34/98] compiling --- rs/consensus/dkg/src/lib.rs | 7 +- rs/consensus/dkg/src/payload_builder.rs | 9 +- rs/consensus/dkg/src/payload_validator.rs | 112 ++----------------- rs/consensus/dkg/src/test_utils.rs | 17 ++- rs/consensus/dkg/src/utils.rs | 77 ++++++++----- rs/consensus/src/consensus/batch_delivery.rs | 2 +- rs/consensus/src/consensus/validator.rs | 1 - rs/types/types/src/consensus/dkg.rs | 2 + 8 files changed, 90 insertions(+), 137 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 0e9637fba725..71ee85c6218e 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -411,6 +411,7 @@ mod tests { p2p::consensus::{MutablePool, UnvalidatedArtifact}, }; use ic_interfaces_registry::RegistryClient; + use ic_logger::no_op_logger; use ic_management_canister_types_private::{MasterPublicKeyId, VetKdCurve, VetKdKeyId}; use ic_metrics::MetricsRegistry; use ic_registry_subnet_features::{ChainKeyConfig, KeyConfig}; @@ -420,7 +421,7 @@ mod tests { use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ RegistryVersion, ReplicaVersion, - consensus::{Block, BlockPayload, HasHeight, HasVersion}, + consensus::{Block, BlockPayload, HasHeight}, crypto::threshold_sig::ni_dkg::{ NiDkgId, NiDkgMasterPublicKeyId, NiDkgTargetId, NiDkgTargetSubnet, }, @@ -1735,12 +1736,12 @@ mod tests { &block.payload.as_ref(), state_manager.as_ref(), &block.context, - no_op_logger(), &MetricsRegistry::new().int_counter_vec( "consensus_dkg_validator", "DKG validator counter", &["type"], - ) + ), + &no_op_logger(), ) .is_ok() ); diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index f840717136bf..24df689dca65 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -245,7 +245,10 @@ pub(crate) fn create_early_remote_transcripts( .iter() .map(|(dkg_id, _, _)| dkg_id.dkg_tag.clone()) .collect::>(); - let expected_tags = TAGS.iter().cloned().collect::>(); + let expected_tags: BTreeSet<_> = [NiDkgTag::LowThreshold, NiDkgTag::HighThreshold] + .iter() + .cloned() + .collect(); if tags != expected_tags { continue; @@ -301,7 +304,7 @@ pub(super) fn create_summary_payload( state_manager: &dyn StateManager, validation_context: &ValidationContext, logger: ReplicaLogger, -) -> Result { +) -> Result { dbg!("Creating summary payload"); let all_dealings = utils::get_dkg_dealings(pool_reader, parent, true); dbg!(&all_dealings.len()); @@ -928,7 +931,7 @@ fn process_setup_initial_dkg_contexts( // Dealers must be in the same registry_version. let dealers = get_node_list(this_subnet_id, registry_client, context.registry_version)?; - match create_low_high_remote_dkg_configs( + match create_remote_dkg_configs( start_block_height, this_subnet_id, context.target_id, diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index 673d7636aa06..737802ec36f4 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -1,5 +1,5 @@ use self::payload_builder::create_early_remote_transcripts; -use super::{PayloadCreationError, crypto_validate_dealing, payload_builder, utils}; +use super::{crypto_validate_dealing, payload_builder, utils}; use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; use ic_interfaces::{ dkg::{DkgPayloadValidationError, DkgPool}, @@ -11,108 +11,16 @@ use ic_logger::{ReplicaLogger, info, warn}; use ic_registry_client_helpers::subnet::SubnetRegistry; use ic_replicated_state::ReplicatedState; use ic_types::{ - Height, NodeId, SubnetId, + SubnetId, batch::ValidationContext, consensus::{ Block, BlockPayload, - dkg::{DkgDataPayload, DkgPayloadCreationError, DkgSummary}, + dkg::{DkgDataPayload, DkgPayloadValidationFailure, DkgSummary, InvalidDkgPayloadReason}, }, - crypto::{ - CryptoError, threshold_sig::ni_dkg::errors::verify_dealing_error::DkgVerifyDealingError, - }, - registry::RegistryClientError, }; use prometheus::IntCounterVec; use std::collections::HashSet; -/// Reasons for why a dkg payload might be invalid. -// The `Debug` implementation is ignored during the dead code analysis and we are getting a `field -// is never used` warning on this enum even though we are implicitly reading them when we log the -// enum. See https://github.com/rust-lang/rust/issues/88900 -#[allow(dead_code)] -#[derive(PartialEq, Debug)] -pub(crate) enum InvalidDkgPayloadReason { - CryptoError(CryptoError), - DkgVerifyDealingError(DkgVerifyDealingError), - MismatchedDkgSummary(DkgSummary, DkgSummary), - MissingDkgConfigForDealing, - DkgStartHeightDoesNotMatchParentBlock, - DkgSummaryAtNonStartHeight(Height), - DkgDealingAtStartHeight(Height), - InvalidDealer(NodeId), - DealerAlreadyDealt(NodeId), - /// There are multiple dealings from the same dealer in the payload. - DuplicateDealers, - /// The number of dealings in the payload exceeds the maximum allowed number of dealings. - TooManyDealings { - limit: usize, - actual: usize, - }, - /// The early transcripts that were included with this payload where invalid - InvalidTranscripts, -} - -/// Possible failures which could occur while validating a dkg payload. They don't imply that the -/// payload is invalid. -#[allow(dead_code)] -#[derive(PartialEq, Debug)] -pub(crate) enum DkgPayloadValidationFailure { - PayloadCreationFailed(PayloadCreationError), - /// Crypto related errors. - CryptoError(CryptoError), - DkgVerifyDealingError(DkgVerifyDealingError), - FailedToGetMaxDealingsPerBlock(RegistryClientError), - FailedToGetRegistryVersion, -} - -/// Dkg errors. -pub(crate) type PayloadValidationError = - ValidationError; - -impl From for InvalidDkgPayloadReason { - fn from(err: DkgVerifyDealingError) -> Self { - InvalidDkgPayloadReason::DkgVerifyDealingError(err) - } -} - -impl From for DkgPayloadValidationFailure { - fn from(err: DkgVerifyDealingError) -> Self { - DkgPayloadValidationFailure::DkgVerifyDealingError(err) - } -} - -impl From for InvalidDkgPayloadReason { - fn from(err: CryptoError) -> Self { - InvalidDkgPayloadReason::CryptoError(err) - } -} - -impl From for DkgPayloadValidationFailure { - fn from(err: CryptoError) -> Self { - DkgPayloadValidationFailure::CryptoError(err) - } -} - -impl From for PayloadValidationError { - fn from(err: InvalidDkgPayloadReason) -> Self { - PayloadValidationError::InvalidArtifact(err) - } -} - -impl From for PayloadValidationError { - fn from(err: DkgPayloadValidationFailure) -> Self { - PayloadValidationError::ValidationFailed(err) - } -} - -impl From for PayloadValidationError { - fn from(err: DkgPayloadCreationError) -> Self { - PayloadValidationError::ValidationFailed( - DkgPayloadValidationFailure::PayloadCreationFailed(err), - ) - } -} - /// Validates the DKG payload. The parent block is expected to be a valid block. #[allow(clippy::too_many_arguments)] #[allow(clippy::result_large_err)] @@ -160,7 +68,7 @@ pub fn validate_payload( registry_version, state_manager, validation_context, - logger, + log.clone(), )?; if summary_payload.dkg != expected_summary { return Err(InvalidDkgPayloadReason::MismatchedDkgSummary( @@ -218,7 +126,7 @@ pub fn validate_payload( &parent, state_manager, validation_context, - logger, + log, metrics, ) } @@ -237,7 +145,7 @@ fn validate_dealings_payload( parent: &Block, state_manager: &dyn StateManager, validation_context: &ValidationContext, - logger: ReplicaLogger, + log: &ReplicaLogger, metrics: &IntCounterVec, ) -> ValidationResult { if dealings.start_height != parent.payload.as_ref().dkg_interval_start_height() { @@ -294,7 +202,7 @@ fn validate_dealings_payload( // TODO: Remove this info!( - logger, + log, "This payload contains {} early remote DKG transcripts in regular block payload!!", dealings.transcripts_for_remote_subnets.len() ); @@ -310,12 +218,12 @@ fn validate_dealings_payload( last_summary, state_manager, validation_context, - logger.clone(), + log.clone(), ); if dealings.transcripts_for_remote_subnets != expected_transcripts { info!( - logger, + log, "Failed to validate {} early remote DKG transcripts in regular block payload", dealings.transcripts_for_remote_subnets.len() ); @@ -323,7 +231,7 @@ fn validate_dealings_payload( } info!( - logger, + log, "Validated {} early remote DKG transcripts in regular block payload", dealings.transcripts_for_remote_subnets.len() ); diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index 3f8d1b534ed2..ad72aff281ca 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -1,11 +1,22 @@ +use ic_interfaces::consensus_pool::ConsensusPool; use ic_interfaces_state_manager::Labeled; use ic_management_canister_types_private::{MasterPublicKeyId, VetKdKeyId}; use ic_replicated_state::metadata_state::subnet_call_context_manager::{ ReshareChainKeyContext, SetupInitialDkgContext, SubnetCallContext, }; +use ic_test_artifact_pool::consensus_pool::TestConsensusPool; use ic_test_utilities::state_manager::RefMockStateManager; use ic_test_utilities_types::{ids::node_test_id, messages::RequestBuilder}; -use ic_types::{Height, RegistryVersion, crypto::threshold_sig::ni_dkg::NiDkgTargetId}; +use ic_types::{ + Height, RegistryVersion, ReplicaVersion, + consensus::dkg::{DealingContent, DealingMessages}, + crypto::{ + BasicSig, + threshold_sig::ni_dkg::{NiDkgDealing, NiDkgId, NiDkgTargetId, NiDkgTranscript}, + }, + messages::CallbackId, + signature::{BasicSignature, BasicSigned}, +}; use std::sync::Arc; pub(super) fn complement_state_manager_with_setup_initial_dkg_request( @@ -119,7 +130,9 @@ pub(super) fn create_dealing(node_idx: u64, dkg_id: NiDkgId) -> BasicSigned, block: &Block, ) -> HashSet<(NiDkgId, NodeId)> { - get_dkg_dealings(pool_reader, block) + get_dkg_dealings(pool_reader, block, false) .into_iter() .flat_map(|(dkg_id, dealings)| { dealings @@ -96,35 +96,62 @@ pub(super) fn get_dealers_from_chain( .collect() } -// Starts with the given block and creates a nested mapping from the DKG Id to -// the node Id to the dealing. This function panics if multiple dealings -// from one dealer are discovered, hence, we assume a valid block chain. +/// Starts with the given block and creates a nested mapping from the DKG Id to +/// the node Id to the dealing. This function panics if multiple dealings +/// from one dealer are discovered, hence, we assume a valid block chain. +/// It also excludes dealings for ni_dkg ids, which already have a transcript in the +/// blockchain. pub(super) fn get_dkg_dealings( pool_reader: &PoolReader<'_>, block: &Block, + exclude_used: bool, ) -> BTreeMap> { - pool_reader + let mut dealings: BTreeMap> = BTreeMap::new(); + let mut used_dealings: BTreeSet = BTreeSet::new(); + + // Note that the chain iterator is guaranteed to iterate from + // newest to oldest blocks and that transcripts can not appear before their dealings. + for block in pool_reader .chain_iterator(block.clone()) .take_while(|block| !block.payload.is_summary()) - .fold(Default::default(), |mut acc, block| { - block - .payload - .as_ref() - .as_data() - .dkg - .messages - .iter() - .for_each(|msg| { - let collected_dealings = acc.entry(msg.content.dkg_id.clone()).or_default(); - assert!( - collected_dealings - .insert(msg.signature.signer, msg.content.dealing.clone()) - .is_none(), - "Dealings from the same dealers discovered." - ); - }); - acc - }) + { + let payload = &block.payload.as_ref().as_data().dkg; + + if exclude_used { + // Update used dealings + used_dealings.extend( + payload + .transcripts_for_remote_subnets + .iter() + .map(|transcript| transcript.0.clone()), + ); + } + + // Find new dealings in this payload + for (signer, ni_dkg_id, dealing) in payload + .messages + .iter() + // Filer out if they are already used + .filter(|message| !used_dealings.contains(&message.content.dkg_id)) + .map(|message| { + ( + message.signature.signer, + message.content.dkg_id.clone(), + message.content.dealing.clone(), + ) + }) + { + let entry = dealings.entry(ni_dkg_id).or_default(); + let old_entry = entry.insert(signer, dealing); + + assert!( + old_entry.is_none(), + "Dealings from the same dealers discovered." + ); + } + } + + dealings } /// Fetch all key ids for which the subnet should hold a key diff --git a/rs/consensus/src/consensus/batch_delivery.rs b/rs/consensus/src/consensus/batch_delivery.rs index 4a0659574b64..59d4aa312bdd 100644 --- a/rs/consensus/src/consensus/batch_delivery.rs +++ b/rs/consensus/src/consensus/batch_delivery.rs @@ -332,7 +332,7 @@ fn generate_responses_to_subnet_calls( } else { let block_payload = block_payload.as_ref().as_data(); - consensus_responses.append(&mut generate_responses_to_setup_initial_dkg_calls( + consensus_responses.append(&mut generate_responses_to_remote_dkgs( &block_payload.dkg.transcripts_for_remote_subnets, log, )); diff --git a/rs/consensus/src/consensus/validator.rs b/rs/consensus/src/consensus/validator.rs index f4f3ca682564..a3d9c602ab20 100644 --- a/rs/consensus/src/consensus/validator.rs +++ b/rs/consensus/src/consensus/validator.rs @@ -1336,7 +1336,6 @@ impl Validator { proposal.payload.as_ref(), self.state_manager.as_ref(), &proposal.context, - self.log.clone(), &self.metrics.dkg_validator, &self.log, ) diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index 64cee7fe85da..3dfefac70969 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -620,6 +620,8 @@ pub enum InvalidDkgPayloadReason { limit: usize, actual: usize, }, + /// The early transcripts that were included with this payload where invalid + InvalidTranscripts, } /// Possible failures which could occur while validating a dkg payload. They don't imply that the From 3004f2eeb027f4af26c583a1b9af8dbe548bf00f Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 15 Jan 2026 11:11:55 +0000 Subject: [PATCH 35/98] clippy --- rs/consensus/dkg/src/payload_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 24df689dca65..c0f122ad5b3c 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1012,7 +1012,7 @@ fn get_callback_id_from_id(state: &ReplicatedState, id: &NiDkgId) -> Option usize { From 6b501aefe5a05cc89f1e8cc473e830a088f2fff7 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 15 Jan 2026 14:51:38 +0000 Subject: [PATCH 36/98] clean --- rs/consensus/dkg/src/lib.rs | 4 +- rs/consensus/dkg/src/payload_builder.rs | 79 ++++++++++++++++++++----- rs/consensus/dkg/src/utils.rs | 3 +- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 71ee85c6218e..d34c197a6fdd 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -1606,7 +1606,7 @@ mod tests { #[test] fn test_early_remote_dkg_transcripts() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { - let node_ids = (1..4).into_iter().map(node_test_id).collect::>(); + let node_ids = (1..4).map(node_test_id).collect::>(); let dkg_interval_length = 99; let subnet_id = subnet_test_id(0); @@ -1733,7 +1733,7 @@ mod tests { &pool_reader, &*dkg_pool.read().unwrap(), parent, - &block.payload.as_ref(), + block.payload.as_ref(), state_manager.as_ref(), &block.context, &MetricsRegistry::new().int_counter_vec( diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index c0f122ad5b3c..b74b492c7104 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -178,7 +178,7 @@ pub(crate) fn create_early_remote_transcripts( return vec![]; }; - // Since this function is relatively expensive, we simply return, if there are no outstanding DKG constexts + // Since this function is relatively expensive, we simply return, if there are no outstanding DKG contexts if number_of_contexts(state.get_ref()) == 0 { return vec![]; } @@ -202,7 +202,7 @@ pub(crate) fn create_early_remote_transcripts( let mut selected_transcripts = vec![]; for (_, dkg_ids) in remote_contexts { - // For each target_id, try to build the necessary (dkg_id, callback_id transcript) triple + // For each target_id, try to build the necessary (dkg_id, callback_id, transcript) triple let mut transcripts = dkg_ids .iter() // Lookup the config from the summary @@ -233,9 +233,17 @@ pub(crate) fn create_early_remote_transcripts( // Here we do some matching, to check that we have the right number of configs match transcripts.len() { 1 => { - // TODO: With vetkd, we need to check that these have a HighTresholdForMasterPublicKeyId tag - // Sould we also check that the keys exists? - continue; + // For VetKD, we need to check that these have a HighThresholdForKey tag + let dkg_id = &transcripts[0].0; + match &dkg_id.dkg_tag { + NiDkgTag::HighThresholdForKey(_) => { + // This is a valid VetKD transcript, include it + } + _ => { + // Single transcript that is not VetKD, skip it + continue; + } + } } // If we have two transcripts for the same ID, we check that it is one low and one high threshold transcript // Note: We do not really need to check whether there is an actual context, since this will happen later when we map @@ -511,7 +519,6 @@ fn compute_remote_dkg_data( let mut expected_configs = Vec::new(); for config in low_high_threshold_configs { let dkg_id = config.dkg_id(); - // Check if we have a transcript in the previous summary for this config, and // if we do, move it to the new summary. if let Some((id, transcript)) = previous_transcripts @@ -520,11 +527,10 @@ fn compute_remote_dkg_data( { new_transcripts.insert(id.clone(), transcript.clone()); } - - // We check if we computed a transcript for this config in the last round. And + // If not, we check if we computed a transcript for this config in the last round. And // if not, we move the config into the new summary so that we try again in // the next round. - if !new_transcripts + else if !new_transcripts .iter() .any(|(id, _)| eq_sans_height(id, dkg_id)) { @@ -931,7 +937,7 @@ fn process_setup_initial_dkg_contexts( // Dealers must be in the same registry_version. let dealers = get_node_list(this_subnet_id, registry_client, context.registry_version)?; - match create_remote_dkg_configs( + match create_low_high_remote_dkg_configs( start_block_height, this_subnet_id, context.target_id, @@ -998,18 +1004,54 @@ fn add_callback_ids_to_transcript_results( } fn get_callback_id_from_id(state: &ReplicatedState, id: &NiDkgId) -> Option { + // First check setup_initial_dkg_contexts let setup_initial_dkg_contexts = &state .metadata .subnet_call_context_manager .setup_initial_dkg_contexts; - setup_initial_dkg_contexts + if let Some(callback_id) = setup_initial_dkg_contexts .iter() .filter_map(|(callback_id, context)| { - if NiDkgTargetSubnet::Remote(context.target_id) == id.target_subnet { - Some(*callback_id) - } else { - None + (NiDkgTargetSubnet::Remote(context.target_id) == id.target_subnet) + .then_some(*callback_id) + }) + .next_back() + { + return Some(callback_id); + } + + // Then check reshare_chain_key_contexts (for VetKD) + // For VetKD, we need to match both target_id and key_id + let reshare_chain_key_contexts = &state + .metadata + .subnet_call_context_manager + .reshare_chain_key_contexts; + + reshare_chain_key_contexts + .iter() + .filter_map(|(callback_id, context)| { + // Match target_subnet + if NiDkgTargetSubnet::Remote(context.target_id) != id.target_subnet { + return None; + } + // For VetKD, also match the key_id from the dkg_tag + match &id.dkg_tag { + NiDkgTag::HighThresholdForKey(dkg_key_id) => { + // Try to convert context.key_id to NiDkgMasterPublicKeyId and compare + if let Ok(context_key_id) = + NiDkgMasterPublicKeyId::try_from(context.key_id.clone()) + { + if context_key_id == *dkg_key_id { + Some(*callback_id) + } else { + None + } + } else { + None + } + } + _ => None, } }) .next_back() @@ -1021,12 +1063,17 @@ fn number_of_contexts(state: &ReplicatedState) -> usize { .subnet_call_context_manager .setup_initial_dkg_contexts .len() + + state + .metadata + .subnet_call_context_manager + .reshare_chain_key_contexts + .len() } /// This function is called for each entry on the SubnetCallContext. It returns /// either the created high and low configs for the entry or returns two errors /// identified by the NiDkgId. -fn create_remote_dkg_configs( +fn create_low_high_remote_dkg_configs( start_block_height: Height, dealer_subnet: SubnetId, target_subnet: NiDkgTargetId, diff --git a/rs/consensus/dkg/src/utils.rs b/rs/consensus/dkg/src/utils.rs index 035baf51a841..5c70e620ef63 100644 --- a/rs/consensus/dkg/src/utils.rs +++ b/rs/consensus/dkg/src/utils.rs @@ -123,6 +123,7 @@ pub(super) fn get_dkg_dealings( payload .transcripts_for_remote_subnets .iter() + .filter(|transcript| transcript.2.is_ok()) .map(|transcript| transcript.0.clone()), ); } @@ -131,7 +132,7 @@ pub(super) fn get_dkg_dealings( for (signer, ni_dkg_id, dealing) in payload .messages .iter() - // Filer out if they are already used + // Filter out if they are already used .filter(|message| !used_dealings.contains(&message.content.dkg_id)) .map(|message| { ( From 00abdea444c5063052112445faed29eda25165e6 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 15 Jan 2026 14:58:51 +0000 Subject: [PATCH 37/98] comment --- rs/consensus/dkg/src/payload_builder.rs | 7 +++---- rs/consensus/dkg/src/payload_validator.rs | 10 +--------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index b74b492c7104..b353bbe1a0c4 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -178,7 +178,7 @@ pub(crate) fn create_early_remote_transcripts( return vec![]; }; - // Since this function is relatively expensive, we simply return, if there are no outstanding DKG contexts + // Since this function is relatively expensive, we simply return if there are no outstanding DKG contexts if number_of_contexts(state.get_ref()) == 0 { return vec![]; } @@ -245,9 +245,8 @@ pub(crate) fn create_early_remote_transcripts( } } } - // If we have two transcripts for the same ID, we check that it is one low and one high threshold transcript - // Note: We do not really need to check whether there is an actual context, since this will happen later when we map - // the transcripts to callback ids + // If we have two transcripts for the same ID, we check that it is + // one low and one high threshold transcript 2 => { let tags = transcripts .iter() diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index 737802ec36f4..cacefb6915ea 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -200,16 +200,8 @@ fn validate_dealings_payload( crypto_validate_dealing(crypto, config, message)?; } - // TODO: Remove this - info!( - log, - "This payload contains {} early remote DKG transcripts in regular block payload!!", - dealings.transcripts_for_remote_subnets.len() - ); - dbg!(dealings.transcripts_for_remote_subnets.len()); // If we have early transcripts, we compare them if !dealings.transcripts_for_remote_subnets.is_empty() { - // TODO: We should actually compare the contents of the vectors let expected_transcripts = create_early_remote_transcripts( pool_reader, crypto, @@ -222,7 +214,7 @@ fn validate_dealings_payload( ); if dealings.transcripts_for_remote_subnets != expected_transcripts { - info!( + warn!( log, "Failed to validate {} early remote DKG transcripts in regular block payload", dealings.transcripts_for_remote_subnets.len() From 7864e1def15439fca2d5bbbf44b99cdd47d94d75 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 15 Jan 2026 15:24:06 +0000 Subject: [PATCH 38/98] fix test --- rs/consensus/dkg/src/lib.rs | 10 ++++-- rs/consensus/dkg/src/payload_builder.rs | 6 ---- rs/consensus/dkg/src/test_utils.rs | 47 +++++++++++++------------ 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index d34c197a6fdd..20bba6ce4858 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -1629,12 +1629,14 @@ mod tests { ); let target_id = NiDkgTargetId::new([0u8; 32]); + let target_id_mutex = Arc::new(Mutex::new(Some(target_id))); + complement_state_manager_with_remote_dkg_requests( state_manager.clone(), registry.get_latest_version(), vec![10, 11, 12, 13], None, - Some(target_id), + target_id_mutex.clone(), ); // Verify that the next summary block contains the configs and no transcripts. @@ -1690,7 +1692,7 @@ mod tests { assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); // Now we put the other dealing into the pool - // The payload buukder will include the dealing + // The payload builder will include the dealing dkg_pool .write() .unwrap() @@ -1706,6 +1708,7 @@ mod tests { // NOTE: We only need one dealing each, since we are using `CryptoReturningOk`. We should consider // using real crypto for these tests, to make them more realistic pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 2); // Check that the payload also validates @@ -1746,6 +1749,9 @@ mod tests { .is_ok() ); + // Simulate the delivery of the block, which removes to context from the state + target_id_mutex.lock().unwrap().take(); + // Advance the pool a until the next DKG, check that the early remote transcripts are not generated multiple times // including that they are not included in the summary for _ in 0..100 { diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index b353bbe1a0c4..0b44c120982d 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -123,8 +123,6 @@ fn create_data_payload( .cloned() .collect(); - dbg!(new_validated_dealings.len()); - // If we have dealings in the payload, we will not try to make transcripts as well if !new_validated_dealings.is_empty() { return Ok(DkgDataPayload::new( @@ -151,7 +149,6 @@ fn create_data_payload( remote_dkg_transcripts.len() ); } - dbg!(remote_dkg_transcripts.len()); // Try to include remote transcripts Ok(DkgDataPayload::new_with_remote_dkg_transcripts( @@ -312,9 +309,7 @@ pub(super) fn create_summary_payload( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { - dbg!("Creating summary payload"); let all_dealings = utils::get_dkg_dealings(pool_reader, parent, true); - dbg!(&all_dealings.len()); let mut transcripts_for_remote_subnets = BTreeMap::new(); let mut next_transcripts = BTreeMap::new(); // Try to create transcripts from the last round. @@ -405,7 +400,6 @@ pub(super) fn create_summary_payload( &last_summary.initial_dkg_attempts, &logger, )?; - dbg!(transcripts_for_remote_subnets.len()); let interval_length = last_summary.next_interval_length; let next_interval_length = get_dkg_interval_length( diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index ad72aff281ca..3e5c3ea95be8 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -17,7 +17,10 @@ use ic_types::{ messages::CallbackId, signature::{BasicSignature, BasicSigned}, }; -use std::sync::Arc; +use std::{ + collections::BTreeSet, + sync::{Arc, Mutex}, +}; pub(super) fn complement_state_manager_with_setup_initial_dkg_request( state_manager: Arc, @@ -92,32 +95,30 @@ pub(super) fn complement_state_manager_with_remote_dkg_requests( registry_version: RegistryVersion, node_ids: Vec, times: Option, - target: Option, + target: Arc>>, ) { - let mut state = ic_test_utilities_state::get_initial_state(0, 0); - // Add the context into state_manager. - let nodes_in_target_subnet = node_ids.into_iter().map(node_test_id).collect(); - - if let Some(target_id) = target { - state.metadata.subnet_call_context_manager.push_context( - SubnetCallContext::SetupInitialDKG(SetupInitialDkgContext { - request: RequestBuilder::new().build(), - nodes_in_target_subnet, - target_id, - registry_version, - time: state.time(), - }), - ); - } + let nodes_in_target_subnet: BTreeSet<_> = node_ids.into_iter().map(node_test_id).collect(); let mut mock = state_manager.get_mut(); - let expectation = - mock.expect_get_state_at() - .return_const(Ok(ic_interfaces_state_manager::Labeled::new( - Height::new(0), - Arc::new(state), - ))); + let expectation = mock.expect_get_state_at().returning(move |_| { + let mut state = ic_test_utilities_state::get_initial_state(0, 0); + if let Some(target_id) = target.lock().unwrap().as_ref() { + state.metadata.subnet_call_context_manager.push_context( + SubnetCallContext::SetupInitialDKG(SetupInitialDkgContext { + request: RequestBuilder::new().build(), + nodes_in_target_subnet: nodes_in_target_subnet.clone(), + target_id: target_id.clone(), + registry_version, + time: state.time(), + }), + ); + } + Ok(ic_interfaces_state_manager::Labeled::new( + Height::new(0), + Arc::new(state), + )) + }); if let Some(times) = times { expectation.times(times); } From bd97a328f48fc126f8c8b6ba726c3a3a2f38569e Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 15 Jan 2026 15:45:12 +0000 Subject: [PATCH 39/98] fix --- rs/consensus/dkg/src/test_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index 3e5c3ea95be8..cef79d29ebb6 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -108,7 +108,7 @@ pub(super) fn complement_state_manager_with_remote_dkg_requests( SubnetCallContext::SetupInitialDKG(SetupInitialDkgContext { request: RequestBuilder::new().build(), nodes_in_target_subnet: nodes_in_target_subnet.clone(), - target_id: target_id.clone(), + target_id: *target_id, registry_version, time: state.time(), }), From 1e126d7330382e9405cd7e052d2e1d5eac9cb096 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 16 Jan 2026 13:40:01 +0000 Subject: [PATCH 40/98] clean --- rs/consensus/dkg/src/payload_builder.rs | 99 ++++++++++--------------- 1 file changed, 38 insertions(+), 61 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 0b44c120982d..f0dd4c9011b9 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -183,88 +183,65 @@ pub(crate) fn create_early_remote_transcripts( // Get all dealings that have not been used in a transcript already let all_dealings = utils::get_dkg_dealings(pool_reader, parent, true); - // Collect map of the dealings remote target_ids to the dkg_ids - let mut remote_contexts: BTreeMap> = BTreeMap::new(); - for (target_id, dkg_id) in - all_dealings - .iter() - .filter_map(|(dkg_id, _)| match dkg_id.target_subnet { - NiDkgTargetSubnet::Local => None, - NiDkgTargetSubnet::Remote(target_id) => Some((target_id, dkg_id)), - }) - { - let entry = remote_contexts.entry(target_id).or_default(); - entry.push(dkg_id.clone()); - } + // Collect map of remote target_ids to dkg_ids + let remote_contexts: BTreeMap> = all_dealings + .iter() + .filter_map(|(dkg_id, _)| match dkg_id.target_subnet { + NiDkgTargetSubnet::Local => None, + NiDkgTargetSubnet::Remote(target_id) => Some((target_id, dkg_id.clone())), + }) + .fold(BTreeMap::new(), |mut acc, (target_id, dkg_id)| { + acc.entry(target_id).or_default().push(dkg_id); + acc + }); + let state_ref = state.get_ref(); let mut selected_transcripts = vec![]; for (_, dkg_ids) in remote_contexts { // For each target_id, try to build the necessary (dkg_id, callback_id, transcript) triple - let mut transcripts = dkg_ids + let mut transcripts: Vec<_> = dkg_ids .iter() - // Lookup the config from the summary .filter_map(|dkg_id| { - last_dkg_summary - .configs - .get(dkg_id) - .map(|config| (dkg_id, config)) - }) - // Lookup the callback id - .filter_map(|(dkg_id, config)| { - get_callback_id_from_id(state.get_ref(), dkg_id) - .map(|callback_id| (dkg_id, callback_id, config)) - }) - // Generate the transcripts. We just skip errors, they will - // be handled in the summary block, if we fail to create an early transcript - .filter_map(|(dkg_id, callback_id, config)| { - match create_transcript(crypto, config, &all_dealings, &logger) { - Ok(transcript) => { - Some((dkg_id.clone(), callback_id, Ok::<_, String>(transcript))) - } - Err(_) => None, - } + // Lookup the config from the summary + let config = last_dkg_summary.configs.get(dkg_id)?; + // Lookup the callback id + let callback_id = get_callback_id_from_id(state_ref, dkg_id)?; + // Generate the transcript. We just skip errors, they will + // be handled in the summary block, if we fail to create an early transcript + let transcript = create_transcript(crypto, config, &all_dealings, &logger).ok()?; + + Some((dkg_id.clone(), callback_id, Ok::<_, String>(transcript))) }) - .collect::>(); + .collect(); - // For inital DKG transcripts, we need a pair of values while for VetKD we need a single config + // For initial DKG transcripts, we need a pair of values while for VetKD we need a single config // Here we do some matching, to check that we have the right number of configs - match transcripts.len() { + let is_valid = match transcripts.len() { 1 => { // For VetKD, we need to check that these have a HighThresholdForKey tag - let dkg_id = &transcripts[0].0; - match &dkg_id.dkg_tag { - NiDkgTag::HighThresholdForKey(_) => { - // This is a valid VetKD transcript, include it - } - _ => { - // Single transcript that is not VetKD, skip it - continue; - } - } + matches!(transcripts[0].0.dkg_tag, NiDkgTag::HighThresholdForKey(_)) } - // If we have two transcripts for the same ID, we check that it is - // one low and one high threshold transcript 2 => { - let tags = transcripts + // If we have two transcripts for the same ID, we check that it is + // one low and one high threshold transcript + let tags: BTreeSet<_> = transcripts .iter() .map(|(dkg_id, _, _)| dkg_id.dkg_tag.clone()) - .collect::>(); + .collect(); let expected_tags: BTreeSet<_> = [NiDkgTag::LowThreshold, NiDkgTag::HighThreshold] .iter() .cloned() .collect(); - - if tags != expected_tags { - continue; - } + tags == expected_tags } - // Other combinations are not supported - _ => continue, - } + _ => false, // Other combinations are not supported + }; - selected_transcripts.append(&mut transcripts); - if selected_transcripts.len() >= num_transcripts { - break; + if is_valid { + selected_transcripts.append(&mut transcripts); + if selected_transcripts.len() >= num_transcripts { + break; + } } } From ae94556e3e922a364c5fe89cb36fb7ebf2e76739 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 22 Jan 2026 15:53:05 +0000 Subject: [PATCH 41/98] update --- rs/consensus/dkg/src/lib.rs | 26 ++++------ rs/consensus/dkg/src/payload_builder.rs | 64 ++++++++++++------------- rs/consensus/dkg/src/utils.rs | 45 +++++++---------- rs/types/types/src/consensus/dkg.rs | 3 +- 4 files changed, 59 insertions(+), 79 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 20bba6ce4858..966051fa7aa0 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -1673,15 +1673,12 @@ mod tests { // Put a dealing in the pool and check that it gets included // Additionally check that there are no remote transcripts - dkg_pool - .write() - .unwrap() - .apply(vec![ChangeAction::AddToValidated(create_dealing( - 1, - remote_dkg_ids[0].clone(), - ))]); + let dealings = (0..3) + .map(|i| ChangeAction::AddToValidated(create_dealing(i, remote_dkg_ids[0].clone()))) + .collect::>(); + dkg_pool.write().unwrap().apply(dealings); pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 1); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 3); assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); // For the next round, we put nothing into the pool @@ -1693,15 +1690,12 @@ mod tests { // Now we put the other dealing into the pool // The payload builder will include the dealing - dkg_pool - .write() - .unwrap() - .apply(vec![ChangeAction::AddToValidated(create_dealing( - 1, - remote_dkg_ids[1].clone(), - ))]); + let dealings = (0..3) + .map(|i| ChangeAction::AddToValidated(create_dealing(i, remote_dkg_ids[1].clone()))) + .collect::>(); + dkg_pool.write().unwrap().apply(dealings); pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 1); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 3); assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); // Now sufficient dealings are in the pool, check that payload contains early remote transcripts diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index f0dd4c9011b9..d5639609c54b 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -19,7 +19,7 @@ use ic_types::{ batch::ValidationContext, consensus::{ Block, - dkg::{DealingMessages, DkgDataPayload, DkgPayload, DkgPayloadCreationError, DkgSummary}, + dkg::{DkgDataPayload, DkgPayload, DkgPayloadCreationError, DkgSummary}, get_faults_tolerated, }, crypto::threshold_sig::ni_dkg::{ @@ -109,7 +109,7 @@ fn create_data_payload( let dealers_from_chain = utils::get_dealers_from_chain(pool_reader, parent); // Filter from the validated pool all dealings whose dealer has no dealing on // the chain yet. - let new_validated_dealings: DealingMessages = dkg_pool + let new_validated_dealings = dkg_pool .read() .expect("Couldn't lock DKG pool for reading.") .get_validated() @@ -123,14 +123,6 @@ fn create_data_payload( .cloned() .collect(); - // If we have dealings in the payload, we will not try to make transcripts as well - if !new_validated_dealings.is_empty() { - return Ok(DkgDataPayload::new( - last_summary_block.height, - new_validated_dealings, - )); - } - let remote_dkg_transcripts = create_early_remote_transcripts( pool_reader, crypto, @@ -153,6 +145,7 @@ fn create_data_payload( // Try to include remote transcripts Ok(DkgDataPayload::new_with_remote_dkg_transcripts( last_summary_block.height, + new_validated_dealings, remote_dkg_transcripts, )) } @@ -183,34 +176,41 @@ pub(crate) fn create_early_remote_transcripts( // Get all dealings that have not been used in a transcript already let all_dealings = utils::get_dkg_dealings(pool_reader, parent, true); - // Collect map of remote target_ids to dkg_ids - let remote_contexts: BTreeMap> = all_dealings - .iter() - .filter_map(|(dkg_id, _)| match dkg_id.target_subnet { - NiDkgTargetSubnet::Local => None, - NiDkgTargetSubnet::Remote(target_id) => Some((target_id, dkg_id.clone())), - }) - .fold(BTreeMap::new(), |mut acc, (target_id, dkg_id)| { - acc.entry(target_id).or_default().push(dkg_id); - acc - }); + // Collect map of remote target_ids to dkg configs + let mut remote_contexts: BTreeMap> = + BTreeMap::new(); + for (dkg_id, config) in last_dkg_summary.configs.iter() { + if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { + remote_contexts + .entry(target_id) + .or_default() + .push((dkg_id, config)); + } + } let state_ref = state.get_ref(); let mut selected_transcripts = vec![]; - for (_, dkg_ids) in remote_contexts { - // For each target_id, try to build the necessary (dkg_id, callback_id, transcript) triple - let mut transcripts: Vec<_> = dkg_ids + for (_, configs) in remote_contexts { + // If any of the configs has less dealings than the threshold, we skip this target_id + if configs.iter().any(|(dkg_id, config)| { + let dealings_count = all_dealings + .get(dkg_id) + .map_or(0, |dealings| dealings.len()); + dealings_count < config.threshold().get().get() as usize + }) { + continue; + } + + // For each config, try to build the necessary (dkg_id, callback_id, transcript) triple + let mut transcripts: Vec<_> = configs .iter() - .filter_map(|dkg_id| { - // Lookup the config from the summary - let config = last_dkg_summary.configs.get(dkg_id)?; + .filter_map(|(dkg_id, config)| { // Lookup the callback id let callback_id = get_callback_id_from_id(state_ref, dkg_id)?; // Generate the transcript. We just skip errors, they will // be handled in the summary block, if we fail to create an early transcript let transcript = create_transcript(crypto, config, &all_dealings, &logger).ok()?; - - Some((dkg_id.clone(), callback_id, Ok::<_, String>(transcript))) + Some(((*dkg_id).clone(), callback_id, Ok::<_, String>(transcript))) }) .collect(); @@ -228,11 +228,7 @@ pub(crate) fn create_early_remote_transcripts( .iter() .map(|(dkg_id, _, _)| dkg_id.dkg_tag.clone()) .collect(); - let expected_tags: BTreeSet<_> = [NiDkgTag::LowThreshold, NiDkgTag::HighThreshold] - .iter() - .cloned() - .collect(); - tags == expected_tags + tags.contains(&NiDkgTag::LowThreshold) && tags.contains(&NiDkgTag::HighThreshold) } _ => false, // Other combinations are not supported }; diff --git a/rs/consensus/dkg/src/utils.rs b/rs/consensus/dkg/src/utils.rs index 5c70e620ef63..07e6461083b4 100644 --- a/rs/consensus/dkg/src/utils.rs +++ b/rs/consensus/dkg/src/utils.rs @@ -107,10 +107,8 @@ pub(super) fn get_dkg_dealings( exclude_used: bool, ) -> BTreeMap> { let mut dealings: BTreeMap> = BTreeMap::new(); - let mut used_dealings: BTreeSet = BTreeSet::new(); + let mut excluded: BTreeSet = BTreeSet::new(); - // Note that the chain iterator is guaranteed to iterate from - // newest to oldest blocks and that transcripts can not appear before their dealings. for block in pool_reader .chain_iterator(block.clone()) .take_while(|block| !block.payload.is_summary()) @@ -118,35 +116,26 @@ pub(super) fn get_dkg_dealings( let payload = &block.payload.as_ref().as_data().dkg; if exclude_used { - // Update used dealings - used_dealings.extend( - payload - .transcripts_for_remote_subnets - .iter() - .filter(|transcript| transcript.2.is_ok()) - .map(|transcript| transcript.0.clone()), - ); + for (dkg_id, _, _) in payload.transcripts_for_remote_subnets.iter() { + // Add the finished DKG to excluded list + excluded.insert(dkg_id.clone()); + // Remove already selected dealings + dealings.remove(dkg_id); + } } - // Find new dealings in this payload - for (signer, ni_dkg_id, dealing) in payload - .messages - .iter() - // Filter out if they are already used - .filter(|message| !used_dealings.contains(&message.content.dkg_id)) - .map(|message| { - ( - message.signature.signer, - message.content.dkg_id.clone(), - message.content.dealing.clone(), - ) - }) - { - let entry = dealings.entry(ni_dkg_id).or_default(); - let old_entry = entry.insert(signer, dealing); + for message in payload.messages.iter() { + if excluded.contains(&message.content.dkg_id) { + continue; + } + + let old_dealing = dealings + .entry(message.content.dkg_id.clone()) + .or_default() + .insert(message.signature.signer, message.content.dealing.clone()); assert!( - old_entry.is_none(), + old_dealing.is_none(), "Dealings from the same dealers discovered." ); } diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index 3dfefac70969..20b9321a2084 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -510,11 +510,12 @@ impl DkgDataPayload { /// Return an new DealingsPayload. pub fn new_with_remote_dkg_transcripts( start_height: Height, + messages: DealingMessages, remote_dkg_transcripts: Vec<(NiDkgId, CallbackId, Result)>, ) -> Self { Self { start_height, - messages: vec![], + messages, transcripts_for_remote_subnets: remote_dkg_transcripts, } } From e96cc22c38a8288ee1ce2acf0fd97ab08781e7cf Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 23 Jan 2026 12:19:43 +0000 Subject: [PATCH 42/98] progress --- rs/consensus/dkg/src/lib.rs | 35 ++++------------------- rs/consensus/dkg/src/payload_builder.rs | 37 +++++++++++++++++++++--- rs/consensus/dkg/src/test_utils.rs | 38 +++++++++++++++++++++++-- rs/consensus/dkg/src/utils.rs | 10 +++++-- 4 files changed, 82 insertions(+), 38 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 966051fa7aa0..357d3fc07242 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -396,6 +396,7 @@ mod tests { complement_state_manager_with_remote_dkg_requests, complement_state_manager_with_reshare_chain_key_request, complement_state_manager_with_setup_initial_dkg_request, create_dealing, + extract_dkg_configs_from_highest_block, extract_remote_dkg_ids_from_highest_block, }; use core::panic; use ic_artifact_pool::dkg_pool::DkgPoolImpl; @@ -1642,34 +1643,10 @@ mod tests { // Verify that the next summary block contains the configs and no transcripts. // This also extracts the DKG ids pool.advance_round_normal_operation_n(dkg_interval_length + 1); - let block: Block = pool - .validated() - .block_proposal() - .get_highest() - .unwrap() - .content - .into_inner(); - let remote_dkg_ids = if block.payload.as_ref().is_summary() { - let dkg_summary = &block.payload.as_ref().as_summary().dkg; - assert!(dkg_summary.transcripts_for_remote_subnets.is_empty()); - assert_eq!(dkg_summary.configs.len(), 4); - - let remote_dkg_ids = dkg_summary - .configs - .iter() - .filter(|(id, _)| id.target_subnet == NiDkgTargetSubnet::Remote(target_id)) - .map(|(id, _)| id) - .cloned() - .collect::>(); - assert_eq!(remote_dkg_ids.len(), 2); - - remote_dkg_ids - } else { - panic!( - "block at height {} is not a summary block", - block.height.get() - ); - }; + let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&pool, target_id); + assert_eq!(remote_dkg_ids.len(), 2); + assert_eq!(extract_dkg_configs_from_highest_block(&pool).len(), 4); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); // Put a dealing in the pool and check that it gets included // Additionally check that there are no remote transcripts @@ -1744,7 +1721,7 @@ mod tests { ); // Simulate the delivery of the block, which removes to context from the state - target_id_mutex.lock().unwrap().take(); + // target_id_mutex.lock().unwrap().take(); // Advance the pool a until the next DKG, check that the early remote transcripts are not generated multiple times // including that they are not included in the summary diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index d5639609c54b..dbca96dbaeaf 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -174,12 +174,15 @@ pub(crate) fn create_early_remote_transcripts( } // Get all dealings that have not been used in a transcript already - let all_dealings = utils::get_dkg_dealings(pool_reader, parent, true); + let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent, true); // Collect map of remote target_ids to dkg configs let mut remote_contexts: BTreeMap> = BTreeMap::new(); for (dkg_id, config) in last_dkg_summary.configs.iter() { + if completed.contains(dkg_id) { + continue; + } if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { remote_contexts .entry(target_id) @@ -282,13 +285,14 @@ pub(super) fn create_summary_payload( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { - let all_dealings = utils::get_dkg_dealings(pool_reader, parent, true); + let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent, true); let mut transcripts_for_remote_subnets = BTreeMap::new(); let mut next_transcripts = BTreeMap::new(); // Try to create transcripts from the last round. for (dkg_id, config) in last_summary.configs.iter() { - // TODO: Filter out configs that no longer have a matching context - + if completed.contains(dkg_id) { + continue; + } match create_transcript(crypto, config, &all_dealings, &logger) { Ok(transcript) => { let previous_value_found = if dkg_id.target_subnet == NiDkgTargetSubnet::Local { @@ -370,6 +374,7 @@ pub(super) fn create_summary_payload( transcripts_for_remote_subnets, &previous_transcripts, &reshared_transcripts, + &completed, &last_summary.initial_dkg_attempts, &logger, )?; @@ -452,6 +457,7 @@ fn compute_remote_dkg_data( mut new_transcripts: BTreeMap>, previous_transcripts: &BTreeMap>, reshared_transcripts: &BTreeMap, + completed: &BTreeSet, previous_attempts: &BTreeMap, logger: &ReplicaLogger, ) -> Result< @@ -472,6 +478,7 @@ fn compute_remote_dkg_data( state.get_ref(), validation_context, reshared_transcripts, + completed, logger, )?; @@ -768,6 +775,7 @@ fn process_subnet_call_context( state: &ReplicatedState, validation_context: &ValidationContext, reshared_transcripts: &BTreeMap, + completed: &BTreeSet, logger: &ReplicaLogger, ) -> Result< ( @@ -784,6 +792,7 @@ fn process_subnet_call_context( registry_client, state, validation_context, + completed, logger, )?; @@ -795,6 +804,7 @@ fn process_subnet_call_context( state, validation_context, reshared_transcripts, + completed, )?; let dkg_configs = init_dkg_configs @@ -821,6 +831,7 @@ fn process_reshare_chain_key_contexts( state: &ReplicatedState, validation_context: &ValidationContext, reshared_transcripts: &BTreeMap, + completed: &BTreeSet, ) -> Result< ( Vec>, @@ -843,6 +854,13 @@ fn process_reshare_chain_key_contexts( continue; } + // If the DKG has already been completed, skip this context + if completed.iter().any(|completed_dkg_id| { + completed_dkg_id.target_subnet == NiDkgTargetSubnet::Remote(context.target_id) + }) { + continue; + } + // Only process NiDkgMasterPublicKeyId let Ok(key_id) = NiDkgMasterPublicKeyId::try_from(context.key_id.clone()) else { continue; @@ -878,6 +896,7 @@ fn process_setup_initial_dkg_contexts( registry_client: &dyn RegistryClient, state: &ReplicatedState, validation_context: &ValidationContext, + completed: &BTreeSet, logger: &ReplicaLogger, ) -> Result< ( @@ -900,6 +919,13 @@ fn process_setup_initial_dkg_contexts( continue; } + // If the DKG has already been completed, skip this context + if completed.iter().any(|completed_dkg_id| { + completed_dkg_id.target_subnet == NiDkgTargetSubnet::Remote(context.target_id) + }) { + continue; + } + // Dealers must be in the same registry_version. let dealers = get_node_list(this_subnet_id, registry_client, context.registry_version)?; @@ -1368,6 +1394,7 @@ mod tests { BTreeMap::new(), &BTreeMap::new(), &BTreeMap::new(), + &BTreeSet::new(), &BTreeMap::new(), &logger, ) @@ -1402,6 +1429,7 @@ mod tests { BTreeMap::new(), &BTreeMap::new(), &BTreeMap::new(), + &BTreeSet::new(), &initial_dkg_attempts, &logger, ) @@ -1446,6 +1474,7 @@ mod tests { BTreeMap::new(), &BTreeMap::new(), &BTreeMap::new(), + &BTreeSet::new(), &initial_dkg_attempts, &logger, ) diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index cef79d29ebb6..e52a57640da5 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -12,13 +12,16 @@ use ic_types::{ consensus::dkg::{DealingContent, DealingMessages}, crypto::{ BasicSig, - threshold_sig::ni_dkg::{NiDkgDealing, NiDkgId, NiDkgTargetId, NiDkgTranscript}, + threshold_sig::ni_dkg::{ + NiDkgDealing, NiDkgId, NiDkgTargetId, NiDkgTargetSubnet, NiDkgTranscript, + config::NiDkgConfig, + }, }, messages::CallbackId, signature::{BasicSignature, BasicSigned}, }; use std::{ - collections::BTreeSet, + collections::{BTreeMap, BTreeSet}, sync::{Arc, Mutex}, }; @@ -189,3 +192,34 @@ pub(super) fn extract_dealings_from_highest_block(pool: &TestConsensusPool) -> D block.payload.as_ref().as_data().dkg.messages.clone() } } + +/// Extract the remote dkg transcripts from the current highest validated block +pub(super) fn extract_remote_dkg_ids_from_highest_block( + pool: &TestConsensusPool, + target_id: NiDkgTargetId, +) -> Vec { + extract_dkg_configs_from_highest_block(pool) + .iter() + .filter(|(id, _)| id.target_subnet == NiDkgTargetSubnet::Remote(target_id)) + .map(|(id, _)| id) + .cloned() + .collect() +} + +pub(super) fn extract_dkg_configs_from_highest_block( + pool: &TestConsensusPool, +) -> BTreeMap { + let block: ic_types::consensus::Block = pool + .validated() + .block_proposal() + .get_highest() + .unwrap() + .content + .into_inner(); + + if block.payload.as_ref().is_summary() { + block.payload.as_ref().as_summary().dkg.configs.clone() + } else { + BTreeMap::new() + } +} diff --git a/rs/consensus/dkg/src/utils.rs b/rs/consensus/dkg/src/utils.rs index 07e6461083b4..08ad5989d5cc 100644 --- a/rs/consensus/dkg/src/utils.rs +++ b/rs/consensus/dkg/src/utils.rs @@ -86,7 +86,8 @@ pub(super) fn get_dealers_from_chain( pool_reader: &PoolReader<'_>, block: &Block, ) -> HashSet<(NiDkgId, NodeId)> { - get_dkg_dealings(pool_reader, block, false) + let (dkg_dealings, _) = get_dkg_dealings(pool_reader, block, false); + dkg_dealings .into_iter() .flat_map(|(dkg_id, dealings)| { dealings @@ -105,7 +106,10 @@ pub(super) fn get_dkg_dealings( pool_reader: &PoolReader<'_>, block: &Block, exclude_used: bool, -) -> BTreeMap> { +) -> ( + BTreeMap>, + BTreeSet, +) { let mut dealings: BTreeMap> = BTreeMap::new(); let mut excluded: BTreeSet = BTreeSet::new(); @@ -141,7 +145,7 @@ pub(super) fn get_dkg_dealings( } } - dealings + (dealings, excluded) } /// Fetch all key ids for which the subnet should hold a key From a461ab6a1ccc34407679e91a0cc2bf43bcdf3e1a Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 23 Jan 2026 14:25:25 +0000 Subject: [PATCH 43/98] errors --- rs/consensus/dkg/src/payload_builder.rs | 48 ++++++++++++++--------- rs/consensus/dkg/src/payload_validator.rs | 2 +- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index dbca96dbaeaf..f12265cc1775 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -132,7 +132,7 @@ fn create_data_payload( state_manager, validation_context, logger.clone(), - ); + )?; if !remote_dkg_transcripts.is_empty() { info!( @@ -159,18 +159,18 @@ pub(crate) fn create_early_remote_transcripts( state_manager: &dyn StateManager, validation_context: &ValidationContext, logger: ReplicaLogger, -) -> Vec<(NiDkgId, CallbackId, Result)> { +) -> Result)>, DkgPayloadCreationError> { // If we cannot access the state manager, we don't return an error. // This is because the early remote transcripts are an optimization. // If we don't return any transcripts here, the protocol will continue anyway // and return the transcripts (or their errors) in the summary block - let Ok(state) = state_manager.get_state_at(validation_context.certified_height) else { - return vec![]; - }; + let state = state_manager + .get_state_at(validation_context.certified_height) + .map_err(DkgPayloadCreationError::StateManagerError)?; // Since this function is relatively expensive, we simply return if there are no outstanding DKG contexts if number_of_contexts(state.get_ref()) == 0 { - return vec![]; + return Ok(vec![]); } // Get all dealings that have not been used in a transcript already @@ -205,17 +205,29 @@ pub(crate) fn create_early_remote_transcripts( } // For each config, try to build the necessary (dkg_id, callback_id, transcript) triple - let mut transcripts: Vec<_> = configs - .iter() - .filter_map(|(dkg_id, config)| { - // Lookup the callback id - let callback_id = get_callback_id_from_id(state_ref, dkg_id)?; - // Generate the transcript. We just skip errors, they will - // be handled in the summary block, if we fail to create an early transcript - let transcript = create_transcript(crypto, config, &all_dealings, &logger).ok()?; - Some(((*dkg_id).clone(), callback_id, Ok::<_, String>(transcript))) - }) - .collect(); + let mut transcripts = vec![]; + for (dkg_id, config) in configs.iter() { + // Lookup the callback id + let Some(callback_id) = get_callback_id_from_id(state_ref, dkg_id) else { + continue; + }; + // Generate the transcript. We just skip reproducible errors, they will + // be handled in the summary block, if we fail to create an early transcript + let transcript = match create_transcript(crypto, config, &all_dealings, &logger) { + Ok(transcript) => transcript, + Err(err) if err.is_reproducible() => { + warn!( + logger, + "Failed to create transcript for dkg id {:?}: {:?}", dkg_id, err + ); + continue; + } + Err(err) => { + return Err(DkgPayloadCreationError::DkgCreateTranscriptError(err)); + } + }; + transcripts.push(((*dkg_id).clone(), callback_id, Ok::<_, String>(transcript))); + } // For initial DKG transcripts, we need a pair of values while for VetKD we need a single config // Here we do some matching, to check that we have the right number of configs @@ -244,7 +256,7 @@ pub(crate) fn create_early_remote_transcripts( } } - selected_transcripts + Ok(selected_transcripts) } /// Creates a summary payload for the given parent and registry_version. diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index cacefb6915ea..2face4dcda60 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -211,7 +211,7 @@ fn validate_dealings_payload( state_manager, validation_context, log.clone(), - ); + )?; if dealings.transcripts_for_remote_subnets != expected_transcripts { warn!( From b5d2d101bc8d5d99ffc719a2aa8fb0a22aa62743 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 26 Jan 2026 12:23:41 +0000 Subject: [PATCH 44/98] clean --- rs/consensus/dkg/src/lib.rs | 29 +++++------ rs/consensus/dkg/src/payload_builder.rs | 63 ++++++++++++----------- rs/consensus/dkg/src/payload_validator.rs | 5 +- rs/consensus/dkg/src/test_utils.rs | 44 ++++++++-------- rs/consensus/dkg/src/utils.rs | 21 ++++++-- rs/types/types/src/consensus/dkg.rs | 2 +- 6 files changed, 87 insertions(+), 77 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 357d3fc07242..d9924ea57ae4 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -42,14 +42,18 @@ mod utils; pub use dkg_key_manager::DkgKeyManager; pub use payload_builder::{create_payload, get_dkg_summary_from_cup_contents}; -// The maximal number of DKGs for other subnets we want to run in one interval. +/// The maximal number of DKGs for other subnets we want to run in one interval. const MAX_REMOTE_DKGS_PER_INTERVAL: usize = 1; -// The maximum number of intervals during which an initial DKG request is -// attempted. +/// The maximum number of early remote transcript responses we want to include in a data payload. +/// Note that responses for `SetupInitialDKG` requests contain two transcripts. +const MAX_EARLY_REMOTE_TRANSCRIPT_RESPONSES: usize = 1; + +/// The maximum number of intervals during which an initial DKG request is +/// attempted. const MAX_REMOTE_DKG_ATTEMPTS: u32 = 5; -// Generic error string for failed remote DKG requests. +/// Generic error string for failed remote DKG requests. const REMOTE_DKG_REPEATED_FAILURE_ERROR: &str = "Attempts to run this DKG repeatedly failed"; struct Metrics { @@ -1630,14 +1634,12 @@ mod tests { ); let target_id = NiDkgTargetId::new([0u8; 32]); - let target_id_mutex = Arc::new(Mutex::new(Some(target_id))); - complement_state_manager_with_remote_dkg_requests( state_manager.clone(), registry.get_latest_version(), vec![10, 11, 12, 13], None, - target_id_mutex.clone(), + target_id, ); // Verify that the next summary block contains the configs and no transcripts. @@ -1648,7 +1650,7 @@ mod tests { assert_eq!(extract_dkg_configs_from_highest_block(&pool).len(), 4); assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); - // Put a dealing in the pool and check that it gets included + // Put three dealing in the pool and check that they get included // Additionally check that there are no remote transcripts let dealings = (0..3) .map(|i| ChangeAction::AddToValidated(create_dealing(i, remote_dkg_ids[0].clone()))) @@ -1659,13 +1661,13 @@ mod tests { assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); // For the next round, we put nothing into the pool - // Since there are no dealings, we will try to build a remote transcript - // This will fail, however, since we need two dealings (one high one low) + // We will try to build a remote transcript, his will fail, however, + // since we don't have enought dealings to build both transcripts (one high one low) pool.advance_round_normal_operation(); assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); - // Now we put the other dealing into the pool + // Now we put the other dealings into the pool // The payload builder will include the dealing let dealings = (0..3) .map(|i| ChangeAction::AddToValidated(create_dealing(i, remote_dkg_ids[1].clone()))) @@ -1676,8 +1678,6 @@ mod tests { assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); // Now sufficient dealings are in the pool, check that payload contains early remote transcripts - // NOTE: We only need one dealing each, since we are using `CryptoReturningOk`. We should consider - // using real crypto for these tests, to make them more realistic pool.advance_round_normal_operation(); assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 2); @@ -1720,9 +1720,6 @@ mod tests { .is_ok() ); - // Simulate the delivery of the block, which removes to context from the state - // target_id_mutex.lock().unwrap().take(); - // Advance the pool a until the next DKG, check that the early remote transcripts are not generated multiple times // including that they are not included in the summary for _ in 0..100 { diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index f12265cc1775..987b72dfc8e9 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1,5 +1,6 @@ use crate::{ - MAX_REMOTE_DKG_ATTEMPTS, MAX_REMOTE_DKGS_PER_INTERVAL, REMOTE_DKG_REPEATED_FAILURE_ERROR, + MAX_EARLY_REMOTE_TRANSCRIPT_RESPONSES, MAX_REMOTE_DKG_ATTEMPTS, MAX_REMOTE_DKGS_PER_INTERVAL, + REMOTE_DKG_REPEATED_FAILURE_ERROR, utils::{self, tags_iter, vetkd_key_ids_for_subnet}, }; use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; @@ -127,7 +128,6 @@ fn create_data_payload( pool_reader, crypto, parent, - 1, last_dkg_summary, state_manager, validation_context, @@ -150,20 +150,17 @@ fn create_data_payload( )) } +#[allow(clippy::type_complexity)] pub(crate) fn create_early_remote_transcripts( pool_reader: &PoolReader<'_>, crypto: &dyn ConsensusCrypto, parent: &Block, - num_transcripts: usize, last_dkg_summary: &DkgSummary, state_manager: &dyn StateManager, validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result)>, DkgPayloadCreationError> { - // If we cannot access the state manager, we don't return an error. - // This is because the early remote transcripts are an optimization. - // If we don't return any transcripts here, the protocol will continue anyway - // and return the transcripts (or their errors) in the summary block + // Return an error on transient state manager errors let state = state_manager .get_state_at(validation_context.certified_height) .map_err(DkgPayloadCreationError::StateManagerError)?; @@ -177,27 +174,24 @@ pub(crate) fn create_early_remote_transcripts( let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent, true); // Collect map of remote target_ids to dkg configs - let mut remote_contexts: BTreeMap> = - BTreeMap::new(); - for (dkg_id, config) in last_dkg_summary.configs.iter() { + let mut remote_contexts: BTreeMap> = BTreeMap::new(); + for config in last_dkg_summary.configs.values() { + let dkg_id = config.dkg_id(); if completed.contains(dkg_id) { continue; } if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { - remote_contexts - .entry(target_id) - .or_default() - .push((dkg_id, config)); + remote_contexts.entry(target_id).or_default().push(config); } } let state_ref = state.get_ref(); let mut selected_transcripts = vec![]; - for (_, configs) in remote_contexts { + for (_target_id, configs) in remote_contexts { // If any of the configs has less dealings than the threshold, we skip this target_id - if configs.iter().any(|(dkg_id, config)| { + if configs.iter().any(|config| { let dealings_count = all_dealings - .get(dkg_id) + .get(config.dkg_id()) .map_or(0, |dealings| dealings.len()); dealings_count < config.threshold().get().get() as usize }) { @@ -206,9 +200,9 @@ pub(crate) fn create_early_remote_transcripts( // For each config, try to build the necessary (dkg_id, callback_id, transcript) triple let mut transcripts = vec![]; - for (dkg_id, config) in configs.iter() { + for config in configs.iter() { // Lookup the callback id - let Some(callback_id) = get_callback_id_from_id(state_ref, dkg_id) else { + let Some(callback_id) = get_callback_id_from_id(state_ref, config.dkg_id()) else { continue; }; // Generate the transcript. We just skip reproducible errors, they will @@ -218,39 +212,52 @@ pub(crate) fn create_early_remote_transcripts( Err(err) if err.is_reproducible() => { warn!( logger, - "Failed to create transcript for dkg id {:?}: {:?}", dkg_id, err + "Failed to create early remote transcript for dkg id {:?}: {:?}", + config.dkg_id(), + err ); continue; } Err(err) => { + // Return on transient crypto errors return Err(DkgPayloadCreationError::DkgCreateTranscriptError(err)); } }; - transcripts.push(((*dkg_id).clone(), callback_id, Ok::<_, String>(transcript))); + transcripts.push(( + config.dkg_id().clone(), + callback_id, + Ok::<_, String>(transcript), + )); } - // For initial DKG transcripts, we need a pair of values while for VetKD we need a single config - // Here we do some matching, to check that we have the right number of configs + // For initial DKG transcripts, we need a pair of values while for VetKD we need a single transcript. + // Here we do some matching, to check that we have the right number of transcripts. let is_valid = match transcripts.len() { 1 => { - // For VetKD, we need to check that these have a HighThresholdForKey tag + // For VetKD, we need to check that it has a HighThresholdForKey tag matches!(transcripts[0].0.dkg_tag, NiDkgTag::HighThresholdForKey(_)) } 2 => { // If we have two transcripts for the same ID, we check that it is - // one low and one high threshold transcript + // one low and one high threshold transcript. let tags: BTreeSet<_> = transcripts .iter() .map(|(dkg_id, _, _)| dkg_id.dkg_tag.clone()) .collect(); tags.contains(&NiDkgTag::LowThreshold) && tags.contains(&NiDkgTag::HighThreshold) } - _ => false, // Other combinations are not supported + other => { + warn!( + logger, + "Produced unexpected number of early remote NiDKG transcripts: {other}" + ); + false + } }; if is_valid { selected_transcripts.append(&mut transcripts); - if selected_transcripts.len() >= num_transcripts { + if selected_transcripts.len() >= MAX_EARLY_REMOTE_TRANSCRIPT_RESPONSES { break; } } @@ -435,8 +442,6 @@ fn create_transcript( let no_dealings = BTreeMap::new(); let dealings = all_dealings.get(config.dkg_id()).unwrap_or(&no_dealings); - // TODO: We need to check that we are not erroring on insufficient dealings here - ic_interfaces::crypto::NiDkgAlgorithm::create_transcript(crypto, config, dealings) } diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index 2face4dcda60..4195b893f260 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -206,7 +206,6 @@ fn validate_dealings_payload( pool_reader, crypto, parent, - 1, last_summary, state_manager, validation_context, @@ -216,7 +215,7 @@ fn validate_dealings_payload( if dealings.transcripts_for_remote_subnets != expected_transcripts { warn!( log, - "Failed to validate {} early remote DKG transcripts in regular block payload", + "Failed to validate {} early remote DKG transcripts in data block payload", dealings.transcripts_for_remote_subnets.len() ); return Err(InvalidDkgPayloadReason::InvalidTranscripts.into()); @@ -224,7 +223,7 @@ fn validate_dealings_payload( info!( log, - "Validated {} early remote DKG transcripts in regular block payload", + "Validated {} early remote DKG transcripts in data block payload", dealings.transcripts_for_remote_subnets.len() ); } diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index e52a57640da5..6dd9e1807875 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -22,7 +22,7 @@ use ic_types::{ }; use std::{ collections::{BTreeMap, BTreeSet}, - sync::{Arc, Mutex}, + sync::Arc, }; pub(super) fn complement_state_manager_with_setup_initial_dkg_request( @@ -98,30 +98,29 @@ pub(super) fn complement_state_manager_with_remote_dkg_requests( registry_version: RegistryVersion, node_ids: Vec, times: Option, - target: Arc>>, + target_id: NiDkgTargetId, ) { // Add the context into state_manager. let nodes_in_target_subnet: BTreeSet<_> = node_ids.into_iter().map(node_test_id).collect(); + let mut state = ic_test_utilities_state::get_initial_state(0, 0); + state + .metadata + .subnet_call_context_manager + .push_context(SubnetCallContext::SetupInitialDKG(SetupInitialDkgContext { + request: RequestBuilder::new().build(), + nodes_in_target_subnet: nodes_in_target_subnet.clone(), + target_id, + registry_version, + time: state.time(), + })); let mut mock = state_manager.get_mut(); - let expectation = mock.expect_get_state_at().returning(move |_| { - let mut state = ic_test_utilities_state::get_initial_state(0, 0); - if let Some(target_id) = target.lock().unwrap().as_ref() { - state.metadata.subnet_call_context_manager.push_context( - SubnetCallContext::SetupInitialDKG(SetupInitialDkgContext { - request: RequestBuilder::new().build(), - nodes_in_target_subnet: nodes_in_target_subnet.clone(), - target_id: *target_id, - registry_version, - time: state.time(), - }), - ); - } - Ok(ic_interfaces_state_manager::Labeled::new( - Height::new(0), - Arc::new(state), - )) - }); + let expectation = + mock.expect_get_state_at() + .return_const(Ok(ic_interfaces_state_manager::Labeled::new( + Height::new(0), + Arc::new(state), + ))); if let Some(times) = times { expectation.times(times); } @@ -129,8 +128,6 @@ pub(super) fn complement_state_manager_with_remote_dkg_requests( /// Create a dealing from the node `node_idx` pub(super) fn create_dealing(node_idx: u64, dkg_id: NiDkgId) -> BasicSigned { - let node_id = node_test_id(node_idx); - BasicSigned { content: DealingContent { version: ReplicaVersion::default(), @@ -141,7 +138,7 @@ pub(super) fn create_dealing(node_idx: u64, dkg_id: NiDkgId) -> BasicSigned BTreeMap { diff --git a/rs/consensus/dkg/src/utils.rs b/rs/consensus/dkg/src/utils.rs index 08ad5989d5cc..1cfd05a9485b 100644 --- a/rs/consensus/dkg/src/utils.rs +++ b/rs/consensus/dkg/src/utils.rs @@ -7,7 +7,7 @@ use ic_types::{ NodeId, RegistryVersion, SubnetId, consensus::{ Block, - dkg::{DkgPayloadCreationError, DkgSummary}, + dkg::{DkgPayloadCreationError, DkgSummary, Message}, }, crypto::{ AlgorithmId, @@ -86,7 +86,7 @@ pub(super) fn get_dealers_from_chain( pool_reader: &PoolReader<'_>, block: &Block, ) -> HashSet<(NiDkgId, NodeId)> { - let (dkg_dealings, _) = get_dkg_dealings(pool_reader, block, false); + let (dkg_dealings, _) = extract_from_dkg_dealing_messages(pool_reader, block, false, |_| ()); dkg_dealings .into_iter() .flat_map(|(dkg_id, dealings)| { @@ -101,7 +101,7 @@ pub(super) fn get_dealers_from_chain( /// the node Id to the dealing. This function panics if multiple dealings /// from one dealer are discovered, hence, we assume a valid block chain. /// It also excludes dealings for ni_dkg ids, which already have a transcript in the -/// blockchain. +/// blockchain, and returns these exlcuded ni_dkg ids. pub(super) fn get_dkg_dealings( pool_reader: &PoolReader<'_>, block: &Block, @@ -110,7 +110,18 @@ pub(super) fn get_dkg_dealings( BTreeMap>, BTreeSet, ) { - let mut dealings: BTreeMap> = BTreeMap::new(); + extract_from_dkg_dealing_messages(pool_reader, block, exclude_used, |message| { + message.content.dealing.clone() + }) +} + +fn extract_from_dkg_dealing_messages( + pool_reader: &PoolReader<'_>, + block: &Block, + exclude_used: bool, + extractor: impl Fn(&Message) -> T, +) -> (BTreeMap>, BTreeSet) { + let mut dealings: BTreeMap> = BTreeMap::new(); let mut excluded: BTreeSet = BTreeSet::new(); for block in pool_reader @@ -136,7 +147,7 @@ pub(super) fn get_dkg_dealings( let old_dealing = dealings .entry(message.content.dkg_id.clone()) .or_default() - .insert(message.signature.signer, message.content.dealing.clone()); + .insert(message.signature.signer, extractor(message)); assert!( old_dealing.is_none(), diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index 20b9321a2084..de58b5123c59 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -621,7 +621,7 @@ pub enum InvalidDkgPayloadReason { limit: usize, actual: usize, }, - /// The early transcripts that were included with this payload where invalid + /// The early transcripts that were included with this payload were invalid InvalidTranscripts, } From 325b6b8455ce8e8ba4361f37bea95596aeaac31c Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 26 Jan 2026 15:16:13 +0000 Subject: [PATCH 45/98] fix --- rs/artifact_pool/BUILD.bazel | 2 ++ rs/artifact_pool/Cargo.toml | 2 ++ rs/artifact_pool/src/consensus_pool_cache.rs | 13 +++++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/rs/artifact_pool/BUILD.bazel b/rs/artifact_pool/BUILD.bazel index 0aa807c72712..853433b6f1d0 100644 --- a/rs/artifact_pool/BUILD.bazel +++ b/rs/artifact_pool/BUILD.bazel @@ -8,6 +8,7 @@ DEV_DEPENDENCIES = [ "//rs/crypto/test_utils/canister_threshold_sigs", "//rs/crypto/test_utils/crypto_returning_ok", "//rs/crypto/test_utils/ni-dkg", + "//rs/interfaces/state_manager", "//rs/limits", "//rs/test_utilities", "//rs/test_utilities/artifact_pool", @@ -16,6 +17,7 @@ DEV_DEPENDENCIES = [ "//rs/test_utilities/registry", "//rs/test_utilities/time", "//rs/test_utilities/types", + "//rs/test_utilities/state", "@crate_index//:rand", "@crate_index//:slog-async", "@crate_index//:slog-term", diff --git a/rs/artifact_pool/Cargo.toml b/rs/artifact_pool/Cargo.toml index d3b839d823ec..f0c90ee72323 100644 --- a/rs/artifact_pool/Cargo.toml +++ b/rs/artifact_pool/Cargo.toml @@ -39,11 +39,13 @@ ic-limits = { path = "../limits" } ic-crypto-test-utils-canister-threshold-sigs = { path = "../crypto/test_utils/canister_threshold_sigs" } ic-crypto-test-utils-crypto-returning-ok = { path = "../crypto/test_utils/crypto_returning_ok" } ic-crypto-test-utils-ni-dkg = { path = "../crypto/test_utils/ni-dkg" } +ic-interfaces-state-manager = { path = "../interfaces/state_manager" } ic-test-artifact-pool = { path = "../test_utilities/artifact_pool" } ic-test-utilities = { path = "../test_utilities" } ic-test-utilities-consensus = { path = "../test_utilities/consensus" } ic-test-utilities-logger = { path = "../test_utilities/logger" } ic-test-utilities-registry = { path = "../test_utilities/registry" } +ic-test-utilities-state = { path = "../test_utilities/state" } ic-test-utilities-time = { path = "../test_utilities/time" } ic-test-utilities-types = { path = "../test_utilities/types" } rand = { workspace = true } diff --git a/rs/artifact_pool/src/consensus_pool_cache.rs b/rs/artifact_pool/src/consensus_pool_cache.rs index 8da4b9750f94..fbcc91782af6 100644 --- a/rs/artifact_pool/src/consensus_pool_cache.rs +++ b/rs/artifact_pool/src/consensus_pool_cache.rs @@ -508,7 +508,7 @@ mod test { use ic_crypto_test_utils_crypto_returning_ok::CryptoReturningOk; use ic_interfaces::consensus_pool::{HEIGHT_CONSIDERED_BEHIND, ValidatedConsensusArtifact}; use ic_test_artifact_pool::consensus_pool::{Round, TestConsensusPool}; - use ic_test_utilities::state_manager::FakeStateManager; + use ic_test_utilities::state_manager::{FakeStateManager, RefMockStateManager}; use ic_test_utilities_consensus::fake::*; use ic_test_utilities_registry::{SubnetRecordBuilder, setup_registry}; use ic_test_utilities_time::FastForwardTimeSource; @@ -637,6 +637,15 @@ mod test { .build(), )]; + let state_manager = RefMockStateManager::default(); + state_manager + .get_mut() + .expect_get_state_at() + .return_const(Ok(ic_interfaces_state_manager::Labeled::new( + Height::new(0), + Arc::new(ic_test_utilities_state::get_initial_state(0, 0)), + ))); + let mut pool = TestConsensusPool::new( node_test_id(0), subnet_test_id(1), @@ -644,7 +653,7 @@ mod test { FastForwardTimeSource::new(), setup_registry(subnet_test_id(1), subnet_records), Arc::new(CryptoReturningOk::default()), - Arc::new(FakeStateManager::new()), + Arc::new(state_manager), None, ); From e2a4aabcf19d11bbcffd57e4cb18b8784f3aff04 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Mon, 26 Jan 2026 15:17:18 +0000 Subject: [PATCH 46/98] Automatically updated Cargo*.lock --- Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3da08319bfd1..424d5dde3b9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6479,6 +6479,7 @@ dependencies = [ "ic-crypto-test-utils-crypto-returning-ok", "ic-crypto-test-utils-ni-dkg", "ic-interfaces", + "ic-interfaces-state-manager", "ic-limits", "ic-logger", "ic-metrics", @@ -6489,6 +6490,7 @@ dependencies = [ "ic-test-utilities-consensus", "ic-test-utilities-logger", "ic-test-utilities-registry", + "ic-test-utilities-state", "ic-test-utilities-time", "ic-test-utilities-types", "ic-types", From d250dbe1445ed3737478df053666166302b34673 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Mon, 26 Jan 2026 15:22:20 +0000 Subject: [PATCH 47/98] Automatically fixing code for linting and formatting issues --- rs/artifact_pool/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/artifact_pool/BUILD.bazel b/rs/artifact_pool/BUILD.bazel index 853433b6f1d0..b38bc4a57e96 100644 --- a/rs/artifact_pool/BUILD.bazel +++ b/rs/artifact_pool/BUILD.bazel @@ -15,9 +15,9 @@ DEV_DEPENDENCIES = [ "//rs/test_utilities/consensus", "//rs/test_utilities/logger", "//rs/test_utilities/registry", + "//rs/test_utilities/state", "//rs/test_utilities/time", "//rs/test_utilities/types", - "//rs/test_utilities/state", "@crate_index//:rand", "@crate_index//:slog-async", "@crate_index//:slog-term", From 7e7dce48db75dfd82843adbe17ed5aee53eba482 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 27 Jan 2026 13:09:14 +0000 Subject: [PATCH 48/98] use correct threshold --- rs/consensus/dkg/src/payload_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 987b72dfc8e9..d0092e992297 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -193,7 +193,7 @@ pub(crate) fn create_early_remote_transcripts( let dealings_count = all_dealings .get(config.dkg_id()) .map_or(0, |dealings| dealings.len()); - dealings_count < config.threshold().get().get() as usize + dealings_count < config.collection_threshold().get() as usize }) { continue; } From 35e6b0060d2aaa992d60e7c4c03a0a4e431ced68 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 27 Jan 2026 14:12:15 +0000 Subject: [PATCH 49/98] include height --- rs/consensus/dkg/src/payload_builder.rs | 12 ++++++++---- rs/consensus/dkg/src/payload_validator.rs | 10 ++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index d0092e992297..20eb80df7d4a 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -137,8 +137,9 @@ fn create_data_payload( if !remote_dkg_transcripts.is_empty() { info!( logger, - "Including {} early remote DKG transcripts in regular block payload", - remote_dkg_transcripts.len() + "Including {} early remote DKG transcripts in data block payload at height {}", + remote_dkg_transcripts.len(), + parent.height.increment(), ); } @@ -212,8 +213,9 @@ pub(crate) fn create_early_remote_transcripts( Err(err) if err.is_reproducible() => { warn!( logger, - "Failed to create early remote transcript for dkg id {:?}: {:?}", + "Failed to create early remote transcript for dkg id {:?} at height {}: {:?}", config.dkg_id(), + parent.height.increment(), err ); continue; @@ -249,7 +251,9 @@ pub(crate) fn create_early_remote_transcripts( other => { warn!( logger, - "Produced unexpected number of early remote NiDKG transcripts: {other}" + "Produced unexpected number of early remote NiDKG transcripts at height {}: {}", + parent.height.increment(), + other ); false } diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index 4195b893f260..5670752b69b5 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -215,16 +215,18 @@ fn validate_dealings_payload( if dealings.transcripts_for_remote_subnets != expected_transcripts { warn!( log, - "Failed to validate {} early remote DKG transcripts in data block payload", - dealings.transcripts_for_remote_subnets.len() + "Failed to validate {} early remote DKG transcripts in data block payload at height {}", + dealings.transcripts_for_remote_subnets.len(), + parent.height.increment(), ); return Err(InvalidDkgPayloadReason::InvalidTranscripts.into()); } info!( log, - "Validated {} early remote DKG transcripts in data block payload", - dealings.transcripts_for_remote_subnets.len() + "Validated {} early remote DKG transcripts in data block payload at height {}", + dealings.transcripts_for_remote_subnets.len(), + parent.height.increment(), ); } From ca29e9b24e375da8da9db72d2cb94a1094919d64 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 2 Feb 2026 12:45:41 +0000 Subject: [PATCH 50/98] types --- rs/consensus/dkg/src/payload_builder.rs | 2 +- rs/consensus/dkg/src/payload_validator.rs | 104 +++++++++++- rs/consensus/dkg/src/utils.rs | 85 ++++++---- rs/consensus/src/consensus/batch_delivery.rs | 162 ++++++++++++++----- rs/protobuf/def/types/v1/dkg.proto | 1 + rs/protobuf/src/gen/types/types.v1.rs | 2 + rs/types/types/src/consensus/dkg.rs | 15 +- 7 files changed, 304 insertions(+), 67 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 236f93fd0b1c..7c5053dc8fb7 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -158,7 +158,7 @@ pub(super) fn create_summary_payload( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { - let all_dealings = utils::get_dkg_dealings(pool_reader, parent); + let (all_dealings, _) = utils::get_dkg_dealings(pool_reader, parent); let mut transcripts_for_remote_subnets = BTreeMap::new(); let mut next_transcripts = BTreeMap::new(); // Try to create transcripts from the last round. diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index 4c93f4138f80..b29f9b833ca4 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -193,6 +193,12 @@ fn validate_dealings_payload( crypto_validate_dealing(crypto, config, message)?; } + // If we have early transcripts, we compare them + if !dealings.transcripts_for_remote_subnets.is_empty() { + // For now payloads with early transcripts are rejected + return Err(InvalidDkgPayloadReason::InvalidEarlyNiDkgTranscripts.into()); + } + Ok(()) } @@ -203,7 +209,7 @@ mod tests { use ic_artifact_pool::dkg_pool::DkgPoolImpl; use ic_consensus_mocks::{Dependencies, dependencies_with_subnet_params}; use ic_crypto_temp_crypto::{NodeKeysToGenerate, TempCryptoComponent}; - use ic_crypto_test_utils_ni_dkg::dummy_dealing; + use ic_crypto_test_utils_ni_dkg::{dummy_dealing, dummy_transcript_for_tests}; use ic_interfaces::{ consensus_pool::ConsensusPool, dkg::ChangeAction, @@ -226,6 +232,7 @@ mod tests { idkg, }, crypto::threshold_sig::ni_dkg::{NiDkgId, NiDkgTag, NiDkgTargetSubnet}, + messages::CallbackId, time::UNIX_EPOCH, }; use std::{ @@ -443,6 +450,101 @@ mod tests { ); } + #[test] + fn validate_dealings_payload_when_remote_transcripts_present_fails_test() { + // Data payloads with early/remote transcripts are rejected for now. + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + let registry_version = 1; + let committee = [NODE_1, NODE_2, NODE_3]; + + let Dependencies { + crypto, + pool, + dkg_pool, + registry, + state_manager, + .. + } = dependencies_with_subnet_params( + pool_config.clone(), + SUBNET_1, + vec![( + registry_version, + SubnetRecordBuilder::from(&committee) + .with_dkg_dealings_per_block(1) + .build(), + )], + ); + + let mut parent = Block::from(pool.make_next_block()); + parent.payload = Payload::new( + ic_types::crypto::crypto_hash, + BlockPayload::Data(DataPayload { + batch: BatchPayload::default(), + dkg: DkgDataPayload::new(Height::from(0), vec![]), + idkg: idkg::Payload::default(), + }), + ); + + let context = ValidationContext { + registry_version: RegistryVersion::from(registry_version), + certified_height: Height::from(0), + time: ic_types::time::UNIX_EPOCH, + }; + + let dkg_data_with_remote_transcripts = DkgDataPayload { + start_height: Height::from(0), + messages: vec![], + transcripts_for_remote_subnets: vec![ + ( + NiDkgId { + start_block_height: Height::from(0), + dealer_subnet: SUBNET_1, + target_subnet: NiDkgTargetSubnet::Local, + dkg_tag: NiDkgTag::HighThreshold, + }, + CallbackId::from(0), + Err("dummy".to_string()), + ), + ( + NiDkgId { + start_block_height: Height::from(0), + dealer_subnet: SUBNET_1, + target_subnet: NiDkgTargetSubnet::Local, + dkg_tag: NiDkgTag::LowThreshold, + }, + CallbackId::from(1), + Ok(dummy_transcript_for_tests()), + ), + ], + }; + + let block_payload = BlockPayload::Data(DataPayload { + batch: BatchPayload::default(), + dkg: dkg_data_with_remote_transcripts, + idkg: idkg::Payload::default(), + }); + + assert_eq!( + validate_payload( + SUBNET_1, + registry.as_ref(), + crypto.as_ref(), + &PoolReader::new(&pool), + dkg_pool.read().unwrap().deref(), + parent, + &block_payload, + state_manager.as_ref(), + &context, + &mock_metrics(), + &no_op_logger(), + ), + Err(DkgPayloadValidationError::InvalidArtifact( + InvalidDkgPayloadReason::InvalidEarlyNiDkgTranscripts + )) + ); + }) + } + /// Configures all the dependencies and calls [`validate_payload`] with /// `dealings_to_validate` as an argument. #[allow(clippy::result_large_err)] diff --git a/rs/consensus/dkg/src/utils.rs b/rs/consensus/dkg/src/utils.rs index cfd51c1f23c1..0313d9c0d212 100644 --- a/rs/consensus/dkg/src/utils.rs +++ b/rs/consensus/dkg/src/utils.rs @@ -7,7 +7,7 @@ use ic_types::{ NodeId, RegistryVersion, SubnetId, consensus::{ Block, - dkg::{DkgPayloadCreationError, DkgSummary}, + dkg::{DkgPayloadCreationError, DkgSummary, Message}, }, crypto::{ AlgorithmId, @@ -18,7 +18,7 @@ use ic_types::{ }, }, }; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; /// Returns the [`MasterPublicKey`]s and also the [`NiDkgId`]s of the `next_transcript` /// (or, if unavialable, of the `current_transcript`) corresponding to the [`MasterPublicKeyId`]s @@ -86,7 +86,8 @@ pub(super) fn get_dealers_from_chain( pool_reader: &PoolReader<'_>, block: &Block, ) -> HashSet<(NiDkgId, NodeId)> { - get_dkg_dealings(pool_reader, block) + let (dkg_dealings, _) = extract_from_dkg_dealing_messages(pool_reader, block, false, |_| ()); + dkg_dealings .into_iter() .flat_map(|(dkg_id, dealings)| { dealings @@ -96,35 +97,65 @@ pub(super) fn get_dealers_from_chain( .collect() } -// Starts with the given block and creates a nested mapping from the DKG Id to -// the node Id to the dealing. This function panics if multiple dealings -// from one dealer are discovered, hence, we assume a valid block chain. +/// Starts with the given block and creates a nested mapping from the DKG Id to +/// the node Id to the dealing. This function panics if multiple dealings +/// from one dealer are discovered, hence, we assume a valid block chain. +/// It also excludes dealings for ni_dkg ids which already have a transcript in the +/// blockchain, and returns these exlcuded ni_dkg ids. pub(super) fn get_dkg_dealings( pool_reader: &PoolReader<'_>, block: &Block, -) -> BTreeMap> { - pool_reader +) -> ( + BTreeMap>, + BTreeSet, +) { + extract_from_dkg_dealing_messages(pool_reader, block, true, |message| { + message.content.dealing.clone() + }) +} + +fn extract_from_dkg_dealing_messages( + pool_reader: &PoolReader<'_>, + block: &Block, + exclude_used: bool, + extractor: impl Fn(&Message) -> T, +) -> (BTreeMap>, BTreeSet) { + let mut dealings: BTreeMap> = BTreeMap::new(); + let mut excluded: BTreeSet = BTreeSet::new(); + + for block in pool_reader .chain_iterator(block.clone()) .take_while(|block| !block.payload.is_summary()) - .fold(Default::default(), |mut acc, block| { - block - .payload - .as_ref() - .as_data() - .dkg - .messages - .iter() - .for_each(|msg| { - let collected_dealings = acc.entry(msg.content.dkg_id.clone()).or_default(); - assert!( - collected_dealings - .insert(msg.signature.signer, msg.content.dealing.clone()) - .is_none(), - "Dealings from the same dealers discovered." - ); - }); - acc - }) + { + let payload = &block.payload.as_ref().as_data().dkg; + + if exclude_used { + for (dkg_id, _, _) in payload.transcripts_for_remote_subnets.iter() { + // Add the finished DKG to excluded list + excluded.insert(dkg_id.clone()); + // Remove already selected dealings + dealings.remove(dkg_id); + } + } + + for message in payload.messages.iter() { + if excluded.contains(&message.content.dkg_id) { + continue; + } + + let old_dealing = dealings + .entry(message.content.dkg_id.clone()) + .or_default() + .insert(message.signature.signer, extractor(message)); + + assert!( + old_dealing.is_none(), + "Dealings from the same dealers discovered." + ); + } + } + + (dealings, excluded) } /// Fetch all key ids for which the subnet should hold a key diff --git a/rs/consensus/src/consensus/batch_delivery.rs b/rs/consensus/src/consensus/batch_delivery.rs index dd1ab0e79268..272f2a612c55 100644 --- a/rs/consensus/src/consensus/batch_delivery.rs +++ b/rs/consensus/src/consensus/batch_delivery.rs @@ -331,6 +331,12 @@ fn generate_responses_to_subnet_calls( )) } else { let block_payload = block_payload.as_ref().as_data(); + + consensus_responses.append(&mut generate_responses_to_remote_dkgs( + &block_payload.dkg.transcripts_for_remote_subnets, + log, + )); + if let Some(payload) = &block_payload.idkg { consensus_responses.append(&mut generate_responses_to_signature_request_contexts( payload, @@ -557,37 +563,45 @@ mod tests { use ic_management_canister_types_private::{SetupInitialDKGResponse, VetKdCurve, VetKdKeyId}; use ic_test_utilities_types::ids::subnet_test_id; use ic_types::{ - PrincipalId, SubnetId, - crypto::threshold_sig::ni_dkg::{ - NiDkgId, NiDkgMasterPublicKeyId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet, + PrincipalId, RegistryVersion, SubnetId, + batch::{BatchPayload, ValidationContext}, + consensus::{DataPayload, Payload as ConsensusPayload, Rank, dkg::DkgDataPayload}, + crypto::{ + CryptoHash, CryptoHashOf, + threshold_sig::ni_dkg::{ + NiDkgId, NiDkgMasterPublicKeyId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet, + }, }, messages::{CallbackId, Payload}, + time::UNIX_EPOCH, }; use std::str::FromStr; + const TARGET_ID: NiDkgTargetId = NiDkgTargetId::new([8; 32]); + + const EXPECTED_FRESH_SUBNET_ID_STR: &str = + "icdrs-3sfmz-hm6r3-cdzf5-cfroa-3cddh-aght7-azz25-eo34b-4strl-wae"; + + fn ni_dkg_id(dkg_tag: NiDkgTag) -> NiDkgId { + NiDkgId { + start_block_height: Height::from(0), + dealer_subnet: subnet_test_id(0), + dkg_tag, + target_subnet: NiDkgTargetSubnet::Remote(TARGET_ID), + } + } + #[test] fn test_generate_setup_initial_dkg_response() { - const TARGET_ID: NiDkgTargetId = NiDkgTargetId::new([8; 32]); - // Build some transcipts with matching ids and tags let transcripts_for_remote_subnets = [ ( - NiDkgId { - start_block_height: Height::from(0), - dealer_subnet: subnet_test_id(0), - dkg_tag: NiDkgTag::LowThreshold, - target_subnet: NiDkgTargetSubnet::Remote(TARGET_ID), - }, + ni_dkg_id(NiDkgTag::LowThreshold), CallbackId::from(1), Ok(dummy_transcript_for_tests()), ), ( - NiDkgId { - start_block_height: Height::from(0), - dealer_subnet: subnet_test_id(0), - dkg_tag: NiDkgTag::HighThreshold, - target_subnet: NiDkgTargetSubnet::Remote(TARGET_ID), - }, + ni_dkg_id(NiDkgTag::HighThreshold), CallbackId::from(1), Ok(dummy_transcript_for_tests()), ), @@ -598,26 +612,18 @@ mod tests { assert_eq!(result.len(), 1); // Deserialize the `SetupInitialDKGResponse` and check the subnet id - let payload = match &result[0].payload { - Payload::Data(data) => data, - Payload::Reject(_) => panic!("Payload was rejected unexpectedly"), + let Payload::Data(payload) = &result[0].payload else { + panic!("Payload was rejected unexpectedly"); }; let initial_transcript_records = SetupInitialDKGResponse::decode(payload).unwrap(); assert_eq!( initial_transcript_records.fresh_subnet_id, - SubnetId::from( - PrincipalId::from_str( - "icdrs-3sfmz-hm6r3-cdzf5-cfroa-3cddh-aght7-azz25-eo34b-4strl-wae" - ) - .unwrap() - ) + SubnetId::from(PrincipalId::from_str(EXPECTED_FRESH_SUBNET_ID_STR).unwrap()) ); } #[test] fn test_generate_request_chain_key_nidkg_response() { - const TARGET_ID: NiDkgTargetId = NiDkgTargetId::new([8; 32]); - let key_id: NiDkgMasterPublicKeyId = NiDkgMasterPublicKeyId::VetKd(VetKdKeyId { curve: VetKdCurve::Bls12_381_G2, name: String::from("test_vetkd_key"), @@ -625,12 +631,7 @@ mod tests { // Build some transcipts with matching ids and tags let transcripts_for_remote_subnets = [( - NiDkgId { - start_block_height: Height::from(0), - dealer_subnet: subnet_test_id(0), - dkg_tag: NiDkgTag::HighThresholdForKey(key_id.clone()), - target_subnet: NiDkgTargetSubnet::Remote(TARGET_ID), - }, + ni_dkg_id(NiDkgTag::HighThresholdForKey(key_id.clone())), CallbackId::from(2), Ok(dummy_transcript_for_tests()), )]; @@ -640,13 +641,100 @@ mod tests { assert_eq!(result.len(), 1); // Deserialize the `ReshareChainKeyResponse` - let payload = match &result[0].payload { - Payload::Data(data) => data, - Payload::Reject(_) => panic!("Payload was rejected unexpectedly"), + let Payload::Data(payload) = &result[0].payload else { + panic!("Payload was rejected unexpectedly"); }; let response = ReshareChainKeyResponse::decode(payload).unwrap(); let ReshareChainKeyResponse::NiDkg(_response) = response else { panic!("Expected a NiDkg response"); }; } + + #[test] + fn test_generate_responses_for_early_remote_dkg_transcripts() { + let key_id: NiDkgMasterPublicKeyId = NiDkgMasterPublicKeyId::VetKd(VetKdKeyId { + curve: VetKdCurve::Bls12_381_G2, + name: String::from("test_vetkd_key"), + }); + + let dummy_transcript = dummy_transcript_for_tests(); + let dkg_data = DkgDataPayload { + start_height: Height::from(0), + messages: vec![], + transcripts_for_remote_subnets: vec![ + // ReshareChainKey (NiDkg) → one response + ( + ni_dkg_id(NiDkgTag::HighThresholdForKey(key_id.clone())), + CallbackId::from(42), + Ok(dummy_transcript.clone()), + ), + // SetupInitialDKG: low + high threshold for same callback → one response + ( + ni_dkg_id(NiDkgTag::LowThreshold), + CallbackId::from(1), + Ok(dummy_transcript.clone()), + ), + ( + ni_dkg_id(NiDkgTag::HighThreshold), + CallbackId::from(1), + Ok(dummy_transcript), + ), + ], + }; + + let block_payload = BlockPayload::Data(DataPayload { + batch: BatchPayload::default(), + dkg: dkg_data, + idkg: None, + }); + + let payload = ConsensusPayload::new(ic_types::crypto::crypto_hash, block_payload); + + let block = Block::new( + CryptoHashOf::from(CryptoHash(vec![0u8; 32])), + payload, + Height::from(1), + Rank(0), + ValidationContext { + registry_version: RegistryVersion::from(1), + certified_height: Height::from(0), + time: UNIX_EPOCH, + }, + ); + + let mut batch_stats = BatchStats::new(Height::from(1)); + let responses = + generate_responses_to_subnet_calls(&block, &mut batch_stats, &no_op_logger()); + + assert_eq!( + responses.len(), + 2, + "expected two responses: ReshareChainKey and SetupInitialDKG" + ); + + let reshare_response = responses + .iter() + .find(|r| r.callback == CallbackId::from(42)) + .expect("expected ReshareChainKey response for callback 42"); + let Payload::Data(payload_data) = &reshare_response.payload else { + panic!("ReshareChainKey payload was rejected unexpectedly"); + }; + let response = ReshareChainKeyResponse::decode(payload_data).unwrap(); + let ReshareChainKeyResponse::NiDkg(_) = response else { + panic!("Expected a NiDkg response for early remote DKG transcript"); + }; + + let setup_initial_response = responses + .iter() + .find(|r| r.callback == CallbackId::from(1)) + .expect("expected SetupInitialDKG response for callback 1"); + let Payload::Data(payload_data) = &setup_initial_response.payload else { + panic!("SetupInitialDKG payload was rejected unexpectedly"); + }; + let initial_response = SetupInitialDKGResponse::decode(payload_data).unwrap(); + assert_eq!( + initial_response.fresh_subnet_id, + SubnetId::from(PrincipalId::from_str(EXPECTED_FRESH_SUBNET_ID_STR).unwrap()) + ); + } } diff --git a/rs/protobuf/def/types/v1/dkg.proto b/rs/protobuf/def/types/v1/dkg.proto index 1dd34f86d559..c86bb33c2d5f 100644 --- a/rs/protobuf/def/types/v1/dkg.proto +++ b/rs/protobuf/def/types/v1/dkg.proto @@ -25,6 +25,7 @@ message DkgPayload { message DkgDataPayload { repeated DkgMessage dealings = 1; uint64 summary_height = 2; + repeated CallbackIdedNiDkgTranscript transcripts_for_remote_subnets = 3; } message Summary { diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index 1888ae51605c..171f91d2f27e 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -366,6 +366,8 @@ pub struct DkgDataPayload { pub dealings: ::prost::alloc::vec::Vec, #[prost(uint64, tag = "2")] pub summary_height: u64, + #[prost(message, repeated, tag = "3")] + pub transcripts_for_remote_subnets: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct Summary { diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index 49b57b4ec6a3..8a0e5547c5e3 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -469,6 +469,8 @@ pub struct DkgDataPayload { pub start_height: Height, /// The dealing messages pub messages: DealingMessages, + /// Transcripts that are computed for remote subnets. + pub transcripts_for_remote_subnets: Vec<(NiDkgId, CallbackId, Result)>, } impl TryFrom for DkgDataPayload { @@ -482,6 +484,10 @@ impl TryFrom for DkgDataPayload { .into_iter() .map(Message::try_from) .collect::>()?, + transcripts_for_remote_subnets: build_transcripts_vec_from_pb( + data_payload.transcripts_for_remote_subnets, + ) + .map_err(ProxyDecodeError::Other)?, }) } } @@ -497,6 +503,7 @@ impl DkgDataPayload { Self { start_height, messages, + transcripts_for_remote_subnets: vec![], } } @@ -505,8 +512,9 @@ impl DkgDataPayload { let DkgDataPayload { start_height: _, messages, + transcripts_for_remote_subnets, } = self; - messages.is_empty() + messages.is_empty() && transcripts_for_remote_subnets.is_empty() } } @@ -543,6 +551,9 @@ impl From<&DkgDataPayload> for pb::DkgPayload { .map(pb::DkgMessage::from) .collect(), summary_height: data_payload.start_height.get(), + transcripts_for_remote_subnets: build_callback_ided_transcripts_vec( + data_payload.transcripts_for_remote_subnets.as_slice(), + ), })), } } @@ -597,6 +608,8 @@ pub enum InvalidDkgPayloadReason { limit: usize, actual: usize, }, + /// The early NiDKG transcripts that were included with this payload are invalid + InvalidEarlyNiDkgTranscripts, } /// Possible failures which could occur while validating a dkg payload. They don't imply that the From e538cd09ada40c09a4912c9fa6ac7fbb6d0bf37b Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 2 Feb 2026 13:46:37 +0000 Subject: [PATCH 51/98] test --- rs/consensus/dkg/src/test_utils.rs | 15 ++++- rs/consensus/dkg/src/utils.rs | 104 ++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index 5622e1ad6481..56cbc6fbb400 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -1,11 +1,17 @@ +use ic_crypto_test_utils_ni_dkg::dummy_dealing; use ic_interfaces_state_manager::Labeled; use ic_management_canister_types_private::{MasterPublicKeyId, VetKdKeyId}; use ic_replicated_state::metadata_state::subnet_call_context_manager::{ ReshareChainKeyContext, SetupInitialDkgContext, SubnetCallContext, }; use ic_test_utilities::state_manager::RefMockStateManager; +use ic_test_utilities_consensus::fake::FakeContentSigner; use ic_test_utilities_types::{ids::node_test_id, messages::RequestBuilder}; -use ic_types::{Height, RegistryVersion, crypto::threshold_sig::ni_dkg::NiDkgTargetId}; +use ic_types::{ + Height, RegistryVersion, + consensus::dkg::{DealingContent, Message}, + crypto::threshold_sig::ni_dkg::{NiDkgId, NiDkgTargetId}, +}; use std::sync::Arc; pub(super) fn complement_state_manager_with_setup_initial_dkg_request( @@ -75,3 +81,10 @@ pub(super) fn complement_state_manager_with_reshare_chain_key_request( expectation.times(times); } } + +/// Creates a fake DKG dealing message for tests, using `node_idx` as the dummy +/// seed and `node_test_id(node_idx)` as the dealer. +pub(super) fn create_dealing(node_idx: u64, dkg_id: NiDkgId) -> Message { + let content = DealingContent::new(dummy_dealing(node_idx as u8), dkg_id); + Message::fake(content, node_test_id(node_idx)) +} diff --git a/rs/consensus/dkg/src/utils.rs b/rs/consensus/dkg/src/utils.rs index 0313d9c0d212..53375dd61e46 100644 --- a/rs/consensus/dkg/src/utils.rs +++ b/rs/consensus/dkg/src/utils.rs @@ -196,7 +196,11 @@ pub(crate) fn tags_iter( #[cfg(test)] mod tests { + use super::get_dkg_dealings; + use crate::test_utils::create_dealing; use crate::utils::vetkd_key_ids_for_subnet; + use ic_consensus_mocks::{Dependencies, dependencies_with_subnet_params}; + use ic_consensus_utils::pool_reader::PoolReader; use ic_interfaces_registry::RegistryValue; use ic_interfaces_registry_mocks::MockRegistryClient; use ic_management_canister_types_private::{ @@ -204,8 +208,17 @@ mod tests { }; use ic_registry_subnet_features::{ChainKeyConfig, KeyConfig}; use ic_test_utilities_registry::SubnetRecordBuilder; - use ic_test_utilities_types::ids::subnet_test_id; - use ic_types::{RegistryVersion, crypto::threshold_sig::ni_dkg::NiDkgMasterPublicKeyId}; + use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; + use ic_types::{ + Height, RegistryVersion, + batch::BatchPayload, + consensus::{Block, BlockPayload, DataPayload, Payload, Rank, dkg::DkgDataPayload, idkg}, + crypto::{ + crypto_hash, + threshold_sig::ni_dkg::{NiDkgId, NiDkgMasterPublicKeyId, NiDkgTag, NiDkgTargetSubnet}, + }, + messages::CallbackId, + }; /// Test that `get_enabled_vet_keys` correctly extracts the vet keys that are in the [`SubnetRecord`] of the /// subnet. @@ -267,5 +280,92 @@ mod tests { ); } + fn dkg_id(subnet_id: ic_types::SubnetId, tag: NiDkgTag) -> NiDkgId { + NiDkgId { + start_block_height: Height::from(0), + dealer_subnet: subnet_id, + target_subnet: NiDkgTargetSubnet::Local, + dkg_tag: tag, + } + } + + #[test] + fn test_get_dkg_dealings_included_and_excluded_by_transcript() { + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + let subnet_id = subnet_test_id(1); + let nodes: Vec<_> = (0..3).map(node_test_id).collect(); + let dkg_interval_len = 10; + let Dependencies { mut pool, .. } = dependencies_with_subnet_params( + pool_config, + subnet_id, + vec![( + 1, + SubnetRecordBuilder::from(&nodes) + .with_dkg_interval_length(dkg_interval_len) + .build(), + )], + ); + + pool.advance_round_normal_operation_n(dkg_interval_len); + let pool_reader = PoolReader::new(&pool); + let tip = pool_reader.get_finalized_tip(); + + // DKG id that has a transcript in this block -> its dealings should be excluded. + let dkg_id_with_transcript = dkg_id(subnet_id, NiDkgTag::HighThreshold); + // DKG id with no transcript -> its dealings should be included. + let dkg_id_without_transcript = dkg_id(subnet_id, NiDkgTag::LowThreshold); + + // Dealings for the transcript DKG id (excluded) and for another DKG id (included). + let msg_excluded_1 = create_dealing(0, dkg_id_with_transcript.clone()); + let msg_excluded_2 = create_dealing(1, dkg_id_with_transcript.clone()); + let msg_included_1 = create_dealing(0, dkg_id_without_transcript.clone()); + let msg_included_2 = create_dealing(1, dkg_id_without_transcript.clone()); + + let dkg_payload = DkgDataPayload { + start_height: tip.payload.as_ref().as_data().dkg.start_height, + messages: vec![ + msg_excluded_1.clone(), + msg_excluded_2.clone(), + msg_included_1.clone(), + msg_included_2.clone(), + ], + transcripts_for_remote_subnets: vec![( + dkg_id_with_transcript.clone(), + CallbackId::from(0), + Err("dummy".to_string()), + )], + }; + + let child_block = Block::new( + crypto_hash(&tip), + Payload::new(crypto_hash, { + BlockPayload::Data(DataPayload { + batch: BatchPayload::default(), + dkg: dkg_payload, + idkg: idkg::Payload::default(), + }) + }), + tip.height.increment(), + Rank(0), + tip.context.clone(), + ); + + let (dealings, excluded) = get_dkg_dealings(&pool_reader, &child_block); + + // Excluded: only the DKG id that has a transcript. + assert_eq!(excluded.len(), 1); + assert!(excluded.contains(&dkg_id_with_transcript)); + + // Dealings for the transcript DKG id must not appear in the result. + assert!(!dealings.contains_key(&dkg_id_with_transcript)); + + // Dealings for the other DKG id must be included with both dealers. + let included = dealings.get(&dkg_id_without_transcript).unwrap(); + assert_eq!(included.len(), 2); + assert!(included.contains_key(&node_test_id(0))); + assert!(included.contains_key(&node_test_id(1))); + }); + } + // TODO: Unit test for `get_vetkey_public_keys`. (Currently its covered through a system test) } From fdcc343dac430c6fd896345a274f475bb72c11d9 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 2 Feb 2026 13:46:51 +0000 Subject: [PATCH 52/98] test --- rs/consensus/dkg/src/test_utils.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index 56cbc6fbb400..00ddacd774d1 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -82,8 +82,6 @@ pub(super) fn complement_state_manager_with_reshare_chain_key_request( } } -/// Creates a fake DKG dealing message for tests, using `node_idx` as the dummy -/// seed and `node_test_id(node_idx)` as the dealer. pub(super) fn create_dealing(node_idx: u64, dkg_id: NiDkgId) -> Message { let content = DealingContent::new(dummy_dealing(node_idx as u8), dkg_id); Message::fake(content, node_test_id(node_idx)) From f7c05f4a6641c75cfaa37de48ddaf2a05344b9c4 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 2 Feb 2026 14:02:33 +0000 Subject: [PATCH 53/98] fix --- rs/consensus/dkg/src/utils.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rs/consensus/dkg/src/utils.rs b/rs/consensus/dkg/src/utils.rs index 53375dd61e46..8209e340901f 100644 --- a/rs/consensus/dkg/src/utils.rs +++ b/rs/consensus/dkg/src/utils.rs @@ -201,6 +201,7 @@ mod tests { use crate::utils::vetkd_key_ids_for_subnet; use ic_consensus_mocks::{Dependencies, dependencies_with_subnet_params}; use ic_consensus_utils::pool_reader::PoolReader; + use ic_crypto_test_utils_ni_dkg::dummy_transcript_for_tests; use ic_interfaces_registry::RegistryValue; use ic_interfaces_registry_mocks::MockRegistryClient; use ic_management_canister_types_private::{ @@ -332,7 +333,7 @@ mod tests { transcripts_for_remote_subnets: vec![( dkg_id_with_transcript.clone(), CallbackId::from(0), - Err("dummy".to_string()), + Ok(dummy_transcript_for_tests()), )], }; From 0e6d05eca12a79954ad65dd827ddf770c61b9ebc Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 2 Feb 2026 16:47:44 +0000 Subject: [PATCH 54/98] fix tests --- rs/types/types/src/crypto/hash/tests.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index de55f37172c8..7fd3151f9d1d 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -497,7 +497,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "5c698f370c0f6bf8d53f71c65309a50aefc5114091e34d40cb027e2340581413", + "764535296841f3db421a928cfadff3460be406d0182da64034eee623a9a97e99", "Hash of CatchUpContent changed" ); } @@ -515,7 +515,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "ae57d4db84330cb8ab8f0040e894b0b4a9b7ce22f107be0c3c86c841b580fe7d", + "7f183aaeb495159567a340b5bf61233cf3226141268febaee47de3e4c69cbc4b", "Hash of CatchUpShareContent changed" ); } @@ -563,7 +563,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "998dcb7e71838ac32c2615f61ddd986676f9a3f9d6a8ab3f5db42a4f80dd49a9", + "31f744bc26627fadbf1d73c66cb54603319a87966a488b6f41c4f0cfc1a30c89", "Hash of CatchUpPackage changed" ); } @@ -593,7 +593,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "44335947edd911fd04952bc699a411889801764e6e2e814bb77cc3fafc58b4f9", + "bff423705e4cb96b7a391c4cccba8ed1ce441dabf2693ed5b9545a2b57d946bd", "Hash of CatchUpPackageShare changed" ); } @@ -934,7 +934,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "0043108046ad04abcc970730f1e7e1cd4b3918725362adc620fe391a3239d6b4", + "b040378bc7d9d2b7c2e9067215eae6380a65316922369a1bc6d8376f31fe5d0a", "Hash of Block changed" ); } @@ -976,7 +976,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "ead07112fa9ef8117f3d4171cf66c3d49bfd9d39b6a3c2d44b5c0ce3ecf4c903", + "d591d695f67c644ddcc5315d96c25f00dede77c725859408ab7f113a18a0bf9a", "Hash of BlockProposal changed" ); } @@ -1013,7 +1013,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "e6030b101e9c9d92eb352d6ada01bd6dc64e1f358d30a3c113411f4f8201b369", + "c94d927dd7300814fef610a7560ba5a7775a859bb3511796cf23cfb59c038a4f", "Hash of BlockPayload changed" ); } From 0654f21c5cf0db8a4acd5314a1b773a8b207857a Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 3 Feb 2026 10:20:37 +0000 Subject: [PATCH 55/98] docs --- rs/consensus/dkg/src/lib.rs | 2 +- rs/consensus/dkg/src/payload_builder.rs | 2 +- rs/types/types/src/consensus/dkg.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index d9924ea57ae4..cc59ee25c2de 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -45,7 +45,7 @@ pub use payload_builder::{create_payload, get_dkg_summary_from_cup_contents}; /// The maximal number of DKGs for other subnets we want to run in one interval. const MAX_REMOTE_DKGS_PER_INTERVAL: usize = 1; -/// The maximum number of early remote transcript responses we want to include in a data payload. +/// The maximum number of early remote DKG responses we want to include in a data payload. /// Note that responses for `SetupInitialDKG` requests contain two transcripts. const MAX_EARLY_REMOTE_TRANSCRIPT_RESPONSES: usize = 1; diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index d4a130f5cfcd..ab4c851d7a69 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -143,7 +143,7 @@ fn create_data_payload( ); } - // Try to include remote transcripts + // Include any early remote transcripts Ok(DkgDataPayload::new_with_remote_dkg_transcripts( last_summary_block.height, new_validated_dealings, diff --git a/rs/types/types/src/consensus/dkg.rs b/rs/types/types/src/consensus/dkg.rs index 8c264c02e9eb..31dc04b88614 100644 --- a/rs/types/types/src/consensus/dkg.rs +++ b/rs/types/types/src/consensus/dkg.rs @@ -493,12 +493,12 @@ impl TryFrom for DkgDataPayload { } impl DkgDataPayload { - /// Return an empty DealingsPayload using the given start_height. + /// Return an empty [`DkgDataPayload`] using the given start_height. pub fn new_empty(start_height: Height) -> Self { Self::new(start_height, vec![]) } - /// Return an new DealingsPayload. + /// Return a new [`DkgDataPayload`]. pub fn new(start_height: Height, messages: DealingMessages) -> Self { Self { start_height, @@ -507,7 +507,7 @@ impl DkgDataPayload { } } - /// Return an new DealingsPayload. + /// Return a new [`DkgDataPayload`] with the given remote DKG transcripts. pub fn new_with_remote_dkg_transcripts( start_height: Height, messages: DealingMessages, From 3fec9d0eb082ae1d842071ae593913a206a30aba Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 3 Feb 2026 11:03:45 +0000 Subject: [PATCH 56/98] add vetkd test case --- rs/consensus/dkg/src/lib.rs | 167 +++++++++++++++++++++- rs/consensus/dkg/src/payload_validator.rs | 4 +- rs/consensus/dkg/src/test_utils.rs | 38 +---- 3 files changed, 163 insertions(+), 46 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index cc59ee25c2de..da6b84699b3d 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -397,7 +397,6 @@ impl BouncerFactory for DkgBouncer { mod tests { use super::*; use crate::test_utils::{ - complement_state_manager_with_remote_dkg_requests, complement_state_manager_with_reshare_chain_key_request, complement_state_manager_with_setup_initial_dkg_request, create_dealing, extract_dkg_configs_from_highest_block, extract_remote_dkg_ids_from_highest_block, @@ -426,7 +425,7 @@ mod tests { use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ RegistryVersion, ReplicaVersion, - consensus::{Block, BlockPayload, HasHeight}, + consensus::{Block, BlockPayload, DataPayload, HasHeight, dkg::DkgDataPayload}, crypto::threshold_sig::ni_dkg::{ NiDkgId, NiDkgMasterPublicKeyId, NiDkgTargetId, NiDkgTargetSubnet, }, @@ -1609,7 +1608,7 @@ mod tests { } #[test] - fn test_early_remote_dkg_transcripts() { + fn test_early_setup_initial_dkg_transcripts() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let node_ids = (1..4).map(node_test_id).collect::>(); let dkg_interval_length = 99; @@ -1634,12 +1633,12 @@ mod tests { ); let target_id = NiDkgTargetId::new([0u8; 32]); - complement_state_manager_with_remote_dkg_requests( + complement_state_manager_with_setup_initial_dkg_request( state_manager.clone(), registry.get_latest_version(), vec![10, 11, 12, 13], None, - target_id, + Some(target_id), ); // Verify that the next summary block contains the configs and no transcripts. @@ -1699,6 +1698,41 @@ mod tests { .map(|block| block.into_inner()) .unwrap(); + assert!( + validate_payload( + subnet_test_id(0), + registry.as_ref(), + crypto.as_ref(), + &pool_reader, + &*dkg_pool.read().unwrap(), + parent.clone(), + block.payload.as_ref(), + state_manager.as_ref(), + &block.context, + &MetricsRegistry::new().int_counter_vec( + "consensus_dkg_validator", + "DKG validator counter", + &["type"], + ), + &no_op_logger(), + ) + .is_ok() + ); + + // Validate the same payload with empty transcripts_for_remote_subnets. + // Since early transcripts are only an optimization,validation should still succeed. + let payload_without_early_remote = match block.payload.as_ref() { + BlockPayload::Data(data) => { + let dkg_without_remote = + DkgDataPayload::new(data.dkg.start_height, data.dkg.messages.clone()); + BlockPayload::Data(DataPayload { + batch: data.batch.clone(), + dkg: dkg_without_remote, + idkg: data.idkg.clone(), + }) + } + _ => panic!("expected data block"), + }; assert!( validate_payload( subnet_test_id(0), @@ -1707,6 +1741,125 @@ mod tests { &pool_reader, &*dkg_pool.read().unwrap(), parent, + &payload_without_early_remote, + state_manager.as_ref(), + &block.context, + &MetricsRegistry::new().int_counter_vec( + "consensus_dkg_validator", + "DKG validator counter", + &["type"], + ), + &no_op_logger(), + ) + .is_ok() + ); + + // Advance the pool a until the next DKG, check that the early remote transcripts are not + // generated multiple times, and in particular that they are not included in the summary. + for _ in 0..100 { + pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + } + }); + } + + #[test] + fn test_early_reshare_chain_key_transcripts() { + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + let node_ids = (1..4).map(node_test_id).collect::>(); + let dkg_interval_length = 99; + let subnet_id = subnet_test_id(0); + let key_id = VetKdKeyId { + curve: VetKdCurve::Bls12_381_G2, + name: String::from("some_vetkey"), + }; + let target_id = NiDkgTargetId::new([0u8; 32]); + + let Dependencies { + mut pool, + registry, + state_manager, + dkg_pool, + crypto, + .. + } = dependencies_with_subnet_records_with_raw_state_manager( + pool_config, + subnet_id, + vec![( + 10, + SubnetRecordBuilder::from(&node_ids) + .with_dkg_interval_length(dkg_interval_length) + .with_chain_key_config(ChainKeyConfig { + key_configs: vec![KeyConfig { + key_id: MasterPublicKeyId::VetKd(key_id.clone()), + pre_signatures_to_create_in_advance: None, + max_queue_size: 20, + }], + signature_request_timeout_ns: None, + idkg_key_rotation_period_ms: None, + max_parallel_pre_signature_transcripts_in_creation: None, + }) + .build(), + )], + ); + + complement_state_manager_with_reshare_chain_key_request( + state_manager.clone(), + registry.get_latest_version(), + key_id, + vec![10, 11, 12, 13], + None, + Some(target_id), + ); + + // Verify that the next summary block contains the configs and no transcripts. + // This also extracts the DKG ids (1 remote for reshare chain key) + pool.advance_round_normal_operation_n(dkg_interval_length + 1); + let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&pool, target_id); + assert_eq!(remote_dkg_ids.len(), 1); + assert_eq!(extract_dkg_configs_from_highest_block(&pool).len(), 4); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + + // Put three dealings in the pool and check that they get included + let dealings = (0..3) + .map(|i| ChangeAction::AddToValidated(create_dealing(i, remote_dkg_ids[0].clone()))) + .collect::>(); + dkg_pool.write().unwrap().apply(dealings); + pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 3); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + + // Now sufficient dealings are in the pool, check that payload contains early remote transcript + pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); + assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 1); + + // Check that the payload also validates + let block: Block = pool + .validated() + .block_proposal() + .get_highest() + .unwrap() + .content + .into_inner(); + let pool_reader = PoolReader::new(&pool); + + let parent = &block.parent; + let height = block.height().decrement(); + let parent = pool_reader + .get_notarized_block(parent, height) + .map(|block| block.into_inner()) + .unwrap(); + + assert!( + validate_payload( + subnet_test_id(0), + registry.as_ref(), + crypto.as_ref(), + &pool_reader, + &*dkg_pool.read().unwrap(), + parent.clone(), block.payload.as_ref(), state_manager.as_ref(), &block.context, @@ -1720,8 +1873,8 @@ mod tests { .is_ok() ); - // Advance the pool a until the next DKG, check that the early remote transcripts are not generated multiple times - // including that they are not included in the summary + // Advance the pool until the next DKG, check that the early remote transcript is not + // generated multiple times for _ in 0..100 { pool.advance_round_normal_operation(); assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index fa87060224f3..5b0807c5d387 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -482,8 +482,8 @@ mod tests { } #[test] - fn validate_dealings_payload_when_remote_transcripts_present_fails_test() { - // Data payloads with early/remote transcripts are rejected for now. + fn validate_dealings_payload_when_invalid_early_remote_transcripts_present_fails_test() { + // Data payloads with invalid early/remote transcripts are rejected. ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let registry_version = 1; let committee = [NODE_1, NODE_2, NODE_3]; diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index 5f5b6482ca8c..99b0173a5567 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -17,10 +17,7 @@ use ic_types::{ }, messages::CallbackId, }; -use std::{ - collections::{BTreeMap, BTreeSet}, - sync::Arc, -}; +use std::{collections::BTreeMap, sync::Arc}; pub(super) fn complement_state_manager_with_setup_initial_dkg_request( state_manager: Arc, @@ -90,39 +87,6 @@ pub(super) fn complement_state_manager_with_reshare_chain_key_request( } } -pub(super) fn complement_state_manager_with_remote_dkg_requests( - state_manager: Arc, - registry_version: RegistryVersion, - node_ids: Vec, - times: Option, - target_id: NiDkgTargetId, -) { - // Add the context into state_manager. - let nodes_in_target_subnet: BTreeSet<_> = node_ids.into_iter().map(node_test_id).collect(); - let mut state = ic_test_utilities_state::get_initial_state(0, 0); - state - .metadata - .subnet_call_context_manager - .push_context(SubnetCallContext::SetupInitialDKG(SetupInitialDkgContext { - request: RequestBuilder::new().build(), - nodes_in_target_subnet: nodes_in_target_subnet.clone(), - target_id, - registry_version, - time: state.time(), - })); - - let mut mock = state_manager.get_mut(); - let expectation = - mock.expect_get_state_at() - .return_const(Ok(ic_interfaces_state_manager::Labeled::new( - Height::new(0), - Arc::new(state), - ))); - if let Some(times) = times { - expectation.times(times); - } -} - /// Extract the remote dkg transcripts from the current highest validated block pub(super) fn extract_remote_dkgs_from_highest_block( pool: &TestConsensusPool, From 917d35ce90cc1821fd6075a43864b314cb24b9d7 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 27 Feb 2026 14:11:08 +0000 Subject: [PATCH 57/98] test --- rs/consensus/dkg/src/lib.rs | 188 +++++++++++++++++++++++- rs/consensus/dkg/src/payload_builder.rs | 68 ++++----- 2 files changed, 211 insertions(+), 45 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index ef5d52c20e1f..fa063615f0d8 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -45,9 +45,9 @@ pub use payload_builder::{create_payload, get_dkg_summary_from_cup_contents}; /// The maximal number of DKGs for other subnets we want to run in one interval. const MAX_REMOTE_DKGS_PER_INTERVAL: usize = 1; -/// The maximum number of early remote DKG responses we want to include in a data payload. +/// The maximum number of early remote DKG transcripts we want to include in a data payload. /// Note that responses for `SetupInitialDKG` requests contain two transcripts. -const MAX_EARLY_REMOTE_TRANSCRIPT_RESPONSES: usize = 1; +const MAX_EARLY_REMOTE_TRANSCRIPTS: usize = 2; /// The maximum number of intervals during which an initial DKG request is /// attempted. @@ -425,7 +425,11 @@ mod tests { use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ RegistryVersion, ReplicaVersion, - consensus::{Block, BlockPayload, DataPayload, HasHeight, dkg::DkgDataPayload}, + batch::ValidationContext, + consensus::{ + Block, BlockPayload, DataPayload, HasHeight, + dkg::{DkgDataPayload, DkgSummary}, + }, crypto::threshold_sig::ni_dkg::{ NiDkgId, NiDkgMasterPublicKeyId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet, }, @@ -1779,6 +1783,184 @@ mod tests { }); } + /// Tests that no early remote transcripts are created when a + /// setup_initial_dkg target has only one of its expected two configs + /// in the summary (because one transcript was already created in a + /// previous summary block). Instead, the next summary block should + /// contain both transcripts. + #[test] + fn test_no_early_transcripts_for_single_setup_initial_dkg_config() { + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + let node_ids = (1..4).map(node_test_id).collect::>(); + let dkg_interval_length = 99; + let subnet_id = subnet_test_id(0); + + let Dependencies { + mut pool, + registry, + state_manager, + dkg_pool, + crypto, + .. + } = dependencies_with_subnet_records_with_raw_state_manager( + pool_config, + subnet_id, + vec![( + 10, + SubnetRecordBuilder::from(&node_ids) + .with_dkg_interval_length(dkg_interval_length) + .build(), + )], + ); + + let target_id = NiDkgTargetId::new([0u8; 32]); + complement_state_manager_with_setup_initial_dkg_request( + state_manager.clone(), + registry.get_latest_version(), + vec![10, 11, 12, 13], + None, + Some(target_id), + ); + + // First summary: 2 remote configs (low + high) for the target + pool.advance_round_normal_operation_n(dkg_interval_length + 1); + let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&pool, target_id); + assert_eq!(remote_dkg_ids.len(), 2); + assert_eq!(extract_dkg_configs_from_highest_block(&pool).len(), 4); + + let original_summary = { + let block: Block = pool + .validated() + .block_proposal() + .get_highest() + .unwrap() + .content + .into_inner(); + block.payload.as_ref().as_summary().dkg.clone() + }; + + // Add dealings for both remote configs to the chain + for dkg_id in &remote_dkg_ids { + let dealings = (0..3) + .map(|i| ChangeAction::AddToValidated(create_dealing(i, dkg_id.clone()))) + .collect::>(); + dkg_pool.write().unwrap().apply(dealings); + pool.advance_round_normal_operation(); + } + + // Construct a modified summary with only 1 remote config. + // This simulates the scenario where one transcript was already + // created in a previous summary block, and only the remaining + // config needs to be computed. + let removed_dkg_id = remote_dkg_ids[0].clone(); + let kept_tag = remote_dkg_ids[1].dkg_tag.clone(); + let mut dummy_transcript = ic_crypto_test_utils_ni_dkg::dummy_transcript_for_tests(); + dummy_transcript.dkg_id = removed_dkg_id.clone(); + let modified_summary = DkgSummary::new( + original_summary + .configs + .values() + .filter(|c| { + c.dkg_id().target_subnet == NiDkgTargetSubnet::Local + || c.dkg_id().dkg_tag == kept_tag + }) + .cloned() + .collect(), + original_summary.current_transcripts().clone(), + original_summary.next_transcripts().clone(), + vec![( + removed_dkg_id, + ic_types::messages::CallbackId::from(0u64), + Ok(dummy_transcript), + )], + original_summary.registry_version, + original_summary.interval_length, + original_summary.next_interval_length, + original_summary.height, + BTreeMap::new(), + ); + + assert_eq!( + modified_summary + .configs + .values() + .filter(|c| matches!(c.dkg_id().target_subnet, NiDkgTargetSubnet::Remote(_))) + .count(), + 1, + ); + + let parent = pool.get_cache().finalized_block(); + let pool_reader = PoolReader::new(&pool); + let validation_context = ValidationContext { + registry_version: registry.get_latest_version(), + certified_height: Height::from(0), + time: UNIX_EPOCH, + }; + + // Even though sufficient dealings exist on chain for both configs, + // no early transcript should be created because the summary only + // has 1 of the expected 2 configs for a setup_initial_dkg target. + let early_transcripts = payload_builder::create_early_remote_transcripts( + &pool_reader, + crypto.as_ref(), + &parent, + &modified_summary, + state_manager.as_ref(), + &validation_context, + no_op_logger(), + ) + .unwrap(); + assert!( + early_transcripts.is_empty(), + "No early transcripts should be created for a single \ + setup_initial_dkg config, but got {early_transcripts:?}", + ); + + // Control: using the original summary with both configs DOES + // produce early transcripts. + let early_transcripts = payload_builder::create_early_remote_transcripts( + &pool_reader, + crypto.as_ref(), + &parent, + &original_summary, + state_manager.as_ref(), + &validation_context, + no_op_logger(), + ) + .unwrap(); + assert_eq!(early_transcripts.len(), 2); + + // If a config exists in the summary but there is no corresponding + // context in the state, no early transcript should be created. + // Set up a state manager whose context has a different target_id. + let unrelated_target_id = NiDkgTargetId::new([1u8; 32]); + let no_match_state_manager = + Arc::new(ic_test_utilities::state_manager::RefMockStateManager::default()); + complement_state_manager_with_setup_initial_dkg_request( + no_match_state_manager.clone(), + registry.get_latest_version(), + vec![10, 11, 12, 13], + None, + Some(unrelated_target_id), + ); + let early_transcripts = payload_builder::create_early_remote_transcripts( + &pool_reader, + crypto.as_ref(), + &parent, + &original_summary, + no_match_state_manager.as_ref(), + &validation_context, + no_op_logger(), + ) + .unwrap(); + assert!( + early_transcripts.is_empty(), + "No early transcripts should be created when config's target_id \ + has no corresponding context, but got {early_transcripts:?}", + ); + }); + } + #[test] fn test_early_reshare_chain_key_transcripts() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index ab4c851d7a69..0ba55edcf340 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1,5 +1,5 @@ use crate::{ - MAX_EARLY_REMOTE_TRANSCRIPT_RESPONSES, MAX_REMOTE_DKG_ATTEMPTS, MAX_REMOTE_DKGS_PER_INTERVAL, + MAX_EARLY_REMOTE_TRANSCRIPTS, MAX_REMOTE_DKG_ATTEMPTS, MAX_REMOTE_DKGS_PER_INTERVAL, REMOTE_DKG_REPEATED_FAILURE_ERROR, utils::{self, tags_iter, vetkd_key_ids_for_subnet}, }; @@ -179,6 +179,7 @@ pub(crate) fn create_early_remote_transcripts( for config in last_dkg_summary.configs.values() { let dkg_id = config.dkg_id(); if completed.contains(dkg_id) { + // Skip DKGs that have already been completed continue; } if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { @@ -189,12 +190,19 @@ pub(crate) fn create_early_remote_transcripts( let state_ref = state.get_ref(); let callback_id_map = build_target_id_callback_map(state_ref); let mut selected_transcripts = vec![]; - for (target_id, configs) in remote_configs { - // Lookup the callback id - let Some(callback_id) = callback_id_map.get(&target_id) else { + 'TARGET_ID: for (target_id, configs) in remote_configs { + // Lookup the callback id and the expected number of configs for this target_id + let Some((expected_config_num, callback_id)) = callback_id_map.get(&target_id) else { continue; }; + // Check that we have the expected number of configs for this target_id + if configs.len() != *expected_config_num { + // This may happen if we only managed to create one transcript (out of two) as part + // of the last summary block. We will handle this in the next summary block. + continue; + } + // If any of the configs has less dealings than the threshold, we skip this target_id if configs.iter().any(|config| { let dealings_count = all_dealings @@ -220,13 +228,15 @@ pub(crate) fn create_early_remote_transcripts( parent.height.increment(), err ); - continue; + continue 'TARGET_ID; } Err(err) => { // Return on transient crypto errors return Err(DkgPayloadCreationError::DkgCreateTranscriptError(err)); } }; + // Push to an intermediate vector such that we append either all transcripts for the + // target_id or none of them. transcripts.push(( config.dkg_id().clone(), *callback_id, @@ -234,38 +244,9 @@ pub(crate) fn create_early_remote_transcripts( )); } - // For initial DKG transcripts, we need a pair of values while for VetKD we need a single transcript. - // Here we do some matching, to check that we have the right number of transcripts. - let is_valid = match transcripts.len() { - 1 => { - // For VetKD, we need to check that it has a HighThresholdForKey tag - matches!(transcripts[0].0.dkg_tag, NiDkgTag::HighThresholdForKey(_)) - } - 2 => { - // If we have two transcripts for the same ID, we check that it is - // one low and one high threshold transcript. - let tags: BTreeSet<_> = transcripts - .iter() - .map(|(dkg_id, _, _)| dkg_id.dkg_tag.clone()) - .collect(); - tags.contains(&NiDkgTag::LowThreshold) && tags.contains(&NiDkgTag::HighThreshold) - } - other => { - warn!( - logger, - "Produced unexpected number of early remote NiDKG transcripts at height {}: {}", - parent.height.increment(), - other - ); - false - } - }; - - if is_valid { - selected_transcripts.append(&mut transcripts); - if selected_transcripts.len() >= MAX_EARLY_REMOTE_TRANSCRIPT_RESPONSES { - break; - } + selected_transcripts.append(&mut transcripts); + if selected_transcripts.len() >= MAX_EARLY_REMOTE_TRANSCRIPTS { + break; } } @@ -316,6 +297,7 @@ pub(super) fn create_summary_payload( // Try to create transcripts from the last round. for (dkg_id, config) in last_summary.configs.iter() { if completed.contains(dkg_id) { + // Skip DKGs that have already been completed as part of data blocks continue; } match create_transcript(crypto, config, &all_dealings, &logger) { @@ -997,19 +979,21 @@ fn eq_sans_height(dkg_id1: &NiDkgId, dkg_id2: &NiDkgId) -> bool { && dkg_id1.target_subnet == dkg_id2.target_subnet } -fn build_target_id_callback_map(state: &ReplicatedState) -> BTreeMap { +fn build_target_id_callback_map( + state: &ReplicatedState, +) -> BTreeMap { let call_contexts = &state.metadata.subnet_call_context_manager; call_contexts .setup_initial_dkg_contexts .iter() - .map(|(&callback_id, context)| (context.target_id, callback_id)) + .map(|(&callback_id, context)| (context.target_id, (2, callback_id))) .chain( call_contexts .reshare_chain_key_contexts .iter() - .map(|(&callback_id, context)| (context.target_id, callback_id)), + .map(|(&callback_id, context)| (context.target_id, (1, callback_id))), ) - .collect::>() + .collect::>() } fn add_callback_ids_to_transcript_results( @@ -1024,7 +1008,7 @@ fn add_callback_ids_to_transcript_results( .filter_map(|(id, result)| match id.target_subnet { NiDkgTargetSubnet::Local => None, NiDkgTargetSubnet::Remote(target_id) => match callback_id_map.get(&target_id) { - Some(&callback_id) => Some((id, callback_id, result)), + Some(&(_, callback_id)) => Some((id, callback_id, result)), None => { error!( log, From 960c67a7593593e20ee5a4e842f81776d46eba8b Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 27 Feb 2026 14:52:53 +0000 Subject: [PATCH 58/98] target_ids --- rs/consensus/dkg/src/payload_builder.rs | 32 ++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 0ba55edcf340..99be79333ae7 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -371,6 +371,14 @@ pub(super) fn create_summary_payload( .map(|(id, _, result)| (id.clone(), result.clone())) .collect(); + let completed_target_ids: BTreeSet = completed + .iter() + .filter_map(|id| match id.target_subnet { + NiDkgTargetSubnet::Remote(target_id) => Some(target_id), + NiDkgTargetSubnet::Local => None, + }) + .collect(); + let (mut configs, transcripts_for_remote_subnets, initial_dkg_attempts) = compute_remote_dkg_data( subnet_id, @@ -381,7 +389,7 @@ pub(super) fn create_summary_payload( transcripts_for_remote_subnets, &previous_transcripts, &reshared_transcripts, - &completed, + &completed_target_ids, &last_summary.initial_dkg_attempts, &logger, )?; @@ -462,7 +470,7 @@ fn compute_remote_dkg_data( mut new_transcripts: BTreeMap>, previous_transcripts: &BTreeMap>, reshared_transcripts: &BTreeMap, - completed: &BTreeSet, + completed_target_ids: &BTreeSet, previous_attempts: &BTreeMap, logger: &ReplicaLogger, ) -> Result< @@ -483,7 +491,7 @@ fn compute_remote_dkg_data( state.get_ref(), validation_context, reshared_transcripts, - completed, + completed_target_ids, logger, )?; @@ -780,7 +788,7 @@ fn process_subnet_call_context( state: &ReplicatedState, validation_context: &ValidationContext, reshared_transcripts: &BTreeMap, - completed: &BTreeSet, + completed_target_ids: &BTreeSet, logger: &ReplicaLogger, ) -> Result< ( @@ -797,7 +805,7 @@ fn process_subnet_call_context( registry_client, state, validation_context, - completed, + completed_target_ids, logger, )?; @@ -809,7 +817,7 @@ fn process_subnet_call_context( state, validation_context, reshared_transcripts, - completed, + completed_target_ids, )?; let dkg_configs = init_dkg_configs @@ -836,7 +844,7 @@ fn process_reshare_chain_key_contexts( state: &ReplicatedState, validation_context: &ValidationContext, reshared_transcripts: &BTreeMap, - completed: &BTreeSet, + completed_target_ids: &BTreeSet, ) -> Result< ( Vec>, @@ -860,9 +868,7 @@ fn process_reshare_chain_key_contexts( } // If the DKG has already been completed, skip this context - if completed.iter().any(|completed_dkg_id| { - completed_dkg_id.target_subnet == NiDkgTargetSubnet::Remote(context.target_id) - }) { + if completed_target_ids.contains(&context.target_id) { continue; } @@ -901,7 +907,7 @@ fn process_setup_initial_dkg_contexts( registry_client: &dyn RegistryClient, state: &ReplicatedState, validation_context: &ValidationContext, - completed: &BTreeSet, + completed_target_ids: &BTreeSet, logger: &ReplicaLogger, ) -> Result< ( @@ -925,9 +931,7 @@ fn process_setup_initial_dkg_contexts( } // If the DKG has already been completed, skip this context - if completed.iter().any(|completed_dkg_id| { - completed_dkg_id.target_subnet == NiDkgTargetSubnet::Remote(context.target_id) - }) { + if completed_target_ids.contains(&context.target_id) { continue; } From ed0dce22ad8cf1a39821c1cf1e6f6facbff3ecff Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 2 Mar 2026 10:36:12 +0000 Subject: [PATCH 59/98] include errors --- rs/consensus/dkg/src/payload_builder.rs | 52 ++++++++++++------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 99be79333ae7..eef7bd4be67a 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -77,15 +77,16 @@ pub fn create_payload( ) .map(DkgPayload::Summary) } else { - // If the height is not a start height, create a payload with new dealings. + // If the height is not a start height, create a payload with new dealings, + // and possibly early remote transcripts. create_data_payload( pool_reader, - parent, dkg_pool, - crypto, - last_dkg_summary, + parent, max_dealings_per_block, &last_summary_block, + last_dkg_summary, + crypto, state_manager, validation_context, logger, @@ -96,12 +97,12 @@ pub fn create_payload( fn create_data_payload( pool_reader: &PoolReader<'_>, - parent: &Block, dkg_pool: Arc>, - crypto: &dyn ConsensusCrypto, - last_dkg_summary: &DkgSummary, + parent: &Block, max_dealings_per_block: usize, last_summary_block: &Block, + last_dkg_summary: &DkgSummary, + crypto: &dyn ConsensusCrypto, state_manager: &dyn StateManager, validation_context: &ValidationContext, logger: ReplicaLogger, @@ -171,10 +172,10 @@ pub(crate) fn create_early_remote_transcripts( return Ok(vec![]); } - // Get all dealings that have not been used in a transcript already + // Get all dealings for DKGs that have not been completed yet let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent); - // Collect map of remote target_ids to dkg configs + // Collect map of remote target_ids to DKG configs let mut remote_configs: BTreeMap> = BTreeMap::new(); for config in last_dkg_summary.configs.values() { let dkg_id = config.dkg_id(); @@ -190,7 +191,7 @@ pub(crate) fn create_early_remote_transcripts( let state_ref = state.get_ref(); let callback_id_map = build_target_id_callback_map(state_ref); let mut selected_transcripts = vec![]; - 'TARGET_ID: for (target_id, configs) in remote_configs { + for (target_id, configs) in remote_configs { // Lookup the callback id and the expected number of configs for this target_id let Some((expected_config_num, callback_id)) = callback_id_map.get(&target_id) else { continue; @@ -214,37 +215,34 @@ pub(crate) fn create_early_remote_transcripts( } // For each config, try to build the necessary (dkg_id, callback_id, transcript) triple - let mut transcripts = vec![]; for config in configs.iter() { - // Generate the transcript. We just skip reproducible errors, they will - // be handled in the summary block, if we fail to create an early transcript - let transcript = match create_transcript(crypto, config, &all_dealings, &logger) { - Ok(transcript) => transcript, + // Generate the transcript. We need to retry transient errors, as a payload containing + // transient errors may not be verifiable by peers. + let transcript_result = match create_transcript(crypto, config, &all_dealings, &logger) + { + Ok(transcript) => Ok(transcript), + // Note that we handled the reproducible error case of not having enough dealings + // already beforehand. Err(err) if err.is_reproducible() => { - warn!( - logger, - "Failed to create early remote transcript for dkg id {:?} at height {}: {:?}", + // Including the error in the payload will cause the context to receive + // a reject response. + let error_message = format!( + "Failed to create early remote transcript for dkg id {:?} at height {}: {}", config.dkg_id(), parent.height.increment(), err ); - continue 'TARGET_ID; + warn!(logger, "{error_message}"); + Err(error_message) } Err(err) => { // Return on transient crypto errors return Err(DkgPayloadCreationError::DkgCreateTranscriptError(err)); } }; - // Push to an intermediate vector such that we append either all transcripts for the - // target_id or none of them. - transcripts.push(( - config.dkg_id().clone(), - *callback_id, - Ok::<_, String>(transcript), - )); + selected_transcripts.push((config.dkg_id().clone(), *callback_id, transcript_result)); } - selected_transcripts.append(&mut transcripts); if selected_transcripts.len() >= MAX_EARLY_REMOTE_TRANSCRIPTS { break; } From cf2c455e32fd256cb8d57ff8d2fadb3ca3f2011d Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 2 Mar 2026 11:11:27 +0000 Subject: [PATCH 60/98] include errors --- rs/consensus/dkg/src/payload_builder.rs | 84 ++++++++++++------------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index eef7bd4be67a..0ba55edcf340 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -77,16 +77,15 @@ pub fn create_payload( ) .map(DkgPayload::Summary) } else { - // If the height is not a start height, create a payload with new dealings, - // and possibly early remote transcripts. + // If the height is not a start height, create a payload with new dealings. create_data_payload( pool_reader, - dkg_pool, parent, + dkg_pool, + crypto, + last_dkg_summary, max_dealings_per_block, &last_summary_block, - last_dkg_summary, - crypto, state_manager, validation_context, logger, @@ -97,12 +96,12 @@ pub fn create_payload( fn create_data_payload( pool_reader: &PoolReader<'_>, - dkg_pool: Arc>, parent: &Block, + dkg_pool: Arc>, + crypto: &dyn ConsensusCrypto, + last_dkg_summary: &DkgSummary, max_dealings_per_block: usize, last_summary_block: &Block, - last_dkg_summary: &DkgSummary, - crypto: &dyn ConsensusCrypto, state_manager: &dyn StateManager, validation_context: &ValidationContext, logger: ReplicaLogger, @@ -172,10 +171,10 @@ pub(crate) fn create_early_remote_transcripts( return Ok(vec![]); } - // Get all dealings for DKGs that have not been completed yet + // Get all dealings that have not been used in a transcript already let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent); - // Collect map of remote target_ids to DKG configs + // Collect map of remote target_ids to dkg configs let mut remote_configs: BTreeMap> = BTreeMap::new(); for config in last_dkg_summary.configs.values() { let dkg_id = config.dkg_id(); @@ -191,7 +190,7 @@ pub(crate) fn create_early_remote_transcripts( let state_ref = state.get_ref(); let callback_id_map = build_target_id_callback_map(state_ref); let mut selected_transcripts = vec![]; - for (target_id, configs) in remote_configs { + 'TARGET_ID: for (target_id, configs) in remote_configs { // Lookup the callback id and the expected number of configs for this target_id let Some((expected_config_num, callback_id)) = callback_id_map.get(&target_id) else { continue; @@ -215,34 +214,37 @@ pub(crate) fn create_early_remote_transcripts( } // For each config, try to build the necessary (dkg_id, callback_id, transcript) triple + let mut transcripts = vec![]; for config in configs.iter() { - // Generate the transcript. We need to retry transient errors, as a payload containing - // transient errors may not be verifiable by peers. - let transcript_result = match create_transcript(crypto, config, &all_dealings, &logger) - { - Ok(transcript) => Ok(transcript), - // Note that we handled the reproducible error case of not having enough dealings - // already beforehand. + // Generate the transcript. We just skip reproducible errors, they will + // be handled in the summary block, if we fail to create an early transcript + let transcript = match create_transcript(crypto, config, &all_dealings, &logger) { + Ok(transcript) => transcript, Err(err) if err.is_reproducible() => { - // Including the error in the payload will cause the context to receive - // a reject response. - let error_message = format!( - "Failed to create early remote transcript for dkg id {:?} at height {}: {}", + warn!( + logger, + "Failed to create early remote transcript for dkg id {:?} at height {}: {:?}", config.dkg_id(), parent.height.increment(), err ); - warn!(logger, "{error_message}"); - Err(error_message) + continue 'TARGET_ID; } Err(err) => { // Return on transient crypto errors return Err(DkgPayloadCreationError::DkgCreateTranscriptError(err)); } }; - selected_transcripts.push((config.dkg_id().clone(), *callback_id, transcript_result)); + // Push to an intermediate vector such that we append either all transcripts for the + // target_id or none of them. + transcripts.push(( + config.dkg_id().clone(), + *callback_id, + Ok::<_, String>(transcript), + )); } + selected_transcripts.append(&mut transcripts); if selected_transcripts.len() >= MAX_EARLY_REMOTE_TRANSCRIPTS { break; } @@ -369,14 +371,6 @@ pub(super) fn create_summary_payload( .map(|(id, _, result)| (id.clone(), result.clone())) .collect(); - let completed_target_ids: BTreeSet = completed - .iter() - .filter_map(|id| match id.target_subnet { - NiDkgTargetSubnet::Remote(target_id) => Some(target_id), - NiDkgTargetSubnet::Local => None, - }) - .collect(); - let (mut configs, transcripts_for_remote_subnets, initial_dkg_attempts) = compute_remote_dkg_data( subnet_id, @@ -387,7 +381,7 @@ pub(super) fn create_summary_payload( transcripts_for_remote_subnets, &previous_transcripts, &reshared_transcripts, - &completed_target_ids, + &completed, &last_summary.initial_dkg_attempts, &logger, )?; @@ -468,7 +462,7 @@ fn compute_remote_dkg_data( mut new_transcripts: BTreeMap>, previous_transcripts: &BTreeMap>, reshared_transcripts: &BTreeMap, - completed_target_ids: &BTreeSet, + completed: &BTreeSet, previous_attempts: &BTreeMap, logger: &ReplicaLogger, ) -> Result< @@ -489,7 +483,7 @@ fn compute_remote_dkg_data( state.get_ref(), validation_context, reshared_transcripts, - completed_target_ids, + completed, logger, )?; @@ -786,7 +780,7 @@ fn process_subnet_call_context( state: &ReplicatedState, validation_context: &ValidationContext, reshared_transcripts: &BTreeMap, - completed_target_ids: &BTreeSet, + completed: &BTreeSet, logger: &ReplicaLogger, ) -> Result< ( @@ -803,7 +797,7 @@ fn process_subnet_call_context( registry_client, state, validation_context, - completed_target_ids, + completed, logger, )?; @@ -815,7 +809,7 @@ fn process_subnet_call_context( state, validation_context, reshared_transcripts, - completed_target_ids, + completed, )?; let dkg_configs = init_dkg_configs @@ -842,7 +836,7 @@ fn process_reshare_chain_key_contexts( state: &ReplicatedState, validation_context: &ValidationContext, reshared_transcripts: &BTreeMap, - completed_target_ids: &BTreeSet, + completed: &BTreeSet, ) -> Result< ( Vec>, @@ -866,7 +860,9 @@ fn process_reshare_chain_key_contexts( } // If the DKG has already been completed, skip this context - if completed_target_ids.contains(&context.target_id) { + if completed.iter().any(|completed_dkg_id| { + completed_dkg_id.target_subnet == NiDkgTargetSubnet::Remote(context.target_id) + }) { continue; } @@ -905,7 +901,7 @@ fn process_setup_initial_dkg_contexts( registry_client: &dyn RegistryClient, state: &ReplicatedState, validation_context: &ValidationContext, - completed_target_ids: &BTreeSet, + completed: &BTreeSet, logger: &ReplicaLogger, ) -> Result< ( @@ -929,7 +925,9 @@ fn process_setup_initial_dkg_contexts( } // If the DKG has already been completed, skip this context - if completed_target_ids.contains(&context.target_id) { + if completed.iter().any(|completed_dkg_id| { + completed_dkg_id.target_subnet == NiDkgTargetSubnet::Remote(context.target_id) + }) { continue; } From 3b051fff6fae8296602dfcecde798dbc35ec15ac Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 3 Mar 2026 07:28:36 +0000 Subject: [PATCH 61/98] Revert "include errors" This reverts commit cf2c455e32fd256cb8d57ff8d2fadb3ca3f2011d. --- rs/consensus/dkg/src/payload_builder.rs | 84 +++++++++++++------------ 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 0ba55edcf340..eef7bd4be67a 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -77,15 +77,16 @@ pub fn create_payload( ) .map(DkgPayload::Summary) } else { - // If the height is not a start height, create a payload with new dealings. + // If the height is not a start height, create a payload with new dealings, + // and possibly early remote transcripts. create_data_payload( pool_reader, - parent, dkg_pool, - crypto, - last_dkg_summary, + parent, max_dealings_per_block, &last_summary_block, + last_dkg_summary, + crypto, state_manager, validation_context, logger, @@ -96,12 +97,12 @@ pub fn create_payload( fn create_data_payload( pool_reader: &PoolReader<'_>, - parent: &Block, dkg_pool: Arc>, - crypto: &dyn ConsensusCrypto, - last_dkg_summary: &DkgSummary, + parent: &Block, max_dealings_per_block: usize, last_summary_block: &Block, + last_dkg_summary: &DkgSummary, + crypto: &dyn ConsensusCrypto, state_manager: &dyn StateManager, validation_context: &ValidationContext, logger: ReplicaLogger, @@ -171,10 +172,10 @@ pub(crate) fn create_early_remote_transcripts( return Ok(vec![]); } - // Get all dealings that have not been used in a transcript already + // Get all dealings for DKGs that have not been completed yet let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent); - // Collect map of remote target_ids to dkg configs + // Collect map of remote target_ids to DKG configs let mut remote_configs: BTreeMap> = BTreeMap::new(); for config in last_dkg_summary.configs.values() { let dkg_id = config.dkg_id(); @@ -190,7 +191,7 @@ pub(crate) fn create_early_remote_transcripts( let state_ref = state.get_ref(); let callback_id_map = build_target_id_callback_map(state_ref); let mut selected_transcripts = vec![]; - 'TARGET_ID: for (target_id, configs) in remote_configs { + for (target_id, configs) in remote_configs { // Lookup the callback id and the expected number of configs for this target_id let Some((expected_config_num, callback_id)) = callback_id_map.get(&target_id) else { continue; @@ -214,37 +215,34 @@ pub(crate) fn create_early_remote_transcripts( } // For each config, try to build the necessary (dkg_id, callback_id, transcript) triple - let mut transcripts = vec![]; for config in configs.iter() { - // Generate the transcript. We just skip reproducible errors, they will - // be handled in the summary block, if we fail to create an early transcript - let transcript = match create_transcript(crypto, config, &all_dealings, &logger) { - Ok(transcript) => transcript, + // Generate the transcript. We need to retry transient errors, as a payload containing + // transient errors may not be verifiable by peers. + let transcript_result = match create_transcript(crypto, config, &all_dealings, &logger) + { + Ok(transcript) => Ok(transcript), + // Note that we handled the reproducible error case of not having enough dealings + // already beforehand. Err(err) if err.is_reproducible() => { - warn!( - logger, - "Failed to create early remote transcript for dkg id {:?} at height {}: {:?}", + // Including the error in the payload will cause the context to receive + // a reject response. + let error_message = format!( + "Failed to create early remote transcript for dkg id {:?} at height {}: {}", config.dkg_id(), parent.height.increment(), err ); - continue 'TARGET_ID; + warn!(logger, "{error_message}"); + Err(error_message) } Err(err) => { // Return on transient crypto errors return Err(DkgPayloadCreationError::DkgCreateTranscriptError(err)); } }; - // Push to an intermediate vector such that we append either all transcripts for the - // target_id or none of them. - transcripts.push(( - config.dkg_id().clone(), - *callback_id, - Ok::<_, String>(transcript), - )); + selected_transcripts.push((config.dkg_id().clone(), *callback_id, transcript_result)); } - selected_transcripts.append(&mut transcripts); if selected_transcripts.len() >= MAX_EARLY_REMOTE_TRANSCRIPTS { break; } @@ -371,6 +369,14 @@ pub(super) fn create_summary_payload( .map(|(id, _, result)| (id.clone(), result.clone())) .collect(); + let completed_target_ids: BTreeSet = completed + .iter() + .filter_map(|id| match id.target_subnet { + NiDkgTargetSubnet::Remote(target_id) => Some(target_id), + NiDkgTargetSubnet::Local => None, + }) + .collect(); + let (mut configs, transcripts_for_remote_subnets, initial_dkg_attempts) = compute_remote_dkg_data( subnet_id, @@ -381,7 +387,7 @@ pub(super) fn create_summary_payload( transcripts_for_remote_subnets, &previous_transcripts, &reshared_transcripts, - &completed, + &completed_target_ids, &last_summary.initial_dkg_attempts, &logger, )?; @@ -462,7 +468,7 @@ fn compute_remote_dkg_data( mut new_transcripts: BTreeMap>, previous_transcripts: &BTreeMap>, reshared_transcripts: &BTreeMap, - completed: &BTreeSet, + completed_target_ids: &BTreeSet, previous_attempts: &BTreeMap, logger: &ReplicaLogger, ) -> Result< @@ -483,7 +489,7 @@ fn compute_remote_dkg_data( state.get_ref(), validation_context, reshared_transcripts, - completed, + completed_target_ids, logger, )?; @@ -780,7 +786,7 @@ fn process_subnet_call_context( state: &ReplicatedState, validation_context: &ValidationContext, reshared_transcripts: &BTreeMap, - completed: &BTreeSet, + completed_target_ids: &BTreeSet, logger: &ReplicaLogger, ) -> Result< ( @@ -797,7 +803,7 @@ fn process_subnet_call_context( registry_client, state, validation_context, - completed, + completed_target_ids, logger, )?; @@ -809,7 +815,7 @@ fn process_subnet_call_context( state, validation_context, reshared_transcripts, - completed, + completed_target_ids, )?; let dkg_configs = init_dkg_configs @@ -836,7 +842,7 @@ fn process_reshare_chain_key_contexts( state: &ReplicatedState, validation_context: &ValidationContext, reshared_transcripts: &BTreeMap, - completed: &BTreeSet, + completed_target_ids: &BTreeSet, ) -> Result< ( Vec>, @@ -860,9 +866,7 @@ fn process_reshare_chain_key_contexts( } // If the DKG has already been completed, skip this context - if completed.iter().any(|completed_dkg_id| { - completed_dkg_id.target_subnet == NiDkgTargetSubnet::Remote(context.target_id) - }) { + if completed_target_ids.contains(&context.target_id) { continue; } @@ -901,7 +905,7 @@ fn process_setup_initial_dkg_contexts( registry_client: &dyn RegistryClient, state: &ReplicatedState, validation_context: &ValidationContext, - completed: &BTreeSet, + completed_target_ids: &BTreeSet, logger: &ReplicaLogger, ) -> Result< ( @@ -925,9 +929,7 @@ fn process_setup_initial_dkg_contexts( } // If the DKG has already been completed, skip this context - if completed.iter().any(|completed_dkg_id| { - completed_dkg_id.target_subnet == NiDkgTargetSubnet::Remote(context.target_id) - }) { + if completed_target_ids.contains(&context.target_id) { continue; } From 7a81bf38bd01fcd716dd27ac34dabb7210a2289c Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 3 Mar 2026 09:34:50 +0000 Subject: [PATCH 62/98] assumption --- rs/consensus/dkg/src/payload_builder.rs | 68 ++++++++++++++++++++----- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index eef7bd4be67a..a4f40d9de532 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -190,6 +190,9 @@ pub(crate) fn create_early_remote_transcripts( let state_ref = state.get_ref(); let callback_id_map = build_target_id_callback_map(state_ref); + + // Try to create transcripts for all configs of each target_id. Note that we either include + // all transcript results for a target_id or none of them. let mut selected_transcripts = vec![]; for (target_id, configs) in remote_configs { // Lookup the callback id and the expected number of configs for this target_id @@ -200,7 +203,7 @@ pub(crate) fn create_early_remote_transcripts( // Check that we have the expected number of configs for this target_id if configs.len() != *expected_config_num { // This may happen if we only managed to create one transcript (out of two) as part - // of the last summary block. We will handle this in the next summary block. + // of the last summary block. We will handle this in the next summary block instead. continue; } @@ -214,7 +217,7 @@ pub(crate) fn create_early_remote_transcripts( continue; } - // For each config, try to build the necessary (dkg_id, callback_id, transcript) triple + // For each config, try to build the necessary (dkg_id, callback_id, transcript_result) triple for config in configs.iter() { // Generate the transcript. We need to retry transient errors, as a payload containing // transient errors may not be verifiable by peers. @@ -289,12 +292,12 @@ pub(super) fn create_summary_payload( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { - let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent); + let (all_dealings, completed_dkgs) = utils::get_dkg_dealings(pool_reader, parent); let mut transcripts_for_remote_subnets = BTreeMap::new(); let mut next_transcripts = BTreeMap::new(); // Try to create transcripts from the last round. for (dkg_id, config) in last_summary.configs.iter() { - if completed.contains(dkg_id) { + if completed_dkgs.contains(dkg_id) { // Skip DKGs that have already been completed as part of data blocks continue; } @@ -369,13 +372,8 @@ pub(super) fn create_summary_payload( .map(|(id, _, result)| (id.clone(), result.clone())) .collect(); - let completed_target_ids: BTreeSet = completed - .iter() - .filter_map(|id| match id.target_subnet { - NiDkgTargetSubnet::Remote(target_id) => Some(target_id), - NiDkgTargetSubnet::Local => None, - }) - .collect(); + let completed_target_ids = + get_completed_target_ids(last_summary.configs.keys(), &completed_dkgs); let (mut configs, transcripts_for_remote_subnets, initial_dkg_attempts) = compute_remote_dkg_data( @@ -972,6 +970,28 @@ fn get_node_list( .collect()) } +/// Returns the set of remote target IDs for which all configured DKGs have +/// been completed. +fn get_completed_target_ids<'a>( + config_ids: impl Iterator, + completed: &BTreeSet, +) -> BTreeSet { + let mut remote_dkgs_by_target: BTreeMap> = BTreeMap::new(); + for dkg_id in config_ids { + if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { + remote_dkgs_by_target + .entry(target_id) + .or_default() + .push(dkg_id); + } + } + remote_dkgs_by_target + .into_iter() + .filter(|(_, dkg_ids)| dkg_ids.iter().all(|id| completed.contains(id))) + .map(|(target_id, _)| target_id) + .collect() +} + /// Compares two DKG ids without considering the start block heights. This /// function is only used for DKGs for other subnets, as the start block height /// is not used to differentiate two DKGs for the same subnet. @@ -1182,7 +1202,7 @@ mod tests { use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ RegistryVersion, - crypto::threshold_sig::ni_dkg::{NiDkgId, NiDkgTag, NiDkgTargetSubnet}, + crypto::threshold_sig::ni_dkg::{NiDkgId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet}, time::UNIX_EPOCH, }; use std::collections::BTreeSet; @@ -2033,4 +2053,28 @@ mod tests { } }); } + + #[test] + fn test_get_completed_target_ids() { + let targets: Vec<_> = (1..=3).map(|i| NiDkgTargetId::new([i; 32])).collect(); + let tags = [NiDkgTag::LowThreshold, NiDkgTag::HighThreshold]; + + let config_ids: Vec<_> = targets + .iter() + .flat_map(|t| { + tags.iter().map(|tag| NiDkgId { + start_block_height: Height::from(1), + dealer_subnet: subnet_test_id(1), + dkg_tag: tag.clone(), + target_subnet: NiDkgTargetSubnet::Remote(*t), + }) + }) + .collect(); + + // target 0 is fully completed, target 1 only has low completed, target 2 is not completed + let completed: BTreeSet<_> = config_ids[..3].iter().cloned().collect(); + + let result = get_completed_target_ids(config_ids.iter(), &completed); + assert_eq!(result, BTreeSet::from([targets[0]])); + } } From 0c410fec1601ee378229593f60cb30f302657309 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 3 Mar 2026 10:09:36 +0000 Subject: [PATCH 63/98] ignore completed test --- rs/consensus/dkg/src/payload_builder.rs | 122 +++++++++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index a4f40d9de532..3d7d12e8107d 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1195,11 +1195,17 @@ mod tests { }; use ic_crypto_test_utils_ni_dkg::dummy_transcript_for_tests_with_params; use ic_logger::replica_logger::no_op_logger; - use ic_management_canister_types_private::{VetKdCurve, VetKdKeyId}; + use ic_management_canister_types_private::{MasterPublicKeyId, VetKdCurve, VetKdKeyId}; use ic_registry_client_helpers::subnet::SubnetRegistry; + use ic_replicated_state::metadata_state::subnet_call_context_manager::{ + ReshareChainKeyContext, SetupInitialDkgContext, SubnetCallContext, + }; use ic_test_utilities_logger::with_test_replica_logger; use ic_test_utilities_registry::{SubnetRecordBuilder, add_subnet_record}; - use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; + use ic_test_utilities_types::{ + ids::{node_test_id, subnet_test_id}, + messages::RequestBuilder, + }; use ic_types::{ RegistryVersion, crypto::threshold_sig::ni_dkg::{NiDkgId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet}, @@ -2077,4 +2083,116 @@ mod tests { let result = get_completed_target_ids(config_ids.iter(), &completed); assert_eq!(result, BTreeSet::from([targets[0]])); } + + #[test] + fn test_process_subnet_call_context_ignores_completed_targets() { + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + let node_ids = vec![node_test_id(0), node_test_id(1)]; + let subnet_id = subnet_test_id(0); + let Dependencies { registry, .. } = + dependencies_with_subnet_records_with_raw_state_manager( + pool_config, + subnet_id, + vec![( + 10, + SubnetRecordBuilder::from(&node_ids) + .with_dkg_interval_length(99) + .build(), + )], + ); + + let key_id = VetKdKeyId { + curve: VetKdCurve::Bls12_381_G2, + name: String::from("some_vetkey"), + }; + let ni_dkg_key_id = NiDkgMasterPublicKeyId::VetKd(key_id.clone()); + let tag = NiDkgTag::HighThresholdForKey(ni_dkg_key_id); + + let registry_version = registry.get_latest_version(); + let completed_init_dkg_target = NiDkgTargetId::new([1u8; 32]); + let pending_init_dkg_target = NiDkgTargetId::new([2u8; 32]); + let completed_reshare_target = NiDkgTargetId::new([3u8; 32]); + let pending_reshare_target = NiDkgTargetId::new([4u8; 32]); + + let mut state = ic_test_utilities_state::get_initial_state(0, 0); + let target_nodes: BTreeSet<_> = + vec![10, 11, 12].into_iter().map(node_test_id).collect(); + + for target_id in [completed_init_dkg_target, pending_init_dkg_target] { + state.metadata.subnet_call_context_manager.push_context( + SubnetCallContext::SetupInitialDKG(SetupInitialDkgContext { + request: RequestBuilder::new().build(), + nodes_in_target_subnet: target_nodes.clone(), + target_id, + registry_version, + time: state.time(), + }), + ); + } + + for target_id in [completed_reshare_target, pending_reshare_target] { + state.metadata.subnet_call_context_manager.push_context( + SubnetCallContext::ReshareChainKey(ReshareChainKeyContext { + request: RequestBuilder::new().build(), + key_id: MasterPublicKeyId::VetKd(key_id.clone()), + nodes: target_nodes.clone(), + registry_version, + time: state.time(), + target_id, + }), + ); + } + + let reshared_transcripts = BTreeMap::from([( + tag.clone(), + dummy_transcript_for_tests_with_params( + node_ids.clone(), + tag.clone(), + tag.threshold_for_subnet_of_size(node_ids.len()) as u32, + 10, + ), + )]); + + let validation_context = ValidationContext { + registry_version, + certified_height: Height::from(0), + time: UNIX_EPOCH, + }; + + let completed_target_ids = + BTreeSet::from([completed_init_dkg_target, completed_reshare_target]); + + let (configs, errors, valid_target_ids) = process_subnet_call_context( + subnet_id, + Height::from(0), + registry.as_ref(), + &state, + &validation_context, + &reshared_transcripts, + &completed_target_ids, + &no_op_logger(), + ) + .unwrap(); + + // One setup_initial_dkg group (low + high) and one reshare_chain_key group + assert_eq!(configs.len(), 2); + assert_eq!(configs[0].len(), 2); + for config in &configs[0] { + assert_eq!( + config.dkg_id().target_subnet, + NiDkgTargetSubnet::Remote(pending_init_dkg_target) + ); + } + assert_eq!(configs[1].len(), 1); + assert_eq!( + configs[1][0].dkg_id().target_subnet, + NiDkgTargetSubnet::Remote(pending_reshare_target) + ); + assert!(errors.is_empty()); + assert_eq!( + valid_target_ids, + vec![pending_init_dkg_target, pending_reshare_target] + ); + }); + } } From b688aea31cdfbca7eac177dfa1208671dce2f92b Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 3 Mar 2026 13:06:11 +0000 Subject: [PATCH 64/98] test --- rs/consensus/dkg/BUILD.bazel | 2 + rs/consensus/dkg/Cargo.toml | 2 + rs/consensus/dkg/src/lib.rs | 523 +++++++++++++++++++---------------- 3 files changed, 289 insertions(+), 238 deletions(-) diff --git a/rs/consensus/dkg/BUILD.bazel b/rs/consensus/dkg/BUILD.bazel index 4b9dcbe2f510..f270aed44004 100644 --- a/rs/consensus/dkg/BUILD.bazel +++ b/rs/consensus/dkg/BUILD.bazel @@ -23,10 +23,12 @@ DEPENDENCIES = [ DEV_DEPENDENCIES = [ # Keep sorted. "//rs/artifact_pool", + "//rs/config", "//rs/consensus/mocks", "//rs/crypto/temp_crypto", "//rs/crypto/test_utils/crypto_returning_ok", "//rs/crypto/test_utils/ni-dkg", + "//rs/interfaces/mocks", "//rs/interfaces/registry/mocks", "//rs/registry/keys", "//rs/registry/subnet_features", diff --git a/rs/consensus/dkg/Cargo.toml b/rs/consensus/dkg/Cargo.toml index 675332fb83be..3e2bb75533c2 100644 --- a/rs/consensus/dkg/Cargo.toml +++ b/rs/consensus/dkg/Cargo.toml @@ -25,6 +25,8 @@ rayon = { workspace = true } [dev-dependencies] +ic-config = { path = "../../config" } +ic-interfaces-mocks = { path = "../../interfaces/mocks" } ic-interfaces-registry-mocks = { path = "../../interfaces/registry/mocks" } ic-test-artifact-pool = { path = "../../test_utilities/artifact_pool" } prost = { workspace = true } diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index fa063615f0d8..e7238035a12e 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -414,12 +414,14 @@ mod tests { consensus_pool::ConsensusPool, p2p::consensus::{MutablePool, UnvalidatedArtifact}, }; + use ic_interfaces_mocks::crypto::MockCrypto; use ic_interfaces_registry::RegistryClient; use ic_logger::no_op_logger; use ic_management_canister_types_private::{MasterPublicKeyId, VetKdCurve, VetKdKeyId}; use ic_metrics::MetricsRegistry; use ic_registry_subnet_features::{ChainKeyConfig, KeyConfig}; use ic_test_artifact_pool::consensus_pool::TestConsensusPool; + use ic_test_utilities_consensus::fake::{FakeContentSigner, FromParent}; use ic_test_utilities_logger::with_test_replica_logger; use ic_test_utilities_registry::{SubnetRecordBuilder, add_subnet_record}; use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; @@ -427,11 +429,16 @@ mod tests { RegistryVersion, ReplicaVersion, batch::ValidationContext, consensus::{ - Block, BlockPayload, DataPayload, HasHeight, + Block, BlockPayload, BlockProposal, DataPayload, HasHeight, Payload, dkg::{DkgDataPayload, DkgSummary}, }, - crypto::threshold_sig::ni_dkg::{ - NiDkgId, NiDkgMasterPublicKeyId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet, + crypto::{ + AlgorithmId, + error::MalformedPublicKeyError, + threshold_sig::ni_dkg::{ + NiDkgId, NiDkgMasterPublicKeyId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet, + errors::create_transcript_error::DkgCreateTranscriptError, + }, }, time::UNIX_EPOCH, }; @@ -1611,79 +1618,141 @@ mod tests { }); } - #[test] - fn test_early_setup_initial_dkg_transcripts() { - ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { - let node_ids = (1..4).map(node_test_id).collect::>(); - let dkg_interval_length = 99; - let subnet_id = subnet_test_id(0); + const EARLY_DKG_INTERVAL: u64 = 99; - let Dependencies { - mut pool, - registry, - state_manager, - dkg_pool, - crypto, - .. - } = dependencies_with_subnet_records_with_raw_state_manager( - pool_config, - subnet_id, - vec![( - 10, - SubnetRecordBuilder::from(&node_ids) - .with_dkg_interval_length(dkg_interval_length) - .build(), - )], - ); + /// Common setup for early transcript tests using `setup_initial_dkg`. + /// Advances to the first summary block and returns the deps, target id, + /// and the two remote DKG ids (low + high threshold). + fn setup_initial_dkg_test( + pool_config: ic_config::artifact_pool::ArtifactPoolConfig, + ) -> (Dependencies, NiDkgTargetId, Vec) { + let node_ids = (1..4).map(node_test_id).collect::>(); - let target_id = NiDkgTargetId::new([0u8; 32]); - complement_state_manager_with_setup_initial_dkg_request( - state_manager.clone(), - registry.get_latest_version(), - vec![10, 11, 12, 13], - None, - Some(target_id), - ); + let mut deps = dependencies_with_subnet_records_with_raw_state_manager( + pool_config, + subnet_test_id(0), + vec![( + 10, + SubnetRecordBuilder::from(&node_ids) + .with_dkg_interval_length(EARLY_DKG_INTERVAL) + .build(), + )], + ); + + let target_id = NiDkgTargetId::new([0u8; 32]); + complement_state_manager_with_setup_initial_dkg_request( + deps.state_manager.clone(), + deps.registry.get_latest_version(), + vec![10, 11, 12, 13], + None, + Some(target_id), + ); + + deps.pool + .advance_round_normal_operation_n(EARLY_DKG_INTERVAL + 1); + + // Verify that the initial summary block contains the two remote configs. + assert_eq!(extract_dkg_configs_from_highest_block(&deps.pool).len(), 4); + assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); + let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&deps.pool, target_id); + assert_eq!(remote_dkg_ids.len(), 2); + + (deps, target_id, remote_dkg_ids) + } - // Verify that the next summary block contains the configs and no transcripts. - // This also extracts the DKG ids - pool.advance_round_normal_operation_n(dkg_interval_length + 1); - let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&pool, target_id); - assert_eq!(remote_dkg_ids.len(), 2); - assert_eq!(extract_dkg_configs_from_highest_block(&pool).len(), 4); - assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + /// Add 3 dealings per config to the DKG pool, advancing the consensus + /// pool one round after each config. + fn add_dealings_for_configs(deps: &mut Dependencies, dkg_ids: &[NiDkgId]) { + for dkg_id in dkg_ids { + let dealings = (0..3) + .map(|i| ChangeAction::AddToValidated(create_dealing(i, dkg_id.clone()))) + .collect::>(); + deps.dkg_pool.write().unwrap().apply(dealings); + deps.pool.advance_round_normal_operation(); + } + } + + /// Assert that the highest block's payload passes DKG validation. + fn assert_highest_block_validates(deps: &Dependencies) { + let block: Block = deps + .pool + .validated() + .block_proposal() + .get_highest() + .unwrap() + .content + .into_inner(); + let pool_reader = PoolReader::new(&deps.pool); + let height = block.height().decrement(); + let parent = pool_reader + .get_notarized_block(&block.parent, height) + .map(|block| block.into_inner()) + .unwrap(); + + assert!( + validate_payload( + subnet_test_id(0), + deps.registry.as_ref(), + deps.crypto.as_ref(), + &pool_reader, + &*deps.dkg_pool.read().unwrap(), + parent, + block.payload.as_ref(), + deps.state_manager.as_ref(), + &block.context, + &MetricsRegistry::new().int_counter_vec( + "consensus_dkg_validator", + "DKG validator counter", + &["type"], + ), + &no_op_logger(), + ) + .is_ok() + ); + } - // Put three dealings in the pool and check that they get included - // Additionally check that there are no remote transcripts + /// Advance through a full DKG interval and verify that no early remote + /// transcripts or dealings appear in any block. + fn assert_no_early_transcript_duplicates(pool: &mut TestConsensusPool, interval_length: u64) { + for _ in 0..interval_length + 1 { + pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(pool).len(), 0); + assert_eq!(extract_remote_dkgs_from_highest_block(pool).len(), 0); + } + } + + #[test] + fn test_early_setup_initial_dkg_transcripts() { + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + let (mut deps, target_id, remote_dkg_ids) = setup_initial_dkg_test(pool_config); + + // Add dealings for first config only; not enough for both transcripts let dealings = (0..3) .map(|i| ChangeAction::AddToValidated(create_dealing(i, remote_dkg_ids[0].clone()))) .collect::>(); - dkg_pool.write().unwrap().apply(dealings); - pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 3); - assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + deps.dkg_pool.write().unwrap().apply(dealings); + deps.pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 3); + assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); - // For the next round, we put nothing into the pool - // We will try to build a remote transcript, this will fail, however, - // since we don't have enough dealings to build both transcripts (one high, one low) - pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); - assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + // No new dealings; building remote transcripts fails (need both high and low) + deps.pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 0); + assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); - // Now we put the other dealings into the pool - // The payload builder will include the dealings + // Add dealings for second config let dealings = (0..3) .map(|i| ChangeAction::AddToValidated(create_dealing(i, remote_dkg_ids[1].clone()))) .collect::>(); - dkg_pool.write().unwrap().apply(dealings); - pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 3); - assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); - - // Now sufficient dealings are on the block chain, check that payload contains early remote transcripts - pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); - let remote_dkgs = extract_remote_dkgs_from_highest_block(&pool); + deps.dkg_pool.write().unwrap().apply(dealings); + deps.pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 3); + assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); + + // Now sufficient dealings are on chain; early remote transcripts should appear + deps.pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 0); + let remote_dkgs = extract_remote_dkgs_from_highest_block(&deps.pool); assert_eq!(remote_dkgs.len(), 2); for (dkg_id, _, result) in &remote_dkgs { assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); @@ -1692,54 +1761,32 @@ mod tests { assert!( remote_dkgs .iter() - .any(|(id, _, __)| id.dkg_tag == NiDkgTag::HighThreshold) + .any(|(id, _, _)| id.dkg_tag == NiDkgTag::HighThreshold) ); assert!( remote_dkgs .iter() - .any(|(id, _, __)| id.dkg_tag == NiDkgTag::LowThreshold) + .any(|(id, _, _)| id.dkg_tag == NiDkgTag::LowThreshold) ); - // Check that the payload also validates - let block: Block = pool + assert_highest_block_validates(&deps); + + // Also validate with empty transcripts_for_remote_subnets (early + // transcripts are only an optimization, so validation must still pass). + let block: Block = deps + .pool .validated() .block_proposal() .get_highest() .unwrap() .content .into_inner(); - let pool_reader = PoolReader::new(&pool); - - let parent = &block.parent; + let pool_reader = PoolReader::new(&deps.pool); let height = block.height().decrement(); let parent = pool_reader - .get_notarized_block(parent, height) + .get_notarized_block(&block.parent, height) .map(|block| block.into_inner()) .unwrap(); - - assert!( - validate_payload( - subnet_test_id(0), - registry.as_ref(), - crypto.as_ref(), - &pool_reader, - &*dkg_pool.read().unwrap(), - parent.clone(), - block.payload.as_ref(), - state_manager.as_ref(), - &block.context, - &MetricsRegistry::new().int_counter_vec( - "consensus_dkg_validator", - "DKG validator counter", - &["type"], - ), - &no_op_logger(), - ) - .is_ok() - ); - - // Validate the same payload with empty transcripts_for_remote_subnets. - // Since early transcripts are only an optimization,validation should still succeed. let payload_without_early_remote = match block.payload.as_ref() { BlockPayload::Data(data) => { let dkg_without_remote = @@ -1755,13 +1802,13 @@ mod tests { assert!( validate_payload( subnet_test_id(0), - registry.as_ref(), - crypto.as_ref(), + deps.registry.as_ref(), + deps.crypto.as_ref(), &pool_reader, - &*dkg_pool.read().unwrap(), + &*deps.dkg_pool.read().unwrap(), parent, &payload_without_early_remote, - state_manager.as_ref(), + deps.state_manager.as_ref(), &block.context, &MetricsRegistry::new().int_counter_vec( "consensus_dkg_validator", @@ -1773,13 +1820,8 @@ mod tests { .is_ok() ); - // Advance the pool a until the next DKG, check that the early remote transcripts are not - // generated multiple times, and in particular that they are not included in the summary. - for _ in 0..dkg_interval_length + 1 { - pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); - assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); - } + // Verify that no more transcripts are created in later blocks. + assert_no_early_transcript_duplicates(&mut deps.pool, EARLY_DKG_INTERVAL); }); } @@ -1791,45 +1833,11 @@ mod tests { #[test] fn test_no_early_transcripts_for_single_setup_initial_dkg_config() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { - let node_ids = (1..4).map(node_test_id).collect::>(); - let dkg_interval_length = 99; - let subnet_id = subnet_test_id(0); - - let Dependencies { - mut pool, - registry, - state_manager, - dkg_pool, - crypto, - .. - } = dependencies_with_subnet_records_with_raw_state_manager( - pool_config, - subnet_id, - vec![( - 10, - SubnetRecordBuilder::from(&node_ids) - .with_dkg_interval_length(dkg_interval_length) - .build(), - )], - ); - - let target_id = NiDkgTargetId::new([0u8; 32]); - complement_state_manager_with_setup_initial_dkg_request( - state_manager.clone(), - registry.get_latest_version(), - vec![10, 11, 12, 13], - None, - Some(target_id), - ); - - // First summary: 2 remote configs (low + high) for the target - pool.advance_round_normal_operation_n(dkg_interval_length + 1); - let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&pool, target_id); - assert_eq!(remote_dkg_ids.len(), 2); - assert_eq!(extract_dkg_configs_from_highest_block(&pool).len(), 4); + let (mut deps, _target_id, remote_dkg_ids) = setup_initial_dkg_test(pool_config); let original_summary = { - let block: Block = pool + let block: Block = deps + .pool .validated() .block_proposal() .get_highest() @@ -1839,14 +1847,7 @@ mod tests { block.payload.as_ref().as_summary().dkg.clone() }; - // Add dealings for both remote configs to the chain - for dkg_id in &remote_dkg_ids { - let dealings = (0..3) - .map(|i| ChangeAction::AddToValidated(create_dealing(i, dkg_id.clone()))) - .collect::>(); - dkg_pool.write().unwrap().apply(dealings); - pool.advance_round_normal_operation(); - } + add_dealings_for_configs(&mut deps, &remote_dkg_ids); // Construct a modified summary with only 1 remote config. // This simulates the scenario where one transcript was already @@ -1889,10 +1890,10 @@ mod tests { 1, ); - let parent = pool.get_cache().finalized_block(); - let pool_reader = PoolReader::new(&pool); + let parent = deps.pool.get_cache().finalized_block(); + let pool_reader = PoolReader::new(&deps.pool); let validation_context = ValidationContext { - registry_version: registry.get_latest_version(), + registry_version: deps.registry.get_latest_version(), certified_height: Height::from(0), time: UNIX_EPOCH, }; @@ -1902,10 +1903,10 @@ mod tests { // has 1 of the expected 2 configs for a setup_initial_dkg target. let early_transcripts = payload_builder::create_early_remote_transcripts( &pool_reader, - crypto.as_ref(), + deps.crypto.as_ref(), &parent, &modified_summary, - state_manager.as_ref(), + deps.state_manager.as_ref(), &validation_context, no_op_logger(), ) @@ -1920,10 +1921,10 @@ mod tests { // produce early transcripts. let early_transcripts = payload_builder::create_early_remote_transcripts( &pool_reader, - crypto.as_ref(), + deps.crypto.as_ref(), &parent, &original_summary, - state_manager.as_ref(), + deps.state_manager.as_ref(), &validation_context, no_op_logger(), ) @@ -1932,20 +1933,19 @@ mod tests { // If a config exists in the summary but there is no corresponding // context in the state, no early transcript should be created. - // Set up a state manager whose context has a different target_id. let unrelated_target_id = NiDkgTargetId::new([1u8; 32]); let no_match_state_manager = Arc::new(ic_test_utilities::state_manager::RefMockStateManager::default()); complement_state_manager_with_setup_initial_dkg_request( no_match_state_manager.clone(), - registry.get_latest_version(), + deps.registry.get_latest_version(), vec![10, 11, 12, 13], None, Some(unrelated_target_id), ); let early_transcripts = payload_builder::create_early_remote_transcripts( &pool_reader, - crypto.as_ref(), + deps.crypto.as_ref(), &parent, &original_summary, no_match_state_manager.as_ref(), @@ -1961,32 +1961,125 @@ mod tests { }); } + #[test] + fn test_early_remote_transcripts_with_reproducible_crypto_error() { + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + let (mut deps, target_id, remote_dkg_ids) = setup_initial_dkg_test(pool_config); + + add_dealings_for_configs(&mut deps, &remote_dkg_ids); + + let mut mock_crypto = MockCrypto::new(); + mock_crypto + .expect_ni_dkg_create_transcript() + .returning(|_config, _dealings| { + Err( + DkgCreateTranscriptError::MalformedResharingTranscriptInConfig( + MalformedPublicKeyError { + algorithm: AlgorithmId::Groth20_Bls12_381, + key_bytes: None, + internal_error: "test error".to_string(), + }, + ), + ) + }); + + let parent = deps.pool.get_cache().finalized_block(); + + // Scope the pool borrow so we can mutate the pool afterwards + let payload_with_errors = { + let pool_reader = PoolReader::new(&deps.pool); + let last_summary_block = pool_reader.dkg_summary_block(&parent).unwrap(); + let last_summary = &last_summary_block.payload.as_ref().as_summary().dkg; + let validation_context = ValidationContext { + registry_version: deps.registry.get_latest_version(), + certified_height: Height::from(0), + time: UNIX_EPOCH, + }; + + let early_transcripts = payload_builder::create_early_remote_transcripts( + &pool_reader, + &mock_crypto, + &parent, + last_summary, + deps.state_manager.as_ref(), + &validation_context, + no_op_logger(), + ) + .unwrap(); + + assert_eq!(early_transcripts.len(), 2); + for (dkg_id, _callback_id, result) in &early_transcripts { + assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); + let error_msg = result.as_ref().unwrap_err(); + assert!( + error_msg.contains("test error"), + "Error message should contain the original error, got: {error_msg}" + ); + } + + let payload = BlockPayload::Data(DataPayload { + batch: ic_types::batch::BatchPayload::default(), + dkg: DkgDataPayload::new_with_remote_dkg_transcripts( + last_summary_block.height, + vec![], + early_transcripts, + ), + idkg: Default::default(), + }); + + assert!( + validate_payload( + subnet_test_id(0), + deps.registry.as_ref(), + &mock_crypto, + &pool_reader, + &*deps.dkg_pool.read().unwrap(), + parent.clone(), + &payload, + deps.state_manager.as_ref(), + &validation_context, + &MetricsRegistry::new().int_counter_vec( + "consensus_dkg_validator", + "DKG validator counter", + &["type"], + ), + &no_op_logger(), + ) + .is_ok(), + "Payload with reproducible crypto errors should validate successfully" + ); + + payload + }; + + // Insert the payload with errors into the pool as part of a new block. + let mut block = Block::from_parent(&parent); + block.payload = Payload::new(ic_types::crypto::crypto_hash, payload_with_errors); + let proposal = BlockProposal::fake(block, node_test_id(0)); + deps.pool.advance_round_with_block(&proposal); + + // Verify that no more transcripts are created in later blocks. + assert_no_early_transcript_duplicates(&mut deps.pool, EARLY_DKG_INTERVAL); + }); + } + #[test] fn test_early_reshare_chain_key_transcripts() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let node_ids = (1..4).map(node_test_id).collect::>(); - let dkg_interval_length = 99; - let subnet_id = subnet_test_id(0); let key_id = VetKdKeyId { curve: VetKdCurve::Bls12_381_G2, name: String::from("some_vetkey"), }; let target_id = NiDkgTargetId::new([0u8; 32]); - let Dependencies { - mut pool, - registry, - state_manager, - dkg_pool, - crypto, - .. - } = dependencies_with_subnet_records_with_raw_state_manager( + let mut deps = dependencies_with_subnet_records_with_raw_state_manager( pool_config, - subnet_id, + subnet_test_id(0), vec![( 10, SubnetRecordBuilder::from(&node_ids) - .with_dkg_interval_length(dkg_interval_length) + .with_dkg_interval_length(EARLY_DKG_INTERVAL) .with_chain_key_config(ChainKeyConfig { key_configs: vec![KeyConfig { key_id: MasterPublicKeyId::VetKd(key_id.clone()), @@ -2002,35 +2095,29 @@ mod tests { ); complement_state_manager_with_reshare_chain_key_request( - state_manager.clone(), - registry.get_latest_version(), + deps.state_manager.clone(), + deps.registry.get_latest_version(), key_id.clone(), vec![10, 11, 12, 13], None, Some(target_id), ); - // Verify that the next summary block contains the configs and no transcripts. - // This also extracts the DKG ids (1 remote for reshare chain key) - pool.advance_round_normal_operation_n(dkg_interval_length + 1); - let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&pool, target_id); + deps.pool + .advance_round_normal_operation_n(EARLY_DKG_INTERVAL + 1); + let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&deps.pool, target_id); assert_eq!(remote_dkg_ids.len(), 1); - assert_eq!(extract_dkg_configs_from_highest_block(&pool).len(), 4); - assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + assert_eq!(extract_dkg_configs_from_highest_block(&deps.pool).len(), 4); + assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); - // Put three dealings in the pool and check that they get included - let dealings = (0..3) - .map(|i| ChangeAction::AddToValidated(create_dealing(i, remote_dkg_ids[0].clone()))) - .collect::>(); - dkg_pool.write().unwrap().apply(dealings); - pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 3); - assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); + add_dealings_for_configs(&mut deps, &remote_dkg_ids); + assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 3); + assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); - // Now sufficient dealings are in the pool, check that payload contains early remote transcript - pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); - let remote_dkgs = extract_remote_dkgs_from_highest_block(&pool); + // Now sufficient dealings are in the pool; early remote transcript should appear + deps.pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 0); + let remote_dkgs = extract_remote_dkgs_from_highest_block(&deps.pool); assert_eq!(remote_dkgs.len(), 1); let (dkg_id, _, result) = &remote_dkgs[0]; assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); @@ -2040,51 +2127,11 @@ mod tests { NiDkgTag::HighThresholdForKey(NiDkgMasterPublicKeyId::VetKd(key_id)) ); - // Check that the payload also validates - let block: Block = pool - .validated() - .block_proposal() - .get_highest() - .unwrap() - .content - .into_inner(); - let pool_reader = PoolReader::new(&pool); - - let parent = &block.parent; - let height = block.height().decrement(); - let parent = pool_reader - .get_notarized_block(parent, height) - .map(|block| block.into_inner()) - .unwrap(); + // Verify that the highest block validates. + assert_highest_block_validates(&deps); - assert!( - validate_payload( - subnet_test_id(0), - registry.as_ref(), - crypto.as_ref(), - &pool_reader, - &*dkg_pool.read().unwrap(), - parent.clone(), - block.payload.as_ref(), - state_manager.as_ref(), - &block.context, - &MetricsRegistry::new().int_counter_vec( - "consensus_dkg_validator", - "DKG validator counter", - &["type"], - ), - &no_op_logger(), - ) - .is_ok() - ); - - // Advance the pool until the next DKG, check that the early remote transcript is not - // generated multiple times - for _ in 0..dkg_interval_length + 1 { - pool.advance_round_normal_operation(); - assert_eq!(extract_dealings_from_highest_block(&pool).len(), 0); - assert_eq!(extract_remote_dkgs_from_highest_block(&pool).len(), 0); - } + // Verify that no more transcripts are created in later blocks. + assert_no_early_transcript_duplicates(&mut deps.pool, EARLY_DKG_INTERVAL); }); } From 63ae5f126c070d2fdca265d0ac3b4af6158b19a3 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Tue, 3 Mar 2026 13:08:01 +0000 Subject: [PATCH 65/98] Automatically updated Cargo*.lock --- Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e6be8f531714..1391ec228b1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7759,12 +7759,14 @@ name = "ic-consensus-dkg" version = "0.9.0" dependencies = [ "ic-artifact-pool", + "ic-config", "ic-consensus-mocks", "ic-consensus-utils", "ic-crypto-temp-crypto", "ic-crypto-test-utils-crypto-returning-ok", "ic-crypto-test-utils-ni-dkg", "ic-interfaces", + "ic-interfaces-mocks", "ic-interfaces-registry", "ic-interfaces-registry-mocks", "ic-interfaces-state-manager", From 53e680e3f2903793629c1ec08290807820a76f02 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 6 Mar 2026 12:21:51 +0000 Subject: [PATCH 66/98] prioritize remote dkg --- rs/consensus/dkg/src/lib.rs | 9 +- rs/consensus/dkg/src/payload_builder.rs | 308 ++++++++++++++++++++++-- 2 files changed, 296 insertions(+), 21 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index e7238035a12e..4a4069457fd0 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -452,7 +452,7 @@ mod tests { fn test_create_dealings_payload() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { with_test_replica_logger(|logger| { - let nodes: Vec<_> = (0..3).map(node_test_id).collect(); + let nodes: Vec<_> = (0..4).map(node_test_id).collect(); let dkg_interval_len = 30; let subnet_id = subnet_test_id(222); let initial_registry_version = 112; @@ -1626,7 +1626,7 @@ mod tests { fn setup_initial_dkg_test( pool_config: ic_config::artifact_pool::ArtifactPoolConfig, ) -> (Dependencies, NiDkgTargetId, Vec) { - let node_ids = (1..4).map(node_test_id).collect::>(); + let node_ids = (1..8).map(node_test_id).collect::>(); let mut deps = dependencies_with_subnet_records_with_raw_state_manager( pool_config, @@ -1732,6 +1732,7 @@ mod tests { .collect::>(); deps.dkg_pool.write().unwrap().apply(dealings); deps.pool.advance_round_normal_operation(); + // f + 1 dealings for high or low remote threshold DKG assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 3); assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); @@ -1746,6 +1747,7 @@ mod tests { .collect::>(); deps.dkg_pool.write().unwrap().apply(dealings); deps.pool.advance_round_normal_operation(); + // f + 1 dealings for low or high remote threshold DKG assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 3); assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); @@ -2066,7 +2068,7 @@ mod tests { #[test] fn test_early_reshare_chain_key_transcripts() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { - let node_ids = (1..4).map(node_test_id).collect::>(); + let node_ids = (1..5).map(node_test_id).collect::>(); let key_id = VetKdKeyId { curve: VetKdCurve::Bls12_381_G2, name: String::from("some_vetkey"), @@ -2111,6 +2113,7 @@ mod tests { assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); add_dealings_for_configs(&mut deps, &remote_dkg_ids); + // 2f + 1 dealings for high threshold VetKD resharing assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 3); assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 3d7d12e8107d..930aaeba2ecb 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -20,7 +20,7 @@ use ic_types::{ batch::ValidationContext, consensus::{ Block, - dkg::{DkgDataPayload, DkgPayload, DkgPayloadCreationError, DkgSummary}, + dkg::{self, DkgDataPayload, DkgPayload, DkgPayloadCreationError, DkgSummary}, get_faults_tolerated, }, crypto::threshold_sig::ni_dkg::{ @@ -32,7 +32,7 @@ use ic_types::{ messages::CallbackId, }; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashSet}, sync::{Arc, RwLock}, }; @@ -107,23 +107,19 @@ fn create_data_payload( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { - // Get all dealer ids from the chain. + // Get all existing dealer ids from the chain. let dealers_from_chain = utils::get_dealers_from_chain(pool_reader, parent); - // Filter from the validated pool all dealings whose dealer has no dealing on - // the chain yet. - let new_validated_dealings = dkg_pool + // Select new dealings for the payload. + let pool_lock = dkg_pool .read() - .expect("Couldn't lock DKG pool for reading.") - .get_validated() - .filter(|msg| { - // Make sure the message relates to one of the ongoing DKGs and it's from a unique - // dealer. - last_dkg_summary.configs.contains_key(&msg.content.dkg_id) - && !dealers_from_chain.contains(&(msg.content.dkg_id.clone(), msg.signature.signer)) - }) - .take(max_dealings_per_block) - .cloned() - .collect(); + .expect("Couldn't lock DKG pool for reading."); + let new_validated_dealings = select_dealings_for_payload( + &last_dkg_summary.configs, + &dealers_from_chain, + &*pool_lock, + max_dealings_per_block, + ); + drop(pool_lock); let remote_dkg_transcripts = create_early_remote_transcripts( pool_reader, @@ -152,6 +148,63 @@ fn create_data_payload( )) } +/// Selects dealings from the validated pool to include in a block payload. +/// +/// Filters dealings to only include those for ongoing DKGs from unique dealers. +/// Never includes more dealings than the collection_threshold for any config. +/// Prioritizes dealings for remote DKGs. +fn select_dealings_for_payload( + configs: &BTreeMap, + dealers_from_chain: &HashSet<(NiDkgId, NodeId)>, + dkg_pool: &dyn DkgPool, + max_dealings_per_block: usize, +) -> Vec { + // Compute remaining capacity (collection_threshold - dealings on chain) for each config. + let mut remaining_capacity: BTreeMap<&NiDkgId, usize> = configs + .iter() + .map(|(dkg_id, config)| (dkg_id, config.collection_threshold().get() as usize)) + .collect(); + for (dkg_id, _) in dealers_from_chain { + if let Some(cap) = remaining_capacity.get_mut(dkg_id) { + *cap = cap.saturating_sub(1); + } + } + + // Filter dealings whose dealer has no dealing on the chain yet, and for which + // the collection_threshold hasn't been reached. Prioritize remote DKGs. + let (remote_priority, rest): (Vec<_>, Vec<_>) = dkg_pool + .get_validated() + .filter(|msg| { + // Make sure the message relates to one of the ongoing DKGs, it's from a unique + // dealer, and the collection_threshold hasn't been reached yet. + let Some(cap) = remaining_capacity.get_mut(&msg.content.dkg_id) else { + return false; + }; + if dealers_from_chain.contains(&(msg.content.dkg_id.clone(), msg.signature.signer)) { + return false; + } + if *cap > 0 { + *cap -= 1; + true + } else { + false + } + }) + .partition(|msg| { + matches!( + msg.content.dkg_id.target_subnet, + NiDkgTargetSubnet::Remote(_) + ) + }); + + remote_priority + .into_iter() + .chain(rest) + .take(max_dealings_per_block) + .cloned() + .collect() +} + #[allow(clippy::type_complexity)] pub(crate) fn create_early_remote_transcripts( pool_reader: &PoolReader<'_>, @@ -1188,7 +1241,12 @@ fn create_remote_dkg_config( mod tests { use crate::tests::test_vet_key_config; - use super::{super::test_utils::complement_state_manager_with_setup_initial_dkg_request, *}; + use super::{ + super::test_utils::{ + complement_state_manager_with_setup_initial_dkg_request, create_dealing, + }, + *, + }; use ic_consensus_mocks::{ Dependencies, dependencies_with_subnet_params, dependencies_with_subnet_records_with_raw_state_manager, @@ -2195,4 +2253,218 @@ mod tests { ); }); } + + struct TestDkgPool { + messages: Vec, + } + + impl DkgPool for TestDkgPool { + fn get_validated(&self) -> Box + '_> { + Box::new(self.messages.iter()) + } + fn get_unvalidated(&self) -> Box + '_> { + Box::new(std::iter::empty()) + } + fn get_current_start_height(&self) -> Height { + Height::from(0) + } + fn validated_contains(&self, _msg: &dkg::Message) -> bool { + false + } + } + + fn make_test_config(dkg_id: NiDkgId, max_corrupt_dealers: u32) -> NiDkgConfig { + let nodes: BTreeSet<_> = (0..10).map(node_test_id).collect(); + NiDkgConfig::new(NiDkgConfigData { + dkg_id, + max_corrupt_dealers: NumberOfNodes::from(max_corrupt_dealers), + dealers: nodes.clone(), + max_corrupt_receivers: NumberOfNodes::from(1), + receivers: nodes, + threshold: NumberOfNodes::from(2), + registry_version: RegistryVersion::from(1), + resharing_transcript: None, + }) + .unwrap() + } + + fn local_dkg_id(tag: NiDkgTag) -> NiDkgId { + NiDkgId { + start_block_height: Height::from(0), + dealer_subnet: subnet_test_id(0), + dkg_tag: tag, + target_subnet: NiDkgTargetSubnet::Local, + } + } + + fn remote_dkg_id(tag: NiDkgTag) -> NiDkgId { + NiDkgId { + start_block_height: Height::from(0), + dealer_subnet: subnet_test_id(0), + dkg_tag: tag, + target_subnet: NiDkgTargetSubnet::Remote(NiDkgTargetId::new([0u8; 32])), + } + } + + #[test] + fn test_select_dealings_prioritizes_remote_over_local() { + let local_id = local_dkg_id(NiDkgTag::LowThreshold); + let remote_id = remote_dkg_id(NiDkgTag::LowThreshold); + + let configs: BTreeMap<_, _> = [ + (local_id.clone(), make_test_config(local_id.clone(), 1)), + (remote_id.clone(), make_test_config(remote_id.clone(), 1)), + ] + .into(); + + // Create dealings: local first in the list, then remote. + let pool = TestDkgPool { + messages: vec![ + create_dealing(0, local_id.clone()), + create_dealing(1, remote_id.clone()), + ], + }; + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 1); + + assert_eq!(selected.len(), 1); + assert_eq!(selected[0].content.dkg_id, remote_id); + } + + #[test] + fn test_select_dealings_caps_at_collection_threshold() { + // collection_threshold = max_corrupt_dealers + 1 = 2 + let id = local_dkg_id(NiDkgTag::LowThreshold); + let configs: BTreeMap<_, _> = [(id.clone(), make_test_config(id.clone(), 1))].into(); + + // Create 4 dealings for the same config, from different dealers. + let pool = TestDkgPool { + messages: (0..4).map(|i| create_dealing(i, id.clone())).collect(), + }; + + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 10); + + // Only collection_threshold (2) dealings should be included. + assert_eq!(selected.len(), 2); + } + + #[test] + fn test_select_dealings_accounts_for_dealers_from_chain() { + // collection_threshold = 2 + let id = local_dkg_id(NiDkgTag::LowThreshold); + let configs: BTreeMap<_, _> = [(id.clone(), make_test_config(id.clone(), 1))].into(); + + // 1 dealing already on chain + let dealers_from_chain: HashSet<_> = [(id.clone(), node_test_id(0))].into(); + + // 3 new dealings (from different dealers) + let pool = TestDkgPool { + messages: (1..4).map(|i| create_dealing(i, id.clone())).collect(), + }; + + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 10); + + // Only 1 more needed to reach collection_threshold of 2. + assert_eq!(selected.len(), 1); + } + + #[test] + fn test_select_dealings_filters_duplicate_dealers() { + let id = local_dkg_id(NiDkgTag::LowThreshold); + let configs: BTreeMap<_, _> = [(id.clone(), make_test_config(id.clone(), 1))].into(); + + // Dealer 0 already on chain + let dealers_from_chain: HashSet<_> = [(id.clone(), node_test_id(0))].into(); + + // Pool also has a dealing from dealer 0 + let pool = TestDkgPool { + messages: vec![create_dealing(0, id.clone()), create_dealing(1, id.clone())], + }; + + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 10); + + // Dealer 0's dealing should be filtered out, only dealer 1's remains. + assert_eq!(selected.len(), 1); + assert_eq!(selected[0].signature.signer, node_test_id(1)); + } + + #[test] + fn test_select_dealings_respects_max_dealings_per_block() { + let local_id = local_dkg_id(NiDkgTag::LowThreshold); + let remote_id = remote_dkg_id(NiDkgTag::LowThreshold); + + // Both configs have collection_threshold = 4 + let configs: BTreeMap<_, _> = [ + (local_id.clone(), make_test_config(local_id.clone(), 3)), + (remote_id.clone(), make_test_config(remote_id.clone(), 3)), + ] + .into(); + + let pool = TestDkgPool { + messages: (0..4) + .map(|i| create_dealing(i, local_id.clone())) + .chain((4..8).map(|i| create_dealing(i, remote_id.clone()))) + .collect(), + }; + + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 3); + + assert_eq!(selected.len(), 3); + // All 3 should be remote (prioritized), since remote has 4 available. + for msg in &selected { + assert_eq!(msg.content.dkg_id, remote_id); + } + } + + #[test] + fn test_select_dealings_ignores_unknown_configs() { + let known_id = local_dkg_id(NiDkgTag::LowThreshold); + let unknown_id = local_dkg_id(NiDkgTag::HighThreshold); + + let configs: BTreeMap<_, _> = + [(known_id.clone(), make_test_config(known_id.clone(), 1))].into(); + + let pool = TestDkgPool { + messages: vec![ + create_dealing(0, unknown_id), + create_dealing(1, known_id.clone()), + ], + }; + + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 10); + + assert_eq!(selected.len(), 1); + assert_eq!(selected[0].content.dkg_id, known_id); + } + + #[test] + fn test_select_dealings_remote_priority_with_threshold_cap() { + let local_id = local_dkg_id(NiDkgTag::LowThreshold); + let remote_id = remote_dkg_id(NiDkgTag::LowThreshold); + + // collection_threshold = 2 for both + let configs: BTreeMap<_, _> = [ + (local_id.clone(), make_test_config(local_id.clone(), 1)), + (remote_id.clone(), make_test_config(remote_id.clone(), 1)), + ] + .into(); + + // 3 local dealings, 3 remote dealings + let pool = TestDkgPool { + messages: (0..3) + .map(|i| create_dealing(i, local_id.clone())) + .chain((3..6).map(|i| create_dealing(i, remote_id.clone()))) + .collect(), + }; + + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 10); + + // 2 remote + 2 local (both capped at collection_threshold) + assert_eq!(selected.len(), 4); + // First 2 should be remote (prioritized) + assert_eq!(selected[0].content.dkg_id, remote_id); + assert_eq!(selected[1].content.dkg_id, remote_id); + // Next 2 should be local + assert_eq!(selected[2].content.dkg_id, local_id); + assert_eq!(selected[3].content.dkg_id, local_id); + } } From ea39195d294aa1d083edcec059d8eff19e0c20a6 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 9 Mar 2026 14:08:56 +0000 Subject: [PATCH 67/98] clean up test utils --- rs/consensus/dkg/src/test_utils.rs | 37 +++++++++++------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index 99b0173a5567..af098129a41c 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -11,7 +11,10 @@ use ic_test_utilities_consensus::fake::FakeContentSigner; use ic_test_utilities_types::{ids::node_test_id, messages::RequestBuilder}; use ic_types::{ Height, RegistryVersion, - consensus::dkg::{DealingContent, DealingMessages, Message}, + consensus::{ + BlockPayload, + dkg::{DealingContent, DealingMessages, Message}, + }, crypto::threshold_sig::ni_dkg::{ NiDkgId, NiDkgTargetId, NiDkgTargetSubnet, NiDkgTranscript, config::NiDkgConfig, }, @@ -99,22 +102,10 @@ pub(super) fn extract_remote_dkgs_from_highest_block( .content .into_inner(); - if block.payload.as_ref().is_summary() { - &block - .payload - .as_ref() - .as_summary() - .dkg - .transcripts_for_remote_subnets - } else { - &block - .payload - .as_ref() - .as_data() - .dkg - .transcripts_for_remote_subnets + match block.payload.as_ref() { + BlockPayload::Summary(summary) => summary.dkg.transcripts_for_remote_subnets.clone(), + BlockPayload::Data(data) => data.dkg.transcripts_for_remote_subnets.clone(), } - .clone() } /// Extract the dealings from the current highest validated block @@ -127,10 +118,9 @@ pub(super) fn extract_dealings_from_highest_block(pool: &TestConsensusPool) -> D .content .into_inner(); - if block.payload.as_ref().is_summary() { - vec![] - } else { - block.payload.as_ref().as_data().dkg.messages.clone() + match block.payload.as_ref() { + BlockPayload::Summary(_) => vec![], + BlockPayload::Data(data) => data.dkg.messages.clone(), } } @@ -159,10 +149,9 @@ pub(super) fn extract_dkg_configs_from_highest_block( .content .into_inner(); - if block.payload.as_ref().is_summary() { - block.payload.as_ref().as_summary().dkg.configs.clone() - } else { - BTreeMap::new() + match block.payload.as_ref() { + BlockPayload::Summary(summary) => summary.dkg.configs.clone(), + BlockPayload::Data(_) => BTreeMap::new(), } } From c50c960d5db538fe7903cf611217c735329f334e Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 9 Mar 2026 14:10:11 +0000 Subject: [PATCH 68/98] inferred return type --- rs/consensus/dkg/src/payload_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 3d7d12e8107d..4a37a8add847 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1015,7 +1015,7 @@ fn build_target_id_callback_map( .iter() .map(|(&callback_id, context)| (context.target_id, (1, callback_id))), ) - .collect::>() + .collect() } fn add_callback_ids_to_transcript_results( From a1e2c0dacdeca0a9aed06bed35fbb6fdd204e345 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 9 Mar 2026 14:12:39 +0000 Subject: [PATCH 69/98] build_target_id_callback_map docs --- rs/consensus/dkg/src/payload_builder.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 4a37a8add847..b72095e2c62a 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1001,6 +1001,10 @@ fn eq_sans_height(dkg_id1: &NiDkgId, dkg_id2: &NiDkgId) -> bool { && dkg_id1.target_subnet == dkg_id2.target_subnet } +// Build a map from target id to callback id according to contexts in the replicated state. +// Additionally, for each target ID, return the expected number of DKG instances necessary +// to answer the request. Specifically, setup initial DKG requests require two DKGs, whereas +// resharing a chain key requires on DKG instance. fn build_target_id_callback_map( state: &ReplicatedState, ) -> BTreeMap { From 00fb1e0d742b39b79997dbbc707377cbdbeb286a Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 9 Mar 2026 14:13:27 +0000 Subject: [PATCH 70/98] generalize comment --- rs/consensus/dkg/src/payload_builder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index b72095e2c62a..ce1c913e5ff2 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -202,8 +202,8 @@ pub(crate) fn create_early_remote_transcripts( // Check that we have the expected number of configs for this target_id if configs.len() != *expected_config_num { - // This may happen if we only managed to create one transcript (out of two) as part - // of the last summary block. We will handle this in the next summary block instead. + // This may happen if we did not manage to create all required transcripts as part of + // the last summary block. We will handle this in the next summary block instead. continue; } From b402350f86112c53ea0255176d461bcdcc9aeb5d Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 10 Mar 2026 08:31:53 +0000 Subject: [PATCH 71/98] remove number_of_contexts --- rs/consensus/dkg/src/payload_builder.rs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index ce1c913e5ff2..02daaa1e3636 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -168,7 +168,8 @@ pub(crate) fn create_early_remote_transcripts( .map_err(DkgPayloadCreationError::StateManagerError)?; // Since this function is relatively expensive, we simply return if there are no outstanding DKG contexts - if number_of_contexts(state.get_ref()) == 0 { + let callback_id_map = build_target_id_callback_map(state.get_ref()); + if callback_id_map.is_empty() { return Ok(vec![]); } @@ -188,9 +189,6 @@ pub(crate) fn create_early_remote_transcripts( } } - let state_ref = state.get_ref(); - let callback_id_map = build_target_id_callback_map(state_ref); - // Try to create transcripts for all configs of each target_id. Note that we either include // all transcript results for a target_id or none of them. let mut selected_transcripts = vec![]; @@ -1049,19 +1047,6 @@ fn add_callback_ids_to_transcript_results( .collect() } -fn number_of_contexts(state: &ReplicatedState) -> usize { - state - .metadata - .subnet_call_context_manager - .setup_initial_dkg_contexts - .len() - + state - .metadata - .subnet_call_context_manager - .reshare_chain_key_contexts - .len() -} - /// This function is called for each entry on the SubnetCallContext. It returns /// either the created high and low configs for the entry or returns two errors /// identified by the NiDkgId. From a9a4ad0a33da2e585d1dfe2a21f248cfebde796a Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 10 Mar 2026 08:40:16 +0000 Subject: [PATCH 72/98] check against MAX_EARLY_REMOTE_TRANSCRIPTS early --- rs/consensus/dkg/src/payload_builder.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 02daaa1e3636..76761eb7ea7a 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -205,6 +205,13 @@ pub(crate) fn create_early_remote_transcripts( continue; } + // Ensure that creating these transcripts would not exceed the maximum number of early + // remote transcripts. We continue with the next target_id in case it requires less + // transcripts. + if selected_transcripts.len() + configs.len() > MAX_EARLY_REMOTE_TRANSCRIPTS { + continue; + } + // If any of the configs has less dealings than the threshold, we skip this target_id if configs.iter().any(|config| { let dealings_count = all_dealings @@ -243,10 +250,6 @@ pub(crate) fn create_early_remote_transcripts( }; selected_transcripts.push((config.dkg_id().clone(), *callback_id, transcript_result)); } - - if selected_transcripts.len() >= MAX_EARLY_REMOTE_TRANSCRIPTS { - break; - } } Ok(selected_transcripts) From bfabda44747dd5e27b73fdbe712473e7cc963c0f Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 10 Mar 2026 10:06:29 +0000 Subject: [PATCH 73/98] MAX_EARLY_REMOTE_TRANSCRIPTS test --- rs/consensus/dkg/src/lib.rs | 217 ++++++++++++++++++++++++++++- rs/consensus/dkg/src/test_utils.rs | 118 ++++++++++------ 2 files changed, 289 insertions(+), 46 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index e7238035a12e..67a324af3458 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -397,6 +397,7 @@ impl BouncerFactory for DkgBouncer { mod tests { use super::*; use crate::test_utils::{ + complement_state_manager_with_both_dkg_contexts, complement_state_manager_with_reshare_chain_key_request, complement_state_manager_with_setup_initial_dkg_request, create_dealing, extract_dkg_configs_from_highest_block, extract_remote_dkg_ids_from_highest_block, @@ -409,7 +410,7 @@ mod tests { }; use ic_consensus_utils::pool_reader::PoolReader; use ic_crypto_test_utils_crypto_returning_ok::CryptoReturningOk; - use ic_crypto_test_utils_ni_dkg::dummy_dealing; + use ic_crypto_test_utils_ni_dkg::{dummy_dealing, dummy_transcript_for_tests_with_params}; use ic_interfaces::{ consensus_pool::ConsensusPool, p2p::consensus::{MutablePool, UnvalidatedArtifact}, @@ -426,18 +427,19 @@ mod tests { use ic_test_utilities_registry::{SubnetRecordBuilder, add_subnet_record}; use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ - RegistryVersion, ReplicaVersion, + NumberOfNodes, RegistryVersion, ReplicaVersion, batch::ValidationContext, consensus::{ - Block, BlockPayload, BlockProposal, DataPayload, HasHeight, Payload, + Block, BlockPayload, BlockProposal, DataPayload, HasHeight, Payload, SummaryPayload, dkg::{DkgDataPayload, DkgSummary}, + get_faults_tolerated, }, crypto::{ AlgorithmId, error::MalformedPublicKeyError, threshold_sig::ni_dkg::{ NiDkgId, NiDkgMasterPublicKeyId, NiDkgTag, NiDkgTargetId, NiDkgTargetSubnet, - errors::create_transcript_error::DkgCreateTranscriptError, + config::NiDkgConfigData, errors::create_transcript_error::DkgCreateTranscriptError, }, }, time::UNIX_EPOCH, @@ -2135,6 +2137,213 @@ mod tests { }); } + /// Tests that when the state has both a SetupInitialDKG context (needing 2 + /// transcripts) and a ReshareChainKey context (needing 1 transcript), they + /// d not all appear in the same block due to MAX_EARLY_REMOTE_TRANSCRIPTS + /// (= 2). Only the transcripts for the first context are included. + #[test] + fn test_early_remote_transcripts_max_limits_to_one_context() { + for (setup_target_bytes, reshare_target_bytes, desc) in [ + ([0u8; 32], [1u8; 32], "SetupInitialDKG first"), + ([1u8; 32], [0u8; 32], "ReshareChainKey first"), + ] { + ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { + let node_ids = (1..4).map(node_test_id).collect::>(); + let key_id = VetKdKeyId { + curve: VetKdCurve::Bls12_381_G2, + name: String::from("some_vetkey"), + }; + let setup_target_id = NiDkgTargetId::new(setup_target_bytes); + let reshare_target_id = NiDkgTargetId::new(reshare_target_bytes); + + let mut deps = dependencies_with_subnet_records_with_raw_state_manager( + pool_config, + subnet_test_id(0), + vec![( + 10, + SubnetRecordBuilder::from(&node_ids) + .with_dkg_interval_length(EARLY_DKG_INTERVAL) + .with_chain_key_config(ChainKeyConfig { + key_configs: vec![KeyConfig { + key_id: MasterPublicKeyId::VetKd(key_id.clone()), + pre_signatures_to_create_in_advance: None, + max_queue_size: 20, + }], + signature_request_timeout_ns: None, + idkg_key_rotation_period_ms: None, + max_parallel_pre_signature_transcripts_in_creation: None, + }) + .build(), + )], + ); + + complement_state_manager_with_both_dkg_contexts( + deps.state_manager.clone(), + deps.registry.get_latest_version(), + key_id.clone(), + vec![10, 11, 12, 13], + setup_target_id, + reshare_target_id, + ); + + // Advance to one block before the summary. + deps.pool + .advance_round_normal_operation_n(EARLY_DKG_INTERVAL); + + // Create the summary block (without inserting it yet). Due to + // MAX_REMOTE_DKGS_PER_INTERVAL=1, only the SetupInitialDKG configs + // are included. + let summary_proposal = deps.pool.make_next_block(); + let mut summary_block: Block = summary_proposal.content.into_inner(); + let original_summary = summary_block.payload.as_ref().as_summary().dkg.clone(); + + let setup_remote_dkg_ids: Vec = original_summary + .configs + .keys() + .filter(|id| id.target_subnet == NiDkgTargetSubnet::Remote(setup_target_id)) + .cloned() + .collect(); + assert_eq!( + setup_remote_dkg_ids.len(), + 2, + "[{desc}] Expected 2 SetupInitialDKG remote configs" + ); + + // Manually create a ReshareChainKey config for the second target. + let reshare_dkg_id = NiDkgId { + start_block_height: original_summary.height, + dealer_subnet: subnet_test_id(0), + dkg_tag: NiDkgTag::HighThresholdForKey(NiDkgMasterPublicKeyId::VetKd( + key_id.clone(), + )), + target_subnet: NiDkgTargetSubnet::Remote(reshare_target_id), + }; + + let dealers: BTreeSet<_> = node_ids.iter().cloned().collect(); + let receivers: BTreeSet<_> = (10..14).into_iter().map(node_test_id).collect(); + + let resharing_transcript = dummy_transcript_for_tests_with_params( + node_ids.clone(), + NiDkgTag::HighThresholdForKey(NiDkgMasterPublicKeyId::VetKd(key_id.clone())), + 2, + 10, + ); + + let reshare_config = NiDkgConfig::new(NiDkgConfigData { + threshold: NumberOfNodes::from( + reshare_dkg_id + .dkg_tag + .threshold_for_subnet_of_size(receivers.len()) + as u32, + ), + dkg_id: reshare_dkg_id.clone(), + max_corrupt_dealers: NumberOfNodes::from( + get_faults_tolerated(dealers.len()) as u32 + ), + max_corrupt_receivers: NumberOfNodes::from( + get_faults_tolerated(receivers.len()) as u32, + ), + dealers, + receivers, + registry_version: deps.registry.get_latest_version(), + resharing_transcript: Some(resharing_transcript), + }) + .expect("Failed to create reshare config"); + + // Build a modified summary with configs for both targets and insert + // it into the pool. This way the pool's actual summary contains both + // config groups, so dealings for all configs are picked up normally. + let mut all_configs: Vec = + original_summary.configs.values().cloned().collect(); + all_configs.push(reshare_config); + let modified_summary = DkgSummary::new( + all_configs, + original_summary.current_transcripts().clone(), + original_summary.next_transcripts().clone(), + vec![], + original_summary.registry_version, + original_summary.interval_length, + original_summary.next_interval_length, + original_summary.height, + BTreeMap::new(), + ); + summary_block.payload = Payload::new( + ic_types::crypto::crypto_hash, + BlockPayload::Summary(SummaryPayload { + dkg: modified_summary, + idkg: None, + }), + ); + let proposal = BlockProposal::fake(summary_block, node_test_id(0)); + deps.pool.advance_round_with_block(&proposal); + + // Add dealings for all 3 remote configs at once, then advance so + // they appear on chain. + let all_remote_dkg_ids: Vec = setup_remote_dkg_ids + .iter() + .cloned() + .chain(std::iter::once(reshare_dkg_id.clone())) + .collect(); + for dkg_id in &all_remote_dkg_ids { + let dealings: Vec<_> = (0..3) + .map(|i| ChangeAction::AddToValidated(create_dealing(i, dkg_id.clone()))) + .collect(); + deps.dkg_pool.write().unwrap().apply(dealings); + } + deps.pool.advance_round_normal_operation(); + assert_eq!( + extract_dealings_from_highest_block(&deps.pool).len(), + 9, + "[{desc}] all 9 dealings should be in the block" + ); + assert_eq!( + extract_remote_dkgs_from_highest_block(&deps.pool).len(), + 0, + "[{desc}] no early transcripts yet" + ); + + // Advance once more: the block maker now sees the dealings on + // chain and creates early remote transcripts, constrained by + // MAX_EARLY_REMOTE_TRANSCRIPTS. + deps.pool.advance_round_normal_operation(); + assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 0); + let remote_dkgs = extract_remote_dkgs_from_highest_block(&deps.pool); + + if setup_target_id < reshare_target_id { + assert_eq!( + remote_dkgs.len(), + 2, + "[{desc}] Expected 2 SetupInitialDKG transcripts, got {}", + remote_dkgs.len() + ); + for (dkg_id, _, result) in &remote_dkgs { + assert_eq!( + dkg_id.target_subnet, + NiDkgTargetSubnet::Remote(setup_target_id), + "[{desc}] transcript should be for SetupInitialDKG target id" + ); + assert!(result.is_ok(), "[{desc}]"); + } + } else { + // Reshare comes first: 1 reshare transcript; setup's 2 exceed the limit. + assert_eq!( + remote_dkgs.len(), + 1, + "[{desc}] Expected 1 ReshareChainKey transcript, got {}", + remote_dkgs.len() + ); + let (dkg_id, _, result) = &remote_dkgs[0]; + assert_eq!( + dkg_id.target_subnet, + NiDkgTargetSubnet::Remote(reshare_target_id), + "[{desc}] transcript should be for ReshareChainKey target id" + ); + assert!(result.is_ok(), "[{desc}]"); + } + }); + } + } + #[test] fn test_dkg_payload_has_transcript_for_reshare_chain_key_request() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index af098129a41c..724f9abd0384 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -22,30 +22,50 @@ use ic_types::{ }; use std::{collections::BTreeMap, sync::Arc}; -pub(super) fn complement_state_manager_with_setup_initial_dkg_request( - state_manager: Arc, +fn make_setup_initial_dkg_context( registry_version: RegistryVersion, node_ids: Vec, + target_id: NiDkgTargetId, +) -> SubnetCallContext { + SubnetCallContext::SetupInitialDKG(SetupInitialDkgContext { + request: RequestBuilder::new().build(), + nodes_in_target_subnet: node_ids.into_iter().map(node_test_id).collect(), + target_id, + registry_version, + time: ic_types::time::UNIX_EPOCH, + }) +} + +fn make_reshare_chain_key_context( + registry_version: RegistryVersion, + key_id: VetKdKeyId, + node_ids: Vec, + target_id: NiDkgTargetId, +) -> SubnetCallContext { + SubnetCallContext::ReshareChainKey(ReshareChainKeyContext { + request: RequestBuilder::new().build(), + key_id: MasterPublicKeyId::VetKd(key_id), + nodes: node_ids.into_iter().map(node_test_id).collect(), + registry_version, + time: ic_types::time::UNIX_EPOCH, + target_id, + }) +} + +/// Set up the state manager mock to return an initial state containing the +/// given subnet call contexts. +pub(super) fn complement_state_manager_with_dkg_contexts( + state_manager: Arc, + contexts: Vec, times: Option, - target: Option, ) { let mut state = ic_test_utilities_state::get_initial_state(0, 0); - - // Add the context into state_manager. - let nodes_in_target_subnet = node_ids.into_iter().map(node_test_id).collect(); - - if let Some(target_id) = target { - state.metadata.subnet_call_context_manager.push_context( - SubnetCallContext::SetupInitialDKG(SetupInitialDkgContext { - request: RequestBuilder::new().build(), - nodes_in_target_subnet, - target_id, - registry_version, - time: state.time(), - }), - ); + for context in contexts { + state + .metadata + .subnet_call_context_manager + .push_context(context); } - let mut mock = state_manager.get_mut(); let expectation = mock .expect_get_state_at() @@ -55,6 +75,20 @@ pub(super) fn complement_state_manager_with_setup_initial_dkg_request( } } +pub(super) fn complement_state_manager_with_setup_initial_dkg_request( + state_manager: Arc, + registry_version: RegistryVersion, + node_ids: Vec, + times: Option, + target: Option, +) { + let contexts = target + .into_iter() + .map(|t| make_setup_initial_dkg_context(registry_version, node_ids.clone(), t)) + .collect(); + complement_state_manager_with_dkg_contexts(state_manager, contexts, times); +} + pub(super) fn complement_state_manager_with_reshare_chain_key_request( state_manager: Arc, registry_version: RegistryVersion, @@ -63,31 +97,31 @@ pub(super) fn complement_state_manager_with_reshare_chain_key_request( times: Option, target: Option, ) { - let mut state = ic_test_utilities_state::get_initial_state(0, 0); - - // Add the context into state_manager. - let nodes_in_target_subnet = node_ids.into_iter().map(node_test_id).collect(); - - if let Some(target_id) = target { - state.metadata.subnet_call_context_manager.push_context( - SubnetCallContext::ReshareChainKey(ReshareChainKeyContext { - request: RequestBuilder::new().build(), - key_id: MasterPublicKeyId::VetKd(key_id), - nodes: nodes_in_target_subnet, - registry_version, - time: state.time(), - target_id, - }), - ); - } + let contexts = target + .into_iter() + .map(|t| { + make_reshare_chain_key_context(registry_version, key_id.clone(), node_ids.clone(), t) + }) + .collect(); + complement_state_manager_with_dkg_contexts(state_manager, contexts, times); +} - let mut mock = state_manager.get_mut(); - let expectation = mock - .expect_get_state_at() - .return_const(Ok(Labeled::new(Height::new(0), Arc::new(state)))); - if let Some(times) = times { - expectation.times(times); - } +pub(super) fn complement_state_manager_with_both_dkg_contexts( + state_manager: Arc, + registry_version: RegistryVersion, + key_id: VetKdKeyId, + node_ids: Vec, + setup_target_id: NiDkgTargetId, + reshare_target_id: NiDkgTargetId, +) { + complement_state_manager_with_dkg_contexts( + state_manager, + vec![ + make_setup_initial_dkg_context(registry_version, node_ids.clone(), setup_target_id), + make_reshare_chain_key_context(registry_version, key_id, node_ids, reshare_target_id), + ], + None, + ); } /// Extract the remote dkg transcripts from the current highest validated block From 2697f4e87b5adcbd9b1ba24986aca4462e870f58 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 10 Mar 2026 10:09:30 +0000 Subject: [PATCH 74/98] rename --- rs/consensus/dkg/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 67a324af3458..9811a6bc420c 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -2142,7 +2142,7 @@ mod tests { /// d not all appear in the same block due to MAX_EARLY_REMOTE_TRANSCRIPTS /// (= 2). Only the transcripts for the first context are included. #[test] - fn test_early_remote_transcripts_max_limits_to_one_context() { + fn test_early_remote_transcripts_respects_max() { for (setup_target_bytes, reshare_target_bytes, desc) in [ ([0u8; 32], [1u8; 32], "SetupInitialDKG first"), ([1u8; 32], [0u8; 32], "ReshareChainKey first"), From 762f6ac4c158921572c83a5b072bd5ac450bf1ba Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 10 Mar 2026 10:35:02 +0000 Subject: [PATCH 75/98] skip context with insufficient dealings test --- rs/consensus/dkg/src/lib.rs | 128 ++++++++++++------ rs/consensus/dkg/src/test_utils.rs | 22 +-- .../artifact_pool/src/consensus_pool.rs | 2 +- 3 files changed, 91 insertions(+), 61 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 9811a6bc420c..8e7bbc837aa8 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -397,10 +397,11 @@ impl BouncerFactory for DkgBouncer { mod tests { use super::*; use crate::test_utils::{ - complement_state_manager_with_both_dkg_contexts, + complement_state_manager_with_dkg_contexts, complement_state_manager_with_reshare_chain_key_request, complement_state_manager_with_setup_initial_dkg_request, create_dealing, extract_dkg_configs_from_highest_block, extract_remote_dkg_ids_from_highest_block, + make_reshare_chain_key_context, make_setup_initial_dkg_context, }; use core::panic; use ic_artifact_pool::dkg_pool::DkgPoolImpl; @@ -2137,15 +2138,18 @@ mod tests { }); } - /// Tests that when the state has both a SetupInitialDKG context (needing 2 - /// transcripts) and a ReshareChainKey context (needing 1 transcript), they - /// d not all appear in the same block due to MAX_EARLY_REMOTE_TRANSCRIPTS - /// (= 2). Only the transcripts for the first context are included. + /// Tests that when the state has a SetupInitialDKG context (needing 2 + /// transcripts), a ReshareChainKey context (needing 1 transcript), and an + /// additional SetupInitialDKG context (lowest target_id) with insufficient + /// dealings for one of its configs: + /// - The first setup target is skipped (insufficient dealings). + /// - The remaining two contexts compete for MAX_EARLY_REMOTE_TRANSCRIPTS + /// (= 2), and only the first context's transcripts are included. #[test] fn test_early_remote_transcripts_respects_max() { for (setup_target_bytes, reshare_target_bytes, desc) in [ - ([0u8; 32], [1u8; 32], "SetupInitialDKG first"), - ([1u8; 32], [0u8; 32], "ReshareChainKey first"), + ([1u8; 32], [2u8; 32], "SetupInitialDKG first"), + ([2u8; 32], [1u8; 32], "ReshareChainKey first"), ] { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let node_ids = (1..4).map(node_test_id).collect::>(); @@ -2153,6 +2157,9 @@ mod tests { curve: VetKdCurve::Bls12_381_G2, name: String::from("some_vetkey"), }; + // This context always comes first but will be + // skipped because one of its two configs lacks dealings. + let skipped_target_id = NiDkgTargetId::new([0u8; 32]); let setup_target_id = NiDkgTargetId::new(setup_target_bytes); let reshare_target_id = NiDkgTargetId::new(reshare_target_bytes); @@ -2177,39 +2184,81 @@ mod tests { )], ); - complement_state_manager_with_both_dkg_contexts( + let registry_version = deps.registry.get_latest_version(); + complement_state_manager_with_dkg_contexts( deps.state_manager.clone(), - deps.registry.get_latest_version(), - key_id.clone(), - vec![10, 11, 12, 13], - setup_target_id, - reshare_target_id, + vec![ + make_setup_initial_dkg_context( + registry_version, + vec![10, 11, 12, 13], + skipped_target_id, + ), + make_setup_initial_dkg_context( + registry_version, + vec![10, 11, 12, 13], + setup_target_id, + ), + make_reshare_chain_key_context( + registry_version, + key_id.clone(), + vec![10, 11, 12, 13], + reshare_target_id, + ), + ], + None, ); // Advance to one block before the summary. deps.pool .advance_round_normal_operation_n(EARLY_DKG_INTERVAL); - // Create the summary block (without inserting it yet). Due to - // MAX_REMOTE_DKGS_PER_INTERVAL=1, only the SetupInitialDKG configs - // are included. + // The original summary only includes configs for the first target + // (skipped_target_id) due to MAX_REMOTE_DKGS_PER_INTERVAL=1. let summary_proposal = deps.pool.make_next_block(); let mut summary_block: Block = summary_proposal.content.into_inner(); let original_summary = summary_block.payload.as_ref().as_summary().dkg.clone(); - let setup_remote_dkg_ids: Vec = original_summary + let skipped_remote_dkg_ids: Vec = original_summary .configs .keys() - .filter(|id| id.target_subnet == NiDkgTargetSubnet::Remote(setup_target_id)) + .filter(|id| id.target_subnet == NiDkgTargetSubnet::Remote(skipped_target_id)) .cloned() .collect(); assert_eq!( - setup_remote_dkg_ids.len(), + skipped_remote_dkg_ids.len(), 2, - "[{desc}] Expected 2 SetupInitialDKG remote configs" + "[{desc}] Expected 2 skipped SetupInitialDKG remote configs" ); - // Manually create a ReshareChainKey config for the second target. + // Create setup configs for setup_target_id by cloning from the + // skipped target's configs (same structure, different target). + let setup_configs: Vec = original_summary + .configs + .values() + .filter(|c| { + c.dkg_id().target_subnet == NiDkgTargetSubnet::Remote(skipped_target_id) + }) + .map(|c| { + NiDkgConfig::new(NiDkgConfigData { + dkg_id: NiDkgId { + target_subnet: NiDkgTargetSubnet::Remote(setup_target_id), + ..c.dkg_id().clone() + }, + max_corrupt_dealers: c.max_corrupt_dealers(), + dealers: c.dealers().get().clone(), + max_corrupt_receivers: c.max_corrupt_receivers(), + receivers: c.receivers().get().clone(), + threshold: c.threshold().get(), + registry_version: c.registry_version(), + resharing_transcript: c.resharing_transcript().clone(), + }) + .unwrap() + }) + .collect(); + let setup_remote_dkg_ids: Vec = + setup_configs.iter().map(|c| c.dkg_id().clone()).collect(); + + // Manually create a ReshareChainKey config. let reshare_dkg_id = NiDkgId { start_block_height: original_summary.height, dealer_subnet: subnet_test_id(0), @@ -2220,7 +2269,7 @@ mod tests { }; let dealers: BTreeSet<_> = node_ids.iter().cloned().collect(); - let receivers: BTreeSet<_> = (10..14).into_iter().map(node_test_id).collect(); + let receivers: BTreeSet<_> = (10..14).map(node_test_id).collect(); let resharing_transcript = dummy_transcript_for_tests_with_params( node_ids.clone(), @@ -2245,16 +2294,15 @@ mod tests { ), dealers, receivers, - registry_version: deps.registry.get_latest_version(), + registry_version, resharing_transcript: Some(resharing_transcript), }) .expect("Failed to create reshare config"); - // Build a modified summary with configs for both targets and insert - // it into the pool. This way the pool's actual summary contains both - // config groups, so dealings for all configs are picked up normally. + // Build a modified summary with configs for all three targets. let mut all_configs: Vec = original_summary.configs.values().cloned().collect(); + all_configs.extend(setup_configs); all_configs.push(reshare_config); let modified_summary = DkgSummary::new( all_configs, @@ -2277,24 +2325,24 @@ mod tests { let proposal = BlockProposal::fake(summary_block, node_test_id(0)); deps.pool.advance_round_with_block(&proposal); - // Add dealings for all 3 remote configs at once, then advance so - // they appear on chain. - let all_remote_dkg_ids: Vec = setup_remote_dkg_ids - .iter() - .cloned() - .chain(std::iter::once(reshare_dkg_id.clone())) - .collect(); - for dkg_id in &all_remote_dkg_ids { + // Add dealings for only ONE of the skipped target's two configs + // (insufficient), all setup target configs, and the reshare config. + let dkg_ids_with_dealings: Vec<&NiDkgId> = + std::iter::once(&skipped_remote_dkg_ids[0]) + .chain(setup_remote_dkg_ids.iter()) + .chain(std::iter::once(&reshare_dkg_id)) + .collect(); + for dkg_id in &dkg_ids_with_dealings { let dealings: Vec<_> = (0..3) - .map(|i| ChangeAction::AddToValidated(create_dealing(i, dkg_id.clone()))) + .map(|i| ChangeAction::AddToValidated(create_dealing(i, (*dkg_id).clone()))) .collect(); deps.dkg_pool.write().unwrap().apply(dealings); } deps.pool.advance_round_normal_operation(); assert_eq!( extract_dealings_from_highest_block(&deps.pool).len(), - 9, - "[{desc}] all 9 dealings should be in the block" + 12, + "[{desc}] all 12 dealings should be in the block" ); assert_eq!( extract_remote_dkgs_from_highest_block(&deps.pool).len(), @@ -2302,9 +2350,9 @@ mod tests { "[{desc}] no early transcripts yet" ); - // Advance once more: the block maker now sees the dealings on - // chain and creates early remote transcripts, constrained by - // MAX_EARLY_REMOTE_TRANSCRIPTS. + // The skipped target is skipped (insufficient dealings for its + // second config). The remaining setup and reshare targets compete + // for MAX_EARLY_REMOTE_TRANSCRIPTS. deps.pool.advance_round_normal_operation(); assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 0); let remote_dkgs = extract_remote_dkgs_from_highest_block(&deps.pool); diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index 724f9abd0384..ae81fc37407d 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -22,7 +22,7 @@ use ic_types::{ }; use std::{collections::BTreeMap, sync::Arc}; -fn make_setup_initial_dkg_context( +pub(super) fn make_setup_initial_dkg_context( registry_version: RegistryVersion, node_ids: Vec, target_id: NiDkgTargetId, @@ -36,7 +36,7 @@ fn make_setup_initial_dkg_context( }) } -fn make_reshare_chain_key_context( +pub(super) fn make_reshare_chain_key_context( registry_version: RegistryVersion, key_id: VetKdKeyId, node_ids: Vec, @@ -106,24 +106,6 @@ pub(super) fn complement_state_manager_with_reshare_chain_key_request( complement_state_manager_with_dkg_contexts(state_manager, contexts, times); } -pub(super) fn complement_state_manager_with_both_dkg_contexts( - state_manager: Arc, - registry_version: RegistryVersion, - key_id: VetKdKeyId, - node_ids: Vec, - setup_target_id: NiDkgTargetId, - reshare_target_id: NiDkgTargetId, -) { - complement_state_manager_with_dkg_contexts( - state_manager, - vec![ - make_setup_initial_dkg_context(registry_version, node_ids.clone(), setup_target_id), - make_reshare_chain_key_context(registry_version, key_id, node_ids, reshare_target_id), - ], - None, - ); -} - /// Extract the remote dkg transcripts from the current highest validated block pub(super) fn extract_remote_dkgs_from_highest_block( pool: &TestConsensusPool, diff --git a/rs/test_utilities/artifact_pool/src/consensus_pool.rs b/rs/test_utilities/artifact_pool/src/consensus_pool.rs index feb338f088d1..fe9f03c2c498 100644 --- a/rs/test_utilities/artifact_pool/src/consensus_pool.rs +++ b/rs/test_utilities/artifact_pool/src/consensus_pool.rs @@ -160,7 +160,7 @@ fn dkg_payload_builder_fn( &*state_manager, validation_context, no_op_logger(), - 10, // at most dealings per block + 20, // at most dealings per block ) .unwrap_or_else(|err| panic!("Couldn't create the payload: {err:?}")) }) From df49ee5fab5a8a73465d3203d22f20287bf5a10c Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 10 Mar 2026 10:50:23 +0000 Subject: [PATCH 76/98] ensure retried transcripts are completed by the summary --- rs/consensus/dkg/src/lib.rs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 8e7bbc837aa8..810281342c5d 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -1836,7 +1836,7 @@ mod tests { #[test] fn test_no_early_transcripts_for_single_setup_initial_dkg_config() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { - let (mut deps, _target_id, remote_dkg_ids) = setup_initial_dkg_test(pool_config); + let (mut deps, target_id, remote_dkg_ids) = setup_initial_dkg_test(pool_config); let original_summary = { let block: Block = deps @@ -1920,6 +1920,32 @@ mod tests { setup_initial_dkg config, but got {early_transcripts:?}", ); + // The next summary should contain both transcripts: the one already + // in the modified summary's transcripts_for_remote_subnets and the + // one created from the remaining config's dealings. + let next_summary = payload_builder::create_summary_payload( + subnet_test_id(0), + deps.registry.as_ref(), + deps.crypto.as_ref(), + &pool_reader, + &modified_summary, + &parent, + deps.registry.get_latest_version(), + deps.state_manager.as_ref(), + &validation_context, + no_op_logger(), + ) + .unwrap(); + assert_eq!( + next_summary.transcripts_for_remote_subnets.len(), + 2, + "The next summary should contain both remote transcripts", + ); + for (dkg_id, _callback_id, result) in &next_summary.transcripts_for_remote_subnets { + assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); + assert!(result.is_ok()); + } + // Control: using the original summary with both configs DOES // produce early transcripts. let early_transcripts = payload_builder::create_early_remote_transcripts( @@ -1933,6 +1959,10 @@ mod tests { ) .unwrap(); assert_eq!(early_transcripts.len(), 2); + for (dkg_id, _callback_id, result) in &early_transcripts { + assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); + assert!(result.is_ok()); + } // If a config exists in the summary but there is no corresponding // context in the state, no early transcript should be created. From 9bc0679b802e8c9bcf8f8c9166228e9ea68d51b3 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 10 Mar 2026 11:03:07 +0000 Subject: [PATCH 77/98] add log --- rs/consensus/dkg/src/payload_builder.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 76761eb7ea7a..de8655916cea 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -195,6 +195,11 @@ pub(crate) fn create_early_remote_transcripts( for (target_id, configs) in remote_configs { // Lookup the callback id and the expected number of configs for this target_id let Some((expected_config_num, callback_id)) = callback_id_map.get(&target_id) else { + warn!( + logger, + "Unable to find callback id associated with remote target id {target_id:?} at block height {}", + parent.height.increment() + ); continue; }; @@ -240,7 +245,7 @@ pub(crate) fn create_early_remote_transcripts( parent.height.increment(), err ); - warn!(logger, "{error_message}"); + error!(logger, "{error_message}"); Err(error_message) } Err(err) => { From a66ee46f8cb779abceb7488fac85906e51171edd Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 10 Mar 2026 13:28:54 +0000 Subject: [PATCH 78/98] review --- rs/consensus/dkg/src/lib.rs | 8 ++++---- rs/consensus/dkg/src/payload_builder.rs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 810281342c5d..b5e20c28477e 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -2177,9 +2177,9 @@ mod tests { /// (= 2), and only the first context's transcripts are included. #[test] fn test_early_remote_transcripts_respects_max() { - for (setup_target_bytes, reshare_target_bytes, desc) in [ - ([1u8; 32], [2u8; 32], "SetupInitialDKG first"), - ([2u8; 32], [1u8; 32], "ReshareChainKey first"), + for (skipped_target_bytes, setup_target_bytes, reshare_target_bytes, desc) in [ + ([0u8; 32], [1u8; 32], [2u8; 32], "SetupInitialDKG first"), + ([0u8; 32], [2u8; 32], [1u8; 32], "ReshareChainKey first"), ] { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let node_ids = (1..4).map(node_test_id).collect::>(); @@ -2189,7 +2189,7 @@ mod tests { }; // This context always comes first but will be // skipped because one of its two configs lacks dealings. - let skipped_target_id = NiDkgTargetId::new([0u8; 32]); + let skipped_target_id = NiDkgTargetId::new(skipped_target_bytes); let setup_target_id = NiDkgTargetId::new(setup_target_bytes); let reshare_target_id = NiDkgTargetId::new(reshare_target_bytes); diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index de8655916cea..740f3c690247 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1007,10 +1007,10 @@ fn eq_sans_height(dkg_id1: &NiDkgId, dkg_id2: &NiDkgId) -> bool { && dkg_id1.target_subnet == dkg_id2.target_subnet } -// Build a map from target id to callback id according to contexts in the replicated state. -// Additionally, for each target ID, return the expected number of DKG instances necessary -// to answer the request. Specifically, setup initial DKG requests require two DKGs, whereas -// resharing a chain key requires on DKG instance. +/// Build a map from target id to callback id according to contexts in the replicated state. +/// Additionally, for each target ID, return the expected number of DKG instances necessary +/// to answer the request. Specifically, setup initial DKG requests require two DKGs, whereas +/// resharing a chain key requires on DKG instance. fn build_target_id_callback_map( state: &ReplicatedState, ) -> BTreeMap { From 68860fbdd8a006e5dd413d14432ddee9db9bd963 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 13 Mar 2026 09:32:34 +0000 Subject: [PATCH 79/98] create configs early --- rs/consensus/dkg/src/lib.rs | 89 ++++++- rs/consensus/dkg/src/payload_builder.rs | 238 ++++++++++++++++--- rs/consensus/dkg/src/payload_validator.rs | 24 +- rs/replica/setup_ic_network/src/lib.rs | 3 + rs/tests/consensus/subnet_recovery/common.rs | 22 +- 5 files changed, 321 insertions(+), 55 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 19958d91edbe..ca1a74d38e60 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -9,14 +9,17 @@ use ic_interfaces::{ p2p::consensus::{Bouncer, BouncerFactory, BouncerValue, PoolMutationsProducer}, validation::ValidationResult, }; +use ic_interfaces_registry::RegistryClient; +use ic_interfaces_state_manager::StateReader; use ic_logger::{ReplicaLogger, error, info}; use ic_metrics::{ MetricsRegistry, buckets::{decimal_buckets, linear_buckets}, }; +use ic_replicated_state::ReplicatedState; use ic_types::{ - Height, NodeId, ReplicaVersion, - consensus::dkg::{DealingContent, DkgMessageId, InvalidDkgPayloadReason, Message}, + Height, NodeId, ReplicaVersion, SubnetId, + consensus::dkg::{DealingContent, DkgMessageId, DkgSummary, InvalidDkgPayloadReason, Message}, crypto::{ Signed, threshold_sig::ni_dkg::{NiDkgId, NiDkgTargetSubnet, config::NiDkgConfig}, @@ -25,7 +28,7 @@ use ic_types::{ use prometheus::Histogram; use rayon::prelude::*; use std::{ - collections::BTreeMap, + collections::{BTreeMap, BTreeSet}, sync::{Arc, Mutex}, }; @@ -33,6 +36,7 @@ pub mod dkg_key_manager; pub mod payload_builder; pub mod payload_validator; +use crate::payload_builder::build_target_id_config_map; pub use crate::utils::get_vetkey_public_keys; #[cfg(test)] @@ -65,6 +69,9 @@ struct Metrics { /// changes in the consensus and DKG pool. pub struct DkgImpl { node_id: NodeId, + subnet_id: SubnetId, + registry_client: Arc, + state_reader: Arc>, crypto: Arc, consensus_cache: Arc, dkg_key_manager: Arc>, @@ -76,6 +83,9 @@ impl DkgImpl { /// Build a new DKG component pub fn new( node_id: NodeId, + subnet_id: SubnetId, + registry_client: Arc, + state_reader: Arc>, crypto: Arc, consensus_cache: Arc, dkg_key_manager: Arc>, @@ -83,9 +93,12 @@ impl DkgImpl { logger: ReplicaLogger, ) -> Self { Self { + node_id, + subnet_id, + registry_client, + state_reader, crypto, consensus_cache, - node_id, dkg_key_manager, logger, metrics: Metrics { @@ -157,7 +170,13 @@ impl DkgImpl { .crypto .sign(&content, self.node_id, config.registry_version()) { - Ok(signature) => Some(ChangeAction::AddToValidated(Signed { content, signature })), + Ok(signature) => { + info!( + self.logger, + "Signed dealing for dkg id {:?}", content.dkg_id + ); + Some(ChangeAction::AddToValidated(Signed { content, signature })) + } Err(err) => { error!(self.logger, "Couldn't sign a DKG dealing: {:?}", err); None @@ -241,7 +260,13 @@ impl DkgImpl { // Verify the dealing and move to validated if it was successful, // reject, if it was rejected, or skip, if there was an error. match crypto_validate_dealing(&*self.crypto, config, message) { - Ok(()) => ChangeAction::MoveToValidated((*message).clone()).into(), + Ok(()) => { + info!( + self.logger, + "Validated dealing for dkg id {:?}", message.content.dkg_id + ); + ChangeAction::MoveToValidated((*message).clone()).into() + } Err(DkgPayloadValidationError::InvalidArtifact(err)) => { get_handle_invalid_change_action( message, @@ -258,6 +283,47 @@ impl DkgImpl { } } } + + fn configs_new( + &self, + start_height: Height, + dkg_summary: &DkgSummary, + ) -> BTreeMap { + let mut summary_configs = dkg_summary.configs.clone(); + let Some(state) = self.state_reader.get_latest_certified_state() else { + return summary_configs; + }; + let map = build_target_id_config_map( + self.subnet_id, + start_height, + self.registry_client.as_ref(), + state.get_ref(), + self.registry_client.get_latest_version(), + dkg_summary.next_transcripts(), + &BTreeSet::new(), + ); + for (_, config_results) in map { + let (mut configs, mut errs) = (vec![], vec![]); + for config_result in config_results { + match config_result { + Ok(config) => configs.push(config), + Err((dkg_id, err)) => errs.push((dkg_id, err)), + } + } + if !errs.is_empty() { + continue; + } + for config in &configs { + if summary_configs.contains_key(&config.dkg_id()) { + continue; + } + } + for config in configs { + summary_configs.insert(config.dkg_id().clone(), config); + } + } + summary_configs + } } /// Validate the signature and dealing of the given message against its config @@ -306,10 +372,13 @@ impl PoolMutationsProducer for DkgImpl { return ChangeAction::Purge(start_height).into(); } - let change_set: Mutations = dkg_summary - .configs + let configs = self.configs_new(start_height, dkg_summary); + // let ids = configs.keys().cloned().collect::>(); + // info!(every_n_seconds => 10, self.logger, "[early remote] Creating/Validating dealings for DKGs: {:?}", ids); + + let change_set: Mutations = configs .par_iter() - .filter_map(|(_id, config)| self.create_dealing(dkg_pool, config)) + .filter_map(|(_, config)| self.create_dealing(dkg_pool, config)) .collect(); if !change_set.is_empty() { return change_set; @@ -336,7 +405,7 @@ impl PoolMutationsProducer for DkgImpl { .map(|dealings| { self.validate_dealings_for_dealer( dkg_pool, - &dkg_summary.configs, + &configs, start_height, dealings.to_vec(), ) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 9d34269373b8..1ad29039879a 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -80,6 +80,8 @@ pub fn create_payload( // If the height is not a start height, create a payload with new dealings, // and possibly early remote transcripts. create_data_payload( + subnet_id, + registry_client, pool_reader, dkg_pool, parent, @@ -95,7 +97,53 @@ pub fn create_payload( } } +pub fn configs_new( + subnet_id: SubnetId, + registry_client: &dyn RegistryClient, + state_manager: &dyn StateManager, + registry_version: RegistryVersion, + start_height: Height, + dkg_summary: &DkgSummary, +) -> BTreeMap { + let mut summary_configs = dkg_summary.configs.clone(); + let Some(state) = state_manager.get_latest_certified_state() else { + return summary_configs; + }; + let map = build_target_id_config_map( + subnet_id, + start_height, + registry_client, + state.get_ref(), + registry_version, + dkg_summary.next_transcripts(), + &BTreeSet::new(), + ); + for (_, config_results) in map { + let (mut configs, mut errs) = (vec![], vec![]); + for config_result in config_results { + match config_result { + Ok(config) => configs.push(config), + Err((dkg_id, err)) => errs.push((dkg_id, err)), + } + } + if !errs.is_empty() { + continue; + } + for config in &configs { + if summary_configs.contains_key(&config.dkg_id()) { + continue; + } + } + for config in configs { + summary_configs.insert(config.dkg_id().clone(), config); + } + } + summary_configs +} + fn create_data_payload( + this_subnet_id: SubnetId, + registry_client: &dyn RegistryClient, pool_reader: &PoolReader<'_>, dkg_pool: Arc>, parent: &Block, @@ -109,19 +157,40 @@ fn create_data_payload( ) -> Result { // Get all existing dealer ids from the chain. let dealers_from_chain = utils::get_dealers_from_chain(pool_reader, parent); + let configs = configs_new( + this_subnet_id, + registry_client, + state_manager, + validation_context.registry_version, + last_summary_block.height, + last_dkg_summary, + ); // Select new dealings for the payload. let pool_lock = dkg_pool .read() .expect("Couldn't lock DKG pool for reading."); let new_validated_dealings = select_dealings_for_payload( - &last_dkg_summary.configs, + &configs, &dealers_from_chain, &*pool_lock, max_dealings_per_block, ); drop(pool_lock); + for d in &new_validated_dealings { + if matches!(d.content.dkg_id.target_subnet, NiDkgTargetSubnet::Remote(_)) { + info!( + logger, + "Including remote dealing for dkg id {:?} in data block payload at height {}", + d.content.dkg_id, + parent.height.increment(), + ); + } + } let remote_dkg_transcripts = create_early_remote_transcripts( + this_subnet_id, + last_summary_block.height, + registry_client, pool_reader, crypto, parent, @@ -207,6 +276,9 @@ fn select_dealings_for_payload( #[allow(clippy::type_complexity)] pub(crate) fn create_early_remote_transcripts( + this_subnet_id: SubnetId, + start_block_height: Height, + registry_client: &dyn RegistryClient, pool_reader: &PoolReader<'_>, crypto: &dyn ConsensusCrypto, parent: &Block, @@ -220,58 +292,60 @@ pub(crate) fn create_early_remote_transcripts( .get_state_at(validation_context.certified_height) .map_err(DkgPayloadCreationError::StateManagerError)?; - // Since this function is relatively expensive, we simply return if there are no outstanding DKG contexts - let callback_id_map = build_target_id_callback_map(state.get_ref()); - if callback_id_map.is_empty() { - return Ok(vec![]); - } - // Get all dealings for DKGs that have not been completed yet let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent); + let completed_target_ids = + get_completed_target_ids(last_dkg_summary.configs.keys(), &completed); - // Collect map of remote target_ids to DKG configs - let mut remote_configs: BTreeMap> = BTreeMap::new(); - for config in last_dkg_summary.configs.values() { - let dkg_id = config.dkg_id(); - if completed.contains(dkg_id) { - // Skip DKGs that have already been completed - continue; - } - if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { - remote_configs.entry(target_id).or_default().push(config); - } + // Since this function is relatively expensive, we simply return if there are no outstanding DKG contexts + let callback_id_map = build_target_id_config_map( + this_subnet_id, + start_block_height, + registry_client, + state.get_ref(), + validation_context.registry_version, + last_dkg_summary.next_transcripts(), + &completed_target_ids, + ); + if callback_id_map.is_empty() { + return Ok(vec![]); } // Try to create transcripts for all configs of each target_id. Note that we either include // all transcript results for a target_id or none of them. let mut selected_transcripts = vec![]; - for (target_id, configs) in remote_configs { - // Lookup the callback id and the expected number of configs for this target_id - let Some((expected_config_num, callback_id)) = callback_id_map.get(&target_id) else { - warn!( - logger, - "Unable to find callback id associated with remote target id {target_id:?} at block height {}", - parent.height.increment() - ); + for (callback_id, config_results) in callback_id_map.into_iter() { + // Ensure that creating these transcripts would not exceed the maximum number of early + // remote transcripts. We continue with the next target_id in case it requires less + // transcripts. + if selected_transcripts.len() + config_results.len() > MAX_EARLY_REMOTE_TRANSCRIPTS { continue; - }; + } - // Check that we have the expected number of configs for this target_id - if configs.len() != *expected_config_num { - // This may happen if we did not manage to create all required transcripts as part of - // the last summary block. We will handle this in the next summary block instead. - continue; + let (mut configs, mut errs) = (vec![], vec![]); + for config_result in config_results { + match config_result { + Ok(config) => configs.push(config), + Err((dkg_id, err)) => errs.push((dkg_id, err)), + } } - // Ensure that creating these transcripts would not exceed the maximum number of early - // remote transcripts. We continue with the next target_id in case it requires less - // transcripts. - if selected_transcripts.len() + configs.len() > MAX_EARLY_REMOTE_TRANSCRIPTS { + if !errs.is_empty() { + for (dkg_id, err) in errs { + error!( + logger, + "Failed to create early remote transcript for dkg id {:?} at height {}: {}", + dkg_id, + parent.height.increment(), + err + ); + selected_transcripts.push((dkg_id, callback_id, Err(err))); + } continue; } // If any of the configs has less dealings than the threshold, we skip this target_id - if configs.iter().any(|config| { + if configs.iter().any(|config: &NiDkgConfig| { let dealings_count = all_dealings .get(config.dkg_id()) .map_or(0, |dealings| dealings.len()); @@ -306,7 +380,7 @@ pub(crate) fn create_early_remote_transcripts( return Err(DkgPayloadCreationError::DkgCreateTranscriptError(err)); } }; - selected_transcripts.push((config.dkg_id().clone(), *callback_id, transcript_result)); + selected_transcripts.push((config.dkg_id().clone(), callback_id, transcript_result)); } } @@ -1081,6 +1155,94 @@ fn build_target_id_callback_map( .collect() } +pub fn build_target_id_config_map( + dealer_subnet: SubnetId, + start_block_height: Height, + registry_client: &dyn RegistryClient, + state: &ReplicatedState, + registry_version: RegistryVersion, + reshared_transcripts: &BTreeMap, + completed_target_ids: &BTreeSet, +) -> BTreeMap>> { + let call_contexts = &state.metadata.subnet_call_context_manager; + let setup_initial_dkg_configs = call_contexts + .setup_initial_dkg_contexts + .iter() + .filter(|(_, context)| { + context.registry_version <= registry_version + && !completed_target_ids.contains(&context.target_id) + }) + .filter_map(|(&callback_id, context)| { + let dealers = + get_node_list(dealer_subnet, registry_client, context.registry_version).ok()?; + let low_thr_dkg_id = NiDkgId { + start_block_height, + dealer_subnet, + dkg_tag: NiDkgTag::LowThreshold, + target_subnet: NiDkgTargetSubnet::Remote(context.target_id), + }; + let high_thr_dkg_id = NiDkgId { + start_block_height, + dealer_subnet, + dkg_tag: NiDkgTag::HighThreshold, + target_subnet: NiDkgTargetSubnet::Remote(context.target_id), + }; + let low_thr_config = create_remote_dkg_config( + low_thr_dkg_id.clone(), + &dealers, + &context.nodes_in_target_subnet, + &context.registry_version, + None, + ) + .map_err(|err| (low_thr_dkg_id, err.to_string())); + let high_thr_config = create_remote_dkg_config( + high_thr_dkg_id.clone(), + &dealers, + &context.nodes_in_target_subnet, + &context.registry_version, + None, + ) + .map_err(|err| (high_thr_dkg_id, err.to_string())); + + Some((callback_id, vec![low_thr_config, high_thr_config])) + }); + + // rehsare chain key contexts + let reshare_chain_key_configs = call_contexts + .reshare_chain_key_contexts + .iter() + .filter(|(_, context)| { + context.registry_version <= registry_version + && !completed_target_ids.contains(&context.target_id) + }) + .filter_map(|(&callback_id, context)| { + let key_id = NiDkgMasterPublicKeyId::try_from(context.key_id.clone()).ok()?; + let tag = NiDkgTag::HighThresholdForKey(key_id); + let reshared_transcript = reshared_transcripts.get(&tag)?; + let dealers = + get_node_list(dealer_subnet, registry_client, context.registry_version).ok()?; + let dkg_id = NiDkgId { + start_block_height, + dealer_subnet, + dkg_tag: tag, + target_subnet: NiDkgTargetSubnet::Remote(context.target_id), + }; + let config = create_remote_dkg_config( + dkg_id.clone(), + &dealers, + &context.nodes, + &context.registry_version, + Some(reshared_transcript.clone()), + ) + .map_err(|err| (dkg_id, err.to_string())); + Some((callback_id, vec![config])) + }); + + setup_initial_dkg_configs + .chain(reshare_chain_key_configs) + .collect() +} + fn add_callback_ids_to_transcript_results( new_transcripts: BTreeMap>, state: &ReplicatedState, diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index 5b0807c5d387..e85937997043 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -1,3 +1,5 @@ +use crate::payload_builder::configs_new; + use self::payload_builder::create_early_remote_transcripts; use super::{crypto_validate_dealing, payload_builder, utils}; use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; @@ -11,7 +13,7 @@ use ic_logger::{ReplicaLogger, info, warn}; use ic_registry_client_helpers::subnet::SubnetRegistry; use ic_replicated_state::ReplicatedState; use ic_types::{ - SubnetId, + Height, SubnetId, batch::ValidationContext, consensus::{ Block, BlockPayload, @@ -117,6 +119,9 @@ pub fn validate_payload( }); validate_dealings_payload( + subnet_id, + last_summary_block.height, + registry_client, crypto, pool_reader, dkg_pool, @@ -136,6 +141,9 @@ pub fn validate_payload( // Validates the payload containing dealings. #[allow(clippy::result_large_err)] fn validate_dealings_payload( + subnet_id: SubnetId, + start_block_height: Height, + registry_client: &dyn RegistryClient, crypto: &dyn ConsensusCrypto, pool_reader: &PoolReader<'_>, dkg_pool: &dyn DkgPool, @@ -181,6 +189,15 @@ fn validate_dealings_payload( return Err(InvalidDkgPayloadReason::DealerAlreadyDealt(dealer_id).into()); } + let configs = configs_new( + subnet_id, + registry_client, + state_manager, + validation_context.registry_version, + start_block_height, + last_summary, + ); + // Check that all messages have a valid DKG config from the summary and the // dealer is valid, then verify each dealing. for message in &dealings.messages { @@ -192,7 +209,7 @@ fn validate_dealings_payload( continue; } - let Some(config) = last_summary.configs.get(&message.content.dkg_id) else { + let Some(config) = configs.get(&message.content.dkg_id) else { return Err(InvalidDkgPayloadReason::MissingDkgConfigForDealing.into()); }; @@ -203,6 +220,9 @@ fn validate_dealings_payload( // If we have early transcripts, we compare them if !dealings.transcripts_for_remote_subnets.is_empty() { let expected_transcripts = create_early_remote_transcripts( + subnet_id, + start_block_height, + registry_client, pool_reader, crypto, parent, diff --git a/rs/replica/setup_ic_network/src/lib.rs b/rs/replica/setup_ic_network/src/lib.rs index 7533bafd7cf9..a77d1c29a308 100644 --- a/rs/replica/setup_ic_network/src/lib.rs +++ b/rs/replica/setup_ic_network/src/lib.rs @@ -614,6 +614,9 @@ fn start_consensus( abortable_broadcast_channels.dkg, ic_consensus_dkg::DkgImpl::new( node_id, + subnet_id, + Arc::clone(®istry_client), + Arc::clone(&state_manager) as Arc<_>, Arc::clone(&consensus_crypto), Arc::clone(&consensus_pool_cache), dkg_key_manager, diff --git a/rs/tests/consensus/subnet_recovery/common.rs b/rs/tests/consensus/subnet_recovery/common.rs index 6c2e1b23d984..6fce97630145 100644 --- a/rs/tests/consensus/subnet_recovery/common.rs +++ b/rs/tests/consensus/subnet_recovery/common.rs @@ -38,7 +38,10 @@ use anyhow::bail; use canister_test::Canister; use ic_base_types::NodeId; use ic_consensus_system_test_utils::{ - node::{assert_node_is_assigned_with_ssh_session, assert_node_is_unassigned_with_ssh_session}, + node::{ + assert_node_is_assigned_with_ssh_session, assert_node_is_unassigned_with_ssh_session, + await_node_certified_height, + }, rw_message::{install_nns_and_check_progress, store_message}, ssh_access::{disable_ssh_access_to_node, wait_until_authentication_is_granted}, subnet::{ @@ -85,7 +88,7 @@ const NNS_NODES_LARGE: usize = 40; const APP_NODES_LARGE: usize = 37; /// 40 dealings * 4 transcripts being reshared (high/local, low/local, high/remote, low/remote) /// plus 14 as a safety margin -const DKG_INTERVAL_LARGE: u64 = 4 * NNS_NODES_LARGE as u64 + 14; +const DKG_INTERVAL_LARGE: u64 = 499; /// A very large DKG interval to test recovery when the subnet stalls during its first DKG /// interval. @@ -417,12 +420,21 @@ fn app_subnet_recovery_test(env: TestEnv, cfg: TestConfig) { )); // The first application subnet encountered during iteration is the source subnet because it was inserted first. - let source_subnet_id = env + let source_subnet = env .topology_snapshot() .subnets() .find(|subnet| subnet.subnet_type() == SubnetType::Application) - .expect("there is no source subnet") - .subnet_id; + .expect("there is no source subnet"); + + let source_subnet_id = source_subnet.subnet_id; + await_node_certified_height( + &source_subnet + .nodes() + .next() + .expect("there is no node in the source subnet"), + Height::from(1000), + logger.clone(), + ); let create_new_subnet = !topology_snapshot .subnets() From c5821b91ec393e44a1e6f3ca222f26b4813430e6 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 13 Mar 2026 10:40:58 +0000 Subject: [PATCH 80/98] rename --- rs/consensus/dkg/src/lib.rs | 94 +++++++++++++---------- rs/consensus/dkg/src/payload_builder.rs | 48 +----------- rs/consensus/dkg/src/payload_validator.rs | 4 +- 3 files changed, 56 insertions(+), 90 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index ca1a74d38e60..d5f19a779b04 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -283,47 +283,6 @@ impl DkgImpl { } } } - - fn configs_new( - &self, - start_height: Height, - dkg_summary: &DkgSummary, - ) -> BTreeMap { - let mut summary_configs = dkg_summary.configs.clone(); - let Some(state) = self.state_reader.get_latest_certified_state() else { - return summary_configs; - }; - let map = build_target_id_config_map( - self.subnet_id, - start_height, - self.registry_client.as_ref(), - state.get_ref(), - self.registry_client.get_latest_version(), - dkg_summary.next_transcripts(), - &BTreeSet::new(), - ); - for (_, config_results) in map { - let (mut configs, mut errs) = (vec![], vec![]); - for config_result in config_results { - match config_result { - Ok(config) => configs.push(config), - Err((dkg_id, err)) => errs.push((dkg_id, err)), - } - } - if !errs.is_empty() { - continue; - } - for config in &configs { - if summary_configs.contains_key(&config.dkg_id()) { - continue; - } - } - for config in configs { - summary_configs.insert(config.dkg_id().clone(), config); - } - } - summary_configs - } } /// Validate the signature and dealing of the given message against its config @@ -357,6 +316,50 @@ fn get_handle_invalid_change_action>(message: &Message, reason: T) ChangeAction::HandleInvalid(DkgMessageId::from(message), reason.as_ref().to_string()) } +pub(crate) fn get_configs_for_start_height( + subnet_id: SubnetId, + registry_client: &dyn RegistryClient, + state_manager: &dyn StateManager, + registry_version: RegistryVersion, + start_height: Height, + dkg_summary: &DkgSummary, +) -> BTreeMap { + let mut summary_configs = dkg_summary.configs.clone(); + let Some(state) = state_manager.get_latest_certified_state() else { + return summary_configs; + }; + let map = build_target_id_config_map( + subnet_id, + start_height, + registry_client, + state.get_ref(), + registry_version, + dkg_summary.next_transcripts(), + &BTreeSet::new(), + ); + for (_, config_results) in map { + let (mut configs, mut errs) = (vec![], vec![]); + for config_result in config_results { + match config_result { + Ok(config) => configs.push(config), + Err((dkg_id, err)) => errs.push((dkg_id, err)), + } + } + if !errs.is_empty() { + continue; + } + for config in &configs { + if summary_configs.contains_key(&config.dkg_id()) { + continue; + } + } + for config in configs { + summary_configs.insert(config.dkg_id().clone(), config); + } + } + summary_configs +} + impl PoolMutationsProducer for DkgImpl { type Mutations = Mutations; @@ -372,7 +375,14 @@ impl PoolMutationsProducer for DkgImpl { return ChangeAction::Purge(start_height).into(); } - let configs = self.configs_new(start_height, dkg_summary); + let configs = get_configs_for_start_height( + self.subnet_id, + self.registry_client.as_ref(), + self.state_reader.as_ref(), + self.registry_client.get_latest_version(), + start_height, + dkg_summary, + ); // let ids = configs.keys().cloned().collect::>(); // info!(every_n_seconds => 10, self.logger, "[early remote] Creating/Validating dealings for DKGs: {:?}", ids); diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 1ad29039879a..2a2ab147ab0f 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1,6 +1,6 @@ use crate::{ MAX_EARLY_REMOTE_TRANSCRIPTS, MAX_REMOTE_DKG_ATTEMPTS, MAX_REMOTE_DKGS_PER_INTERVAL, - REMOTE_DKG_REPEATED_FAILURE_ERROR, + REMOTE_DKG_REPEATED_FAILURE_ERROR, get_configs_for_start_height, utils::{self, tags_iter, vetkd_key_ids_for_subnet}, }; use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; @@ -97,50 +97,6 @@ pub fn create_payload( } } -pub fn configs_new( - subnet_id: SubnetId, - registry_client: &dyn RegistryClient, - state_manager: &dyn StateManager, - registry_version: RegistryVersion, - start_height: Height, - dkg_summary: &DkgSummary, -) -> BTreeMap { - let mut summary_configs = dkg_summary.configs.clone(); - let Some(state) = state_manager.get_latest_certified_state() else { - return summary_configs; - }; - let map = build_target_id_config_map( - subnet_id, - start_height, - registry_client, - state.get_ref(), - registry_version, - dkg_summary.next_transcripts(), - &BTreeSet::new(), - ); - for (_, config_results) in map { - let (mut configs, mut errs) = (vec![], vec![]); - for config_result in config_results { - match config_result { - Ok(config) => configs.push(config), - Err((dkg_id, err)) => errs.push((dkg_id, err)), - } - } - if !errs.is_empty() { - continue; - } - for config in &configs { - if summary_configs.contains_key(&config.dkg_id()) { - continue; - } - } - for config in configs { - summary_configs.insert(config.dkg_id().clone(), config); - } - } - summary_configs -} - fn create_data_payload( this_subnet_id: SubnetId, registry_client: &dyn RegistryClient, @@ -157,7 +113,7 @@ fn create_data_payload( ) -> Result { // Get all existing dealer ids from the chain. let dealers_from_chain = utils::get_dealers_from_chain(pool_reader, parent); - let configs = configs_new( + let configs = get_configs_for_start_height( this_subnet_id, registry_client, state_manager, diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index e85937997043..ee451c911530 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -1,4 +1,4 @@ -use crate::payload_builder::configs_new; +use crate::get_configs_for_start_height; use self::payload_builder::create_early_remote_transcripts; use super::{crypto_validate_dealing, payload_builder, utils}; @@ -189,7 +189,7 @@ fn validate_dealings_payload( return Err(InvalidDkgPayloadReason::DealerAlreadyDealt(dealer_id).into()); } - let configs = configs_new( + let configs = get_configs_for_start_height( subnet_id, registry_client, state_manager, From a14982896db199d5d7c448c4928ea3c37415f14a Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 13 Mar 2026 10:46:59 +0000 Subject: [PATCH 81/98] rm --- rs/consensus/dkg/src/lib.rs | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index d5f19a779b04..3a9821878279 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -170,13 +170,7 @@ impl DkgImpl { .crypto .sign(&content, self.node_id, config.registry_version()) { - Ok(signature) => { - info!( - self.logger, - "Signed dealing for dkg id {:?}", content.dkg_id - ); - Some(ChangeAction::AddToValidated(Signed { content, signature })) - } + Ok(signature) => Some(ChangeAction::AddToValidated(Signed { content, signature })), Err(err) => { error!(self.logger, "Couldn't sign a DKG dealing: {:?}", err); None @@ -260,13 +254,7 @@ impl DkgImpl { // Verify the dealing and move to validated if it was successful, // reject, if it was rejected, or skip, if there was an error. match crypto_validate_dealing(&*self.crypto, config, message) { - Ok(()) => { - info!( - self.logger, - "Validated dealing for dkg id {:?}", message.content.dkg_id - ); - ChangeAction::MoveToValidated((*message).clone()).into() - } + Ok(()) => ChangeAction::MoveToValidated((*message).clone()).into(), Err(DkgPayloadValidationError::InvalidArtifact(err)) => { get_handle_invalid_change_action( message, @@ -383,12 +371,9 @@ impl PoolMutationsProducer for DkgImpl { start_height, dkg_summary, ); - // let ids = configs.keys().cloned().collect::>(); - // info!(every_n_seconds => 10, self.logger, "[early remote] Creating/Validating dealings for DKGs: {:?}", ids); - let change_set: Mutations = configs .par_iter() - .filter_map(|(_, config)| self.create_dealing(dkg_pool, config)) + .filter_map(|(_id, config)| self.create_dealing(dkg_pool, config)) .collect(); if !change_set.is_empty() { return change_set; From a59dd90300016879e87098e21e3271f7ed7cc6bc Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 26 Mar 2026 12:45:19 +0000 Subject: [PATCH 82/98] test --- rs/consensus/dkg/src/lib.rs | 8 +- rs/tests/consensus/BUILD.bazel | 24 ++ rs/tests/consensus/Cargo.toml | 4 + rs/tests/consensus/nidkg_performance_test.rs | 207 ++++++++++++++++++ .../consensus/subnet_recovery/BUILD.bazel | 4 +- rs/tests/consensus/subnet_recovery/Cargo.toml | 2 +- ...p_large_no_upgrade_with_chain_keys_test.rs | 19 -- .../consensus/subnet_recovery/sr_large.rs | 39 ++++ 8 files changed, 282 insertions(+), 25 deletions(-) create mode 100644 rs/tests/consensus/nidkg_performance_test.rs delete mode 100644 rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs create mode 100644 rs/tests/consensus/subnet_recovery/sr_large.rs diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 3a9821878279..47ec7aaf06c0 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -18,7 +18,7 @@ use ic_metrics::{ }; use ic_replicated_state::ReplicatedState; use ic_types::{ - Height, NodeId, ReplicaVersion, SubnetId, + Height, NodeId, RegistryVersion, ReplicaVersion, SubnetId, consensus::dkg::{DealingContent, DkgMessageId, DkgSummary, InvalidDkgPayloadReason, Message}, crypto::{ Signed, @@ -47,11 +47,11 @@ pub use dkg_key_manager::DkgKeyManager; pub use payload_builder::{create_payload, get_dkg_summary_from_cup_contents}; /// The maximal number of DKGs for other subnets we want to run in one interval. -const MAX_REMOTE_DKGS_PER_INTERVAL: usize = 1; +const MAX_REMOTE_DKGS_PER_INTERVAL: usize = 10; /// The maximum number of early remote DKG transcripts we want to include in a data payload. /// Note that responses for `SetupInitialDKG` requests contain two transcripts. -const MAX_EARLY_REMOTE_TRANSCRIPTS: usize = 2; +const MAX_EARLY_REMOTE_TRANSCRIPTS: usize = 4; /// The maximum number of intervals during which an initial DKG request is /// attempted. @@ -307,7 +307,7 @@ fn get_handle_invalid_change_action>(message: &Message, reason: T) pub(crate) fn get_configs_for_start_height( subnet_id: SubnetId, registry_client: &dyn RegistryClient, - state_manager: &dyn StateManager, + state_manager: &dyn StateReader, registry_version: RegistryVersion, start_height: Height, dkg_summary: &DkgSummary, diff --git a/rs/tests/consensus/BUILD.bazel b/rs/tests/consensus/BUILD.bazel index 43b448b20668..12af1a36b3ce 100644 --- a/rs/tests/consensus/BUILD.bazel +++ b/rs/tests/consensus/BUILD.bazel @@ -364,6 +364,30 @@ system_test_nns( ], ) +system_test_nns( + name = "nidkg_performance_test", + additional_colocate_tags = [ + "system_test_benchmark", + ], + enable_head_nns_variant = False, + enable_metrics = True, + prometheus_vm_required_host_features = ["performance"], + tags = [ + "colocate", + "manual", + ], + test_timeout = "eternal", + deps = [ + # Keep sorted. + "//rs/registry/canister", + "//rs/registry/subnet_type", + "//rs/tests/driver:ic-system-test-driver", + "@crate_index//:anyhow", + "@crate_index//:futures", + "@crate_index//:slog", + ], +) + system_test_nns( name = "adding_nodes_to_subnet_test", tags = [ diff --git a/rs/tests/consensus/Cargo.toml b/rs/tests/consensus/Cargo.toml index 3d401dc59b4a..fe1a558e772e 100644 --- a/rs/tests/consensus/Cargo.toml +++ b/rs/tests/consensus/Cargo.toml @@ -107,5 +107,9 @@ path = "catch_up_loop_prevention_test.rs" name = "ic-systest-subnet-splitting" path = "subnet_splitting_test.rs" +[[bin]] +name = "ic-systest-nidkg-performance-test" +path = "nidkg_performance_test.rs" + [features] upload_perf_systest_results = [] diff --git a/rs/tests/consensus/nidkg_performance_test.rs b/rs/tests/consensus/nidkg_performance_test.rs new file mode 100644 index 000000000000..830e5f4b28df --- /dev/null +++ b/rs/tests/consensus/nidkg_performance_test.rs @@ -0,0 +1,207 @@ +use anyhow::Result; +use std::time::{Duration, Instant}; + +use futures::future::join_all; +use ic_registry_subnet_type::SubnetType; +use ic_system_test_driver::driver::group::SystemTestGroup; +use ic_system_test_driver::driver::test_env_api::HasPublicApiUrl; +use ic_system_test_driver::driver::{ + farm::HostFeature, + ic::{AmountOfMemoryKiB, ImageSizeGiB, InternetComputer, NrOfVCPUs, Subnet, VmResources}, + prometheus_vm::HasPrometheus, + test_env::TestEnv, + test_env_api::{HasTopologySnapshot, IcNodeContainer, NnsInstallationBuilder, SshSession}, +}; +use ic_system_test_driver::nns::{ + await_proposal_execution, get_software_version_from_snapshot, + submit_create_application_subnet_proposal, vote_on_proposal, +}; +use ic_system_test_driver::systest; +use ic_system_test_driver::util::{block_on, runtime_from_url}; +use registry_canister::mutations::do_create_subnet::CanisterCyclesCostSchedule; +use slog::info; + +const NNS_NODES_COUNT: usize = 40; +const DEFAULT_UNASSIGNED_NODES_COUNT: usize = 10; +const DEFAULT_NNS_DKG_DEALINGS_PER_BLOCK: usize = 10; + +fn setup(env: TestEnv) { + let unassigned_nodes_count = std::env::var("UNASSIGNED_NODES_COUNT") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_UNASSIGNED_NODES_COUNT); + let nns_dkg_dealings_per_block = std::env::var("NNS_DKG_DEALINGS_PER_BLOCK") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_NNS_DKG_DEALINGS_PER_BLOCK); + + let vm_resources = VmResources { + vcpus: Some(NrOfVCPUs::new(64)), + memory_kibibytes: Some(AmountOfMemoryKiB::new(512_142_680)), + boot_image_minimal_size_gibibytes: Some(ImageSizeGiB::new(500)), + }; + + info!( + env.logger(), + "Deploying nIDKG performance testnet with {} system-subnet nodes, {} unassigned nodes, and {} DKG dealings/block", + NNS_NODES_COUNT, + unassigned_nodes_count, + nns_dkg_dealings_per_block + ); + + let mut nns_subnet = Subnet::new(SubnetType::System) + .with_required_host_features(vec![HostFeature::Performance]) + .with_default_vm_resources(vm_resources) + .add_nodes(NNS_NODES_COUNT); + nns_subnet.dkg_dealings_per_block = Some(nns_dkg_dealings_per_block); + + InternetComputer::new() + .add_subnet(nns_subnet) + .with_unassigned_nodes(unassigned_nodes_count) + .setup_and_start(&env) + .expect("Failed to setup IC under test"); + + let topology_snapshot = env.topology_snapshot(); + topology_snapshot + .subnets() + .flat_map(|subnet| subnet.nodes()) + .for_each(|node| node.await_status_is_healthy().unwrap()); + topology_snapshot + .unassigned_nodes() + .for_each(|node| node.await_can_login_as_admin_via_ssh().unwrap()); +} + +fn test(env: TestEnv) { + let log = env.logger(); + let topology_snapshot = env.topology_snapshot(); + let nns_node = topology_snapshot.root_subnet().nodes().next().unwrap(); + + let unassigned_node_ids = topology_snapshot + .unassigned_nodes() + .map(|node| node.node_id) + .collect::>(); + assert!( + !unassigned_node_ids.is_empty(), + "Expected at least one unassigned node to create single-node subnets" + ); + + info!(log, "Installing NNS canisters"); + NnsInstallationBuilder::new() + .install(&nns_node, &env) + .expect("Could not install NNS canisters"); + + let nns_runtime = runtime_from_url(nns_node.get_public_url(), nns_node.effective_canister_id()); + let governance = ic_system_test_driver::nns::get_governance_canister(&nns_runtime); + let version = block_on(get_software_version_from_snapshot(&nns_node)) + .expect("Could not obtain replica software version from the NNS node"); + + info!( + log, + "Starting {} create-subnet proposals (one unassigned node per subnet)", + unassigned_node_ids.len() + ); + let submit_start = Instant::now(); + let mut proposal_ids = Vec::with_capacity(unassigned_node_ids.len()); + for (idx, node_id) in unassigned_node_ids.iter().cloned().enumerate() { + info!( + log, + "Submitting create-subnet proposal {}/{} for node {}", + idx + 1, + unassigned_node_ids.len(), + node_id + ); + let proposal_id = block_on(submit_create_application_subnet_proposal( + &governance, + vec![node_id], + version.clone(), + Some(CanisterCyclesCostSchedule::Normal), + )); + proposal_ids.push(proposal_id); + } + let submit_elapsed = submit_start.elapsed(); + + info!( + log, + "Submitted all {} proposals in {:.2}s; voting and awaiting execution in parallel", + proposal_ids.len(), + submit_elapsed.as_secs_f64() + ); + let execution_results = block_on(async { + join_all(proposal_ids.into_iter().map(|proposal_id| { + let governance = governance.clone(); + let log = log.clone(); + async move { + vote_on_proposal(&governance, proposal_id).await; + let start = Instant::now(); + let is_executed = await_proposal_execution( + &log, + &governance, + proposal_id, + Duration::from_secs(1), + Duration::from_secs(2 * 60), + ) + .await; + assert!(is_executed, "proposal {proposal_id} did not execute in time"); + (proposal_id, start.elapsed()) + } + })) + .await + }); + + let count = execution_results.len(); + let mut min_secs = f64::INFINITY; + let mut max_secs = 0.0_f64; + let mut sum_secs = 0.0_f64; + for (proposal_id, elapsed) in execution_results { + let secs = elapsed.as_secs_f64(); + min_secs = min_secs.min(secs); + max_secs = max_secs.max(secs); + sum_secs += secs; + info!(log, "Proposal {proposal_id} executed {:.2}s after voting", secs); + } + let avg_secs = sum_secs / count as f64; + + info!( + log, + "Execution latency after voting for {} proposals: min {:.2}s, avg {:.2}s, max {:.2}s", + count, + min_secs, + avg_secs, + max_secs + ); + env.emit_report(format!( + "nIDKG performance (vote->execute): proposals={} min={:.2}s avg={:.2}s max={:.2}s", + count, + min_secs, + avg_secs, + max_secs + )); +} + +fn teardown(env: TestEnv) { + let should_download_prometheus_data = + std::env::var("DOWNLOAD_P8S_DATA").is_ok_and(|v| v == "true" || v == "1"); + if should_download_prometheus_data { + env.download_prometheus_data_dir_if_exists(); + env.emit_report(String::from( + "Downloaded prometheus data to 'prometheus-data-dir.tar.zst' in the test output \ + directory. You can now use `rs/tests/run-p8s.sh` script to play with the metrics", + )); + } else { + env.emit_report(String::from( + "Not downloading the prometheus data. \ + If you want to download it on the next test run, \ + please pass `--test_env DOWNLOAD_P8S_DATA=1` as an argument to the `ict` command", + )); + } +} + +fn main() -> Result<()> { + SystemTestGroup::new() + .with_setup(setup) + .with_timeout_per_test(Duration::from_secs(120 * 60)) + .add_test(systest!(test)) + .with_teardown(teardown) + .execute_from_args()?; + Ok(()) +} diff --git a/rs/tests/consensus/subnet_recovery/BUILD.bazel b/rs/tests/consensus/subnet_recovery/BUILD.bazel index fcffac1394cf..32f9fc263e55 100644 --- a/rs/tests/consensus/subnet_recovery/BUILD.bazel +++ b/rs/tests/consensus/subnet_recovery/BUILD.bazel @@ -228,9 +228,11 @@ system_test_nns( ) system_test_nns( - name = "sr_app_large_no_upgrade_with_chain_keys_test", + name = "sr_large", enable_head_nns_variant = False, # Let's not run this expensive test against the HEAD NNS canisters to save resources. + enable_metrics = True, guestos_update = "test", + prometheus_vm_required_host_features = ["performance"], tags = [ "colocate", "subnet_recovery", diff --git a/rs/tests/consensus/subnet_recovery/Cargo.toml b/rs/tests/consensus/subnet_recovery/Cargo.toml index 3cd472307f38..e0115dad8b74 100644 --- a/rs/tests/consensus/subnet_recovery/Cargo.toml +++ b/rs/tests/consensus/subnet_recovery/Cargo.toml @@ -83,4 +83,4 @@ path = "sr_app_no_upgrade_with_chain_keys_test.rs" [[bin]] name = "ic-systest-subnet-recovery-app-large-no-upgrade-with-chain-keys" -path = "sr_app_large_no_upgrade_with_chain_keys_test.rs" +path = "sr_large.rs" diff --git a/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs b/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs deleted file mode 100644 index 2ed9c451bc38..000000000000 --- a/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs +++ /dev/null @@ -1,19 +0,0 @@ -use anyhow::Result; -use std::time::Duration; - -use ic_consensus_system_test_subnet_recovery::common::{ - setup_large_chain_keys as setup, test_large_no_upgrade_with_chain_keys as test, -}; -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::systest; - -fn main() -> Result<()> { - SystemTestGroup::new() - .with_setup(setup) - .with_overall_timeout(Duration::from_secs(55 * 60)) - .with_timeout_per_test(Duration::from_secs(50 * 60)) - .without_assert_no_replica_restarts() - .add_test(systest!(test)) - .execute_from_args()?; - Ok(()) -} diff --git a/rs/tests/consensus/subnet_recovery/sr_large.rs b/rs/tests/consensus/subnet_recovery/sr_large.rs new file mode 100644 index 000000000000..0d8683dd5164 --- /dev/null +++ b/rs/tests/consensus/subnet_recovery/sr_large.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use std::time::Duration; + +use ic_consensus_system_test_subnet_recovery::common::{ + setup_large_chain_keys as setup, test_large_no_upgrade_with_chain_keys as test, +}; +use ic_system_test_driver::driver::group::SystemTestGroup; +use ic_system_test_driver::driver::{prometheus_vm::HasPrometheus, test_env::TestEnv}; +use ic_system_test_driver::systest; + +fn teardown(env: TestEnv) { + let should_download_prometheus_data = + std::env::var("DOWNLOAD_P8S_DATA").is_ok_and(|v| v == "true" || v == "1"); + if should_download_prometheus_data { + env.download_prometheus_data_dir_if_exists(); + env.emit_report(String::from( + "Downloaded prometheus data to 'prometheus-data-dir.tar.zst' in the test output \ + directory. You can now use `rs/tests/run-p8s.sh` script to play with the metrics", + )); + } else { + env.emit_report(String::from( + "Not downloading the prometheus data. \ + If you want to download it on the next test run, \ + please pass `--test_env DOWNLOAD_P8S_DATA=1` as an argument to the `ict` command", + )); + } +} + +fn main() -> Result<()> { + SystemTestGroup::new() + .with_setup(setup) + .with_overall_timeout(Duration::from_secs(55 * 60)) + .with_timeout_per_test(Duration::from_secs(50 * 60)) + .without_assert_no_replica_restarts() + .add_test(systest!(test)) + .with_teardown(teardown) + .execute_from_args()?; + Ok(()) +} From 3de6ab124c5f674db1acf33720cfae3447ab7d45 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 31 Mar 2026 08:02:19 +0000 Subject: [PATCH 83/98] update --- rs/tests/consensus/BUILD.bazel | 9 ++ rs/tests/consensus/nidkg_performance_test.rs | 130 ++++++++++++------- 2 files changed, 89 insertions(+), 50 deletions(-) diff --git a/rs/tests/consensus/BUILD.bazel b/rs/tests/consensus/BUILD.bazel index 12af1a36b3ce..e39c9cc2de57 100644 --- a/rs/tests/consensus/BUILD.bazel +++ b/rs/tests/consensus/BUILD.bazel @@ -369,6 +369,15 @@ system_test_nns( additional_colocate_tags = [ "system_test_benchmark", ], + colocated_test_driver_vm_required_host_features = [ + "performance", + "spm", + ], + colocated_test_driver_vm_resources = { + "vcpus": 64, + "memory_kibibytes": 512142680, + "boot_image_minimal_size_gibibytes": 500, + }, enable_head_nns_variant = False, enable_metrics = True, prometheus_vm_required_host_features = ["performance"], diff --git a/rs/tests/consensus/nidkg_performance_test.rs b/rs/tests/consensus/nidkg_performance_test.rs index 830e5f4b28df..e2b25a5b9864 100644 --- a/rs/tests/consensus/nidkg_performance_test.rs +++ b/rs/tests/consensus/nidkg_performance_test.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use std::collections::HashSet; use std::time::{Duration, Instant}; use futures::future::join_all; @@ -9,6 +10,7 @@ use ic_system_test_driver::driver::{ farm::HostFeature, ic::{AmountOfMemoryKiB, ImageSizeGiB, InternetComputer, NrOfVCPUs, Subnet, VmResources}, prometheus_vm::HasPrometheus, + simulate_network::{FixedNetworkSimulation, SimulateNetwork}, test_env::TestEnv, test_env_api::{HasTopologySnapshot, IcNodeContainer, NnsInstallationBuilder, SshSession}, }; @@ -22,19 +24,17 @@ use registry_canister::mutations::do_create_subnet::CanisterCyclesCostSchedule; use slog::info; const NNS_NODES_COUNT: usize = 40; -const DEFAULT_UNASSIGNED_NODES_COUNT: usize = 10; +const NEW_SUBNETS_COUNT: usize = 10; +const NODES_PER_NEW_SUBNET: usize = 4; +const UNASSIGNED_NODES_COUNT: usize = NEW_SUBNETS_COUNT * NODES_PER_NEW_SUBNET; + const DEFAULT_NNS_DKG_DEALINGS_PER_BLOCK: usize = 10; -fn setup(env: TestEnv) { - let unassigned_nodes_count = std::env::var("UNASSIGNED_NODES_COUNT") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(DEFAULT_UNASSIGNED_NODES_COUNT); - let nns_dkg_dealings_per_block = std::env::var("NNS_DKG_DEALINGS_PER_BLOCK") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(DEFAULT_NNS_DKG_DEALINGS_PER_BLOCK); +const APPLY_NETWORK_SIMULATION: bool = true; +const BANDWIDTH_MBITS: u32 = 300; +const LATENCY: Duration = Duration::from_millis(120); +fn setup(env: TestEnv) { let vm_resources = VmResources { vcpus: Some(NrOfVCPUs::new(64)), memory_kibibytes: Some(AmountOfMemoryKiB::new(512_142_680)), @@ -43,21 +43,21 @@ fn setup(env: TestEnv) { info!( env.logger(), - "Deploying nIDKG performance testnet with {} system-subnet nodes, {} unassigned nodes, and {} DKG dealings/block", + "Deploying NiDKG performance testnet with {} system-subnet nodes, {} unassigned nodes, and {} DKG dealings/block", NNS_NODES_COUNT, - unassigned_nodes_count, - nns_dkg_dealings_per_block + UNASSIGNED_NODES_COUNT, + DEFAULT_NNS_DKG_DEALINGS_PER_BLOCK ); let mut nns_subnet = Subnet::new(SubnetType::System) .with_required_host_features(vec![HostFeature::Performance]) .with_default_vm_resources(vm_resources) .add_nodes(NNS_NODES_COUNT); - nns_subnet.dkg_dealings_per_block = Some(nns_dkg_dealings_per_block); + nns_subnet.dkg_dealings_per_block = Some(DEFAULT_NNS_DKG_DEALINGS_PER_BLOCK); InternetComputer::new() .add_subnet(nns_subnet) - .with_unassigned_nodes(unassigned_nodes_count) + .with_unassigned_nodes(UNASSIGNED_NODES_COUNT) .setup_and_start(&env) .expect("Failed to setup IC under test"); @@ -74,16 +74,14 @@ fn setup(env: TestEnv) { fn test(env: TestEnv) { let log = env.logger(); let topology_snapshot = env.topology_snapshot(); + let initial_subnet_ids: HashSet<_> = topology_snapshot.subnets().map(|s| s.subnet_id).collect(); let nns_node = topology_snapshot.root_subnet().nodes().next().unwrap(); let unassigned_node_ids = topology_snapshot .unassigned_nodes() .map(|node| node.node_id) .collect::>(); - assert!( - !unassigned_node_ids.is_empty(), - "Expected at least one unassigned node to create single-node subnets" - ); + assert_eq!(unassigned_node_ids.len(), UNASSIGNED_NODES_COUNT,); info!(log, "Installing NNS canisters"); NnsInstallationBuilder::new() @@ -95,24 +93,47 @@ fn test(env: TestEnv) { let version = block_on(get_software_version_from_snapshot(&nns_node)) .expect("Could not obtain replica software version from the NNS node"); + if APPLY_NETWORK_SIMULATION { + let network_simulation = FixedNetworkSimulation::new() + .with_latency(LATENCY) + .with_bandwidth(BANDWIDTH_MBITS); + info!( + log, + "Applying network simulation to NNS subnet: {} Mbit/s, {:?} latency", + BANDWIDTH_MBITS, + LATENCY + ); + topology_snapshot + .root_subnet() + .apply_network_settings(network_simulation); + } else { + info!(log, "Network simulation disabled"); + } + info!( log, - "Starting {} create-subnet proposals (one unassigned node per subnet)", + "Starting create-subnet proposals with {} nodes per subnet from {} unassigned nodes", + NODES_PER_NEW_SUBNET, unassigned_node_ids.len() ); let submit_start = Instant::now(); - let mut proposal_ids = Vec::with_capacity(unassigned_node_ids.len()); - for (idx, node_id) in unassigned_node_ids.iter().cloned().enumerate() { + let node_groups = unassigned_node_ids + .chunks(NODES_PER_NEW_SUBNET) + .map(|chunk| chunk.to_vec()) + .collect::>(); + let total_proposals = node_groups.len(); + let mut proposal_ids = Vec::with_capacity(total_proposals); + for (idx, node_ids) in node_groups.into_iter().enumerate() { info!( log, - "Submitting create-subnet proposal {}/{} for node {}", + "Submitting create-subnet proposal {}/{} for nodes {:?}", idx + 1, - unassigned_node_ids.len(), - node_id + total_proposals, + node_ids ); let proposal_id = block_on(submit_create_application_subnet_proposal( &governance, - vec![node_id], + node_ids, version.clone(), Some(CanisterCyclesCostSchedule::Normal), )); @@ -141,7 +162,7 @@ fn test(env: TestEnv) { Duration::from_secs(2 * 60), ) .await; - assert!(is_executed, "proposal {proposal_id} did not execute in time"); + assert!(is_executed,); (proposal_id, start.elapsed()) } })) @@ -157,7 +178,10 @@ fn test(env: TestEnv) { min_secs = min_secs.min(secs); max_secs = max_secs.max(secs); sum_secs += secs; - info!(log, "Proposal {proposal_id} executed {:.2}s after voting", secs); + info!( + log, + "Proposal {proposal_id} executed {:.2}s after voting", secs + ); } let avg_secs = sum_secs / count as f64; @@ -170,29 +194,36 @@ fn test(env: TestEnv) { max_secs ); env.emit_report(format!( - "nIDKG performance (vote->execute): proposals={} min={:.2}s avg={:.2}s max={:.2}s", - count, - min_secs, - avg_secs, - max_secs + "NiDKG performance (vote->execute): proposals={} min={:.2}s avg={:.2}s max={:.2}s", + count, min_secs, avg_secs, max_secs )); -} -fn teardown(env: TestEnv) { - let should_download_prometheus_data = - std::env::var("DOWNLOAD_P8S_DATA").is_ok_and(|v| v == "true" || v == "1"); - if should_download_prometheus_data { - env.download_prometheus_data_dir_if_exists(); - env.emit_report(String::from( - "Downloaded prometheus data to 'prometheus-data-dir.tar.zst' in the test output \ - directory. You can now use `rs/tests/run-p8s.sh` script to play with the metrics", - )); - } else { - env.emit_report(String::from( - "Not downloading the prometheus data. \ - If you want to download it on the next test run, \ - please pass `--test_env DOWNLOAD_P8S_DATA=1` as an argument to the `ict` command", - )); + let expected_total_subnets = initial_subnet_ids.len() + count; + info!( + log, + "Waiting for topology to show {} total subnets", expected_total_subnets + ); + let mut refreshed_snapshot = topology_snapshot; + while refreshed_snapshot.subnets().count() < expected_total_subnets { + refreshed_snapshot = block_on(refreshed_snapshot.block_for_newer_registry_version()) + .expect("Failed to fetch updated topology snapshot"); + } + + let newly_created_subnets = refreshed_snapshot + .subnets() + .filter(|subnet| !initial_subnet_ids.contains(&subnet.subnet_id)) + .collect::>(); + assert_eq!(newly_created_subnets.len(), count,); + + info!( + log, + "Asserting health for all {} newly created subnets", + newly_created_subnets.len() + ); + for subnet in newly_created_subnets { + subnet + .nodes() + .for_each(|node| node.await_status_is_healthy().unwrap()); } } @@ -201,7 +232,6 @@ fn main() -> Result<()> { .with_setup(setup) .with_timeout_per_test(Duration::from_secs(120 * 60)) .add_test(systest!(test)) - .with_teardown(teardown) .execute_from_args()?; Ok(()) } From 1c227ad29f84f29814c8cf495137e6530b091173 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 31 Mar 2026 11:06:00 +0000 Subject: [PATCH 84/98] update --- rs/tests/consensus/nidkg_performance_test.rs | 65 +++++++++----------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/rs/tests/consensus/nidkg_performance_test.rs b/rs/tests/consensus/nidkg_performance_test.rs index e2b25a5b9864..3b5efd0ece42 100644 --- a/rs/tests/consensus/nidkg_performance_test.rs +++ b/rs/tests/consensus/nidkg_performance_test.rs @@ -9,14 +9,13 @@ use ic_system_test_driver::driver::test_env_api::HasPublicApiUrl; use ic_system_test_driver::driver::{ farm::HostFeature, ic::{AmountOfMemoryKiB, ImageSizeGiB, InternetComputer, NrOfVCPUs, Subnet, VmResources}, - prometheus_vm::HasPrometheus, simulate_network::{FixedNetworkSimulation, SimulateNetwork}, test_env::TestEnv, test_env_api::{HasTopologySnapshot, IcNodeContainer, NnsInstallationBuilder, SshSession}, }; use ic_system_test_driver::nns::{ await_proposal_execution, get_software_version_from_snapshot, - submit_create_application_subnet_proposal, vote_on_proposal, + submit_create_application_subnet_proposal, }; use ic_system_test_driver::systest; use ic_system_test_driver::util::{block_on, runtime_from_url}; @@ -81,7 +80,7 @@ fn test(env: TestEnv) { .unassigned_nodes() .map(|node| node.node_id) .collect::>(); - assert_eq!(unassigned_node_ids.len(), UNASSIGNED_NODES_COUNT,); + assert_eq!(unassigned_node_ids.len(), UNASSIGNED_NODES_COUNT); info!(log, "Installing NNS canisters"); NnsInstallationBuilder::new() @@ -116,43 +115,36 @@ fn test(env: TestEnv) { NODES_PER_NEW_SUBNET, unassigned_node_ids.len() ); - let submit_start = Instant::now(); let node_groups = unassigned_node_ids .chunks(NODES_PER_NEW_SUBNET) .map(|chunk| chunk.to_vec()) .collect::>(); - let total_proposals = node_groups.len(); - let mut proposal_ids = Vec::with_capacity(total_proposals); - for (idx, node_ids) in node_groups.into_iter().enumerate() { - info!( - log, - "Submitting create-subnet proposal {}/{} for nodes {:?}", - idx + 1, - total_proposals, - node_ids - ); - let proposal_id = block_on(submit_create_application_subnet_proposal( - &governance, - node_ids, - version.clone(), - Some(CanisterCyclesCostSchedule::Normal), - )); - proposal_ids.push(proposal_id); - } - let submit_elapsed = submit_start.elapsed(); - + assert_eq!(node_groups.len(), NEW_SUBNETS_COUNT); info!( log, - "Submitted all {} proposals in {:.2}s; voting and awaiting execution in parallel", - proposal_ids.len(), - submit_elapsed.as_secs_f64() + "Submitting and awaiting {} proposals in parallel", NEW_SUBNETS_COUNT ); + let execution_results = block_on(async { - join_all(proposal_ids.into_iter().map(|proposal_id| { + join_all(node_groups.into_iter().enumerate().map(|(idx, node_ids)| { let governance = governance.clone(); let log = log.clone(); + let version = version.clone(); async move { - vote_on_proposal(&governance, proposal_id).await; + info!( + log, + "Submitting create-subnet proposal {}/{} for nodes {:?}", + idx + 1, + NEW_SUBNETS_COUNT, + node_ids + ); + let proposal_id = submit_create_application_subnet_proposal( + &governance, + node_ids, + version, + Some(CanisterCyclesCostSchedule::Normal), + ) + .await; let start = Instant::now(); let is_executed = await_proposal_execution( &log, @@ -162,7 +154,10 @@ fn test(env: TestEnv) { Duration::from_secs(2 * 60), ) .await; - assert!(is_executed,); + assert!( + is_executed, + "proposal {proposal_id} did not execute in time" + ); (proposal_id, start.elapsed()) } })) @@ -180,21 +175,21 @@ fn test(env: TestEnv) { sum_secs += secs; info!( log, - "Proposal {proposal_id} executed {:.2}s after voting", secs + "Proposal {proposal_id} executed {:.2}s after submission", secs ); } let avg_secs = sum_secs / count as f64; info!( log, - "Execution latency after voting for {} proposals: min {:.2}s, avg {:.2}s, max {:.2}s", + "Execution latency after submission for {} proposals: min {:.2}s, avg {:.2}s, max {:.2}s", count, min_secs, avg_secs, max_secs ); env.emit_report(format!( - "NiDKG performance (vote->execute): proposals={} min={:.2}s avg={:.2}s max={:.2}s", + "NiDKG performance (submit->execute): proposals={} min={:.2}s avg={:.2}s max={:.2}s", count, min_secs, avg_secs, max_secs )); @@ -213,7 +208,7 @@ fn test(env: TestEnv) { .subnets() .filter(|subnet| !initial_subnet_ids.contains(&subnet.subnet_id)) .collect::>(); - assert_eq!(newly_created_subnets.len(), count,); + assert_eq!(newly_created_subnets.len(), count); info!( log, @@ -230,7 +225,7 @@ fn test(env: TestEnv) { fn main() -> Result<()> { SystemTestGroup::new() .with_setup(setup) - .with_timeout_per_test(Duration::from_secs(120 * 60)) + .with_timeout_per_test(Duration::from_secs(600)) .add_test(systest!(test)) .execute_from_args()?; Ok(()) From f6f203761c8e0ab31d01e84aa4a1e68139e5e368 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 31 Mar 2026 12:17:41 +0000 Subject: [PATCH 85/98] revert --- .../consensus/subnet_recovery/BUILD.bazel | 4 +- rs/tests/consensus/subnet_recovery/Cargo.toml | 2 +- ...arge_no_upgrade_with_chain_keys_test.rs.rs | 19 +++++++++ .../consensus/subnet_recovery/sr_large.rs | 39 ------------------- 4 files changed, 21 insertions(+), 43 deletions(-) create mode 100644 rs/tests/consensus/subnet_recovery/rs/testssr_app_large_no_upgrade_with_chain_keys_test.rs.rs delete mode 100644 rs/tests/consensus/subnet_recovery/sr_large.rs diff --git a/rs/tests/consensus/subnet_recovery/BUILD.bazel b/rs/tests/consensus/subnet_recovery/BUILD.bazel index 32f9fc263e55..fcffac1394cf 100644 --- a/rs/tests/consensus/subnet_recovery/BUILD.bazel +++ b/rs/tests/consensus/subnet_recovery/BUILD.bazel @@ -228,11 +228,9 @@ system_test_nns( ) system_test_nns( - name = "sr_large", + name = "sr_app_large_no_upgrade_with_chain_keys_test", enable_head_nns_variant = False, # Let's not run this expensive test against the HEAD NNS canisters to save resources. - enable_metrics = True, guestos_update = "test", - prometheus_vm_required_host_features = ["performance"], tags = [ "colocate", "subnet_recovery", diff --git a/rs/tests/consensus/subnet_recovery/Cargo.toml b/rs/tests/consensus/subnet_recovery/Cargo.toml index e0115dad8b74..3cd472307f38 100644 --- a/rs/tests/consensus/subnet_recovery/Cargo.toml +++ b/rs/tests/consensus/subnet_recovery/Cargo.toml @@ -83,4 +83,4 @@ path = "sr_app_no_upgrade_with_chain_keys_test.rs" [[bin]] name = "ic-systest-subnet-recovery-app-large-no-upgrade-with-chain-keys" -path = "sr_large.rs" +path = "sr_app_large_no_upgrade_with_chain_keys_test.rs" diff --git a/rs/tests/consensus/subnet_recovery/rs/testssr_app_large_no_upgrade_with_chain_keys_test.rs.rs b/rs/tests/consensus/subnet_recovery/rs/testssr_app_large_no_upgrade_with_chain_keys_test.rs.rs new file mode 100644 index 000000000000..b3a6bcc8e8d6 --- /dev/null +++ b/rs/tests/consensus/subnet_recovery/rs/testssr_app_large_no_upgrade_with_chain_keys_test.rs.rs @@ -0,0 +1,19 @@ +use anyhow::Result; +use std::time::Duration; + +use ic_consensus_system_test_subnet_recovery::common::{ + setup_large_chain_keys as setup, test_large_no_upgrade_with_chain_keys as test, +}; +use ic_system_test_driver::driver::group::SystemTestGroup; +use ic_system_test_driver::systest; + +fn main() -> Result<()> { + SystemTestGroup::new() + .with_setup(setup) + .with_overall_timeout(Duration::from_secs(55 * 60)) + .with_timeout_per_test(Duration::from_secs(50 * 60)) + .without_assert_no_replica_restarts() + .add_test(systest!(test)) + .execute_from_args()?; + Ok(()) +} \ No newline at end of file diff --git a/rs/tests/consensus/subnet_recovery/sr_large.rs b/rs/tests/consensus/subnet_recovery/sr_large.rs deleted file mode 100644 index 0d8683dd5164..000000000000 --- a/rs/tests/consensus/subnet_recovery/sr_large.rs +++ /dev/null @@ -1,39 +0,0 @@ -use anyhow::Result; -use std::time::Duration; - -use ic_consensus_system_test_subnet_recovery::common::{ - setup_large_chain_keys as setup, test_large_no_upgrade_with_chain_keys as test, -}; -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::driver::{prometheus_vm::HasPrometheus, test_env::TestEnv}; -use ic_system_test_driver::systest; - -fn teardown(env: TestEnv) { - let should_download_prometheus_data = - std::env::var("DOWNLOAD_P8S_DATA").is_ok_and(|v| v == "true" || v == "1"); - if should_download_prometheus_data { - env.download_prometheus_data_dir_if_exists(); - env.emit_report(String::from( - "Downloaded prometheus data to 'prometheus-data-dir.tar.zst' in the test output \ - directory. You can now use `rs/tests/run-p8s.sh` script to play with the metrics", - )); - } else { - env.emit_report(String::from( - "Not downloading the prometheus data. \ - If you want to download it on the next test run, \ - please pass `--test_env DOWNLOAD_P8S_DATA=1` as an argument to the `ict` command", - )); - } -} - -fn main() -> Result<()> { - SystemTestGroup::new() - .with_setup(setup) - .with_overall_timeout(Duration::from_secs(55 * 60)) - .with_timeout_per_test(Duration::from_secs(50 * 60)) - .without_assert_no_replica_restarts() - .add_test(systest!(test)) - .with_teardown(teardown) - .execute_from_args()?; - Ok(()) -} From 065c9e6018e78f1018c982663881581dc2e485bd Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 31 Mar 2026 12:18:27 +0000 Subject: [PATCH 86/98] fix --- ...t.rs.rs => sr_app_large_no_upgrade_with_chain_keys_test.rs.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rs/tests/consensus/subnet_recovery/{rs/testssr_app_large_no_upgrade_with_chain_keys_test.rs.rs => sr_app_large_no_upgrade_with_chain_keys_test.rs.rs} (100%) diff --git a/rs/tests/consensus/subnet_recovery/rs/testssr_app_large_no_upgrade_with_chain_keys_test.rs.rs b/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs.rs similarity index 100% rename from rs/tests/consensus/subnet_recovery/rs/testssr_app_large_no_upgrade_with_chain_keys_test.rs.rs rename to rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs.rs From fcf1669d498f53930cc0452b3111dba7bc629440 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 31 Mar 2026 12:19:18 +0000 Subject: [PATCH 87/98] fix --- ...test.rs.rs => sr_app_large_no_upgrade_with_chain_keys_test.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rs/tests/consensus/subnet_recovery/{sr_app_large_no_upgrade_with_chain_keys_test.rs.rs => sr_app_large_no_upgrade_with_chain_keys_test.rs} (100%) diff --git a/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs.rs b/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs similarity index 100% rename from rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs.rs rename to rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs From 9f8de9d6bb3017c74ca15dfd72f94447f77df3d8 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 31 Mar 2026 12:19:48 +0000 Subject: [PATCH 88/98] fix --- .../sr_app_large_no_upgrade_with_chain_keys_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs b/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs index b3a6bcc8e8d6..2ed9c451bc38 100644 --- a/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs +++ b/rs/tests/consensus/subnet_recovery/sr_app_large_no_upgrade_with_chain_keys_test.rs @@ -16,4 +16,4 @@ fn main() -> Result<()> { .add_test(systest!(test)) .execute_from_args()?; Ok(()) -} \ No newline at end of file +} From a9be68f7c4b13b8ab6fff9d3ffe961c727705f52 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Wed, 1 Apr 2026 12:15:13 +0000 Subject: [PATCH 89/98] impl --- rs/artifact_pool/src/dkg_pool.rs | 2 +- rs/consensus/dkg/src/lib.rs | 97 ++++++--- rs/consensus/dkg/src/payload_builder.rs | 254 ++++++++++------------ rs/consensus/dkg/src/payload_validator.rs | 34 +-- 4 files changed, 196 insertions(+), 191 deletions(-) diff --git a/rs/artifact_pool/src/dkg_pool.rs b/rs/artifact_pool/src/dkg_pool.rs index c15f892c5934..a169ddb7fbb4 100644 --- a/rs/artifact_pool/src/dkg_pool.rs +++ b/rs/artifact_pool/src/dkg_pool.rs @@ -36,7 +36,7 @@ impl DkgPoolImpl { ), validated: PoolSection::new(metrics_registry.clone(), POOL_DKG, POOL_TYPE_VALIDATED), unvalidated: PoolSection::new(metrics_registry, POOL_DKG, POOL_TYPE_UNVALIDATED), - current_start_height: Height::from(1), + current_start_height: Height::from(0), log, } } diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index d1af3d15ca19..bd3bbccf537c 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -18,17 +18,18 @@ use ic_metrics::{ }; use ic_replicated_state::ReplicatedState; use ic_types::{ - Height, NodeId, ReplicaVersion, SubnetId, + Height, NodeId, RegistryVersion, ReplicaVersion, SubnetId, consensus::dkg::{DealingContent, DkgMessageId, DkgSummary, InvalidDkgPayloadReason, Message}, crypto::{ Signed, threshold_sig::ni_dkg::{NiDkgId, NiDkgTargetSubnet, config::NiDkgConfig}, }, + messages::CallbackId, }; use prometheus::Histogram; use rayon::prelude::*; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::BTreeMap, sync::{Arc, Mutex}, }; @@ -36,7 +37,7 @@ pub mod dkg_key_manager; pub mod payload_builder; pub mod payload_validator; -use crate::payload_builder::build_target_id_config_map; +use crate::payload_builder::{ConfigResult, build_callback_id_config_map}; pub use crate::utils::get_vetkey_public_keys; #[cfg(test)] @@ -222,9 +223,12 @@ impl DkgImpl { } // If the dealing refers a config which is not among the ongoing DKGs, - // we reject it. + // we reject it, unless it is a remote DKG, in which case we skip it. let config = match configs.get(message_dkg_id) { Some(config) => config, + None if message_dkg_id.target_subnet != NiDkgTargetSubnet::Local => { + return Mutations::new(); + } None => { return get_handle_invalid_change_action( message, @@ -304,48 +308,76 @@ fn get_handle_invalid_change_action>(message: &Message, reason: T) ChangeAction::HandleInvalid(DkgMessageId::from(message), reason.as_ref().to_string()) } -pub(crate) fn get_configs_for_start_height( +pub(crate) fn merge_configs( + mut summary_configs: BTreeMap, + config_results: BTreeMap, + logger: &ReplicaLogger, +) -> BTreeMap { + for (_, config_result) in config_results { + let configs = match config_result { + Ok(configs) => configs, + Err(errs) => { + for (dkg_id, err) in errs { + error!( + logger, + "Failed to create DKG config for dkg id {:?}: {}", dkg_id, err + ); + } + continue; + } + }; + if configs + .iter() + .any(|config| summary_configs.contains_key(config.dkg_id())) + { + error!( + logger, + "Skipping DKG configs {:?} because they already exist in the summary", + configs + .iter() + .map(|config| config.dkg_id()) + .collect::>() + ); + continue; + } + for config in configs { + summary_configs.insert(config.dkg_id().clone(), config); + } + } + summary_configs +} + +fn get_configs_for_start_height( subnet_id: SubnetId, registry_client: &dyn RegistryClient, - state_manager: &dyn StateManager, + state_reader: &dyn StateReader, registry_version: RegistryVersion, start_height: Height, dkg_summary: &DkgSummary, + logger: &ReplicaLogger, ) -> BTreeMap { - let mut summary_configs = dkg_summary.configs.clone(); - let Some(state) = state_manager.get_latest_certified_state() else { + let summary_configs = dkg_summary.configs.clone(); + // Since early remote transcripts are an optimization, fallback to only the summary configs + // if the state is not available. Dealings for received for early remote transcripts will be + // rejected. + let Some(state) = state_reader.get_latest_certified_state() else { return summary_configs; }; - let map = build_target_id_config_map( + // Fallback to the summary configs if we cannot build the configs for early remote transcripts. + // Dealings received for early remote transcripts will be rejected. + let Ok(map) = build_callback_id_config_map( subnet_id, start_height, registry_client, state.get_ref(), registry_version, dkg_summary.next_transcripts(), - &BTreeSet::new(), - ); - for (_, config_results) in map { - let (mut configs, mut errs) = (vec![], vec![]); - for config_result in config_results { - match config_result { - Ok(config) => configs.push(config), - Err((dkg_id, err)) => errs.push((dkg_id, err)), - } - } - if !errs.is_empty() { - continue; - } - for config in &configs { - if summary_configs.contains_key(&config.dkg_id()) { - continue; - } - } - for config in configs { - summary_configs.insert(config.dkg_id().clone(), config); - } - } - summary_configs + logger, + ) else { + return summary_configs; + }; + + merge_configs(summary_configs, map, logger) } impl PoolMutationsProducer for DkgImpl { @@ -370,6 +402,7 @@ impl PoolMutationsProducer for DkgImpl { self.registry_client.get_latest_version(), start_height, dkg_summary, + &self.logger, ); let change_set: Mutations = configs .par_iter() diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 391c6b39b4a7..ace6e790c229 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1,12 +1,12 @@ use crate::{ MAX_EARLY_REMOTE_TRANSCRIPTS, MAX_REMOTE_DKG_ATTEMPTS, MAX_REMOTE_DKGS_PER_INTERVAL, - REMOTE_DKG_REPEATED_FAILURE_ERROR, get_configs_for_start_height, + REMOTE_DKG_REPEATED_FAILURE_ERROR, merge_configs, utils::{self, tags_iter, vetkd_key_ids_for_subnet}, }; use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; use ic_interfaces::{crypto::ErrorReproducibility, dkg::DkgPool}; use ic_interfaces_registry::RegistryClient; -use ic_interfaces_state_manager::StateManager; +use ic_interfaces_state_manager::{StateManager, StateReader}; use ic_logger::{ReplicaLogger, error, info, warn}; use ic_protobuf::registry::subnet::v1::{ CatchUpPackageContents, chain_key_initialization::Initialization, @@ -107,31 +107,41 @@ fn create_data_payload( last_summary_block: &Block, last_dkg_summary: &DkgSummary, crypto: &dyn ConsensusCrypto, - state_manager: &dyn StateManager, + state_reader: &dyn StateReader, validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { // Get all existing dealer ids from the chain. let dealers_from_chain = utils::get_dealers_from_chain(pool_reader, parent); - let configs = get_configs_for_start_height( + + // Determine all current configs. + let state = state_reader + .get_state_at(validation_context.certified_height) + .map_err(DkgPayloadCreationError::StateManagerError)?; + let remote_config_results = build_callback_id_config_map( this_subnet_id, + last_summary_block.height, registry_client, - state_manager, + state.get_ref(), validation_context.registry_version, - last_summary_block.height, - last_dkg_summary, + last_dkg_summary.next_transcripts(), + &logger, + )?; + let configs = merge_configs( + last_dkg_summary.configs.clone(), + remote_config_results.clone(), + &logger, ); + // Select new dealings for the payload. - let pool_lock = dkg_pool - .read() - .expect("Couldn't lock DKG pool for reading."); let new_validated_dealings = select_dealings_for_payload( &configs, &dealers_from_chain, - &*pool_lock, + &*dkg_pool + .read() + .expect("Couldn't lock DKG pool for reading."), max_dealings_per_block, ); - drop(pool_lock); for d in &new_validated_dealings { if matches!(d.content.dkg_id.target_subnet, NiDkgTargetSubnet::Remote(_)) { info!( @@ -144,16 +154,11 @@ fn create_data_payload( } let remote_dkg_transcripts = create_early_remote_transcripts( - this_subnet_id, - last_summary_block.height, - registry_client, pool_reader, crypto, parent, - last_dkg_summary, - state_manager, - validation_context, - logger.clone(), + remote_config_results, + &logger, )?; if !remote_dkg_transcripts.is_empty() { @@ -232,71 +237,48 @@ fn select_dealings_for_payload( #[allow(clippy::type_complexity)] pub(crate) fn create_early_remote_transcripts( - this_subnet_id: SubnetId, - start_block_height: Height, - registry_client: &dyn RegistryClient, pool_reader: &PoolReader<'_>, crypto: &dyn ConsensusCrypto, parent: &Block, - last_dkg_summary: &DkgSummary, - state_manager: &dyn StateManager, - validation_context: &ValidationContext, - logger: ReplicaLogger, + callback_id_map: BTreeMap, + logger: &ReplicaLogger, ) -> Result)>, DkgPayloadCreationError> { - // Return an error on transient state manager errors - let state = state_manager - .get_state_at(validation_context.certified_height) - .map_err(DkgPayloadCreationError::StateManagerError)?; - - // Get all dealings for DKGs that have not been completed yet - let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent); - let completed_target_ids = - get_completed_target_ids(last_dkg_summary.configs.keys(), &completed); - // Since this function is relatively expensive, we simply return if there are no outstanding DKG contexts - let callback_id_map = build_target_id_config_map( - this_subnet_id, - start_block_height, - registry_client, - state.get_ref(), - validation_context.registry_version, - last_dkg_summary.next_transcripts(), - &completed_target_ids, - ); if callback_id_map.is_empty() { return Ok(vec![]); } + // Get all dealings for DKGs that have not been completed yet + let (all_dealings, _) = utils::get_dkg_dealings(pool_reader, parent); + // Try to create transcripts for all configs of each target_id. Note that we either include // all transcript results for a target_id or none of them. let mut selected_transcripts = vec![]; for (callback_id, config_results) in callback_id_map.into_iter() { + let configs = match config_results { + Ok(configs) => configs, + Err(errs) => { + // Reject contexts for which we failed to create configs. + for (dkg_id, err) in errs { + error!( + logger, + "Failed to create early remote transcript for dkg id {:?} at height {}: {}", + dkg_id, + parent.height.increment(), + err + ); + // Including the error in the payload will cause the context to receive + // a reject response. + selected_transcripts.push((dkg_id, callback_id, Err(err))); + } + continue; + } + }; + // Ensure that creating these transcripts would not exceed the maximum number of early // remote transcripts. We continue with the next target_id in case it requires less // transcripts. - if selected_transcripts.len() + config_results.len() > MAX_EARLY_REMOTE_TRANSCRIPTS { - continue; - } - - let (mut configs, mut errs) = (vec![], vec![]); - for config_result in config_results { - match config_result { - Ok(config) => configs.push(config), - Err((dkg_id, err)) => errs.push((dkg_id, err)), - } - } - - if !errs.is_empty() { - for (dkg_id, err) in errs { - error!( - logger, - "Failed to create early remote transcript for dkg id {:?} at height {}: {}", - dkg_id, - parent.height.increment(), - err - ); - selected_transcripts.push((dkg_id, callback_id, Err(err))); - } + if selected_transcripts.len() + configs.len() > MAX_EARLY_REMOTE_TRANSCRIPTS { continue; } @@ -1111,92 +1093,74 @@ fn build_target_id_callback_map( .collect() } -pub fn build_target_id_config_map( - dealer_subnet: SubnetId, +/// A result of creating DKG configs for a given callback id. +pub type ConfigResult = Result, Vec<(NiDkgId, String)>>; + +pub fn build_callback_id_config_map( + this_subnet_id: SubnetId, start_block_height: Height, registry_client: &dyn RegistryClient, state: &ReplicatedState, registry_version: RegistryVersion, reshared_transcripts: &BTreeMap, - completed_target_ids: &BTreeSet, -) -> BTreeMap>> { + logger: &ReplicaLogger, +) -> Result, DkgPayloadCreationError> { + let mut callback_id_config_map = BTreeMap::new(); let call_contexts = &state.metadata.subnet_call_context_manager; - let setup_initial_dkg_configs = call_contexts - .setup_initial_dkg_contexts - .iter() - .filter(|(_, context)| { - context.registry_version <= registry_version - && !completed_target_ids.contains(&context.target_id) - }) - .filter_map(|(&callback_id, context)| { - let dealers = - get_node_list(dealer_subnet, registry_client, context.registry_version).ok()?; - let low_thr_dkg_id = NiDkgId { - start_block_height, - dealer_subnet, - dkg_tag: NiDkgTag::LowThreshold, - target_subnet: NiDkgTargetSubnet::Remote(context.target_id), - }; - let high_thr_dkg_id = NiDkgId { - start_block_height, - dealer_subnet, - dkg_tag: NiDkgTag::HighThreshold, - target_subnet: NiDkgTargetSubnet::Remote(context.target_id), - }; - let low_thr_config = create_remote_dkg_config( - low_thr_dkg_id.clone(), - &dealers, - &context.nodes_in_target_subnet, - &context.registry_version, - None, - ) - .map_err(|err| (low_thr_dkg_id, err.to_string())); - let high_thr_config = create_remote_dkg_config( - high_thr_dkg_id.clone(), - &dealers, - &context.nodes_in_target_subnet, - &context.registry_version, - None, - ) - .map_err(|err| (high_thr_dkg_id, err.to_string())); - Some((callback_id, vec![low_thr_config, high_thr_config])) - }); + // Setup initial DKG contexts + for (callback_id, context) in call_contexts.setup_initial_dkg_contexts.iter() { + // If the registry version is not reached, skip this context + if context.registry_version <= registry_version { + continue; + } + // Return an error if the dealers cannot be retrieved + let dealers = get_node_list(this_subnet_id, registry_client, context.registry_version)?; - // rehsare chain key contexts - let reshare_chain_key_configs = call_contexts - .reshare_chain_key_contexts - .iter() - .filter(|(_, context)| { - context.registry_version <= registry_version - && !completed_target_ids.contains(&context.target_id) - }) - .filter_map(|(&callback_id, context)| { - let key_id = NiDkgMasterPublicKeyId::try_from(context.key_id.clone()).ok()?; - let tag = NiDkgTag::HighThresholdForKey(key_id); - let reshared_transcript = reshared_transcripts.get(&tag)?; - let dealers = - get_node_list(dealer_subnet, registry_client, context.registry_version).ok()?; - let dkg_id = NiDkgId { - start_block_height, - dealer_subnet, - dkg_tag: tag, - target_subnet: NiDkgTargetSubnet::Remote(context.target_id), - }; - let config = create_remote_dkg_config( - dkg_id.clone(), - &dealers, - &context.nodes, - &context.registry_version, - Some(reshared_transcript.clone()), - ) - .map_err(|err| (dkg_id, err.to_string())); - Some((callback_id, vec![config])) - }); + let result = create_low_high_remote_dkg_configs( + start_block_height, + this_subnet_id, + context.target_id, + &dealers, + &context.nodes_in_target_subnet, + &context.registry_version, + logger, + ) + .map(|(config0, config1)| vec![config0, config1]); - setup_initial_dkg_configs - .chain(reshare_chain_key_configs) - .collect() + callback_id_config_map.insert(*callback_id, result); + } + + // Reshare chain key contexts + for (callback_id, context) in call_contexts.reshare_chain_key_contexts.iter() { + // If the registry version is not reached, skip this context + if context.registry_version <= registry_version { + continue; + } + // Only process NiDkgMasterPublicKeyId + let Ok(key_id) = NiDkgMasterPublicKeyId::try_from(context.key_id.clone()) else { + continue; + }; + // Return an error if the dealers cannot be retrieved + let dealers = get_node_list(this_subnet_id, registry_client, context.registry_version)?; + + let result = create_remote_dkg_config_for_key_id( + key_id, + start_block_height, + this_subnet_id, + context.target_id, + &dealers, + &context.nodes, + reshared_transcripts, + &context.registry_version, + ) + .map(|config| vec![config]) + .map_err(|err_box| vec![*err_box]); + + callback_id_config_map.insert(*callback_id, result); + } + + Ok(callback_id_config_map) } fn add_callback_ids_to_transcript_results( diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index ee451c911530..8a6dc2a773a3 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -1,4 +1,4 @@ -use crate::get_configs_for_start_height; +use crate::{merge_configs, payload_builder::build_callback_id_config_map}; use self::payload_builder::create_early_remote_transcripts; use super::{crypto_validate_dealing, payload_builder, utils}; @@ -17,7 +17,10 @@ use ic_types::{ batch::ValidationContext, consensus::{ Block, BlockPayload, - dkg::{DkgDataPayload, DkgPayloadValidationFailure, DkgSummary, InvalidDkgPayloadReason}, + dkg::{ + DkgDataPayload, DkgPayloadCreationError, DkgPayloadValidationFailure, DkgSummary, + InvalidDkgPayloadReason, + }, }, }; use prometheus::IntCounterVec; @@ -189,13 +192,23 @@ fn validate_dealings_payload( return Err(InvalidDkgPayloadReason::DealerAlreadyDealt(dealer_id).into()); } - let configs = get_configs_for_start_height( + let state = state_manager + .get_state_at(validation_context.certified_height) + .map_err(DkgPayloadCreationError::StateManagerError)?; + + let remote_config_results = build_callback_id_config_map( subnet_id, + start_block_height, registry_client, - state_manager, + state.get_ref(), validation_context.registry_version, - start_block_height, - last_summary, + last_summary.next_transcripts(), + log, + )?; + let configs = merge_configs( + last_summary.configs.clone(), + remote_config_results.clone(), + log, ); // Check that all messages have a valid DKG config from the summary and the @@ -220,16 +233,11 @@ fn validate_dealings_payload( // If we have early transcripts, we compare them if !dealings.transcripts_for_remote_subnets.is_empty() { let expected_transcripts = create_early_remote_transcripts( - subnet_id, - start_block_height, - registry_client, pool_reader, crypto, parent, - last_summary, - state_manager, - validation_context, - log.clone(), + remote_config_results, + log, )?; if dealings.transcripts_for_remote_subnets != expected_transcripts { From 4713eb9a03e32d55c249a6db7d018b2d60d35019 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 2 Apr 2026 10:03:15 +0000 Subject: [PATCH 90/98] reader --- rs/consensus/dkg/src/payload_builder.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 03c1907f104f..18b95d71e8fe 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -6,7 +6,7 @@ use crate::{ use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; use ic_interfaces::{crypto::ErrorReproducibility, dkg::DkgPool}; use ic_interfaces_registry::RegistryClient; -use ic_interfaces_state_manager::{StateManager, StateReader}; +use ic_interfaces_state_manager::StateReader; use ic_logger::{ReplicaLogger, error, info, warn}; use ic_protobuf::registry::subnet::v1::{ CatchUpPackageContents, chain_key_initialization::Initialization, @@ -48,7 +48,7 @@ pub fn create_payload( pool_reader: &PoolReader<'_>, dkg_pool: Arc>, parent: &Block, - state_manager: &dyn StateManager, + state_manager: &dyn StateReader, validation_context: &ValidationContext, logger: ReplicaLogger, max_dealings_per_block: usize, @@ -355,7 +355,7 @@ pub(super) fn create_summary_payload( last_summary: &DkgSummary, parent: &Block, registry_version: RegistryVersion, - state_manager: &dyn StateManager, + state_manager: &dyn StateReader, validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { @@ -528,7 +528,7 @@ fn compute_remote_dkg_data( subnet_id: SubnetId, height: Height, registry_client: &dyn RegistryClient, - state_manager: &dyn StateManager, + state_manager: &dyn StateReader, validation_context: &ValidationContext, mut new_transcripts: BTreeMap>, previous_transcripts: &BTreeMap>, From 189ea7841fd185d2a6f28305b688ee14f4ca745a Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 13 Apr 2026 08:45:20 +0000 Subject: [PATCH 91/98] clippy --- rs/consensus/dkg/src/lib.rs | 12 ++++++------ rs/consensus/dkg/src/payload_builder.rs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 7481352a396a..54d9c14bbd08 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -1655,7 +1655,7 @@ mod tests { )], ); - let target_id = NiDkgTargetId::new([0u8; 32]); + let target_id = NiDkgTargetId::new([0_u8; 32]); complement_state_manager_with_setup_initial_dkg_request( deps.state_manager.clone(), deps.registry.get_latest_version(), @@ -1887,7 +1887,7 @@ mod tests { original_summary.next_transcripts().clone(), vec![( removed_dkg_id, - ic_types::messages::CallbackId::from(0u64), + ic_types::messages::CallbackId::from(0_u64), Ok(dummy_transcript), )], original_summary.registry_version, @@ -1979,7 +1979,7 @@ mod tests { // If a config exists in the summary but there is no corresponding // context in the state, no early transcript should be created. - let unrelated_target_id = NiDkgTargetId::new([1u8; 32]); + let unrelated_target_id = NiDkgTargetId::new([1_u8; 32]); let no_match_state_manager = Arc::new(ic_test_utilities::state_manager::RefMockStateManager::default()); complement_state_manager_with_setup_initial_dkg_request( @@ -2117,7 +2117,7 @@ mod tests { curve: VetKdCurve::Bls12_381_G2, name: String::from("some_vetkey"), }; - let target_id = NiDkgTargetId::new([0u8; 32]); + let target_id = NiDkgTargetId::new([0_u8; 32]); let mut deps = dependencies_with_subnet_records_with_raw_state_manager( pool_config, @@ -2191,8 +2191,8 @@ mod tests { #[test] fn test_early_remote_transcripts_respects_max() { for (skipped_target_bytes, setup_target_bytes, reshare_target_bytes, desc) in [ - ([0u8; 32], [1u8; 32], [2u8; 32], "SetupInitialDKG first"), - ([0u8; 32], [2u8; 32], [1u8; 32], "ReshareChainKey first"), + ([0_u8; 32], [1_u8; 32], [2_u8; 32], "SetupInitialDKG first"), + ([0_u8; 32], [2_u8; 32], [1_u8; 32], "ReshareChainKey first"), ] { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let node_ids = (1..4).map(node_test_id).collect::>(); diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 4dea04002976..3efee04cbab9 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -2139,10 +2139,10 @@ mod tests { let tag = NiDkgTag::HighThresholdForKey(ni_dkg_key_id); let registry_version = registry.get_latest_version(); - let completed_init_dkg_target = NiDkgTargetId::new([1u8; 32]); - let pending_init_dkg_target = NiDkgTargetId::new([2u8; 32]); - let completed_reshare_target = NiDkgTargetId::new([3u8; 32]); - let pending_reshare_target = NiDkgTargetId::new([4u8; 32]); + let completed_init_dkg_target = NiDkgTargetId::new([1_u8; 32]); + let pending_init_dkg_target = NiDkgTargetId::new([2_u8; 32]); + let completed_reshare_target = NiDkgTargetId::new([3_u8; 32]); + let pending_reshare_target = NiDkgTargetId::new([4_u8; 32]); let mut state = ic_test_utilities_state::get_initial_state(0, 0); let target_nodes: BTreeSet<_> = From d365095ece54233166b019515bc0960c70393290 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Wed, 15 Apr 2026 12:36:23 +0000 Subject: [PATCH 92/98] impl --- rs/consensus/dkg/src/lib.rs | 425 +++++++++++++------ rs/consensus/dkg/src/payload_builder.rs | 119 +++--- rs/consensus/dkg/src/payload_validator.rs | 22 +- rs/consensus/dkg/src/test_utils.rs | 66 ++- rs/consensus/tests/framework/runner.rs | 3 + rs/consensus/tests/payload.rs | 9 + rs/tests/consensus/subnet_recovery/common.rs | 16 +- 7 files changed, 448 insertions(+), 212 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 6df2b3e67b5c..ec2ef4c719e8 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -52,7 +52,7 @@ const MAX_REMOTE_DKGS_PER_INTERVAL: usize = 10; /// The maximum number of early remote DKG transcripts we want to include in a data payload. /// Note that responses for `SetupInitialDKG` requests contain two transcripts. -const MAX_EARLY_REMOTE_TRANSCRIPTS: usize = 4; +const MAX_EARLY_REMOTE_TRANSCRIPTS: usize = 2; /// The maximum number of intervals during which an initial DKG request is /// attempted. @@ -313,8 +313,7 @@ pub(crate) fn merge_configs<'a>( config_results: &'a BTreeMap, logger: &ReplicaLogger, ) -> BTreeMap<&'a NiDkgId, &'a NiDkgConfig> { - let mut merged_configs = - BTreeMap::from_iter(summary_configs.iter().map(|(id, config)| (id, config))); + let mut merged_configs: BTreeMap<&NiDkgId, &NiDkgConfig> = summary_configs.iter().collect(); for (_, config_result) in config_results { let configs = match config_result { Ok(configs) => configs, @@ -328,20 +327,6 @@ pub(crate) fn merge_configs<'a>( continue; } }; - if configs - .iter() - .any(|config| summary_configs.contains_key(config.dkg_id())) - { - error!( - logger, - "Skipping DKG configs {:?} because they already exist in the summary", - configs - .iter() - .map(|config| config.dkg_id()) - .collect::>() - ); - continue; - } for config in configs { merged_configs.insert(config.dkg_id(), config); } @@ -370,11 +355,10 @@ impl PoolMutationsProducer for DkgImpl { .and_then(|state| { build_callback_id_config_map( self.subnet_id, - start_height, self.registry_client.as_ref(), state.get_ref(), self.registry_client.get_latest_version(), - dkg_summary.next_transcripts(), + dkg_summary, &self.logger, ) .ok() @@ -472,7 +456,7 @@ impl BouncerFactory for DkgBouncer { mod tests { use super::*; use crate::test_utils::{ - complement_state_manager_with_dkg_contexts, + complement_state_manager_with_dkg_contexts, complement_state_manager_with_dkg_contexts_mut, complement_state_manager_with_reshare_chain_key_request, complement_state_manager_with_setup_initial_dkg_request, create_dealing, extract_dkg_configs_from_highest_block, extract_remote_dkg_ids_from_highest_block, @@ -493,6 +477,7 @@ mod tests { }; use ic_interfaces_mocks::crypto::MockCrypto; use ic_interfaces_registry::RegistryClient; + use ic_interfaces_state_manager::Labeled; use ic_logger::no_op_logger; use ic_management_canister_types_private::{MasterPublicKeyId, VetKdCurve, VetKdKeyId}; use ic_metrics::MetricsRegistry; @@ -501,6 +486,7 @@ mod tests { use ic_test_utilities_consensus::fake::{FakeContentSigner, FromParent}; use ic_test_utilities_logger::with_test_replica_logger; use ic_test_utilities_registry::{SubnetRecordBuilder, add_subnet_record}; + use ic_test_utilities_state::get_initial_state; use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ NumberOfNodes, RegistryVersion, ReplicaVersion, @@ -539,6 +525,8 @@ mod tests { crypto, mut pool, dkg_pool, + registry, + state_manager, .. } = dependencies_with_subnet_params( pool_config, @@ -551,6 +539,13 @@ mod tests { .build(), )], ); + state_manager + .get_mut() + .expect_get_latest_certified_state() + .return_const(Some(Labeled::new( + Height::new(0), + Arc::new(get_initial_state(0, 0)), + ))); // Now we instantiate the DKG component for node Id = 1, who is a dealer. let replica_1 = node_test_id(1); @@ -558,6 +553,9 @@ mod tests { new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg = DkgImpl::new( replica_1, + subnet_id, + registry.clone(), + state_manager.clone(), crypto.clone(), pool.get_cache(), dkg_key_manager.clone(), @@ -625,6 +623,9 @@ mod tests { new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg_2 = DkgImpl::new( replica_2, + subnet_id, + registry, + state_manager, crypto, pool.get_cache(), dkg_key_manager_2.clone(), @@ -688,14 +689,29 @@ mod tests { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { with_test_replica_logger(|logger| { let Dependencies { - mut pool, crypto, .. + mut pool, + crypto, + registry, + state_manager, + replica_config, + .. } = dependencies(pool_config.clone(), 2); + state_manager + .get_mut() + .expect_get_latest_certified_state() + .return_const(Some(Labeled::new( + Height::new(0), + Arc::new(get_initial_state(0, 0)), + ))); let mut dkg_pool = DkgPoolImpl::new(MetricsRegistry::new(), logger.clone()); // Let's check that replica 3, who's not a dealer, does not produce dealings. let dkg_key_manager = new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg = DkgImpl::new( node_test_id(3), + replica_config.subnet_id, + registry.clone(), + state_manager.clone(), crypto.clone(), pool.get_cache(), dkg_key_manager, @@ -709,6 +725,9 @@ mod tests { new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg = DkgImpl::new( node_test_id(1), + replica_config.subnet_id, + registry, + state_manager, crypto, pool.get_cache(), dkg_key_manager.clone(), @@ -777,6 +796,7 @@ mod tests { crypto, registry, state_manager, + dkg_pool, .. } = dependencies_with_subnet_records_with_raw_state_manager( pool_config, @@ -791,7 +811,7 @@ mod tests { let target_id = NiDkgTargetId::new([0_u8; 32]); complement_state_manager_with_setup_initial_dkg_request( - state_manager, + state_manager.clone(), registry.get_latest_version(), vec![10, 11, 12], None, @@ -803,6 +823,9 @@ mod tests { new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg = DkgImpl::new( node_test_id(1), + subnet_id, + registry.clone(), + state_manager.clone(), crypto, pool.get_cache(), dkg_key_manager.clone(), @@ -810,42 +833,10 @@ mod tests { logger.clone(), ); - // We did not advance the consensus pool yet. The configs for remote transcripts - // are not added to a summary block yet. That's why we see two dealings for - // local thresholds. - let mut dkg_pool = DkgPoolImpl::new(MetricsRegistry::new(), logger); + // We will create dealings for remote requests immediately, even if we haven't + // reached a summary block yet. sync_dkg_key_manager(&dkg_key_manager, &pool); - let change_set = dkg.on_state_change(&dkg_pool); - match &change_set.as_slice() { - &[ - ChangeAction::AddToValidated(a), - ChangeAction::AddToValidated(b), - ] => { - assert_eq!(a.content.dkg_id.target_subnet, NiDkgTargetSubnet::Local); - assert_eq!(b.content.dkg_id.target_subnet, NiDkgTargetSubnet::Local); - } - val => panic!("Unexpected change set: {:?}", val), - }; - - // Apply the changes and make sure, we do not produce any dealings anymore. - dkg_pool.apply(change_set); - assert!(dkg.on_state_change(&dkg_pool).is_empty()); - - // Advance _past_ the new summary to make sure the configs for remote - // transcripts are added into the summary. - pool.advance_round_normal_operation_n(dkg_interval_length + 1); - - // First we expect a new purge. - let change_set = dkg.on_state_change(&dkg_pool); - match &change_set.as_slice() { - &[ChangeAction::Purge(purge_height)] - if *purge_height == Height::from(dkg_interval_length + 1) => {} - val => panic!("Unexpected change set: {:?}", val), - }; - dkg_pool.apply(change_set); - - // And then we validate two local and two remote dealings. - let change_set = dkg.on_state_change(&dkg_pool); + let change_set = dkg.on_state_change(&*dkg_pool.read().unwrap()); match &change_set.as_slice() { &[ ChangeAction::AddToValidated(a), @@ -873,14 +864,73 @@ mod tests { val => panic!("Unexpected change set: {:?}", val), }; // Just check again, we do not reproduce a dealing once changes are applied. - dkg_pool.apply(change_set); - assert!(dkg.on_state_change(&dkg_pool).is_empty()); + dkg_pool.write().unwrap().apply(change_set); + assert!(dkg.on_state_change(&*dkg_pool.read().unwrap()).is_empty()); + + pool.advance_round_normal_operation(); + let dealings = extract_dealings_from_highest_block(&pool); + assert_eq!(dealings.len(), 4); + let remote_dealings = dealings + .iter() + .filter(|d| { + d.content.dkg_id.target_subnet == NiDkgTargetSubnet::Remote(target_id) + }) + .count(); + assert_eq!(remote_dealings, 2); + + // Once enough remote dealings are available on chain, they are turned into + // early remote transcripts in the data payload. + pool.advance_round_normal_operation(); + let early_remote = extract_remote_dkgs_from_highest_block(&pool); + assert_eq!(early_remote.len(), 2); + for (dkg_id, _, result) in &early_remote { + assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); + assert!(result.is_ok()); + } + + // After the next summary, remote transcripts are finalized and we should not + // attempt creating remote dealings again. + pool.advance_round_normal_operation_n(dkg_interval_length); + let latest_summary = PoolReader::new(&pool).get_highest_finalized_summary_block(); + assert_eq!( + latest_summary + .payload + .as_ref() + .as_summary() + .dkg + .initial_dkg_attempts + .get(&target_id), + Some(&0), + "Expected initial_dkg_attempts[{target_id:?}] to be 0" + ); + let change_set = dkg.on_state_change(&*dkg_pool.read().unwrap()); + match &change_set.as_slice() { + &[ChangeAction::Purge(purge_height)] if *purge_height == Height::from(100) => {} + val => panic!("Unexpected change set: {:?}", val), + }; + dkg_pool.write().unwrap().apply(change_set); + + let change_set = dkg.on_state_change(&*dkg_pool.read().unwrap()); + let remote_dealings = change_set + .iter() + .filter(|change| match change { + ChangeAction::AddToValidated(message) => { + message.content.dkg_id.target_subnet + == NiDkgTargetSubnet::Remote(target_id) + } + _ => false, + }) + .count(); + assert_eq!( + remote_dealings, 0, + "Unexpected remote dealings: {change_set:?}" + ); }); }); } #[test] - fn test_config_generation_failures_are_added_to_the_summary() { + fn test_config_generation_failures_are_added_to_data_blocks() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { use ic_types::crypto::threshold_sig::ni_dkg::*; let node_ids = vec![node_test_id(0), node_test_id(1)]; @@ -911,12 +961,24 @@ mod tests { Some(target_id), ); - // Advance _past_ the new summary to make sure the replicas attempt to create + // Advance one round + pool.advance_round_normal_operation(); + // Verify that the latest block contains errors for both requests + let block: Block = PoolReader::new(&pool).get_finalized_tip(); + if let BlockPayload::Data(data) = block.payload.as_ref() { + assert_eq!(data.dkg.transcripts_for_remote_subnets.len(), 2); + for dkg in data.dkg.transcripts_for_remote_subnets.iter() { + assert!(dkg.2.is_err()); + } + } else { + panic!("block at height {} is not a data block", block.height.get()); + } + + // Advance _past_ the new summary to make sure the replicas don't attempt to create // the configs for remote transcripts. pool.advance_round_normal_operation_n(dkg_interval_length + 1); - // Verify that the first summary block contains only two local configs and the - // two errors for the remote DKG request. + // Verify that the first summary block contains only two local configs let block: Block = PoolReader::new(&pool).get_highest_finalized_summary_block(); if let BlockPayload::Summary(summary) = block.payload.as_ref() { assert_eq!( @@ -928,11 +990,7 @@ mod tests { for (dkg_id, _) in summary.dkg.configs.iter() { assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Local); } - assert_eq!(summary.dkg.transcripts_for_remote_subnets.len(), 2); - for (dkg_id, _, result) in summary.dkg.transcripts_for_remote_subnets.iter() { - assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); - assert!(result.is_err()); - } + assert_eq!(summary.dkg.transcripts_for_remote_subnets.len(), 0); } else { panic!( "block at height {} is not a summary block", @@ -942,6 +1000,9 @@ mod tests { }); } + #[test] + fn test_config_generation_failures_are_added_to_summary_blocks() {} + /// These components are used for the validation tests. struct ValidationTestComponents { dkg: DkgImpl, @@ -963,8 +1024,29 @@ mod tests { let node_id_1 = node_test_id(1); // This is not a dealer! let node_id_2 = node_test_id(0); - let consensus_pool_1 = dependencies(pool_config_1, 2).pool; - let consensus_pool_2 = dependencies(pool_config_2, 2).pool; + let Dependencies { + pool: consensus_pool_1, + registry: registry_1, + state_manager: state_manager_1, + replica_config: replica_config_1, + .. + } = dependencies(pool_config_1, 2); + let Dependencies { + pool: consensus_pool_2, + registry: registry_2, + state_manager: state_manager_2, + replica_config: replica_config_2, + .. + } = dependencies(pool_config_2, 2); + for state_manager in [&state_manager_1, &state_manager_2] { + state_manager + .get_mut() + .expect_get_latest_certified_state() + .return_const(Some(Labeled::new( + Height::new(0), + Arc::new(get_initial_state(0, 0)), + ))); + } with_test_replica_logger(|logger| { let dkg_pool_1 = DkgPoolImpl::new(MetricsRegistry::new(), logger.clone()); @@ -978,6 +1060,9 @@ mod tests { ); let dkg_1 = DkgImpl::new( node_id_1, + replica_config_1.subnet_id, + registry_1, + state_manager_1, crypto.clone(), consensus_pool_1.get_cache(), dkg_key_manager_1.clone(), @@ -992,6 +1077,9 @@ mod tests { ); let dkg_2 = DkgImpl::new( node_id_2, + replica_config_2.subnet_id, + registry_2, + state_manager_2, crypto.clone(), consensus_pool_2.get_cache(), dkg_key_manager_2.clone(), @@ -1435,6 +1523,12 @@ mod tests { let crypto_1 = dependencies_1.crypto.clone(); let crypto_2 = dependencies_2.crypto.clone(); + let registry_1 = dependencies_1.registry.clone(); + let registry_2 = dependencies_2.registry.clone(); + let state_manager_1 = dependencies_1.state_manager.clone(); + let state_manager_2 = dependencies_2.state_manager.clone(); + let subnet_id_1 = dependencies_1.replica_config.subnet_id; + let subnet_id_2 = dependencies_2.replica_config.subnet_id; let mut pool_1 = dependencies_1.pool; let mut pool_2 = dependencies_2.pool; @@ -1479,6 +1573,9 @@ mod tests { ); let dkg_1 = DkgImpl::new( node_test_id(1), + subnet_id_1, + registry_1, + state_manager_1, crypto_1, pool_1.get_cache(), dgk_key_manager_1.clone(), @@ -1488,6 +1585,9 @@ mod tests { let dkg_2 = DkgImpl::new( node_test_id(2), + subnet_id_2, + registry_2, + state_manager_2, crypto_2.clone(), pool_2.get_cache(), new_dkg_key_manager(crypto_2, logger.clone(), &PoolReader::new(&pool_2)), @@ -1905,13 +2005,13 @@ mod tests { }); } - /// Tests that no early remote transcripts are created when a + /// Tests that early remote transcripts are created when a /// setup_initial_dkg target has only one of its expected two configs /// in the summary (because one transcript was already created in a - /// previous summary block). Instead, the next summary block should - /// contain both transcripts. + /// previous summary block). We will still retry both of the configs + /// as part of early remote transcript creation. #[test] - fn test_no_early_transcripts_for_single_setup_initial_dkg_config() { + fn test_early_transcripts_are_createdfor_single_setup_initial_dkg_config() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let (mut deps, target_id, remote_dkg_ids) = setup_initial_dkg_test(pool_config); @@ -1977,6 +2077,19 @@ mod tests { certified_height: Height::from(0), time: UNIX_EPOCH, }; + let state = deps + .state_manager + .get_state_at(validation_context.certified_height) + .unwrap(); + let modified_callback_id_map = payload_builder::build_callback_id_config_map( + subnet_test_id(0), + deps.registry.as_ref(), + state.get_ref(), + validation_context.registry_version, + &modified_summary, + &no_op_logger(), + ) + .unwrap(); // Even though sufficient dealings exist on chain for both configs, // no early transcript should be created because the summary only @@ -1985,17 +2098,15 @@ mod tests { &pool_reader, deps.crypto.as_ref(), &parent, - &modified_summary, - deps.state_manager.as_ref(), - &validation_context, - no_op_logger(), + modified_callback_id_map, + &no_op_logger(), ) .unwrap(); - assert!( - early_transcripts.is_empty(), - "No early transcripts should be created for a single \ - setup_initial_dkg config, but got {early_transcripts:?}", - ); + assert_eq!(early_transcripts.len(), 2,); + for (dkg_id, _callback_id, result) in &early_transcripts { + assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); + assert!(result.is_ok()); + } // The next summary should contain both transcripts: the one already // in the modified summary's transcripts_for_remote_subnets and the @@ -2025,14 +2136,21 @@ mod tests { // Control: using the original summary with both configs DOES // produce early transcripts. + let original_callback_id_map = payload_builder::build_callback_id_config_map( + subnet_test_id(0), + deps.registry.as_ref(), + state.get_ref(), + validation_context.registry_version, + &original_summary, + &no_op_logger(), + ) + .unwrap(); let early_transcripts = payload_builder::create_early_remote_transcripts( &pool_reader, deps.crypto.as_ref(), &parent, - &original_summary, - deps.state_manager.as_ref(), - &validation_context, - no_op_logger(), + original_callback_id_map, + &no_op_logger(), ) .unwrap(); assert_eq!(early_transcripts.len(), 2); @@ -2053,14 +2171,24 @@ mod tests { None, Some(unrelated_target_id), ); + let no_match_state = no_match_state_manager + .get_state_at(validation_context.certified_height) + .unwrap(); + let no_match_callback_id_map = payload_builder::build_callback_id_config_map( + subnet_test_id(0), + deps.registry.as_ref(), + no_match_state.get_ref(), + validation_context.registry_version, + &original_summary, + &no_op_logger(), + ) + .unwrap(); let early_transcripts = payload_builder::create_early_remote_transcripts( &pool_reader, deps.crypto.as_ref(), &parent, - &original_summary, - no_match_state_manager.as_ref(), - &validation_context, - no_op_logger(), + no_match_callback_id_map, + &no_op_logger(), ) .unwrap(); assert!( @@ -2105,15 +2233,26 @@ mod tests { certified_height: Height::from(0), time: UNIX_EPOCH, }; + let state = deps + .state_manager + .get_state_at(validation_context.certified_height) + .unwrap(); + let callback_id_map = payload_builder::build_callback_id_config_map( + subnet_test_id(0), + deps.registry.as_ref(), + state.get_ref(), + validation_context.registry_version, + last_summary, + &no_op_logger(), + ) + .unwrap(); let early_transcripts = payload_builder::create_early_remote_transcripts( &pool_reader, &mock_crypto, &parent, - last_summary, - deps.state_manager.as_ref(), - &validation_context, - no_op_logger(), + callback_id_map, + &no_op_logger(), ) .unwrap(); @@ -2204,21 +2343,35 @@ mod tests { )], ); - complement_state_manager_with_reshare_chain_key_request( + let contexts = Arc::new(Mutex::new(Vec::new())); + complement_state_manager_with_dkg_contexts_mut( deps.state_manager.clone(), - deps.registry.get_latest_version(), - key_id.clone(), - vec![10, 11, 12, 13], + contexts.clone(), None, - Some(target_id), ); + // Advance until the first vetkd transcript is created deps.pool - .advance_round_normal_operation_n(EARLY_DKG_INTERVAL + 1); - let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&deps.pool, target_id); - assert_eq!(remote_dkg_ids.len(), 1); - assert_eq!(extract_dkg_configs_from_highest_block(&deps.pool).len(), 4); - assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); + .advance_round_normal_operation_n(EARLY_DKG_INTERVAL + 3); + + contexts + .lock() + .unwrap() + .push(make_reshare_chain_key_context( + deps.registry.get_latest_version(), + key_id.clone(), + vec![10, 11, 12, 13], + target_id, + )); + + let remote_dkg_ids = vec![NiDkgId { + start_block_height: Height::from(EARLY_DKG_INTERVAL + 1), + dealer_subnet: subnet_test_id(0), + dkg_tag: NiDkgTag::HighThresholdForKey(NiDkgMasterPublicKeyId::VetKd( + key_id.clone(), + )), + target_subnet: NiDkgTargetSubnet::Remote(target_id), + }]; add_dealings_for_configs(&mut deps, &remote_dkg_ids); // 2f + 1 dealings for high threshold VetKD resharing @@ -2255,21 +2408,21 @@ mod tests { /// (= 2), and only the first context's transcripts are included. #[test] fn test_early_remote_transcripts_respects_max() { - for (skipped_target_bytes, setup_target_bytes, reshare_target_bytes, desc) in [ - ([0u8; 32], [1u8; 32], [2u8; 32], "SetupInitialDKG first"), - ([0u8; 32], [2u8; 32], [1u8; 32], "ReshareChainKey first"), + for (setup_first, desc) in [ + (true, "SetupInitialDKG first"), + (false, "ReshareChainKey first"), ] { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { - let node_ids = (1..4).map(node_test_id).collect::>(); + let node_ids = (1..5).map(node_test_id).collect::>(); let key_id = VetKdKeyId { curve: VetKdCurve::Bls12_381_G2, name: String::from("some_vetkey"), }; // This context always comes first but will be // skipped because one of its two configs lacks dealings. - let skipped_target_id = NiDkgTargetId::new(skipped_target_bytes); - let setup_target_id = NiDkgTargetId::new(setup_target_bytes); - let reshare_target_id = NiDkgTargetId::new(reshare_target_bytes); + let skipped_target_id = NiDkgTargetId::new([0u8; 32]); + let setup_target_id = NiDkgTargetId::new([1u8; 32]); + let reshare_target_id = NiDkgTargetId::new([2u8; 32]); let mut deps = dependencies_with_subnet_records_with_raw_state_manager( pool_config, @@ -2293,26 +2446,30 @@ mod tests { ); let registry_version = deps.registry.get_latest_version(); + let mut contexts = vec![ + make_setup_initial_dkg_context( + registry_version, + vec![10, 11, 12, 13], + skipped_target_id, + ), + make_setup_initial_dkg_context( + registry_version, + vec![10, 11, 12, 13], + setup_target_id, + ), + make_reshare_chain_key_context( + registry_version, + key_id.clone(), + vec![10, 11, 12, 13], + reshare_target_id, + ), + ]; + if !setup_first { + contexts.swap(1, 2); + } complement_state_manager_with_dkg_contexts( deps.state_manager.clone(), - vec![ - make_setup_initial_dkg_context( - registry_version, - vec![10, 11, 12, 13], - skipped_target_id, - ), - make_setup_initial_dkg_context( - registry_version, - vec![10, 11, 12, 13], - setup_target_id, - ), - make_reshare_chain_key_context( - registry_version, - key_id.clone(), - vec![10, 11, 12, 13], - reshare_target_id, - ), - ], + contexts, None, ); @@ -2441,7 +2598,7 @@ mod tests { .chain(std::iter::once(&reshare_dkg_id)) .collect(); for dkg_id in &dkg_ids_with_dealings { - let dealings: Vec<_> = (0..3) + let dealings: Vec<_> = (1..5) .map(|i| ChangeAction::AddToValidated(create_dealing(i, (*dkg_id).clone()))) .collect(); deps.dkg_pool.write().unwrap().apply(dealings); @@ -2449,8 +2606,8 @@ mod tests { deps.pool.advance_round_normal_operation(); assert_eq!( extract_dealings_from_highest_block(&deps.pool).len(), - 12, - "[{desc}] all 12 dealings should be in the block" + 9, + "[{desc}] 9 dealings should be in the block" ); assert_eq!( extract_remote_dkgs_from_highest_block(&deps.pool).len(), @@ -2465,7 +2622,7 @@ mod tests { assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 0); let remote_dkgs = extract_remote_dkgs_from_highest_block(&deps.pool); - if setup_target_id < reshare_target_id { + if setup_first { assert_eq!( remote_dkgs.len(), 2, diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 18b95d71e8fe..8cc5a7eb4a36 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -120,11 +120,10 @@ fn create_data_payload( .map_err(DkgPayloadCreationError::StateManagerError)?; let remote_config_results = build_callback_id_config_map( this_subnet_id, - last_summary_block.height, registry_client, state.get_ref(), validation_context.registry_version, - last_dkg_summary.next_transcripts(), + last_dkg_summary, &logger, )?; let configs = merge_configs(&last_dkg_summary.configs, &remote_config_results, &logger); @@ -195,24 +194,29 @@ fn select_dealings_for_payload( *cap = cap.saturating_sub(1); } } - + println!("remaining capacity: {:?}", remaining_capacity); // Filter dealings whose dealer has no dealing on the chain yet, and for which // the collection_threshold hasn't been reached. Prioritize remote DKGs. let (remote_priority, rest): (Vec<_>, Vec<_>) = dkg_pool .get_validated() .filter(|msg| { + println!("msg: {:?}", msg.content.dkg_id); // Make sure the message relates to one of the ongoing DKGs, it's from a unique // dealer, and the collection_threshold hasn't been reached yet. let Some(cap) = remaining_capacity.get_mut(&msg.content.dkg_id) else { + println!("no cap for dkg id: {:?}", msg.content.dkg_id); return false; }; if dealers_from_chain.contains(&(msg.content.dkg_id.clone(), msg.signature.signer)) { + println!("dealer from chain: {:?}", msg.signature.signer); return false; } if *cap > 0 { *cap -= 1; + println!("cap: {:?}", *cap); true } else { + println!("cap is 0"); false } }) @@ -439,10 +443,9 @@ pub(super) fn create_summary_payload( .map(|(id, _, result)| (id.clone(), result.clone())) .collect(); - let completed_target_ids = - get_completed_target_ids(last_summary.configs.keys(), &completed_dkgs); + let completed_target_ids = get_completed_target_ids(&completed_dkgs); - let (mut configs, transcripts_for_remote_subnets, initial_dkg_attempts) = + let (mut configs, transcripts_for_remote_subnets, mut initial_dkg_attempts) = compute_remote_dkg_data( subnet_id, height, @@ -456,6 +459,9 @@ pub(super) fn create_summary_payload( &last_summary.initial_dkg_attempts, &logger, )?; + for target_id in completed_target_ids { + initial_dkg_attempts.insert(target_id, 0); + } let interval_length = last_summary.next_interval_length; let next_interval_length = get_dkg_interval_length( @@ -1037,25 +1043,18 @@ fn get_node_list( .collect()) } -/// Returns the set of remote target IDs for which all configured DKGs have -/// been completed. -fn get_completed_target_ids<'a>( - config_ids: impl Iterator, - completed: &BTreeSet, -) -> BTreeSet { - let mut remote_dkgs_by_target: BTreeMap> = BTreeMap::new(); - for dkg_id in config_ids { - if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { - remote_dkgs_by_target - .entry(target_id) - .or_default() - .push(dkg_id); - } - } - remote_dkgs_by_target - .into_iter() - .filter(|(_, dkg_ids)| dkg_ids.iter().all(|id| completed.contains(id))) - .map(|(target_id, _)| target_id) +/// Returns the set of remote target IDs for which at least one DKG instance +/// was completed. +fn get_completed_target_ids(completed: &BTreeSet) -> BTreeSet { + completed + .iter() + .filter_map(|dkg_id| { + if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { + Some(target_id) + } else { + None + } + }) .collect() } @@ -1094,11 +1093,10 @@ pub type ConfigResult = Result, Vec<(NiDkgId, String)>>; pub fn build_callback_id_config_map( this_subnet_id: SubnetId, - start_block_height: Height, registry_client: &dyn RegistryClient, state: &ReplicatedState, registry_version: RegistryVersion, - reshared_transcripts: &BTreeMap, + dkg_summary: &DkgSummary, logger: &ReplicaLogger, ) -> Result, DkgPayloadCreationError> { let mut callback_id_config_map = BTreeMap::new(); @@ -1106,6 +1104,14 @@ pub fn build_callback_id_config_map( // Setup initial DKG contexts for (callback_id, context) in call_contexts.setup_initial_dkg_contexts.iter() { + // If the DKG has already been completed, skip this context + if dkg_summary + .initial_dkg_attempts + .get(&context.target_id) + .is_some_and(|x| *x == 0) + { + continue; + } // If the registry version is not reached, skip this context if context.registry_version > registry_version { continue; @@ -1114,7 +1120,7 @@ pub fn build_callback_id_config_map( let dealers = get_node_list(this_subnet_id, registry_client, context.registry_version)?; let result = create_low_high_remote_dkg_configs( - start_block_height, + dkg_summary.height, this_subnet_id, context.target_id, &dealers, @@ -1129,6 +1135,14 @@ pub fn build_callback_id_config_map( // Reshare chain key contexts for (callback_id, context) in call_contexts.reshare_chain_key_contexts.iter() { + // If the DKG has already been completed, skip this context + if dkg_summary + .initial_dkg_attempts + .get(&context.target_id) + .is_some_and(|x| *x == 0) + { + continue; + } // If the registry version is not reached, skip this context if context.registry_version > registry_version { continue; @@ -1142,12 +1156,12 @@ pub fn build_callback_id_config_map( let result = create_remote_dkg_config_for_key_id( key_id, - start_block_height, + dkg_summary.height, this_subnet_id, context.target_id, &dealers, &context.nodes, - reshared_transcripts, + dkg_summary.next_transcripts(), &context.registry_version, ) .map(|config| vec![config]) @@ -2193,30 +2207,6 @@ mod tests { }); } - #[test] - fn test_get_completed_target_ids() { - let targets: Vec<_> = (1..=3).map(|i| NiDkgTargetId::new([i; 32])).collect(); - let tags = [NiDkgTag::LowThreshold, NiDkgTag::HighThreshold]; - - let config_ids: Vec<_> = targets - .iter() - .flat_map(|t| { - tags.iter().map(|tag| NiDkgId { - start_block_height: Height::from(1), - dealer_subnet: subnet_test_id(1), - dkg_tag: tag.clone(), - target_subnet: NiDkgTargetSubnet::Remote(*t), - }) - }) - .collect(); - - // target 0 is fully completed, target 1 only has low completed, target 2 is not completed - let completed: BTreeSet<_> = config_ids[..3].iter().cloned().collect(); - - let result = get_completed_target_ids(config_ids.iter(), &completed); - assert_eq!(result, BTreeSet::from([targets[0]])); - } - #[test] fn test_process_subnet_call_context_ignores_completed_targets() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { @@ -2399,7 +2389,8 @@ mod tests { create_dealing(1, remote_id.clone()), ], }; - let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 1); + let selected = + select_dealings_for_payload(configs.iter().collect(), &HashSet::new(), &pool, 1); assert_eq!(selected.len(), 1); assert_eq!(selected[0].content.dkg_id, remote_id); @@ -2416,7 +2407,8 @@ mod tests { messages: (0..4).map(|i| create_dealing(i, id.clone())).collect(), }; - let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 10); + let selected = + select_dealings_for_payload(configs.iter().collect(), &HashSet::new(), &pool, 10); // Only collection_threshold (2) dealings should be included. assert_eq!(selected.len(), 2); @@ -2436,7 +2428,8 @@ mod tests { messages: (1..4).map(|i| create_dealing(i, id.clone())).collect(), }; - let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 10); + let selected = + select_dealings_for_payload(configs.iter().collect(), &dealers_from_chain, &pool, 10); // Only 1 more needed to reach collection_threshold of 2. assert_eq!(selected.len(), 1); @@ -2455,7 +2448,8 @@ mod tests { messages: vec![create_dealing(0, id.clone()), create_dealing(1, id.clone())], }; - let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 10); + let selected = + select_dealings_for_payload(configs.iter().collect(), &dealers_from_chain, &pool, 10); // Dealer 0's dealing should be filtered out, only dealer 1's remains. assert_eq!(selected.len(), 1); @@ -2481,7 +2475,8 @@ mod tests { .collect(), }; - let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 3); + let selected = + select_dealings_for_payload(configs.iter().collect(), &HashSet::new(), &pool, 3); assert_eq!(selected.len(), 3); // All 3 should be remote (prioritized), since remote has 4 available. @@ -2505,7 +2500,8 @@ mod tests { ], }; - let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 10); + let selected = + select_dealings_for_payload(configs.iter().collect(), &HashSet::new(), &pool, 10); assert_eq!(selected.len(), 1); assert_eq!(selected[0].content.dkg_id, known_id); @@ -2531,7 +2527,8 @@ mod tests { .collect(), }; - let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 10); + let selected = + select_dealings_for_payload(configs.iter().collect(), &HashSet::new(), &pool, 10); // 2 remote + 2 local (both capped at collection_threshold) assert_eq!(selected.len(), 4); diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index bad37d66f37c..9d67ff766c3f 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -13,7 +13,7 @@ use ic_logger::{ReplicaLogger, info, warn}; use ic_registry_client_helpers::subnet::SubnetRegistry; use ic_replicated_state::ReplicatedState; use ic_types::{ - Height, SubnetId, + SubnetId, batch::ValidationContext, consensus::{ Block, BlockPayload, @@ -123,7 +123,6 @@ pub fn validate_payload( validate_dealings_payload( subnet_id, - last_summary_block.height, registry_client, crypto, pool_reader, @@ -145,7 +144,6 @@ pub fn validate_payload( #[allow(clippy::result_large_err)] fn validate_dealings_payload( subnet_id: SubnetId, - start_block_height: Height, registry_client: &dyn RegistryClient, crypto: &dyn ConsensusCrypto, pool_reader: &PoolReader<'_>, @@ -198,11 +196,10 @@ fn validate_dealings_payload( let remote_config_results = build_callback_id_config_map( subnet_id, - start_block_height, registry_client, state.get_ref(), validation_context.registry_version, - last_summary.next_transcripts(), + last_summary, log, )?; let configs = merge_configs(&last_summary.configs, &remote_config_results, log); @@ -270,11 +267,13 @@ mod tests { dkg::ChangeAction, p2p::consensus::{MutablePool, PoolMutationsProducer}, }; + use ic_interfaces_state_manager::Labeled; use ic_logger::no_op_logger; use ic_metrics::MetricsRegistry; use ic_registry_keys::make_subnet_record_key; use ic_test_utilities_consensus::fake::FakeContentSigner; use ic_test_utilities_registry::SubnetRecordBuilder; + use ic_test_utilities_state::get_initial_state; use ic_test_utilities_types::ids::{ NODE_1, NODE_2, NODE_3, SUBNET_1, SUBNET_2, node_test_id, subnet_test_id, }; @@ -731,6 +730,13 @@ mod tests { .build(), )], ); + state_manager + .get_mut() + .expect_get_latest_certified_state() + .return_const(Some(Labeled::new( + Height::new(0), + Arc::new(get_initial_state(0, 0)), + ))); // Both summary registry versions should be 1 initially let summary_block = pool.as_cache().summary_block(); @@ -782,6 +788,9 @@ mod tests { let key_manager = Arc::new(Mutex::new(key_manager)); let dkg_impl = DkgImpl::new( node_id, + subnet_id, + registry.clone(), + state_manager.clone(), crypto.clone(), pool.get_cache(), key_manager, @@ -801,9 +810,10 @@ mod tests { }; // It should be possible to validate the dealing + let configs = dkg_summary.configs.iter().collect(); let result = dkg_impl.validate_dealings_for_dealer( &dkg_pool, - &dkg_summary.configs, + &configs, start_height, vec![dealing], ); diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index ae81fc37407d..69626829a087 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -20,7 +20,10 @@ use ic_types::{ }, messages::CallbackId, }; -use std::{collections::BTreeMap, sync::Arc}; +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, +}; pub(super) fn make_setup_initial_dkg_context( registry_version: RegistryVersion, @@ -52,6 +55,23 @@ pub(super) fn make_reshare_chain_key_context( }) } +fn clone_subnet_call_context(context: &SubnetCallContext) -> SubnetCallContext { + match context { + SubnetCallContext::SetupInitialDKG(c) => SubnetCallContext::SetupInitialDKG(c.clone()), + SubnetCallContext::CanisterHttpRequest(c) => { + SubnetCallContext::CanisterHttpRequest(c.clone()) + } + SubnetCallContext::ReshareChainKey(c) => SubnetCallContext::ReshareChainKey(c.clone()), + SubnetCallContext::BitcoinGetSuccessors(c) => { + SubnetCallContext::BitcoinGetSuccessors(c.clone()) + } + SubnetCallContext::BitcoinSendTransactionInternal(c) => { + SubnetCallContext::BitcoinSendTransactionInternal(c.clone()) + } + SubnetCallContext::SignWithThreshold(c) => SubnetCallContext::SignWithThreshold(c.clone()), + } +} + /// Set up the state manager mock to return an initial state containing the /// given subnet call contexts. pub(super) fn complement_state_manager_with_dkg_contexts( @@ -66,13 +86,53 @@ pub(super) fn complement_state_manager_with_dkg_contexts( .subnet_call_context_manager .push_context(context); } + let state = Arc::new(state); let mut mock = state_manager.get_mut(); let expectation = mock .expect_get_state_at() - .return_const(Ok(Labeled::new(Height::new(0), Arc::new(state)))); + .return_const(Ok(Labeled::new(Height::new(0), state.clone()))); + if let Some(times) = times { + expectation.times(times); + } + + mock.expect_get_latest_certified_state() + .return_const(Some(Labeled::new(Height::new(0), state.clone()))); +} + +/// Set up the state manager mock to return an initial state built from a +/// mutable context vector on every call. +pub(super) fn complement_state_manager_with_dkg_contexts_mut( + state_manager: Arc, + contexts: Arc>>, + times: Option, +) { + let mut mock = state_manager.get_mut(); + let state_for_get_state_at = contexts.clone(); + let expectation = mock.expect_get_state_at().returning(move |_| { + let mut state = ic_test_utilities_state::get_initial_state(0, 0); + for context in state_for_get_state_at.lock().unwrap().iter() { + state + .metadata + .subnet_call_context_manager + .push_context(clone_subnet_call_context(context)); + } + Ok(Labeled::new(Height::new(0), Arc::new(state))) + }); if let Some(times) = times { expectation.times(times); } + + let state_for_latest_certified = contexts; + mock.expect_get_latest_certified_state().returning(move || { + let mut state = ic_test_utilities_state::get_initial_state(0, 0); + for context in state_for_latest_certified.lock().unwrap().iter() { + state + .metadata + .subnet_call_context_manager + .push_context(clone_subnet_call_context(context)); + } + Some(Labeled::new(Height::new(0), Arc::new(state))) + }); } pub(super) fn complement_state_manager_with_setup_initial_dkg_request( @@ -140,7 +200,7 @@ pub(super) fn extract_dealings_from_highest_block(pool: &TestConsensusPool) -> D } } -/// Extract the remote dkg transcripts from the current highest validated block +/// Extract remote DKG ids from the highest validated block summary configs. pub(super) fn extract_remote_dkg_ids_from_highest_block( pool: &TestConsensusPool, target_id: NiDkgTargetId, diff --git a/rs/consensus/tests/framework/runner.rs b/rs/consensus/tests/framework/runner.rs index 7caddd443062..d3fbdf72f76a 100644 --- a/rs/consensus/tests/framework/runner.rs +++ b/rs/consensus/tests/framework/runner.rs @@ -172,6 +172,9 @@ impl<'a> ConsensusRunner<'a> { ); let dkg = ic_consensus_dkg::DkgImpl::new( deps.replica_config.node_id, + deps.replica_config.subnet_id, + Arc::clone(&deps.registry_client), + deps.state_manager.clone(), Arc::clone(&consensus_crypto), deps.consensus_pool.read().unwrap().get_cache(), dkg_key_manager, diff --git a/rs/consensus/tests/payload.rs b/rs/consensus/tests/payload.rs index 21d7729a4d8f..3ca038040e09 100644 --- a/rs/consensus/tests/payload.rs +++ b/rs/consensus/tests/payload.rs @@ -91,6 +91,12 @@ fn consensus_produces_expected_batches() { Height::new(0), Arc::new(get_initial_state(0, 0)), ))); + state_manager + .expect_get_latest_certified_state() + .return_const(Some(Labeled::new( + Height::new(0), + Arc::new(get_initial_state(0, 0)), + ))); state_manager .expect_get_certified_state_snapshot() .returning(|| None); @@ -184,6 +190,9 @@ fn consensus_produces_expected_batches() { ic_consensus::consensus::ConsensusBouncer::new(&metrics_registry, router.clone()); let dkg = ic_consensus_dkg::DkgImpl::new( replica_config.node_id, + replica_config.subnet_id, + Arc::clone(®istry_client) as Arc<_>, + Arc::clone(&state_manager) as Arc<_>, Arc::clone(&fake_crypto) as Arc<_>, Arc::clone(&consensus_cache), dkg_key_manager, diff --git a/rs/tests/consensus/subnet_recovery/common.rs b/rs/tests/consensus/subnet_recovery/common.rs index fdbc42223dbf..7118e42bcc70 100644 --- a/rs/tests/consensus/subnet_recovery/common.rs +++ b/rs/tests/consensus/subnet_recovery/common.rs @@ -426,14 +426,14 @@ fn app_subnet_recovery_test(env: TestEnv, cfg: TestConfig) { .expect("there is no source subnet"); let source_subnet_id = source_subnet.subnet_id; - await_node_certified_height( - &source_subnet - .nodes() - .next() - .expect("there is no node in the source subnet"), - Height::from(1000), - logger.clone(), - ); + // await_node_certified_height( + // &source_subnet + // .nodes() + // .next() + // .expect("there is no node in the source subnet"), + // Height::from(1000), + // logger.clone(), + // ); let create_new_subnet = !topology_snapshot .subnets() From cf0fb8d1ce1101088faea8a6022c5c52bddc921b Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Wed, 15 Apr 2026 12:49:07 +0000 Subject: [PATCH 93/98] fix --- rs/consensus/dkg/src/lib.rs | 5 +---- rs/consensus/dkg/src/payload_builder.rs | 9 +-------- rs/consensus/dkg/src/payload_validator.rs | 1 + 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index ec2ef4c719e8..28bded9e811f 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -314,7 +314,7 @@ pub(crate) fn merge_configs<'a>( logger: &ReplicaLogger, ) -> BTreeMap<&'a NiDkgId, &'a NiDkgConfig> { let mut merged_configs: BTreeMap<&NiDkgId, &NiDkgConfig> = summary_configs.iter().collect(); - for (_, config_result) in config_results { + for config_result in config_results.values() { let configs = match config_result { Ok(configs) => configs, Err(errs) => { @@ -1000,9 +1000,6 @@ mod tests { }); } - #[test] - fn test_config_generation_failures_are_added_to_summary_blocks() {} - /// These components are used for the validation tests. struct ValidationTestComponents { dkg: DkgImpl, diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 8cc5a7eb4a36..ccd6fc16385a 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -194,29 +194,23 @@ fn select_dealings_for_payload( *cap = cap.saturating_sub(1); } } - println!("remaining capacity: {:?}", remaining_capacity); // Filter dealings whose dealer has no dealing on the chain yet, and for which // the collection_threshold hasn't been reached. Prioritize remote DKGs. let (remote_priority, rest): (Vec<_>, Vec<_>) = dkg_pool .get_validated() .filter(|msg| { - println!("msg: {:?}", msg.content.dkg_id); // Make sure the message relates to one of the ongoing DKGs, it's from a unique // dealer, and the collection_threshold hasn't been reached yet. let Some(cap) = remaining_capacity.get_mut(&msg.content.dkg_id) else { - println!("no cap for dkg id: {:?}", msg.content.dkg_id); return false; }; if dealers_from_chain.contains(&(msg.content.dkg_id.clone(), msg.signature.signer)) { - println!("dealer from chain: {:?}", msg.signature.signer); return false; } if *cap > 0 { *cap -= 1; - println!("cap: {:?}", *cap); true } else { - println!("cap is 0"); false } }) @@ -296,8 +290,7 @@ pub(crate) fn create_early_remote_transcripts( for config in configs.iter() { // Generate the transcript. We need to retry transient errors, as a payload containing // transient errors may not be verifiable by peers. - let transcript_result = match create_transcript(crypto, config, &all_dealings, &logger) - { + let transcript_result = match create_transcript(crypto, config, &all_dealings, logger) { Ok(transcript) => Ok(transcript), // Note that we handled the reproducible error case of not having enough dealings // already beforehand. diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index 9d67ff766c3f..1287889c8a8a 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -141,6 +141,7 @@ pub fn validate_payload( } // Validates the payload containing dealings. +#[allow(clippy::too_many_arguments)] #[allow(clippy::result_large_err)] fn validate_dealings_payload( subnet_id: SubnetId, From 5097da3088ed34da803fd0c4b9f2d1d42844540b Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 16 Apr 2026 08:11:26 +0000 Subject: [PATCH 94/98] fix --- rs/consensus/dkg/src/lib.rs | 14 +++++++------- rs/consensus/dkg/src/payload_builder.rs | 10 +++++----- rs/tests/consensus/subnet_recovery/common.rs | 17 ++++------------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index 28bded9e811f..40a1aca8d170 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -1814,7 +1814,7 @@ mod tests { )], ); - let target_id = NiDkgTargetId::new([0u8; 32]); + let target_id = NiDkgTargetId::new([0_u8; 32]); complement_state_manager_with_setup_initial_dkg_request( deps.state_manager.clone(), deps.registry.get_latest_version(), @@ -2048,7 +2048,7 @@ mod tests { original_summary.next_transcripts().clone(), vec![( removed_dkg_id, - ic_types::messages::CallbackId::from(0u64), + ic_types::messages::CallbackId::from(0_u64), Ok(dummy_transcript), )], original_summary.registry_version, @@ -2158,7 +2158,7 @@ mod tests { // If a config exists in the summary but there is no corresponding // context in the state, no early transcript should be created. - let unrelated_target_id = NiDkgTargetId::new([1u8; 32]); + let unrelated_target_id = NiDkgTargetId::new([1_u8; 32]); let no_match_state_manager = Arc::new(ic_test_utilities::state_manager::RefMockStateManager::default()); complement_state_manager_with_setup_initial_dkg_request( @@ -2317,7 +2317,7 @@ mod tests { curve: VetKdCurve::Bls12_381_G2, name: String::from("some_vetkey"), }; - let target_id = NiDkgTargetId::new([0u8; 32]); + let target_id = NiDkgTargetId::new([0_u8; 32]); let mut deps = dependencies_with_subnet_records_with_raw_state_manager( pool_config, @@ -2417,9 +2417,9 @@ mod tests { }; // This context always comes first but will be // skipped because one of its two configs lacks dealings. - let skipped_target_id = NiDkgTargetId::new([0u8; 32]); - let setup_target_id = NiDkgTargetId::new([1u8; 32]); - let reshare_target_id = NiDkgTargetId::new([2u8; 32]); + let skipped_target_id = NiDkgTargetId::new([0_u8; 32]); + let setup_target_id = NiDkgTargetId::new([1_u8; 32]); + let reshare_target_id = NiDkgTargetId::new([2_u8; 32]); let mut deps = dependencies_with_subnet_records_with_raw_state_manager( pool_config, diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index ccd6fc16385a..ef3e9011b177 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -2225,10 +2225,10 @@ mod tests { let tag = NiDkgTag::HighThresholdForKey(ni_dkg_key_id); let registry_version = registry.get_latest_version(); - let completed_init_dkg_target = NiDkgTargetId::new([1u8; 32]); - let pending_init_dkg_target = NiDkgTargetId::new([2u8; 32]); - let completed_reshare_target = NiDkgTargetId::new([3u8; 32]); - let pending_reshare_target = NiDkgTargetId::new([4u8; 32]); + let completed_init_dkg_target = NiDkgTargetId::new([1_u8; 32]); + let pending_init_dkg_target = NiDkgTargetId::new([2_u8; 32]); + let completed_reshare_target = NiDkgTargetId::new([3_u8; 32]); + let pending_reshare_target = NiDkgTargetId::new([4_u8; 32]); let mut state = ic_test_utilities_state::get_initial_state(0, 0); let target_nodes: BTreeSet<_> = @@ -2360,7 +2360,7 @@ mod tests { start_block_height: Height::from(0), dealer_subnet: subnet_test_id(0), dkg_tag: tag, - target_subnet: NiDkgTargetSubnet::Remote(NiDkgTargetId::new([0u8; 32])), + target_subnet: NiDkgTargetSubnet::Remote(NiDkgTargetId::new([0_u8; 32])), } } diff --git a/rs/tests/consensus/subnet_recovery/common.rs b/rs/tests/consensus/subnet_recovery/common.rs index 7118e42bcc70..c6e9f4a4d2d8 100644 --- a/rs/tests/consensus/subnet_recovery/common.rs +++ b/rs/tests/consensus/subnet_recovery/common.rs @@ -87,7 +87,7 @@ const NNS_NODES_LARGE: usize = 40; const APP_NODES_LARGE: usize = 37; /// 40 dealings * 4 transcripts being reshared (high/local, low/local, high/remote, low/remote) /// plus 14 as a safety margin -const DKG_INTERVAL_LARGE: u64 = 499; +const DKG_INTERVAL_LARGE: u64 = 4 * NNS_NODES_LARGE as u64 + 14; /// A very large DKG interval to test recovery when the subnet stalls during its first DKG /// interval. @@ -419,21 +419,12 @@ fn app_subnet_recovery_test(env: TestEnv, cfg: TestConfig) { )); // The first application subnet encountered during iteration is the source subnet because it was inserted first. - let source_subnet = env + let source_subnet_id = env .topology_snapshot() .subnets() .find(|subnet| subnet.subnet_type() == SubnetType::Application) - .expect("there is no source subnet"); - - let source_subnet_id = source_subnet.subnet_id; - // await_node_certified_height( - // &source_subnet - // .nodes() - // .next() - // .expect("there is no node in the source subnet"), - // Height::from(1000), - // logger.clone(), - // ); + .expect("there is no source subnet") + .subnet_id; let create_new_subnet = !topology_snapshot .subnets() From 3333cb96572a8deddebd4a52cd57596c84850984 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 16 Apr 2026 08:11:59 +0000 Subject: [PATCH 95/98] fix --- rs/tests/consensus/subnet_recovery/common.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rs/tests/consensus/subnet_recovery/common.rs b/rs/tests/consensus/subnet_recovery/common.rs index c6e9f4a4d2d8..95cf3cb6751b 100644 --- a/rs/tests/consensus/subnet_recovery/common.rs +++ b/rs/tests/consensus/subnet_recovery/common.rs @@ -37,10 +37,7 @@ use crate::utils::{ use canister_test::Canister; use ic_base_types::NodeId; use ic_consensus_system_test_utils::{ - node::{ - assert_node_is_assigned_with_ssh_session, assert_node_is_unassigned_with_ssh_session, - await_node_certified_height, - }, + node::{assert_node_is_assigned_with_ssh_session, assert_node_is_unassigned_with_ssh_session}, rw_message::{install_nns_and_check_progress, store_message_with_retries}, ssh_access::{disable_ssh_access_to_node, wait_until_authentication_is_granted}, subnet::{ From 899c3dc5df9016b17427defe5a2d8e8010f48a70 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 16 Apr 2026 09:41:59 +0000 Subject: [PATCH 96/98] fix --- rs/consensus/dkg/src/payload_builder.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index ef3e9011b177..6d0098f72dad 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -942,6 +942,7 @@ fn process_reshare_chain_key_contexts( // Dealers must be in the same registry_version. let dealers = get_node_list(this_subnet_id, registry_client, context.registry_version)?; + let get_transcript = |tag: &NiDkgTag| reshared_transcripts.get(tag).cloned(); match create_remote_dkg_config_for_key_id( key_id, start_block_height, @@ -949,7 +950,7 @@ fn process_reshare_chain_key_contexts( context.target_id, &dealers, &context.nodes, - reshared_transcripts, + get_transcript, &context.registry_version, ) { Ok(config) => { @@ -1147,6 +1148,13 @@ pub fn build_callback_id_config_map( // Return an error if the dealers cannot be retrieved let dealers = get_node_list(this_subnet_id, registry_client, context.registry_version)?; + let get_transcript = |tag: &NiDkgTag| { + dkg_summary + .next_transcripts() + .get(tag) + .cloned() + .or_else(|| dkg_summary.current_transcripts().get(tag).cloned()) + }; let result = create_remote_dkg_config_for_key_id( key_id, dkg_summary.height, @@ -1154,7 +1162,7 @@ pub fn build_callback_id_config_map( context.target_id, &dealers, &context.nodes, - dkg_summary.next_transcripts(), + get_transcript, &context.registry_version, ) .map(|config| vec![config]) @@ -1269,7 +1277,7 @@ fn create_remote_dkg_config_for_key_id( target_id: NiDkgTargetId, dealers: &BTreeSet, receivers: &BTreeSet, - reshared_transcripts: &BTreeMap, + get_transcript: impl Fn(&NiDkgTag) -> Option, registry_version: &RegistryVersion, ) -> Result> { let dkg_id = NiDkgId { @@ -1280,7 +1288,7 @@ fn create_remote_dkg_config_for_key_id( }; // Find the resharing transcript corresponding to the remote dkg id - let Some(resharing_transcript) = reshared_transcripts.get(&dkg_id.dkg_tag) else { + let Some(resharing_transcript) = get_transcript(&dkg_id.dkg_tag) else { let err = format!( "Failed to find resharing transcript for a remote dkg for tag {:?}", &dkg_id.dkg_tag @@ -1293,7 +1301,7 @@ fn create_remote_dkg_config_for_key_id( dealers, receivers, registry_version, - Some(resharing_transcript.clone()), + Some(resharing_transcript), ) .map_err(|err| Box::new((dkg_id, format!("{err:?}")))) } From 11abd3da8017354b07a22d979ab939c7f2de36d7 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Thu, 16 Apr 2026 10:12:27 +0000 Subject: [PATCH 97/98] fix --- rs/consensus/dkg/src/payload_builder.rs | 50 ++----------------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index 5ddcf2cb4b3a..50562892d7a8 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -2451,31 +2451,6 @@ mod tests { } } - #[test] - fn test_select_dealings_prioritizes_remote_over_local() { - let local_id = local_dkg_id(NiDkgTag::LowThreshold); - let remote_id = remote_dkg_id(NiDkgTag::LowThreshold); - - let configs: BTreeMap<_, _> = [ - (local_id.clone(), make_test_config(local_id.clone(), 1)), - (remote_id.clone(), make_test_config(remote_id.clone(), 1)), - ] - .into(); - - // Create dealings: local first in the list, then remote. - let pool = TestDkgPool { - messages: vec![ - create_dealing(0, local_id.clone()), - create_dealing(1, remote_id.clone()), - ], - }; - let selected = - select_dealings_for_payload(configs.iter().collect(), &HashSet::new(), &pool, 1); - - assert_eq!(selected.len(), 1); - assert_eq!(selected[0].content.dkg_id, remote_id); - } - #[test] fn test_select_dealings_caps_at_collection_threshold() { // collection_threshold = max_corrupt_dealers + 1 = 2 @@ -2492,27 +2467,10 @@ mod tests { // Only collection_threshold (2) dealings should be included. assert_eq!(selected.len(), 2); - } - - #[test] - fn test_select_dealings_accounts_for_dealers_from_chain() { - // collection_threshold = 2 - let id = local_dkg_id(NiDkgTag::LowThreshold); - let configs: BTreeMap<_, _> = [(id.clone(), make_test_config(id.clone(), 1))].into(); - - // 1 dealing already on chain - let dealers_from_chain: HashSet<_> = [(id.clone(), node_test_id(0))].into(); - - // 3 new dealings (from different dealers) - let pool = TestDkgPool { - messages: (1..4).map(|i| create_dealing(i, id.clone())).collect(), - }; - - let selected = - select_dealings_for_payload(configs.iter().collect(), &dealers_from_chain, &pool, 10); - - // Only 1 more needed to reach collection_threshold of 2. - assert_eq!(selected.len(), 1); + for (i, msg) in selected.iter().enumerate() { + assert_eq!(msg.content.dkg_id, id); + assert_eq!(msg.signature.signer, node_test_id(i as u64)); + } } #[test] From 7074dd44d42170fb8430256d5171e3b67bbf3562 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn <99166915+eichhorl@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:22:13 +0200 Subject: [PATCH 98/98] Revert "Draft: create configs early" --- rs/consensus/dkg/src/lib.rs | 444 ++++++---------------- rs/consensus/dkg/src/payload_builder.rs | 214 ++++++----- rs/consensus/dkg/src/payload_validator.rs | 51 +-- rs/consensus/dkg/src/test_utils.rs | 61 +-- rs/consensus/tests/framework/runner.rs | 3 - rs/consensus/tests/payload.rs | 9 - rs/replica/setup_ic_network/src/lib.rs | 3 - 7 files changed, 244 insertions(+), 541 deletions(-) diff --git a/rs/consensus/dkg/src/lib.rs b/rs/consensus/dkg/src/lib.rs index fe777919c944..69db12c562b2 100644 --- a/rs/consensus/dkg/src/lib.rs +++ b/rs/consensus/dkg/src/lib.rs @@ -9,16 +9,13 @@ use ic_interfaces::{ p2p::consensus::{Bouncer, BouncerFactory, BouncerValue, PoolMutationsProducer}, validation::ValidationResult, }; -use ic_interfaces_registry::RegistryClient; -use ic_interfaces_state_manager::StateReader; use ic_logger::{ReplicaLogger, error, info}; use ic_metrics::{ MetricsRegistry, buckets::{decimal_buckets, linear_buckets}, }; -use ic_replicated_state::ReplicatedState; use ic_types::{ - Height, NodeId, ReplicaVersion, SubnetId, + Height, NodeId, ReplicaVersion, consensus::dkg::{DealingContent, DkgMessageId, InvalidDkgPayloadReason, Message}, crypto::{ Signed, @@ -35,9 +32,9 @@ use std::{ pub mod dkg_key_manager; pub mod payload_builder; pub mod payload_validator; +#[allow(dead_code)] pub(crate) mod remote; -use crate::remote::{build_callback_id_config_map, merge_configs}; pub use crate::utils::get_vetkey_public_keys; #[cfg(test)] @@ -70,9 +67,6 @@ struct Metrics { /// changes in the consensus and DKG pool. pub struct DkgImpl { node_id: NodeId, - subnet_id: SubnetId, - registry_client: Arc, - state_reader: Arc>, crypto: Arc, consensus_cache: Arc, dkg_key_manager: Arc>, @@ -84,9 +78,6 @@ impl DkgImpl { /// Build a new DKG component pub fn new( node_id: NodeId, - subnet_id: SubnetId, - registry_client: Arc, - state_reader: Arc>, crypto: Arc, consensus_cache: Arc, dkg_key_manager: Arc>, @@ -94,12 +85,9 @@ impl DkgImpl { logger: ReplicaLogger, ) -> Self { Self { - node_id, - subnet_id, - registry_client, - state_reader, crypto, consensus_cache, + node_id, dkg_key_manager, logger, metrics: Metrics { @@ -195,7 +183,7 @@ impl DkgImpl { fn validate_dealings_for_dealer( &self, dkg_pool: &dyn DkgPool, - configs: &BTreeMap<&NiDkgId, &NiDkgConfig>, + configs: &BTreeMap, dkg_start_height: Height, messages: Vec<&Message>, ) -> Mutations { @@ -223,12 +211,9 @@ impl DkgImpl { } // If the dealing refers a config which is not among the ongoing DKGs, - // we reject it, unless it is a remote DKG, in which case we skip it. + // we reject it. let config = match configs.get(message_dkg_id) { Some(config) => config, - None if message_dkg_id.target_subnet != NiDkgTargetSubnet::Local => { - return Mutations::new(); - } None => { return get_handle_invalid_change_action( message, @@ -323,24 +308,8 @@ impl PoolMutationsProducer for DkgImpl { return ChangeAction::Purge(start_height).into(); } - let remote_config_results = self - .state_reader - .get_latest_certified_state() - .and_then(|state| { - build_callback_id_config_map( - self.subnet_id, - self.registry_client.as_ref(), - state.get_ref(), - self.registry_client.get_latest_version(), - dkg_summary, - &self.logger, - ) - .ok() - }) - .unwrap_or_default(); - let configs = merge_configs(&dkg_summary.configs, &remote_config_results); - - let change_set: Mutations = configs + let change_set: Mutations = dkg_summary + .configs .par_iter() .filter_map(|(_id, config)| self.create_dealing(dkg_pool, config)) .collect(); @@ -369,7 +338,7 @@ impl PoolMutationsProducer for DkgImpl { .map(|dealings| { self.validate_dealings_for_dealer( dkg_pool, - &configs, + &dkg_summary.configs, start_height, dealings.to_vec(), ) @@ -430,7 +399,7 @@ impl BouncerFactory for DkgBouncer { mod tests { use super::*; use crate::test_utils::{ - complement_state_manager_with_dkg_contexts, complement_state_manager_with_dkg_contexts_mut, + complement_state_manager_with_dkg_contexts, complement_state_manager_with_reshare_chain_key_request, complement_state_manager_with_setup_initial_dkg_request, create_dealing, extract_dkg_configs_from_highest_block, extract_remote_dkg_ids_from_highest_block, @@ -451,7 +420,6 @@ mod tests { }; use ic_interfaces_mocks::crypto::MockCrypto; use ic_interfaces_registry::RegistryClient; - use ic_interfaces_state_manager::Labeled; use ic_logger::no_op_logger; use ic_management_canister_types_private::{MasterPublicKeyId, VetKdCurve, VetKdKeyId}; use ic_metrics::MetricsRegistry; @@ -460,7 +428,6 @@ mod tests { use ic_test_utilities_consensus::fake::{FakeContentSigner, FromParent}; use ic_test_utilities_logger::with_test_replica_logger; use ic_test_utilities_registry::{SubnetRecordBuilder, add_subnet_record}; - use ic_test_utilities_state::get_initial_state; use ic_test_utilities_types::ids::{node_test_id, subnet_test_id}; use ic_types::{ NumberOfNodes, RegistryVersion, ReplicaVersion, @@ -525,8 +492,6 @@ mod tests { crypto, mut pool, dkg_pool, - registry, - state_manager, .. } = dependencies_with_subnet_params( pool_config, @@ -539,13 +504,6 @@ mod tests { .build(), )], ); - state_manager - .get_mut() - .expect_get_latest_certified_state() - .return_const(Some(Labeled::new( - Height::new(0), - Arc::new(get_initial_state(0, 0)), - ))); // Now we instantiate the DKG component for node Id = 1, who is a dealer. let replica_1 = node_test_id(1); @@ -553,9 +511,6 @@ mod tests { new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg = DkgImpl::new( replica_1, - subnet_id, - registry.clone(), - state_manager.clone(), crypto.clone(), pool.get_cache(), dkg_key_manager.clone(), @@ -623,9 +578,6 @@ mod tests { new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg_2 = DkgImpl::new( replica_2, - subnet_id, - registry, - state_manager, crypto, pool.get_cache(), dkg_key_manager_2.clone(), @@ -690,20 +642,8 @@ mod tests { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { with_test_replica_logger(|logger| { let Dependencies { - mut pool, - crypto, - registry, - state_manager, - replica_config, - .. + mut pool, crypto, .. } = dependencies(pool_config.clone(), 2); - state_manager - .get_mut() - .expect_get_latest_certified_state() - .return_const(Some(Labeled::new( - Height::new(0), - Arc::new(get_initial_state(0, 0)), - ))); let mut dkg_pool = DkgPoolImpl::new(MetricsRegistry::new(), logger.clone(), Height::from(0)); // Let's check that replica 3, who's not a dealer, does not produce dealings. @@ -711,9 +651,6 @@ mod tests { new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg = DkgImpl::new( node_test_id(3), - replica_config.subnet_id, - registry.clone(), - state_manager.clone(), crypto.clone(), pool.get_cache(), dkg_key_manager, @@ -727,9 +664,6 @@ mod tests { new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg = DkgImpl::new( node_test_id(1), - replica_config.subnet_id, - registry, - state_manager, crypto, pool.get_cache(), dkg_key_manager.clone(), @@ -798,7 +732,6 @@ mod tests { crypto, registry, state_manager, - dkg_pool, .. } = dependencies_with_subnet_records_with_raw_state_manager( pool_config, @@ -813,7 +746,7 @@ mod tests { let target_id = NiDkgTargetId::new([0_u8; 32]); complement_state_manager_with_setup_initial_dkg_request( - state_manager.clone(), + state_manager, registry.get_latest_version(), vec![10, 11, 12], None, @@ -825,9 +758,6 @@ mod tests { new_dkg_key_manager(crypto.clone(), logger.clone(), &PoolReader::new(&pool)); let dkg = DkgImpl::new( node_test_id(1), - subnet_id, - registry.clone(), - state_manager.clone(), crypto, pool.get_cache(), dkg_key_manager.clone(), @@ -835,10 +765,43 @@ mod tests { logger.clone(), ); - // We will create dealings for remote requests immediately, even if we haven't - // reached a summary block yet. + // We did not advance the consensus pool yet. The configs for remote transcripts + // are not added to a summary block yet. That's why we see two dealings for + // local thresholds. + let mut dkg_pool = + DkgPoolImpl::new(MetricsRegistry::new(), logger, Height::from(0)); sync_dkg_key_manager(&dkg_key_manager, &pool); - let change_set = dkg.on_state_change(&*dkg_pool.read().unwrap()); + let change_set = dkg.on_state_change(&dkg_pool); + match &change_set.as_slice() { + &[ + ChangeAction::AddToValidated(a), + ChangeAction::AddToValidated(b), + ] => { + assert_eq!(a.content.dkg_id.target_subnet, NiDkgTargetSubnet::Local); + assert_eq!(b.content.dkg_id.target_subnet, NiDkgTargetSubnet::Local); + } + val => panic!("Unexpected change set: {:?}", val), + }; + + // Apply the changes and make sure, we do not produce any dealings anymore. + dkg_pool.apply(change_set); + assert!(dkg.on_state_change(&dkg_pool).is_empty()); + + // Advance _past_ the new summary to make sure the configs for remote + // transcripts are added into the summary. + pool.advance_round_normal_operation_n(dkg_interval_length + 1); + + // First we expect a new purge. + let change_set = dkg.on_state_change(&dkg_pool); + match &change_set.as_slice() { + &[ChangeAction::Purge(purge_height)] + if *purge_height == Height::from(dkg_interval_length + 1) => {} + val => panic!("Unexpected change set: {:?}", val), + }; + dkg_pool.apply(change_set); + + // And then we validate two local and two remote dealings. + let change_set = dkg.on_state_change(&dkg_pool); match &change_set.as_slice() { &[ ChangeAction::AddToValidated(a), @@ -866,73 +829,14 @@ mod tests { val => panic!("Unexpected change set: {:?}", val), }; // Just check again, we do not reproduce a dealing once changes are applied. - dkg_pool.write().unwrap().apply(change_set); - assert!(dkg.on_state_change(&*dkg_pool.read().unwrap()).is_empty()); - - pool.advance_round_normal_operation(); - let dealings = extract_dealings_from_highest_block(&pool); - assert_eq!(dealings.len(), 4); - let remote_dealings = dealings - .iter() - .filter(|d| { - d.content.dkg_id.target_subnet == NiDkgTargetSubnet::Remote(target_id) - }) - .count(); - assert_eq!(remote_dealings, 2); - - // Once enough remote dealings are available on chain, they are turned into - // early remote transcripts in the data payload. - pool.advance_round_normal_operation(); - let early_remote = extract_remote_dkgs_from_highest_block(&pool); - assert_eq!(early_remote.len(), 2); - for (dkg_id, _, result) in &early_remote { - assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); - assert!(result.is_ok()); - } - - // After the next summary, remote transcripts are finalized and we should not - // attempt creating remote dealings again. - pool.advance_round_normal_operation_n(dkg_interval_length); - let latest_summary = PoolReader::new(&pool).get_highest_finalized_summary_block(); - assert_eq!( - latest_summary - .payload - .as_ref() - .as_summary() - .dkg - .initial_dkg_attempts - .get(&target_id), - Some(&0), - "Expected initial_dkg_attempts[{target_id:?}] to be 0" - ); - let change_set = dkg.on_state_change(&*dkg_pool.read().unwrap()); - match &change_set.as_slice() { - &[ChangeAction::Purge(purge_height)] if *purge_height == Height::from(100) => {} - val => panic!("Unexpected change set: {:?}", val), - }; - dkg_pool.write().unwrap().apply(change_set); - - let change_set = dkg.on_state_change(&*dkg_pool.read().unwrap()); - let remote_dealings = change_set - .iter() - .filter(|change| match change { - ChangeAction::AddToValidated(message) => { - message.content.dkg_id.target_subnet - == NiDkgTargetSubnet::Remote(target_id) - } - _ => false, - }) - .count(); - assert_eq!( - remote_dealings, 0, - "Unexpected remote dealings: {change_set:?}" - ); + dkg_pool.apply(change_set); + assert!(dkg.on_state_change(&dkg_pool).is_empty()); }); }); } #[test] - fn test_config_generation_failures_are_added_to_data_blocks() { + fn test_config_generation_failures_are_added_to_the_summary() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { use ic_types::crypto::threshold_sig::ni_dkg::*; let node_ids = vec![node_test_id(0), node_test_id(1)]; @@ -963,24 +867,12 @@ mod tests { Some(target_id), ); - // Advance one round - pool.advance_round_normal_operation(); - // Verify that the latest block contains errors for both requests - let block: Block = PoolReader::new(&pool).get_finalized_tip(); - if let BlockPayload::Data(data) = block.payload.as_ref() { - assert_eq!(data.dkg.transcripts_for_remote_subnets.len(), 2); - for dkg in data.dkg.transcripts_for_remote_subnets.iter() { - assert!(dkg.2.is_err()); - } - } else { - panic!("block at height {} is not a data block", block.height.get()); - } - - // Advance _past_ the new summary to make sure the replicas don't attempt to create + // Advance _past_ the new summary to make sure the replicas attempt to create // the configs for remote transcripts. pool.advance_round_normal_operation_n(dkg_interval_length + 1); - // Verify that the first summary block contains only two local configs + // Verify that the first summary block contains only two local configs and the + // two errors for the remote DKG request. let block: Block = PoolReader::new(&pool).get_highest_finalized_summary_block(); if let BlockPayload::Summary(summary) = block.payload.as_ref() { assert_eq!( @@ -992,7 +884,11 @@ mod tests { for (dkg_id, _) in summary.dkg.configs.iter() { assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Local); } - assert_eq!(summary.dkg.transcripts_for_remote_subnets.len(), 0); + assert_eq!(summary.dkg.transcripts_for_remote_subnets.len(), 2); + for (dkg_id, _, result) in summary.dkg.transcripts_for_remote_subnets.iter() { + assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); + assert!(result.is_err()); + } } else { panic!( "block at height {} is not a summary block", @@ -1023,29 +919,8 @@ mod tests { let node_id_1 = node_test_id(1); // This is not a dealer! let node_id_2 = node_test_id(0); - let Dependencies { - pool: consensus_pool_1, - registry: registry_1, - state_manager: state_manager_1, - replica_config: replica_config_1, - .. - } = dependencies(pool_config_1, 2); - let Dependencies { - pool: consensus_pool_2, - registry: registry_2, - state_manager: state_manager_2, - replica_config: replica_config_2, - .. - } = dependencies(pool_config_2, 2); - for state_manager in [&state_manager_1, &state_manager_2] { - state_manager - .get_mut() - .expect_get_latest_certified_state() - .return_const(Some(Labeled::new( - Height::new(0), - Arc::new(get_initial_state(0, 0)), - ))); - } + let consensus_pool_1 = dependencies(pool_config_1, 2).pool; + let consensus_pool_2 = dependencies(pool_config_2, 2).pool; with_test_replica_logger(|logger| { let dkg_pool_1 = @@ -1061,9 +936,6 @@ mod tests { ); let dkg_1 = DkgImpl::new( node_id_1, - replica_config_1.subnet_id, - registry_1, - state_manager_1, crypto.clone(), consensus_pool_1.get_cache(), dkg_key_manager_1.clone(), @@ -1078,9 +950,6 @@ mod tests { ); let dkg_2 = DkgImpl::new( node_id_2, - replica_config_2.subnet_id, - registry_2, - state_manager_2, crypto.clone(), consensus_pool_2.get_cache(), dkg_key_manager_2.clone(), @@ -1524,12 +1393,6 @@ mod tests { let crypto_1 = dependencies_1.crypto.clone(); let crypto_2 = dependencies_2.crypto.clone(); - let registry_1 = dependencies_1.registry.clone(); - let registry_2 = dependencies_2.registry.clone(); - let state_manager_1 = dependencies_1.state_manager.clone(); - let state_manager_2 = dependencies_2.state_manager.clone(); - let subnet_id_1 = dependencies_1.replica_config.subnet_id; - let subnet_id_2 = dependencies_2.replica_config.subnet_id; let mut pool_1 = dependencies_1.pool; let mut pool_2 = dependencies_2.pool; @@ -1574,9 +1437,6 @@ mod tests { ); let dkg_1 = DkgImpl::new( node_test_id(1), - subnet_id_1, - registry_1, - state_manager_1, crypto_1, pool_1.get_cache(), dgk_key_manager_1.clone(), @@ -1586,9 +1446,6 @@ mod tests { let dkg_2 = DkgImpl::new( node_test_id(2), - subnet_id_2, - registry_2, - state_manager_2, crypto_2.clone(), pool_2.get_cache(), new_dkg_key_manager(crypto_2, logger.clone(), &PoolReader::new(&pool_2)), @@ -1893,7 +1750,6 @@ mod tests { .collect::>(); deps.dkg_pool.write().unwrap().apply(dealings); deps.pool.advance_round_normal_operation(); - // f + 1 dealings for high or low remote threshold DKG assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 3); assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); @@ -1908,7 +1764,6 @@ mod tests { .collect::>(); deps.dkg_pool.write().unwrap().apply(dealings); deps.pool.advance_round_normal_operation(); - // f + 1 dealings for low or high remote threshold DKG assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 3); assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); @@ -1988,13 +1843,13 @@ mod tests { }); } - /// Tests that early remote transcripts are created when a + /// Tests that no early remote transcripts are created when a /// setup_initial_dkg target has only one of its expected two configs /// in the summary (because one transcript was already created in a - /// previous summary block). We will still retry both of the configs - /// as part of early remote transcript creation. + /// previous summary block). Instead, the next summary block should + /// contain both transcripts. #[test] - fn test_early_transcripts_are_createdfor_single_setup_initial_dkg_config() { + fn test_no_early_transcripts_for_single_setup_initial_dkg_config() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let (mut deps, target_id, remote_dkg_ids) = setup_initial_dkg_test(pool_config); @@ -2060,19 +1915,6 @@ mod tests { certified_height: Height::from(0), time: UNIX_EPOCH, }; - let state = deps - .state_manager - .get_state_at(validation_context.certified_height) - .unwrap(); - let modified_callback_id_map = remote::build_callback_id_config_map( - subnet_test_id(0), - deps.registry.as_ref(), - state.get_ref(), - validation_context.registry_version, - &modified_summary, - &no_op_logger(), - ) - .unwrap(); // Even though sufficient dealings exist on chain for both configs, // no early transcript should be created because the summary only @@ -2081,15 +1923,17 @@ mod tests { &pool_reader, deps.crypto.as_ref(), &parent, - modified_callback_id_map, - &no_op_logger(), + &modified_summary, + deps.state_manager.as_ref(), + &validation_context, + no_op_logger(), ) .unwrap(); - assert_eq!(early_transcripts.len(), 2,); - for (dkg_id, _callback_id, result) in &early_transcripts { - assert_eq!(dkg_id.target_subnet, NiDkgTargetSubnet::Remote(target_id)); - assert!(result.is_ok()); - } + assert!( + early_transcripts.is_empty(), + "No early transcripts should be created for a single \ + setup_initial_dkg config, but got {early_transcripts:?}", + ); // The next summary should contain both transcripts: the one already // in the modified summary's transcripts_for_remote_subnets and the @@ -2119,21 +1963,14 @@ mod tests { // Control: using the original summary with both configs DOES // produce early transcripts. - let original_callback_id_map = remote::build_callback_id_config_map( - subnet_test_id(0), - deps.registry.as_ref(), - state.get_ref(), - validation_context.registry_version, - &original_summary, - &no_op_logger(), - ) - .unwrap(); let early_transcripts = payload_builder::create_early_remote_transcripts( &pool_reader, deps.crypto.as_ref(), &parent, - original_callback_id_map, - &no_op_logger(), + &original_summary, + deps.state_manager.as_ref(), + &validation_context, + no_op_logger(), ) .unwrap(); assert_eq!(early_transcripts.len(), 2); @@ -2154,24 +1991,14 @@ mod tests { None, Some(unrelated_target_id), ); - let no_match_state = no_match_state_manager - .get_state_at(validation_context.certified_height) - .unwrap(); - let no_match_callback_id_map = remote::build_callback_id_config_map( - subnet_test_id(0), - deps.registry.as_ref(), - no_match_state.get_ref(), - validation_context.registry_version, - &original_summary, - &no_op_logger(), - ) - .unwrap(); let early_transcripts = payload_builder::create_early_remote_transcripts( &pool_reader, deps.crypto.as_ref(), &parent, - no_match_callback_id_map, - &no_op_logger(), + &original_summary, + no_match_state_manager.as_ref(), + &validation_context, + no_op_logger(), ) .unwrap(); assert!( @@ -2216,26 +2043,15 @@ mod tests { certified_height: Height::from(0), time: UNIX_EPOCH, }; - let state = deps - .state_manager - .get_state_at(validation_context.certified_height) - .unwrap(); - let callback_id_map = remote::build_callback_id_config_map( - subnet_test_id(0), - deps.registry.as_ref(), - state.get_ref(), - validation_context.registry_version, - last_summary, - &no_op_logger(), - ) - .unwrap(); let early_transcripts = payload_builder::create_early_remote_transcripts( &pool_reader, &mock_crypto, &parent, - callback_id_map, - &no_op_logger(), + last_summary, + deps.state_manager.as_ref(), + &validation_context, + no_op_logger(), ) .unwrap(); @@ -2326,35 +2142,21 @@ mod tests { )], ); - let contexts = Arc::new(Mutex::new(Vec::new())); - complement_state_manager_with_dkg_contexts_mut( + complement_state_manager_with_reshare_chain_key_request( deps.state_manager.clone(), - contexts.clone(), + deps.registry.get_latest_version(), + key_id.clone(), + vec![10, 11, 12, 13], None, + Some(target_id), ); - // Advance until the first vetkd transcript is created deps.pool - .advance_round_normal_operation_n(EARLY_DKG_INTERVAL + 3); - - contexts - .lock() - .unwrap() - .push(make_reshare_chain_key_context( - deps.registry.get_latest_version(), - key_id.clone(), - vec![10, 11, 12, 13], - target_id, - )); - - let remote_dkg_ids = vec![NiDkgId { - start_block_height: Height::from(EARLY_DKG_INTERVAL + 1), - dealer_subnet: subnet_test_id(0), - dkg_tag: NiDkgTag::HighThresholdForKey(NiDkgMasterPublicKeyId::VetKd( - key_id.clone(), - )), - target_subnet: NiDkgTargetSubnet::Remote(target_id), - }]; + .advance_round_normal_operation_n(EARLY_DKG_INTERVAL + 1); + let remote_dkg_ids = extract_remote_dkg_ids_from_highest_block(&deps.pool, target_id); + assert_eq!(remote_dkg_ids.len(), 1); + assert_eq!(extract_dkg_configs_from_highest_block(&deps.pool).len(), 4); + assert_eq!(extract_remote_dkgs_from_highest_block(&deps.pool).len(), 0); add_dealings_for_configs(&mut deps, &remote_dkg_ids); // 2f + 1 dealings for high threshold VetKD resharing @@ -2391,9 +2193,9 @@ mod tests { /// (= 2), and only the first context's transcripts are included. #[test] fn test_early_remote_transcripts_respects_max() { - for (setup_first, desc) in [ - (true, "SetupInitialDKG first"), - (false, "ReshareChainKey first"), + for (skipped_target_bytes, setup_target_bytes, reshare_target_bytes, desc) in [ + ([0_u8; 32], [1_u8; 32], [2_u8; 32], "SetupInitialDKG first"), + ([0_u8; 32], [2_u8; 32], [1_u8; 32], "ReshareChainKey first"), ] { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let node_ids = (0..4).map(node_test_id).collect::>(); @@ -2403,9 +2205,9 @@ mod tests { }; // This context always comes first but will be // skipped because one of its two configs lacks dealings. - let skipped_target_id = NiDkgTargetId::new([0_u8; 32]); - let setup_target_id = NiDkgTargetId::new([1_u8; 32]); - let reshare_target_id = NiDkgTargetId::new([2_u8; 32]); + let skipped_target_id = NiDkgTargetId::new(skipped_target_bytes); + let setup_target_id = NiDkgTargetId::new(setup_target_bytes); + let reshare_target_id = NiDkgTargetId::new(reshare_target_bytes); let mut deps = dependencies_with_subnet_records_with_raw_state_manager( pool_config, @@ -2429,30 +2231,26 @@ mod tests { ); let registry_version = deps.registry.get_latest_version(); - let mut contexts = vec![ - make_setup_initial_dkg_context( - registry_version, - vec![10, 11, 12, 13], - skipped_target_id, - ), - make_setup_initial_dkg_context( - registry_version, - vec![10, 11, 12, 13], - setup_target_id, - ), - make_reshare_chain_key_context( - registry_version, - key_id.clone(), - vec![10, 11, 12, 13], - reshare_target_id, - ), - ]; - if !setup_first { - contexts.swap(1, 2); - } complement_state_manager_with_dkg_contexts( deps.state_manager.clone(), - contexts, + vec![ + make_setup_initial_dkg_context( + registry_version, + vec![10, 11, 12, 13], + skipped_target_id, + ), + make_setup_initial_dkg_context( + registry_version, + vec![10, 11, 12, 13], + setup_target_id, + ), + make_reshare_chain_key_context( + registry_version, + key_id.clone(), + vec![10, 11, 12, 13], + reshare_target_id, + ), + ], None, ); @@ -2581,7 +2379,7 @@ mod tests { .chain(std::iter::once(&reshare_dkg_id)) .collect(); for dkg_id in &dkg_ids_with_dealings { - let dealings: Vec<_> = (1..5) + let dealings: Vec<_> = (0..3) .map(|i| ChangeAction::AddToValidated(create_dealing(i, (*dkg_id).clone()))) .collect(); deps.dkg_pool.write().unwrap().apply(dealings); @@ -2606,7 +2404,7 @@ mod tests { assert_eq!(extract_dealings_from_highest_block(&deps.pool).len(), 0); let remote_dkgs = extract_remote_dkgs_from_highest_block(&deps.pool); - if setup_first { + if setup_target_id < reshare_target_id { assert_eq!( remote_dkgs.len(), 2, diff --git a/rs/consensus/dkg/src/payload_builder.rs b/rs/consensus/dkg/src/payload_builder.rs index e9295215199f..312c94c7dc52 100644 --- a/rs/consensus/dkg/src/payload_builder.rs +++ b/rs/consensus/dkg/src/payload_builder.rs @@ -1,7 +1,6 @@ use crate::{ MAX_EARLY_REMOTE_TRANSCRIPTS, MAX_REMOTE_DKG_ATTEMPTS, MAX_REMOTE_DKGS_PER_INTERVAL, REMOTE_DKG_REPEATED_FAILURE_ERROR, - remote::{ConfigResult, build_callback_id_config_map, merge_configs}, utils::{self, tags_iter, vetkd_key_ids_for_subnet}, }; use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; @@ -81,8 +80,6 @@ pub fn create_payload( // If the height is not a start height, create a payload with new dealings, // and possibly early remote transcripts. create_data_payload( - subnet_id, - registry_client, pool_reader, dkg_pool, parent, @@ -99,8 +96,6 @@ pub fn create_payload( } fn create_data_payload( - this_subnet_id: SubnetId, - registry_client: &dyn RegistryClient, pool_reader: &PoolReader<'_>, dkg_pool: Arc>, parent: &Block, @@ -112,49 +107,26 @@ fn create_data_payload( validation_context: &ValidationContext, logger: ReplicaLogger, ) -> Result { - // Get all existing dealer ids from the chain. + // Get all dealer ids from the chain. let dealers_from_chain = utils::get_dealers_from_chain(pool_reader, parent); - - // Determine all current configs. - let state = state_reader - .get_state_at(validation_context.certified_height) - .map_err(DkgPayloadCreationError::StateManagerError)?; - let remote_config_results = build_callback_id_config_map( - this_subnet_id, - registry_client, - state.get_ref(), - validation_context.registry_version, - last_dkg_summary, - &logger, - )?; - let configs = merge_configs(&last_dkg_summary.configs, &remote_config_results); - // Select new dealings for the payload. let new_validated_dealings = select_dealings_for_payload( - configs, + &last_dkg_summary.configs, &dealers_from_chain, &*dkg_pool .read() .expect("Couldn't lock DKG pool for reading."), max_dealings_per_block, ); - for d in &new_validated_dealings { - if matches!(d.content.dkg_id.target_subnet, NiDkgTargetSubnet::Remote(_)) { - info!( - logger, - "Including remote dealing for dkg id {:?} in data block payload at height {}", - d.content.dkg_id, - parent.height.increment(), - ); - } - } let remote_dkg_transcripts = create_early_remote_transcripts( pool_reader, crypto, parent, - remote_config_results, - &logger, + last_dkg_summary, + state_reader, + validation_context, + logger.clone(), )?; if !remote_dkg_transcripts.is_empty() { @@ -179,41 +151,59 @@ pub(crate) fn create_early_remote_transcripts( pool_reader: &PoolReader<'_>, crypto: &dyn ConsensusCrypto, parent: &Block, - callback_id_map: BTreeMap, - logger: &ReplicaLogger, + last_dkg_summary: &DkgSummary, + state_reader: &dyn StateReader, + validation_context: &ValidationContext, + logger: ReplicaLogger, ) -> Result)>, DkgPayloadCreationError> { + // Return an error on transient state manager errors + let state = state_reader + .get_state_at(validation_context.certified_height) + .map_err(DkgPayloadCreationError::StateManagerError)?; + // Since this function is relatively expensive, we simply return if there are no outstanding DKG contexts + let callback_id_map = build_target_id_callback_map(state.get_ref()); if callback_id_map.is_empty() { return Ok(vec![]); } // Get all dealings for DKGs that have not been completed yet - let (all_dealings, _) = utils::get_dkg_dealings(pool_reader, parent); + let (all_dealings, completed) = utils::get_dkg_dealings(pool_reader, parent); + + // Collect map of remote target_ids to DKG configs + let mut remote_configs: BTreeMap> = BTreeMap::new(); + for config in last_dkg_summary.configs.values() { + let dkg_id = config.dkg_id(); + if completed.contains(dkg_id) { + // Skip DKGs that have already been completed + continue; + } + if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { + remote_configs.entry(target_id).or_default().push(config); + } + } // Try to create transcripts for all configs of each target_id. Note that we either include // all transcript results for a target_id or none of them. let mut selected_transcripts = vec![]; - for (callback_id, config_results) in callback_id_map.into_iter() { - let configs = match config_results { - Ok(configs) => configs, - Err(errs) => { - // Reject contexts for which we failed to create configs. - for (dkg_id, err) in errs { - error!( - logger, - "Failed to create early remote transcript for dkg id {:?} at height {}: {}", - dkg_id, - parent.height.increment(), - err - ); - // Including the error in the payload will cause the context to receive - // a reject response. - selected_transcripts.push((dkg_id, callback_id, Err(err))); - } - continue; - } + for (target_id, configs) in remote_configs { + // Lookup the callback id and the expected number of configs for this target_id + let Some((expected_config_num, callback_id)) = callback_id_map.get(&target_id) else { + warn!( + logger, + "Unable to find callback id associated with remote target id {target_id:?} at block height {}", + parent.height.increment() + ); + continue; }; + // Check that we have the expected number of configs for this target_id + if configs.len() != *expected_config_num { + // This may happen if we did not manage to create all required transcripts as part of + // the last summary block. We will handle this in the next summary block instead. + continue; + } + // Ensure that creating these transcripts would not exceed the maximum number of early // remote transcripts. We continue with the next target_id in case it requires less // transcripts. @@ -222,7 +212,7 @@ pub(crate) fn create_early_remote_transcripts( } // If any of the configs has less dealings than the threshold, we skip this target_id - if configs.iter().any(|config: &NiDkgConfig| { + if configs.iter().any(|config| { let dealings_count = all_dealings .get(config.dkg_id()) .map_or(0, |dealings| dealings.len()); @@ -235,7 +225,8 @@ pub(crate) fn create_early_remote_transcripts( for config in configs.iter() { // Generate the transcript. We need to retry transient errors, as a payload containing // transient errors may not be verifiable by peers. - let transcript_result = match create_transcript(crypto, config, &all_dealings, logger) { + let transcript_result = match create_transcript(crypto, config, &all_dealings, &logger) + { Ok(transcript) => Ok(transcript), // Note that we handled the reproducible error case of not having enough dealings // already beforehand. @@ -256,7 +247,7 @@ pub(crate) fn create_early_remote_transcripts( return Err(DkgPayloadCreationError::DkgCreateTranscriptError(err)); } }; - selected_transcripts.push((config.dkg_id().clone(), callback_id, transcript_result)); + selected_transcripts.push((config.dkg_id().clone(), *callback_id, transcript_result)); } } @@ -277,7 +268,7 @@ pub(crate) fn create_early_remote_transcripts( /// 2. Among remote targets, prioritize dealings for targets that are closer to their threshold. /// 3. Use `target_subnet` as a tie-breaker between dealings for targets with the same remaining capacity. fn select_dealings_for_payload( - configs: BTreeMap<&NiDkgId, &NiDkgConfig>, + configs: &BTreeMap, dealers_from_chain: &HashSet<(NiDkgId, NodeId)>, dkg_pool: &dyn DkgPool, max_dealings_per_block: usize, @@ -285,7 +276,7 @@ fn select_dealings_for_payload( // Compute remaining capacity (collection_threshold - dealings on chain) for each config. let mut remaining_capacity: BTreeMap<&NiDkgId, usize> = configs .iter() - .map(|(&dkg_id, config)| (dkg_id, config.collection_threshold().get() as usize)) + .map(|(dkg_id, config)| (dkg_id, config.collection_threshold().get() as usize)) .collect(); for (dkg_id, _) in dealers_from_chain { if let Some(cap) = remaining_capacity.get_mut(dkg_id) { @@ -481,9 +472,10 @@ pub(super) fn create_summary_payload( .map(|(id, _, result)| (id.clone(), result.clone())) .collect(); - let completed_target_ids = get_completed_target_ids(&completed_dkgs); + let completed_target_ids = + get_completed_target_ids(last_summary.configs.keys(), &completed_dkgs); - let (mut configs, transcripts_for_remote_subnets, mut initial_dkg_attempts) = + let (mut configs, transcripts_for_remote_subnets, initial_dkg_attempts) = compute_remote_dkg_data( subnet_id, height, @@ -497,9 +489,6 @@ pub(super) fn create_summary_payload( &last_summary.initial_dkg_attempts, &logger, )?; - for target_id in completed_target_ids { - initial_dkg_attempts.insert(target_id, 0); - } let interval_length = last_summary.next_interval_length; let next_interval_length = get_dkg_interval_length( @@ -1085,18 +1074,25 @@ pub(crate) fn get_node_list( .collect()) } -/// Returns the set of remote target IDs for which at least one DKG instance -/// was completed. -fn get_completed_target_ids(completed: &BTreeSet) -> BTreeSet { - completed - .iter() - .filter_map(|dkg_id| { - if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { - Some(target_id) - } else { - None - } - }) +/// Returns the set of remote target IDs for which all configured DKGs have +/// been completed. +fn get_completed_target_ids<'a>( + config_ids: impl Iterator, + completed: &BTreeSet, +) -> BTreeSet { + let mut remote_dkgs_by_target: BTreeMap> = BTreeMap::new(); + for dkg_id in config_ids { + if let NiDkgTargetSubnet::Remote(target_id) = dkg_id.target_subnet { + remote_dkgs_by_target + .entry(target_id) + .or_default() + .push(dkg_id); + } + } + remote_dkgs_by_target + .into_iter() + .filter(|(_, dkg_ids)| dkg_ids.iter().all(|id| completed.contains(id))) + .map(|(target_id, _)| target_id) .collect() } @@ -2173,6 +2169,30 @@ mod tests { }); } + #[test] + fn test_get_completed_target_ids() { + let targets: Vec<_> = (1..=3).map(|i| NiDkgTargetId::new([i; 32])).collect(); + let tags = [NiDkgTag::LowThreshold, NiDkgTag::HighThreshold]; + + let config_ids: Vec<_> = targets + .iter() + .flat_map(|t| { + tags.iter().map(|tag| NiDkgId { + start_block_height: Height::from(1), + dealer_subnet: subnet_test_id(1), + dkg_tag: tag.clone(), + target_subnet: NiDkgTargetSubnet::Remote(*t), + }) + }) + .collect(); + + // target 0 is fully completed, target 1 only has low completed, target 2 is not completed + let completed: BTreeSet<_> = config_ids[..3].iter().cloned().collect(); + + let result = get_completed_target_ids(config_ids.iter(), &completed); + assert_eq!(result, BTreeSet::from([targets[0]])); + } + #[test] fn test_process_subnet_call_context_ignores_completed_targets() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { @@ -2315,8 +2335,7 @@ mod tests { messages: (0..4).map(|i| create_dealing(i, id.clone())).collect(), }; - let selected = - select_dealings_for_payload(configs.iter().collect(), &HashSet::new(), &pool, 10); + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 10); // Only collection_threshold (2) dealings should be included. assert_eq!(selected.len(), 2); @@ -2330,7 +2349,6 @@ mod tests { fn test_select_dealings_filters_duplicate_dealers() { let id = local_dkg_id(NiDkgTag::LowThreshold); let configs: BTreeMap<_, _> = [(id.clone(), make_test_config(id.clone(), 1))].into(); - let configs: BTreeMap<&NiDkgId, &NiDkgConfig> = configs.iter().collect(); // Dealer 0 already on chain let dealers_from_chain: HashSet<_> = [(id.clone(), node_test_id(0))].into(); @@ -2340,7 +2358,7 @@ mod tests { messages: (0..4).map(|i| create_dealing(i, id.clone())).collect(), }; - let selected = select_dealings_for_payload(configs, &dealers_from_chain, &pool, 10); + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 10); // Dealer 0's dealing should be filtered out, only dealer 1's remains. assert_eq!(selected.len(), 1); @@ -2359,7 +2377,6 @@ mod tests { (remote_id.clone(), make_test_config(remote_id.clone(), 3)), ] .into(); - let configs: BTreeMap<&NiDkgId, &NiDkgConfig> = configs.iter().collect(); let pool = TestDkgPool { messages: (0..4) @@ -2368,7 +2385,7 @@ mod tests { .collect(), }; - let selected = select_dealings_for_payload(configs, &HashSet::new(), &pool, 3); + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 3); assert_eq!(selected.len(), 3); // All 3 should be remote (prioritized), since remote has 4 available. @@ -2385,7 +2402,6 @@ mod tests { let configs: BTreeMap<_, _> = [(known_id.clone(), make_test_config(known_id.clone(), 1))].into(); - let configs: BTreeMap<&NiDkgId, &NiDkgConfig> = configs.iter().collect(); let pool = TestDkgPool { messages: vec![ @@ -2394,7 +2410,7 @@ mod tests { ], }; - let selected = select_dealings_for_payload(configs, &HashSet::new(), &pool, 10); + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 10); assert_eq!(selected.len(), 1); assert_eq!(selected[0].content.dkg_id, known_id); @@ -2412,7 +2428,6 @@ mod tests { (remote_id.clone(), make_test_config(remote_id.clone(), 1)), ] .into(); - let configs: BTreeMap<&NiDkgId, &NiDkgConfig> = configs.iter().collect(); // 3 local dealings, 3 remote dealings let pool = TestDkgPool { @@ -2422,7 +2437,7 @@ mod tests { .collect(), }; - let selected = select_dealings_for_payload(configs.clone(), &HashSet::new(), &pool, 2); + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 2); assert_eq!(selected.len(), 2); assert!(selected.iter().all(|msg| msg.content.dkg_id == remote_id)); assert_eq!( @@ -2430,7 +2445,7 @@ mod tests { BTreeSet::from_iter((3..5).map(node_test_id)) ); - let selected = select_dealings_for_payload(configs, &HashSet::new(), &pool, 10); + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 10); // 2 remote + 2 local (both capped at collection_threshold) assert_eq!(selected.len(), 4); @@ -2467,7 +2482,6 @@ mod tests { ), ] .into(); - let configs: BTreeMap<&NiDkgId, &NiDkgConfig> = configs.iter().collect(); // Some dealings already included on chain: // - remote_low_remaining has 2 on chain, so 1 remaining. @@ -2486,11 +2500,11 @@ mod tests { ], }; - let selected = select_dealings_for_payload(configs.clone(), &dealers_from_chain, &pool, 1); + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 1); assert_eq!(selected.len(), 1); assert_eq!(selected[0].content.dkg_id, remote_low_remaining_id); - let selected = select_dealings_for_payload(configs, &dealers_from_chain, &pool, 10); + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 10); assert_eq!(selected.len(), 2); assert_eq!( BTreeSet::from_iter(selected.iter().map(|msg| msg.content.dkg_id.clone())), @@ -2517,7 +2531,6 @@ mod tests { ), ] .into(); - let configs: BTreeMap<&NiDkgId, &NiDkgConfig> = configs.iter().collect(); // remote_completed has no remaining capacity, so with // MAX_REMOTE_DKGS_PER_INTERVAL = 1 we should not prioritize remote. @@ -2538,13 +2551,13 @@ mod tests { }; // With max_dealings_per_block = 1, we should prioritize local. - let selected = select_dealings_for_payload(configs.clone(), &dealers_from_chain, &pool, 1); + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 1); assert_eq!(selected.len(), 1); assert_eq!(selected[0].content.dkg_id, local_id); // With max_dealings_per_block = 10, we should prioritize local (although it has higher remaining capacity). // We should also include remote, once local is capped at collection_threshold. - let selected = select_dealings_for_payload(configs, &dealers_from_chain, &pool, 10); + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 10); assert_eq!(selected.len(), 3); assert_eq!( selected @@ -2579,7 +2592,6 @@ mod tests { ), ] .into(); - let configs: BTreeMap<&NiDkgId, &NiDkgConfig> = configs.iter().collect(); let pool = TestDkgPool { messages: vec![ @@ -2588,7 +2600,7 @@ mod tests { ], }; - let selected = select_dealings_for_payload(configs, &HashSet::new(), &pool, 1); + let selected = select_dealings_for_payload(&configs, &HashSet::new(), &pool, 1); assert_eq!(selected.len(), 1); assert_eq!(selected[0].content.dkg_id, remote_target_0); @@ -2617,7 +2629,6 @@ mod tests { ), ] .into(); - let configs: BTreeMap<&NiDkgId, &NiDkgConfig> = configs.iter().collect(); // remote_low is completed, remote_high still has remaining capacity. // Both remotes share one target ID, so that target should not count as "completed". @@ -2637,11 +2648,11 @@ mod tests { // With MAX_REMOTE_DKGS_PER_INTERVAL = 1 and no completed remote target IDs, // remote dealings should still be prioritized. - let selected = select_dealings_for_payload(configs.clone(), &dealers_from_chain, &pool, 1); + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 1); assert_eq!(selected.len(), 1); assert_eq!(selected[0].content.dkg_id, remote_high_id); - let selected = select_dealings_for_payload(configs, &dealers_from_chain, &pool, 10); + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 10); assert_eq!(selected.len(), 2); assert_eq!( BTreeSet::from_iter(selected.iter().map(|msg| msg.content.dkg_id.clone())), @@ -2656,7 +2667,6 @@ mod tests { // collection_threshold = 2 let configs: BTreeMap<_, _> = [(local_id.clone(), make_test_config(local_id.clone(), 1))].into(); - let configs: BTreeMap<&NiDkgId, &NiDkgConfig> = configs.iter().collect(); // Capacity for this config is already exhausted on chain. let dealers_from_chain: HashSet<_> = [ @@ -2673,7 +2683,7 @@ mod tests { ], }; - let selected = select_dealings_for_payload(configs, &dealers_from_chain, &pool, 10); + let selected = select_dealings_for_payload(&configs, &dealers_from_chain, &pool, 10); assert!(selected.is_empty()); } } diff --git a/rs/consensus/dkg/src/payload_validator.rs b/rs/consensus/dkg/src/payload_validator.rs index 2f3848aa710f..c20a9abed3d3 100644 --- a/rs/consensus/dkg/src/payload_validator.rs +++ b/rs/consensus/dkg/src/payload_validator.rs @@ -1,5 +1,5 @@ +use self::payload_builder::create_early_remote_transcripts; use super::{crypto_validate_dealing, payload_builder, utils}; -use crate::remote::{build_callback_id_config_map, merge_configs}; use ic_consensus_utils::{crypto::ConsensusCrypto, pool_reader::PoolReader}; use ic_interfaces::{ dkg::{DkgPayloadValidationError, DkgPool}, @@ -15,10 +15,7 @@ use ic_types::{ batch::ValidationContext, consensus::{ Block, BlockPayload, - dkg::{ - DkgDataPayload, DkgPayloadCreationError, DkgPayloadValidationFailure, DkgSummary, - InvalidDkgPayloadReason, - }, + dkg::{DkgDataPayload, DkgPayloadValidationFailure, DkgSummary, InvalidDkgPayloadReason}, }, }; use prometheus::IntCounterVec; @@ -120,8 +117,6 @@ pub fn validate_payload( }); validate_dealings_payload( - subnet_id, - registry_client, crypto, pool_reader, dkg_pool, @@ -139,11 +134,8 @@ pub fn validate_payload( } // Validates the payload containing dealings. -#[allow(clippy::too_many_arguments)] #[allow(clippy::result_large_err)] fn validate_dealings_payload( - subnet_id: SubnetId, - registry_client: &dyn RegistryClient, crypto: &dyn ConsensusCrypto, pool_reader: &PoolReader<'_>, dkg_pool: &dyn DkgPool, @@ -189,20 +181,6 @@ fn validate_dealings_payload( return Err(InvalidDkgPayloadReason::DealerAlreadyDealt(dealer_id).into()); } - let state = state_reader - .get_state_at(validation_context.certified_height) - .map_err(DkgPayloadCreationError::StateManagerError)?; - - let remote_config_results = build_callback_id_config_map( - subnet_id, - registry_client, - state.get_ref(), - validation_context.registry_version, - last_summary, - log, - )?; - let configs = merge_configs(&last_summary.configs, &remote_config_results); - // Check that all messages have a valid DKG config from the summary and the // dealer is valid, then verify each dealing. for message in &dealings.messages { @@ -214,7 +192,7 @@ fn validate_dealings_payload( continue; } - let Some(config) = configs.get(&message.content.dkg_id) else { + let Some(config) = last_summary.configs.get(&message.content.dkg_id) else { return Err(InvalidDkgPayloadReason::MissingDkgConfigForDealing.into()); }; @@ -224,12 +202,14 @@ fn validate_dealings_payload( // If we have early transcripts, we compare them if !dealings.transcripts_for_remote_subnets.is_empty() { - let expected_transcripts = payload_builder::create_early_remote_transcripts( + let expected_transcripts = create_early_remote_transcripts( pool_reader, crypto, parent, - remote_config_results, - log, + last_summary, + state_reader, + validation_context, + log.clone(), )?; if dealings.transcripts_for_remote_subnets != expected_transcripts { @@ -266,13 +246,11 @@ mod tests { dkg::ChangeAction, p2p::consensus::{MutablePool, PoolMutationsProducer}, }; - use ic_interfaces_state_manager::Labeled; use ic_logger::no_op_logger; use ic_metrics::MetricsRegistry; use ic_registry_keys::make_subnet_record_key; use ic_test_utilities_consensus::fake::FakeContentSigner; use ic_test_utilities_registry::SubnetRecordBuilder; - use ic_test_utilities_state::get_initial_state; use ic_test_utilities_types::ids::{ NODE_1, NODE_2, NODE_3, SUBNET_1, SUBNET_2, node_test_id, subnet_test_id, }; @@ -729,13 +707,6 @@ mod tests { .build(), )], ); - state_manager - .get_mut() - .expect_get_latest_certified_state() - .return_const(Some(Labeled::new( - Height::new(0), - Arc::new(get_initial_state(0, 0)), - ))); // Both summary registry versions should be 1 initially let summary_block = pool.as_cache().summary_block(); @@ -787,9 +758,6 @@ mod tests { let key_manager = Arc::new(Mutex::new(key_manager)); let dkg_impl = DkgImpl::new( node_id, - subnet_id, - registry.clone(), - state_manager.clone(), crypto.clone(), pool.get_cache(), key_manager, @@ -810,10 +778,9 @@ mod tests { }; // It should be possible to validate the dealing - let configs = dkg_summary.configs.iter().collect(); let result = dkg_impl.validate_dealings_for_dealer( &dkg_pool, - &configs, + &dkg_summary.configs, start_height, vec![dealing], ); diff --git a/rs/consensus/dkg/src/test_utils.rs b/rs/consensus/dkg/src/test_utils.rs index e7a99b3d0961..f242a1c07f8d 100644 --- a/rs/consensus/dkg/src/test_utils.rs +++ b/rs/consensus/dkg/src/test_utils.rs @@ -26,7 +26,7 @@ use ic_types::{ }; use std::{ collections::{BTreeMap, BTreeSet}, - sync::{Arc, Mutex}, + sync::Arc, }; pub(super) fn make_setup_initial_dkg_context( @@ -59,23 +59,6 @@ pub(super) fn make_reshare_chain_key_context( }) } -fn clone_subnet_call_context(context: &SubnetCallContext) -> SubnetCallContext { - match context { - SubnetCallContext::SetupInitialDKG(c) => SubnetCallContext::SetupInitialDKG(c.clone()), - SubnetCallContext::CanisterHttpRequest(c) => { - SubnetCallContext::CanisterHttpRequest(c.clone()) - } - SubnetCallContext::ReshareChainKey(c) => SubnetCallContext::ReshareChainKey(c.clone()), - SubnetCallContext::BitcoinGetSuccessors(c) => { - SubnetCallContext::BitcoinGetSuccessors(c.clone()) - } - SubnetCallContext::BitcoinSendTransactionInternal(c) => { - SubnetCallContext::BitcoinSendTransactionInternal(c.clone()) - } - SubnetCallContext::SignWithThreshold(c) => SubnetCallContext::SignWithThreshold(c.clone()), - } -} - /// Set up the state manager mock to return an initial state containing the /// given subnet call contexts. pub(super) fn complement_state_manager_with_dkg_contexts( @@ -90,53 +73,13 @@ pub(super) fn complement_state_manager_with_dkg_contexts( .subnet_call_context_manager .push_context(context); } - let state = Arc::new(state); let mut mock = state_manager.get_mut(); let expectation = mock .expect_get_state_at() - .return_const(Ok(Labeled::new(Height::new(0), state.clone()))); - if let Some(times) = times { - expectation.times(times); - } - - mock.expect_get_latest_certified_state() - .return_const(Some(Labeled::new(Height::new(0), state.clone()))); -} - -/// Set up the state manager mock to return an initial state built from a -/// mutable context vector on every call. -pub(super) fn complement_state_manager_with_dkg_contexts_mut( - state_manager: Arc, - contexts: Arc>>, - times: Option, -) { - let mut mock = state_manager.get_mut(); - let state_for_get_state_at = contexts.clone(); - let expectation = mock.expect_get_state_at().returning(move |_| { - let mut state = ic_test_utilities_state::get_initial_state(0, 0); - for context in state_for_get_state_at.lock().unwrap().iter() { - state - .metadata - .subnet_call_context_manager - .push_context(clone_subnet_call_context(context)); - } - Ok(Labeled::new(Height::new(0), Arc::new(state))) - }); + .return_const(Ok(Labeled::new(Height::new(0), Arc::new(state)))); if let Some(times) = times { expectation.times(times); } - - let state_for_latest_certified = contexts; - mock.expect_get_latest_certified_state().returning(move || { - let mut state = ic_test_utilities_state::get_initial_state(0, 0); - for context in state_for_latest_certified.lock().unwrap().iter() { - state - .metadata - .subnet_call_context_manager - .push_context(clone_subnet_call_context(context)); - } - Some(Labeled::new(Height::new(0), Arc::new(state))) - }); } pub(super) fn complement_state_manager_with_setup_initial_dkg_request( diff --git a/rs/consensus/tests/framework/runner.rs b/rs/consensus/tests/framework/runner.rs index b6b38687aa44..23bc79b816a8 100644 --- a/rs/consensus/tests/framework/runner.rs +++ b/rs/consensus/tests/framework/runner.rs @@ -171,9 +171,6 @@ impl<'a> ConsensusRunner<'a> { ); let dkg = ic_consensus_dkg::DkgImpl::new( deps.replica_config.node_id, - deps.replica_config.subnet_id, - Arc::clone(&deps.registry_client), - deps.state_manager.clone(), Arc::clone(&consensus_crypto), deps.consensus_pool.read().unwrap().get_cache(), dkg_key_manager, diff --git a/rs/consensus/tests/payload.rs b/rs/consensus/tests/payload.rs index cea5a9749104..2a4d334ff370 100644 --- a/rs/consensus/tests/payload.rs +++ b/rs/consensus/tests/payload.rs @@ -90,12 +90,6 @@ fn consensus_produces_expected_batches() { Height::new(0), Arc::new(get_initial_state(0, 0)), ))); - state_manager - .expect_get_latest_certified_state() - .return_const(Some(Labeled::new( - Height::new(0), - Arc::new(get_initial_state(0, 0)), - ))); state_manager .expect_get_certified_state_snapshot() .returning(|| None); @@ -188,9 +182,6 @@ fn consensus_produces_expected_batches() { ic_consensus::consensus::ConsensusBouncer::new(&metrics_registry, router.clone()); let dkg = ic_consensus_dkg::DkgImpl::new( replica_config.node_id, - replica_config.subnet_id, - Arc::clone(®istry_client) as Arc<_>, - Arc::clone(&state_manager) as Arc<_>, Arc::clone(&fake_crypto) as Arc<_>, Arc::clone(&consensus_cache), dkg_key_manager, diff --git a/rs/replica/setup_ic_network/src/lib.rs b/rs/replica/setup_ic_network/src/lib.rs index 2ffe6e98079e..d3b6082d7533 100644 --- a/rs/replica/setup_ic_network/src/lib.rs +++ b/rs/replica/setup_ic_network/src/lib.rs @@ -612,9 +612,6 @@ fn start_consensus( abortable_broadcast_channels.dkg, ic_consensus_dkg::DkgImpl::new( node_id, - subnet_id, - Arc::clone(®istry_client), - Arc::clone(&state_manager) as Arc<_>, Arc::clone(&consensus_crypto), Arc::clone(&consensus_pool_cache), dkg_key_manager,