Implement an AVID protocol for batched AVSS#954
Conversation
|
|
||
| /// BCS-serialized length of a `SharesForNode` for a node of the given weight at the given | ||
| /// batch size. | ||
| fn bcs_serialized_size(weight: usize, batch_size: usize) -> usize { |
There was a problem hiding this comment.
This is a bit of a hack: The erasure code pads zeros to fit the block size, so when decoding, we need to truncate before decrypting. This function computes the expected size of a parties' shares given its weight.
This is safer than relying on the dealer to give the expected length and more efficient than saving this along with the shards, but suggestions are welcome.
Splits the support-module changes out of #954 so that PR can focus on the AVID/AVSS protocol itself. No protocol changes here: - Cargo.toml: drop serde-big-array (unused), add reed-solomon-erasure for the RS code. - ecies_v1.rs: small additions used by the AVSS complaint flow, with a regression test. - nodes.rs: add `collect_to_nodes` helper. - reed_solomon.rs: extend the RS wrapper used for AVID dispersal, with doc comments on encode/decode.
| .collect_vec(); | ||
|
|
||
| // The encryption used, counter-mode, is length-preserving, so the length of the ciphertext is equal to the length of the plaintext. | ||
| let expected_length = SharesForNode::bcs_serialized_size( |
There was a problem hiding this comment.
"Expected length" recomputed from weight + batch_size depends silently on SharesForNode's BCS layout. If the layout ever changes, the AVID consistency check breaks without a compile error. Consider putting the expected length in CommonMessage?
There was a problem hiding this comment.
Yea, I thought about that. That's both extra data (manageable) but also an attack vector for the dealer. I've added a unit test which checks that the computation is always correct but I agree, it is indeed a bit of hack.
There was a problem hiding this comment.
I've already put a todo to consider other options, so I'll leave it as is for now.
There was a problem hiding this comment.
Fair to defer — just leaving an option for when you revisit. The wire-bloat / dealer-supplied-length concerns only apply when the length is on the wire. Computing this from a fresh SharesForNode of the right shape (weight * ShareBatch of batch_size default scalars) via bcs::to_bytes(...).len(), cached at Receiver::new, removes the duplicated layout knowledge — a future field added to ShareBatch would automatically update both the wire serialization and this computation.
There was a problem hiding this comment.
That's a good point, but then it assumes that the length of a SharesForNode is constant for any inputs (given weight and batch_size) which is true today, but is still an assumption to make.
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { |
There was a problem hiding this comment.
Consider adding tests for the three newly-added validation paths?
3ea8534 to
8ef6f6b
Compare
| .collect_vec(); | ||
|
|
||
| // The encryption used, counter-mode, is length-preserving, so the length of the ciphertext is equal to the length of the plaintext. | ||
| let expected_length = SharesForNode::bcs_serialized_size( |
There was a problem hiding this comment.
Fair to defer — just leaving an option for when you revisit. The wire-bloat / dealer-supplied-length concerns only apply when the length is on the wire. Computing this from a fresh SharesForNode of the right shape (weight * ShareBatch of batch_size default scalars) via bcs::to_bytes(...).len(), cached at Receiver::new, removes the duplicated layout knowledge — a future field added to ShareBatch would automatically update both the wire serialization and this computation.
|
|
||
| /// RS-encode `ciphertext`, rebuild the per-recipient Merkle tree, and check its root matches | ||
| /// the dealer's `expected_root`. Errors with [InvalidMessage] on mismatch. | ||
| fn check_avid_consistency( |
There was a problem hiding this comment.
It does a full RS-encode of W shards + Merkle tree rebuild per call. With W = 700 and per-dealer recovery, this is the dominant recurring cost on the recovery path. Worth a follow-up to see whether the per-accuser tree can be cached across handle_* paths for the same accuser, since both currently re-derive it.
9e76919 to
acef21d
Compare
…quired certificate
…mers 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.
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.
…Message Align the message type names with the module's optimistic/pessimistic phase vocabulary.
benr-ml
left a comment
There was a problem hiding this comment.
The names and documentation need a cleanup - consider using
- Avss for the first round / optimistic message; Avid for the second round.
- DealerCommonAvssMessage, DealerCommonAvidMessage; DealerAvssMessage, DealerAvidMessage; AvidEchoMessage, and so on
- Let's move documentation as steps in the impl of dealer and receiver (we have that partially there, and partially in the top; let's move that to the impl, with concrete order 1, 2, 3..., and clear assumptions about the caller / things needs to be done (e.g., check sig, persist message, etc)
|
|
||
| /// Threshold parameters for the AVSS protocol. | ||
| #[derive(Copy, Clone, Debug)] | ||
| pub struct Parameters { |
There was a problem hiding this comment.
should avss use this as well?
There was a problem hiding this comment.
AVSS doesn't use the f parameter (only t) right now, but if should. So yes, but probably in a follow up pr
| if self | ||
| .nodes | ||
| .total_weight_of(verified_confirmers.confirmers.iter())? | ||
| < self.params.t + self.params.f |
There was a problem hiding this comment.
I think W - f actually?
… 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.
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.
…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.
- 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.
- 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.
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.
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
…ow, 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
| /// One recipient's slice of a dispersal. | ||
| #[derive(Clone, Debug, Serialize, Deserialize)] | ||
| pub struct DispersalEntry { | ||
| pub recipient_root: merkle::Node, |
There was a problem hiding this comment.
isn't that included/can be computed from in the merkle proof?
| } | ||
|
|
||
| /// 1. Disperse one payload per recipient. Returns the `dispersal_hash` (the signing target | ||
| /// for the dispersal layer) and one [Dispersal] per party in `nodes`. The `dst` |
There was a problem hiding this comment.
why is the dst needed? binding to the protocol is done by the signature, no?
| pub fn verify_echo( | ||
| &self, | ||
| echo: Echo, | ||
| dispersal: &VerifiedDispersal, |
There was a problem hiding this comment.
when do i need to verify echos if i already have VerifiedDispersal? shouldn't this be just dispersal_hash?
There was a problem hiding this comment.
But you still need echos (and verifying them) even though you verified the dispersal message, since that just checks the hash. Or do you mean that using the entire VerifiedDispersal is overkill since you just need the hash?
There was a problem hiding this comment.
Oh, I saw your comment in the other file - it's an issue in case you don't have the dispersal message. But how do you verify the merkle proofs then?
There was a problem hiding this comment.
just need the hash - me receiving echos is independent of me sending them to others
|
|
||
| /// 3b. Collect the shards from a quorum of [VerifiedEcho]s, keyed by sender. Rejects duplicate | ||
| /// senders and requires `≥ W − 2f` weight. | ||
| pub fn collect_shards( |
There was a problem hiding this comment.
when do we need to call collect_shards and not to decode_or_complain?
| verified_common: VerifiedAvssCommonMessage, | ||
| verified_confirmers: &VerifiedConfirmers, | ||
| ) -> FastCryptoResult<(VerifiedPessimisticMessage, BTreeMap<PartyId, Echo>, Vote)> { | ||
| // Require `W − f` weight of confirmers so that at least `W − 2f` of them are honest — the |
There was a problem hiding this comment.
nit - what we want here is t+f
There was a problem hiding this comment.
But we should use t for the rs quorom then, right?
There was a problem hiding this comment.
we need t+f for the first cert and 2f for the second
| pub fn verify_echo( | ||
| &self, | ||
| echo: Echo, | ||
| verified_message: &VerifiedPessimisticMessage, |
There was a problem hiding this comment.
as before, how will i have VerifiedPessimisticMessage in case the sender didn't send me anything
There was a problem hiding this comment.
(this is one of the reasons i prefer to skip pushed echos and rely on polling after seeing the cert and the hashes)
There was a problem hiding this comment.
So if you don't get anything, you should wait for the cert of Votes and use the hash from there?
There was a problem hiding this comment.
yes, otherwise we need to pass with the echo the first cert + a signature by the dealer (otherwise how can the receiver indeed verify them?)
No description provided.