Skip to content

Commit 90c4363

Browse files
jonas-ljbenr-ml
andauthored
Implement an AVID protocol for batched AVSS (#954)
* Erasure code + split encryption * Better interface for erasure code * Compute challenge from roots * Group shards to parties * Individual messages * Add avid part of message * Roots as part of avid msg * Add echo_message function * e2e works * Add root check * Remove ciphertext from message * refactoring * Return root from process_echo_messages * More refactor * draft * doc * Refactor complaint handling - Add Complaint enum with Reveal and Blame variants - Unify handle_complaint to verify both - Rename DecryptedShares -> DecryptionOutcome, AvidMessage -> AuthenticatedShards, Message.avid -> dispersal - Cache ErasureCoder on Receiver * Renames and doc trims * Drop r_i/pi_i from Complaint, add share recovery test - handle_complaint and recover take &Message; verifier looks up r_i from message.dispersal[accuser_id] - create_message takes an optional plaintext mutation closure for tests - Restore test_share_recovery exercising the cheating-dealer/Reveal/ recover path * fmt * Truncate ciphertext and add bcs_serialized_size - Add SharesForNode::bcs_serialized_size and a roundtrip test against BCS over various weight × batch_size combinations - Truncate the Reed-Solomon decoded ciphertext to the original length in reconstruct_ciphertext, removing the zero-padding TODOs - Reject contributions whose shard count doesn't match the contributor's weight before decoding - Doc and naming cleanups * Split Complaint into Reveal/Blame, add Response and Vote - Replace the Complaint enum with two standalone Reveal and Blame structs - Add Response (Vote / Reveal / Blame) and Vote types; expose DecryptionOutcome::into_response - Drop r_i and pi_i from the Reveal/Blame payload — the verifier looks them up locally from message.dispersal[accuser_id] - Inline verify_reveal/verify_blame into the public handle_* methods - Use fastcrypto's SCALAR_SIZE_IN_BYTES instead of a local constant - Doc trims and a few clarifying TODOs * Resurrect test_e2e, take Message in verify_and_decrypt - verify_and_decrypt and the test happy path now take &Message instead of &CommonMessage (consistent with handle_reveal/blame and recover) - Restore test_e2e exercising DKG -> presigning -> signing -> key rotation -> signing against the current API, with explanatory comments and a reshare-commitment check - Remove the rest of the commented-out tests in batch_avss.rs - Add short docs on EchoMessage, ProcessedEchoMessages, DecryptionOutcome - Rename shadowed echo_messages bindings to echoes_by_sender / echoes_by_recipient * Restore original style in test_e2e * Cargo fmt + clippy fixes; rename Reveal/Blame variants - Variants in DecryptionOutcome and Response renamed to InvalidShares / InvalidDispersal (struct types stay Reveal / Blame) - Field renames in EchoMessage, ProcessedEchoMessages, Vote, Reveal, Blame, ShardContribution, AuthenticatedShards for clarity (sender, global_root, recipient_root, recipient_root_proof, shards_proof, common_message_hash) - Use SCALAR_SIZE_IN_BYTES from fastcrypto::groups::secp256k1 - Construct AuthenticatedShards directly in create_message; lift CommonMessage out of the per-receiver loop - Drop the orphan `pub` on EchoMessage.shards_proof - Bump protocol step numbers to 1..6 - Apply cargo fmt and resolve all clippy --all-targets warnings: unused imports, doc_lazy_continuation, large_enum_variant, too_many_arguments, needless_borrow, implied_bounds_in_impls - Rewrite the broken batch_avss bench to match the current API * Fold shards triple into AuthenticatedShards in EchoMessage - Replace EchoMessage's (recipient_root, shards, shards_proof) trio with a single authenticated_shards: AuthenticatedShards field - Add AuthenticatedShards::verify(leaf_index) and use it in echo_message and process_echo_messages instead of inline Merkle proof checks - Move impl AuthenticatedShards / impl DecryptionOutcome after the type definitions block - Misc tidies (typo fix, rename `r` -> `global_root`, etc.) * Hoist roots, rename echo_messages -> valid_echoes * Rename EchoMessage/ProcessedEchoMessages and field renames - EchoMessage -> Echo, ProcessedEchoMessages -> ProcessedEchos - AuthenticatedShards: root -> recipient_root, shards_proof -> proof - ShardContribution: shards_proof -> proof - Local rename digest -> common_message_hash - Add a short doc on AuthenticatedShards - Reuse assert_valid in test_happy_path * Add Echo::verify and recipient_tree helpers - Echo::verify wraps both Merkle proof checks (inner shards + outer recipient-root binding); used in process_echo_messages - Free function recipient_tree builds the per-recipient Merkle tree over per-node shard chunks; used by Dealer::create_message and Receiver::check_avid_consistency - global_root method becomes global_tree returning a MerkleTree; callers take .root() as needed - Drop the recipient_roots Vec; just call tree.root() inline * Add ShardContribution::verify and use it in handle_blame * Clean up * Reorder + clean up * Update docs + function signature * Change signature * Drop AVID consistency check from handle_reveal * refactor * refactor * Fix inverted length check in RS decode Recovered data is shard-aligned, so it's always >= expected_len; the old check rejected every roundtrip whose length wasn't an exact multiple of the shard size. * Add State to DecryptionOutcome::Valid State carries common_message + global_root + recipient_root, so a receiver only needs (State, ReceiverOutput) to handle later Reveal / Blame requests. handle_reveal and handle_blame drop their separate ProcessedEchos / CommonMessage parameters. * Rename echo_message -> echo, process_echo_messages -> process_echos * Bind complaints to dealer broadcast via recipient_root + proof Reveal and Blame now carry the accuser's per-ciphertext Merkle root plus a proof binding it under the dealer's global_root. handle_reveal verifies the proof and re-runs check_avid_consistency, closing a share-extraction attack where a malicious accuser could submit any ciphertext that decrypts to invalid shares and trick honest parties into responding. handle_blame verifies the same proof and uses the verified accuser's r_i to authenticate the contributed shards (the previous code checked them against the verifier's own r_i, silently rejecting genuine dispersal complaints). State drops the verifier's own recipient_root since it's no longer needed for either handler. Adds test_share_recovery_blame mirroring test_share_recovery, plus a shard-mutation hook on create_message_with_mutation so the test can corrupt the dealer's RS dispersal. * Pass State to recover instead of Message DecryptionOutcome is now { state, kind: OutcomeKind } so every outcome carries the state, and the accuser feeds their own state into recover. Removes the trust-an-arbitrary-Message footgun. * Prefer Blame to Reveal; group AVID helpers at end of impl Dispatch in verify_and_decrypt now routes any AVID-inconsistent outcome to Blame, regardless of share-decryption result. Reveal only fires when AVID is consistent but shares are bad — the genuine shares-layer case where Blame would not verify. reconstruct_ciphertext and check_avid_consistency moved next to the other Receiver helpers. * Use all_unique; mention recover in State doc * Extract ComplaintHeader from Reveal/Blame Reveal and Blame each carry the same recipient_root + proof + hash binding the complaint to the dealer's broadcast; pull them into a shared ComplaintHeader with a verify method so handle_reveal and handle_blame share that step. * Clean up * Update docs * Take id in process_echos * Fix inverted AVID check in handle_blame; rename ProcessedEchos -> DecodedCiphertext The AVID check in handle_blame was propagating Err on mismatch, but a valid Blame requires the re-encoded ciphertext to NOT match the accuser's r_i. Invert the check so genuine dispersal complaints verify and bogus ones are rejected. * Move AVID consistency check into decode_ciphertext_for_party The re-encoding faulty_dealer check belongs at the AVID layer, not mixed into verify_and_decrypt. decode_ciphertext_for_party now returns a DecodeOutcome of Decoded(DecodedCiphertext) or InvalidDispersal { blame, global_root }; verify_and_decrypt only sees the Decoded case and produces Valid or InvalidShares. A Blame accuser obtains global_root from the InvalidDispersal arm so they can later assemble a State for recover once they hold the CommonMessage. Also adds short doc comments on RS encode/decode. * Drop global_root and State; carry recipient_roots in CommonMessage Replaces the global Merkle root over per-recipient roots with the explicit Vec<recipient_root> in CommonMessage, used directly when deriving the challenge. Echoes lose global_root and recipient_root_proof; AuthenticatedShards loses recipient_root; ComplaintHeader and Vote shed their global_root fields; Reveal/Blame collapse to just the hash. decode_ciphertext_for_party now takes &CommonMessage to verify echoes against the dealer's r_i. handle_reveal/handle_blame/recover take &CommonMessage; the State wrapper is gone — DecryptionOutcome carries common_message directly. Wire size: per-echo Merkle proof for recipient_root removed (W of them); CommonMessage grows by n × 32 bytes for recipient_roots. * Slim Blame; drop DecodedCiphertext and outcome wrappers - DecodeOutcome::Decoded now carries Vec<u8> directly (DecodedCiphertext was a single-field wrapper). - DecryptionOutcome collapses to a flat enum (no common_message field, no OutcomeKind wrapper). Tests pull common_message from the message they already hold. - decode_ciphertext_for_party returns InvalidDispersal on RS-decode failure too, not just on re-encode mismatch — closes the AVID liveness gap when the dealer ships shards that don't form a codeword. - Blame loses its shards/ShardContribution payload. handle_blame takes the verifier's locally observed echoes and re-runs decode for the accuser; valid iff that decode yields InvalidDispersal. * Documentation pass - Module doc: setup, happy path, complaint paths - Per-method protocol docs (create_message, echo, decode, verify_and_decrypt, handle_reveal, handle_blame, recover) - CommonMessage doc: usage, longevity, recovery-path note - Digest type alias using Blake2b256::OUTPUT_SIZE * Update module doc * Restore Blame shards; doc + ergonomics polish - Blame regains its Vec<ShardContribution>; handle_blame validates the carried shards against recipient_roots[accuser_id], runs the same decode-and-re-encode logic, and works under strict point-to-point echoes (no broadcast assumption needed). - decode_ciphertext_for_party renamed to decode_ciphertext and drops the redundant `party` argument (always self.id at every call site). - New CommonMessage::verify checks the well-formedness + polynomial commitment and returns the Fiat-Shamir challenge. Called by all five receiver entry points that take a CommonMessage. - Module + per-fn doc rewrite; Digest type alias via Blake2b256::OUTPUT_SIZE. * Drop unused Response enum and into_response Neither was referenced outside its own definition. * cargo fmt * docs * Authenticate ComplaintResponses with recovery packages ComplaintResponse now carries (responder_id, ciphertext, recovery_package) instead of plaintext shares. handle_reveal/handle_blame take the responder's own ciphertext and build a fresh ECIES recovery package; recover authenticates each response by AVID-binding the ciphertext to the dealer's broadcast and decrypting via the recovery package. Closes the L-degrees-of-freedom hole where a malicious responder could forge verify-passing-but-fake shares: forging now requires an ECIES NIZK on the dealer's actual ciphertext, which is infeasible. Tests now retain each receiver's ciphertext after decode and pass it to the complaint handlers. * Drop ShardContribution; Blame.shards is BTreeMap Replace `Vec<ShardContribution>` with `BTreeMap<PartyId, AuthenticatedShards>` — sender uniqueness is now enforced by the map keys, lookup in reconstruct_ciphertext is O(log n), serialization is canonical (BCS sorted keys), and the wrapper struct goes away. * Simplify decode_ciphertext's dispersal-consistency branch * Clean up * Tighten CommonMessage API and AVID helpers - CommonMessage gains hash() and recipient_root(id) accessors. Replaces free fns and repeated `recipient_roots.get(id as usize).ok_or(...)`. - reconstruct_ciphertext now takes &BTreeMap<PartyId, AuthenticatedShards> directly, dropping the closure indirection. - recover drops the upfront total_response_weight check (the filter_map already handles invalid responder ids and the post-filter weight check enforces the quorum). - Inline require_uniform_common_message_hash at its only call site. * Rewrite batch_avss module doc * Touch up batch_avss docs and decode_ciphertext branch * Various clean up * Collapse map().flatten() to flat_map * Rename DecryptionOutcome::InvalidShares to Invalid; verify_and_decrypt takes &[u8] * simplify * clippy * Add VerifiedEcho; Receiver::verify_echo wraps private Echo::verify * Add VerifiedComplaintResponse and Receiver::verify_complaint_response * Tidy verify_complaint_response * Add VerifiedCommonMessage; CommonMessage::verify returns it * Clean up * Reject malformed Blame in handle_blame; tidy module doc and reconstruct_ciphertext doc * Document Blame must wait for matching certificate before broadcast * Switch erasure coding to GF(2^16); rename Blame/Reveal; tighten input checks * Tidy: hoist cfg_attr to function level, rename encode locals * Apply suggestions from code review Co-authored-by: benr-ml <112846738+benr-ml@users.noreply.github.com> * Note that echo emits one entry addressed to self * Replace stored ErasureCoder with Receiver::get_coder; explicit param checks * Add validate_parameters and shared get_coder helper used by Dealer and Receiver * Clean up * Tighten bytes_to_elems error handling; note GF(2^16) bound * Introduce Element/ELEMENT_SIZE_IN_BYTES; verify zero padding; tidy decode error * Simplify decode: drop redundant guard, merge length/zero check, untangle shadowed binding * Rename Complaint to RecoveryProof; move accuser_id to wire types * Move ComplaintResponse into avss; drop unused generic parameter * Rename complaint.rs to recovery_proof.rs * Add benches for verify_common_message, echo, verify_echo, verify_and_decrypt * Add optimistic phase; AVID runs only for pending recipients with a required certificate * Replace IndirectMessage.confirmers with a caller-built VerifiedConfirmers Move the confirmer set out of every IndirectMessage and into a VerifiedConfirmers the caller constructs (confirmer ids + the common_message_hash they attested to). echo now takes it, checks the t+f weight quorum and that the confirmers attested to the same common message, and the dispersal/confirmer partition is checked as a single multiset equality. Make IndirectMessage::verify private and drop the unused pending_recipients. Document the load-bearing H(E_i) ciphertext check and the RevealComplaint.accuser_id unforgeability. * Split verify_and_decrypt into a strict core and a complaint wrapper Replace the `broadcast_hash: Option<Digest>` overload with explicit steps: check_ciphertext_hash (hard error on a swapped ciphertext) and decrypt_and_verify_shares (returns the underlying error). The optimistic path now surfaces the real error instead of collapsing every failure into InvalidMessage; verify_and_decrypt takes a plain Digest and turns a share-verification failure into a RevealComplaint. * Rename DirectMessage/IndirectMessage to OptimisticMessage/PessimisticMessage Align the message type names with the module's optimistic/pessimistic phase vocabulary. * batch_avss: key messages/echoes by PartyId, rename types/fields, tidy docs - create_optimistic_messages and echo now return BTreeMap<PartyId, _> keyed by recipient instead of Vec; update tests and bench accordingly. - Rename for clarity: VerifiedMessage -> VerifiedPessimisticMessage, CommonMessage -> AvssCommonMessage (+ Verified variant), broadcast_hash -> dispersal_hash, the `common` fields -> `avss_common`, and common_message_hash -> avss_common_message_hash. - Module doc trimmed to an overview; the per-step protocol detail, signing targets, complaint semantics, and retention guidance now live on the functions/types that produce them. Protocol steps renumbered 1..8 consistently. - Fix the benchmark for these changes (it had also drifted from earlier refactors): 3-arg echo with a VerifiedConfirmers, BTreeMap-keyed echoes, and make the benchmarked recipient the straggler so it runs. * Extract a generic AVID layer into threshold_schnorr::avid The new `avid` module disperses opaque byte payloads (one per recipient) over the weighted node set: RS-encode, Merkle-commit, echo, reconstruct, blame. It binds each dispersal to a caller-supplied `context` digest. batch_avss now layers on top of it: PessimisticMessage/Echo/BlameComplaint are re-exports of the avid types, create_pessimistic_messages serializes the ciphertexts as payloads bound to H(v), and the receiver keeps only the AVSS-specific checks (confirmer partition, H(E_i) match against v, decrypt). This removes ~280 lines of RS/Merkle plumbing from batch_avss. Tests and bench updated; behavior unchanged. * Return BTreeMap from create_pessimistic_messages; add avid complaint test - create_pessimistic_messages now returns BTreeMap<PartyId, PessimisticMessage> directly from the AVID layer, dropping the redundant dispersal_hash (already pinned in every message) and the Vec flattening. - Add cheating_dealer_complaint unit test in avid.rs covering complaint create + verify end-to-end. * Refine AVID context binding and confirmer-weight check - AVID disperse/verify_dispersal take a dst: &[u8] context instead of a Digest. - Rename enough_weight -> sufficient_weight, decode_or_complain param -> id. - echo requires W - f confirmer weight. * Clarify AVID/AVSS docs after refactor - avid.rs: fix stale context -> dst references in dispersal_hash docs. - echo: document why confirmers need W - f weight. - decode_ciphertext/handle_blame: note the length-preserving-ciphertext assumption behind expected_len. - verify_and_decrypt: document that dispersal_hash is unauthenticated and must come from the VerifiedPessimisticMessage. - VerifiedConfirmers: spell out the caller-asserted signature-verification safety contract. * Reuse one Reed-Solomon coder per session via an Avid field Building the (W, W-2f) coder inverts a k*k matrix (O(k^3)), and it was being rebuilt on every avid() call -- twice per decode_ciphertext/handle_blame. This dominated process_message (seconds at W=1500). Give Dealer/Receiver an owned avid::Avid field that builds the coder once and reuses it. Avid owns its coder and shares the node set with its parent via Arc<Nodes>, so there's a single node set, no per-call rebuilds, and no lifetimes threaded through the API. * Split AVID impl into batch_avss_avid; restore original batch_avss Move the new AVID-based implementation into a new batch_avss_avid module (plus its benchmark) and restore the original batch_avss module verbatim, along with the complaint module it depends on. - presigning stays wired to the original batch_avss output types - e2e tests and benchmarks exercise the new batch_avss_avid implementation, converting its (structurally identical) outputs via From impls so they can feed presigning * Clean up docs * Hash dst length also * Rewrap AVID docs, fix echoes spelling, return echoes from verify_dispersal * Address review: harden ciphertext indexing, guard AVID weight underflow, drop dead code, avoid shard clones - avss: reject ciphertexts with the wrong recipient count and index complaint encryptions fallibly, so a Byzantine dealer cannot panic responders - avid: guard W - 2f against underflow in Avid::new - ecies_v1: remove unused EncryptedPart struct and PhantomData import - avid: move per-sender shard chunks into dispersal messages instead of cloning * Revert shard-clone change, trim review-fix comments * Use checked_sub for AVID coder parameters * refactor * fix benchmarks * Better names + weight checks * Clean up * CLean up docs * Get rid of expected_len * Wrap docs * Rename echo function * Algin docs * unify trees * Clean up types * Clean up docs * Clean up * More clean up * Clean up tests * Clean up docs * More clean up * Clean up some more * Viz * Reencode * Naming * Add nested merkle tree * Simplify nested merkle tree --------- Co-authored-by: benr-ml <112846738+benr-ml@users.noreply.github.com>
1 parent 76a9da5 commit 90c4363

15 files changed

Lines changed: 3482 additions & 305 deletions

File tree

Cargo.lock

Lines changed: 136 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fastcrypto-tbls/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ zeroize.workspace = true
2323
itertools = "0.10.5"
2424
hex = "0.4.3"
2525
tap = { version = "1.0.1", features = [] }
26-
serde-big-array = "0.5.1"
26+
reed-solomon-erasure = "6.0.0"
2727

2828
[dev-dependencies]
2929
criterion = "0.5.1"
@@ -51,7 +51,7 @@ name = "avss"
5151
harness = false
5252

5353
[[bench]]
54-
name = "batch_avss"
54+
name = "batch_avss_avid"
5555
harness = false
5656

5757
[features]

0 commit comments

Comments
 (0)