Commit 90c4363
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
- fastcrypto-tbls
- benches
- src
- tests
- threshold_schnorr
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
| 26 | + | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
54 | | - | |
| 54 | + | |
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| |||
0 commit comments