diff --git a/tinycloud-threshold/README.md b/tinycloud-threshold/README.md index 64526814..b80b32f0 100644 --- a/tinycloud-threshold/README.md +++ b/tinycloud-threshold/README.md @@ -4,7 +4,7 @@ BLS12-381 identity-based wrapping of 32-byte data keys and context-bound encrypt The crate is a workspace member with `publish = false`. No other workspace crate depends on it; the node server remains the default member. Release-plz neither releases nor publishes it. -## Implemented surface through S0.3 +## Implemented surface through S0.4 - Bounded, canonical scalar, point, identity, ciphertext, request and share encodings. - Boneh–Franklin FullIdent wrapping and unwrapping with the TinyCloud domains in [SPEC.md](SPEC.md). @@ -16,8 +16,9 @@ The crate is a workspace member with `publish = false`. No other workspace crate - Signed response fault classification, readiness receipts and all-holder or explicit outage-budget activation verification. - Ceremony-scoped RFC 9180 sealed dealings. - Genesis DKG (GJKR New-DKG) over owner-closed rounds: consume-self member states for dealing, complaints, answers, Feldman extraction, the common R5 round and R6 reconstruction, and `dkg_verify_transcript`, which recomputes the outcome from the raw signed round records. +- Redistribution for reshare and refresh over owner-closed rounds R1–R3: Feldman dealings from the current certificate's members to an authorized cohort with a new threshold, complaints and answers, deterministic selection of the first t eligible dealers, and `reshare_verify_transcript`. The master public key is unchanged; the output is a candidate for the successor certificate. -`VerifiedCertificate` supplies `KeyContext` and `PublicKeySet`; `KeyShare::bind` validates a candidate against its certificate. Raw candidate-scalar construction remains crate-private, and the dealer exists only under `#[cfg(test)]`. Redistribution rounds, durable storage and node-runtime paths remain separate slices. Their contracts and the implemented canonical byte layouts are recorded in [SPEC.md](SPEC.md). +`VerifiedCertificate` supplies `KeyContext` and `PublicKeySet`; `KeyShare::bind` validates a candidate against its certificate. Raw candidate-scalar construction remains crate-private, and the dealer exists only under `#[cfg(test)]`. Redistribution does not erase earlier shares; old and new shares carry different key contexts. Durable storage, retirement and node-runtime paths remain separate slices. Their contracts and the implemented canonical byte layouts are recorded in [SPEC.md](SPEC.md). The §3.6 request-digest amendment is explicit in SPEC.md. It makes an identity or transport-key transplant a context mismatch before share equations are evaluated. @@ -39,7 +40,7 @@ cargo deny --manifest-path tinycloud-threshold/Cargo.toml --locked --config tiny The `threshold` job in `.github/workflows/rust.yml` runs the full release suite, the explicitly selected cargo-deny policy and the independent primitive checker. Workspace test, build, formatting and clippy jobs also include this member. -The `checkpoint_2of3` filter runs two checkpoints: the test-dealer checkpoint prints nine executed outcomes, and `checkpoint_2of3_dkg` prints five outcomes for a signed 2-of-3 DKG with one reconstructed dealer, its certificate binding and threshold derivation. Required RFC tests are separately named `rfc9380_appendix_j_g1_sswu_ro`, `rfc9380_appendix_j_g2_sswu_ro` and `rfc9380_appendix_k1_expand_xmd_sha256`. +The `checkpoint_2of3` filter runs three checkpoints: the test-dealer checkpoint prints nine executed outcomes, `checkpoint_2of3_dkg` prints five outcomes for a signed 2-of-3 DKG with one reconstructed dealer, its certificate binding and threshold derivation, and `checkpoint_2of3_reshare` prints six outcomes for a DKG genesis redistributed to 3-of-4 and a refused redistribution below t valid dealers. Required RFC tests are separately named `rfc9380_appendix_j_g1_sswu_ro`, `rfc9380_appendix_j_g2_sswu_ro` and `rfc9380_appendix_k1_expand_xmd_sha256`. ## Vectors @@ -59,6 +60,12 @@ TC_THRESHOLD_REGEN_VECTORS=1 cargo test -p tinycloud-threshold primitive_vectors TC_THRESHOLD_REGEN_VECTORS=1 cargo test -p tinycloud-threshold dkg_vectors_regenerate_byte_for_byte_and_replay ``` +`vectors/v1/reshare.json` contains five redistribution transcripts from fixed seeds: the current certificate, the signed ceremony statement, the initial transcript link, every owner closure and member message, and either the recomputed dealers, exclusion reasons, commitments, public shares and transcript hash, or the expected error. `reshare_vectors_regenerate_byte_for_byte_and_replay` regenerates the file in memory, compares its complete bytes and replays every case through `reshare_verify_transcript`. To update it deliberately: + +```sh +TC_THRESHOLD_REGEN_VECTORS=1 cargo test -p tinycloud-threshold reshare_vectors_regenerate_byte_for_byte_and_replay +``` + ## Dependencies and licence This crate uses the workspace's [TinyCloud EGPL v1.5](../LICENSE.md) through `license-file`. Production dependencies: `blst` (Apache-2.0), `sha2` (MIT OR Apache-2.0), `zeroize` (Apache-2.0 OR MIT), `subtle` (BSD-3-Clause), `rand_core` (MIT OR Apache-2.0), and `thiserror` (MIT OR Apache-2.0). Development dependencies: `proptest`, `rand_chacha`, `serde`, `serde_json`, and `hex` (MIT OR Apache-2.0). diff --git a/tinycloud-threshold/SPEC.md b/tinycloud-threshold/SPEC.md index 1fa5b5ac..0d75f340 100644 --- a/tinycloud-threshold/SPEC.md +++ b/tinycloud-threshold/SPEC.md @@ -220,7 +220,7 @@ All integer fields below are big-endian. `lp` is u32 length followed by bytes. - No S0.1 public decoder consumes a member list; lists introduced in later slices use the §3.2 sorted/count rules. - An availability budget counts all unavailable ready signers, including dishonest claims: `q - b >= t′`. All four ready holders in 3-of-4 support one unavailable holder, not two. `WithOutageBudget(0)` is not accepted. -## Implemented surface through S0.3 +## Implemented surface through S0.4 S0.1 implements §§3.1–3.6 and the typed parameters in §3.0. S0.2 adds the authenticated-context boundaries in §3.0, the certificate and fault-proof APIs in §3.9, and the receipt/activation verification in §3.12: @@ -233,7 +233,9 @@ S0.1 implements §§3.1–3.6 and the typed parameters in §3.0. S0.2 adds the a S0.3 adds the §3.10 sealed dealings with ceremony-scoped recipient keys and the §3.7 genesis DKG: consume-self member states over owner-closed rounds R1–R6, and `dkg_verify_transcript`, which recomputes the outcome from the raw signed round records. Its byte layouts and deterministic rules are in the S0.3 genesis DKG appendix below. -The dealer remains test-only. Redistribution rounds in §3.8, durable storage/retirement and node-runtime integration are separate slices. The crate remains an unwired workspace member. +S0.4 adds the §3.8 redistribution for reshare and refresh: dealer and recipient consume-self states over owner-closed rounds R1–R3, and `reshare_verify_transcript`. The output is a candidate for the successor certificate. Its byte layouts and deterministic rules are in the S0.4 redistribution appendix below. + +The dealer remains test-only. Durable storage/retirement and node-runtime integration are separate slices. The crate remains an unwired workspace member. ## S0.2 canonical byte appendix @@ -438,3 +440,65 @@ Agreement among members and the verifier relies on the §3.0 owner relay closing ### In-memory lifecycle `CeremonyHpkeSecretKey` is dropped when R1 has been processed. Polynomial coefficients are dropped when R4 has been produced. Received private pairs remain in the state until the member finishes after R5 or R6. States are consumed by each transition, so an error drops the state; scalars clear their storage on drop and states have redacted `Debug`. The caller's RNG state and any durable copies are outside this crate (§3.11). + +## S0.4 redistribution appendix + +This appendix records the byte layouts and deterministic rules that members and `reshare_verify_transcript` apply to §3.8 for ceremony kinds reshare (`2`) and refresh (`3`). Integers are unsigned big-endian; `lp`, `TH`, point and scalar encodings are as in §3.2 and the S0.2 appendix. Rounds are numbered R1 = 1 through R3 = 3. Dealers are the members of the pinned current certificate (cohort `2`); recipients are the authorized directory (cohort `1`). R1 and R3 count only cohort-`2` envelopes and R2 counts only cohort-`1` envelopes; an authenticated envelope under the other cohort is ignored for that round and attributes nothing to either role. A node in both cohorts signs each role separately. + +### Authorization and refresh + +Every entry point (`reshare_deal`, `reshare_start_receiver`, `reshare_verify_transcript`) first requires an `AuthorizedCeremony` of kind reshare or refresh (`InvalidCeremony` otherwise) whose current-certificate digest equals the supplied `VerifiedCertificate` (`ContextMismatch` otherwise). Refresh additionally requires t′ = t, n′ = n and a directory whose index→DID map equals the current certificate's members (`InvalidCeremony` otherwise). A dealer's `KeyShare` must carry the current certificate's key context (`ContextMismatch`). A recipient's ceremony key must derive the directory key at its index (`ContextMismatch`; unknown index `InvalidIndex`). These checks precede any randomness, opening or emitted message. + +### Initial link and transcript + +```text +H0 = TH("TC-THRESHOLD-V1-RESHARE-START", authorized_ceremony_digest:32) +``` + +`reshare_initial_transcript(c)` returns `H0` for both reshare and refresh; the authorized-ceremony digest binds the kind. The label is distinct from, and not a prefix of, every §3.3 tag and the DKG start label. Closed R1 names `H0`; R2 and R3 name the S0.2 transcript update of the preceding round. The redistribution transcript hash is the update after closed R3. Each transition checks the closed round's ceremony digest, round number and previous hash before reading any message (`InvalidRound`). + +`ReshareTranscript` holds exactly three raw round records. `reshare_verify_transcript` sums every closure and member byte length and signature length first and returns `LimitExceeded` above 4194304 bytes before any signature check; missing or additional records return `InvalidRound`. + +### Dealing + +Dealer i samples g_i of degree t′−1 with g_i(0) = s_i and coefficients 1..t′−1 drawn in order as 64 bytes reduced mod r, redrawn if zero; then one HPKE encapsulation per recipient in index order. It publishes B_ik = b_ik·g2 for k = 0..t′−1. + +```text +R1 deal = 01 || t′:u16 || [B_k:96] || n′:u16 || [(recipient:u16 || lp(sealed_dealing))] +R2 complaints = 01 || count:u16 || [dealer:u16] +R3 answers = 01 || count:u16 || [(recipient:u16 || value:32)] +``` + +R1 and R2 reuse the genesis R1/R2 layouts. HPKE `info` is `DEALING || authorized_ceremony_digest:32 || 01 || dealer:u16 || recipient:u16`; AAD is `TH(COMMITMENTS, t′:u16 || B_0 || … || B_(t′−1))`; the sealed plaintext is exactly the canonical scalar g_i(j):32. Counts are at most 16 and checked before allocation; index lists are strictly increasing; trailing bytes and other versions are rejected. A payload that fails decoding is *malformed*. Messages with byte-identical envelopes are one message; different envelopes from one sender in one round are *conflicting*. + +### Exclusion and dealer selection + +R1 *public checks* for dealer i: a single non-conflicting decodable R1, exactly t′ coefficients, B_i0 = pk_i of the current certificate, B_i,t′−1 ≠ identity, and recipient entries exactly 1..=n′. + +A complaint by recipient j against dealer i counts once per distinct j, only from a non-conflicting, decodable R2 whose dealer indices are all at most n. Malformed and conflicting R2 supply no complaints and have no effect on the sender's dealer role. Required answers for i are its counted complainers. + +An R3 is used only if it is non-conflicting, decodes and names recipients at most n′; otherwise it is malformed. For each dealer i, the first applicable reason excludes it: + +1. `MissingDeal`: no R1 from i. +2. `Equivocated { round: 1 }`: conflicting R1. +3. `MalformedDeal`: R1 fails the public checks. +4. `Equivocated { round: 3 }`: conflicting R3. +5. `Complaints`: at least t′ distinct counted complainers. +6. `InvalidAnswer`: any answer in i's decoded R3 fails g·g2 = Σ_k j^k·B_ik at its recipient j, required or not. +7. `MissingAnswer`: a required answer is absent (R3 missing, malformed, or omitting a complainer). + +A valid answer that was not required is ignored. A valid required answer replaces the complainer's value from i. D is the first t dealers, in order of each dealer's first R1 message in the owner's closed list, that are not excluded; fewer than t return `InsufficientQualified` and the current epoch stays in force. + +A dealer answers in R3 only when its R1 passes the public checks and carries its own commitment vector, and it has fewer than t′ counted complainers; with t′ or more it publishes an empty answer list, so it never publishes t′ points of g_i. Every dealer sends R3 and every recipient sends R2, possibly empty. + +### Output + +λ_i = λ_i^D over the old indices in D. B′_k = Σ_{i∈D} λ_i·B_ik; B′_0 must equal the current mpk and B′_(t′−1) and every pk′_j must be non-identity. Recipient j's candidate scalar is Σ_{i∈D} λ_i·g_i(j) with R3 replacements; a recipient without a valid value from some dealer in D returns `LocalShareUnavailable`, which only arises when its own complaint is absent from closed R2. `ReshareOutput` carries the public material, D in selection order, the exclusion reasons and the transcript hash. The candidate belongs to the successor certificate (`CertificateBody::successor`, epoch + 1) and is usable only after `KeyShare::bind`. + +### Scope + +Completion requires the §3.0 owner relay to close each round once and deliver every conforming member's message, and at least t old dealers that pass the rules above. Faulty old dealers within the secrecy budget can still leave fewer than t valid dealers; the ceremony then refuses with `InsufficientQualified`. Completion also does not hold if a selected dealer can read other dealers' R1 before its own R1 is closed and acts with a new member: it can then choose a dealing that passes every check but makes a new public share the identity, and every recipient and the verifier refuse with `IdentityPoint` without attributing a dealer; this affects completion only, and the current epoch stays in force. The protocol does not implement reliable broadcast. Redistribution does not erase anything: old shares remain valid for the old certificate until integration erases them (§3.11, §3.12); old and new shares carry different key contexts and are refused together by `verify_share` and `combine`. + +### In-memory lifecycle + +`ReshareDealer` holds g_i until `answer` consumes it. `ReshareReceiving` holds the ceremony recipient key until `complain` consumes it; `ReshareComplained` holds the opened values until `finish` consumes it. Scalars clear their storage on drop and every state has redacted `Debug`. The dealer's `KeyShare`, the caller's RNG state and any durable copies are outside this crate (§3.11). diff --git a/tinycloud-threshold/src/checkpoint.rs b/tinycloud-threshold/src/checkpoint.rs index f4b31e48..74b5499c 100644 --- a/tinycloud-threshold/src/checkpoint.rs +++ b/tinycloud-threshold/src/checkpoint.rs @@ -781,3 +781,162 @@ fn checkpoint_2of3_dkg() { assert!(keys.windows(2).all(|p| p[0] == p[1])); println!("5. PASS: each DKG share verified; pairs {{1,2}}, {{1,3}}, {{2,3}} derived the same key and unwrapped the data key"); } + +/// Same checkpoint name filter: a real DKG genesis redistributed to 3-of-4, +/// then a refused redistribution below t valid dealers. Details: TC-2221 +#[test] +fn checkpoint_2of3_reshare() { + use crate::ceremony::{CeremonyKind, Decode}; + use crate::reshare::{ReshareAnswers, ReshareComplaints}; + use crate::reshare_relay::{ + agreed, bad_dealing, fixture_on, install, public_shares_interpolate_to_mpk, refused, + run_with, unwraps_through_every_subset, wrapped, + }; + use std::collections::BTreeMap; + + let dkg = crate::dkg_relay::honest(2, 3, 0x73); + let output = crate::dkg_relay::agreed(&dkg, &[1, 2, 3]); + let crate::dkg_relay::Run { + fixture, outcomes, .. + } = dkg; + let anchor = fixture.signers.anchor(); + let body = CertificateBody::genesis( + &anchor, + &fixture.ceremony, + &output.public, + &output.transcript_hash, + ) + .unwrap(); + let genesis = verify_certificate( + &anchor, + &crate::cert::tests::sign(body, &fixture.signers), + &fixture.signers, + ) + .unwrap(); + let shares: BTreeMap = outcomes + .into_iter() + .map(|(i, o)| (i, KeyShare::bind(o.unwrap().0, &genesis).unwrap())) + .collect(); + let mpk = genesis.public_key_set().master_public_key().encode(); + let (data, ct) = wrapped(&genesis, "records/reshare", 0x73); + println!( + "1. PASS: signed 2-of-3 genesis DKG bound to its certificate; data key wrapped under mpk {}", + hex::encode(&mpk[..8]) + ); + + let f = fixture_on( + genesis, + shares.into_values(), + CeremonyKind::Reshare, + 3, + 4, + 0x74, + &[1, 2, 3, 4], + ); + let mut complaint = Vec::new(); + let run = run_with(f, |out| match out.round() { + 1 => { + let forged = bad_dealing(out, 2, 4); + out.replace(2, &forged); + } + 2 => { + complaint = ReshareComplaints::decode(&out.payload(4)).unwrap().dealers; + } + _ => {} + }); + assert_eq!(complaint, vec![MemberIndex::new(2).unwrap()]); + let answers = ReshareAnswers::decode(run.answers[&2].as_ref().unwrap()).unwrap(); + assert_eq!(answers.answers.len(), 1); + let reshared = agreed(&run, &[1, 2, 3, 4]); + assert_eq!(reshared.public.master_public_key().encode(), mpk); + assert!(reshared.excluded.is_empty()); + public_shares_interpolate_to_mpk(&reshared.public); + println!( + "2. PASS: signed redistribution 2-of-3 -> 3-of-4 closed R1-R3; dealer 2's bad value to recipient 4 drew one complaint and one valid answer; recipients 1-4 and reshare_verify_transcript agree on dealers {:?}, commitments, unchanged mpk and transcript hash {}", + reshared.dealers.iter().map(|i| i.get()).collect::>(), + hex::encode(reshared.transcript_hash.as_bytes()) + ); + + let next = install(run, &reshared); + assert_eq!(next.cert.key_context().epoch(), Epoch::new(1)); + assert_eq!( + next.cert.key_context().threshold(), + Threshold::new(3, 4).unwrap() + ); + println!("3. PASS: owner-signed successor certificate verified against the pinned genesis; all four candidates bound at epoch 1"); + assert_eq!( + unwraps_through_every_subset( + &next.cert, + &next.shares, + "records/reshare", + &data, + &ct, + 0x75 + ), + 4 + ); + println!("4. PASS: each of the four 3-member subsets derived the key and unwrapped the data key wrapped under the genesis mpk; two shares returned NotEnoughShares"); + + let old = &next.previous; + let mut rng = ChaCha20Rng::from_seed([0x76; 32]); + let tsk = TransportSecretKey::generate(&mut rng); + let old_context = old.current.key_context(); + let new_context = next.cert.key_context(); + let old_req = DecryptRequest::new( + &old_context, + identity(&old_context, "records/reshare"), + tsk.public_key(), + ) + .unwrap(); + let new_req = DecryptRequest::new( + &new_context, + identity(&new_context, "records/reshare"), + tsk.public_key(), + ) + .unwrap(); + let old_issued = issue_share(&old.shares[&1], &authorize(old_req), &mut rng).unwrap(); + let new_issued = issue_share(&next.shares[&1], &authorize(new_req.clone()), &mut rng).unwrap(); + assert_eq!( + verify_share(next.cert.public_key_set(), &new_req, &old_issued), + Err(ShareError::ContextMismatch) + ); + assert!(matches!( + combine( + next.cert.public_key_set(), + &new_req, + &[new_issued, old_issued] + ), + Err(CombineError::MixedContexts) + )); + println!("5. PASS: an old-epoch share returned ContextMismatch against the new key set and MixedContexts in a new combine; old shares stay valid for the old certificate until integration erases them"); + + let shares: Vec = next.shares.into_values().collect(); + let f = fixture_on( + next.cert, + shares, + CeremonyKind::Reshare, + 2, + 3, + 0x77, + &[1, 2, 3], + ); + let refused_run = run_with(f, |out| { + if out.round() == 1 { + out.remove(3); + out.remove(4); + } + }); + refused(&refused_run, &[1, 2, 3], Error::InsufficientQualified); + assert_eq!( + unwraps_through_every_subset( + &refused_run.fixture.current, + &refused_run.fixture.shares, + "records/reshare", + &data, + &ct, + 0x78 + ), + 4 + ); + println!("6. PASS: with dealers 3 and 4 withholding R1, only 2 < t = 3 valid dealers remained; every recipient and the verifier returned InsufficientQualified and the 3-of-4 epoch still derived and unwrapped"); +} diff --git a/tinycloud-threshold/src/lib.rs b/tinycloud-threshold/src/lib.rs index b08bc3ab..285905bb 100644 --- a/tinycloud-threshold/src/lib.rs +++ b/tinycloud-threshold/src/lib.rs @@ -30,6 +30,7 @@ pub mod ibe; pub mod identity; pub mod ids; mod poly; +pub mod reshare; pub mod seal; pub use activation::{ @@ -55,6 +56,11 @@ pub use fault::{verify_fault_proof, FaultProof, FaultVerdict, NotAFaultReason}; pub use ibe::*; pub use identity::IbeIdentity; pub use ids::*; +pub use reshare::{ + reshare_deal, reshare_initial_transcript, reshare_start_receiver, reshare_verify_transcript, + ExcludeReason, ReshareAnswers, ReshareComplained, ReshareComplaints, ReshareDeal, + ReshareDealer, ReshareMessage, ReshareOutput, ReshareReceiving, ReshareTranscript, +}; pub use seal::{CeremonyHpkePublicKey, CeremonyHpkeSecretKey, SealedDealing}; #[cfg(test)] @@ -62,4 +68,6 @@ mod checkpoint; #[cfg(test)] mod dkg_relay; #[cfg(test)] +mod reshare_relay; +#[cfg(test)] mod vectors; diff --git a/tinycloud-threshold/src/reshare.rs b/tinycloud-threshold/src/reshare.rs new file mode 100644 index 00000000..f21a04a4 --- /dev/null +++ b/tinycloud-threshold/src/reshare.rs @@ -0,0 +1,919 @@ +//! Authorized redistribution of the current master secret to a new cohort over +//! owner-closed rounds. Details: TC-2221 +//! +//! Current-certificate dealers deal (R1) and answer (R3); authorized recipients +//! complain (R2). Members and `reshare_verify_transcript` apply the same public +//! rules to the authenticated round records. Liveness assumes the trusted owner +//! relay delivers every conforming member's message. + +use core::fmt; +use std::collections::{btree_map::Entry, BTreeMap, BTreeSet}; + +use rand_core::CryptoRngCore; +use zeroize::Zeroizing; + +use crate::{ + ceremony::{ + ceremony_message_body, AuthorizedCeremony, CeremonyKind, ClosedRound, Decode, + DidSignatureVerifier, MemberCohort, RoundMessage, + }, + cert::{CandidateShare, PublicMaterial, VerifiedCertificate}, + curve::{Scalar, G2}, + derive::KeyShare, + dkg::{DkgComplaints, DkgDeal, DkgRoundRecord}, + dst, + encoding::{Decoder, Encoder}, + error::Error, + ids::{MemberIndex, Threshold, TranscriptHash}, + poly, + seal::{ + open_dealing, seal_dealing, CeremonyHpkePublicKey, CeremonyHpkeSecretKey, DealingContext, + }, +}; + +/// Initial transcript link tag for reshare and refresh; not part of the frozen +/// S0.1 domain table. Details: TC-2221 +pub(crate) const RESHARE_START: &[u8] = b"TC-THRESHOLD-V1-RESHARE-START"; +const VERSION: u8 = 1; +const MAX_ENTRIES: u16 = 16; +const TRANSCRIPT_CAP: usize = 4_194_304; +const SCALAR_LEN: usize = 32; + +const DEAL: u8 = 1; +const COMPLAIN: u8 = 2; +const ANSWER: u8 = 3; + +/// `TH(RESHARE_START, authorized_ceremony_digest)`, the link preceding closed R1. Details: TC-2221 +pub fn reshare_initial_transcript(c: &AuthorizedCeremony) -> TranscriptHash { + TranscriptHash::new(dst::tagged_hash(RESHARE_START, &c.digest())) +} + +// ------------------------------------------------------------------ payloads + +/// R1: Feldman coefficients B_ik in the genesis R1 layout, one sealed g_i(j) per recipient. Details: TC-2221 +pub type ReshareDeal = DkgDeal; + +/// R2: dealers whose dealing failed for the sending recipient, in the genesis R2 layout. Details: TC-2221 +pub type ReshareComplaints = DkgComplaints; + +/// R3: a dealer's published g_i(j) values for complaining recipients. Details: TC-2221 +#[derive(Debug)] +pub struct ReshareAnswers { + pub(crate) answers: Vec<(MemberIndex, Scalar)>, +} + +impl ReshareAnswers { + /// `01 || count:u16 || [(recipient:u16 || value:32)]`, strictly increasing. Details: TC-2221 + pub fn encode(&self) -> Vec { + let mut e = Encoder::new(); + e.u8(VERSION); + e.u16(self.answers.len() as u16); + for (index, value) in &self.answers { + e.u16(index.get()); + e.fixed(Zeroizing::new(value.to_bytes()).as_slice()); + } + e.finish() + } +} + +impl Decode for ReshareAnswers { + fn decode(bytes: &[u8]) -> Result { + let mut d = Decoder::new(bytes)?; + if d.u8()? != VERSION { + return Err(Error::InvalidEncoding); + } + let count = d.u16()?; + if count > MAX_ENTRIES { + return Err(Error::LimitExceeded); + } + let mut answers = Vec::with_capacity(usize::from(count)); + let mut previous: Option = None; + for _ in 0..count { + let index = MemberIndex::new(d.u16()?)?; + if previous.is_some_and(|p| index <= p) { + return Err(Error::InvalidEncoding); + } + previous = Some(index); + let value = Zeroizing::new(d.fixed::()?); + answers.push((index, Scalar::from_bytes(value.as_slice())?)); + } + d.finish()?; + Ok(Self { answers }) + } +} + +/// An unsigned member envelope for one round; the node signs `signing_preimage`. Details: TC-2221 +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReshareMessage { + round: u8, + body: Vec, +} + +impl ReshareMessage { + fn new( + c: &AuthorizedCeremony, + round: u8, + cohort: MemberCohort, + me: MemberIndex, + payload: &[u8], + ) -> Result { + Ok(Self { + round, + body: ceremony_message_body(c, round, cohort, me, payload)?, + }) + } + /// Round number of this envelope. Details: TC-2221 + pub fn round(&self) -> u8 { + self.round + } + /// Exact canonical member envelope bytes. Details: TC-2221 + pub fn body(&self) -> &[u8] { + &self.body + } + /// `TH(CEREMONY_MSG, body)`, signed with the member's node key. Details: TC-2221 + pub fn signing_preimage(&self) -> Vec { + dst::tagged_hash(dst::CEREMONY_MSG, &self.body).to_vec() + } +} + +// --------------------------------------------------------------- outputs + +/// Why an old dealer is not eligible for selection. Details: TC-2221 +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ExcludeReason { + /// No R1 from the dealer in closed R1. Details: TC-2221 + MissingDeal, + /// R1 failed to decode or failed a public check. Details: TC-2221 + MalformedDeal, + /// Conflicting authenticated content in R1 or R3. Details: TC-2221 + Equivocated { + /// Round of the conflicting messages. Details: TC-2221 + round: u8, + }, + /// At least t′ distinct recipients complained. Details: TC-2221 + Complaints, + /// A published answer fails the Feldman relation. Details: TC-2221 + InvalidAnswer, + /// A required answer is absent from closed R3. Details: TC-2221 + MissingAnswer, +} + +/// Public outcome shared by recipients and the transcript verifier. Details: TC-2221 +#[derive(Clone, Debug)] +pub struct ReshareOutput { + /// New Feldman polynomial, unchanged master key and new public shares. Details: TC-2221 + pub public: PublicMaterial, + /// Selected dealers D in closed-R1 order. Details: TC-2221 + pub dealers: Vec, + /// Excluded dealers with the first applicable reason. Details: TC-2221 + pub excluded: BTreeMap, + /// Transcript link after closed R3. Details: TC-2221 + pub transcript_hash: TranscriptHash, +} + +/// Raw signed rounds R1..R3. Details: TC-2221 +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReshareTranscript { + /// Round records in order. Details: TC-2221 + pub rounds: Vec, +} + +// ------------------------------------------------------------ public rules + +enum Sent<'a, M> { + One(&'a RoundMessage), + Conflicting, +} +impl Clone for Sent<'_, M> { + fn clone(&self) -> Self { + *self + } +} +impl Copy for Sent<'_, M> {} + +/// Group one cohort's authenticated messages by sender; identical envelope bytes +/// are one message. Also returns each sender's first-appearance order. +fn by_sender( + round: &ClosedRound, + cohort: MemberCohort, +) -> (BTreeMap>, Vec) { + let mut senders = BTreeMap::new(); + let mut order = Vec::new(); + for message in round.messages().iter().filter(|m| m.cohort() == cohort) { + match senders.entry(message.member()) { + Entry::Vacant(slot) => { + order.push(message.member()); + slot.insert(Sent::One(message)); + } + Entry::Occupied(mut slot) => { + if let Sent::One(first) = *slot.get() { + if first.statement().bytes != message.statement().bytes { + slot.insert(Sent::Conflicting); + } + } + } + } + } + (senders, order) +} + +fn decoded<'a, M>(sent: Option>) -> Option<&'a M> { + match sent { + Some(Sent::One(message)) => message.payload().as_ref().ok(), + _ => None, + } +} + +fn evaluate_points(points: &[G2], x: MemberIndex) -> G2 { + let x = Scalar::from_u64(u64::from(x.get())); + points + .iter() + .rev() + .fold(G2::identity(), |acc, point| acc.mul(&x).add(point)) +} + +fn feldman(value: &Scalar, commitments: &[G2], x: MemberIndex) -> bool { + G2::generator().mul(value) == evaluate_points(commitments, x) +} + +fn commitment_hash(points: &[G2]) -> [u8; 32] { + let mut e = Encoder::new(); + e.u16(points.len() as u16); + for point in points { + e.fixed(&point.encode()); + } + dst::tagged_hash(dst::COMMITMENTS, &e.finish()) +} + +fn copy(value: &Scalar) -> Scalar { + value.add(&Scalar::from_u64(0)) +} + +/// Public redistribution state; recipients, dealers and the verifier apply identical rules. +struct Public { + ceremony: AuthorizedCeremony, + old: Threshold, + mpk: G2, + /// Current public shares pk_i by old dealer index. + old_shares: BTreeMap, + previous: TranscriptHash, + /// First-appearance order of dealer-cohort R1 senders. + order: Vec, + /// Publicly valid R1 commitments of dealers not yet excluded. + commitments: BTreeMap>, + excluded: BTreeMap, + complaints: BTreeMap>, +} + +impl Public { + /// Authorized reshare/refresh of exactly the supplied current certificate. + fn new(c: &AuthorizedCeremony, current: &VerifiedCertificate) -> Result { + if !matches!(c.kind(), CeremonyKind::Reshare | CeremonyKind::Refresh) { + return Err(Error::InvalidCeremony); + } + if c.statement().current != Some(current.digest()) { + return Err(Error::ContextMismatch); + } + let old = current.key_context().threshold(); + if c.kind() == CeremonyKind::Refresh { + let same_directory = c.recipients().iter().map(|r| (r.index, &r.node)).eq(current + .body() + .members() + .iter() + .map(|m| (m.index, &m.node))); + if c.threshold() != old || !same_directory { + return Err(Error::InvalidCeremony); + } + } + let keys = current.public_key_set(); + Ok(Self { + ceremony: c.clone(), + old, + mpk: keys.mpk.0.clone(), + old_shares: keys.public_shares.iter().cloned().collect(), + previous: reshare_initial_transcript(c), + order: Vec::new(), + commitments: BTreeMap::new(), + excluded: BTreeMap::new(), + complaints: BTreeMap::new(), + }) + } + + fn threshold(&self) -> Threshold { + self.ceremony.threshold() + } + + /// The closed round must belong to this ceremony, phase and exact previous link. + fn bind(&mut self, round: &ClosedRound, expected: u8) -> Result<(), Error> { + if round.ceremony_digest() != self.ceremony.digest() + || round.round() != expected + || round.previous_hash() != self.previous + { + return Err(Error::InvalidRound); + } + self.previous = round.transcript_hash(); + Ok(()) + } + + fn publicly_valid(&self, deal: &ReshareDeal, pk: &G2) -> bool { + let threshold = self.threshold(); + deal.commitments.len() == usize::from(threshold.t()) + && deal.commitments.first() == Some(pk) + && deal + .commitments + .last() + .is_some_and(|top| !top.is_identity()) + && deal + .dealings + .iter() + .map(|(j, _)| j.get()) + .eq(1..=threshold.n()) + } + + /// Apply closed R1; return the publicly valid deals. + fn deals<'r>( + &mut self, + r1: &'r ClosedRound, + ) -> Result, Error> { + self.bind(r1, DEAL)?; + let (senders, order) = by_sender(r1, MemberCohort::Current); + self.order = order; + let mut valid = BTreeMap::new(); + for (i, pk) in &self.old_shares { + let reason = match senders.get(i).copied() { + None => Some(ExcludeReason::MissingDeal), + Some(Sent::Conflicting) => Some(ExcludeReason::Equivocated { round: DEAL }), + Some(Sent::One(message)) => match message.payload() { + Ok(deal) if self.publicly_valid(deal, pk) => { + valid.insert(*i, deal); + None + } + _ => Some(ExcludeReason::MalformedDeal), + }, + }; + if let Some(reason) = reason { + self.excluded.insert(*i, reason); + } + } + for (i, deal) in &valid { + self.commitments.insert(*i, deal.commitments.clone()); + } + Ok(valid) + } + + /// Apply closed R2: one complaint per distinct recipient and named dealer. + fn complaints(&mut self, r2: &ClosedRound) -> Result<(), Error> { + self.bind(r2, COMPLAIN)?; + let (senders, _) = by_sender(r2, MemberCohort::Recipients); + let n = self.old.n(); + for (j, sent) in senders { + let Some(list) = decoded(Some(sent)) else { + continue; + }; + if !list.dealers.iter().all(|i| i.get() <= n) { + continue; + } + for i in &list.dealers { + self.complaints.entry(*i).or_default().insert(j); + } + } + Ok(()) + } + + /// Apply closed R3 and fix the eligible dealers; return valid required + /// answers addressed to `recipient`. + fn answers( + &mut self, + r3: &ClosedRound, + recipient: Option, + ) -> Result, Error> { + self.bind(r3, ANSWER)?; + let (senders, _) = by_sender(r3, MemberCohort::Current); + let t = usize::from(self.threshold().t()); + let n = self.threshold().n(); + let empty = BTreeSet::new(); + let mut replacements = BTreeMap::new(); + let mut removed = BTreeMap::new(); + for (i, commitments) in &self.commitments { + let required = self.complaints.get(i).unwrap_or(&empty); + let sent = senders.get(i).copied(); + let answers = decoded(sent).filter(|a| a.answers.iter().all(|(j, _)| j.get() <= n)); + let reason = if matches!(sent, Some(Sent::Conflicting)) { + Some(ExcludeReason::Equivocated { round: ANSWER }) + } else if required.len() >= t { + Some(ExcludeReason::Complaints) + } else if answers.is_some_and(|a| { + a.answers + .iter() + .any(|(j, value)| !feldman(value, commitments, *j)) + }) { + Some(ExcludeReason::InvalidAnswer) + } else if !required + .iter() + .all(|j| answers.is_some_and(|a| a.answers.iter().any(|(k, _)| k == j))) + { + Some(ExcludeReason::MissingAnswer) + } else { + None + }; + match reason { + Some(reason) => { + removed.insert(*i, reason); + } + None => { + if let (Some(me), Some(answers)) = (recipient, answers) { + if required.contains(&me) { + if let Some((_, value)) = answers.answers.iter().find(|(j, _)| *j == me) + { + replacements.insert(*i, copy(value)); + } + } + } + } + } + } + for (i, reason) in removed { + self.commitments.remove(&i); + self.excluded.insert(i, reason); + } + Ok(replacements) + } + + /// D: the first t eligible dealers in closed-R1 order. + fn select(&self) -> Result, Error> { + let t = usize::from(self.old.t()); + let dealers: Vec = self + .order + .iter() + .copied() + .filter(|i| self.commitments.contains_key(i)) + .take(t) + .collect(); + if dealers.len() < t { + return Err(Error::InsufficientQualified); + } + Ok(dealers) + } + + fn output(&self, dealers: Vec) -> Result { + let mut sums: Vec = (0..self.threshold().t()).map(|_| G2::identity()).collect(); + for i in &dealers { + let lambda = poly::lagrange(*i, dealers.iter().copied())?; + let commitments = self.commitments.get(i).ok_or(Error::InvalidRound)?; + for (sum, coefficient) in sums.iter_mut().zip(commitments) { + *sum = sum.add(&coefficient.mul(&lambda)); + } + } + if sums.first() != Some(&self.mpk) { + return Err(Error::InvalidPoint); + } + Ok(ReshareOutput { + public: PublicMaterial::from_commitments(self.threshold(), sums)?, + dealers, + excluded: self.excluded.clone(), + transcript_hash: self.previous, + }) + } +} + +// ------------------------------------------------------------ member states + +/// Holds this dealer's polynomial g_i until it answers closed R2. Details: TC-2221 +pub struct ReshareDealer { + public: Public, + me: MemberIndex, + coefficients: Vec, + commitments: Vec, +} + +/// Holds this recipient's ceremony key until closed R1. Details: TC-2221 +pub struct ReshareReceiving { + public: Public, + me: MemberIndex, + hpke: CeremonyHpkeSecretKey, +} + +/// Holds this recipient's opened values until closed R2 and R3. Details: TC-2221 +pub struct ReshareComplained { + public: Public, + me: MemberIndex, + values: BTreeMap, +} + +macro_rules! redacted { + ($($name:ident),*) => {$( + impl fmt::Debug for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(concat!(stringify!($name), "([REDACTED])")) + } + } + )*}; +} + +redacted!(ReshareDealer, ReshareReceiving, ReshareComplained); + +/// Validate the authorized context and the dealer's current share, then deal R1. Details: TC-2221 +pub fn reshare_deal( + c: &AuthorizedCeremony, + current: &VerifiedCertificate, + share: &KeyShare, + rng: &mut impl CryptoRngCore, +) -> Result<(ReshareDealer, ReshareMessage), Error> { + let public = Public::new(c, current)?; + if share.context() != ¤t.key_context() { + return Err(Error::ContextMismatch); + } + let me = share.index(); + if !public.old_shares.contains_key(&me) { + return Err(Error::InvalidIndex); + } + let t = usize::from(c.threshold().t()); + let mut coefficients = Vec::with_capacity(t); + coefficients.push(copy(&share.scalar)); + for _ in 1..t { + coefficients.push(Scalar::random(&mut *rng, true)); + } + let commitments: Vec = coefficients + .iter() + .map(|b| G2::generator().mul(b)) + .collect(); + let hash = commitment_hash(&commitments); + let mut dealings = Vec::with_capacity(c.recipients().len()); + for recipient in c.recipients() { + let value = poly::evaluate( + &coefficients, + &Scalar::from_u64(u64::from(recipient.index.get())), + ); + let plaintext = Zeroizing::new(value.to_bytes()); + let context = DealingContext { + ceremony_digest: c.digest(), + round: DEAL, + sender: me, + recipient: recipient.index, + commitment_hash: hash, + }; + let key = CeremonyHpkePublicKey::decode(&recipient.hpke_public_key)?; + dealings.push(( + recipient.index, + seal_dealing(&key, &context, plaintext.as_slice(), &mut *rng)?, + )); + } + let deal = ReshareDeal { + commitments: commitments.clone(), + dealings, + }; + let message = ReshareMessage::new(c, DEAL, MemberCohort::Current, me, &deal.encode())?; + Ok(( + ReshareDealer { + public, + me, + coefficients, + commitments, + }, + message, + )) +} + +impl ReshareDealer { + /// Publish g_i(j) for every counted complaint against this dealer (R3), + /// unless t′ or more recipients complained. Details: TC-2221 + pub fn answer( + mut self, + r1: &ClosedRound, + r2: &ClosedRound, + ) -> Result { + self.public.deals(r1)?; + self.public.complaints(r2)?; + let t = usize::from(self.public.threshold().t()); + let answers = match ( + self.public.commitments.get(&self.me), + self.public.complaints.get(&self.me), + ) { + (Some(published), Some(required)) + if *published == self.commitments && required.len() < t => + { + required + .iter() + .map(|j| { + let x = Scalar::from_u64(u64::from(j.get())); + (*j, poly::evaluate(&self.coefficients, &x)) + }) + .collect() + } + _ => Vec::new(), + }; + ReshareMessage::new( + &self.public.ceremony, + ANSWER, + MemberCohort::Current, + self.me, + &ReshareAnswers { answers }.encode(), + ) + } +} + +/// Validate the authorized context and the local recipient key. Details: TC-2221 +pub fn reshare_start_receiver( + c: &AuthorizedCeremony, + current: &VerifiedCertificate, + me: MemberIndex, + hpke_sk: CeremonyHpkeSecretKey, +) -> Result { + let public = Public::new(c, current)?; + let own = c + .recipients() + .iter() + .find(|r| r.index == me) + .ok_or(Error::InvalidIndex)?; + if hpke_sk.public_key()?.encode() != own.hpke_public_key { + return Err(Error::ContextMismatch); + } + Ok(ReshareReceiving { + public, + me, + hpke: hpke_sk, + }) +} + +impl ReshareReceiving { + fn open(&self, dealer: MemberIndex, deal: &ReshareDeal) -> Result { + let (_, sealed) = deal + .dealings + .iter() + .find(|(j, _)| *j == self.me) + .ok_or(Error::InvalidSealedDealing)?; + let context = DealingContext { + ceremony_digest: self.public.ceremony.digest(), + round: DEAL, + sender: dealer, + recipient: self.me, + commitment_hash: commitment_hash(&deal.commitments), + }; + let plaintext = open_dealing(&self.hpke, &context, sealed)?; + if plaintext.len() != SCALAR_LEN { + return Err(Error::InvalidSealedDealing); + } + Scalar::from_bytes(plaintext.as_slice()) + } + + /// Open and check every publicly valid dealing; complain about failures (R2). Details: TC-2221 + pub fn complain( + mut self, + r1: &ClosedRound, + ) -> Result<(ReshareComplained, ReshareMessage), Error> { + let deals = self.public.deals(r1)?; + let mut values = BTreeMap::new(); + let mut dealers = Vec::new(); + for (i, deal) in deals { + match self.open(i, deal) { + Ok(value) if feldman(&value, &deal.commitments, self.me) => { + values.insert(i, value); + } + _ => dealers.push(i), + } + } + let message = ReshareMessage::new( + &self.public.ceremony, + COMPLAIN, + MemberCohort::Recipients, + self.me, + &ReshareComplaints { dealers }.encode(), + )?; + let Self { public, me, hpke } = self; + drop(hpke); + Ok((ReshareComplained { public, me, values }, message)) + } +} + +impl ReshareComplained { + /// Apply closed R2 and R3, select D and return this recipient's candidate. Details: TC-2221 + pub fn finish( + mut self, + r2: &ClosedRound, + r3: &ClosedRound, + ) -> Result<(CandidateShare, ReshareOutput), Error> { + self.public.complaints(r2)?; + let replacements = self.public.answers(r3, Some(self.me))?; + self.values.extend(replacements); + let dealers = self.public.select()?; + let mut scalar = Scalar::from_u64(0); + for i in &dealers { + let value = self.values.get(i).ok_or(Error::LocalShareUnavailable)?; + let lambda = poly::lagrange(*i, dealers.iter().copied())?; + scalar = scalar.add(&lambda.mul(value)); + } + let output = self.public.output(dealers)?; + Ok(( + CandidateShare { + transcript: output.transcript_hash, + index: self.me, + scalar, + }, + output, + )) + } +} + +/// Recompute the outcome from raw signed rounds; no expected output is trusted. Details: TC-2221 +pub fn reshare_verify_transcript( + c: &AuthorizedCeremony, + current: &VerifiedCertificate, + t: &ReshareTranscript, + v: &impl DidSignatureVerifier, +) -> Result { + let mut size = 0usize; + for record in &t.rounds { + let statements = core::iter::once(&record.closure).chain(record.messages.iter()); + for statement in statements { + size = size + .checked_add(statement.bytes.len()) + .and_then(|s| s.checked_add(statement.signature.len())) + .ok_or(Error::LimitExceeded)?; + if size > TRANSCRIPT_CAP { + return Err(Error::LimitExceeded); + } + } + } + let mut public = Public::new(c, current)?; + let mut records = t.rounds.iter(); + let r1 = verified_round::(c, records.next(), DEAL, public.previous, v)?; + public.deals(&r1)?; + let r2 = verified_round::(c, records.next(), COMPLAIN, public.previous, v)?; + public.complaints(&r2)?; + let r3 = verified_round::(c, records.next(), ANSWER, public.previous, v)?; + public.answers(&r3, None)?; + if records.next().is_some() { + return Err(Error::InvalidRound); + } + let dealers = public.select()?; + public.output(dealers) +} + +fn verified_round( + c: &AuthorizedCeremony, + record: Option<&DkgRoundRecord>, + round: u8, + previous: TranscriptHash, + v: &impl DidSignatureVerifier, +) -> Result, Error> { + let record = record.ok_or(Error::InvalidRound)?; + ClosedRound::verify(c, round, &previous, &record.closure, &record.messages, v) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + ceremony::{tests::Signers, CeremonyKind, Decode}, + curve::{Scalar, G2}, + dkg::{dkg_initial_transcript, DKG_START}, + dst, + error::Error, + ids::*, + }; + use proptest::prelude::*; + use sha2::{Digest, Sha256}; + + fn idx(i: u16) -> MemberIndex { + MemberIndex::new(i).unwrap() + } + + const R: [u8; 32] = [ + 0x73, 0xed, 0xa7, 0x53, 0x29, 0x9d, 0x7d, 0x48, 0x33, 0x39, 0xd8, 0x08, 0x09, 0xa1, 0xd8, + 0x05, 0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x01, + ]; + + fn answers() -> ReshareAnswers { + ReshareAnswers { + answers: vec![(idx(1), Scalar::from_u64(7)), (idx(3), Scalar::from_u64(9))], + } + } + + fn rejects_every_truncation_and_trailing_byte(bytes: &[u8]) { + for cut in 0..bytes.len() { + assert!(M::decode(&bytes[..cut]).is_err(), "truncated at {cut}"); + } + let mut trailing = bytes.to_vec(); + trailing.push(0); + assert!(M::decode(&trailing).is_err()); + let mut version = bytes.to_vec(); + version[0] = 2; + assert!(matches!(M::decode(&version), Err(Error::InvalidEncoding))); + } + + #[test] + fn reshare_start_label_is_distinct_from_and_not_a_prefix_of_every_frozen_tag() { + assert_eq!(RESHARE_START, b"TC-THRESHOLD-V1-RESHARE-START"); + assert_eq!(dst::DOMAINS.len(), 19); + for tag in dst::DOMAINS.iter().copied().chain([DKG_START]) { + assert_ne!(tag, RESHARE_START); + assert!(!tag.starts_with(RESHARE_START)); + assert!(!RESHARE_START.starts_with(tag)); + } + } + + #[test] + fn initial_transcript_hashes_the_exact_ceremony_digest_for_reshare_and_refresh() { + let signers = Signers::new(); + let (current, _) = crate::cert::tests::genesis(&signers); + let mut links = Vec::new(); + for kind in [CeremonyKind::Reshare, CeremonyKind::Refresh] { + let (_, _, ceremony) = crate::cert::tests::successor( + &signers, + ¤t, + kind, + current.key_context().threshold(), + 11, + ); + let mut hash = Sha256::new(); + hash.update(29u32.to_be_bytes()); + hash.update(b"TC-THRESHOLD-V1-RESHARE-START"); + hash.update(ceremony.digest()); + let expected: [u8; 32] = hash.finalize().into(); + let link = reshare_initial_transcript(&ceremony); + assert_eq!(link.as_bytes(), &expected); + assert_ne!(link, dkg_initial_transcript(&ceremony)); + assert_ne!(expected, ceremony.digest()); + links.push(link); + } + // The ceremony digest binds the kind. + assert_ne!(links[0], links[1]); + } + + #[test] + fn answer_payload_has_the_exact_layout_and_rejects_noncanonical_bytes() { + let bytes = answers().encode(); + let mut expected = vec![1, 0, 2]; + for (index, value) in [(1u16, 7u8), (3, 9)] { + expected.extend_from_slice(&index.to_be_bytes()); + let mut scalar = [0; 32]; + scalar[31] = value; + expected.extend_from_slice(&scalar); + } + assert_eq!(bytes, expected); + assert_eq!(ReshareAnswers::decode(&bytes).unwrap().encode(), bytes); + assert_eq!(ReshareAnswers::decode(&[1, 0, 0]).unwrap().answers.len(), 0); + rejects_every_truncation_and_trailing_byte::(&bytes); + + let mut noncanonical = expected.clone(); + noncanonical[5..37].copy_from_slice(&R); + assert!(matches!( + ReshareAnswers::decode(&noncanonical), + Err(Error::InvalidScalar) + )); + let mut unsorted = expected.clone(); + unsorted[3..5].copy_from_slice(&[0, 3]); + unsorted[37..39].copy_from_slice(&[0, 1]); + assert!(ReshareAnswers::decode(&unsorted).is_err()); + let mut duplicate = expected.clone(); + duplicate[37..39].copy_from_slice(&[0, 1]); + assert!(ReshareAnswers::decode(&duplicate).is_err()); + let mut zero = expected; + zero[3..5].copy_from_slice(&[0, 0]); + assert!(ReshareAnswers::decode(&zero).is_err()); + assert!(matches!( + ReshareAnswers::decode(&[1, 0, 17]), + Err(Error::LimitExceeded) + )); + } + + #[test] + fn redistribution_rounds_reuse_the_genesis_deal_and_complaint_layouts() { + // R1 carries Feldman coefficients in the genesis R1 layout; R2 is the + // genesis complaint list. Individual coefficients may be the identity. + let deal = ReshareDeal { + commitments: vec![G2::generator(), G2::identity()], + dealings: Vec::new(), + }; + let bytes = deal.encode(); + assert_eq!(&bytes[..3], &[1, 0, 2]); + assert_eq!(&bytes[3 + 96..3 + 192], &G2::identity().encode()); + assert_eq!(&bytes[3 + 192..], &[0, 0]); + assert_eq!(ReshareDeal::decode(&bytes).unwrap().encode(), bytes); + let complaints = ReshareComplaints { + dealers: vec![idx(2)], + }; + assert_eq!(complaints.encode(), vec![1, 0, 1, 0, 2]); + } + + proptest! { + #[test] + fn random_answer_bytes_reject_or_reencode_canonically( + bytes in proptest::collection::vec(any::(), 0..800) + ) { + if let Ok(v) = ReshareAnswers::decode(&bytes) { prop_assert_eq!(v.encode(), bytes.clone()); } + } + + #[test] + fn structured_answer_mutations_reject_or_reencode_canonically( + offset in 0usize..256, + mask in 1u8..=255 + ) { + let mut mutated = answers().encode(); + let n = mutated.len(); + mutated[offset % n] ^= mask; + if let Ok(v) = ReshareAnswers::decode(&mutated) { prop_assert_eq!(v.encode(), mutated.clone()); } + } + } +} diff --git a/tinycloud-threshold/src/reshare_relay.rs b/tinycloud-threshold/src/reshare_relay.rs new file mode 100644 index 00000000..50788cc3 --- /dev/null +++ b/tinycloud-threshold/src/reshare_relay.rs @@ -0,0 +1,2435 @@ +//! Test-only owner relay driving real redistribution states over signed closed +//! rounds. Details: TC-2221 +//! +//! Completion scenarios deliver every honest message and keep at least t valid +//! old dealers. Omissions and substitutions below are explicit failure cases +//! chosen by each test, not a broadcast model. Nothing here claims that old +//! shares stop working: old and new key contexts stay separate, and old shares +//! keep working with the old certificate until integration erases them. + +use std::collections::BTreeMap; + +use rand_chacha::ChaCha20Rng; +use rand_core::{CryptoRng, RngCore, SeedableRng}; +use serde_json::{json, Value}; + +use crate::{ + ceremony::{ + ceremony_message_body, ceremony_message_preimage, round_closure_body, + round_closure_preimage, tests::Signers, AuthorizedCeremony, CeremonyKind, + CeremonyRecipient, CeremonyStatement, ClosedRound, Decode, DidSignatureVerifier, + MemberCohort, SignedStatement, + }, + cert::{ + verify_certificate, verify_successor, CandidateShare, CertificateBody, PublicMaterial, + SignedCertificate, VerifiedCertificate, + }, + checkpoint::{authorize, identity}, + curve::{Scalar, G2}, + derive::{ + combine, decrypt_derived_key, issue_share, verify_share, DecryptRequest, EncryptedKeyShare, + KeyShare, TransportSecretKey, + }, + dkg::{dkg_initial_transcript, DkgRoundRecord}, + dkg_relay::commitment_hash, + dst, + error::{CombineError, Error, ShareError, SignatureError}, + ibe::{unwrap_key, wrap_key, DataKey, IbeCiphertext}, + ids::*, + reshare::*, + seal::{ + open_dealing, seal_dealing, CeremonyHpkePublicKey, CeremonyHpkeSecretKey, DealingContext, + }, +}; + +use CeremonyKind::{Refresh, Reshare}; +use MemberCohort::{Current, Recipients}; + +pub(crate) fn idx(i: u16) -> MemberIndex { + MemberIndex::new(i).unwrap() +} + +// ------------------------------------------------------------------ fixture + +/// The current certificate, its bound shares, and an owner-authorized successor +/// ceremony whose directory holds real ceremony recipient keys. +pub(crate) struct Fixture { + pub(crate) signers: Signers, + pub(crate) current: VerifiedCertificate, + pub(crate) shares: BTreeMap, + pub(crate) statement: SignedStatement, + pub(crate) ceremony: AuthorizedCeremony, + pub(crate) seed: u8, +} + +/// Successor statement for `current`: new threshold, recipient DIDs in index +/// order, and recipient keys regenerated from `seed`. +pub(crate) fn statement_body( + current: &VerifiedCertificate, + kind: CeremonyKind, + t: u16, + n: u16, + seed: u8, + dids: &[u8], +) -> CeremonyStatement { + assert_eq!(dids.len(), usize::from(n)); + let mut rng = ChaCha20Rng::from_seed([seed; 32]); + let context = current.key_context(); + CeremonyStatement { + kind, + id: CeremonyId::new([seed; 32]), + network: current.anchor().network.clone(), + owner: current.anchor().expected_owner.clone(), + current: Some(current.digest()), + threshold: Threshold::new(t, n).unwrap(), + rekey: context.rekey(), + epoch: context.epoch().checked_next().unwrap(), + recipients: dids + .iter() + .enumerate() + .map(|(k, did)| CeremonyRecipient { + index: idx(k as u16 + 1), + node: Signers::did(*did), + hpke_public_key: CeremonyHpkeSecretKey::generate(&mut rng).1.encode(), + }) + .collect(), + } +} + +pub(crate) fn fixture_on( + current: VerifiedCertificate, + shares: impl IntoIterator, + kind: CeremonyKind, + t: u16, + n: u16, + seed: u8, + dids: &[u8], +) -> Fixture { + let signers = Signers::new(); + let body = statement_body(¤t, kind, t, n, seed, dids); + let statement = signers.statement(&body); + let ceremony = AuthorizedCeremony::verify_for(¤t, &statement, &signers).unwrap(); + let shares = shares.into_iter().map(|s| (s.index().get(), s)).collect(); + Fixture { + signers, + current, + shares, + statement, + ceremony, + seed, + } +} + +/// A test-dealer genesis certificate with every share bound to it. +pub(crate) fn dealer_genesis(t: u16, n: u16) -> (VerifiedCertificate, Vec) { + let signers = Signers::new(); + let threshold = Threshold::new(t, n).unwrap(); + let (public, raw) = crate::cert::tests::material(11, threshold); + let ceremony = AuthorizedCeremony::verify_genesis( + &signers.anchor(), + &signers.statement(&signers.body(CeremonyKind::Genesis, threshold)), + &signers, + ) + .unwrap(); + let transcript = TranscriptHash::new([51; 32]); + let body = + CertificateBody::genesis(&signers.anchor(), &ceremony, &public, &transcript).unwrap(); + let cert = verify_certificate( + &signers.anchor(), + &crate::cert::tests::sign(body, &signers), + &signers, + ) + .unwrap(); + let shares = raw + .into_iter() + .map(|mut raw| { + let scalar = core::mem::replace(&mut raw.scalar, Scalar::from_u64(0)); + KeyShare::bind(CandidateShare::new(transcript, raw.index, scalar), &cert).unwrap() + }) + .collect(); + (cert, shares) +} + +/// Redistribution from the 2-of-3 test-dealer genesis to recipients did 1..=n. +pub(crate) fn fixture(kind: CeremonyKind, t: u16, n: u16, seed: u8) -> Fixture { + let (current, shares) = dealer_genesis(2, 3); + let dids: Vec = (1..=n as u8).collect(); + fixture_on(current, shares, kind, t, n, seed, &dids) +} + +/// A verified pinned successor of `f.current` (same master key) and its shares. +pub(crate) fn newer_certificate(f: &Fixture) -> (VerifiedCertificate, Vec) { + let (signed, raw, _) = crate::cert::tests::successor( + &f.signers, + &f.current, + Reshare, + Threshold::new(2, 3).unwrap(), + // Seed 11 matches the test-dealer genesis, so the successor keeps its mpk. + 11, + ); + let newer = verify_successor(&f.current, &signed, &f.signers).unwrap(); + let transcript = newer.body().transcript_hash(); + let shares = raw + .into_iter() + .map(|mut raw| { + let scalar = core::mem::replace(&mut raw.scalar, Scalar::from_u64(0)); + KeyShare::bind(CandidateShare::new(transcript, raw.index, scalar), &newer).unwrap() + }) + .collect(); + (newer, shares) +} + +impl Fixture { + /// Regenerate new member j's ceremony recipient key from the fixture seed. + pub(crate) fn secret(&self, j: u16) -> CeremonyHpkeSecretKey { + let mut rng = ChaCha20Rng::from_seed([self.seed; 32]); + let mut key = None; + for _ in 1..=j { + key = Some(CeremonyHpkeSecretKey::generate(&mut rng).0); + } + key.unwrap() + } + + pub(crate) fn member_rng(&self, i: u16) -> ChaCha20Rng { + let mut seed = [self.seed; 32]; + seed[0] = i as u8; + seed[1] = 0xd2; + ChaCha20Rng::from_seed(seed) + } + + /// The node DID for a coordinate in the named cohort. + pub(crate) fn did(&self, cohort: MemberCohort, member: u16) -> Did { + match cohort { + Current => self.current.member(idx(member)).unwrap().node.clone(), + Recipients => self.ceremony.recipients()[usize::from(member - 1)] + .node + .clone(), + } + } + + pub(crate) fn sign_bytes( + &self, + cohort: MemberCohort, + member: u16, + bytes: Vec, + ) -> SignedStatement { + let preimage = ceremony_message_preimage(&bytes).unwrap(); + SignedStatement { + signature: self.signers.sign(&self.did(cohort, member), &preimage), + bytes, + } + } + + pub(crate) fn sign( + &self, + cohort: MemberCohort, + member: u16, + message: &ReshareMessage, + ) -> SignedStatement { + assert_eq!( + message.signing_preimage(), + ceremony_message_preimage(message.body()).unwrap() + ); + self.sign_bytes(cohort, member, message.body().to_vec()) + } + + pub(crate) fn n(&self) -> u16 { + self.ceremony.threshold().n() + } + + pub(crate) fn old_t(&self) -> u16 { + self.current.key_context().threshold().t() + } +} + +// -------------------------------------------------------------------- relay + +/// The signed cohort byte and coordinate of a member envelope. +pub(crate) fn sender(statement: &SignedStatement) -> (u8, u16) { + ( + statement.bytes[34], + u16::from_be_bytes([statement.bytes[35], statement.bytes[36]]), + ) +} + +/// Messages the owner is about to close for one round. R1 and R3 default to +/// the current-certificate dealer cohort, R2 to the recipient cohort. +pub(crate) struct Outbox<'a> { + round: u8, + fixture: &'a Fixture, + rng: &'a mut ChaCha20Rng, + pub(crate) messages: Vec, +} + +impl Outbox<'_> { + pub(crate) fn round(&self) -> u8 { + self.round + } + pub(crate) fn fixture(&self) -> &Fixture { + self.fixture + } + pub(crate) fn ceremony(&self) -> &AuthorizedCeremony { + &self.fixture.ceremony + } + pub(crate) fn rng(&mut self) -> &mut ChaCha20Rng { + &mut *self.rng + } + fn cohort(&self) -> MemberCohort { + if self.round == 2 { + Recipients + } else { + Current + } + } + fn key(&self, member: u16) -> (u8, u16) { + (self.cohort() as u8, member) + } + /// A member-signed envelope for this round under an explicit cohort. + pub(crate) fn signed_as( + &self, + cohort: MemberCohort, + member: u16, + payload: &[u8], + ) -> SignedStatement { + let bytes = ceremony_message_body( + &self.fixture.ceremony, + self.round, + cohort, + idx(member), + payload, + ) + .unwrap(); + self.fixture.sign_bytes(cohort, member, bytes) + } + pub(crate) fn has(&self, member: u16) -> bool { + let key = self.key(member); + self.messages.iter().any(|m| sender(m) == key) + } + pub(crate) fn payload(&self, member: u16) -> Vec { + let key = self.key(member); + self.messages + .iter() + .find(|m| sender(m) == key) + .expect("member message in this round") + .bytes[41..] + .to_vec() + } + pub(crate) fn remove(&mut self, member: u16) { + let key = self.key(member); + self.messages.retain(|m| sender(m) != key); + } + pub(crate) fn replace(&mut self, member: u16, payload: &[u8]) { + let signed = self.signed_as(self.cohort(), member, payload); + let key = self.key(member); + let position = self.messages.iter().position(|m| sender(m) == key); + self.remove(member); + match position { + Some(p) => self.messages.insert(p, signed), + None => self.messages.push(signed), + } + } + pub(crate) fn push(&mut self, member: u16, payload: &[u8]) { + let signed = self.signed_as(self.cohort(), member, payload); + self.messages.push(signed); + } + pub(crate) fn push_as(&mut self, cohort: MemberCohort, member: u16, payload: &[u8]) { + let signed = self.signed_as(cohort, member, payload); + self.messages.push(signed); + } + pub(crate) fn duplicate(&mut self, member: u16) { + let key = self.key(member); + let copy = self + .messages + .iter() + .find(|m| sender(m) == key) + .cloned() + .expect("member message in this round"); + self.messages.push(copy); + } + /// Move the listed members' first messages to the front, in this order. + pub(crate) fn order(&mut self, order: &[u16]) { + let mut front = Vec::new(); + for member in order { + let key = self.key(*member); + if let Some(p) = self.messages.iter().position(|m| sender(m) == key) { + front.push(self.messages.remove(p)); + } + } + front.append(&mut self.messages); + self.messages = front; + } +} + +pub(crate) type Outcome = Result<(CandidateShare, ReshareOutput), Error>; + +pub(crate) struct Run { + pub(crate) fixture: Fixture, + /// Receiver outcomes by new coordinate. + pub(crate) outcomes: BTreeMap, + /// Each dealer's R3 payload as produced by its own state. + pub(crate) answers: BTreeMap, Error>>, + pub(crate) transcript: ReshareTranscript, +} + +impl Run { + pub(crate) fn verify(&self) -> Result { + reshare_verify_transcript( + &self.fixture.ceremony, + &self.fixture.current, + &self.transcript, + &self.fixture.signers, + ) + } +} + +struct Relay<'a, V> { + fixture: &'a Fixture, + verifier: &'a V, + previous: TranscriptHash, + transcript: ReshareTranscript, + rng: ChaCha20Rng, +} + +impl Relay<'_, V> { + fn close( + &mut self, + round: u8, + messages: Vec, + tamper: &mut impl FnMut(&mut Outbox), + ) -> ClosedRound { + let mut outbox = Outbox { + round, + fixture: self.fixture, + rng: &mut self.rng, + messages, + }; + tamper(&mut outbox); + let messages = outbox.messages; + let c = &self.fixture.ceremony; + let bytes = round_closure_body(c, round, &self.previous, &messages).unwrap(); + let closure = SignedStatement { + signature: self.fixture.signers.sign( + &self.fixture.signers.anchor().expected_owner, + &round_closure_preimage(&bytes).unwrap(), + ), + bytes, + }; + let closed = + ClosedRound::::verify(c, round, &self.previous, &closure, &messages, self.verifier) + .unwrap(); + self.previous = closed.transcript_hash(); + self.transcript + .rounds + .push(DkgRoundRecord { closure, messages }); + closed + } +} + +/// Run every old dealer's and new recipient's real state through a signing +/// owner relay. +pub(crate) fn drive( + fixture: Fixture, + verifier: &V, + mut tamper: impl FnMut(&mut Outbox), +) -> Run { + let mut relay = Relay { + fixture: &fixture, + verifier, + previous: reshare_initial_transcript(&fixture.ceremony), + transcript: ReshareTranscript { rounds: Vec::new() }, + rng: ChaCha20Rng::from_seed([fixture.seed ^ 0xaa; 32]), + }; + let mut dealers = BTreeMap::new(); + let mut r1 = Vec::new(); + for (i, share) in &fixture.shares { + let mut rng = fixture.member_rng(*i); + let (state, message) = + reshare_deal(&fixture.ceremony, &fixture.current, share, &mut rng).unwrap(); + assert_eq!(message.round(), 1); + r1.push(fixture.sign(Current, *i, &message)); + dealers.insert(*i, state); + } + let closed1 = relay.close::(1, r1, &mut tamper); + let mut receivers = BTreeMap::new(); + let mut r2 = Vec::new(); + for j in 1..=fixture.n() { + let started = reshare_start_receiver( + &fixture.ceremony, + &fixture.current, + idx(j), + fixture.secret(j), + ); + match started.and_then(|s| s.complain(&closed1)) { + Ok((state, message)) => { + assert_eq!(message.round(), 2); + r2.push(fixture.sign(Recipients, j, &message)); + receivers.insert(j, Ok(state)); + } + Err(e) => { + receivers.insert(j, Err(e)); + } + } + } + let closed2 = relay.close::(2, r2, &mut tamper); + let mut answers = BTreeMap::new(); + let mut r3 = Vec::new(); + for (i, state) in dealers { + match state.answer(&closed1, &closed2) { + Ok(message) => { + assert_eq!(message.round(), 3); + answers.insert(i, Ok(message.body()[41..].to_vec())); + r3.push(fixture.sign(Current, i, &message)); + } + Err(e) => { + answers.insert(i, Err(e)); + } + } + } + let closed3 = relay.close::(3, r3, &mut tamper); + let outcomes = receivers + .into_iter() + .map(|(j, state)| (j, state.and_then(|s| s.finish(&closed2, &closed3)))) + .collect(); + let transcript = relay.transcript; + Run { + fixture, + outcomes, + answers, + transcript, + } +} + +pub(crate) fn run_with(fixture: Fixture, tamper: impl FnMut(&mut Outbox)) -> Run { + let signers = Signers::new(); + drive(fixture, &signers, tamper) +} + +pub(crate) fn run( + kind: CeremonyKind, + t: u16, + n: u16, + seed: u8, + tamper: impl FnMut(&mut Outbox), +) -> Run { + run_with(fixture(kind, t, n, seed), tamper) +} + +pub(crate) fn honest(kind: CeremonyKind, t: u16, n: u16, seed: u8) -> Run { + run(kind, t, n, seed, |_| {}) +} + +// --------------------------------------------------------------- assertions + +type View = ( + Vec<[u8; 96]>, + [u8; 96], + Vec<[u8; 96]>, + Vec, + BTreeMap, + TranscriptHash, +); + +pub(crate) fn view(output: &ReshareOutput) -> View { + let n = output.public.threshold().n(); + ( + output.public.commitments().collect(), + output.public.master_public_key().encode(), + (1..=n) + .map(|j| output.public.public_share(idx(j)).unwrap()) + .collect(), + output.dealers.clone(), + output.excluded.clone(), + output.transcript_hash, + ) +} + +fn last_hash(run: &Run) -> TranscriptHash { + let mut previous = reshare_initial_transcript(&run.fixture.ceremony); + for record in &run.transcript.rounds { + let mut body = previous.as_bytes().to_vec(); + body.extend_from_slice(&(record.closure.bytes.len() as u32).to_be_bytes()); + body.extend_from_slice(&record.closure.bytes); + body.extend_from_slice(&(record.closure.signature.len() as u32).to_be_bytes()); + body.extend_from_slice(&record.closure.signature); + previous = TranscriptHash::new(dst::tagged_hash(dst::TRANSCRIPT, &body)); + } + previous +} + +/// Every listed receiver and the public verifier agree, the master key is the +/// current certificate's, t dealers were selected and each candidate matches +/// its public share. +pub(crate) fn agreed(run: &Run, receivers: &[u16]) -> ReshareOutput { + let verified = run.verify().unwrap(); + assert_eq!(verified.transcript_hash, last_hash(run)); + assert_eq!( + verified.public.master_public_key().encode(), + run.fixture + .current + .public_key_set() + .master_public_key() + .encode() + ); + assert_eq!( + verified.public.threshold(), + run.fixture.ceremony.threshold() + ); + assert_eq!(verified.dealers.len(), usize::from(run.fixture.old_t())); + for j in receivers { + let outcome = &run.outcomes[j]; + assert!( + outcome.is_ok(), + "receiver {j}: {:?}", + outcome.as_ref().err() + ); + let (candidate, output) = run.outcomes[j].as_ref().unwrap(); + assert_eq!(view(output), view(&verified), "receiver {j}"); + assert_eq!(candidate.index(), idx(*j)); + assert_eq!(candidate.transcript_hash(), verified.transcript_hash); + assert_eq!( + G2::generator().mul(&candidate.scalar).encode(), + verified.public.public_share(idx(*j)).unwrap(), + "candidate {j} matches its public share" + ); + } + verified +} + +/// Every listed receiver and the verifier refuse with the same typed error. +pub(crate) fn refused(run: &Run, receivers: &[u16], error: Error) { + assert_eq!(run.verify().err(), Some(error.clone())); + for j in receivers { + assert_eq!(run.outcomes[j].as_ref().err(), Some(&error), "receiver {j}"); + } +} + +/// The successor certificate built from the agreed output, and every +/// receiver's candidate bound to it. +pub(crate) struct Next { + pub(crate) previous: Fixture, + pub(crate) cert: VerifiedCertificate, + pub(crate) shares: BTreeMap, +} + +pub(crate) fn install(run: Run, output: &ReshareOutput) -> Next { + let Run { + fixture, outcomes, .. + } = run; + let body = CertificateBody::successor( + &fixture.current, + &fixture.ceremony, + &output.public, + &output.transcript_hash, + ) + .unwrap(); + let cert = verify_successor( + &fixture.current, + &crate::cert::tests::sign(body, &fixture.signers), + &fixture.signers, + ) + .unwrap(); + let shares = outcomes + .into_iter() + .map(|(j, outcome)| { + let share = KeyShare::bind(outcome.unwrap().0, &cert).unwrap(); + assert_eq!(share.index(), idx(j)); + (j, share) + }) + .collect(); + Next { + previous: fixture, + cert, + shares, + } +} + +/// Wrap a fixed data key to `path` under the certificate's master key. +pub(crate) fn wrapped( + cert: &VerifiedCertificate, + path: &str, + seed: u8, +) -> ([u8; 32], IbeCiphertext) { + let context = cert.key_context(); + let data = [seed; 32]; + let ct = wrap_key( + cert.public_key_set().master_public_key(), + &identity(&context, path), + &DataKey::new(data), + &mut ChaCha20Rng::from_seed([seed; 32]), + ) + .unwrap(); + (data, ct) +} + +/// Every t-subset of the shares derives the key and unwraps `ct`; t−1 shares +/// return `NotEnoughShares`. Returns the number of subsets exercised. +pub(crate) fn unwraps_through_every_subset( + cert: &VerifiedCertificate, + shares: &BTreeMap, + path: &str, + data: &[u8; 32], + ct: &IbeCiphertext, + seed: u8, +) -> usize { + let context = cert.key_context(); + let t = usize::from(context.threshold().t()); + let mut rng = ChaCha20Rng::from_seed([seed ^ 0x5a; 32]); + let tsk = TransportSecretKey::generate(&mut rng); + let req = DecryptRequest::new(&context, identity(&context, path), tsk.public_key()).unwrap(); + let auth = authorize(req.clone()); + let issued: Vec = shares + .values() + .map(|s| issue_share(s, &auth, &mut rng).unwrap()) + .collect(); + for share in &issued { + verify_share(cert.public_key_set(), &req, share).unwrap(); + } + let expected = DataKey::new(*data); + let mut subsets = 0; + for mask in 0u32..(1u32 << issued.len()) { + if mask.count_ones() as usize != t { + continue; + } + let selected: Vec = issued + .iter() + .enumerate() + .filter(|(k, _)| mask & (1u32 << k) != 0) + .map(|(_, s)| s.clone()) + .collect(); + let encrypted = combine(cert.public_key_set(), &req, &selected).unwrap(); + let key = decrypt_derived_key(&encrypted, &tsk, cert.public_key_set(), &req).unwrap(); + assert_eq!( + unwrap_key(&key, req.identity(), ct).unwrap().as_bytes(), + expected.as_bytes() + ); + subsets += 1; + } + assert!(matches!( + combine(cert.public_key_set(), &req, &issued[..t - 1]), + Err(CombineError::NotEnoughShares) + )); + subsets +} + +/// Every t′-subset of the new public shares interpolates to the master key. +pub(crate) fn public_shares_interpolate_to_mpk(public: &PublicMaterial) { + let t = public.threshold().t(); + let n = public.threshold().n(); + let mpk = public.master_public_key().encode(); + for mask in 0u32..(1u32 << n) { + if mask.count_ones() != u32::from(t) { + continue; + } + let subset: Vec = (1..=n) + .filter(|j| mask & (1u32 << (j - 1)) != 0) + .map(idx) + .collect(); + let sum = subset.iter().fold(G2::identity(), |acc, j| { + let lambda = crate::poly::lagrange(*j, subset.iter().copied()).unwrap(); + let share = G2::decode(&public.public_share(*j).unwrap(), false).unwrap(); + acc.add(&share.mul(&lambda)) + }); + assert_eq!(sum.encode(), mpk, "{subset:?}"); + } +} + +// ----------------------------------------------------------------- payloads + +pub(crate) fn complaints(dealers: &[u16]) -> Vec { + ReshareComplaints { + dealers: dealers.iter().map(|i| idx(*i)).collect(), + } + .encode() +} + +pub(crate) fn answer(recipient: u16, value: u64) -> Vec { + ReshareAnswers { + answers: vec![(idx(recipient), Scalar::from_u64(value))], + } + .encode() +} + +/// Re-seal one recipient's entry of a dealer's R1 with a canonical wrong value. +/// The commitments are unchanged, so the dealer's own state can still answer. +pub(crate) fn bad_dealing(out: &mut Outbox, dealer: u16, recipient: u16) -> Vec { + let mut deal = ReshareDeal::decode(&out.payload(dealer)).unwrap(); + let public = CeremonyHpkePublicKey::decode( + &out.ceremony().recipients()[usize::from(recipient - 1)].hpke_public_key, + ) + .unwrap(); + let context = DealingContext { + ceremony_digest: out.ceremony().digest(), + round: 1, + sender: idx(dealer), + recipient: idx(recipient), + commitment_hash: commitment_hash(&deal.commitments), + }; + let mut plaintext = [0u8; 32]; + plaintext[31] = 5; + let sealed = seal_dealing(&public, &context, &plaintext, out.rng()).unwrap(); + deal.dealings + .iter_mut() + .find(|(j, _)| j.get() == recipient) + .unwrap() + .1 = sealed; + deal.encode() +} + +/// Open a dealer's R1 entry with the recipient's regenerated key. +pub(crate) fn opened(fixture: &Fixture, deal: &[u8], dealer: u16, recipient: u16) -> Scalar { + let deal = ReshareDeal::decode(deal).unwrap(); + let context = DealingContext { + ceremony_digest: fixture.ceremony.digest(), + round: 1, + sender: idx(dealer), + recipient: idx(recipient), + commitment_hash: commitment_hash(&deal.commitments), + }; + let sealed = &deal + .dealings + .iter() + .find(|(j, _)| j.get() == recipient) + .unwrap() + .1; + let plaintext = open_dealing(&fixture.secret(recipient), &context, sealed).unwrap(); + assert_eq!(plaintext.len(), 32); + Scalar::from_bytes(&plaintext[..]).unwrap() +} + +fn r2_dealers(run: &Run, recipient: u16) -> Vec { + let message = run.transcript.rounds[1] + .messages + .iter() + .find(|m| sender(m) == (Recipients as u8, recipient)) + .unwrap(); + ReshareComplaints::decode(&message.bytes[41..]) + .unwrap() + .dealers +} + +struct CountingRng { + inner: ChaCha20Rng, + draws: usize, +} +impl RngCore for CountingRng { + fn next_u32(&mut self) -> u32 { + self.draws += 1; + self.inner.next_u32() + } + fn next_u64(&mut self) -> u64 { + self.draws += 1; + self.inner.next_u64() + } + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.draws += 1; + self.inner.fill_bytes(dest) + } + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + self.draws += 1; + self.inner.try_fill_bytes(dest) + } +} +impl CryptoRng for CountingRng {} + +fn counting() -> CountingRng { + CountingRng { + inner: ChaCha20Rng::from_seed([0x78; 32]), + draws: 0, + } +} + +fn close_for( + f: &Fixture, + ceremony: &AuthorizedCeremony, + round: u8, + previous: &TranscriptHash, + messages: &[SignedStatement], +) -> Result, Error> { + let bytes = round_closure_body(ceremony, round, previous, messages)?; + let closure = SignedStatement { + signature: f.signers.sign( + &f.signers.anchor().expected_owner, + &round_closure_preimage(&bytes)?, + ), + bytes, + }; + ClosedRound::::verify(ceremony, round, previous, &closure, messages, &f.signers) +} + +fn excluded(entries: &[(u16, ExcludeReason)]) -> BTreeMap { + entries.iter().map(|(i, r)| (idx(*i), *r)).collect() +} + +fn dealers(list: &[u16]) -> Vec { + list.iter().map(|i| idx(*i)).collect() +} + +const EMPTY_ANSWERS: [u8; 3] = [1, 0, 0]; + +// --------------------------------------------------- completion and handoff + +#[test] +fn genuine_2of3_to_3of4_preserves_the_master_key_and_binds_every_candidate() { + let run = honest(Reshare, 3, 4, 0x81); + let output = agreed(&run, &[1, 2, 3, 4]); + assert_eq!(output.dealers, dealers(&[1, 2])); + assert!(output.excluded.is_empty()); + assert_eq!(run.transcript.rounds.len(), 3); + assert_eq!(output.public.threshold(), Threshold::new(3, 4).unwrap()); + assert_eq!(output.public.commitments().len(), 3); + for answers in run.answers.values() { + assert_eq!(answers.as_ref().unwrap().as_slice(), &EMPTY_ANSWERS); + } + public_shares_interpolate_to_mpk(&output.public); + // 3-of-4 is reachable by redistribution although genesis refuses it. + assert_eq!( + Threshold::new(3, 4).unwrap().check_genesis_budget(), + Err(Error::UnsupportedFaultBudget) + ); + let (data, ct) = wrapped(&run.fixture.current, "records/reshare", 0x81); + let next = install(run, &output); + assert_eq!(next.cert.key_context().epoch().get(), 1); + assert_eq!( + unwraps_through_every_subset( + &next.cert, + &next.shares, + "records/reshare", + &data, + &ct, + 0x82 + ), + 4 + ); +} + +#[test] +fn dkg_genesis_reshare_chain_keeps_the_master_key_through_refresh() { + let dkg = crate::dkg_relay::honest(2, 3, 0x83); + let genesis_output = crate::dkg_relay::agreed(&dkg, &[1, 2, 3]); + let crate::dkg_relay::Run { + fixture: dkg_fixture, + outcomes, + .. + } = dkg; + let anchor = dkg_fixture.signers.anchor(); + let body = CertificateBody::genesis( + &anchor, + &dkg_fixture.ceremony, + &genesis_output.public, + &genesis_output.transcript_hash, + ) + .unwrap(); + let genesis = verify_certificate( + &anchor, + &crate::cert::tests::sign(body, &dkg_fixture.signers), + &dkg_fixture.signers, + ) + .unwrap(); + let mut held: BTreeMap = outcomes + .into_iter() + .map(|(i, o)| (i, KeyShare::bind(o.unwrap().0, &genesis).unwrap())) + .collect(); + let mpk = genesis.public_key_set().master_public_key().encode(); + let (data, ct) = wrapped(&genesis, "records/chain", 0x84); + assert_eq!( + unwraps_through_every_subset(&genesis, &held, "records/chain", &data, &ct, 0x84), + 3 + ); + let mut current = genesis; + let steps = [ + (Reshare, 3u16, 4u16, 0x85u8, 4usize), + (Reshare, 2, 3, 0x86, 3), + (Refresh, 2, 3, 0x87, 3), + ]; + for (kind, t, n, seed, subsets) in steps { + let epoch = current.key_context().epoch().get(); + let dids: Vec = (1..=n as u8).collect(); + let f = fixture_on(current, held.into_values(), kind, t, n, seed, &dids); + let run = run_with(f, |_| {}); + let all: Vec = (1..=n).collect(); + let output = agreed(&run, &all); + assert_eq!(output.public.master_public_key().encode(), mpk, "{kind:?}"); + public_shares_interpolate_to_mpk(&output.public); + let next = install(run, &output); + assert_eq!(next.cert.key_context().epoch().get(), epoch + 1); + assert_eq!(next.cert.key_context().rekey().get(), 0); + assert_eq!( + next.cert.key_context().threshold(), + Threshold::new(t, n).unwrap() + ); + assert_eq!( + unwraps_through_every_subset( + &next.cert, + &next.shares, + "records/chain", + &data, + &ct, + seed + ), + subsets + ); + current = next.cert; + held = next.shares; + } +} + +#[test] +fn old_shares_stay_in_their_own_context_after_redistribution() { + let run = honest(Reshare, 2, 3, 0x88); + let output = agreed(&run, &[1, 2, 3]); + let transcript = output.transcript_hash; + let next = install(run, &output); + let old = &next.previous; + let old_context = old.current.key_context(); + let new_context = next.cert.key_context(); + assert_ne!(old_context.digest(), new_context.digest()); + let mut rng = ChaCha20Rng::from_seed([0x88; 32]); + let tsk = TransportSecretKey::generate(&mut rng); + let old_req = DecryptRequest::new( + &old_context, + identity(&old_context, "records/split"), + tsk.public_key(), + ) + .unwrap(); + let new_req = DecryptRequest::new( + &new_context, + identity(&new_context, "records/split"), + tsk.public_key(), + ) + .unwrap(); + let old_issued = issue_share(&old.shares[&1], &authorize(old_req.clone()), &mut rng).unwrap(); + let new_issued: Vec = next + .shares + .values() + .map(|s| issue_share(s, &authorize(new_req.clone()), &mut rng).unwrap()) + .collect(); + assert!(matches!( + issue_share(&old.shares[&1], &authorize(new_req.clone()), &mut rng), + Err(Error::ContextMismatch) + )); + assert_eq!( + verify_share(next.cert.public_key_set(), &new_req, &old_issued), + Err(ShareError::ContextMismatch) + ); + assert_eq!( + verify_share(next.cert.public_key_set(), &old_req, &old_issued), + Err(ShareError::ContextMismatch) + ); + assert!(matches!( + combine( + next.cert.public_key_set(), + &new_req, + &[new_issued[0].clone(), old_issued.clone()] + ), + Err(CombineError::MixedContexts) + )); + assert!(matches!( + combine( + old.current.public_key_set(), + &old_req, + &[old_issued.clone(), new_issued[1].clone()] + ), + Err(CombineError::MixedContexts) + )); + // The old scalar is not a candidate share for the new certificate. + let old_scalar = Scalar::from_bytes(&old.shares[&1].scalar.to_bytes()).unwrap(); + assert!(matches!( + KeyShare::bind( + CandidateShare::new(transcript, idx(1), old_scalar), + &next.cert + ), + Err(Error::CandidateMismatch) + )); + // Each epoch still derives under its own certificate; nothing here erases + // the old shares. + let (data, ct) = wrapped(&old.current, "records/split", 0x89); + assert_eq!( + unwraps_through_every_subset(&old.current, &old.shares, "records/split", &data, &ct, 0x8a), + 3 + ); + assert_eq!( + unwraps_through_every_subset(&next.cert, &next.shares, "records/split", &data, &ct, 0x8b), + 3 + ); +} + +#[test] +fn fewer_than_t_valid_dealers_abort_everywhere_and_the_old_epoch_stays_usable() { + let run = run(Reshare, 3, 4, 0x8c, |out| { + if out.round() == 1 { + out.remove(2); + out.remove(3); + } + }); + refused(&run, &[1, 2, 3, 4], Error::InsufficientQualified); + let (data, ct) = wrapped(&run.fixture.current, "records/abort", 0x8c); + assert_eq!( + unwraps_through_every_subset( + &run.fixture.current, + &run.fixture.shares, + "records/abort", + &data, + &ct, + 0x8d + ), + 3 + ); +} + +#[test] +fn within_the_secrecy_budget_but_below_t_valid_dealers_refuses_and_keeps_the_old_epoch() { + // Old 3-of-4 (reached by redistribution): t−1 = 2 faulty dealers may + // withhold R1 within the secrecy budget, leaving 2 < t = 3 dealers. + let first = honest(Reshare, 3, 4, 0x8e); + let output = agreed(&first, &[1, 2, 3, 4]); + let next = install(first, &output); + let f = fixture_on( + next.cert, + next.shares.into_values(), + Reshare, + 2, + 3, + 0x8f, + &[1, 2, 3], + ); + let run = run_with(f, |out| { + if out.round() == 1 { + out.remove(3); + out.remove(4); + } + }); + refused(&run, &[1, 2, 3], Error::InsufficientQualified); + let (data, ct) = wrapped(&run.fixture.current, "records/liveness", 0x8f); + assert_eq!( + unwraps_through_every_subset( + &run.fixture.current, + &run.fixture.shares, + "records/liveness", + &data, + &ct, + 0x90 + ), + 4 + ); +} + +#[test] +fn a_member_of_both_cohorts_signs_distinct_roles() { + // Old member 1 (did 1) is new member 3; new members 1, 2 and 4 are new nodes. + let (current, shares) = dealer_genesis(2, 3); + let f = fixture_on(current, shares, Reshare, 2, 4, 0x91, &[4, 5, 1, 6]); + assert_eq!(f.did(Current, 1), f.did(Recipients, 3)); + assert_ne!(f.did(Current, 1), f.did(Recipients, 1)); + let mut r2 = Vec::new(); + let run = run_with(f, |out| { + if out.round() == 2 { + r2 = out.messages.clone(); + } + }); + let output = agreed(&run, &[1, 2, 3, 4]); + assert_eq!(output.dealers, dealers(&[1, 2])); + let f = &run.fixture; + let own = r2 + .iter() + .find(|m| sender(m) == (Recipients as u8, 3)) + .unwrap(); + f.signers + .verify( + &Signers::did(1), + &ceremony_message_preimage(&own.bytes).unwrap(), + &own.signature, + ) + .unwrap(); + // did 1 cannot speak for recipient coordinate 1. + let h0 = reshare_initial_transcript(&f.ceremony); + let bytes = ceremony_message_body(&f.ceremony, 1, Recipients, idx(1), &EMPTY_ANSWERS).unwrap(); + let forged = SignedStatement { + signature: f.signers.sign( + &Signers::did(1), + &ceremony_message_preimage(&bytes).unwrap(), + ), + bytes, + }; + assert!(matches!( + close_for::(f, &f.ceremony, 1, &h0, &[forged]), + Err(Error::InvalidSignature) + )); +} + +// ------------------------------------------------------- complaints and R3 + +#[test] +fn t_prime_complainers_exclude_the_dealer_and_it_publishes_no_answer() { + let run = run(Reshare, 2, 3, 0x92, |out| match out.round() { + 1 => out.order(&[3, 1, 2]), + 2 => { + out.replace(1, &complaints(&[3])); + out.replace(2, &complaints(&[3])); + } + _ => {} + }); + assert_eq!(run.answers[&3].as_ref().unwrap().as_slice(), &EMPTY_ANSWERS); + let from3: Vec<_> = run.transcript.rounds[2] + .messages + .iter() + .filter(|m| sender(m) == (Current as u8, 3)) + .collect(); + assert_eq!(from3.len(), 1); + assert_eq!(&from3[0].bytes[41..], &EMPTY_ANSWERS); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.excluded, excluded(&[(3, ExcludeReason::Complaints)])); + assert_eq!(output.dealers, dealers(&[1, 2])); +} + +#[test] +fn false_complaint_is_refuted_by_a_valid_answer() { + let mut deal = Vec::new(); + let run = run(Reshare, 2, 3, 0x93, |out| match out.round() { + 1 => deal = out.payload(1), + 2 => out.replace(3, &complaints(&[1])), + _ => {} + }); + let published = ReshareAnswers::decode(run.answers[&1].as_ref().unwrap()).unwrap(); + assert_eq!(published.answers.len(), 1); + assert_eq!(published.answers[0].0, idx(3)); + assert_eq!( + published.answers[0].1.to_bytes(), + opened(&run.fixture, &deal, 1, 3).to_bytes() + ); + let output = agreed(&run, &[1, 2, 3]); + assert!(output.excluded.is_empty()); + assert_eq!(output.dealers, dealers(&[1, 2])); +} + +#[test] +fn one_recipient_bad_value_is_replaced_by_a_valid_answer() { + let run = run(Reshare, 2, 3, 0x94, |out| match out.round() { + 1 => { + let forged = bad_dealing(out, 1, 2); + out.replace(1, &forged); + } + 2 => assert_eq!( + ReshareComplaints::decode(&out.payload(2)).unwrap().dealers, + dealers(&[1]) + ), + _ => {} + }); + // Receiver 2's candidate matches its public share only through the answer. + let output = agreed(&run, &[1, 2, 3]); + assert!(output.excluded.is_empty()); + assert_eq!(output.dealers, dealers(&[1, 2])); + let published = ReshareAnswers::decode(run.answers[&1].as_ref().unwrap()).unwrap(); + assert_eq!( + published + .answers + .iter() + .map(|(j, _)| *j) + .collect::>(), + dealers(&[2]) + ); +} + +#[test] +fn invalid_answer_excludes_the_dealer_and_the_next_dealer_is_selected() { + let run = run(Reshare, 2, 3, 0x95, |out| match out.round() { + 1 => { + let forged = bad_dealing(out, 1, 2); + out.replace(1, &forged); + } + 3 => out.replace(1, &answer(2, 5)), + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::InvalidAnswer)]) + ); + assert_eq!(output.dealers, dealers(&[2, 3])); +} + +#[test] +fn missing_r3_excludes_only_with_required_answers() { + let run = run(Reshare, 2, 3, 0x96, |out| match out.round() { + 2 => out.replace(3, &complaints(&[1])), + 3 => { + out.remove(1); + out.remove(2); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingAnswer)]) + ); + assert_eq!(output.dealers, dealers(&[2, 3])); +} + +#[test] +fn malformed_r3_excludes_only_with_required_answers() { + let run = run(Reshare, 2, 3, 0x97, |out| match out.round() { + 2 => out.replace(3, &complaints(&[1])), + 3 => { + out.replace(1, &[1, 0]); + out.replace(2, &[1, 0]); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingAnswer)]) + ); + assert_eq!(output.dealers, dealers(&[2, 3])); +} + +#[test] +fn invalid_unsolicited_answer_excludes_its_dealer() { + let run = run(Reshare, 2, 3, 0x98, |out| { + if out.round() == 3 { + out.replace(2, &answer(1, 5)); + } + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(2, ExcludeReason::InvalidAnswer)]) + ); + assert_eq!(output.dealers, dealers(&[1, 3])); +} + +#[test] +fn valid_unsolicited_answer_is_ignored() { + let mut deal = Vec::new(); + let run = run(Reshare, 2, 3, 0x99, |out| match out.round() { + 1 => deal = out.payload(2), + 3 => { + let value = opened(out.fixture(), &deal, 2, 1); + let unsolicited = ReshareAnswers { + answers: vec![(idx(1), value)], + }; + out.replace(2, &unsolicited.encode()); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert!(output.excluded.is_empty()); + assert_eq!(output.dealers, dealers(&[1, 2])); +} + +#[test] +fn conflicting_r3_excludes_its_dealer() { + let run = run(Reshare, 2, 3, 0x9a, |out| { + if out.round() == 3 { + out.push(2, &[1, 0, 0, 0]); + } + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(2, ExcludeReason::Equivocated { round: 3 })]) + ); + assert_eq!(output.dealers, dealers(&[1, 3])); +} + +#[test] +fn r3_naming_a_recipient_outside_the_new_cohort_is_malformed() { + let run = run(Reshare, 2, 3, 0x9b, |out| match out.round() { + 2 => out.replace(3, &complaints(&[1])), + 3 => { + let mut published = ReshareAnswers::decode(&out.payload(1)).unwrap(); + published.answers.push((idx(4), Scalar::from_u64(1))); + out.replace(1, &published.encode()); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingAnswer)]) + ); + assert_eq!(output.dealers, dealers(&[2, 3])); +} + +#[test] +fn malformed_r2_supplies_no_complaints() { + let run = run(Reshare, 2, 3, 0x9c, |out| match out.round() { + 2 => { + let mut malformed = complaints(&[1]); + malformed.push(0); + out.replace(3, &malformed); + } + 3 => out.remove(1), + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert!(output.excluded.is_empty()); + assert_eq!(output.dealers, dealers(&[1, 2])); +} + +#[test] +fn conflicting_r2_supplies_no_complaints_and_does_not_affect_the_same_nodes_dealer_role() { + // Recipient 1 is the same node (did 1) as old dealer 1. + let run = run(Reshare, 2, 3, 0x9d, |out| match out.round() { + 2 => { + out.replace(1, &complaints(&[2])); + out.push(1, &complaints(&[2, 3])); + } + 3 => { + out.remove(2); + out.remove(3); + } + _ => {} + }); + assert_eq!(run.answers[&2].as_ref().unwrap().as_slice(), &EMPTY_ANSWERS); + let output = agreed(&run, &[1, 2, 3]); + assert!(output.excluded.is_empty()); + assert_eq!(output.dealers, dealers(&[1, 2])); +} + +#[test] +fn duplicate_complaints_count_once() { + // t′ = 3: two distinct complainers, one retransmitting, stay below t′. + let run = run(Reshare, 3, 5, 0x9e, |out| { + if out.round() == 2 { + out.replace(4, &complaints(&[1])); + out.replace(5, &complaints(&[1])); + out.duplicate(4); + out.duplicate(4); + } + }); + let published = ReshareAnswers::decode(run.answers[&1].as_ref().unwrap()).unwrap(); + assert_eq!( + published + .answers + .iter() + .map(|(j, _)| *j) + .collect::>(), + dealers(&[4, 5]) + ); + let output = agreed(&run, &[1, 2, 3, 4, 5]); + assert!(output.excluded.is_empty()); + assert_eq!(output.dealers, dealers(&[1, 2])); +} + +#[test] +fn r2_naming_a_dealer_outside_the_old_cohort_supplies_no_complaints() { + let run = run(Reshare, 2, 3, 0x9f, |out| match out.round() { + 2 => out.replace(3, &complaints(&[1, 4])), + 3 => out.remove(1), + _ => {} + }); + assert_eq!(run.answers[&1].as_ref().unwrap().as_slice(), &EMPTY_ANSWERS); + let output = agreed(&run, &[1, 2, 3]); + assert!(output.excluded.is_empty()); + assert_eq!(output.dealers, dealers(&[1, 2])); +} + +#[test] +fn r2_naming_an_old_dealer_above_the_new_cohort_size_counts() { + // Old 3-of-5 to new 3-of-4: dealer indices are bounded by the old n = 5, + // so recipient 1's complaint against dealer 5 counts and is answered. + let (current, shares) = dealer_genesis(3, 5); + let f = fixture_on(current, shares, Reshare, 3, 4, 0xbe, &[1, 2, 3, 4]); + let run = run_with(f, |out| match out.round() { + 1 => { + out.order(&[5, 1, 2, 3, 4]); + let forged = bad_dealing(out, 5, 1); + out.replace(5, &forged); + } + 2 => assert_eq!( + ReshareComplaints::decode(&out.payload(1)).unwrap().dealers, + dealers(&[5]) + ), + _ => {} + }); + let published = ReshareAnswers::decode(run.answers[&5].as_ref().unwrap()).unwrap(); + assert_eq!( + published + .answers + .iter() + .map(|(j, _)| *j) + .collect::>(), + dealers(&[1]) + ); + let output = agreed(&run, &[1, 2, 3, 4]); + assert!(output.excluded.is_empty()); + assert_eq!(output.dealers, dealers(&[5, 1, 2])); +} + +#[test] +fn relay_omitted_complaint_fails_only_the_affected_receiver() { + // Failure scenario outside the trusted-relay liveness model. + let run = run(Reshare, 2, 3, 0xa0, |out| match out.round() { + 1 => { + let forged = bad_dealing(out, 1, 2); + out.replace(1, &forged); + } + 2 => out.remove(2), + _ => {} + }); + assert!(matches!( + run.outcomes[&2], + Err(Error::LocalShareUnavailable) + )); + let output = agreed(&run, &[1, 3]); + assert_eq!(output.dealers, dealers(&[1, 2])); +} + +// --------------------------------------------------------------- R1 checks + +type Mutation = fn(&mut Outbox); + +#[test] +fn r1_public_checks_exclude_malformed_dealings_and_keep_the_master_key() { + let cases: [(&str, Mutation); 6] = [ + ("t′+1 coefficients", |out| { + let mut deal = ReshareDeal::decode(&out.payload(1)).unwrap(); + deal.commitments.push(G2::generator()); + out.replace(1, &deal.encode()); + }), + ("t′−1 coefficients", |out| { + let mut deal = ReshareDeal::decode(&out.payload(1)).unwrap(); + deal.commitments.pop(); + out.replace(1, &deal.encode()); + }), + ("identity top coefficient", |out| { + let mut deal = ReshareDeal::decode(&out.payload(1)).unwrap(); + let top = deal.commitments.len() - 1; + deal.commitments[top] = G2::identity(); + out.replace(1, &deal.encode()); + }), + ("constant is not the old public share", |out| { + let mut deal = ReshareDeal::decode(&out.payload(1)).unwrap(); + deal.commitments[0] = G2::generator().mul(&Scalar::from_u64(5)); + out.replace(1, &deal.encode()); + }), + ("recipient set is not 1..=n′", |out| { + let mut deal = ReshareDeal::decode(&out.payload(1)).unwrap(); + deal.dealings.pop(); + out.replace(1, &deal.encode()); + }), + ("undecodable", |out| out.replace(1, &[1, 0])), + ]; + for (k, (name, mutate)) in cases.iter().enumerate() { + let run = run(Reshare, 2, 3, 0xa1 + k as u8, |out| { + if out.round() == 1 { + mutate(out); + } + }); + // `agreed` also checks that the master key is the current one. + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MalformedDeal)]), + "{name}" + ); + assert_eq!(output.dealers, dealers(&[2, 3]), "{name}"); + } +} + +#[test] +fn missing_and_equivocated_r1_exclude() { + let missing = run(Reshare, 2, 3, 0xa7, |out| { + if out.round() == 1 { + out.remove(1); + } + }); + let output = agreed(&missing, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingDeal)]) + ); + assert_eq!(output.dealers, dealers(&[2, 3])); + + let equivocated = run(Reshare, 2, 3, 0xa8, |out| { + if out.round() == 1 { + let second = bad_dealing(out, 1, 3); + out.push(1, &second); + } + }); + let output = agreed(&equivocated, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::Equivocated { round: 1 })]) + ); + assert_eq!(output.dealers, dealers(&[2, 3])); +} + +#[test] +fn dealers_are_the_first_t_in_closed_round_order() { + let reordered = run(Reshare, 2, 3, 0xa9, |out| { + if out.round() == 1 { + out.order(&[3, 1, 2]); + } + }); + assert_eq!(agreed(&reordered, &[1, 2, 3]).dealers, dealers(&[3, 1])); + + // A dealer's position is that of its first message. + let repeated = run(Reshare, 2, 3, 0xaa, |out| { + if out.round() == 1 { + out.order(&[3, 1, 2]); + let copy = out.messages[2].clone(); + out.messages.insert(0, copy); + } + }); + assert_eq!(agreed(&repeated, &[1, 2, 3]).dealers, dealers(&[2, 3])); + + // An excluded dealer's place passes to the next eligible dealer. + let skipped = run(Reshare, 2, 3, 0xab, |out| { + if out.round() == 1 { + out.order(&[3, 1, 2]); + out.replace(3, &[1, 0]); + } + }); + let output = agreed(&skipped, &[1, 2, 3]); + assert_eq!(output.dealers, dealers(&[1, 2])); + assert_eq!( + output.excluded, + excluded(&[(3, ExcludeReason::MalformedDeal)]) + ); +} + +// --------------------------------------------------------- wrong cohorts + +#[test] +fn wrong_cohort_r1_is_ignored_without_blame() { + // Dealer 1's deal under the recipient role does not count; recipient 2 + // (the same node as dealer 2) sending an R1 under the recipient role does + // not make dealer 2 equivocate. + let run = run(Reshare, 2, 3, 0xac, |out| { + if out.round() == 1 { + let payload = out.payload(1); + out.remove(1); + out.push_as(Recipients, 1, &payload); + out.push_as(Recipients, 2, &[1, 0]); + } + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingDeal)]) + ); + assert_eq!(output.dealers, dealers(&[2, 3])); +} + +#[test] +fn wrong_cohort_r2_is_ignored_without_blame() { + // Two old dealers' complaints under the dealer role would reach t′ = 2. + let run = run(Reshare, 2, 3, 0xad, |out| match out.round() { + 1 => out.order(&[3, 1, 2]), + 2 => { + out.push_as(Current, 1, &complaints(&[3])); + out.push_as(Current, 2, &complaints(&[3])); + } + _ => {} + }); + assert_eq!(run.answers[&3].as_ref().unwrap().as_slice(), &EMPTY_ANSWERS); + let output = agreed(&run, &[1, 2, 3]); + assert!(output.excluded.is_empty()); + assert_eq!(output.dealers, dealers(&[3, 1])); +} + +#[test] +fn wrong_cohort_r3_is_ignored_without_blame() { + let run = run(Reshare, 2, 3, 0xae, |out| match out.round() { + 2 => out.replace(3, &complaints(&[1])), + 3 => { + // Dealer 1's required answer under the recipient role does not count. + let genuine = out.payload(1); + out.remove(1); + out.push_as(Recipients, 1, &genuine); + // An invalid answer under the recipient role does not blame dealer 2. + out.push_as(Recipients, 2, &answer(1, 5)); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingAnswer)]) + ); + assert_eq!(output.dealers, dealers(&[2, 3])); +} + +// ------------------------------------------- authorization and state binding + +#[test] +fn dealing_refuses_unauthorized_contexts_before_any_randomness() { + let f = fixture(Reshare, 2, 3, 0xb0); + let mut rng = counting(); + let share = &f.shares[&1]; + assert!(matches!( + reshare_deal(&f.signers.genesis(), &f.current, share, &mut rng), + Err(Error::InvalidCeremony) + )); + let (_, _, rekey) = crate::cert::tests::successor( + &f.signers, + &f.current, + CeremonyKind::Rekey, + Threshold::new(2, 3).unwrap(), + 12, + ); + assert!(matches!( + reshare_deal(&rekey, &f.current, share, &mut rng), + Err(Error::InvalidCeremony) + )); + // The ceremony names a certificate other than the one supplied as current. + let (newer, newer_shares) = newer_certificate(&f); + assert!(matches!( + reshare_deal(&f.ceremony, &newer, &newer_shares[0], &mut rng), + Err(Error::ContextMismatch) + )); + // A share bound to another certificate. + assert!(matches!( + reshare_deal(&f.ceremony, &f.current, &newer_shares[0], &mut rng), + Err(Error::ContextMismatch) + )); + // Refresh keeps t, n and the index→DID directory of the current certificate. + for (t, n, dids) in [ + (2u16, 4u16, vec![1u8, 2, 3, 4]), + (3, 3, vec![1, 2, 3]), + (2, 3, vec![2, 1, 3]), + (2, 3, vec![1, 2, 9]), + ] { + let body = statement_body(&f.current, Refresh, t, n, 0xb1, &dids); + let refresh = + AuthorizedCeremony::verify_for(&f.current, &f.signers.statement(&body), &f.signers) + .unwrap(); + assert!( + matches!( + reshare_deal(&refresh, &f.current, share, &mut rng), + Err(Error::InvalidCeremony) + ), + "{t}-of-{n} {dids:?}" + ); + } + assert_eq!(rng.draws, 0); + let body = statement_body(&f.current, Refresh, 2, 3, 0xb1, &[1, 2, 3]); + let refresh = + AuthorizedCeremony::verify_for(&f.current, &f.signers.statement(&body), &f.signers) + .unwrap(); + assert!(reshare_deal(&refresh, &f.current, share, &mut rng).is_ok()); + assert!(rng.draws > 0); + let (state, message) = reshare_deal(&f.ceremony, &f.current, share, &mut rng).unwrap(); + assert_eq!(message.round(), 1); + assert_eq!(format!("{state:?}"), "ReshareDealer([REDACTED])"); +} + +#[test] +fn receiver_start_checks_ceremony_index_and_recipient_key() { + let f = fixture(Reshare, 2, 3, 0xb2); + assert!(matches!( + reshare_start_receiver(&f.ceremony, &f.current, idx(1), f.secret(2)), + Err(Error::ContextMismatch) + )); + assert!(matches!( + reshare_start_receiver(&f.ceremony, &f.current, idx(4), f.secret(1)), + Err(Error::InvalidIndex) + )); + assert!(matches!( + reshare_start_receiver(&f.signers.genesis(), &f.current, idx(1), f.secret(1)), + Err(Error::InvalidCeremony) + )); + let (newer, _) = newer_certificate(&f); + assert!(matches!( + reshare_start_receiver(&f.ceremony, &newer, idx(1), f.secret(1)), + Err(Error::ContextMismatch) + )); + let body = statement_body(&f.current, Refresh, 3, 3, 0xb2, &[1, 2, 3]); + let refresh = + AuthorizedCeremony::verify_for(&f.current, &f.signers.statement(&body), &f.signers) + .unwrap(); + assert!(matches!( + reshare_start_receiver(&refresh, &f.current, idx(1), f.secret(1)), + Err(Error::InvalidCeremony) + )); + let state = reshare_start_receiver(&f.ceremony, &f.current, idx(1), f.secret(1)).unwrap(); + assert_eq!(format!("{state:?}"), "ReshareReceiving([REDACTED])"); +} + +#[test] +fn forged_statement_fields_fail_owner_authentication() { + let f = fixture(Reshare, 3, 4, 0xb3); + let body = statement_body(&f.current, Reshare, 3, 4, 0xb3, &[1, 2, 3, 4]); + assert_eq!(body.encode(), f.statement.bytes); + for mutation in 0..5 { + let mut changed = body.clone(); + match mutation { + 0 => changed.recipients[3].hpke_public_key = [7; 32], + 1 => changed.recipients[3].node = Signers::did(9), + 2 => { + changed.recipients[2].node = Signers::did(4); + changed.recipients[3].node = Signers::did(3); + } + 3 => changed.threshold = Threshold::new(2, 4).unwrap(), + _ => changed.current = Some(CertDigest::new([9; 32])), + } + let forged = SignedStatement { + bytes: changed.encode(), + signature: f.statement.signature.clone(), + }; + assert!( + AuthorizedCeremony::verify_for(&f.current, &forged, &f.signers).is_err(), + "{mutation}" + ); + } +} + +#[test] +fn transitions_refuse_other_ceremonies_rounds_and_histories() { + let f = fixture(Reshare, 2, 3, 0xb4); + // Same current certificate, another authorized ceremony. + let other = fixture(Reshare, 2, 3, 0xb5); + let h0 = reshare_initial_transcript(&f.ceremony); + let deal = |f: &Fixture, i: u16| { + let mut rng = f.member_rng(i); + reshare_deal(&f.ceremony, &f.current, &f.shares[&i], &mut rng).unwrap() + }; + let receiver = + |j: u16| reshare_start_receiver(&f.ceremony, &f.current, idx(j), f.secret(j)).unwrap(); + let r1: Vec<_> = (1..=3) + .map(|i| f.sign(Current, i, &deal(&f, i).1)) + .collect(); + let closed1 = close_for::(&f, &f.ceremony, 1, &h0, &r1).unwrap(); + let other_r1: Vec<_> = (1..=3) + .map(|i| other.sign(Current, i, &deal(&other, i).1)) + .collect(); + let foreign = close_for::( + &other, + &other.ceremony, + 1, + &reshare_initial_transcript(&other.ceremony), + &other_r1, + ) + .unwrap(); + assert!(matches!( + receiver(1).complain(&foreign), + Err(Error::InvalidRound) + )); + assert!(close_for::(&f, &f.ceremony, 1, &h0, &other_r1).is_err()); + let on_our_link = close_for::(&other, &other.ceremony, 1, &h0, &other_r1).unwrap(); + assert!(matches!( + receiver(1).complain(&on_our_link), + Err(Error::InvalidRound) + )); + let rebased = + close_for::(&f, &f.ceremony, 1, &TranscriptHash::new([9; 32]), &r1).unwrap(); + assert!(matches!( + receiver(1).complain(&rebased), + Err(Error::InvalidRound) + )); + let dkg_link = close_for::( + &f, + &f.ceremony, + 1, + &dkg_initial_transcript(&f.ceremony), + &r1, + ) + .unwrap(); + assert!(matches!( + receiver(1).complain(&dkg_link), + Err(Error::InvalidRound) + )); + let wrong_round = close_for::(&f, &f.ceremony, 3, &h0, &[]).unwrap(); + assert!(matches!( + receiver(1).complain(&wrong_round), + Err(Error::InvalidRound) + )); + + let mut complained = BTreeMap::new(); + let mut r2 = Vec::new(); + for j in 1..=3u16 { + let (state, message) = receiver(j).complain(&closed1).unwrap(); + r2.push(f.sign(Recipients, j, &message)); + complained.insert(j, state); + } + assert_eq!( + format!("{:?}", complained[&1]), + "ReshareComplained([REDACTED])" + ); + let closed2 = + close_for::(&f, &f.ceremony, 2, &closed1.transcript_hash(), &r2) + .unwrap(); + let skipped2 = close_for::(&f, &f.ceremony, 2, &h0, &r2).unwrap(); + assert!(matches!( + deal(&f, 1).0.answer(&closed1, &skipped2), + Err(Error::InvalidRound) + )); + assert!(matches!( + deal(&f, 1).0.answer(&foreign, &closed2), + Err(Error::InvalidRound) + )); + assert!(matches!( + deal(&f, 1).0.answer(&rebased, &closed2), + Err(Error::InvalidRound) + )); + let r3: Vec<_> = (1..=3) + .map(|i| { + f.sign( + Current, + i, + &deal(&f, i).0.answer(&closed1, &closed2).unwrap(), + ) + }) + .collect(); + let skipped3 = + close_for::(&f, &f.ceremony, 3, &closed1.transcript_hash(), &r3).unwrap(); + let chained3 = + close_for::(&f, &f.ceremony, 3, &closed2.transcript_hash(), &r3).unwrap(); + assert!(matches!( + complained.remove(&1).unwrap().finish(&closed2, &skipped3), + Err(Error::InvalidRound) + )); + assert!(matches!( + complained.remove(&2).unwrap().finish(&skipped2, &chained3), + Err(Error::InvalidRound) + )); + assert!(complained + .remove(&3) + .unwrap() + .finish(&closed2, &chained3) + .is_ok()); +} + +#[test] +fn retry_under_a_new_ceremony_cannot_reuse_an_earlier_dealing() { + let earlier = fixture(Reshare, 2, 3, 0xb6); + let mut rng = earlier.member_rng(1); + let (_, message) = reshare_deal( + &earlier.ceremony, + &earlier.current, + &earlier.shares[&1], + &mut rng, + ) + .unwrap(); + let earlier_payload = message.body()[41..].to_vec(); + let run = run(Reshare, 2, 3, 0xb7, |out| { + if out.round() == 1 { + out.replace(1, &earlier_payload); + } + }); + // The earlier dealing passes the public checks but opens for no recipient. + for j in 1..=3 { + assert_eq!(r2_dealers(&run, j), dealers(&[1]), "recipient {j}"); + } + assert_eq!(run.answers[&1].as_ref().unwrap().as_slice(), &EMPTY_ANSWERS); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.excluded, excluded(&[(1, ExcludeReason::Complaints)])); + assert_eq!(output.dealers, dealers(&[2, 3])); +} + +/// A test DID verifier that also accepts `signature || 00`, modelling a +/// signature scheme with more than one valid encoding of one statement. +struct Malleable(Signers); +impl DidSignatureVerifier for Malleable { + fn verify(&self, signer: &Did, message: &[u8], signature: &[u8]) -> Result<(), SignatureError> { + match signature { + [inner @ .., 0] if inner.len() == 64 => self.0.verify(signer, message, inner), + _ => self.0.verify(signer, message, signature), + } + } +} + +/// Retransmit a member's first message with an alternate valid signature encoding. +fn alternate_signature(out: &mut Outbox, member: u16) { + let key = out.key(member); + let mut copy = out + .messages + .iter() + .find(|m| sender(m) == key) + .cloned() + .unwrap(); + copy.signature.push(0); + out.messages.push(copy); +} + +#[test] +fn different_signature_encoding_is_not_equivocation() { + // Dealer 1 (R1, R3) and recipient 2 (R2) retransmit with another signature + // encoding. Recipient 2 needs dealer 1's answer, so a retransmitted R2 + // counted as conflicting would also drop its complaint. + let verifier = Malleable(Signers::new()); + let run = drive(fixture(Reshare, 2, 3, 0xbb), &verifier, |out| { + if out.round() == 1 { + let forged = bad_dealing(out, 1, 2); + out.replace(1, &forged); + } + let member = if out.round() == 2 { 2 } else { 1 }; + alternate_signature(out, member); + }); + for record in &run.transcript.rounds { + assert_eq!(record.messages.len(), 4); + } + let verified = reshare_verify_transcript( + &run.fixture.ceremony, + &run.fixture.current, + &run.transcript, + &verifier, + ) + .unwrap(); + assert!(verified.excluded.is_empty()); + assert_eq!(verified.dealers, dealers(&[1, 2])); + for j in [1, 2, 3] { + let outcome = &run.outcomes[&j]; + assert!( + outcome.is_ok(), + "receiver {j}: {:?}", + outcome.as_ref().err() + ); + assert_eq!(view(&outcome.as_ref().unwrap().1), view(&verified)); + } + let published = ReshareAnswers::decode(run.answers[&1].as_ref().unwrap()).unwrap(); + assert_eq!( + published + .answers + .iter() + .map(|(j, _)| *j) + .collect::>(), + dealers(&[2]) + ); +} + +#[test] +fn dealer_answers_only_for_its_accepted_commitments() { + // A dealer that dealt twice in one ceremony (a retry without the + // integration marker): the owner closes R1 with the second dealing while + // the answering state holds the first polynomial. It publishes no answers + // and is excluded with MissingAnswer. + let run = run(Reshare, 2, 3, 0xbc, |out| match out.round() { + 1 => { + let payload = { + let f = out.fixture(); + let mut rng = ChaCha20Rng::from_seed([0xbd; 32]); + let (_, second) = + reshare_deal(&f.ceremony, &f.current, &f.shares[&1], &mut rng).unwrap(); + second.body()[41..].to_vec() + }; + out.replace(1, &payload); + } + 2 => out.replace(3, &complaints(&[1])), + _ => {} + }); + for j in 1..=2 { + assert!(r2_dealers(&run, j).is_empty(), "recipient {j}"); + } + assert_eq!(run.answers[&1].as_ref().unwrap().as_slice(), &EMPTY_ANSWERS); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingAnswer)]) + ); + assert_eq!(output.dealers, dealers(&[2, 3])); +} + +// ------------------------------------------------------- public verifier + +#[test] +fn verifier_recomputes_from_authenticated_records_only() { + let run = run(Reshare, 2, 3, 0xb8, |out| { + if out.round() == 1 { + let forged = bad_dealing(out, 1, 2); + out.replace(1, &forged); + } + }); + let output = agreed(&run, &[1, 2, 3]); + let f = &run.fixture; + let verify = + |t: &ReshareTranscript| reshare_verify_transcript(&f.ceremony, &f.current, t, &f.signers); + assert_eq!( + verify(&run.transcript).unwrap().transcript_hash, + output.transcript_hash + ); + let mut tampered = run.transcript.clone(); + tampered.rounds[1].messages[0].signature[0] ^= 1; + assert!(verify(&tampered).is_err()); + let mut swapped = run.transcript.clone(); + swapped.rounds.swap(1, 2); + assert!(verify(&swapped).is_err()); + let mut missing = run.transcript.clone(); + missing.rounds.pop(); + assert_eq!(verify(&missing).err(), Some(Error::InvalidRound)); + let mut extra = run.transcript.clone(); + extra.rounds.push(run.transcript.rounds[2].clone()); + assert_eq!(verify(&extra).err(), Some(Error::InvalidRound)); + let other = fixture(Reshare, 2, 3, 0xb9); + assert!(reshare_verify_transcript( + &other.ceremony, + &other.current, + &run.transcript, + &f.signers + ) + .is_err()); + let (newer, _) = newer_certificate(f); + assert_eq!( + reshare_verify_transcript(&f.ceremony, &newer, &run.transcript, &f.signers).err(), + Some(Error::ContextMismatch) + ); +} + +struct Refusing; +impl DidSignatureVerifier for Refusing { + fn verify(&self, _: &Did, _: &[u8], _: &[u8]) -> Result<(), SignatureError> { + panic!("size bound must precede signature verification") + } +} + +#[test] +fn transcript_bound_of_4_mib_precedes_verification() { + let f = fixture(Reshare, 2, 3, 0xba); + let record = |copies: usize| DkgRoundRecord { + closure: SignedStatement { + bytes: vec![0; 98], + signature: vec![0; 64], + }, + messages: vec![ + SignedStatement { + bytes: vec![0; 65_536], + signature: vec![0; 64], + }; + copies + ], + }; + // 64 × (65 536 + 64) + 98 + 64 bytes exceeds 4 194 304. + let over = ReshareTranscript { + rounds: vec![record(64)], + }; + assert_eq!( + reshare_verify_transcript(&f.ceremony, &f.current, &over, &Refusing).err(), + Some(Error::LimitExceeded) + ); + let split = ReshareTranscript { + rounds: vec![record(32), record(32)], + }; + assert_eq!( + reshare_verify_transcript(&f.ceremony, &f.current, &split, &Refusing).err(), + Some(Error::LimitExceeded) + ); + let under = ReshareTranscript { + rounds: vec![record(63)], + }; + let result = reshare_verify_transcript(&f.ceremony, &f.current, &under, &f.signers); + assert!(result.is_err()); + assert_ne!(result.err(), Some(Error::LimitExceeded)); +} + +// --------------------------------------------- within-budget enumeration + +/// Faulty old dealer model (R1/R3); `target` is an honest recipient. +fn misdeal(out: &mut Outbox, m: u16, behaviour: u8, target: u16) { + let present = out.has(m); + match (out.round(), behaviour) { + (1, 1) => out.remove(m), + (1, 2) if present => { + let mut p = out.payload(m); + p.pop(); + out.replace(m, &p); + } + // 3: bad value, honestly answered; 4: bad value, R3 withheld. + (1, 3) | (1, 4) if present => { + let forged = bad_dealing(out, m, target); + out.replace(m, &forged); + } + (1, 5) if present => { + let mut deal = ReshareDeal::decode(&out.payload(m)).unwrap(); + deal.commitments[0] = G2::generator(); + out.replace(m, &deal.encode()); + } + (1, 6) if present => { + let second = bad_dealing(out, m, target); + out.push(m, &second); + } + (3, 4) => out.remove(m), + (3, 7) => out.replace(m, &answer(target, 5)), + _ => {} + } +} +const DEALER_BEHAVIOURS: u8 = 8; + +/// Faulty new recipient model (R2); `targets` are honest dealers. +fn miscomplain(out: &mut Outbox, m: u16, behaviour: u8, targets: &[u16]) { + match (out.round(), behaviour) { + (2, 1) => out.remove(m), + (2, 2) => out.replace(m, &complaints(&targets[..1])), + (2, 3) => out.replace(m, &complaints(targets)), + (2, 4) => out.replace(m, &[1, 0]), + (2, 5) => out.push(m, &complaints(targets)), + _ => {} + } +} +const RECIPIENT_BEHAVIOURS: u8 = 6; + +const ENUMERATION_SEEDS: [u8; 2] = [0x01, 0x9d]; + +// Coverage: every behaviour assignment of the in-tree `misdeal`/`miscomplain` +// models for (a) old 2-of-3 → new 2-of-3 with faulty old dealer 3 and faulty +// recipient 3, under two seeds (2 × 8 × 6 = 96 ceremonies), and (b) old 3-of-5 +// → new 3-of-4 with faulty old dealers 4, 5 (unordered behaviour pairs) and +// faulty recipient 4 in three behaviours (36 × 3 = 108 ceremonies). Faulty +// dealers are placed first in closed R1, so they are selected whenever they +// stay eligible. Every case keeps at least t valid old dealers; the +// below-t refusal is covered by its own tests above. Honest messages are +// always delivered; this is not a claim about arbitrary behaviour. + +/// One completion run: honest receivers and the verifier agree, only faulty +/// dealers are excluded, D is the first t eligible dealers in closed order and +/// the master key is the current one (`agreed`). +fn within_budget_agreement( + old: (u16, u16), + new: (u16, u16), + seed: u8, + dealer_behaviours: &[u8], + recipient_behaviour: u8, +) { + let (old_t, old_n) = old; + let (t, n) = new; + let faulty_dealers: Vec = (old_n + 1 - dealer_behaviours.len() as u16..=old_n).collect(); + let order: Vec = faulty_dealers + .iter() + .copied() + .chain(1..=old_n - dealer_behaviours.len() as u16) + .collect(); + assert!(old_n - faulty_dealers.len() as u16 >= old_t); + let (current, shares) = dealer_genesis(old_t, old_n); + let dids: Vec = (1..=n as u8).collect(); + let f = fixture_on(current, shares, Reshare, t, n, seed, &dids); + let run = run_with(f, |out| { + if out.round() == 1 { + out.order(&order); + } + for (m, behaviour) in faulty_dealers.iter().zip(dealer_behaviours) { + misdeal(out, *m, *behaviour, 1); + } + miscomplain(out, n, recipient_behaviour, &[1, 2]); + }); + let honest_receivers: Vec = (1..n).collect(); + let label = format!("{dealer_behaviours:?}/{recipient_behaviour}"); + let output = agreed(&run, &honest_receivers); + assert!( + output + .excluded + .keys() + .all(|i| faulty_dealers.contains(&i.get())), + "{label}" + ); + let expected: Vec = order + .iter() + .map(|i| idx(*i)) + .filter(|i| !output.excluded.contains_key(i)) + .take(usize::from(old_t)) + .collect(); + assert_eq!(output.dealers, expected, "{label}"); +} + +#[test] +fn every_within_budget_fault_pattern_with_t_valid_dealers_agrees_2of3() { + for seed in ENUMERATION_SEEDS { + for dealer in 0..DEALER_BEHAVIOURS { + for recipient in 0..RECIPIENT_BEHAVIOURS { + within_budget_agreement((2, 3), (2, 3), seed, &[dealer], recipient); + } + } + } +} + +#[test] +fn every_within_budget_fault_pattern_with_t_valid_dealers_agrees_3of5_to_3of4() { + for first in 0..DEALER_BEHAVIOURS { + for second in first..DEALER_BEHAVIOURS { + for recipient in [0, 3, 5] { + within_budget_agreement((3, 5), (3, 4), 0x5e, &[first, second], recipient); + } + } + } +} + +// ------------------------------------------------------------------ vectors + +fn hex_statement(s: &SignedStatement) -> Value { + json!({"body": hex::encode(&s.bytes), "signature": hex::encode(&s.signature)}) +} + +fn reason(r: &ExcludeReason) -> Value { + match r { + ExcludeReason::MissingDeal => json!({"reason": "MissingDeal"}), + ExcludeReason::MalformedDeal => json!({"reason": "MalformedDeal"}), + ExcludeReason::Equivocated { round } => json!({"reason": "Equivocated", "round": round}), + ExcludeReason::Complaints => json!({"reason": "Complaints"}), + ExcludeReason::MissingAnswer => json!({"reason": "MissingAnswer"}), + ExcludeReason::InvalidAnswer => json!({"reason": "InvalidAnswer"}), + } +} + +fn output_json(o: &ReshareOutput) -> Value { + let n = o.public.threshold().n(); + json!({ + "dealers": o.dealers.iter().map(|i| i.get()).collect::>(), + "excluded": o.excluded.iter().map(|(i, r)| { + let mut entry = reason(r); + entry["member"] = json!(i.get()); + entry + }).collect::>(), + "threshold": o.public.threshold().t(), + "members": n, + "mpk": hex::encode(o.public.master_public_key().encode()), + "commitments": o.public.commitments().map(hex::encode).collect::>(), + "public_shares": (1..=n).map(|j| json!({ + "member": j, + "point": hex::encode(o.public.public_share(idx(j)).unwrap()), + })).collect::>(), + "transcript_hash": hex::encode(o.transcript_hash.as_bytes()), + }) +} + +fn case(name: &str, description: &str, run: &Run) -> Value { + let f = &run.fixture; + let current = f.current.body(); + let inputs = json!({ + "current_certificate": { + "body": hex::encode(current.encode()), + "owner_signature": hex::encode(f.signers.sign(&f.current.anchor().expected_owner, ¤t.owner_statement())), + }, + "ceremony": hex_statement(&f.statement), + "ceremony_digest": hex::encode(f.ceremony.digest()), + "initial_transcript": hex::encode(reshare_initial_transcript(&f.ceremony).as_bytes()), + "rounds": run.transcript.rounds.iter().map(|r| json!({ + "closure": hex_statement(&r.closure), + "messages": r.messages.iter().map(hex_statement).collect::>(), + })).collect::>(), + }); + match run.verify() { + Ok(output) => { + json!({"name": name, "description": description, "operation": "reshare_verify_transcript", "inputs": inputs, "outputs": output_json(&output)}) + } + Err(error) => { + json!({"name": name, "description": description, "operation": "reshare_verify_transcript", "inputs": inputs, "error": format!("{error:?}")}) + } + } +} + +fn vectors() -> Value { + let honest_run = honest(Reshare, 3, 4, 0xc1); + let answered = run(Refresh, 2, 3, 0xc2, |out| { + if out.round() == 1 { + let forged = bad_dealing(out, 1, 2); + out.replace(1, &forged); + } + }); + let constant = run(Reshare, 2, 3, 0xc3, |out| { + if out.round() == 1 { + let mut deal = ReshareDeal::decode(&out.payload(1)).unwrap(); + deal.commitments[0] = G2::generator(); + out.replace(1, &deal.encode()); + } + }); + let capped = run(Reshare, 2, 3, 0xc4, |out| match out.round() { + 1 => out.order(&[3, 1, 2]), + 2 => { + out.replace(1, &complaints(&[3])); + out.replace(2, &complaints(&[3])); + } + _ => {} + }); + let insufficient = run(Reshare, 2, 3, 0xc5, |out| { + if out.round() == 1 { + out.remove(2); + out.remove(3); + } + }); + let cases = vec![ + case("reshare_2of3_to_3of4", "All old dealers and new recipients follow the protocol; dealers 1 and 2 are selected.", &honest_run), + case("refresh_2of3_answered_complaint", "Dealer 1's value for recipient 2 fails the Feldman check; recipient 2 complains and dealer 1's valid answer replaces it.", &answered), + case("reshare_wrong_constant", "Dealer 1's constant coefficient is not its old public share; dealers 2 and 3 are selected.", &constant), + case("reshare_t_prime_complaints", "Dealer 3 deals first; recipients 1 and 2 complain against it, so it publishes no answers and is excluded.", &capped), + case("reshare_insufficient_dealers", "Only dealer 1 publishes R1.", &insufficient), + ]; + let signers: Vec<_> = (0..=4u8) + .map(|i| { + json!({ + "did": Signers::did(i).as_str(), + "ed25519_public_key": hex::encode(ed25519_dalek::SigningKey::from_bytes(&[i; 32]).verifying_key().to_bytes()), + }) + }) + .collect(); + let anchor = Signers::new().anchor(); + json!({ + "format_version": 1, + "suite": "BLS12-381 G2 Feldman redistribution; RFC 9180 base mode DHKEM(X25519, HKDF-SHA256)/HKDF-SHA256/ChaCha20-Poly1305 dealings; Ed25519 test signatures", + "spec_sections": ["3.2", "3.3", "3.8", "3.10", "S0.4 redistribution appendix"], + "encoding": "hex; statements are exact signed bytes; scalars 32-byte big-endian; G2 96-byte compressed", + "seeds": {"ceremonies": [0xc1, 0xc2, 0xc3, 0xc4, 0xc5]}, + "dst": { + "reshare_start": String::from_utf8_lossy(RESHARE_START), + "commitments": String::from_utf8_lossy(dst::COMMITMENTS), + "dealing": String::from_utf8_lossy(dst::DEALING), + "ceremony_auth": String::from_utf8_lossy(dst::CEREMONY_AUTH), + "ceremony_msg": String::from_utf8_lossy(dst::CEREMONY_MSG), + "round_close": String::from_utf8_lossy(dst::ROUND_CLOSE), + "transcript": String::from_utf8_lossy(dst::TRANSCRIPT), + "network_cert": String::from_utf8_lossy(dst::NETWORK_CERT), + "cert_statement": String::from_utf8_lossy(dst::CERT_STATEMENT), + }, + "anchor": {"owner": anchor.expected_owner.as_str(), "network": anchor.network.as_str()}, + "signers": signers, + "cases": cases, + }) +} + +#[test] +fn reshare_vectors_regenerate_byte_for_byte_and_replay() { + let encoded = format!("{}\n", serde_json::to_string_pretty(&vectors()).unwrap()); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("vectors/v1/reshare.json"); + if std::env::var("TC_THRESHOLD_REGEN_VECTORS").as_deref() == Ok("1") { + std::fs::write(&path, &encoded).unwrap(); + } + let published = std::fs::read_to_string(&path).unwrap(); + assert_eq!(published, encoded); + + let file: Value = serde_json::from_str(&published).unwrap(); + let signers = Signers::new(); + let bytes = |v: &Value| hex::decode(v.as_str().unwrap()).unwrap(); + let statement = |v: &Value| SignedStatement { + bytes: bytes(&v["body"]), + signature: bytes(&v["signature"]), + }; + for case in file["cases"].as_array().unwrap() { + let inputs = &case["inputs"]; + let current = verify_certificate( + &signers.anchor(), + &SignedCertificate { + body: CertificateBody::decode(&bytes(&inputs["current_certificate"]["body"])) + .unwrap(), + owner_signature: bytes(&inputs["current_certificate"]["owner_signature"]), + }, + &signers, + ) + .unwrap(); + let ceremony = + AuthorizedCeremony::verify_for(¤t, &statement(&inputs["ceremony"]), &signers) + .unwrap(); + assert_eq!( + reshare_initial_transcript(&ceremony).as_bytes().to_vec(), + bytes(&inputs["initial_transcript"]) + ); + let transcript = ReshareTranscript { + rounds: inputs["rounds"] + .as_array() + .unwrap() + .iter() + .map(|r| DkgRoundRecord { + closure: statement(&r["closure"]), + messages: r["messages"] + .as_array() + .unwrap() + .iter() + .map(statement) + .collect(), + }) + .collect(), + }; + match reshare_verify_transcript(&ceremony, ¤t, &transcript, &signers) { + Ok(output) => assert_eq!(output_json(&output), case["outputs"], "{}", case["name"]), + Err(error) => assert_eq!(format!("{error:?}"), case["error"].as_str().unwrap()), + } + } +} diff --git a/tinycloud-threshold/vectors/v1/reshare.json b/tinycloud-threshold/vectors/v1/reshare.json new file mode 100644 index 00000000..54531cf8 --- /dev/null +++ b/tinycloud-threshold/vectors/v1/reshare.json @@ -0,0 +1,583 @@ +{ + "anchor": { + "network": "tinycloud:test:certificates", + "owner": "did:key:tc2222-fixture-0" + }, + "cases": [ + { + "description": "All old dealers and new recipients follow the protocol; dealers 1 and 2 are selected.", + "inputs": { + "ceremony": { + "body": "0102c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c10000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300104e8e9d6ee5ed5afd83017d9ef4b0fe3c7dd7e11cce8ef81e0ce5396575dd85f000300040000000000000000000000000000000100040001000000186469643a6b65793a7463323232322d666978747572652d314e807b88485fd40f731e0fe68478137b4700566f5ace427241fb8719865afd3f0002000000186469643a6b65793a7463323232322d666978747572652d32b9b2ac6a8538435489c4bb66ab0301199f1b7ebd9709d6106a6d8eaf184b85170003000000186469643a6b65793a7463323232322d666978747572652d33ca269189f3853866684958df4b906299e4a2d52160db93ece22d1bc5535632370004000000186469643a6b65793a7463323232322d666978747572652d349cf916833ec29c63f81ed231263bdf22fc7f37ebf2549c4cf5a81e78b27ad41c", + "signature": "18fe45775f90e319111f320d86faa73e1c201b6b021f46af4b3144b64ed880c528d8d1c5380b4fe5ce5fe599285e746c355b1984c2725a2e6fea75bbb722df08" + }, + "ceremony_digest": "588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b", + "current_certificate": { + "body": "010000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300000000000000000000000000000000000020002000300030001000000186469643a6b65793a7463323232322d666978747572652d310002000000186469643a6b65793a7463323232322d666978747572652d320003000000186469643a6b65793a7463323232322d666978747572652d33b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f260002b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26804e74038644dc4cb66863b4c0aee636e5135b8807a02eb8ad69df866ce621a10522a9a9c6113800bf154484c290406d0995867148eb0cfce714a54edd74c5866aaca1cdcd2b0b697f98d175c51156b727d1dcce86294f4cc2484d57e168831b33333333333333333333333333333333333333333333333333333333333333330000", + "owner_signature": "8b2ba369370c6e657546de5be183154a0b656aaaa3e3bbf80ee03f93cfaa5201371469f8a37ebc8faff966d7a25c4566ea83792c271f7d159ad1b1c900843903" + }, + "initial_transcript": "a1faec76f0c3682339455e482ec3d3daf474ed430842529817810fe5db845780", + "rounds": [ + { + "closure": { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b015933d0148e69699dd369b85bd0c129bdaf0feca979c8714f2e99e9d1789fbb5fa1faec76f0c3682339455e482ec3d3daf474ed430842529817810fe5db845780", + "signature": "0cc54c1c585bb0327b83776ed28bc0178f01ef276c4ac8f080de78b92b335a4cdd565eb4edcb457319b272cdd06b85b93b3d283393d7c6494195274e9b6b0c0d" + }, + "messages": [ + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0102000100000291010003a8e6fe5955d6d69b2ba5275c14faad458e2a9eb5ebd4c6bc6e91e2340efc8ef64954bd8bf23b47fdd9c870080501ea2f061edb1e5dd207594048b2a7e6bcce92c1b0e44d2a25097035d15d767443224e1541a518535a01244b383115bf8ee2db92dc8957a0a3adaf169f2c6b9dd84127511401fe15d739d5e0ba0afb50d8bd36c91c477a064fc38bcf61ba9da1ff370d06d1e2974ddb680a31a47d0d697c92f25487fef1e9004ff29c09287fa5e41eaca4bc7e3b26c23baf25cacc4b60fba95fb51200b2569e10fefd1da7305e5ff9e0558572455289b0696c5e98a96d6f030617c556e2f7a490528fa5acb26158221414565e7ca3cb4cbdedc5e9d6b14ff146b1a22f72a0c7ed2012f9a24b348af5b6c11a4b7f3d4dcdd5c38b733c7d7056dd000400010000005501c316841653acbf02db23e5f0f14e4bcd8b7fc251c8a79bae53c53a828921606100000030eeaf1ff4243bde4028589f5ded2feecd013bf28a673ed04891bac4a0a42b1954ac70d0252586c99ab02003992623cfd300020000005501d5b879c3b9826333423e407a4a1b4598f01b604ed009791b752445ea86a9f30a00000030c29a0f73546fa742051e8469b8d829302dda02c50b3aef334d140a77c115c7efea169131b85be8ea44b886dde945af0e00030000005501ca385d0cb82b355d801fcded67fd25488911ff4b857a8b29129b293dd57e3e5200000030001ab627b6000bf5ee9922f36bf46db09d8789a780d91302d93ce8914f69dc3061157c86d27017a99941c2846d372fde000400000055017e50716909ceb9c6a6d07205738ea3acd0a063d1dd3de542bb14dc9f80270b0e000000300fcbf45e67d0b5a3c1268cce14fe7c3899d21e36c8c04357d55dac437110c0693485daa5461cd1028b7b5013f51ee885", + "signature": "c951388746308351abd0366c39a5bf12407341318ba9dac81e875b6cfb0dc7142974243e5998ff5e8cc3e47c34b725e8fb1d2fcd32cba4b230af113886dfcd0a" + }, + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0102000200000291010003b6e92fe9a4a7aada5d4151e009ab0d35aab13cb579aeb7e7df7efff50206394be5de2e94149145193740621ad4b7382c0aff3791a0b888574045ee11061679cdfef9b081cc09794b60115f5181b07943816f0a39a2d54973c053d190c0261fc58f1e43af17d801bcb698c936187bae7ce0d3cb90bbe3ed80abdaea19a634e791fdf78c36fd449c93d758131adfab7584096ff60c84b923415c8b1656a03ca1947aa743c74cc95d578f2d8a9310941e0c625f229d8933a4805c1b5a0b374b70c6acdb841014c689aa132d7b38eaf54dd1f0c83ac5afd6f3018ea3d00a870a3b26d51e7945f307c06c4036b8019b1aaa761345051d0456e9f964e7ad1e82b6ede4495656158b64236f3845fd29710e2f12b34aab430642c3a74da484ab6a23e395000400010000005501ca6d005aa20ca708d59e79518ada8d8c6e31384b1a1bde350588b3603f66937d0000003039f3ce00c1cc0f09ac538bc682a6214ac121404ea2013b0d5c035e128054c895217649457bd26900136da3348b89fb2000020000005501b64a1dc5a17345e0053e6d8304cc670471ddc5a3147f84a375a997cadec62b17000000300c106beb33e934aef3ae29fbd0ce45de83a43623f081c6b41fe3182700bd57cc40f7da045196c52e43d8d9f91808644700030000005501d03af02bed7493fa240c284017b646c3fc919e85addc13b591add714ed52a74300000030080af8b335ed4f4ec604cf4a0574d563e89bf4ba6f191e04bd2b9e43f65770e60af1f6653b47b860b923c9690a57ece700040000005501d5cc6156629338c219c9021dcbc755d937c1441e3526eba5361c2c3a34b77e26000000308e9de6649c6d1d6a4e30d9b0b3f1dfea3b49b032131a598a64d5f242e27b8df1289644b2270910aaec9c36bfa2c53f4e", + "signature": "7bb80390b6a7031f75ae1276ddd7fb197f59caf62d3ab5423a6828cc04cd194140fd5659b81e24f3838118adf8b9649f53c3650c359eb8450a9b2db35fb9ba08" + }, + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0102000300000291010003acd61752315afa5019e2522faef66d5482eed81e18ca74ddb6df79a4cb40f4b074cb9c582deaa8f2744c0247deb9ebfe1673135cbf20b67132c87363f3706889a56f995fd23856bfa6fcdd0aebe437174b271d92afca73b14e377cff2a3caa42a1207a77fd82c7a20fd1935248a4a3539f43750fe38addc56769cfe00d4cb0ff0fb7c81ae04432ac090e15d8123bcac006b6bb2f4fa94452885c37c0f75fe60af0206750c1f7e6f32eaef09a9c0b95971c0b02befe1fcf7ef06821c97870f182957108f60821abbf1f2660c7b1f10d1a087d2ec876753a943d797e170668646c88b26095c9421c3298e294c46ea502af048da5e997e90d3ddb944adcb291ed16f121921cf7ac3f55a6936cac8c70d95c6c7426620d223f9648f5f3b079e289d50004000100000055012169926571a6b18a969044ee84699015b2d794373f526c6dda9914faf75cdc5f00000030f2272c9e05f63c15f8aa8f332df478e3000bdcc747053a44da441abbd37f0edcc04fc31f99d6a347cb9ca74cdb19e877000200000055014460e697817dcb37702b188e38a2549e931c6b8d14beaeec92a41bc8cc32ed21000000307ee48e47294ae174ff2116c37041e3c4af615e46677ed10dfbb5b67d506f2b62a643082b57e53d6ed3a677886a01b80d0003000000550104bb47150758f365cf6463cf1988641df9459f8b96bac76ee44c6bc60440413400000030cd767180aa198dc0796bb94780bb45224d582e01071a204b73c0ea5022a66525364e6b0da956f62eaefb86f606c6b92400040000005501c2470d9da7ddec951cc727dceecf9920b07fa519e427b739a31a89d07734390e000000303bb925d670e13134dad7a448bbe881ab293f0cdc49359f80b145a28086752d51aa1b854a08a2bf7832d280e450c37154", + "signature": "6ffa1de08759288c70aa6155796ef26c6c9a3d8450a3b0a79d49b83f7b39310b37b35fd6d89b3703dfbd5e7d2c84968f817d6e469ee22deaca03f1449aec5507" + } + ] + }, + { + "closure": { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b02e4d8709e2e6e981458395aa066d223cc2e50baf8c27ca8d69e026ea7d1719899b6bb1f8891a9e74460a2dfcae606f93e6cf3491cba282f9f0d6ad800db9af827", + "signature": "fc70975c307338882763bc312fa62e15d0d7d2e384c0dd443d479a94d2851bbb01576abed9bdf88916db52d7f70f4989dc14f3b9059c501644dafa1980dcc505" + }, + "messages": [ + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0201000100000003010000", + "signature": "b70fa6f850deec23ae9a02094a1e507310918abedae0b91b2f2c487d14398896afade582ceb6627606e8ae2787f6a8e686bb99e3e31fd7bcede640cf2be15e06" + }, + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0201000200000003010000", + "signature": "b1bc0be7ec098cf99feec1717e5a0086da0c122ab182bd425e90ef222455bc5ffab3da513368fc95b525588c48de456eb15180fef51e8a0626e7815335f0f205" + }, + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0201000300000003010000", + "signature": "4aa07a686b1f680a4cfcbdb91259c42da2ad2f4683cf19cdc96016aafcc4f2421a332cceb7e64f22625bece150936386512ac7db3e33e8e8959b43b697ffee05" + }, + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0201000400000003010000", + "signature": "89106659e420f06240783f9bca9ec9ebf8bcb6d2752f35bbd19b7624f5083db1a21150fbfc1e6c1963b3bc1d145de717c46c8181ef8fe8a9d1857210f0d5f503" + } + ] + }, + { + "closure": { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b03bd5c3957c19ad5b2cefe18058f064791d149be35c084b44168941871311fb1fcb6f3c60caf528fe7ed1d14db1e7be0efd38f4ad1e91415f7964d7bc42ae1c20a", + "signature": "eab38e4dcdb9f519c3ec88892b91ad6118a8dddf0e99c5ca357da0eb06d89c4f6aa32de8bf734b7d8b277e7fb4586451300b0762b1159a8de0385e895ae7b002" + }, + "messages": [ + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0302000100000003010000", + "signature": "09aa075d4e7da42b4c477cff3f58d7717504e34f8205454843fa4ed05b5f127c77d61cf8ac395561eefef2a79fbb1113fa16878d2448c4b4bc6ccdf31b3f7202" + }, + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0302000200000003010000", + "signature": "475d23fcd9e8e27291cd4cb35a214910c2b6f3fb573499e8591112fd65f81d9be79c641d6f68383d4dc89469eaec1afbd3e8b1b51f2da09e978a18126b313707" + }, + { + "body": "01588cb3fdb980a9fa8be2676a6ec25234277109ab1c18ad4f34a7824017973e7b0302000300000003010000", + "signature": "32eadf88878ae0a1c1b049319ac5ce238c7d1e542d1edbfccddc5463adfb1a84378da77ddbe74822aa7669c31e06dd164580322ea15897fa81a555970268e702" + } + ] + } + ] + }, + "name": "reshare_2of3_to_3of4", + "operation": "reshare_verify_transcript", + "outputs": { + "commitments": [ + "b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26", + "94c0c58dd5b6fa82901521627baaae1448791b178492f7298891e2a0fe3c8202ceb8de466ce4ba23705a1a30a5231f3013192653d6145072239928534a4afb09e667bf2c94e8c3eec7ae74a931c3cb24a6ef2e4f268538d314c6e3e965063cbd", + "a2d59b73c523e4876c72a1cac83e6ab4d5aeb4de613ac10b3c194be147d2b6d58a9226c888a6139c01e670b95cc8564015eb95722fd3e37e0e30329173496fa9b210d3e878c75115df76286968953261ee922762901721498f711b02d38d78cd" + ], + "dealers": [ + 1, + 2 + ], + "excluded": [], + "members": 4, + "mpk": "b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26", + "public_shares": [ + { + "member": 1, + "point": "8ec2edd2f7ee490ab300af92870cb7e71c7251f6cef51cda342e989b05a953fee462bf157d535a858e3921c2ec5153930fe4dae51296efd77a520b933f3c900cc7f82daffd8d61e9a6f93863c9e83b1f54e693053a0f7544548e02cb958ba7c9" + }, + { + "member": 2, + "point": "b777766ae33f2538b6af671d2114c35419482d1e0e18f214f711c6a31660dd2e3b8d04f797e83c064db2de91c887f03917bf153740a90661097b9b79d75ead4b346a699893e847f29d474df3eae2009ce1d772779e445feb168eca9a68746919" + }, + { + "member": 3, + "point": "aaad632e46192b8a7f2fe4eb85b194b34da184c639a0e3e8681cdc3d7fb0e10addc7ce7631baf3ed9bc44157538ec610106bf192f5157eb1329b04f72eace52e3fd892eed0820d73aa89b00bf53cb96226ba5159c7e723604b203589e25afd5e" + }, + { + "member": 4, + "point": "afbb5f10cce55c58f635f2a775f9afae862b8a2d199693a4f4685a1fc5096df8861956790309afabc12d76f37e2f9ea8156b7a7a4cf48e24d152473fc8ff4e660b918663b28a0d5d4ab3a63b013a55ef1967f2ad257f6fe319f852c96144d341" + } + ], + "threshold": 3, + "transcript_hash": "33d4ae33570dbd0c8cd5d14d875977b711fb137a8404ca4eddc8b742bee1fc2b" + } + }, + { + "description": "Dealer 1's value for recipient 2 fails the Feldman check; recipient 2 complains and dealer 1's valid answer replaces it.", + "inputs": { + "ceremony": { + "body": "0103c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c20000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300104e8e9d6ee5ed5afd83017d9ef4b0fe3c7dd7e11cce8ef81e0ce5396575dd85f000200030000000000000000000000000000000100030001000000186469643a6b65793a7463323232322d666978747572652d31e811a287267d00d49c5ddfc4e14ed7e891a4a1bb0cc37d32363635c750e498700002000000186469643a6b65793a7463323232322d666978747572652d32ffefb9e3906a8325ecc5a637efbb0271373af905f643204e6451be86808362070003000000186469643a6b65793a7463323232322d666978747572652d336250ac006cfb2cee51c74fafb3da37efd4219c90572d9874a93fc28c5c4d6f11", + "signature": "924f75edef67e11de2acf321af6b75f78342f767727feac19ce80a86b95dd5e29b6d515244adbaf64e46d5eea31e683a78cd2f554c2cb43db33177c58e786c00" + }, + "ceremony_digest": "605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f9", + "current_certificate": { + "body": "010000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300000000000000000000000000000000000020002000300030001000000186469643a6b65793a7463323232322d666978747572652d310002000000186469643a6b65793a7463323232322d666978747572652d320003000000186469643a6b65793a7463323232322d666978747572652d33b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f260002b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26804e74038644dc4cb66863b4c0aee636e5135b8807a02eb8ad69df866ce621a10522a9a9c6113800bf154484c290406d0995867148eb0cfce714a54edd74c5866aaca1cdcd2b0b697f98d175c51156b727d1dcce86294f4cc2484d57e168831b33333333333333333333333333333333333333333333333333333333333333330000", + "owner_signature": "8b2ba369370c6e657546de5be183154a0b656aaaa3e3bbf80ee03f93cfaa5201371469f8a37ebc8faff966d7a25c4566ea83792c271f7d159ad1b1c900843903" + }, + "initial_transcript": "3f52ab01d7067c4f7ed866fb1803b3442297c0ba481c45cfd9db9aaad9b0ff91", + "rounds": [ + { + "closure": { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f901c64c66d0be793ff254db224bc24a2a9673de84d0fbb1864deac00cbfbd200b3c3f52ab01d7067c4f7ed866fb1803b3442297c0ba481c45cfd9db9aaad9b0ff91", + "signature": "c0907648ee123004ed44df3bd6c476f1cd030b466bd4f17b4e61f8ddff6221ba69a1bc905bca6321fdd636ccc07eb210d63b1598a9a08f2d40947472f8f7bf0b" + }, + "messages": [ + { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f901020001000001d6010002a8e6fe5955d6d69b2ba5275c14faad458e2a9eb5ebd4c6bc6e91e2340efc8ef64954bd8bf23b47fdd9c870080501ea2f061edb1e5dd207594048b2a7e6bcce92c1b0e44d2a25097035d15d767443224e1541a518535a01244b383115bf8ee2db864decf95da94e7b2a212f89cf3423f35487e811d4043f35fd7d1e690262544ed2d0b3c11fcb16ea66077cb671d5a7270efd4b94797c52103467153e35f5317ca43bb7b161d9746ccf7de432223b5b6332e564969bbef439e9d8ff788d9284c3000300010000005501f40a37a6151f5ee1d4562d6ef1cc438210bd3b0e806a62ffe04d84247cb10c2700000030106ac9f330571c93cff59c5bf19b5dbab026b08885e1c0da84127ac5702e68a9997cb0bfb7cf97736fb7baa5795d65ea00020000005501013c34decb09b75df6583f62ed5c5ea76c39f6ba78411e410789ac7fe777246500000030fa76ad0c34d44fabc8634f782863d2c94a64f98d3d43db5e59eee4d2008c1e0694ff0256087adee7bd0193a41c04a3cc00030000005501cf462fa0028443c468c2c03fa81024bc480a43a5727cf27212368bac6cf2e55900000030128c3e4f55f180f4a79dc0dd203f764bfc1eb2420fdcb4a9c383dd33902d8579ade9e40f5c51c7a18ade1ef6cf4972b1", + "signature": "d3cfa9ad55c785f9713e9a73f1f8faac1c80a2d83a03d261f2c2ee97ab304cce4806f69fb6e94df67ff663c5286a7781d2277c65ec2f1f8c53ae805ef1faf703" + }, + { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f901020002000001d6010002b6e92fe9a4a7aada5d4151e009ab0d35aab13cb579aeb7e7df7efff50206394be5de2e94149145193740621ad4b7382c0aff3791a0b888574045ee11061679cdfef9b081cc09794b60115f5181b07943816f0a39a2d54973c053d190c0261fc5aa27d465ffe39a719560d4185204d059f644e19b272da885cb54a61e11a14fabfaef82b4edabbd4543f6daff54669a6c12b83a7b11cb5378c2b7b22fae2773e343be571478e6418187d4882d62d803f986982959d689b0aeb6a0bf25b7d12f82000300010000005501ccdf9843cd28828c679a26a0f6922e08081ee0edc5b66618007741ee62d30565000000309a9a0020a16e66a9c7139548b8ac1c1ee3e2efff3b26c9468f70c7333378966b1e73caf064acf83edf88f6338fdc6a0300020000005501a4ed899893de654461113d9b6c5f9cb16eb9af07477af1681f7567dc1388cd1e00000030e7844d2bdbb2d258988340a28342e108b1884fa4401b39e2b5f792047da989625b47301a75978654207534313010f2a600030000005501ad4ce81a3ac1ad2dba6e51a81c31ec7bc89b346a495d38b10d461d573aaaec61000000301a8eb91f1386ddded5bf6e923fc8a57b84bd500e6a2b83356982bcd07fff15a6468b6e56e0c41ee46ecb6a7fc649800c", + "signature": "ae0ce17cff2fe5f7c1f94859e070f76810d712e60b855cf7b321b5b5b1f19bfa818057e5c5c7dfd91ec191f8838a6c3ca06e3f492bc650da21af91e6b38f4e04" + }, + { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f901020003000001d6010002acd61752315afa5019e2522faef66d5482eed81e18ca74ddb6df79a4cb40f4b074cb9c582deaa8f2744c0247deb9ebfe1673135cbf20b67132c87363f3706889a56f995fd23856bfa6fcdd0aebe437174b271d92afca73b14e377cff2a3caa429951be7e8fe301d0d1a0eb67f105a3e7c720d3a3ec53f9f731ca6601cda85f7456ca1992b2922393347c34be1f47e585174c02779cde3489a219f9ac3a1eb46070fc04228165e56db26722556a89c20f1040de0330341a9e04fd35b7f20a21210003000100000055013634ddd3548a2c9d27abd3162a97f14bd4b4d20ce283569da4989f6a8d9a3b730000003030b430d21fa2f553c6d70d6611637829a8b6b7e31d1fc88196963b44ce2c7e97ce1d6925ab20789b2495afdec2c6a38600020000005501e885c09c63dd0290215c398019abee15165b712f3324a796bf26bec9e373f62a00000030620fdffdfb75b70910f10663b55ad0432c5413de40ac6b4b361be5becc1582c75efa978991bf3a188f3c1de309d667ac00030000005501b8f5b0a6f41f3165d5606b202091eca3a921c97ecaeb39b510eb7fda37930f7900000030a0bc04c1470e3ab5b96e97ec9e6e50aba90c3ca501bb66ae855bd6e17811b91f3e60ebe7967fb7713f6f0ed292bf5462", + "signature": "09ac35b49432e8ee0802fd10c126a69dae1c4a3125ffa37c8a0bdce20fb3c47b490ef55d4d02ff3eda4fa2302d519636a41a0e858c6c9bc7a61b243fa830550e" + } + ] + }, + { + "closure": { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f902f37c75b5f7d58abf0c245014259dbab4ba0d6d630f3669dafc4c2f1000e76a6cc33d906e57881f879fd9acd2955967d9dcf049582fa0cda89e9cd27fe288c48c", + "signature": "c442e73244a1685bfe928447f461d2a81b3971aa198ff944e2c6ac5dc5ee4ebf8aaabca2c72e34bfe99db99fef48fb05da10b6dc356dce43a3bb16d1ce2a8100" + }, + "messages": [ + { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f90201000100000003010000", + "signature": "cbdb2db57fb54a0f7d885e961a54860357ab430aa29a52ed102217023b891a4da67663cef2655a199d19a82d9b67e062b3b5dbc83a5127050a76ed80f5619e0d" + }, + { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f902010002000000050100010001", + "signature": "6fd09f82dea01f3723da9b890c78770a3cb5a77b51172a1812260eee098144c6d40fe2d1eec40a67086656ac6a78817f561118019d3e438adb85217b2bb4110c" + }, + { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f90201000300000003010000", + "signature": "aaf5803b3658a7c431de56ba4dd18d09c21d0f4bfebdef0ef6170d9493c0fda2b0a79b9f15a7f2b188b2502fb7dbc62cee71d87b9d43446fc2f1ef513f3b0f05" + } + ] + }, + { + "closure": { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f9031f163b123ba84e066777b5b785a403980d23379310e59ab64cd6b7e523f40c9ad48397d500d07f12fa5c0e8ff433b954b7e57b3d0b0cc3a8d42bc34fb3ae2a07", + "signature": "2392217ab49f12f72cb4d0fe85f4f4aafca8b48ff2436404984d908b97954c9ea0f4052b4f6cbc85646056cea3ae26f1591b360a16d5808877d90669f0d01507" + }, + "messages": [ + { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f903020001000000250100010002432d3dfeace4492fe619ee284e021f8b1a72d549526de652d59fc7b1c90eb692", + "signature": "0b9c6eae1c0e62d1ae0ec8f565ba7fb3cbeb945b8b23cceb328f09b90bed30b9d34a781184161c823aad28037df64740c29e7d11839033a197e3cd62b0ed190e" + }, + { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f90302000200000003010000", + "signature": "7b26f4c6f1a9d60872ce280611af43b7c46c6120b6b10653b257c1f35f1bfc16f048e93f600dbe77c921208bd6dc723941db83125a7921048f7607a2deed1402" + }, + { + "body": "01605735b5efc3f036ae2fef241308180b8d8de0d3a2ec92c94d63ca15a3cd65f90302000300000003010000", + "signature": "010f57356c64fafbfc4ad3504011c6e0a94ef98f3597d6a1cd3ed4a8fe92f9b79a339a30893fe6981fae852e2a464a5ccbe0d74ba31dc1d2c55d92c592ba560c" + } + ] + } + ] + }, + "name": "refresh_2of3_answered_complaint", + "operation": "reshare_verify_transcript", + "outputs": { + "commitments": [ + "b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26", + "a358b6938c00fd3bc073f27f8d934cd3296f085d0f3da453a98bd40b2f7b2c91a6e9e04fdee0727f9dd5d892d760e53e02c179bb984e3b15807932d087986bf53ae6ed14026806220856afa46600a97055fbf59d2c6a9a46b1e49198293df728" + ], + "dealers": [ + 1, + 2 + ], + "excluded": [], + "members": 3, + "mpk": "b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26", + "public_shares": [ + { + "member": 1, + "point": "86cc0a09e7dcff1e4bf7e5772f94766ab8a1116f1739990b2f8cc9f1ab1fce448a1232b9734bca36ddba97204d807ddf0da6a4cfa00e05eff058365c4cce7f7439b423d991b5630204a0f5f819c0159e86cd5b09ea69750fa6624db5fdfd54c4" + }, + { + "member": 2, + "point": "aa4de41cafd03a6e08a36b5ee35cd8ba3c5c6a254c037ab147fe2019e6d16cb859f4e855a9e4365f55dc2d4ffe582f36142c5e43190939723c311c104e3c1552d0295ef6534e907e72a268598f72c2ae525e59dc134185b71b60a292db0943bb" + }, + { + "member": 3, + "point": "94a6e1e07be755bf0aeb05b065849d35caa5e12e1b0e4510ff6f172c9f3e2c5ba54fcfe90f69253e7d0e40ab64bddb74047107dc65994efcc9a61b9b9e6e5fecb526a44f034f1471ccbe7c3ab9528605eea7094fbd44787a99400e87b59c62bd" + } + ], + "threshold": 2, + "transcript_hash": "f8ac44d40c220023914e90f28708b91b1271015ff28302da0a886b044893a0e2" + } + }, + { + "description": "Dealer 1's constant coefficient is not its old public share; dealers 2 and 3 are selected.", + "inputs": { + "ceremony": { + "body": "0102c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c30000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300104e8e9d6ee5ed5afd83017d9ef4b0fe3c7dd7e11cce8ef81e0ce5396575dd85f000200030000000000000000000000000000000100030001000000186469643a6b65793a7463323232322d666978747572652d31450653befd228cb8913be199de0f28f85d474eb539a40022c97770d776a885710002000000186469643a6b65793a7463323232322d666978747572652d32996130e65e1c3e7fd177adce516b4fc0b850dd91390c5497753db60e9aff805e0003000000186469643a6b65793a7463323232322d666978747572652d33a523e473e3238330cb86ee7dfb8ef896a0d7ecabfeac2d509a52bd7742230571", + "signature": "afa5f24e20b96b88b522e00eec1d8fc655c939ed4b5d8c2c7da7bccb3216f0d4999f68f69291203224a58ebd8e973b6e0cc3b77069115725d0cd25352c7cf90d" + }, + "ceremony_digest": "d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b", + "current_certificate": { + "body": "010000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300000000000000000000000000000000000020002000300030001000000186469643a6b65793a7463323232322d666978747572652d310002000000186469643a6b65793a7463323232322d666978747572652d320003000000186469643a6b65793a7463323232322d666978747572652d33b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f260002b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26804e74038644dc4cb66863b4c0aee636e5135b8807a02eb8ad69df866ce621a10522a9a9c6113800bf154484c290406d0995867148eb0cfce714a54edd74c5866aaca1cdcd2b0b697f98d175c51156b727d1dcce86294f4cc2484d57e168831b33333333333333333333333333333333333333333333333333333333333333330000", + "owner_signature": "8b2ba369370c6e657546de5be183154a0b656aaaa3e3bbf80ee03f93cfaa5201371469f8a37ebc8faff966d7a25c4566ea83792c271f7d159ad1b1c900843903" + }, + "initial_transcript": "55afba1086fbbc7e32a9716639aebd9ec81f0fb76f1c7f1f9fbf5b38e8141fd8", + "rounds": [ + { + "closure": { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b0182959afaeec2a53e3eeb33bcd7357e8fbe057bbd332cadc3639c57519c30f16855afba1086fbbc7e32a9716639aebd9ec81f0fb76f1c7f1f9fbf5b38e8141fd8", + "signature": "8197f0ef5454abdf0639081df2929974a3f258f2a0ce5b95643e1a97836a6686e56e96d8a932138efb3738a4780646426d72cb4cbd26db60f82b25f5e4e8c002" + }, + "messages": [ + { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b01020001000001d601000293e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb88bbe74a12fa18ab80276d106d8e04a9486cd5416e353c878883eaeacdf885a821159755a9aa55ebfa6ed8d55f17be8a200e1d6d199f275174ac76f0773b16ae0b10bfcead68d6e43be404602ce0c459646a5e442b9121591171890c1fe8852df0003000100000055015a8a7f5525fb3e89030073a76c4228c4c8d8982e4eae954f6324999b76970d03000000301c268f9fb9a9bef69e74bde6d520a2496f15db0e050afd57677d967dea673a4335318c2a70551177ba48a324abec606200020000005501330333b3ca11a367b64795a16c7e1d81a2baef2bcd31cf4ea690cc4cf347c60700000030f946560130ec5636540cee58c55051548d750fc9e34ec92b49eace07550909d2b116213cd5fa889c41cd5421679d970200030000005501fe8fa0437c85a4733e066948e9388704066334654adde9b8d337183180a7c11b000000303d9a9c42f57d02af781d2b668d6f2437d54cceba755dda92e61881062c889373d64f37afec0d3466e0b619e2bdf61ca2", + "signature": "26a7af69a3a8ad86a22444b1ffe28ddea8b7d23e2982b6341fb2c10c2951c1c026801915ab2b547efc5edf11e7adab656f2a583db0626935f1589b1509c0a805" + }, + { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b01020002000001d6010002b6e92fe9a4a7aada5d4151e009ab0d35aab13cb579aeb7e7df7efff50206394be5de2e94149145193740621ad4b7382c0aff3791a0b888574045ee11061679cdfef9b081cc09794b60115f5181b07943816f0a39a2d54973c053d190c0261fc5b192cdd22df16e5a5b12f18d0373fee24a913a3f45a14c61bca6713e02e34ed2d7c4a50f40cb64f4bc97f69cf035391f16c031c7649a03a217fa14e75f3c3c2763423a9aef67024c43bb97f19a197a2c4c5db2c8eb492d72bc8df67597dcff3b000300010000005501d65ded13d989cba7ea4470465b7eac38d652441c966a2054aebff493f702393a00000030709b6317485f29a2fe8fc6bac75032d41207f45ae6501670b6813454c28e9391bc73cde2542ae94c00418ea5fd66125c0002000000550143324bdf6d5fbb5692d4aa0a149b67feec5ed1f1b6a91c5083dfadebc57ef62b000000300144854288400c164ec72e05bdc0fede9d0311380d790be79fefac1ade44d7575497a108194586c566894276e13f102f00030000005501f1abed6ead04615715ac8ebcb606d697082772622e30993d9f4e59fa9a6e686c00000030d2c141798c33d3d20a3aa0374e85c054f74431693af4069c3d4b256b540d9dc1725699f2a3fd5407579dae5881aec653", + "signature": "2ee08f888947b7613fd165c9188032eab4b7f8f612b87837a8a445ff2aeb79c57a05eb3fed1a61e92d4aabb6cfbc44e17276a845c6688e38eb9a53f0a30c5601" + }, + { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b01020003000001d6010002acd61752315afa5019e2522faef66d5482eed81e18ca74ddb6df79a4cb40f4b074cb9c582deaa8f2744c0247deb9ebfe1673135cbf20b67132c87363f3706889a56f995fd23856bfa6fcdd0aebe437174b271d92afca73b14e377cff2a3caa4281699e4fbd4d9f9dc56a73d0fbb5d9804048e298c081797f6ffb929343c8bb7a9700b49df64c78b70af5ca31a0fe108319cc44b78494485738f55274cd869fd25ef4eb20fcc70c5772941e240bb6768ff8e015f40cf1e1f780a9d26bbce07fa4000300010000005501b99b54a227bd3dd1dd01f860cc4747500648d8310a152153260e697867ea9e5400000030c912c925e15ec581984b5c4e96b8cfd6f4a1e82f1c2af15dce9a2dc7eb8251619ccf191be7209f245a587af6e6fb067d00020000005501f55a58e6d5335066489b36ce3822f5048ac6b3dd62e5e00a16f21e03bb9df9580000003091975506ec7e5913bfe564c5a1b7b9445cd302e36da3608fe1fa53b5d5cfb1525e9c51ad61b2d2088cca86f6fbe8a1fb00030000005501947fc44fff72ce54cb61ae5fd157067421f796ab9e8cf288146b5e1642166d5500000030e4e892a19b9f83c48658f2391ce38c2901669e19ae16c10509ccf8c099ceb269be92f393f6fb2125a4c05746f5bbe0cf", + "signature": "b9e31e0c9b3d4fbcafc9a6f4c90ab9ce4c059e69716d0730ee5e5d38eb8e8711d1cb23583599b77b7ed2cfcce70ab80ef26094841196f8f6f0c9baf6926d0906" + } + ] + }, + { + "closure": { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b021e3f01fe667695d54710e1f7a1d5f64138560ff0d5926005641f273bd6f119f632a79ad2bb648fd73bc691e49782c815ee3a96a50f33560254babe12740e339c", + "signature": "a35a082062c63c9a54f034495abc6a15ed578fe843b76921dd074c28d71bbd56a9c4553692402e3edfea9dd76470c545cc453568bf517e8252715a0f3e7c370b" + }, + "messages": [ + { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b0201000100000003010000", + "signature": "d8f8725500bc2e84afa8c333cd9fcc932f5f1dce92bb804334783b3b35abb290a481bca31f03ad189a5d4f2fc3a84d35916c8847e5f6c0ba255f7029581e4d0c" + }, + { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b0201000200000003010000", + "signature": "7bb97aae412f6ee33e2afee82bdaa1190af899cb4a5d79bbee5c216c56b22d23e6fb46a1cc439521b0e9638f3742bc77a3517ab17e02fb4de7ede58c347fcd01" + }, + { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b0201000300000003010000", + "signature": "6ccb19e60d9dc09b35643ce37f7d296a9ec39b3755fcaf15f85cca3d25ccaedd076d720cd687b1d7b74a2bf3ac68467f649a75f775b410fdc16f92e1e8c51508" + } + ] + }, + { + "closure": { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b0321177bc323e0e2f5146c0d77c2623e9a6a61c670d2eafff0533550653a42271cc50d635aa022a94edc5fcfaf98fbe8372d838ed9651dc4f34076531f229c70e7", + "signature": "2500bfeec2e2ae87e7510ff7a8da94f20e7f5cea990cef7f09c069bf00438e78d16050838538b9a03236ce506f6d1f407a9212f7679162d88b078bd7e1667d00" + }, + "messages": [ + { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b0302000100000003010000", + "signature": "4af306588bfb2214c97ae593f30eb64a293257ade07df5cb5776a14a29387f3cf631c232cb7b82a11a82dcfec1c6ff46b334205c11f14ff8a6439a6c5437be0a" + }, + { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b0302000200000003010000", + "signature": "285741d3c47a80b2e206426b11d19858f85d344e0b109cd39ec9d663031c1e0bbb8994dc96ba7c584531443abf54e6d2228f6c39c00ddbaea57cc0f1624bb806" + }, + { + "body": "01d3d214c83813441757a1e4a20bfc5d59abcd1a28f344c9012ba6e95d3911934b0302000300000003010000", + "signature": "8273bb85c17e9260d6deb36161f009e6e7f50350b0516f78e86a24f679503c4814a295d381a2e5dee511b8156d3ec877de7da0c89039e236ce86dfab8e97550e" + } + ] + } + ] + }, + "name": "reshare_wrong_constant", + "operation": "reshare_verify_transcript", + "outputs": { + "commitments": [ + "b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26", + "a062a4c6494ee1a0d2a52be09e022fbf6331cfbe9b8643e4e0371b24434732cd14c88e6a3c1212e66a388b99748541230dc0001384d2b915d4dab240d163935adeffd8aee2a2e2ff8c7fa1aaefb340c14a76b79600cd7eb4e3a8134bbf64b4c8" + ], + "dealers": [ + 2, + 3 + ], + "excluded": [ + { + "member": 1, + "reason": "MalformedDeal" + } + ], + "members": 3, + "mpk": "b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26", + "public_shares": [ + { + "member": 1, + "point": "994d2c959b5ae16593904a9ce75bb0acb20f962ce7a7378c4ad946b93f1b4eccec96a6b5a29e94ae549117ecae2140960da70256b7175f8b7d71148873fb02f6b1fd8b9aad9cd897d1dd5e9a62732e9a26466c6bb52e60e179856c47d3f29e2b" + }, + { + "member": 2, + "point": "a342570a48556ef99d686f78b5a976ea6c4614017de6f6ea9575a0dda97ef149806c81c26d6d72c738b541ddd9c0a29813518ae8da0ce4b0f32356ee737a3d37fb9358664aec88b964eb751687e52c975dccaff89569d15aae4874d9857987fc" + }, + { + "member": 3, + "point": "953854162d463b835701ad2991051dff7878595eb5510379fb0f9ca907cb0ae7190895ef41b60cfd92efe5f61d2c601b02a932ef9d6c4a34c5e08de6b9fb15e20fdfe17e9f1a98c633d2768ec78e1b3954f8fc9dcfe6c648e7af8018d2cf0292" + } + ], + "threshold": 2, + "transcript_hash": "f6e1179faf4774e1fbfd45f1fcb55b8cce0c2fe892d0f406f97f1d3ef1b72dff" + } + }, + { + "description": "Dealer 3 deals first; recipients 1 and 2 complain against it, so it publishes no answers and is excluded.", + "inputs": { + "ceremony": { + "body": "0102c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c40000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300104e8e9d6ee5ed5afd83017d9ef4b0fe3c7dd7e11cce8ef81e0ce5396575dd85f000200030000000000000000000000000000000100030001000000186469643a6b65793a7463323232322d666978747572652d3136ba8707bf814dbf59ebb33d9e96035b6b2a23a9883773c29cbda62dee8e4a530002000000186469643a6b65793a7463323232322d666978747572652d32fe96920f0f14365ab172947caa049672cab194d9b13015507082a90ae2a28c1d0003000000186469643a6b65793a7463323232322d666978747572652d330a3bd8553bd6d9b4351fb29310d6ae84ce871a81ceaf813e741e6bd9ea5d4d65", + "signature": "bcea954e95e0657de3c5c46b36ef6e3a5376c8b36a24f456ee9a53b3ec217cc5574eba100742f2341ad0b65a7bb461705b352da25e224491726d5d576cccf80f" + }, + "ceremony_digest": "7aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f", + "current_certificate": { + "body": "010000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300000000000000000000000000000000000020002000300030001000000186469643a6b65793a7463323232322d666978747572652d310002000000186469643a6b65793a7463323232322d666978747572652d320003000000186469643a6b65793a7463323232322d666978747572652d33b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f260002b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26804e74038644dc4cb66863b4c0aee636e5135b8807a02eb8ad69df866ce621a10522a9a9c6113800bf154484c290406d0995867148eb0cfce714a54edd74c5866aaca1cdcd2b0b697f98d175c51156b727d1dcce86294f4cc2484d57e168831b33333333333333333333333333333333333333333333333333333333333333330000", + "owner_signature": "8b2ba369370c6e657546de5be183154a0b656aaaa3e3bbf80ee03f93cfaa5201371469f8a37ebc8faff966d7a25c4566ea83792c271f7d159ad1b1c900843903" + }, + "initial_transcript": "e3d33531c8f6332ed54c5f498c394a7ee51899af0c954f98ed0c2c4d29f17ca1", + "rounds": [ + { + "closure": { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f01d4c91214d8b4a2c8e5fd57df80436a48e830a713f1686d55eb0e6d7018310dafe3d33531c8f6332ed54c5f498c394a7ee51899af0c954f98ed0c2c4d29f17ca1", + "signature": "be3532c9ad326ae84d8205dcc01a65f0e10e49174b947a20ec5e863857328abc7295d967baa0e8e601b7a6497a7acf4b1006e36b146543c48ea6a13c8c613406" + }, + "messages": [ + { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f01020003000001d6010002acd61752315afa5019e2522faef66d5482eed81e18ca74ddb6df79a4cb40f4b074cb9c582deaa8f2744c0247deb9ebfe1673135cbf20b67132c87363f3706889a56f995fd23856bfa6fcdd0aebe437174b271d92afca73b14e377cff2a3caa428f5558280487a0c59d6306e1ee494be56d7e283d4432c882d9ac868dcc27a99476c4812928d43f08f5c2595290e05c9e0bae01e343d50b2971a627838f2403946e828d022f8e80fff90666f75f886eb24194bd5576a56337ab08046ac3148f36000300010000005501a1770e0fcb7d0a0dfed7cb52bece6fd5429c9e9d92f45d0b68406c3f0986b92a00000030da03a48f76e8939a5ae3e3be6ef292c281ae6792470739226cc82e0e705e2060f937763e02bfb8767741df4b696f947000020000005501d5f62fed9c60f1d6fa0169c0b8b3438742a5cf0874a02a64365b1f8ceef33f7b0000003004fa827d5e8959ab3f30f363c7d7fbb23705c7d5fcb46186b6978b43779e1466a5025e3c807a2876afd42a7bc29e9f8f000300000055014f73dae2fa091808475cbb4abd0b2e8a6ca81e1284661cc45aab345805996f14000000304afb26e616168b1f98d7aeb51dc26be62b14317b4289e43ee2ce2eaa8b112086eb68c8308b7c63256fe65f5fe2e40464", + "signature": "b16cfbdd6cdb956c501e6af26ddebc9432731e302f2383a26fb3223f3f4579a1d3ced850d0b03b3ac419f5f82afda3dee92be10d6689e3defc6e6f550efbbd02" + }, + { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f01020001000001d6010002a8e6fe5955d6d69b2ba5275c14faad458e2a9eb5ebd4c6bc6e91e2340efc8ef64954bd8bf23b47fdd9c870080501ea2f061edb1e5dd207594048b2a7e6bcce92c1b0e44d2a25097035d15d767443224e1541a518535a01244b383115bf8ee2dbaa1920532d5f2d1feb75c29a2adf63259487e7acdd62f5492f6c3b62c99ef6fddf7e43b6524a827231ec47709085e54a1961a077ab028b6bc07c565e12035a76b953df8b9fe55c447af0363465ecdc4d509ae53d0c5cc0180684d19258480ebc000300010000005501dc57fc5aa97706b04aec49f7f68e062a8adaaae07d065245fa61bceb91fde374000000309a8927f4243300229404d3c672f2976f782c91d0643c4783ebfcb9876dcec01e2c122ec1aa8a21265ec71b83eb79bd8f000200000055012ab1673e823d9826bc76ec7f6d3233e5b4623df4d90a01624b2cb85c2836894400000030c9b1105e15bced05b5b29acc3288c539a0b5ceeeb1148a1b95ea70c7720c05abe110ccaeddad5c11fcb08868fb50691b00030000005501c1eacf1127d051043ae4d5cc7d2cb5341bc6478e1918b2181d31a84140dd1663000000306298bff6f4438f73e1407c7ff4a133fdc0f4392f01cb99525acc3a72aecdc318a2709c1ceb9f1e34b5c26838b1f3405a", + "signature": "267b578840f112c2616d551f407944c03adf23bb643cc2b968a906bef1776df4336df2da6ec4c38921cd36bde03089f12d1a04491c2239663596867d538a5a0c" + }, + { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f01020002000001d6010002b6e92fe9a4a7aada5d4151e009ab0d35aab13cb579aeb7e7df7efff50206394be5de2e94149145193740621ad4b7382c0aff3791a0b888574045ee11061679cdfef9b081cc09794b60115f5181b07943816f0a39a2d54973c053d190c0261fc5a984e0d41edac682075ee1a549c457691bdb3a07f8db6dccfc91a67b0be0d3633dbc45d248be2a0cdfc0e2f325d202820056d531aa0c783801cc75484431bb00b54a5e4a92a3267e5747953c2142a66614d83d30748b036f894b36d993770098000300010000005501acc41c2cb774285223bf8c041f8ac98b6170e2063d057167b58b06f58d8d147e000000301cb563b090f578979f6c7e835a8063155dce89ef056208125985c30e22e5657c4f72653a76e4828d3b25e83e74658cfb000200000055012ae0df251b7999bb0bebc29330c49dd929ca7dbb9bcf18904282042bcb4aac2800000030c22190ac98d5fae207878f4b09342071ee80adc14ef973a9779008015c4844a4db56e45408c257b180f6bbe1f3e8e36600030000005501eeb02c4e026aae03439d464b3731763b431a7d8d5aeeb5c039063b3a9583e909000000307e41101e538c9eb55910ef58112475b46289ee40cfaefac0b29cd25899abdbff55681749c64380c90d5d1078f024b1bf", + "signature": "d0f31160bb7855727add4df90be12a3787f25669719c159cd42702c95862978b98b2630439b825429d3baeea62e459efd0a39e9fb2abaf931615e2ac3a797b00" + } + ] + }, + { + "closure": { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f021ebb99959bf6e0ea7351c58cbcb44ee1b91292f174f36e802e8a149d87025917ac66b64212d34e93bd19c9e178c5bb91b11130d7837aa2d94f5b580022c5cf3b", + "signature": "4e19174028c3d1a53c8af3032f38c2a9689d2b3d0880b02a422ec2cd7c259343100257ef2135414df6bad73fb91ad9dd33bbca50ebddb741b6e0b2621d18ce0f" + }, + "messages": [ + { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f02010001000000050100010003", + "signature": "bc4f8200a73411adcc9cf47b650b90273cdbf6e28228d16ba08397cee68a22e88ac5659438b4f8d6b8bbb04af95b6b56b5928e2452bbd8064c7e4526bfe01105" + }, + { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f02010002000000050100010003", + "signature": "bb63bd325c56ec98c3f60c4fd8f9aa841529c187831b44a68357209a9bf9106d944569e24ff505340ff221d49748836fe004a6dbbef2d2d4eb0610734a1d3c0a" + }, + { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f0201000300000003010000", + "signature": "b96e537d664d56087b788586019d2c6c05fd7d952b424a45ef402c1868e079265392c238103bbbfae2f2fca0e6bb8a025ce38596b0c06f05fd3dc2db30515b05" + } + ] + }, + { + "closure": { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f033d7b81d8d0aedae8fa04b830f040e227144f66578e6a828b540b75cf966d55d136e3382eee8108ed7ed5ddd2adf72ce3a9ca4ee047bd8711c49ca613cca93f24", + "signature": "01a59c2ba70dfedc31c51d2951d0ded44ea8312c7fbe78d47df89fd4c4db995c170d589cb55feeaa398a4171bb5475dc79a9163cc81a41d35377a95f31f2dc00" + }, + "messages": [ + { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f0302000100000003010000", + "signature": "ee3d6356de0c41bf11e041a40708c3dde1ed41960972320d61634bc00486e2ed15a7d844c5ecb6628d17db39ad70332a99e3c526e77a1f08548cd9229b390e0e" + }, + { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f0302000200000003010000", + "signature": "3c3845e4162dd797543c83ee83f17df947c844ebc398f7d917ba5183f2affa90469f739033c282ade5dd939bebb0bb28618ce46d0f25dc43bf7462ea0ad6bb0c" + }, + { + "body": "017aa3fc1c35b751e6bacd2fdaf5f079929574a2827c2cbaf1edeec07118afb58f0302000300000003010000", + "signature": "528afb56d04b78ecdba270d11deba3741eaede013d9fc373f3e99a2e99433f42bcaa0c0d465efa01f8777df2797a402d7ac32ba893c54a57279f5a210618280c" + } + ] + } + ] + }, + "name": "reshare_t_prime_complaints", + "operation": "reshare_verify_transcript", + "outputs": { + "commitments": [ + "b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26", + "80fbf1a1fbce9fb2d14ce83c88437d2132cc72ebc43c440c46df0e3cb9505812ab08e457a7ff1bd8e521c018d8e5ce0d18d09ef3ce9ec94ea076f48e2453e18f9ce0824be7099af9abd2d9e6dc000611f8a819f9dae08c79440d281f0ad6597e" + ], + "dealers": [ + 1, + 2 + ], + "excluded": [ + { + "member": 3, + "reason": "Complaints" + } + ], + "members": 3, + "mpk": "b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26", + "public_shares": [ + { + "member": 1, + "point": "b78e3df46566f1a8e5293c0d0a4478a4602c3c44b230b57f1e06e309c58a270d87156adb2536b02faff8194254f0fb7601e75d52668046c41317251018699618e8b914d14d229181928ee968e4b538ab6cd12bc473a5be6bcb32387c693186ac" + }, + { + "member": 2, + "point": "80790fb2a85ac5b53421e3dbe52a7bf2c0e3c9cfe9946172fd8c52f5064350b53c12cb4ef55df1ab859d7694d810d1f8130a058d8369cb31ce4a65da78daf7dc7a8c5933ed005515b23b0f33592c3a1b9929d19206f90b17cd1b0bdc4de14218" + }, + { + "member": 3, + "point": "a2e0614d425efb5f52f78f09420670b2765143db2e111d665abac5e943c48891894884bbc002384252189ab60a96912e07f77a564bef7f5685daded79e7291bd33f4f55361c1b71b8ba7d0878aabfeb0e9952f4b1ac27c38a239b082f3bf9784" + } + ], + "threshold": 2, + "transcript_hash": "26a72fee9b4e4ff2310f36980c99331e17a4b84a8ecf8beec84ea98d740a645b" + } + }, + { + "description": "Only dealer 1 publishes R1.", + "error": "InsufficientQualified", + "inputs": { + "ceremony": { + "body": "0102c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c50000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300104e8e9d6ee5ed5afd83017d9ef4b0fe3c7dd7e11cce8ef81e0ce5396575dd85f000200030000000000000000000000000000000100030001000000186469643a6b65793a7463323232322d666978747572652d315db0a9e55cf06d4b779c04a6787a29bb6245f8c9789e9015660b1129f93a2e170002000000186469643a6b65793a7463323232322d666978747572652d325c5a8527ce72fc225ace57fc6a29cfbdfbcfb00ea5d750fef977516d8c9577650003000000186469643a6b65793a7463323232322d666978747572652d33c280beeb8e6a51d00eb9dcb387c17181595d96dd9a95287d87f7809e2f24aa3d", + "signature": "7afb79a62d051dd1f991537122e8cce6b87957987f658e82f24135695369584ac9a3dbd1a4996a29045585e56c0608ac06c2159b7bae69028241ad1e18f06d01" + }, + "ceremony_digest": "68a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc", + "current_certificate": { + "body": "010000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d300000000000000000000000000000000000020002000300030001000000186469643a6b65793a7463323232322d666978747572652d310002000000186469643a6b65793a7463323232322d666978747572652d320003000000186469643a6b65793a7463323232322d666978747572652d33b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f260002b8843eef09af04752ff7fb7311e00e8245d132fecd6d7191d801799e359e505072faf996c3a22b85bbf41e92bdac244a00e157246cacd65288575147db7a5a467dd56fa826076ae55e1007ba369872fb4d56d5c4052dcf96fdb27bc8003c5f26804e74038644dc4cb66863b4c0aee636e5135b8807a02eb8ad69df866ce621a10522a9a9c6113800bf154484c290406d0995867148eb0cfce714a54edd74c5866aaca1cdcd2b0b697f98d175c51156b727d1dcce86294f4cc2484d57e168831b33333333333333333333333333333333333333333333333333333333333333330000", + "owner_signature": "8b2ba369370c6e657546de5be183154a0b656aaaa3e3bbf80ee03f93cfaa5201371469f8a37ebc8faff966d7a25c4566ea83792c271f7d159ad1b1c900843903" + }, + "initial_transcript": "b3885c23f8298fb4acb380c0dae01debb0ff44379d8eb9b155564fc53808f173", + "rounds": [ + { + "closure": { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc01e184872f49cad1d934e806ac6bfe9e6e7eaeb529102c9f3211348386b648173cb3885c23f8298fb4acb380c0dae01debb0ff44379d8eb9b155564fc53808f173", + "signature": "20474075e6e4cf5e7203211cd1cfa8af33bef7f34014c3689c2992a527e3c2cf31bca34500b2c513291df1e832275a28c4954bcff24746c3684d21e17bf8ea08" + }, + "messages": [ + { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc01020001000001d6010002a8e6fe5955d6d69b2ba5275c14faad458e2a9eb5ebd4c6bc6e91e2340efc8ef64954bd8bf23b47fdd9c870080501ea2f061edb1e5dd207594048b2a7e6bcce92c1b0e44d2a25097035d15d767443224e1541a518535a01244b383115bf8ee2dbb2637aabc0aa10e7db277fb992d64daa8d2c38bd860d877a9114e254df2a86a6507ac76b8aa728e0319842ab9433e2f202c7701ccd44b0f4aa358d10df50b4ca0511c7f31202073d17a4a139c2a7b2524bbc083217899473eb292f925122ab00000300010000005501629c7b451d29a49d74e149126083c8cff6cdce5061c13f247adc7be3754b7a4700000030b662f23e0d6af3500b44aa97f157e80571077d41a1a40ca57aca3e4f92199c201c90a5a4875efa38edc3a23eedd6e38c000200000055010022e486c36014292c0ba8699b613acc3a88d6935d75ba8370c85974789a0a7000000030457a5cdb781206c72b3d53bb55e74aa19d9560029abe719769a37363085e7fd82ea46ddfa51bb2cfbca6b96d96674e2500030000005501387f1d14117128739c100432a80cf6e418b583aabc99230a9f88a89bfc180b3000000030e1b3eb306696bee3f9382697fc91fc95b1eab1a79294338d973f77b4f29c8a05a4efcabefaa9088a283330dc696b8b53", + "signature": "c72d56eae139d88dbe0275fe5470b15d2fc37cde7ea69c24043a6c8bf29ed18894ee62df8bd93b3a01ffdd5cd89c6a29c3c23ac21563469ecb25b320b68c1402" + } + ] + }, + { + "closure": { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc02b5bfbaab519d8fd73c9e22f925917ab6e2da26c3cf28b0a339f166ef3a5c1a595014916dcf43d8bf1c80ceb7d397449491138a92b8d33a3f162bee854990a1bc", + "signature": "34326145a895c635cee1dc8ce3c60eedbd60e81e1d86a615fde3f1a66d169d24556a009efa854b6f8f0b23a1aff9ad2d24aa9b5401bbc91a3d5c4577ec68a902" + }, + "messages": [ + { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc0201000100000003010000", + "signature": "54da50133141d93efa5df8e50c2ef2358afd8e826d69fffb8b20d96e9a64fea1f10b577cc18d29b5d97333467cace3c46602aa7545c20f898956cecdfae79d0a" + }, + { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc0201000200000003010000", + "signature": "4da39c73a7d9c1c9444c993ceeedef52929d6ffcbaa8bdca5c87dfd0fff58fa5d20049705457f481bfa38811e76c8ffc33f8e583a6f242c7e13b38e2f6edf802" + }, + { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc0201000300000003010000", + "signature": "992042d4df77a907bc2993c898d741d1eccc35b14f79ab61684ab150abe310ea51847ca874534643239dd2cbf7283dda2c19d928c28a9d5cc0eae6a74d6cb702" + } + ] + }, + { + "closure": { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc038e7a7ca95023957dbb4be6f9d9c830e0761de7639f3f81b196eaa7ce0cbfdffbb4b5a2b3da4a4798d9803642aeffe4852637e7713f9da0042d70399c28d533f2", + "signature": "2e3cd1941de095a9c2294db0a663d543ad6b6e4bdd1ffb1ba70d02b87a55abcc7d7d24de47961c709ca3fe922ad853dd3918f34f0d05bffa9cec8ba343acdc02" + }, + "messages": [ + { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc0302000100000003010000", + "signature": "2d8b73d9d3bce2a68d9d0ef23c557eb1799b865382321647bdd79675ad324a139d7774cf0cb0f08449c6b7e837c56e7e4fe3ab6d86551e5c16fa9c4740d12a0d" + }, + { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc0302000200000003010000", + "signature": "ed48cfba3a9479970e9c62c3de70ef670bf296c9b8e4ca5b76184a959211274e7b8585f029806642ebaebe3e64b6192d54a765b10fb9e1b61e705db2ed71a203" + }, + { + "body": "0168a7ac2d4e98b5fabc123dceb58350da249528f1df13a7ad5b0f06bc7544fafc0302000300000003010000", + "signature": "69717fda414eea1148c55df5669000849d5f70dada2be4d6bfc00599e7d813c987666a7c6443e3eae00deba35fc0ffc7ac47dc8bbeef8263a37a93e9f72aaa0e" + } + ] + } + ] + }, + "name": "reshare_insufficient_dealers", + "operation": "reshare_verify_transcript" + } + ], + "dst": { + "ceremony_auth": "TC-THRESHOLD-V1-CEREMONY-AUTH", + "ceremony_msg": "TC-THRESHOLD-V1-CEREMONY-MSG", + "cert_statement": "TC-THRESHOLD-V1-CERT-STATEMENT", + "commitments": "TC-THRESHOLD-V1-COMMITMENTS", + "dealing": "TC-THRESHOLD-V1-DEALING", + "network_cert": "TC-THRESHOLD-V1-NETWORK-CERT", + "reshare_start": "TC-THRESHOLD-V1-RESHARE-START", + "round_close": "TC-THRESHOLD-V1-ROUND-CLOSE", + "transcript": "TC-THRESHOLD-V1-TRANSCRIPT" + }, + "encoding": "hex; statements are exact signed bytes; scalars 32-byte big-endian; G2 96-byte compressed", + "format_version": 1, + "seeds": { + "ceremonies": [ + 193, + 194, + 195, + 196, + 197 + ] + }, + "signers": [ + { + "did": "did:key:tc2222-fixture-0", + "ed25519_public_key": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29" + }, + { + "did": "did:key:tc2222-fixture-1", + "ed25519_public_key": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c" + }, + { + "did": "did:key:tc2222-fixture-2", + "ed25519_public_key": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394" + }, + { + "did": "did:key:tc2222-fixture-3", + "ed25519_public_key": "ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1" + }, + { + "did": "did:key:tc2222-fixture-4", + "ed25519_public_key": "ca93ac1705187071d67b83c7ff0efe8108e8ec4530575d7726879333dbdabe7c" + } + ], + "spec_sections": [ + "3.2", + "3.3", + "3.8", + "3.10", + "S0.4 redistribution appendix" + ], + "suite": "BLS12-381 G2 Feldman redistribution; RFC 9180 base mode DHKEM(X25519, HKDF-SHA256)/HKDF-SHA256/ChaCha20-Poly1305 dealings; Ed25519 test signatures" +}