//! Size limits for the payloads marshal sends. use crate::{ BoundedBlock, marshal::{core::Variant, resolver::handler::Key}, simplex::types::{Finalization, Notarization}, }; use commonware_codec::Read; use commonware_cryptography::certificate::Scheme; use commonware_resolver::p2p::max_message_size; /// Returns an upper bound on the backfill messages marshal sends, including their resolver /// framing, for committees of at most `participants` members and application blocks of at most /// `application` encoded bytes. fn max_backfill_size(participants: u32, application: usize) -> usize { // A response carries the backfill block alone, a notarization with the backfill block, or a // finalization with the application block. `max_message_size` covers the widest of these and // a request for a key, each with its resolver framing. let block = V::max_backfill_block_size(application); let notarized = Notarization::::max_encode_size(participants) + block; let finalized = Finalization::::max_encode_size(participants) + application; max_message_size::>(block.max(notarized).max(finalized)) } /// Returns an upper bound on the payloads marshal sends for committees of at most `participants` /// members and blocks decoded under `block_codec_config`: the larger of the backfill and buffer /// bounds. pub fn max_payload_size( participants: u32, block_codec_config: &::Cfg, ) -> usize where V::ApplicationBlock: BoundedBlock, { let application = V::ApplicationBlock::max_encode_size(block_codec_config); max_backfill_size::(participants, application) .max(V::max_buffer_payload_size(participants, application)) } #[cfg(test)] mod tests { use super::*; use crate::{ marshal::{ coding::{ Coding, types::{Shard, coding_config_for_participants}, }, mocks::{ block::encode_block, harness::{CodingB, CodingCtx, default_leader, make_coding_block}, }, standard::Standard, }, simplex::{ scheme::{Scheme, bls12381_multisig, bls12381_threshold, ed25519, secp256r1}, types::{Finalization, Notarization, Proposal, Subject}, }, types::{Epoch, Height, Round, View, coding::Commitment}, }; use commonware_codec::{Decode, Encode, EncodeSize, FixedSize, MaxEncodeSize}; use commonware_coding::{Config as CodingConfig, ReedSolomon, Scheme as _}; use commonware_cryptography::{ Committable, Sha256 as Sha256Hasher, bls12381::primitives::variant::{MinPk, MinSig}, certificate::{Scoped, Verifier as _, mocks::Fixture}, ed25519::PublicKey, sha256::Digest as Sha256, }; use commonware_math::algebra::Random as _; use commonware_parallel::Sequential; use commonware_resolver::p2p::{MAX_RESPONSE_OVERHEAD, REQUEST_OVERHEAD}; use commonware_utils::{Widen, non_empty, ordered::Quorum, test_rng}; use std::sync::Arc; /// Signing namespace for the certificates these tests assemble. const NAMESPACE: &[u8] = b"_COMMONWARE_CONSENSUS_MARSHAL_LIMITS"; type RS = ReedSolomon; /// Both variants over the coding harness block. Its context names its parent by commitment, so /// it is an application block of either variant. type StandardVariant = Standard; type CodingVariant = Coding; /// Checks backfill bounds and encodings for application blocks of at most `application` bytes. fn check_encoding>( fixture: Fixture, application: usize, ) { let schemes = fixture.schemes; let participants = u32::try_from(schemes.len()).unwrap(); let max_certificate_size = S::max_certificate_size(participants); let scoped = Scoped::verifier(Arc::new(fixture.verifier)); let backfill = max_backfill_size::(participants, application); let block = V::max_backfill_block_size(application); // The limit is the widest notarized response: a proposal and a certificate at their // bounds, the backfill block, and the response framing. assert_eq!( backfill, Proposal::::MAX_ENCODE_SIZE + max_certificate_size + block + MAX_RESPONSE_OVERHEAD ); // A request for each key at its widest and a block response fit the limit with their // resolver framing. let commitment = V::Commitment::random(test_rng()); for key in [ Key::Block(commitment), Key::Finalized { height: Height::new(u64::MAX), }, Key::Notarized { round: Round::new(Epoch::new(u64::MAX), View::new(u64::MAX)), }, ] { assert!(key.encode().len() + REQUEST_OVERHEAD <= backfill); } assert!(block + MAX_RESPONSE_OVERHEAD <= backfill); // Certificates for views around the one-byte varint boundary and the largest view, from a // quorum or every signer, fit `max_certificate_size` and decode. A notarized response adds // the backfill block to a notarization and a finalized response the application block to a // finalization. Each fits the limit. for view in [1, 127, 128, u64::MAX] { let proposal = Proposal::new( Round::new(Epoch::new(view), View::new(view)), View::new(view - 1), V::Commitment::random(test_rng()), ); for count in [ Widen::widen(schemes[0].participants().quorum::()), schemes.len(), ] { let attestations = schemes.iter().take(count).map(|scheme| { scheme .sign::(Subject::Notarize { proposal: &proposal, }) .unwrap() }); let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); let encoded = certificate.encode(); assert!(encoded.len() <= max_certificate_size); assert!( S::Certificate::decode_cfg(encoded, &scoped.certificate_codec_config()).is_ok() ); let notarization = Notarization:: { proposal: proposal.clone(), certificate: certificate.clone(), }; let finalization = Finalization:: { proposal: proposal.clone(), certificate, }; let notarized = notarization.encode().len() + block + MAX_RESPONSE_OVERHEAD; let finalized = finalization.encode().len() + application + MAX_RESPONSE_OVERHEAD; assert!(notarized <= backfill); assert!(finalized <= backfill); } } } #[test] fn fixed_certificate_overhead() { // Threshold certificates have one fixed size for any committee and signer count. let mut rng = test_rng(); check_encoding::( bls12381_threshold::standard::fixture::(&mut rng, NAMESPACE, 4), 0, ); check_encoding::( bls12381_threshold::standard::fixture::(&mut rng, NAMESPACE, 4), 1000, ); check_encoding::( bls12381_threshold::vrf::fixture::(&mut rng, NAMESPACE, 4), 0, ); check_encoding::( bls12381_threshold::vrf::fixture::(&mut rng, NAMESPACE, 4), 1000, ); } #[test] fn variable_certificate_overhead() { // These certificates carry a signer bitmap sized by the committee, and ed25519 and // secp256r1 add one signature per signer. let mut rng = test_rng(); for participants in [4, 128] { check_encoding::( ed25519::fixture(&mut rng, NAMESPACE, participants), 1000, ); } check_encoding::(secp256r1::fixture(&mut rng, NAMESPACE, 4), 0); check_encoding::( bls12381_multisig::fixture::(&mut rng, NAMESPACE, 4), 0, ); check_encoding::( bls12381_multisig::fixture::(&mut rng, NAMESPACE, 4), 1000, ); } #[test] fn coding_commitment_overhead() { // Coding commitments are wider than digests, and a coded block adds its configuration. let mut rng = test_rng(); check_encoding::( bls12381_threshold::vrf::fixture::(&mut rng, NAMESPACE, 4), 1000, ); for participants in [4, 128] { check_encoding::( ed25519::fixture(&mut rng, NAMESPACE, participants), 1000, ); } } #[test] fn standard_limits() { // Under standard, backfill grows byte for byte with the block and the buffer sends the // block alone, so the backfill bound is the payload bound for the block bound. let backfill = max_backfill_size::(4, 1000); let empty = max_backfill_size::(4, 0); assert_eq!(backfill, empty + 1000); assert_eq!(StandardVariant::max_buffer_payload_size(4, 1000), 1000); let application = CodingB::max_encode_size(&()); assert_eq!( max_payload_size::(4, &()), max_backfill_size::(4, application) ); } #[test] fn largest_block() { // The response-value bound can reach the resolver's `u32::MAX` byte limit. let empty = max_backfill_size::(4, 0); let widest: usize = Widen::widen(u32::MAX); let largest = widest - (empty - MAX_RESPONSE_OVERHEAD); let backfill = max_backfill_size::(4, largest); assert_eq!(backfill, widest + MAX_RESPONSE_OVERHEAD); } #[test] #[should_panic(expected = "value must fit the u32 length prefix")] fn response_overflow() { // One more block byte pushes the response-value bound past `u32::MAX`. let empty = max_backfill_size::(4, 0); let widest: usize = Widen::widen(u32::MAX); let largest = widest - (empty - MAX_RESPONSE_OVERHEAD); max_backfill_size::(4, largest + 1); } #[test] #[should_panic(expected = "committee must have at least 4 members")] fn coding_too_few_participants() { // Coding needs a committee of at least 4. CodingVariant::max_buffer_payload_size(3, 100); } #[test] #[should_panic(expected = "backend must support the shard counts")] fn coding_unsupported_participants() { // One member more than the largest committee the Reed-Solomon coder supports. CodingVariant::max_buffer_payload_size(49_154, 100); } #[test] fn coding_supported_participants() { // The largest committee the Reed-Solomon coder supports. assert!(CodingVariant::max_buffer_payload_size(49_153, 100) > 0); } /// Codes `data` bytes for every committee of 4 to `participants` members and returns the /// encoded size of the widest shard at any index, with the smallest committee size that /// produces it. fn widest_shard(participants: u16, data: usize) -> (usize, u16) { let data = vec![0xAB; data]; let mut widest = (0, 0); for n in 4..=participants { let config = coding_config_for_participants(n); let (root, shards) = RS::encode(&config, data.as_slice(), &Sequential).unwrap(); let commitment = Commitment::::from(( Sha256::random(test_rng()), root, Sha256::random(test_rng()), config, )); for (index, shard) in shards.into_iter().enumerate() { let index = u16::try_from(index).unwrap(); let size = Shard::new(commitment, index, shard).encode().len(); if size > widest.0 { widest = (size, n); } } } widest } /// Checks that `widest_shard` measures the shards a real coded block produces. #[test] fn coded_block_shards_match_encoded_data() { // The block's parent, context, and payload are arbitrary. let parent = Commitment::::from(( Sha256::random(test_rng()), Sha256::random(test_rng()), Sha256::random(test_rng()), coding_config_for_participants(4), )); let context = CodingCtx { round: Round::new(Epoch::zero(), View::new(1)), leader: default_leader(), parent: (View::zero(), parent), }; let block = make_coding_block(context, Sha256::random(test_rng()), Height::new(1), 7); for n in [4u16, 5, 33] { // A coded block encodes as the application block followed by its coding configuration, // so its size is what `max_backfill_block_size` reports. let config = coding_config_for_participants(n); let coded = encode_block::(block.clone(), config); let data = coded.encode(); assert_eq!( data.len(), Coding::::max_backfill_block_size( block.encode_size() ) ); // The same root proves the coder saw exactly the coded block's encoding, and each real // shard encodes to the size of the shard `widest_shard` builds from that data. let (root, shards) = RS::encode(&config, data.as_ref(), &Sequential).unwrap(); assert_eq!(root, coded.commitment().root()); for (index, shard) in shards.into_iter().enumerate() { let index = u16::try_from(index).unwrap(); let real = coded.shard(index).expect("shard must be encoded"); assert_eq!( real.encode().len(), Shard::new(coded.commitment(), index, shard).encode().len() ); } } } #[test] fn coding_limits() { // Committees of 4 to 6 members split a block into 2 data shards, and 5 or 6 members need // one more proof sibling than 4, so a large block is first widest at 5. A small block is // widest where the proof is deepest, and among 4 to 33 members only 33 needs 6 siblings. for (participants, block, n, expected) in [(16, 64 * 1024, 5, 32986), (33, 100, 33, 320)] { let buffer = CodingVariant::max_buffer_payload_size(u32::from(participants), block); let widest = widest_shard(participants, block + CodingConfig::SIZE); assert!(widest.0 <= buffer); assert_eq!(buffer, expected); assert_eq!(widest.1, n); } } #[test] fn coding_covers_shards() { type S = bls12381_threshold::standard::Scheme; // With a small certificate and block, the widest shard exceeds the backfill bound, so the // buffer bound sets the payload bound. for (participants, block, expected) in [(33, 100, 320), (100, 100, 348)] { let n = u32::from(participants); let (widest, _) = widest_shard(participants, block + CodingConfig::SIZE); assert!(widest > max_backfill_size::(n, block)); let buffer = CodingVariant::max_buffer_payload_size(n, block); assert!(widest <= buffer); assert_eq!(buffer, expected); } // The payload bound for the blocks the codec decodes is the larger of the two bounds. let application = CodingB::max_encode_size(&()); for n in [4, 33, 100] { assert_eq!( max_payload_size::(n, &()), max_backfill_size::(n, application) .max(CodingVariant::max_buffer_payload_size(n, application)) ); } } }