//! One-shot engine for generating an initial BLS threshold output. //! //! The engine runs an independent Ed25519 Simplex chain for one epoch and //! performs a single ceremony among the configured participants, using the //! dealing and inclusion protocol of [`reshare`]. The resulting [`EpochInfo`] //! describes only the ceremony participants (see [`Completion::info`] before //! using it as the genesis of a continuously reshared chain). //! //! # Message Sizes //! //! The engine runs Simplex on its vote, certificate, and resolver channels, marshal on its backfill //! and broadcast channels, and the [`reshare`] dealing protocol on its DKG channel. Marshal's //! backfill responses and broadcast messages carry the one-shot [`Block`]: //! //! ```text //! Block: //! +----------------------+---------------------+-------------------------+ //! | epoch (varint, 1-10) | view (varint, 1-10) | leader (32) | //! +----------------------+-----+---------------+-----+-------------------+ //! | parent view (varint, 1-10) | context parent (32) | block parent (32) | //! +-----------------------+----+-------------+-------+-------------------+ //! | height (varint, 1-10) | payload flag (1) | payload | //! +-----------------------+------------------+---------------------------+ //! ``` //! //! The payload follows only when the flag is 1 and is at most //! [`Payload::max_encode_size`](crate::dkg::types::Payload::max_encode_size) for the configured //! participants. [`BoundedBlock::max_encode_size`] bounds every block the block codec //! configuration admits. //! //! [`max_payload_size`] is the largest of the payload bounds of Simplex, of marshal for that block, //! and of the DKG. //! //! ``` //! use commonware_cryptography::bls12381::primitives::{sharing::ModeVersion, variant::MinSig}; //! use commonware_glue::dkg::bootstrap::max_payload_size; //! use commonware_p2p::max_message_size; //! use commonware_utils::{NZU32, sequence::Unit}; //! //! // The network limit for a ceremony among 4 participants with a key-only directory. //! let size = max_message_size(&[max_payload_size::(NZU32!(4), ModeVersion::v0())]); //! ``` use crate::dkg::{ self, ParticipantsProvider, Registrar, ReshareBlock, SecretStore, fence::Fence, network::{Directory, Manager}, reshare::{self, DkgConfig}, state_sync::Plan as StateSyncPlan, types::{EpochInfo, Payload, SchemeInfo, max_participants}, }; use commonware_broadcast::buffered; use commonware_codec::{ Buf, Encode, EncodeSize, Error as CodecError, FixedSize, MaxEncodeSize, Read, ReadExt as _, Write, }; use commonware_consensus::{ Application, Block as ConsensusBlock, BoundedBlock, CertifiableBlock, Heightable, marshal::{ self, Start, ancestry::Ancestry, core::Actor as MarshalActor, resolver::p2p as marshal_resolver, standard::{Deferred, Standard}, }, simplex::{ self, Floor, config::{ForwardPolicy, SkipBudget, SkipPolicy}, elector::RoundRobin, types::Context, }, types::{Epoch, Epocher, FixedEpocher, Height, Round, View, ViewDelta}, }; use commonware_cryptography::{ Digest as _, Digested, Digestible, Hasher, PublicKey, Sha256, Signer as _, bls12381::{ dkg::feldman_desmedt::Reveal, primitives::{ sharing::{Mode as SharingMode, ModeVersion}, variant::Variant, }, }, certificate::{ConstantProvider, Verifier as _}, ed25519, sha256::{self, Digest as Sha256Digest}, }; use commonware_p2p::{Blocker, Receiver, Sender}; use commonware_parallel::Strategy; use commonware_runtime::{ BufMut, Clock, ContextCell, Handle, Metrics, Spawner, Storage, buffer::paged::CacheRef, spawn_cell, }; use commonware_storage::{archive::prunable, translator::TwoCap}; use commonware_utils::{ NZUsize, NonZeroDuration, TryCollect, channel::{fallible::OneshotExt, oneshot}, ordered::Set, sequence::Unit, vec::NonEmptyVec, }; use rand_core::CryptoRng; use std::{ marker::PhantomData, num::{NonZeroU32, NonZeroU64, NonZeroUsize}, time::Duration, }; type ConsensusScheme = simplex::scheme::ed25519::Scheme; /// Configuration for [`Engine`]. pub struct Config { /// Ed25519 signer used for the one-shot consensus chain and DKG protocol messages. pub signer: ed25519::PrivateKey, /// Peer manager for the one-shot chain. /// /// Do not register peer sets before passing this manager to the engine. pub manager: M, /// Blocker used for invalid peer behavior. pub blocker: X, /// Application-owned store for this ceremony's secret material. /// /// Do not share this store with a [`reshare`] actor: both write epoch-zero /// dealer seeds and dealings. Copy only the resulting epoch-zero share. /// The engine never prunes this store. pub secret_store: SS, /// Parallel verification strategy. pub strategy: T, /// Application namespace for DKG transcript separation. pub namespace: &'static [u8], /// Sharing mode used for the generated threshold output. pub sharing_mode: SharingMode, /// Revealed-share calculation used for the DKG ceremony. pub reveal: Reveal, /// Maximum sharing mode version accepted when decoding blocks. pub max_supported_mode: ModeVersion, /// Runtime-storage partition prefix. pub partition_prefix: String, /// Page cache for the one-shot chain's journals and archives. pub page_cache: CacheRef, /// The size of the write buffer to use for each blob in the one-shot chain's storage. pub write_buffer: NonZeroUsize, /// Number of bytes to buffer when replaying the one-shot chain's storage during startup. pub replay_buffer: NonZeroUsize, /// Maximum number of messages to buffer in each of the engine's mailboxes. pub mailbox_size: NonZeroUsize, /// Maximum wait for a peer to respond to a resolver request. pub fetch_timeout: NonZeroDuration, /// Delay after an unresolved attempt before retrying its fetch. /// Zero permits immediate retries, subject to the sender's rate limit. pub fetch_retry_delay: Duration, /// Number of items per section in each of the one-shot chain's archives, including /// marshal's cache. pub items_per_section: NonZeroU64, /// Participants in the DKG. pub participants: Set, /// Transport directory for the participants. /// /// Used to activate the one-shot chain's peer set and embedded verbatim in /// the resulting [`EpochInfo`]. It must contain exactly `participants`, and /// every participant must configure the same directory. pub directory: D, /// Number of blocks in the single epoch of the dedicated DKG chain. /// /// The second half of the epoch is the inclusion window, subject to the same /// [liveness](crate::dkg::reshare#liveness) requirement as resharing. pub blocks_per_epoch: NonZeroU64, } /// Outcome of the one-shot ceremony, reported at most once per engine start. /// /// The engine reports it when this node processes the one-shot chain's final /// block, or at startup if a previous run already persisted this node's share /// or processed that block. It reports a completion without an artifact if the /// ceremony cannot activate its peer set. pub struct Completion = Unit> { /// Final DKG artifact, if the ceremony succeeded. /// /// Its `next_players` set is empty and its directory covers the one-shot /// ceremony participants. Before using it as continuous-resharing genesis, /// the application must choose a nonempty next-player set and ensure the /// directory exactly covers the resulting participant union. pub info: Option>, } /// Block type used by the one-shot DKG chain. #[derive(Clone, PartialEq, Eq)] pub struct Block = Unit> { context: Context, parent: sha256::Digest, height: Height, payload: Option>, } impl> Block { const fn new( context: Context, parent: sha256::Digest, height: Height, payload: Option>, ) -> Self { Self { context, parent, height, payload, } } const fn genesis(leader: ed25519::PublicKey) -> Self { Self::new( Context { round: Round::new(Epoch::zero(), View::zero()), leader, parent: (View::zero(), Sha256Digest::EMPTY), }, Sha256Digest::EMPTY, Height::zero(), None, ) } /// Returns the DKG result carried by this block, if present. pub const fn epoch_info(&self) -> Option<&EpochInfo> { match &self.payload { Some(Payload::EpochInfo(info)) => Some(info), _ => None, } } } impl> Write for Block { fn write(&self, buf: &mut impl BufMut) { self.context.write(buf); self.parent.write(buf); self.height.write(buf); self.payload.write(buf); } } impl> EncodeSize for Block { fn encode_size(&self) -> usize { self.context.encode_size() + self.parent.encode_size() + self.height.encode_size() + self.payload.encode_size() } } impl> Read for Block { /// Maximum participants and maximum supported sharing mode. type Cfg = (NonZeroU32, ModeVersion); fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result { let context = Context::read(buf)?; let parent = sha256::Digest::read(buf)?; let height = Height::read(buf)?; let payload = Option::>::read_cfg(buf, cfg)?; Ok(Self::new(context, parent, height, payload)) } } impl> BoundedBlock for Block { fn max_encode_size((participants, _): &Self::Cfg) -> usize { // `Payload::max_encode_size` covers payloads of every sharing mode. Payload::::max_encode_size(*participants) + Context::::MAX_ENCODE_SIZE + sha256::Digest::SIZE + Height::MAX_ENCODE_SIZE + bool::SIZE } } impl> Digestible for Block { type Digest = sha256::Digest; fn digest(&self) -> sha256::Digest { Sha256::hash(&[&self.encode()]) } } impl> Heightable for Block { fn height(&self) -> Height { self.height } } impl> ConsensusBlock for Block { fn parent(&self) -> sha256::Digest { self.parent } } impl> CertifiableBlock for Block { type Context = Context; fn context(&self) -> Self::Context { self.context.clone() } } impl> ReshareBlock for Block { type Variant = V; type PublicKey = ed25519::PublicKey; type Directory = D; fn payload(&self) -> Option> { self.payload.clone() } } /// Returns an upper bound on the payloads the bootstrap [`Engine`] sends for a ceremony among /// `participants` participants with [`Config::max_supported_mode`] set to `max_supported_mode`. pub fn max_payload_size>( participants: NonZeroU32, max_supported_mode: ModeVersion, ) -> usize { // Bound blocks by the codec configuration the engine decodes them with. let block_codec_config = (participants, max_supported_mode); simplex::max_payload_size::(participants.get()) .max(marshal::max_payload_size::< Standard>, ConsensusScheme, >(participants.get(), &block_codec_config)) .max(dkg::max_payload_size::(participants)) } /// Self-contained DKG engine. /// /// After reporting [`Completion`], the engine keeps running and serves the /// one-shot chain so participants that have not completed can catch up. Keep /// it running until every participant has completed. /// /// At startup, the engine requests the final block's finalization in the /// background, retrying until it is available. This lets lagging participants /// catch up from peers that only serve the chain. /// /// A restarted engine does not run the ceremony again once this node has /// persisted its share or processed the final block. It reports the outcome /// carried by the stored final block and keeps serving. pub struct Engine where V: Variant, { context: ContextCell, config: Config, _variant: PhantomData, } impl Engine where V: Variant, { /// Creates a new engine. /// /// # Panics /// /// Panics if `max_supported_mode` does not support `sharing_mode`. The /// started engine panics if `participants` is empty, exceeds `u32::MAX` /// entries, or does not contain `signer`. It also panics if a successful /// ceremony's `directory` does not contain exactly `participants`, if a /// component fails, or if startup finds an epoch-zero share in `secret_store` /// without the one-shot chain's final block under `partition_prefix`. pub const fn new(context: E, config: Config) -> Self { assert!( config.max_supported_mode.supports(&config.sharing_mode), "sharing mode must be supported by max supported mode", ); Self { context: ContextCell::new(context), config, _variant: PhantomData, } } } impl Engine where E: CryptoRng + Spawner + Metrics + Clock + Storage, V: Variant, M: Manager, X: Blocker, SS: SecretStore, T: Strategy, D: Directory, { /// Starts the engine and returns its handle and a receiver for its [`Completion`]. /// /// The receiver closes without a value if the engine stops before reporting a completion. /// /// The network carrying these channels must have a `max_message_size` of at least /// [`max_payload_size`] for the configured participants, mode, and directory type. pub fn start( mut self, votes: ( impl Sender, impl Receiver, ), certificates: ( impl Sender, impl Receiver, ), resolver: ( impl Sender, impl Receiver, ), backfill: ( impl Sender, impl Receiver, ), broadcast: ( impl Sender, impl Receiver, ), dkg: ( impl Sender, impl Receiver, ), ) -> (Handle<()>, oneshot::Receiver>) { let (completion_tx, completion_rx) = oneshot::channel(); let handle = spawn_cell!( self.context, self.run( votes, certificates, resolver, backfill, broadcast, dkg, completion_tx ) ); (handle, completion_rx) } #[allow(clippy::too_many_arguments)] async fn run( self, votes: ( impl Sender, impl Receiver, ), certificates: ( impl Sender, impl Receiver, ), resolver_network: ( impl Sender, impl Receiver, ), backfill: ( impl Sender, impl Receiver, ), broadcast: ( impl Sender, impl Receiver, ), dkg: ( impl Sender, impl Receiver, ), completion: oneshot::Sender>, ) { assert!( !self.config.participants.is_empty(), "DKG requires at least one participant" ); let max_participants = max_participants(self.config.participants.len()); let block_codec_config = (max_participants, self.config.max_supported_mode); let context = self.context.into_present(); let page_cache = self.config.page_cache; let write_buffer = self.config.write_buffer; let replay_buffer = self.config.replay_buffer; let items_per_section = self.config.items_per_section; let public_key = self.config.signer.public_key(); let consensus_namespace = [self.config.namespace, b"_INITIAL_CONSENSUS"].concat(); let scheme = ConsensusScheme::signer( &consensus_namespace, self.config.participants.clone(), self.config.signer.clone(), ) .expect("DKG signer must be a participant"); let provider = ConstantProvider::<_, Epoch>::new(scheme.clone()); let genesis = Block::::genesis( self.config .participants .iter() .next() .expect("participants must be non-empty") .clone(), ); let genesis = Digested::compute(genesis, &self.config.strategy).await; let (buffer, buffer_mailbox) = buffered::Engine::new( context.child("buffer"), buffered::Config { public_key: public_key.clone(), mailbox_size: self.config.mailbox_size, deque_size: 16, priority: false, codec_config: block_codec_config, peer_provider: self.config.manager.clone(), strategy: self.config.strategy.clone(), }, ); let buffer_handle = buffer.start(broadcast); let (backfill_handler, backfill_resolver) = marshal_resolver::init( context.child("backfill"), marshal_resolver::Config { public_key: public_key.clone(), peer_provider: self.config.manager.clone(), blocker: self.config.blocker.clone(), mailbox_size: self.config.mailbox_size, fetch_timeout: self.config.fetch_timeout, fetch_retry_delay: self.config.fetch_retry_delay, priority_requests: false, priority_responses: false, }, backfill, ); let finalizations = prunable::Archive::init( context.child("finalizations"), archive_config( &self.config.partition_prefix, "finalizations", page_cache.clone(), write_buffer, replay_buffer, items_per_section, ConsensusScheme::certificate_codec_config_unbounded(), ), ) .await .expect("failed to initialize DKG finalization archive"); let blocks = prunable::Archive::init( context.child("blocks"), archive_config( &self.config.partition_prefix, "blocks", page_cache.clone(), write_buffer, replay_buffer, items_per_section, block_codec_config, ), ) .await .expect("failed to initialize DKG block archive"); let (marshal_actor, marshal_mailbox, _) = MarshalActor::init( context.child("marshal"), finalizations, blocks, marshal::Config { provider: provider.clone(), epocher: FixedEpocher::new(self.config.blocks_per_epoch), start: Start::Genesis(genesis.clone()), partition_prefix: format!("{}-marshal", self.config.partition_prefix), mailbox_size: self.config.mailbox_size, view_retention: ViewDelta::new(10), prunable_items_per_section: items_per_section, page_cache: page_cache.clone(), replay_buffer, key_write_buffer: write_buffer, value_write_buffer: write_buffer, block_codec_config, max_repair: NZUsize!(10), max_pending_acks: NZUsize!(1), strategy: self.config.strategy.clone(), }, ) .await; // The one-shot chain ends at the epoch-zero final block. let last = FixedEpocher::new(self.config.blocks_per_epoch) .last(Epoch::zero()) .expect("epocher must know epoch zero"); if let Ok(targets) = self .config .participants .iter() .filter(|participant| **participant != public_key) .cloned() .try_collect::>() { marshal_mailbox.hint_finalized(last, targets); } // Run the ceremony in a DKG-mode reshare actor. Bootstrap runs the // one-shot chain's Simplex engine itself, without an orchestrator, so // nothing waits on the fence. let (fence, _gate) = Fence::new(); let (reshare_actor, reshare_mailbox) = reshare::Actor::new_dkg( context.child("reshare"), reshare::Config { signer: self.config.signer.clone(), manager: self.config.manager.clone(), blocker: self.config.blocker.clone(), participants_provider: StaticParticipants { participants: self.config.participants.clone(), directory: self.config.directory.clone(), }, secret_store: self.config.secret_store, strategy: self.config.strategy.clone(), registrar: NoopRegistrar(PhantomData), marshal: marshal_mailbox.clone(), state_sync: StateSyncPlan::disabled(), fence, namespace: self.config.namespace, sharing_mode: self.config.sharing_mode, reveal: self.config.reveal, mailbox_size: self.config.mailbox_size, partition_prefix: format!("{}-reshare", self.config.partition_prefix), page_cache: page_cache.clone(), write_buffer, replay_buffer, max_participants, blocks_per_epoch: self.config.blocks_per_epoch, }, DkgConfig { participants: self.config.participants.clone(), directory: self.config.directory.clone(), completion: Box::new(move |info| { let _ = completion.send_lossy(Completion { info }); }), }, ); let app = reshare::Application::new( DkgApp { _marker: PhantomData, strategy: self.config.strategy.clone(), }, reshare_mailbox.clone(), self.config.blocks_per_epoch, ); let deferred = Deferred::new( context.child("deferred"), app, marshal_mailbox.clone(), FixedEpocher::new(self.config.blocks_per_epoch), ); let floor = Floor::Genesis(genesis.digest()); let simplex = simplex::Engine::new( context.child("simplex"), simplex::Config { scheme, elector: RoundRobin::::default(), blocker: self.config.blocker, automaton: deferred.clone(), relay: deferred.clone(), reporter: deferred, strategy: self.config.strategy, partition: format!("{}-simplex", self.config.partition_prefix), mailbox_size: self.config.mailbox_size, epoch: Epoch::zero(), floor, replay_buffer, write_buffer, page_cache, leader_timeout: Duration::from_secs(1), certification_timeout: Duration::from_secs(2), timeout_retry: Duration::from_millis(500), view_retention: ViewDelta::new(10), skip: SkipPolicy::Enabled { timeout: Duration::from_secs(5), budget: SkipBudget::Participants, }, fetch_timeout: self.config.fetch_timeout, fetch_retry_delay: self.config.fetch_retry_delay, forward: ForwardPolicy::Disabled, track_historical_votes: false, }, ); let reshare_handle = reshare_actor.start(dkg); let marshal_handle = marshal_actor.start( reshare_mailbox, buffer_mailbox, (backfill_handler, backfill_resolver), ); let simplex_handle = simplex.start(votes, certificates, resolver_network); Handle::select([ buffer_handle, reshare_handle, marshal_handle, simplex_handle, ]) .await .expect("failed dkg"); } } #[derive(Clone)] struct DkgApp { _marker: PhantomData<(V, D)>, strategy: T, } impl Application for DkgApp where E: Send, V: Variant, D: Directory, T: Strategy, { type SigningScheme = ConsensusScheme; type Context = Context; type Block = Block; type Input = reshare::Input<(), V, ed25519::PrivateKey, D>; async fn propose( &mut self, (_, context): (E, Self::Context), ancestry: impl Ancestry, input: Self::Input, ) -> Option> { let parent = ancestry.peek()?.clone(); let height = parent.height().next(); let block = Block::new(context, parent.digest(), height, input.payload); Some(Digested::compute(block, &self.strategy).await) } async fn verify( &mut self, _: (E, Self::Context), _ancestry: impl Ancestry, ) -> bool { // reshare::Application already rejects a mismatched final block and an // earlier block carrying any payload except a dealer log from the // midpoint onward. The chain has no other state. true } } #[derive(Clone)] struct StaticParticipants { participants: Set

, directory: D, } impl ParticipantsProvider for StaticParticipants where P: PublicKey, D: Directory

, { type PublicKey = P; type Directory = D; async fn participants(&mut self, _: Epoch) -> Set { self.participants.clone() } async fn directory(&mut self, _: Epoch, _: Set) -> Self::Directory { self.directory.clone() } } #[derive(Clone)] struct NoopRegistrar(PhantomData<(V, P)>); impl Registrar for NoopRegistrar where V: Variant, P: PublicKey, { type Variant = V; type PublicKey = P; async fn register(&self, _: Epoch, _: SchemeInfo) {} } fn archive_config( prefix: &str, name: &str, page_cache: CacheRef, write_buffer: NonZeroUsize, replay_buffer: NonZeroUsize, items_per_section: NonZeroU64, codec_config: C, ) -> prunable::Config { prunable::Config { translator: TwoCap, metadata_partition: format!("{prefix}-{name}-metadata"), key_partition: format!("{prefix}-{name}-key"), key_page_cache: page_cache, value_partition: format!("{prefix}-{name}-value"), compression: None, codec_config, items_per_section, key_write_buffer: write_buffer, value_write_buffer: write_buffer, replay_buffer, } } #[cfg(test)] mod tests { use super::*; use crate::dkg::{network::Addresses, tests::mocks::widest_addresses}; use bytes::Bytes; use commonware_codec::{Decode, varint::MAX_U32_VARINT_SIZE}; use commonware_cryptography::{ bls12381::primitives::{ group::Scalar, variant::{MinPk, MinSig}, }, transcript::{Transcript, Version}, }; use commonware_math::poly::Poly; use commonware_p2p::Address; use commonware_utils::{NZDuration, NZU16, NZU32, NZU64, Widen, test_rng}; /// Namespace for the transcripts these tests summarize. const NAMESPACE: &[u8] = b"_COMMONWARE_GLUE_DKG_BOOTSTRAP"; /// A one-shot block with a key-only directory. type KeyOnly = Block; /// A one-shot block with an address directory. type Addressed = Block>; /// Returns `count` sorted participants. fn participants(count: u32) -> Set { Set::from_iter_dedup( (0..u64::from(count)).map(|seed| ed25519::PrivateKey::from_seed(seed).public_key()), ) } /// Returns the encoding of the widest block whose epoch artifact names `sets[0]` in its /// output, `sets[1]` as players, and `sets[2]` as next players, ending with `directory`. /// Epochs, views, and height use their widest encodings. fn widest_block>( sets: [&Set; 3], directory: &D, ) -> Bytes { // Start from the widest payload-free block and set its payload flag. let count = u32::try_from(sets[0].len()).unwrap(); let mut bytes = Block:: { context: Context { round: Round::new(Epoch::new(u64::MAX), View::new(u64::MAX)), leader: ed25519::PrivateKey::from_seed(0).public_key(), parent: (View::new(u64::MAX), Sha256::hash(&[b"parent"])), }, parent: Sha256::hash(&[b"parent"]), height: Height::new(u64::MAX), payload: None, } .encode() .to_vec(); assert_eq!(bytes.pop(), Some(0)); 1u8.write(&mut bytes); // Append the epoch artifact's tag, outcome, and epoch, then its output: a summary, a // sharing of `count` coefficients, and dealer, player, and revealed sets naming `sets[0]`. 1u8.write(&mut bytes); 0u8.write(&mut bytes); Epoch::new(u64::MAX).write(&mut bytes); Transcript::new(NAMESPACE, Version::V1) .summarize() .write(&mut bytes); 0u8.write(&mut bytes); count.write(&mut bytes); Poly::<::Public>::commit(Poly::::new(test_rng(), count - 1)) .write(&mut bytes); for _ in 0..3 { sets[0].write(&mut bytes); } // Append the artifact's player and next player sets, then the directory. sets[1].write(&mut bytes); sets[2].write(&mut bytes); directory.write(&mut bytes); Bytes::from(bytes) } /// Checks the block bound against the widest encoding and the unused width of its prefixes. #[test] fn max_encode_size_covers_widest_block() { for participants in [1u32, 4, 128] { let cfg = (NZU32!(participants), ModeVersion::v0()); let count: usize = Widen::widen(participants); let prefix_slack = MAX_U32_VARINT_SIZE - count.encode_size(); // The widest block with a key-only directory names every peer in every set. Its bound // adds the unused width of the artifact's commitment prefix and five participant-set // prefixes. let peers = self::participants(participants); let key_only = KeyOnly::decode_cfg(widest_block([&peers; 3], &Unit), &cfg) .expect("the widest block with a key-only directory must decode"); assert_eq!( key_only.encode().len() + 6 * prefix_slack, KeyOnly::max_encode_size(&cfg) ); // The widest block with an address directory names disjoint output players, players, // and next players, so its directory holds three times `participants` peers, each // behind the widest address. Its bound also adds the unused width of the directory's // key-set and address-list prefixes and of each address's hostname prefix. let peers = self::participants(3 * participants); let sets: Vec> = (0..3) .map(|i| Set::from_iter_dedup(peers.iter().skip(i * count).take(count).cloned())) .collect(); let directory = widest_addresses(&peers); let entries = 3 * count; let address_slack = Address::MAX_ENCODE_SIZE - directory.get(&peers[0]).unwrap().encode_size(); let directory_slack = 2 * (MAX_U32_VARINT_SIZE - entries.encode_size()) + entries * address_slack; let addressed = Addressed::decode_cfg( widest_block([&sets[0], &sets[1], &sets[2]], &directory), &cfg, ) .expect("the widest block with an address directory must decode"); assert_eq!( addressed.encode().len() + 6 * prefix_slack + directory_slack, Addressed::max_encode_size(&cfg) ); } } /// `max_payload_size` is the largest of the Simplex, marshal, and DKG payload bounds for /// both directory types. #[test] fn covers_every_component() { for participants in [1u32, 4, 128] { let cfg = (NZU32!(participants), ModeVersion::v0()); let simplex = simplex::max_payload_size::(participants); let dkg = dkg::max_payload_size::(cfg.0); for (bound, marshal) in [ ( max_payload_size::(cfg.0, ModeVersion::v0()), marshal::max_payload_size::, ConsensusScheme>( participants, &cfg, ), ), ( max_payload_size::>( cfg.0, ModeVersion::v0(), ), marshal::max_payload_size::, ConsensusScheme>( participants, &cfg, ), ), ] { let terms = [simplex, marshal, dkg]; assert!(terms.iter().all(|term| *term <= bound)); assert!(terms.contains(&bound)); } } } #[test] #[should_panic(expected = "sharing mode must be supported by max supported mode")] fn rejects_unsupported_sharing_mode() { let config = Config { signer: ed25519::PrivateKey::from_seed(0), manager: (), blocker: (), secret_store: (), strategy: (), namespace: b"test", sharing_mode: SharingMode::RootsOfUnity, reveal: Reveal::V1, max_supported_mode: ModeVersion::v0(), partition_prefix: "test".into(), page_cache: CacheRef::new(NZU16!(1024), NZUsize!(1)), write_buffer: NZUsize!(2048), replay_buffer: NZUsize!(2048), mailbox_size: NZUsize!(16), fetch_timeout: NZDuration!(Duration::from_secs(2)), fetch_retry_delay: Duration::from_millis(100), items_per_section: NZU64!(10), participants: Set::default(), directory: Unit, blocks_per_epoch: NZU64!(1), }; let _ = Engine::<_, MinPk, _, _, _, _, _>::new((), config); } }