From b8d52580562c992a3b4bf5e2f6dd49c43d68b764 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:32:11 +0000 Subject: [PATCH 01/15] test(threshold): require recipient public-key derivation; Details: TC-2220 --- tinycloud-threshold/src/seal.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tinycloud-threshold/src/seal.rs b/tinycloud-threshold/src/seal.rs index f9d883fe..48859985 100644 --- a/tinycloud-threshold/src/seal.rs +++ b/tinycloud-threshold/src/seal.rs @@ -626,4 +626,11 @@ mod tests { proptest::prop_assert_eq!(decoded.encode(), wire); } } + + #[test] + fn secret_public_key_matches_generated_recipient() { + let mut rng = ChaCha20Rng::from_seed([0x39; 32]); + let (secret, public) = CeremonyHpkeSecretKey::generate(&mut rng); + assert_eq!(secret.public_key(), Ok(public)); + } } From 073412bab05ce64a7da6fe5b50e7125a06172d8a Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:08:36 +0000 Subject: [PATCH 02/15] test(threshold): specify genesis DKG rounds and signed relay scenarios; Details: TC-2220 --- tinycloud-threshold/src/checkpoint.rs | 101 ++ tinycloud-threshold/src/dkg.rs | 305 ++++ tinycloud-threshold/src/dkg_relay.rs | 1896 +++++++++++++++++++++++++ tinycloud-threshold/src/lib.rs | 9 + 4 files changed, 2311 insertions(+) create mode 100644 tinycloud-threshold/src/dkg.rs create mode 100644 tinycloud-threshold/src/dkg_relay.rs diff --git a/tinycloud-threshold/src/checkpoint.rs b/tinycloud-threshold/src/checkpoint.rs index 1c0d7306..d4e388c0 100644 --- a/tinycloud-threshold/src/checkpoint.rs +++ b/tinycloud-threshold/src/checkpoint.rs @@ -589,3 +589,104 @@ proptest! { if let Ok(v)=IbeCiphertext::decode(&c) { prop_assert_eq!(v.encode(),c); } } } + +/// Same checkpoint name filter, driven by real signed DKG candidates. Details: TC-2220 +#[test] +fn checkpoint_2of3_dkg() { + use crate::ceremony::Decode; + use crate::dkg::{DkgFeldman, DkgOutput}; + use crate::dkg_relay::{agreed, members, run, sum, vanishing_feldman, Run}; + + let mut constants = Vec::new(); + let dkg = run(2, 3, 0x71, |out| { + if out.round() == 4 { + constants = (1..=3) + .map(|i| DkgFeldman::decode(&out.payload(i)).unwrap().commitments[0].encode()) + .collect(); + let forged = vanishing_feldman(&out.payload(3), &[1]); + out.replace(3, &forged); + } + }); + let output: DkgOutput = agreed(&dkg, &[1, 2, 3]); + assert_eq!(dkg.transcript.rounds.len(), 6); + println!( + "1. PASS: signed 2-of-3 genesis DKG closed R1-R6; members 1, 2, 3 and dkg_verify_transcript agree on QUAL, commitments, mpk and transcript hash {}", + hex::encode(output.transcript_hash.as_bytes()) + ); + assert!(dkg.reconstruct.values().all(|r| *r)); + assert_eq!(output.qual, members(&[1, 2, 3])); + assert_eq!(output.reconstructed, members(&[3])); + assert!(output.disqualified.is_empty()); + println!("2. PASS: dealer 3's R4 passed only member 1; every member waited for the common closed R5, and R6 recovered dealer 3, which stayed in QUAL"); + assert_eq!( + output.public.master_public_key().encode(), + sum(constants.iter().copied()) + ); + println!("3. PASS: mpk equals the sum of all three dealers' committed constants, including dealer 3's recovered contribution"); + + let Run { + fixture, outcomes, .. + } = dkg; + let anchor = fixture.signers.anchor(); + let body = CertificateBody::genesis( + &anchor, + &fixture.ceremony, + &output.public, + &output.transcript_hash, + ) + .unwrap(); + let cert = verify_certificate( + &anchor, + &crate::cert::tests::sign(body, &fixture.signers), + &fixture.signers, + ) + .unwrap(); + let shares: Vec = outcomes + .into_values() + .map(|outcome| KeyShare::bind(outcome.unwrap().0, &cert).unwrap()) + .collect(); + assert_eq!(shares.len(), 3); + println!("4. PASS: owner-signed genesis certificate verified under the anchor; all three DKG candidate shares bound to it"); + + let mut rng = ChaCha20Rng::from_seed([0x72; 32]); + let tsk = TransportSecretKey::generate(&mut rng); + let req = DecryptRequest::new( + &cert.key_context(), + identity(&cert.key_context(), "records/dkg"), + tsk.public_key(), + ) + .unwrap(); + let auth = authorize(req.clone()); + let issued: Vec<_> = shares + .iter() + .map(|s| issue_share(s, &auth, &mut rng).unwrap()) + .collect(); + for share in &issued { + verify_share(cert.public_key_set(), &req, share).unwrap(); + } + let data = DataKey::new([0x24; 32]); + let ct = wrap_key( + cert.public_key_set().master_public_key(), + req.identity(), + &data, + &mut rng, + ) + .unwrap(); + let mut keys = Vec::new(); + for (a, b) in [(0, 1), (0, 2), (1, 2)] { + let encrypted = combine( + cert.public_key_set(), + &req, + &[issued[a].clone(), issued[b].clone()], + ) + .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(), + data.as_bytes() + ); + keys.push(key.key.encode()); + } + 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"); +} diff --git a/tinycloud-threshold/src/dkg.rs b/tinycloud-threshold/src/dkg.rs new file mode 100644 index 00000000..80f35c0e --- /dev/null +++ b/tinycloud-threshold/src/dkg.rs @@ -0,0 +1,305 @@ +//! Genesis GJKR New-DKG over owner-closed rounds. Details: TC-2220 + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + ceremony::{tests::Signers, AuthorizedCeremony, CeremonyKind, Decode}, + curve::{Scalar, G2}, + dst, + error::Error, + ids::*, + seal::{seal_dealing, CeremonyHpkeSecretKey, DealingContext}, + }; + use proptest::prelude::*; + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; + use sha2::{Digest, Sha256}; + + fn idx(i: u16) -> MemberIndex { + MemberIndex::new(i).unwrap() + } + + fn pair(value: u64, blinding: u64) -> SharePair { + SharePair { + value: Scalar::from_u64(value), + blinding: Scalar::from_u64(blinding), + } + } + + 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 sealed(recipient: u16, seed: u8) -> crate::seal::SealedDealing { + let mut rng = ChaCha20Rng::from_seed([seed; 32]); + let (_, public) = CeremonyHpkeSecretKey::generate(&mut rng); + let context = DealingContext { + ceremony_digest: [seed; 32], + round: 1, + sender: idx(1), + recipient: idx(recipient), + commitment_hash: [7; 32], + }; + seal_dealing(&public, &context, &[0; 64], &mut rng).unwrap() + } + + fn deal() -> DkgDeal { + let g = G2::generator(); + DkgDeal { + commitments: vec![g.clone(), g.mul(&Scalar::from_u64(2))], + dealings: vec![(idx(1), sealed(1, 3)), (idx(2), sealed(2, 4))], + } + } + + 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 dkg_start_label_is_distinct_from_and_not_a_prefix_of_every_frozen_tag() { + assert_eq!(DKG_START, b"TC-THRESHOLD-V1-DKG-START"); + assert_eq!(dst::DOMAINS.len(), 19); + for tag in dst::DOMAINS { + assert_ne!(*tag, DKG_START); + assert!(!tag.starts_with(DKG_START)); + assert!(!DKG_START.starts_with(tag)); + } + } + + #[test] + fn initial_transcript_hashes_the_exact_ceremony_digest() { + let signers = Signers::new(); + let ceremony = signers.genesis(); + let mut hash = Sha256::new(); + hash.update(25u32.to_be_bytes()); + hash.update(b"TC-THRESHOLD-V1-DKG-START"); + hash.update(ceremony.digest()); + let expected: [u8; 32] = hash.finalize().into(); + assert_eq!(dkg_initial_transcript(&ceremony).as_bytes(), &expected); + assert_ne!(expected, [0; 32]); + assert_ne!(expected, ceremony.digest()); + + let mut body = signers.body(CeremonyKind::Genesis, ceremony.threshold()); + body.id = CeremonyId::new([42; 32]); + let other = AuthorizedCeremony::verify_genesis( + &signers.anchor(), + &signers.statement(&body), + &signers, + ) + .unwrap(); + assert_ne!( + dkg_initial_transcript(&other), + dkg_initial_transcript(&ceremony) + ); + } + + #[test] + fn deal_payload_has_the_exact_layout_and_rejects_noncanonical_bytes() { + let deal = deal(); + let bytes = deal.encode(); + let mut expected = vec![1, 0, 2]; + for c in &deal.commitments { + expected.extend_from_slice(&c.encode()); + } + expected.extend_from_slice(&[0, 2]); + for (index, dealing) in &deal.dealings { + let envelope = dealing.encode(); + expected.extend_from_slice(&index.get().to_be_bytes()); + expected.extend_from_slice(&(envelope.len() as u32).to_be_bytes()); + expected.extend_from_slice(&envelope); + } + assert_eq!(bytes, expected); + assert_eq!(DkgDeal::decode(&bytes).unwrap().encode(), bytes); + rejects_every_truncation_and_trailing_byte::(&bytes); + + let swapped = DkgDeal { + commitments: deal.commitments.clone(), + dealings: vec![(idx(2), sealed(2, 4)), (idx(1), sealed(1, 3))], + }; + assert!(DkgDeal::decode(&swapped.encode()).is_err()); + let duplicate = DkgDeal { + commitments: deal.commitments.clone(), + dealings: vec![(idx(1), sealed(1, 3)), (idx(1), sealed(1, 4))], + }; + assert!(DkgDeal::decode(&duplicate.encode()).is_err()); + + let mut bad_point = bytes.clone(); + // Setting the infinity flag on a nonzero compressed x is invalid. + bad_point[3] ^= 0x40; + assert!(DkgDeal::decode(&bad_point).is_err()); + let mut bad_envelope = bytes.clone(); + let envelope_version = 3 + 2 * 96 + 2 + 2 + 4; + bad_envelope[envelope_version] = 2; + assert!(DkgDeal::decode(&bad_envelope).is_err()); + let mut index_zero = bytes.clone(); + index_zero[3 + 2 * 96 + 2..3 + 2 * 96 + 4].copy_from_slice(&[0, 0]); + assert!(DkgDeal::decode(&index_zero).is_err()); + + assert!(matches!( + DkgDeal::decode(&[1, 0, 17]), + Err(Error::LimitExceeded) + )); + let mut many_dealings = vec![1, 0, 0]; + many_dealings.extend_from_slice(&[0, 17]); + assert!(matches!( + DkgDeal::decode(&many_dealings), + Err(Error::LimitExceeded) + )); + } + + #[test] + fn complaint_payload_is_a_sorted_unique_index_list() { + let complaints = DkgComplaints { + dealers: vec![idx(2), idx(5)], + }; + let bytes = complaints.encode(); + assert_eq!(bytes, vec![1, 0, 2, 0, 2, 0, 5]); + assert_eq!(DkgComplaints::decode(&bytes).unwrap().encode(), bytes); + assert_eq!( + DkgComplaints::decode(&[1, 0, 0]).unwrap().dealers, + Vec::::new() + ); + rejects_every_truncation_and_trailing_byte::(&bytes); + for invalid in [ + vec![1, 0, 2, 0, 5, 0, 2], + vec![1, 0, 2, 0, 2, 0, 2], + vec![1, 0, 1, 0, 0], + vec![1, 0, 1, 0, 17], + ] { + assert!(DkgComplaints::decode(&invalid).is_err()); + } + assert!(matches!( + DkgComplaints::decode(&[1, 0, 17]), + Err(Error::LimitExceeded) + )); + } + + #[test] + fn published_pair_payloads_share_one_canonical_layout() { + let entries = || vec![(idx(1), pair(7, 8)), (idx(3), pair(9, 10))]; + let answers = DkgAnswers { answers: entries() }.encode(); + let extraction = DkgExtractionComplaints { + complaints: entries(), + } + .encode(); + let reconstruction = DkgReconstruction { values: entries() }.encode(); + let mut expected = vec![1, 0, 2]; + for (index, value, blinding) in [(1u16, 7u8, 8u8), (3, 9, 10)] { + expected.extend_from_slice(&index.to_be_bytes()); + let mut scalar = [0; 32]; + scalar[31] = value; + expected.extend_from_slice(&scalar); + scalar[31] = blinding; + expected.extend_from_slice(&scalar); + } + assert_eq!(answers, expected); + assert_eq!(extraction, expected); + assert_eq!(reconstruction, expected); + assert_eq!(DkgAnswers::decode(&answers).unwrap().encode(), answers); + assert_eq!( + DkgExtractionComplaints::decode(&extraction) + .unwrap() + .encode(), + extraction + ); + assert_eq!( + DkgReconstruction::decode(&reconstruction).unwrap().encode(), + reconstruction + ); + rejects_every_truncation_and_trailing_byte::(&answers); + rejects_every_truncation_and_trailing_byte::(&extraction); + rejects_every_truncation_and_trailing_byte::(&reconstruction); + + let mut noncanonical = expected.clone(); + noncanonical[5..37].copy_from_slice(&R); + assert!(matches!( + DkgAnswers::decode(&noncanonical), + Err(Error::InvalidScalar) + )); + let mut blinding = expected.clone(); + blinding[37..69].copy_from_slice(&R); + assert!(matches!( + DkgReconstruction::decode(&blinding), + Err(Error::InvalidScalar) + )); + let mut unsorted = expected.clone(); + unsorted[3..5].copy_from_slice(&[0, 3]); + unsorted[69..71].copy_from_slice(&[0, 1]); + assert!(DkgExtractionComplaints::decode(&unsorted).is_err()); + let mut duplicate = expected; + duplicate[69..71].copy_from_slice(&[0, 1]); + assert!(DkgAnswers::decode(&duplicate).is_err()); + assert!(matches!( + DkgAnswers::decode(&[1, 0, 17]), + Err(Error::LimitExceeded) + )); + } + + #[test] + fn feldman_payload_allows_individual_identity_coefficients() { + let g = G2::generator(); + let feldman = DkgFeldman { + commitments: vec![g.clone(), G2::identity(), g.mul(&Scalar::from_u64(3))], + }; + let bytes = feldman.encode(); + assert_eq!(bytes.len(), 3 + 3 * 96); + assert_eq!(&bytes[..3], &[1, 0, 3]); + assert_eq!(&bytes[3 + 96..3 + 192], &G2::identity().encode()); + assert_eq!(DkgFeldman::decode(&bytes).unwrap().encode(), bytes); + rejects_every_truncation_and_trailing_byte::(&bytes); + let mut off_curve = bytes.clone(); + off_curve[3 + 95] ^= 1; + assert!(DkgFeldman::decode(&off_curve).is_err()); + assert!(matches!( + DkgFeldman::decode(&[1, 0, 17]), + Err(Error::LimitExceeded) + )); + } + + proptest! { + #[test] + fn random_payload_bytes_reject_or_reencode_canonically( + bytes in proptest::collection::vec(any::(), 0..1400) + ) { + if let Ok(v) = DkgDeal::decode(&bytes) { prop_assert_eq!(v.encode(), bytes.clone()); } + if let Ok(v) = DkgComplaints::decode(&bytes) { prop_assert_eq!(v.encode(), bytes.clone()); } + if let Ok(v) = DkgAnswers::decode(&bytes) { prop_assert_eq!(v.encode(), bytes.clone()); } + if let Ok(v) = DkgFeldman::decode(&bytes) { prop_assert_eq!(v.encode(), bytes.clone()); } + if let Ok(v) = DkgExtractionComplaints::decode(&bytes) { prop_assert_eq!(v.encode(), bytes.clone()); } + if let Ok(v) = DkgReconstruction::decode(&bytes) { prop_assert_eq!(v.encode(), bytes.clone()); } + } + + #[test] + fn structured_payload_mutations_reject_or_reencode_canonically( + offset in 0usize..2048, + mask in 1u8..=255 + ) { + let valid = [ + deal().encode(), + DkgComplaints { dealers: vec![idx(1), idx(4)] }.encode(), + DkgAnswers { answers: vec![(idx(2), pair(3, 4))] }.encode(), + DkgFeldman { commitments: vec![G2::generator(), G2::identity()] }.encode(), + ]; + for bytes in valid { + let mut mutated = bytes.clone(); + let n = mutated.len(); + mutated[offset % n] ^= mask; + if let Ok(v) = DkgDeal::decode(&mutated) { prop_assert_eq!(v.encode(), mutated.clone()); } + if let Ok(v) = DkgComplaints::decode(&mutated) { prop_assert_eq!(v.encode(), mutated.clone()); } + if let Ok(v) = DkgAnswers::decode(&mutated) { prop_assert_eq!(v.encode(), mutated.clone()); } + if let Ok(v) = DkgFeldman::decode(&mutated) { prop_assert_eq!(v.encode(), mutated.clone()); } + } + } + } +} diff --git a/tinycloud-threshold/src/dkg_relay.rs b/tinycloud-threshold/src/dkg_relay.rs new file mode 100644 index 00000000..f2861d52 --- /dev/null +++ b/tinycloud-threshold/src/dkg_relay.rs @@ -0,0 +1,1896 @@ +//! Test-only owner relay driving real member states over signed closed rounds. Details: TC-2220 +//! +//! Liveness scenarios deliver every honest message. Omissions and substitutions +//! below are explicit failure cases chosen by each test, not a broadcast model. + +use std::collections::{BTreeMap, BTreeSet}; + +use proptest::prelude::*; +use rand_chacha::ChaCha20Rng; +use rand_core::{CryptoRng, RngCore, SeedableRng}; +use serde_json::{json, Value}; + +use crate::{ + ceremony::{ + ceremony_message_body, round_closure_body, round_closure_preimage, tests::Signers, + AuthorizedCeremony, CeremonyKind, CeremonyRecipient, CeremonyStatement, ClosedRound, + Decode, DidSignatureVerifier, MemberCohort, SignedStatement, + }, + cert::{verify_certificate, CandidateShare, CertificateBody}, + curve::{hash_g2, Scalar, G2}, + dkg::*, + dst, + error::{Error, SignatureError}, + ids::*, + seal::{ + open_dealing, seal_dealing, CeremonyHpkePublicKey, CeremonyHpkeSecretKey, DealingContext, + }, + KeyShare, +}; + +pub(crate) fn idx(i: u16) -> MemberIndex { + MemberIndex::new(i).unwrap() +} + +pub(crate) fn pair(value: u64, blinding: u64) -> SharePair { + SharePair { + value: Scalar::from_u64(value), + blinding: Scalar::from_u64(blinding), + } +} + +/// Owner, member DIDs, Ed25519 dev keys and a genesis ceremony with real recipient keys. +pub(crate) struct Fixture { + pub(crate) signers: Signers, + pub(crate) ceremony: AuthorizedCeremony, + pub(crate) statement: SignedStatement, + pub(crate) seed: u8, +} + +pub(crate) fn statement(t: u16, n: u16, seed: u8) -> (Signers, SignedStatement) { + let signers = Signers::new(); + let mut rng = ChaCha20Rng::from_seed([seed; 32]); + let recipients = (1..=n) + .map(|i| CeremonyRecipient { + index: idx(i), + node: Signers::did(i as u8), + hpke_public_key: CeremonyHpkeSecretKey::generate(&mut rng).1.encode(), + }) + .collect(); + let body = CeremonyStatement { + kind: CeremonyKind::Genesis, + id: CeremonyId::new([seed; 32]), + network: signers.anchor().network, + owner: Signers::did(0), + current: None, + threshold: Threshold::new(t, n).unwrap(), + rekey: RekeyCounter::new(0), + epoch: Epoch::new(0), + recipients, + }; + let signed = signers.statement(&body); + (signers, signed) +} + +pub(crate) fn fixture(t: u16, n: u16, seed: u8) -> Fixture { + let (signers, statement) = statement(t, n, seed); + let ceremony = + AuthorizedCeremony::verify_genesis(&signers.anchor(), &statement, &signers).unwrap(); + Fixture { + signers, + ceremony, + statement, + seed, + } +} + +impl Fixture { + /// Regenerate member i's ceremony recipient key from the fixture seed. + pub(crate) fn secret(&self, i: u16) -> CeremonyHpkeSecretKey { + let mut rng = ChaCha20Rng::from_seed([self.seed; 32]); + let mut key = None; + for _ in 1..=i { + 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] = 0xd1; + ChaCha20Rng::from_seed(seed) + } + + pub(crate) fn sign(&self, member: u16, message: &DkgMessage) -> SignedStatement { + assert_eq!( + message.signing_preimage(), + crate::ceremony::ceremony_message_preimage(message.body()).unwrap() + ); + SignedStatement { + bytes: message.body().to_vec(), + signature: self + .signers + .sign(&Signers::did(member as u8), &message.signing_preimage()), + } + } + + pub(crate) fn n(&self) -> u16 { + self.ceremony.threshold().n() + } + + pub(crate) fn t(&self) -> u16 { + self.ceremony.threshold().t() + } +} + +fn sender(statement: &SignedStatement) -> u16 { + u16::from_be_bytes([statement.bytes[35], statement.bytes[36]]) +} + +/// Messages the owner is about to close for one round. +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 + } + /// A member-signed envelope carrying arbitrary payload bytes for this round. + pub(crate) fn signed(&self, member: u16, payload: &[u8]) -> SignedStatement { + let bytes = ceremony_message_body( + &self.fixture.ceremony, + self.round, + MemberCohort::Recipients, + idx(member), + payload, + ) + .unwrap(); + let preimage = crate::ceremony::ceremony_message_preimage(&bytes).unwrap(); + SignedStatement { + signature: self + .fixture + .signers + .sign(&Signers::did(member as u8), &preimage), + bytes, + } + } + pub(crate) fn has(&self, member: u16) -> bool { + self.messages.iter().any(|m| sender(m) == member) + } + pub(crate) fn payload(&self, member: u16) -> Vec { + let message = self + .messages + .iter() + .find(|m| sender(m) == member) + .expect("member message in this round"); + message.bytes[41..].to_vec() + } + pub(crate) fn remove(&mut self, member: u16) { + self.messages.retain(|m| sender(m) != member); + } + pub(crate) fn replace(&mut self, member: u16, payload: &[u8]) { + let signed = self.signed(member, payload); + let position = self.messages.iter().position(|m| sender(m) == member); + 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(member, payload); + self.messages.push(signed); + } + pub(crate) fn duplicate(&mut self, member: u16) { + let copy = self + .messages + .iter() + .find(|m| sender(m) == member) + .cloned() + .expect("member message in this round"); + self.messages.push(copy); + } +} + +pub(crate) type Outcome = Result<(CandidateShare, DkgOutput), Error>; + +pub(crate) struct Run { + pub(crate) fixture: Fixture, + pub(crate) outcomes: BTreeMap, + /// Members that asked for R6 after the common closed R5. + pub(crate) reconstruct: BTreeMap, + pub(crate) transcript: DkgTranscript, +} + +impl Run { + pub(crate) fn verify(&self) -> Result { + dkg_verify_transcript( + &self.fixture.ceremony, + &self.transcript, + &self.fixture.signers, + ) + } +} + +struct Relay<'a, V> { + fixture: &'a Fixture, + verifier: &'a V, + previous: TranscriptHash, + transcript: DkgTranscript, + 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(); + assert_eq!(closed.previous_hash(), self.previous); + self.previous = closed.transcript_hash(); + self.transcript + .rounds + .push(DkgRoundRecord { closure, messages }); + closed + } +} + +type States = BTreeMap>; + +fn advance( + fixture: &Fixture, + states: States, + closed: &ClosedRound, + round: u8, + step: impl Fn(S, &ClosedRound) -> Result<(N, DkgMessage), Error>, +) -> (States, Vec) { + let mut next = BTreeMap::new(); + let mut out = Vec::new(); + for (i, state) in states { + match state.and_then(|s| step(s, closed)) { + Ok((s, message)) => { + assert_eq!(message.round(), round); + out.push(fixture.sign(i, &message)); + next.insert(i, Ok(s)); + } + Err(e) => { + next.insert(i, Err(e)); + } + } + } + (next, out) +} + +fn stopped(states: &States) -> bool { + states.values().all(Result::is_err) +} + +fn errors(states: States) -> BTreeMap { + states + .into_iter() + .map(|(i, s)| (i, Err(s.err().expect("stopped member")))) + .collect() +} + +/// Run every member's real state machine through a signing owner relay. +pub(crate) fn drive( + fixture: Fixture, + verifier: &V, + mut tamper: impl FnMut(&mut Outbox), +) -> Run { + let n = fixture.n(); + let mut relay = Relay { + fixture: &fixture, + verifier, + previous: dkg_initial_transcript(&fixture.ceremony), + transcript: DkgTranscript { rounds: Vec::new() }, + rng: ChaCha20Rng::from_seed([fixture.seed ^ 0xaa; 32]), + }; + let mut dealt = BTreeMap::new(); + let mut r1 = Vec::new(); + for i in 1..=n { + let mut rng = fixture.member_rng(i); + let (state, message) = + dkg_start(&fixture.ceremony, idx(i), fixture.secret(i), &mut rng).unwrap(); + assert_eq!(message.round(), 1); + r1.push(fixture.sign(i, &message)); + dealt.insert(i, Ok(state)); + } + let closed1 = relay.close::(1, r1, &mut tamper); + let (complained, r2) = advance(&fixture, dealt, &closed1, 2, DkgDealt::complain); + let closed2 = relay.close::(2, r2, &mut tamper); + let (answered, r3) = advance(&fixture, complained, &closed2, 3, DkgComplained::answer); + let closed3 = relay.close::(3, r3, &mut tamper); + let (extracting, r4) = advance(&fixture, answered, &closed3, 4, DkgAnswered::extract); + let mut outcomes = BTreeMap::new(); + let mut reconstruct = BTreeMap::new(); + if stopped(&extracting) { + outcomes = errors(extracting); + } else { + let closed4 = relay.close::(4, r4, &mut tamper); + let (waiting, r5) = advance( + &fixture, + extracting, + &closed4, + 5, + DkgExtracting::extraction_complaints, + ); + let closed5 = relay.close::(5, r5, &mut tamper); + let mut reconstructing = BTreeMap::new(); + let mut r6 = Vec::new(); + for (i, state) in waiting { + match state.and_then(|s| s.after_barrier(&closed5)) { + Ok(AfterBarrier::Finished(candidate, output)) => { + reconstruct.insert(i, false); + outcomes.insert(i, Ok((candidate, output))); + } + Ok(AfterBarrier::Reconstruct(state, message)) => { + assert_eq!(message.round(), 6); + reconstruct.insert(i, true); + r6.push(fixture.sign(i, &message)); + reconstructing.insert(i, state); + } + Err(e) => { + outcomes.insert(i, Err(e)); + } + } + } + if !reconstructing.is_empty() { + let closed6 = relay.close::(6, r6, &mut tamper); + for (i, state) in reconstructing { + outcomes.insert(i, state.finish(&closed6)); + } + } + } + let transcript = relay.transcript; + Run { + fixture, + outcomes, + reconstruct, + transcript, + } +} + +pub(crate) fn run(t: u16, n: u16, seed: u8, tamper: impl FnMut(&mut Outbox)) -> Run { + let fixture = fixture(t, n, seed); + let signers = Signers::new(); + drive(fixture, &signers, tamper) +} + +pub(crate) fn honest(t: u16, n: u16, seed: u8) -> Run { + run(t, n, seed, |_| {}) +} + +type View = ( + Vec<[u8; 96]>, + [u8; 96], + Vec<[u8; 96]>, + BTreeSet, + BTreeMap, + BTreeSet, + TranscriptHash, +); + +pub(crate) fn view(output: &DkgOutput) -> View { + let n = output.public.threshold().n(); + ( + output.public.commitments().collect(), + output.public.master_public_key().encode(), + (1..=n) + .map(|i| output.public.public_share(idx(i)).unwrap()) + .collect(), + output.qual.clone(), + output.disqualified.clone(), + output.reconstructed.clone(), + output.transcript_hash, + ) +} + +/// Every listed member and the public transcript verifier agree; each candidate binds. +pub(crate) fn agreed(run: &Run, members: &[u16]) -> DkgOutput { + let verified = run.verify().unwrap(); + assert_eq!(verified.transcript_hash, last_hash(run)); + for i in members { + let (candidate, output) = run.outcomes[i].as_ref().unwrap(); + assert_eq!(view(output), view(&verified), "member {i}"); + assert_eq!(candidate.index(), idx(*i)); + assert_eq!(candidate.transcript_hash(), verified.transcript_hash); + assert_eq!( + G2::generator().mul(&candidate.scalar).encode(), + verified.public.public_share(idx(*i)).unwrap(), + "candidate {i} matches its public share" + ); + } + verified +} + +fn last_hash(run: &Run) -> TranscriptHash { + let mut previous = dkg_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 +} + +pub(crate) fn members(set: &[u16]) -> BTreeSet { + set.iter().map(|i| idx(*i)).collect() +} + +pub(crate) fn sum(points: impl IntoIterator) -> [u8; 96] { + points + .into_iter() + .fold(G2::identity(), |acc, p| { + acc.add(&G2::decode(&p, true).unwrap()) + }) + .encode() +} + +pub(crate) fn commitment_hash(commitments: &[G2]) -> [u8; 32] { + let mut body = (commitments.len() as u16).to_be_bytes().to_vec(); + for c in commitments { + body.extend_from_slice(&c.encode()); + } + dst::tagged_hash(dst::COMMITMENTS, &body) +} + +fn pedersen_h() -> G2 { + hash_g2(b"pedersen-h", dst::PEDERSEN_H) +} + +/// Re-seal one recipient's entry of a dealer's R1 with canonical but wrong scalars. +pub(crate) fn bad_dealing(out: &mut Outbox, dealer: u16, recipient: u16) -> Vec { + let mut deal = DkgDeal::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; 64]; + plaintext[31] = 5; + plaintext[63] = 6; + 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) -> SharePair { + let deal = DkgDeal::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(); + SharePair { + value: Scalar::from_bytes(&plaintext[..32]).unwrap(), + blinding: Scalar::from_bytes(&plaintext[32..]).unwrap(), + } +} + +/// Add δ(x)=Π(x−root)·D to a Feldman vector: the check passes exactly at the roots. +pub(crate) fn vanishing_feldman(payload: &[u8], roots: &[u16]) -> Vec { + let mut feldman = DkgFeldman::decode(payload).unwrap(); + let d = G2::generator().mul(&Scalar::from_u64(0x5eed)); + let mut poly = vec![Scalar::from_u64(1)]; + for root in roots { + let mut next: Vec = (0..=poly.len()).map(|_| Scalar::from_u64(0)).collect(); + for (k, c) in poly.iter().enumerate() { + next[k + 1] = next[k + 1].add(c); + next[k] = next[k].sub(&c.mul(&Scalar::from_u64(u64::from(*root)))); + } + poly = next; + } + assert!(poly.len() <= feldman.commitments.len()); + for (k, c) in poly.iter().enumerate() { + feldman.commitments[k] = feldman.commitments[k].add(&d.mul(c)); + } + feldman.encode() +} + +fn constants(out: &Outbox, into: &mut BTreeMap) { + for i in 1..=out.fixture().n() { + if out.has(i) { + let feldman = DkgFeldman::decode(&out.payload(i)).unwrap(); + into.insert(i, feldman.commitments[0].encode()); + } + } +} + +fn genuine_mpk(constants: &BTreeMap, qual: &BTreeSet) -> [u8; 96] { + sum(qual.iter().map(|i| constants[&i.get()])) +} + +/// 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), + } + } +} + +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([0x77; 32]), + draws: 0, + } +} + +// ---------------------------------------------------------------- completion + +#[test] +fn genuine_2of3_completes_with_every_member_and_the_verifier_agreeing() { + let run = honest(2, 3, 0x31); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3])); + assert!(output.disqualified.is_empty()); + assert!(output.reconstructed.is_empty()); + assert_eq!(run.transcript.rounds.len(), 5); + assert!(run.reconstruct.values().all(|r| !r)); + assert_eq!(output.public.threshold(), Threshold::new(2, 3).unwrap()); + let mut sorted = output.public.commitments().collect::>(); + assert_eq!(sorted.len(), 2); + sorted.dedup(); + assert_eq!(sorted.len(), 2); +} + +#[test] +fn genesis_3of4_is_refused_before_any_dealing() { + let (signers, statement) = statement(3, 4, 0x32); + assert!(matches!( + AuthorizedCeremony::verify_genesis(&signers.anchor(), &statement, &signers), + Err(Error::UnsupportedFaultBudget) + )); + assert_eq!( + Threshold::new(3, 4).unwrap().check_genesis_budget(), + Err(Error::UnsupportedFaultBudget) + ); + assert_eq!(Threshold::new(3, 5).unwrap().check_genesis_budget(), Ok(())); +} + +#[test] +fn three_of_five_with_two_faulty_members_recovers_the_committed_master_key() { + // Members 4 and 5 are faulty. Dealer 5 publishes a Feldman vector that + // passes only at indices 4 and 5; member 4 files a false R5 complaint + // against honest dealer 1; both withhold R6. + let mut constants_seen = BTreeMap::new(); + let run = run(3, 5, 0x33, |out| match out.round() { + 4 => { + constants(out, &mut constants_seen); + let forged = vanishing_feldman(&out.payload(5), &[4, 5]); + out.replace(5, &forged); + } + 5 => { + let complaint = DkgExtractionComplaints { + complaints: vec![(idx(1), pair(11, 12))], + }; + out.replace(4, &complaint.encode()); + } + 6 => { + out.remove(4); + out.remove(5); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3, 4, 5])); + assert!(output.disqualified.is_empty()); + assert_eq!(output.reconstructed, members(&[5])); + assert_eq!( + output.public.master_public_key().encode(), + genuine_mpk(&constants_seen, &output.qual) + ); + for i in [1, 2, 3] { + assert!(run.reconstruct[&i]); + } +} + +#[test] +fn one_recipient_bad_private_value_is_replaced_by_a_valid_answer() { + let mut answer = None; + let run = run(2, 3, 0x34, |out| match out.round() { + 1 => { + let forged = bad_dealing(out, 1, 2); + out.replace(1, &forged); + } + 2 => assert_eq!( + DkgComplaints::decode(&out.payload(2)).unwrap().dealers, + vec![idx(1)] + ), + 3 => answer = Some(DkgAnswers::decode(&out.payload(1)).unwrap()), + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3])); + assert!(output.disqualified.is_empty()); + let answer = answer.unwrap(); + assert_eq!(answer.answers.len(), 1); + assert_eq!(answer.answers[0].0, idx(2)); +} + +// ------------------------------------------------------------ R2/R3 rulings + +#[test] +fn false_complaint_is_refuted_by_a_valid_answer() { + let run = run(2, 3, 0x35, |out| { + if out.round() == 2 { + out.replace( + 3, + &DkgComplaints { + dealers: vec![idx(1)], + } + .encode(), + ); + } + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3])); +} + +#[test] +fn t_distinct_complainers_disqualify_even_with_valid_answers() { + let run = run(2, 3, 0x36, |out| { + if out.round() == 1 { + let forged = bad_dealing(out, 3, 1); + out.replace(3, &forged); + let forged = bad_dealing(out, 3, 2); + out.replace(3, &forged); + } + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2])); + assert_eq!( + output.disqualified, + BTreeMap::from([(idx(3), DisqualifyReason::Complaints)]) + ); +} + +#[test] +fn duplicate_complaints_count_once() { + // t = 3: two distinct complainers, one retransmitting, stay below t. + let run = run(3, 5, 0x37, |out| { + if out.round() == 2 { + let complaint = DkgComplaints { + dealers: vec![idx(1)], + } + .encode(); + out.replace(4, &complaint); + out.replace(5, &complaint); + out.duplicate(4); + out.duplicate(4); + } + }); + let output = agreed(&run, &[1, 2, 3, 4, 5]); + assert_eq!(output.qual, members(&[1, 2, 3, 4, 5])); +} + +#[test] +fn self_complaints_are_ignored() { + // Dealer 3 sends member 1 a bad value and complains against itself. With + // the self-complaint counted, two complainers would reach t = 2. + let run = run(2, 3, 0x38, |out| match out.round() { + 1 => { + let forged = bad_dealing(out, 3, 1); + out.replace(3, &forged); + } + 2 => out.replace( + 3, + &DkgComplaints { + dealers: vec![idx(3)], + } + .encode(), + ), + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3])); +} + +#[test] +fn malformed_r2_supplies_no_complaints() { + // A well-formed complaint with a trailing byte is malformed: it creates no + // required answer, so dealer 1's missing R3 does not disqualify it. + let run = run(2, 3, 0x39, |out| match out.round() { + 2 => { + let mut malformed = DkgComplaints { + dealers: vec![idx(1)], + } + .encode(); + malformed.push(0); + out.replace(3, &malformed); + } + 3 => out.remove(1), + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3])); +} + +#[test] +fn conflicting_r2_content_is_equivocation_even_when_malformed() { + let run = run(3, 5, 0x3a, |out| { + if out.round() == 2 { + out.replace(5, &[1, 0]); + out.push(5, &[1, 0, 1]); + out.replace(4, &[1, 0, 1, 0]); + out.duplicate(4); + } + }); + let output = agreed(&run, &[1, 2, 3, 4]); + assert_eq!( + output.disqualified, + BTreeMap::from([(idx(5), DisqualifyReason::Equivocated { round: 2 })]) + ); + assert_eq!(output.qual, members(&[1, 2, 3, 4])); +} + +/// Retransmit a member's first message with an alternate valid signature encoding. +fn alternate_signature(out: &mut Outbox, member: u16) { + let mut copy = out + .messages + .iter() + .find(|m| sender(m) == member) + .cloned() + .unwrap(); + copy.signature.push(0); + out.messages.push(copy); +} + +#[test] +fn different_signature_encoding_is_not_equivocation() { + let verifier = Malleable(Signers::new()); + let run = drive(fixture(2, 3, 0x3b), &verifier, |out| { + if out.round() <= 3 { + alternate_signature(out, 3); + } + }); + assert_eq!(run.transcript.rounds[0].messages.len(), 4); + let verified = + dkg_verify_transcript(&run.fixture.ceremony, &run.transcript, &verifier).unwrap(); + assert_eq!(verified.qual, members(&[1, 2, 3])); + assert!(verified.disqualified.is_empty()); + for i in [1, 2, 3] { + assert_eq!(view(&run.outcomes[&i].as_ref().unwrap().1), view(&verified)); + } + + let changed = drive(fixture(2, 3, 0x3b), &verifier, |out| { + if out.round() == 1 { + let forged = bad_dealing(out, 3, 2); + out.push(3, &forged); + } + }); + let verified = + dkg_verify_transcript(&changed.fixture.ceremony, &changed.transcript, &verifier).unwrap(); + assert_eq!( + verified.disqualified, + BTreeMap::from([(idx(3), DisqualifyReason::Equivocated { round: 1 })]) + ); +} + +#[test] +fn missing_r3_disqualifies_only_with_required_answers() { + let run = run(2, 3, 0x3c, |out| match out.round() { + 2 => out.replace( + 3, + &DkgComplaints { + dealers: vec![idx(1)], + } + .encode(), + ), + 3 => { + out.remove(1); + out.remove(2); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.disqualified, + BTreeMap::from([(idx(1), DisqualifyReason::MissingAnswer)]) + ); + assert_eq!(output.qual, members(&[2, 3])); +} + +#[test] +fn malformed_r3_disqualifies_only_with_required_answers() { + let run = run(2, 3, 0x3d, |out| match out.round() { + 2 => out.replace( + 3, + &DkgComplaints { + dealers: vec![idx(1)], + } + .encode(), + ), + 3 => { + for dealer in [1, 2] { + let mut malformed = out.payload(dealer); + malformed.push(0); + out.replace(dealer, &malformed); + } + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.disqualified, + BTreeMap::from([(idx(1), DisqualifyReason::MissingAnswer)]) + ); + assert_eq!(output.qual, members(&[2, 3])); +} + +#[test] +fn invalid_unsolicited_answer_disqualifies_its_dealer() { + let run = run(2, 3, 0x3e, |out| { + if out.round() == 3 { + let answer = DkgAnswers { + answers: vec![(idx(3), pair(5, 6))], + }; + out.replace(2, &answer.encode()); + } + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.disqualified, + BTreeMap::from([(idx(2), DisqualifyReason::InvalidAnswer)]) + ); + assert_eq!(output.qual, members(&[1, 3])); +} + +#[test] +fn valid_unsolicited_answer_is_ignored() { + let mut deal = Vec::new(); + let run = run(2, 3, 0x3f, |out| match out.round() { + 1 => deal = out.payload(2), + 3 => { + let answer = DkgAnswers { + answers: vec![(idx(3), opened(out.fixture(), &deal, 2, 3))], + }; + out.replace(2, &answer.encode()); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3])); + assert!(output.disqualified.is_empty()); +} + +#[test] +fn relay_omitted_complaint_fails_only_the_affected_member() { + // Failure scenario outside the trusted-relay liveness model. + let run = run(2, 3, 0x40, |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.qual, members(&[1, 2, 3])); +} + +// ------------------------------------------------- R4, barrier and R6 rules + +#[test] +fn selective_r4_reaches_the_common_barrier_and_is_reconstructed() { + let mut constants_seen = BTreeMap::new(); + let mut r5 = BTreeMap::new(); + let run = run(2, 3, 0x41, |out| match out.round() { + 4 => { + constants(out, &mut constants_seen); + let forged = vanishing_feldman(&out.payload(3), &[1]); + out.replace(3, &forged); + } + 5 => { + for i in 1..=3 { + r5.insert( + i, + DkgExtractionComplaints::decode(&out.payload(i)) + .unwrap() + .complaints + .len(), + ); + } + } + _ => {} + }); + // Member 1's own check passed; member 2 complained. Both waited for the + // same closed R5 and both entered R6. + assert_eq!(r5, BTreeMap::from([(1, 0), (2, 1), (3, 0)])); + assert_eq!( + run.reconstruct, + BTreeMap::from([(1, true), (2, true), (3, true)]) + ); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3])); + assert_eq!(output.reconstructed, members(&[3])); + assert_eq!( + output.public.master_public_key().encode(), + genuine_mpk(&constants_seen, &output.qual) + ); + assert_eq!(run.transcript.rounds.len(), 6); +} + +#[test] +fn bad_missing_and_malformed_r4_are_extraction_faults() { + let cases: [(&str, fn(&mut Outbox)); 5] = [ + ("bad", |out| { + let forged = vanishing_feldman(&out.payload(3), &[]); + out.replace(3, &forged); + }), + ("missing", |out| out.remove(3)), + ("trailing byte", |out| { + let mut p = out.payload(3); + p.push(0); + out.replace(3, &p); + }), + ("wrong length", |out| { + let mut f = DkgFeldman::decode(&out.payload(3)).unwrap(); + f.commitments.pop(); + out.replace(3, &f.encode()); + }), + ("invalid point", |out| { + let mut p = out.payload(3); + p[3 + 95] ^= 1; + out.replace(3, &p); + }), + ]; + for (name, mutate) in cases { + let mut constants_seen = BTreeMap::new(); + let run = run(2, 3, 0x42, |out| { + if out.round() == 4 { + constants(out, &mut constants_seen); + mutate(out); + } + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3]), "{name}"); + assert_eq!(output.reconstructed, members(&[3]), "{name}"); + assert_eq!( + output.public.master_public_key().encode(), + genuine_mpk(&constants_seen, &output.qual), + "{name}" + ); + } +} + +#[test] +fn r1_equivocation_disqualifies_but_r4_equivocation_reconstructs() { + let mut constants_seen = BTreeMap::new(); + let run = run(3, 5, 0x43, |out| match out.round() { + 1 => { + let second = bad_dealing(out, 5, 1); + out.push(5, &second); + } + 4 => { + constants(out, &mut constants_seen); + let second = vanishing_feldman(&out.payload(4), &[]); + out.push(4, &second); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.disqualified, + BTreeMap::from([(idx(5), DisqualifyReason::Equivocated { round: 1 })]) + ); + assert_eq!(output.qual, members(&[1, 2, 3, 4])); + assert_eq!(output.reconstructed, members(&[4])); + assert_eq!( + output.public.master_public_key().encode(), + genuine_mpk(&constants_seen, &output.qual) + ); +} + +#[test] +fn every_member_sends_r5_and_an_empty_barrier_finishes() { + let mut r5 = Vec::new(); + let run = run(2, 3, 0x44, |out| { + if out.round() == 5 { + for i in 1..=3 { + r5.push(DkgExtractionComplaints::decode(&out.payload(i)).unwrap()); + } + assert_eq!(out.messages.len(), 3); + } + }); + assert!(r5.iter().all(|r| r.complaints.is_empty())); + let output = agreed(&run, &[1, 2, 3]); + assert!(output.reconstructed.is_empty()); + assert_eq!(run.transcript.rounds.len(), 5); +} + +#[test] +fn missing_or_malformed_r5_counts_as_an_empty_list() { + let reference = agreed(&honest(2, 3, 0x45), &[1, 2, 3]); + for malformed in [false, true] { + let run = run(2, 3, 0x45, |out| { + if out.round() == 5 { + if malformed { + out.replace(2, &[1, 0]); + } else { + out.remove(2); + } + } + }); + let output = agreed(&run, &[1, 2, 3]); + assert!(output.reconstructed.is_empty()); + assert_eq!(output.qual, reference.qual); + assert_eq!( + output.public.master_public_key().encode(), + reference.public.master_public_key().encode() + ); + } +} + +#[test] +fn false_extraction_complaints_are_refuted() { + // Member 3 complains against dealer 1 with (a) a Pedersen-invalid pair + // and (b) its genuine, Feldman-valid pair; neither triggers R6. + for genuine in [false, true] { + let mut deal = Vec::new(); + let run = run(2, 3, 0x46, |out| match out.round() { + 1 => deal = out.payload(1), + 5 => { + let value = if genuine { + opened(out.fixture(), &deal, 1, 3) + } else { + pair(5, 6) + }; + let complaint = DkgExtractionComplaints { + complaints: vec![(idx(1), value)], + }; + out.replace(3, &complaint.encode()); + } + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert!(output.reconstructed.is_empty()); + assert!(run.reconstruct.values().all(|r| !r)); + } +} + +#[test] +fn corrupt_dealer_withholding_its_r6_is_recovered_and_kept() { + let mut constants_seen = BTreeMap::new(); + let run = run(2, 3, 0x47, |out| match out.round() { + 4 => { + constants(out, &mut constants_seen); + let forged = vanishing_feldman(&out.payload(3), &[]); + out.replace(3, &forged); + } + 6 => out.remove(3), + _ => {} + }); + let output = agreed(&run, &[1, 2]); + assert_eq!(output.qual, members(&[1, 2, 3])); + assert_eq!(output.reconstructed, members(&[3])); + assert_eq!( + output.public.master_public_key().encode(), + genuine_mpk(&constants_seen, &output.qual) + ); +} + +#[test] +fn invalid_or_equivocated_r6_values_do_not_count() { + // Dealer 3 is faulted; member 2 equivocates in R6 and member 3 sends an + // invalid value. Only member 1's value is valid, below t = 2. + let run = run(2, 3, 0x48, |out| match out.round() { + 4 => { + let forged = vanishing_feldman(&out.payload(3), &[]); + out.replace(3, &forged); + } + 6 => { + let changed = DkgReconstruction { + values: vec![(idx(3), pair(1, 2))], + }; + out.push(2, &changed.encode()); + out.replace(3, &changed.encode()); + } + _ => {} + }); + for i in [1, 2, 3] { + assert!(matches!(run.outcomes[&i], Err(Error::InsufficientRecovery))); + } + assert!(matches!(run.verify(), Err(Error::InsufficientRecovery))); +} + +#[test] +fn insufficient_valid_r6_values_abort_with_a_typed_error() { + let run = run(2, 3, 0x49, |out| match out.round() { + 4 => { + let forged = vanishing_feldman(&out.payload(3), &[]); + out.replace(3, &forged); + } + 6 => { + out.remove(2); + out.remove(3); + } + _ => {} + }); + for i in [1, 2, 3] { + assert!(matches!(run.outcomes[&i], Err(Error::InsufficientRecovery))); + } + assert!(matches!(run.verify(), Err(Error::InsufficientRecovery))); +} + +#[test] +fn fewer_than_t_qualified_dealers_abort_with_a_typed_error() { + let run = run(2, 3, 0x4a, |out| { + if out.round() == 1 { + out.remove(2); + out.remove(3); + } + }); + for i in [1, 2, 3] { + assert!(matches!( + run.outcomes[&i], + Err(Error::InsufficientQualified) + )); + } + assert_eq!(run.transcript.rounds.len(), 3); + assert!(matches!(run.verify(), Err(Error::InsufficientQualified))); +} + +#[test] +fn missing_and_malformed_r1_disqualify() { + let missing = run(3, 5, 0x4b, |out| { + if out.round() == 1 { + out.remove(4); + let mut truncated = out.payload(5); + truncated.pop(); + out.replace(5, &truncated); + } + }); + let output = agreed(&missing, &[1, 2, 3]); + assert_eq!( + output.disqualified, + BTreeMap::from([ + (idx(4), DisqualifyReason::MissingDeal), + (idx(5), DisqualifyReason::MalformedDeal) + ]) + ); + let malformed = run(3, 5, 0x4c, |out| { + if out.round() == 1 { + let mut deal = DkgDeal::decode(&out.payload(5)).unwrap(); + deal.commitments.pop(); + out.replace(5, &deal.encode()); + let mut deal = DkgDeal::decode(&out.payload(4)).unwrap(); + deal.dealings.pop(); + out.replace(4, &deal.encode()); + } + }); + let output = agreed(&malformed, &[1, 2, 3]); + assert_eq!( + output.disqualified, + BTreeMap::from([ + (idx(4), DisqualifyReason::MalformedDeal), + (idx(5), DisqualifyReason::MalformedDeal) + ]) + ); +} + +// --------------------------------------------- authorization and state binding + +#[test] +fn local_recipient_key_and_context_are_checked_before_any_randomness() { + let f = fixture(2, 3, 0x50); + let mut rng = counting(); + assert!(matches!( + dkg_start(&f.ceremony, idx(1), f.secret(2), &mut rng), + Err(Error::ContextMismatch) + )); + assert!(matches!( + dkg_start(&f.ceremony, idx(4), f.secret(1), &mut rng), + Err(Error::InvalidIndex) + )); + let signers = Signers::new(); + let (current, _) = crate::cert::tests::genesis(&signers); + let (_, _, reshare) = crate::cert::tests::successor( + &signers, + ¤t, + CeremonyKind::Reshare, + Threshold::new(2, 3).unwrap(), + 12, + ); + assert!(matches!( + dkg_start(&reshare, idx(1), f.secret(1), &mut rng), + Err(Error::InvalidCeremony) + )); + assert_eq!(rng.draws, 0); + let (state, message) = dkg_start(&f.ceremony, idx(1), f.secret(1), &mut rng).unwrap(); + assert!(rng.draws > 0); + assert_eq!(message.round(), 1); + assert_eq!(format!("{state:?}"), "DkgDealt([REDACTED])"); +} + +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) +} + +#[test] +fn transitions_refuse_other_ceremonies_rounds_and_histories() { + let f = fixture(2, 3, 0x51); + let other = fixture(2, 3, 0x52); + let h0 = dkg_initial_transcript(&f.ceremony); + let start = |i: u16| { + let mut rng = f.member_rng(i); + dkg_start(&f.ceremony, idx(i), f.secret(i), &mut rng).unwrap() + }; + let r1: Vec<_> = (1..=3).map(|i| f.sign(i, &start(i).1)).collect(); + let closed1 = close_for::(&f, &f.ceremony, 1, &h0, &r1).unwrap(); + + // Another authorized ceremony, its own correct history. + let other_r1: Vec<_> = (1..=3) + .map(|i| { + let mut rng = other.member_rng(i); + let (_, m) = dkg_start(&other.ceremony, idx(i), other.secret(i), &mut rng).unwrap(); + other.sign(i, &m) + }) + .collect(); + let foreign = close_for::( + &other, + &other.ceremony, + 1, + &dkg_initial_transcript(&other.ceremony), + &other_r1, + ) + .unwrap(); + assert!(matches!( + start(1).0.complain(&foreign), + Err(Error::InvalidRound) + )); + // The same messages are not accepted under this ceremony's closure. + assert!(close_for::(&f, &f.ceremony, 1, &h0, &other_r1).is_err()); + + // Correct ceremony, wrong previous link. + let rebased = + close_for::(&f, &f.ceremony, 1, &TranscriptHash::new([9; 32]), &r1).unwrap(); + assert!(matches!( + start(1).0.complain(&rebased), + Err(Error::InvalidRound) + )); + + // A round-2 closure presented to the round-1 transition. + let empty = DkgComplaints { dealers: vec![] }.encode(); + let r2_as_deal: Vec<_> = (1..=3) + .map(|i| { + let bytes = + ceremony_message_body(&f.ceremony, 2, MemberCohort::Recipients, idx(i), &empty) + .unwrap(); + let preimage = crate::ceremony::ceremony_message_preimage(&bytes).unwrap(); + SignedStatement { + signature: f.signers.sign(&Signers::did(i as u8), &preimage), + bytes, + } + }) + .collect(); + let wrong_round = close_for::(&f, &f.ceremony, 2, &h0, &r2_as_deal).unwrap(); + assert!(matches!( + start(1).0.complain(&wrong_round), + Err(Error::InvalidRound) + )); + + // Later phase: R3 chained to R1 instead of R2. + let mut states = BTreeMap::new(); + for i in 1..=3u16 { + states.insert(i, start(i).0); + } + let mut complained = BTreeMap::new(); + let mut r2 = Vec::new(); + for (i, s) in states { + let (s, m) = s.complain(&closed1).unwrap(); + r2.push(f.sign(i, &m)); + complained.insert(i, s); + } + let closed2 = + close_for::(&f, &f.ceremony, 2, &closed1.transcript_hash(), &r2).unwrap(); + let mut r3 = Vec::new(); + let mut answered = BTreeMap::new(); + for (i, s) in complained { + let (s, m) = s.answer(&closed2).unwrap(); + r3.push(f.sign(i, &m)); + answered.insert(i, s); + } + let skipped = + close_for::(&f, &f.ceremony, 3, &closed1.transcript_hash(), &r3).unwrap(); + let state = answered.remove(&1).unwrap(); + assert!(matches!(state.extract(&skipped), Err(Error::InvalidRound))); + let chained = + close_for::(&f, &f.ceremony, 3, &closed2.transcript_hash(), &r3).unwrap(); + assert!(answered.remove(&2).unwrap().extract(&chained).is_ok()); +} + +#[test] +fn closed_rounds_reject_wrong_signers_cohorts_and_signatures() { + let f = fixture(2, 3, 0x53); + let h0 = dkg_initial_transcript(&f.ceremony); + let mut rng = f.member_rng(1); + let (_, message) = dkg_start(&f.ceremony, idx(1), f.secret(1), &mut rng).unwrap(); + let genuine = f.sign(1, &message); + assert!(close_for::(&f, &f.ceremony, 1, &h0, &[genuine.clone()]).is_ok()); + + let mut wrong_signer = genuine.clone(); + wrong_signer.signature = f + .signers + .sign(&Signers::did(2), &message.signing_preimage()); + assert!(matches!( + close_for::(&f, &f.ceremony, 1, &h0, &[wrong_signer]), + Err(Error::InvalidSignature) + )); + let mut flipped = genuine.clone(); + flipped.signature[0] ^= 1; + assert!(matches!( + close_for::(&f, &f.ceremony, 1, &h0, &[flipped]), + Err(Error::InvalidSignature) + )); + let mut current_cohort = genuine.clone(); + current_cohort.bytes[34] = MemberCohort::Current as u8; + current_cohort.signature = f.signers.sign( + &Signers::did(1), + &crate::ceremony::ceremony_message_preimage(¤t_cohort.bytes).unwrap(), + ); + assert!(close_for::(&f, &f.ceremony, 1, &h0, &[current_cohort]).is_err()); +} + +// -------------------------------------------------------- public verifier + +#[test] +fn verifier_recomputes_from_authenticated_records_only() { + let run = run(2, 3, 0x54, |out| { + if out.round() == 4 { + let forged = vanishing_feldman(&out.payload(2), &[]); + out.replace(2, &forged); + } + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.reconstructed, members(&[2])); + + let mut tampered = DkgTranscript { + rounds: run.transcript.rounds.clone(), + }; + tampered.rounds[1].messages[0].signature[0] ^= 1; + assert!(dkg_verify_transcript(&run.fixture.ceremony, &tampered, &run.fixture.signers).is_err()); + + let mut swapped = DkgTranscript { + rounds: run.transcript.rounds.clone(), + }; + swapped.rounds.swap(2, 3); + assert!(dkg_verify_transcript(&run.fixture.ceremony, &swapped, &run.fixture.signers).is_err()); + + let other = fixture(2, 3, 0x55); + assert!(dkg_verify_transcript(&other.ceremony, &run.transcript, &run.fixture.signers).is_err()); + + let mut missing_r6 = DkgTranscript { + rounds: run.transcript.rounds.clone(), + }; + missing_r6.rounds.pop(); + assert!(matches!( + dkg_verify_transcript(&run.fixture.ceremony, &missing_r6, &run.fixture.signers), + Err(Error::InvalidRound) + )); + + let plain = honest(2, 3, 0x56); + let mut extra = DkgTranscript { + rounds: plain.transcript.rounds.clone(), + }; + extra.rounds.push(run.transcript.rounds[5].clone()); + assert!(matches!( + dkg_verify_transcript(&plain.fixture.ceremony, &extra, &plain.fixture.signers), + Err(Error::InvalidRound) + )); +} + +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(2, 3, 0x57); + 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 = DkgTranscript { + rounds: vec![record(64)], + }; + assert!(matches!( + dkg_verify_transcript(&f.ceremony, &over, &Refusing), + Err(Error::LimitExceeded) + )); + let split = DkgTranscript { + rounds: vec![record(32), record(32)], + }; + assert!(matches!( + dkg_verify_transcript(&f.ceremony, &split, &Refusing), + Err(Error::LimitExceeded) + )); + let under = DkgTranscript { + rounds: vec![record(63)], + }; + let result = dkg_verify_transcript(&f.ceremony, &under, &f.signers); + assert!(result.is_err()); + assert!(!matches!(result, Err(Error::LimitExceeded))); +} + +// ----------------------------------------------------- within-budget property + +fn misbehave(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); + } + (1, 3) if present => { + let second = bad_dealing(out, m, target); + out.push(m, &second); + } + (1, 4) | (1, 5) if present => { + let forged = bad_dealing(out, m, target); + out.replace(m, &forged); + } + (2, 6) => out.replace( + m, + &DkgComplaints { + dealers: vec![idx(target)], + } + .encode(), + ), + (2, 7) => out.push(m, &[1, 0, 1, 0, target as u8]), + (3, 5) => out.remove(m), + (3, 8) => out.replace( + m, + &DkgAnswers { + answers: vec![(idx(target), pair(5, 6))], + } + .encode(), + ), + (4, 9) if present => { + let forged = vanishing_feldman(&out.payload(m), &[m]); + out.replace(m, &forged); + } + (4, 10) => out.remove(m), + (4, 11) if present => { + let mut p = out.payload(m); + p.push(0); + out.replace(m, &p); + } + (4, 12) if present => { + let second = vanishing_feldman(&out.payload(m), &[]); + out.push(m, &second); + } + (5, 13) => out.replace( + m, + &DkgExtractionComplaints { + complaints: vec![(idx(target), pair(5, 6))], + } + .encode(), + ), + (6, 13) | (5, 14) | (6, 14) => out.remove(m), + _ => {} + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(12))] + + #[test] + fn within_budget_fault_patterns_finish_with_identical_outcomes( + shape in 0usize..2, + seed in any::(), + behaviours in proptest::collection::vec(0u8..15, 2), + ) { + let (t, n) = [(2u16, 3u16), (3, 5)][shape]; + let honest_members: Vec = (1..=n - (t - 1)).collect(); + let faulty: Vec = (n - (t - 1) + 1..=n).collect(); + let mut constants_seen = BTreeMap::new(); + let run = run(t, n, seed, |out| { + if out.round() == 4 { + constants(out, &mut constants_seen); + } + for (k, m) in faulty.iter().enumerate() { + misbehave(out, *m, behaviours[k], 1); + } + }); + let output = agreed(&run, &honest_members); + let faulty_set = members(&faulty); + prop_assert!(members(&honest_members).is_subset(&output.qual)); + prop_assert!(output.disqualified.keys().all(|i| faulty_set.contains(i))); + prop_assert!(output.reconstructed.is_subset(&faulty_set)); + prop_assert_eq!( + output.public.master_public_key().encode(), + genuine_mpk(&constants_seen, &output.qual) + ); + } +} + +// ------------------------------------------------------------------ vectors + +fn hex_statement(s: &SignedStatement) -> Value { + json!({"body": hex::encode(&s.bytes), "signature": hex::encode(&s.signature)}) +} + +fn reason(r: &DisqualifyReason) -> Value { + match r { + DisqualifyReason::MissingDeal => json!({"reason": "MissingDeal"}), + DisqualifyReason::MalformedDeal => json!({"reason": "MalformedDeal"}), + DisqualifyReason::Equivocated { round } => { + json!({"reason": "Equivocated", "round": round}) + } + DisqualifyReason::Complaints => json!({"reason": "Complaints"}), + DisqualifyReason::MissingAnswer => json!({"reason": "MissingAnswer"}), + DisqualifyReason::InvalidAnswer => json!({"reason": "InvalidAnswer"}), + } +} + +fn output_json(o: &DkgOutput) -> Value { + let n = o.public.threshold().n(); + json!({ + "qual": o.qual.iter().map(|i| i.get()).collect::>(), + "disqualified": o.disqualified.iter().map(|(i, r)| { + let mut entry = reason(r); + entry["member"] = json!(i.get()); + entry + }).collect::>(), + "reconstructed": o.reconstructed.iter().map(|i| i.get()).collect::>(), + "mpk": hex::encode(o.public.master_public_key().encode()), + "commitments": o.public.commitments().map(hex::encode).collect::>(), + "public_shares": (1..=n).map(|i| json!({ + "member": i, + "point": hex::encode(o.public.public_share(idx(i)).unwrap()), + })).collect::>(), + "transcript_hash": hex::encode(o.transcript_hash.as_bytes()), + }) +} + +fn case(name: &str, description: &str, run: &Run) -> Value { + let inputs = json!({ + "threshold": run.fixture.t(), + "members": run.fixture.n(), + "ceremony": hex_statement(&run.fixture.statement), + "ceremony_digest": hex::encode(run.fixture.ceremony.digest()), + "initial_transcript": hex::encode(dkg_initial_transcript(&run.fixture.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": "dkg_verify_transcript", "inputs": inputs, "outputs": output_json(&output)}) + } + Err(error) => { + json!({"name": name, "description": description, "operation": "dkg_verify_transcript", "inputs": inputs, "error": format!("{error:?}")}) + } + } +} + +fn vectors() -> Value { + let honest_run = honest(2, 3, 0x61); + let selective = run(2, 3, 0x62, |out| { + if out.round() == 4 { + let forged = vanishing_feldman(&out.payload(3), &[1]); + out.replace(3, &forged); + } + }); + let two_faulty = run(3, 5, 0x63, |out| match out.round() { + 4 => { + let forged = vanishing_feldman(&out.payload(5), &[4, 5]); + out.replace(5, &forged); + } + 5 => out.replace( + 4, + &DkgExtractionComplaints { + complaints: vec![(idx(1), pair(11, 12))], + } + .encode(), + ), + 6 => { + out.remove(4); + out.remove(5); + } + _ => {} + }); + let answers = run(3, 5, 0x64, |out| match out.round() { + 1 => { + let forged = bad_dealing(out, 1, 2); + out.replace(1, &forged); + let second = bad_dealing(out, 4, 3); + out.push(4, &second); + let forged = bad_dealing(out, 5, 1); + out.replace(5, &forged); + } + 3 => out.remove(5), + _ => {} + }); + let insufficient = run(2, 3, 0x65, |out| match out.round() { + 4 => { + let forged = vanishing_feldman(&out.payload(3), &[]); + out.replace(3, &forged); + } + 6 => { + out.remove(2); + out.remove(3); + } + _ => {} + }); + let unqualified = run(2, 3, 0x66, |out| { + if out.round() == 1 { + out.remove(2); + out.remove(3); + } + }); + let cases = vec![ + case("genesis_2of3", "All members follow the protocol; R5 is empty and no R6 round exists.", &honest_run), + case("genesis_2of3_selective_extraction", "Dealer 3's R4 vector passes only member 1's check; member 2's R5 complaint leads to R6 reconstruction of dealer 3.", &selective), + case("genesis_3of5_two_faulty", "Dealer 5's R4 passes only at members 4 and 5; member 4 files an invalid R5 complaint; members 4 and 5 send no R6.", &two_faulty), + case("genesis_3of5_answers_and_disqualification", "Dealer 1's bad value to member 2 is answered; dealer 4 sends two R1 messages; dealer 5 does not answer member 1's complaint.", &answers), + case("insufficient_reconstruction_values", "Only member 1 sends R6 for faulted dealer 3.", &insufficient), + case("insufficient_qualified_dealers", "Only dealer 1 publishes R1.", &unqualified), + ]; + let signers: Vec<_> = (0..=5u8) + .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 Pedersen/Feldman GJKR New-DKG; RFC 9180 base mode DHKEM(X25519, HKDF-SHA256)/HKDF-SHA256/ChaCha20-Poly1305 dealings; Ed25519 test signatures", + "spec_sections": ["3.2", "3.3", "3.7", "3.10", "S0.3 DKG appendix"], + "encoding": "hex; statements are exact signed bytes; scalars 32-byte big-endian; G2 96-byte compressed", + "seeds": {"ceremonies": [0x61, 0x62, 0x63, 0x64, 0x65, 0x66]}, + "dst": { + "dkg_start": String::from_utf8_lossy(DKG_START), + "pedersen_h": String::from_utf8_lossy(dst::PEDERSEN_H), + "pedersen_h_message": "pedersen-h", + "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), + }, + "anchor": {"owner": anchor.expected_owner.as_str(), "network": anchor.network.as_str()}, + "signers": signers, + "cases": cases, + }) +} + +#[test] +fn dkg_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/dkg-genesis.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 ceremony = AuthorizedCeremony::verify_genesis( + &signers.anchor(), + &statement(&inputs["ceremony"]), + &signers, + ) + .unwrap(); + assert_eq!( + dkg_initial_transcript(&ceremony).as_bytes().to_vec(), + bytes(&inputs["initial_transcript"]) + ); + let transcript = DkgTranscript { + 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 dkg_verify_transcript(&ceremony, &transcript, &signers) { + Ok(output) => assert_eq!(output_json(&output), case["outputs"], "{}", case["name"]), + Err(error) => assert_eq!(format!("{error:?}"), case["error"].as_str().unwrap()), + } + } +} + +#[test] +fn pedersen_generator_is_the_frozen_hash_to_g2_output() { + let fixture: Value = + serde_json::from_str(include_str!("../vectors/v1/primitives.json")).unwrap(); + let case = fixture["cases"] + .as_array() + .unwrap() + .iter() + .find(|c| c["name"] == "pedersen_h") + .unwrap(); + assert_eq!( + hex::encode(pedersen_h().encode()), + case["outputs"]["point"].as_str().unwrap() + ); + // The relay's commitment check: every member's Pedersen relation holds. + let run = honest(2, 3, 0x58); + let deal = DkgDeal::decode(&run.transcript.rounds[0].messages[0].bytes[41..]).unwrap(); + let opened_pair = opened( + &run.fixture, + &run.transcript.rounds[0].messages[0].bytes[41..], + 1, + 2, + ); + let x = Scalar::from_u64(2); + let expected = deal + .commitments + .iter() + .rev() + .fold(G2::identity(), |acc, c| acc.mul(&x).add(c)); + assert_eq!( + G2::generator() + .mul(&opened_pair.value) + .add(&pedersen_h().mul(&opened_pair.blinding)), + expected + ); +} + +#[test] +fn certificate_binding_uses_the_dkg_output() { + let run = honest(2, 3, 0x59); + let output = agreed(&run, &[1, 2, 3]); + let f = &run.fixture; + let body = CertificateBody::genesis( + &f.signers.anchor(), + &f.ceremony, + &output.public, + &output.transcript_hash, + ) + .unwrap(); + let cert = verify_certificate( + &f.signers.anchor(), + &crate::cert::tests::sign(body, &f.signers), + &f.signers, + ) + .unwrap(); + let Run { outcomes, .. } = run; + for (i, outcome) in outcomes { + let (candidate, _) = outcome.unwrap(); + let share = KeyShare::bind(candidate, &cert).unwrap(); + assert_eq!(share.index(), idx(i)); + } +} diff --git a/tinycloud-threshold/src/lib.rs b/tinycloud-threshold/src/lib.rs index a864b3bb..0d7ed9d2 100644 --- a/tinycloud-threshold/src/lib.rs +++ b/tinycloud-threshold/src/lib.rs @@ -21,6 +21,7 @@ pub mod context; #[allow(unsafe_code)] pub mod curve; pub mod derive; +pub mod dkg; pub mod dst; mod encoding; pub mod error; @@ -50,6 +51,12 @@ pub use cert::{ }; pub use context::{AuthorizedDerivation, KeyContext}; pub use derive::*; +pub use dkg::{ + dkg_initial_transcript, dkg_start, dkg_verify_transcript, AfterBarrier, DisqualifyReason, + DkgAnswered, DkgAnswers, DkgAwaitingBarrier, DkgComplained, DkgComplaints, DkgDeal, DkgDealt, + DkgExtracting, DkgExtractionComplaints, DkgFeldman, DkgMessage, DkgOutput, DkgReconstructing, + DkgReconstruction, DkgRoundRecord, DkgTranscript, +}; pub use error::{CertError, CombineError, Error, ShareError, SignatureError}; pub use fault::{verify_fault_proof, FaultProof, FaultVerdict, NotAFaultReason}; pub use ibe::*; @@ -60,4 +67,6 @@ pub use seal::{CeremonyHpkePublicKey, CeremonyHpkeSecretKey, SealedDealing}; #[cfg(test)] mod checkpoint; #[cfg(test)] +mod dkg_relay; +#[cfg(test)] mod vectors; From 23849d717d9fd0a0b0dda6c26591234515fd3548 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:17:54 +0000 Subject: [PATCH 03/15] feat(threshold): derive ceremony recipient public keys; Details: TC-2220 --- tinycloud-threshold/src/seal.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tinycloud-threshold/src/seal.rs b/tinycloud-threshold/src/seal.rs index 48859985..86900cd8 100644 --- a/tinycloud-threshold/src/seal.rs +++ b/tinycloud-threshold/src/seal.rs @@ -47,6 +47,16 @@ impl CeremonyHpkeSecretKey { public.write_exact(&mut public_bytes); (owned, CeremonyHpkePublicKey(public_bytes)) } + + /// Derive this ceremony recipient's public key. Details: TC-2220 + pub(crate) fn public_key(&self) -> Result { + let secret = ::PrivateKey::from_bytes(&self.bytes) + .map_err(|_| Error::InvalidHpkeKey)?; + let public = HpkeKem::sk_to_pk(&secret); + let mut public_bytes = [0; 32]; + public.write_exact(&mut public_bytes); + Ok(CeremonyHpkePublicKey(public_bytes)) + } } impl fmt::Debug for CeremonyHpkeSecretKey { From fd9073b8ee20dcadcdc7dde6a7a36860961e8f94 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:21:47 +0000 Subject: [PATCH 04/15] feat(threshold): add genesis DKG over owner-closed rounds; Details: TC-2220 --- tinycloud-threshold/src/curve.rs | 4 - tinycloud-threshold/src/dkg.rs | 1233 ++++++++++++++++++++++++++++++ tinycloud-threshold/src/error.rs | 6 + tinycloud-threshold/src/lib.rs | 7 - tinycloud-threshold/src/poly.rs | 1 - 5 files changed, 1239 insertions(+), 12 deletions(-) diff --git a/tinycloud-threshold/src/curve.rs b/tinycloud-threshold/src/curve.rs index 0c35e726..36c91954 100644 --- a/tinycloud-threshold/src/curve.rs +++ b/tinycloud-threshold/src/curve.rs @@ -98,7 +98,6 @@ macro_rules! scalar_binary { } impl Scalar { - #[cfg(test)] pub(crate) fn from_bytes(bytes: &[u8]) -> Result { let bytes: &[u8; 32] = bytes.try_into().map_err(|_| Error::InvalidLength)?; let mut out = Self(blst_scalar::default()); @@ -113,7 +112,6 @@ impl Scalar { } /// The returned secret bytes must be guarded by their caller. - #[cfg(test)] pub(crate) fn to_bytes(&self) -> [u8; 32] { let mut out = [0; 32]; // SAFETY: exactly 32 writable bytes; canonical initialized scalar. @@ -162,7 +160,6 @@ impl Scalar { bool::from(self.0.b.ct_eq(&[0; 32])) } - #[cfg(test)] scalar_binary!(add, blst_fr_add); scalar_binary!(sub, blst_fr_sub); scalar_binary!(mul, blst_fr_mul); @@ -379,7 +376,6 @@ pub(crate) fn hash_g1(msg: &[u8], dst: &[u8]) -> G1 { } out } -#[cfg(test)] pub(crate) fn hash_g2(msg: &[u8], dst: &[u8]) -> G2 { let mut out = G2::identity(); // SAFETY: readable pointer-length pairs, no augmentation, initialized output. diff --git a/tinycloud-threshold/src/dkg.rs b/tinycloud-threshold/src/dkg.rs index 80f35c0e..591b4a2e 100644 --- a/tinycloud-threshold/src/dkg.rs +++ b/tinycloud-threshold/src/dkg.rs @@ -1,4 +1,1237 @@ //! Genesis GJKR New-DKG over owner-closed rounds. Details: TC-2220 +//! +//! Members consume owner-closed rounds in a fixed order through consume-self +//! states; `dkg_verify_transcript` applies the same public rules to the +//! authenticated round records. Liveness under the genesis budget 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, SignedStatement, + }, + cert::{CandidateShare, PublicMaterial}, + curve::{hash_g2, Scalar, G2}, + dst, + encoding::{Decoder, Encoder}, + error::Error, + ids::{MemberIndex, Threshold, TranscriptHash}, + poly, + seal::{ + open_dealing, seal_dealing, CeremonyHpkePublicKey, CeremonyHpkeSecretKey, DealingContext, + SealedDealing, + }, +}; + +/// Initial transcript link tag; not part of the frozen S0.1 domain table. Details: TC-2220 +pub(crate) const DKG_START: &[u8] = b"TC-THRESHOLD-V1-DKG-START"; +const PEDERSEN_H_MESSAGE: &[u8] = b"pedersen-h"; +const VERSION: u8 = 1; +const MAX_ENTRIES: u16 = 16; +const MESSAGE_CAP: usize = 65_536; +const TRANSCRIPT_CAP: usize = 4_194_304; +const PLAINTEXT_LEN: usize = 64; + +const DEAL: u8 = 1; +const COMPLAIN: u8 = 2; +const ANSWER: u8 = 3; +const EXTRACT: u8 = 4; +const EXTRACTION_COMPLAINTS: u8 = 5; +const RECONSTRUCT: u8 = 6; + +/// `TH(DKG_START, authorized_ceremony_digest)`, the link preceding closed R1. Details: TC-2220 +pub fn dkg_initial_transcript(c: &AuthorizedCeremony) -> TranscriptHash { + TranscriptHash::new(dst::tagged_hash(DKG_START, &c.digest())) +} + +// ------------------------------------------------------------------ payloads + +/// A scalar value and its Pedersen blinding value at one member index. Details: TC-2220 +pub(crate) struct SharePair { + pub(crate) value: Scalar, + pub(crate) blinding: Scalar, +} + +impl SharePair { + fn duplicate(&self) -> Self { + let zero = Scalar::from_u64(0); + Self { + value: self.value.add(&zero), + blinding: self.blinding.add(&zero), + } + } + + fn encode_into(&self, e: &mut Encoder) { + e.fixed(Zeroizing::new(self.value.to_bytes()).as_slice()); + e.fixed(Zeroizing::new(self.blinding.to_bytes()).as_slice()); + } + + fn decode_from(d: &mut Decoder) -> Result { + let value = Zeroizing::new(d.fixed::<32>()?); + let blinding = Zeroizing::new(d.fixed::<32>()?); + Ok(Self { + value: Scalar::from_bytes(value.as_slice())?, + blinding: Scalar::from_bytes(blinding.as_slice())?, + }) + } +} + +impl fmt::Debug for SharePair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SharePair([REDACTED])") + } +} + +fn version(d: &mut Decoder) -> Result<(), Error> { + if d.u8()? != VERSION { + return Err(Error::InvalidEncoding); + } + Ok(()) +} + +fn count(d: &mut Decoder) -> Result { + let count = d.u16()?; + if count > MAX_ENTRIES { + return Err(Error::LimitExceeded); + } + Ok(usize::from(count)) +} + +fn index_after(d: &mut Decoder, previous: &mut Option) -> Result { + let index = MemberIndex::new(d.u16()?)?; + if previous.is_some_and(|p| index <= p) { + return Err(Error::InvalidEncoding); + } + *previous = Some(index); + Ok(index) +} + +fn encode_points(e: &mut Encoder, points: &[G2]) { + e.u16(points.len() as u16); + for point in points { + e.fixed(&point.encode()); + } +} + +fn decode_points(d: &mut Decoder) -> Result, Error> { + let count = count(d)?; + let mut points = Vec::with_capacity(count); + for _ in 0..count { + points.push(G2::decode(&d.fixed::<96>()?, true)?); + } + Ok(points) +} + +fn encode_pairs(entries: &[(MemberIndex, SharePair)]) -> Vec { + let mut e = Encoder::new(); + e.u8(VERSION); + e.u16(entries.len() as u16); + for (index, pair) in entries { + e.u16(index.get()); + pair.encode_into(&mut e); + } + e.finish() +} + +fn decode_pairs(bytes: &[u8]) -> Result, Error> { + let mut d = Decoder::new(bytes)?; + version(&mut d)?; + let count = count(&mut d)?; + let mut entries = Vec::with_capacity(count); + let mut previous = None; + for _ in 0..count { + let index = index_after(&mut d, &mut previous)?; + entries.push((index, SharePair::decode_from(&mut d)?)); + } + d.finish()?; + Ok(entries) +} + +/// R1: Pedersen commitments and one sealed dealing per recipient. Details: TC-2220 +#[derive(Debug)] +pub struct DkgDeal { + pub(crate) commitments: Vec, + pub(crate) dealings: Vec<(MemberIndex, SealedDealing)>, +} + +impl DkgDeal { + /// `01 || t:u16 || C_k:96… || n:u16 || [(recipient:u16 || lp(sealed))]`. Details: TC-2220 + pub fn encode(&self) -> Vec { + let mut e = Encoder::new(); + e.u8(VERSION); + encode_points(&mut e, &self.commitments); + e.u16(self.dealings.len() as u16); + for (index, dealing) in &self.dealings { + e.u16(index.get()); + e.lp(&dealing.encode()); + } + e.finish() + } +} + +impl Decode for DkgDeal { + fn decode(bytes: &[u8]) -> Result { + let mut d = Decoder::new(bytes)?; + version(&mut d)?; + let commitments = decode_points(&mut d)?; + let count = count(&mut d)?; + let mut dealings = Vec::with_capacity(count); + let mut previous = None; + for _ in 0..count { + let index = index_after(&mut d, &mut previous)?; + dealings.push((index, SealedDealing::decode(d.lp(MESSAGE_CAP)?)?)); + } + d.finish()?; + Ok(Self { + commitments, + dealings, + }) + } +} + +/// R2: dealers whose dealing failed for the sender. Details: TC-2220 +#[derive(Debug)] +pub struct DkgComplaints { + pub(crate) dealers: Vec, +} + +impl DkgComplaints { + /// `01 || count:u16 || [dealer:u16]`, strictly increasing. Details: TC-2220 + pub fn encode(&self) -> Vec { + let mut e = Encoder::new(); + e.u8(VERSION); + e.u16(self.dealers.len() as u16); + for dealer in &self.dealers { + e.u16(dealer.get()); + } + e.finish() + } +} + +impl Decode for DkgComplaints { + fn decode(bytes: &[u8]) -> Result { + let mut d = Decoder::new(bytes)?; + version(&mut d)?; + let count = count(&mut d)?; + let mut dealers = Vec::with_capacity(count); + let mut previous = None; + for _ in 0..count { + dealers.push(index_after(&mut d, &mut previous)?); + } + d.finish()?; + Ok(Self { dealers }) + } +} + +/// R3: a dealer's published pairs for complaining recipients. Details: TC-2220 +#[derive(Debug)] +pub struct DkgAnswers { + pub(crate) answers: Vec<(MemberIndex, SharePair)>, +} + +impl DkgAnswers { + /// `01 || count:u16 || [(recipient:u16 || value:32 || blinding:32)]`. Details: TC-2220 + pub fn encode(&self) -> Vec { + encode_pairs(&self.answers) + } +} + +impl Decode for DkgAnswers { + fn decode(bytes: &[u8]) -> Result { + Ok(Self { + answers: decode_pairs(bytes)?, + }) + } +} + +/// R4: a qualified dealer's Feldman coefficients. Details: TC-2220 +#[derive(Debug)] +pub struct DkgFeldman { + pub(crate) commitments: Vec, +} + +impl DkgFeldman { + /// `01 || t:u16 || A_k:96…`. Details: TC-2220 + pub fn encode(&self) -> Vec { + let mut e = Encoder::new(); + e.u8(VERSION); + encode_points(&mut e, &self.commitments); + e.finish() + } +} + +impl Decode for DkgFeldman { + fn decode(bytes: &[u8]) -> Result { + let mut d = Decoder::new(bytes)?; + version(&mut d)?; + let commitments = decode_points(&mut d)?; + d.finish()?; + Ok(Self { commitments }) + } +} + +/// R5: the sender's Pedersen-valid, Feldman-invalid pairs by dealer. Details: TC-2220 +#[derive(Debug)] +pub struct DkgExtractionComplaints { + pub(crate) complaints: Vec<(MemberIndex, SharePair)>, +} + +impl DkgExtractionComplaints { + /// Same layout as R3, keyed by dealer. Details: TC-2220 + pub fn encode(&self) -> Vec { + encode_pairs(&self.complaints) + } +} + +impl Decode for DkgExtractionComplaints { + fn decode(bytes: &[u8]) -> Result { + Ok(Self { + complaints: decode_pairs(bytes)?, + }) + } +} + +/// R6: the sender's pairs for every reconstructed dealer. Details: TC-2220 +#[derive(Debug)] +pub struct DkgReconstruction { + pub(crate) values: Vec<(MemberIndex, SharePair)>, +} + +impl DkgReconstruction { + /// Same layout as R3, keyed by dealer. Details: TC-2220 + pub fn encode(&self) -> Vec { + encode_pairs(&self.values) + } +} + +impl Decode for DkgReconstruction { + fn decode(bytes: &[u8]) -> Result { + Ok(Self { + values: decode_pairs(bytes)?, + }) + } +} + +/// An unsigned member envelope for one round; the node signs `signing_preimage`. Details: TC-2220 +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DkgMessage { + round: u8, + body: Vec, +} + +impl DkgMessage { + fn new( + c: &AuthorizedCeremony, + round: u8, + me: MemberIndex, + payload: &[u8], + ) -> Result { + Ok(Self { + round, + body: ceremony_message_body(c, round, MemberCohort::Recipients, me, payload)?, + }) + } + /// Round number of this envelope. Details: TC-2220 + pub fn round(&self) -> u8 { + self.round + } + /// Exact canonical member envelope bytes. Details: TC-2220 + pub fn body(&self) -> &[u8] { + &self.body + } + /// `TH(CEREMONY_MSG, body)`, signed with the member's node key. Details: TC-2220 + pub fn signing_preimage(&self) -> Vec { + dst::tagged_hash(dst::CEREMONY_MSG, &self.body).to_vec() + } +} + +// --------------------------------------------------------------- outputs + +/// Why a dealer is outside QUAL. Details: TC-2220 +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DisqualifyReason { + /// No R1 message in closed R1. Details: TC-2220 + MissingDeal, + /// R1 failed to decode or had the wrong commitment count or recipient set. Details: TC-2220 + MalformedDeal, + /// Conflicting authenticated content in R1, R2 or R3. Details: TC-2220 + Equivocated { + /// Round of the conflicting messages. Details: TC-2220 + round: u8, + }, + /// At least t distinct recipients complained. Details: TC-2220 + Complaints, + /// A required answer is absent from closed R3. Details: TC-2220 + MissingAnswer, + /// A published answer fails the Pedersen relation. Details: TC-2220 + InvalidAnswer, +} + +/// Public outcome shared by members and the transcript verifier. Details: TC-2220 +#[derive(Clone, Debug)] +pub struct DkgOutput { + /// Summed Feldman polynomial, master key and public shares. Details: TC-2220 + pub public: PublicMaterial, + /// Qualified dealers fixed after closed R3. Details: TC-2220 + pub qual: BTreeSet, + /// Dealers outside QUAL with the first applicable reason. Details: TC-2220 + pub disqualified: BTreeMap, + /// QUAL dealers whose Feldman coefficients were recovered from R6. Details: TC-2220 + pub reconstructed: BTreeSet, + /// Transcript link after the last closed round. Details: TC-2220 + pub transcript_hash: TranscriptHash, +} + +/// One owner closure and its exact ordered member statements. Details: TC-2220 +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DkgRoundRecord { + /// Signed owner closure. Details: TC-2220 + pub closure: SignedStatement, + /// Member statements in owner order. Details: TC-2220 + pub messages: Vec, +} + +/// Raw signed rounds R1..R5, plus R6 when reconstruction is required. Details: TC-2220 +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DkgTranscript { + /// Round records in order. Details: TC-2220 + 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 authenticated messages by sender; identical envelope bytes are one message. +fn by_sender(round: &ClosedRound) -> BTreeMap> { + let mut senders = BTreeMap::new(); + for message in round.messages() { + match senders.entry(message.member()) { + Entry::Vacant(slot) => { + 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 +} + +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 pedersen_h() -> G2 { + hash_g2(PEDERSEN_H_MESSAGE, dst::PEDERSEN_H) +} + +fn commitment_hash(points: &[G2]) -> [u8; 32] { + let mut e = Encoder::new(); + encode_points(&mut e, points); + dst::tagged_hash(dst::COMMITMENTS, &e.finish()) +} + +fn times_linear(polynomial: &[Scalar], root: &Scalar) -> Vec { + let zero = Scalar::from_u64(0); + let below = core::iter::once(&zero).chain(polynomial.iter()); + let at = polynomial.iter().map(Some).chain(core::iter::once(None)); + below + .zip(at) + .map(|(below, at)| match at { + Some(at) => below.sub(&at.mul(root)), + None => below.add(&zero), + }) + .collect() +} + +/// Coefficients of the unique polynomial through the supplied distinct points. +fn interpolate(points: &[(Scalar, &Scalar)]) -> Result, Error> { + let mut coefficients: Vec = points.iter().map(|_| Scalar::from_u64(0)).collect(); + for (m, (xm, ym)) in points.iter().enumerate() { + let mut basis = vec![Scalar::from_u64(1)]; + let mut denominator = Scalar::from_u64(1); + for (l, (xl, _)) in points.iter().enumerate() { + if l != m { + basis = times_linear(&basis, xl); + denominator = denominator.mul(&xm.sub(xl)); + } + } + let scale = ym.mul(&denominator.inverse()?); + for (coefficient, b) in coefficients.iter_mut().zip(&basis) { + *coefficient = coefficient.add(&b.mul(&scale)); + } + } + Ok(coefficients) +} + +/// Public ceremony state; members and the verifier apply identical rules. +struct Public { + ceremony: AuthorizedCeremony, + h: G2, + previous: TranscriptHash, + /// Valid R1 commitments of dealers not yet disqualified; QUAL after R3. + commitments: BTreeMap>, + disqualified: BTreeMap, + complaints: BTreeMap>, + equivocated: BTreeSet, + feldman: BTreeMap>, + faulted: BTreeSet, +} + +impl Public { + fn new(c: &AuthorizedCeremony) -> Result { + if c.kind() != CeremonyKind::Genesis { + return Err(Error::InvalidCeremony); + } + c.threshold().check_genesis_budget()?; + Ok(Self { + ceremony: c.clone(), + h: pedersen_h(), + previous: dkg_initial_transcript(c), + commitments: BTreeMap::new(), + disqualified: BTreeMap::new(), + complaints: BTreeMap::new(), + equivocated: BTreeSet::new(), + feldman: BTreeMap::new(), + faulted: BTreeSet::new(), + }) + } + + fn threshold(&self) -> Threshold { + self.ceremony.threshold() + } + + fn t(&self) -> usize { + usize::from(self.threshold().t()) + } + + fn members(&self) -> impl Iterator + '_ { + self.ceremony.recipients().iter().map(|r| r.index) + } + + fn in_cohort(&self, index: MemberIndex) -> bool { + index.get() <= self.threshold().n() + } + + /// 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 pedersen(&self, pair: &SharePair, commitments: &[G2], x: MemberIndex) -> bool { + G2::generator() + .mul(&pair.value) + .add(&self.h.mul(&pair.blinding)) + == evaluate_points(commitments, x) + } + + fn deals<'r>( + &mut self, + r1: &'r ClosedRound, + ) -> Result, Error> { + self.bind(r1, DEAL)?; + let senders = by_sender(r1); + let t = self.t(); + let n = self.threshold().n(); + let mut valid = BTreeMap::new(); + for i in self.members().collect::>() { + let reason = match senders.get(&i).copied() { + None => Some(DisqualifyReason::MissingDeal), + Some(Sent::Conflicting) => Some(DisqualifyReason::Equivocated { round: DEAL }), + Some(Sent::One(message)) => match message.payload() { + Ok(deal) + if deal.commitments.len() == t + && deal.dealings.iter().map(|(j, _)| j.get()).eq(1..=n) => + { + self.commitments.insert(i, deal.commitments.clone()); + valid.insert(i, deal); + None + } + _ => Some(DisqualifyReason::MalformedDeal), + }, + }; + if let Some(reason) = reason { + self.disqualified.insert(i, reason); + } + } + Ok(valid) + } + + fn complaints(&mut self, r2: &ClosedRound) -> Result<(), Error> { + self.bind(r2, COMPLAIN)?; + for (j, sent) in by_sender(r2) { + match sent { + Sent::Conflicting => { + self.equivocated.insert(j); + } + Sent::One(message) => { + let Ok(list) = message.payload() else { + continue; + }; + if !list.dealers.iter().all(|i| self.in_cohort(*i)) { + continue; + } + for i in list.dealers.iter().filter(|i| **i != j) { + self.complaints.entry(*i).or_default().insert(j); + } + } + } + } + Ok(()) + } + + /// Fix QUAL; 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); + 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 answers = decoded(senders.get(i).copied()) + .filter(|a| a.answers.iter().all(|(j, _)| self.in_cohort(*j))); + let reason = if self.equivocated.contains(i) { + Some(DisqualifyReason::Equivocated { round: COMPLAIN }) + } else if matches!(senders.get(i), Some(Sent::Conflicting)) { + Some(DisqualifyReason::Equivocated { round: ANSWER }) + } else if required.len() >= self.t() { + Some(DisqualifyReason::Complaints) + } else if answers.is_some_and(|a| { + a.answers + .iter() + .any(|(j, pair)| !self.pedersen(pair, commitments, *j)) + }) { + Some(DisqualifyReason::InvalidAnswer) + } else if !required + .iter() + .all(|j| answers.is_some_and(|a| a.answers.iter().any(|(k, _)| k == j))) + { + Some(DisqualifyReason::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((_, pair)) = answers.answers.iter().find(|(j, _)| *j == me) + { + replacements.insert(*i, pair.duplicate()); + } + } + } + } + } + } + for (i, reason) in removed { + self.commitments.remove(&i); + self.disqualified.insert(i, reason); + } + if self.commitments.len() < self.t() { + return Err(Error::InsufficientQualified); + } + Ok(replacements) + } + + fn qual(&self) -> BTreeSet { + self.commitments.keys().copied().collect() + } + + fn extract(&mut self, r4: &ClosedRound) -> Result<(), Error> { + self.bind(r4, EXTRACT)?; + let senders = by_sender(r4); + let t = self.t(); + for i in self.qual() { + match decoded(senders.get(&i).copied()) { + Some(feldman) if feldman.commitments.len() == t => { + self.feldman.insert(i, feldman.commitments.clone()); + } + _ => { + self.faulted.insert(i); + } + } + } + Ok(()) + } + + /// Apply closed R5; return whether R6 is required. + fn barrier(&mut self, r5: &ClosedRound) -> Result { + self.bind(r5, EXTRACTION_COMPLAINTS)?; + let senders = by_sender(r5); + let mut upheld = BTreeSet::new(); + for (j, sent) in &senders { + let Some(list) = decoded(Some(*sent)) else { + continue; + }; + for (i, pair) in &list.complaints { + if i == j || self.faulted.contains(i) { + continue; + } + let (Some(commitments), Some(feldman)) = + (self.commitments.get(i), self.feldman.get(i)) + else { + continue; + }; + if self.pedersen(pair, commitments, *j) + && G2::generator().mul(&pair.value) != evaluate_points(feldman, *j) + { + upheld.insert(*i); + } + } + } + self.faulted.extend(upheld); + Ok(!self.faulted.is_empty()) + } + + fn reconstruct(&mut self, r6: &ClosedRound) -> Result<(), Error> { + self.bind(r6, RECONSTRUCT)?; + let senders = by_sender(r6); + let t = self.t(); + for i in self.faulted.clone() { + let commitments = self.commitments.get(&i).ok_or(Error::InvalidRound)?; + let points: Vec<(Scalar, &Scalar)> = senders + .iter() + .filter_map(|(m, sent)| { + let list = decoded(Some(*sent))?; + let (_, pair) = list.values.iter().find(|(k, _)| *k == i)?; + self.pedersen(pair, commitments, *m) + .then(|| (Scalar::from_u64(u64::from(m.get())), &pair.value)) + }) + .take(t) + .collect(); + if points.len() < t { + return Err(Error::InsufficientRecovery); + } + let coefficients = interpolate(&points)?; + let recovered = coefficients + .iter() + .map(|a| G2::generator().mul(a)) + .collect(); + self.feldman.insert(i, recovered); + } + Ok(()) + } + + fn output(&self) -> Result { + let mut sums: Vec = (0..self.t()).map(|_| G2::identity()).collect(); + for i in self.commitments.keys() { + let feldman = self.feldman.get(i).ok_or(Error::InvalidRound)?; + for (sum, coefficient) in sums.iter_mut().zip(feldman) { + *sum = sum.add(coefficient); + } + } + Ok(DkgOutput { + public: PublicMaterial::from_commitments(self.threshold(), sums)?, + qual: self.qual(), + disqualified: self.disqualified.clone(), + reconstructed: self.faulted.clone(), + transcript_hash: self.previous, + }) + } +} + +// ------------------------------------------------------------ member states + +struct Polynomial { + values: Vec, + blinding: Vec, +} + +impl Polynomial { + fn at(&self, x: MemberIndex) -> SharePair { + let x = Scalar::from_u64(u64::from(x.get())); + SharePair { + value: poly::evaluate(&self.values, &x), + blinding: poly::evaluate(&self.blinding, &x), + } + } +} + +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])")) + } + } + )*}; +} + +/// Holds this member's polynomials and recipient key until closed R1. Details: TC-2220 +pub struct DkgDealt { + public: Public, + me: MemberIndex, + hpke: CeremonyHpkeSecretKey, + polynomial: Polynomial, +} + +/// Holds the polynomials and private pairs until closed R2. Details: TC-2220 +pub struct DkgComplained { + public: Public, + me: MemberIndex, + polynomial: Polynomial, + pairs: BTreeMap, +} + +/// Holds the polynomials and private pairs until closed R3 fixes QUAL. Details: TC-2220 +pub struct DkgAnswered { + public: Public, + me: MemberIndex, + polynomial: Polynomial, + pairs: BTreeMap, +} + +/// Holds one private pair per QUAL dealer until closed R4. Details: TC-2220 +pub struct DkgExtracting { + public: Public, + me: MemberIndex, + pairs: BTreeMap, +} + +/// Holds the private pairs until the common closed R5. Details: TC-2220 +pub struct DkgAwaitingBarrier { + public: Public, + me: MemberIndex, + pairs: BTreeMap, +} + +/// Holds the private pairs until closed R6. Details: TC-2220 +pub struct DkgReconstructing { + public: Public, + me: MemberIndex, + pairs: BTreeMap, +} + +redacted!( + DkgDealt, + DkgComplained, + DkgAnswered, + DkgExtracting, + DkgAwaitingBarrier, + DkgReconstructing +); + +/// The outcome of consuming the common closed R5. Details: TC-2220 +#[derive(Debug)] +pub enum AfterBarrier { + /// No extraction fault or upheld R5 complaint. Details: TC-2220 + Finished(CandidateShare, DkgOutput), + /// R6 is required; send the message and consume closed R6. Details: TC-2220 + Reconstruct(DkgReconstructing, DkgMessage), +} + +/// Validate the genesis context and local recipient key, then deal R1. Details: TC-2220 +pub fn dkg_start( + c: &AuthorizedCeremony, + me: MemberIndex, + hpke_sk: CeremonyHpkeSecretKey, + rng: &mut impl CryptoRngCore, +) -> Result<(DkgDealt, DkgMessage), Error> { + let public = Public::new(c)?; + 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); + } + let t = public.t(); + let polynomial = Polynomial { + values: (0..t).map(|_| Scalar::random(&mut *rng, true)).collect(), + blinding: (0..t).map(|_| Scalar::random(&mut *rng, true)).collect(), + }; + let commitments: Vec = polynomial + .values + .iter() + .zip(&polynomial.blinding) + .map(|(a, b)| G2::generator().mul(a).add(&public.h.mul(b))) + .collect(); + let hash = commitment_hash(&commitments); + let mut dealings = Vec::with_capacity(c.recipients().len()); + for recipient in c.recipients() { + let pair = polynomial.at(recipient.index); + let value = Zeroizing::new(pair.value.to_bytes()); + let blinding = Zeroizing::new(pair.blinding.to_bytes()); + let mut plaintext = Zeroizing::new([0u8; PLAINTEXT_LEN]); + for (out, byte) in plaintext + .iter_mut() + .zip(value.iter().chain(blinding.iter())) + { + *out = *byte; + } + 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 = DkgDeal { + commitments, + dealings, + }; + let message = DkgMessage::new(c, DEAL, me, &deal.encode())?; + Ok(( + DkgDealt { + public, + me, + hpke: hpke_sk, + polynomial, + }, + message, + )) +} + +impl DkgDealt { + fn open(&self, dealer: MemberIndex, deal: &DkgDeal) -> 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() != PLAINTEXT_LEN { + return Err(Error::InvalidSealedDealing); + } + let (value, blinding) = plaintext.split_at(32); + Ok(SharePair { + value: Scalar::from_bytes(value)?, + blinding: Scalar::from_bytes(blinding)?, + }) + } + + /// Open and check every valid dealing; complain about failures (R2). Details: TC-2220 + pub fn complain( + mut self, + r1: &ClosedRound, + ) -> Result<(DkgComplained, DkgMessage), Error> { + let deals = self.public.deals(r1)?; + let mut pairs = BTreeMap::new(); + let mut dealers = Vec::new(); + for (i, deal) in deals { + match self.open(i, deal) { + Ok(pair) if self.public.pedersen(&pair, &deal.commitments, self.me) => { + pairs.insert(i, pair); + } + _ if i != self.me => dealers.push(i), + _ => {} + } + } + let message = DkgMessage::new( + &self.public.ceremony, + COMPLAIN, + self.me, + &DkgComplaints { dealers }.encode(), + )?; + let Self { + public, + me, + hpke, + polynomial, + } = self; + drop(hpke); + Ok(( + DkgComplained { + public, + me, + polynomial, + pairs, + }, + message, + )) + } +} + +impl DkgComplained { + /// Publish this dealer's pairs for every counted complaint against it (R3). Details: TC-2220 + pub fn answer( + mut self, + r2: &ClosedRound, + ) -> Result<(DkgAnswered, DkgMessage), Error> { + self.public.complaints(r2)?; + let answers = match ( + self.public.commitments.contains_key(&self.me), + self.public.complaints.get(&self.me), + ) { + (true, Some(required)) => required + .iter() + .map(|j| (*j, self.polynomial.at(*j))) + .collect(), + _ => Vec::new(), + }; + let message = DkgMessage::new( + &self.public.ceremony, + ANSWER, + self.me, + &DkgAnswers { answers }.encode(), + )?; + Ok(( + DkgAnswered { + public: self.public, + me: self.me, + polynomial: self.polynomial, + pairs: self.pairs, + }, + message, + )) + } +} + +impl DkgAnswered { + /// Fix QUAL, apply valid answers and publish Feldman coefficients (R4). Details: TC-2220 + pub fn extract( + mut self, + r3: &ClosedRound, + ) -> Result<(DkgExtracting, DkgMessage), Error> { + let replacements = self.public.answers(r3, Some(self.me))?; + self.pairs.extend(replacements); + let qual = self.public.qual(); + self.pairs.retain(|i, _| qual.contains(i)); + if self.pairs.len() != qual.len() { + return Err(Error::LocalShareUnavailable); + } + let feldman = DkgFeldman { + commitments: self + .polynomial + .values + .iter() + .map(|a| G2::generator().mul(a)) + .collect(), + }; + let message = DkgMessage::new(&self.public.ceremony, EXTRACT, self.me, &feldman.encode())?; + Ok(( + DkgExtracting { + public: self.public, + me: self.me, + pairs: self.pairs, + }, + message, + )) + } +} + +impl DkgExtracting { + /// Report Feldman-invalid pairs; R5 is always sent, possibly empty. Details: TC-2220 + pub fn extraction_complaints( + mut self, + r4: &ClosedRound, + ) -> Result<(DkgAwaitingBarrier, DkgMessage), Error> { + self.public.extract(r4)?; + let complaints = self + .public + .feldman + .iter() + .filter(|(i, _)| **i != self.me) + .filter_map(|(i, feldman)| { + let pair = self.pairs.get(i)?; + (G2::generator().mul(&pair.value) != evaluate_points(feldman, self.me)) + .then(|| (*i, pair.duplicate())) + }) + .collect(); + let message = DkgMessage::new( + &self.public.ceremony, + EXTRACTION_COMPLAINTS, + self.me, + &DkgExtractionComplaints { complaints }.encode(), + )?; + Ok(( + DkgAwaitingBarrier { + public: self.public, + me: self.me, + pairs: self.pairs, + }, + message, + )) + } +} + +fn candidate( + public: &Public, + me: MemberIndex, + pairs: &BTreeMap, +) -> Result<(CandidateShare, DkgOutput), Error> { + let output = public.output()?; + let mut scalar = Scalar::from_u64(0); + for i in &output.qual { + let pair = pairs.get(i).ok_or(Error::LocalShareUnavailable)?; + scalar = scalar.add(&pair.value); + } + Ok(( + CandidateShare { + transcript: output.transcript_hash, + index: me, + scalar, + }, + output, + )) +} + +impl DkgAwaitingBarrier { + /// Consume the common closed R5; finish or publish R6 values. Details: TC-2220 + pub fn after_barrier( + mut self, + r5: &ClosedRound, + ) -> Result { + if !self.public.barrier(r5)? { + let (share, output) = candidate(&self.public, self.me, &self.pairs)?; + return Ok(AfterBarrier::Finished(share, output)); + } + let values = self + .public + .faulted + .iter() + .filter_map(|i| Some((*i, self.pairs.get(i)?.duplicate()))) + .collect(); + let message = DkgMessage::new( + &self.public.ceremony, + RECONSTRUCT, + self.me, + &DkgReconstruction { values }.encode(), + )?; + Ok(AfterBarrier::Reconstruct( + DkgReconstructing { + public: self.public, + me: self.me, + pairs: self.pairs, + }, + message, + )) + } +} + +impl DkgReconstructing { + /// Recover every faulted QUAL dealer from closed R6 and finish. Details: TC-2220 + pub fn finish( + mut self, + r6: &ClosedRound, + ) -> Result<(CandidateShare, DkgOutput), Error> { + self.public.reconstruct(r6)?; + candidate(&self.public, self.me, &self.pairs) + } +} + +/// Recompute the outcome from raw signed rounds; no expected output is trusted. Details: TC-2220 +pub fn dkg_verify_transcript( + c: &AuthorizedCeremony, + t: &DkgTranscript, + 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)?; + 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)?; + let r4 = verified_round::(c, records.next(), EXTRACT, public.previous, v)?; + public.extract(&r4)?; + let r5 = verified_round::( + c, + records.next(), + EXTRACTION_COMPLAINTS, + public.previous, + v, + )?; + if public.barrier(&r5)? { + let r6 = verified_round::( + c, + records.next(), + RECONSTRUCT, + public.previous, + v, + )?; + public.reconstruct(&r6)?; + } + if records.next().is_some() { + return Err(Error::InvalidRound); + } + public.output() +} + +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 { diff --git a/tinycloud-threshold/src/error.rs b/tinycloud-threshold/src/error.rs index 0c567eb2..ff3009bc 100644 --- a/tinycloud-threshold/src/error.rs +++ b/tinycloud-threshold/src/error.rs @@ -52,6 +52,12 @@ pub enum Error { NotEnoughReady, #[error("candidate does not match certificate")] CandidateMismatch, + #[error("fewer than t qualified dealers")] + InsufficientQualified, + #[error("fewer than t valid reconstruction values")] + InsufficientRecovery, + #[error("no valid private value from a qualified dealer")] + LocalShareUnavailable, } /// Verification distinguishes context selection from a member's share equation. diff --git a/tinycloud-threshold/src/lib.rs b/tinycloud-threshold/src/lib.rs index 0d7ed9d2..b08bc3ab 100644 --- a/tinycloud-threshold/src/lib.rs +++ b/tinycloud-threshold/src/lib.rs @@ -30,13 +30,6 @@ pub mod ibe; pub mod identity; pub mod ids; mod poly; -#[cfg_attr( - not(test), - allow( - dead_code, - reason = "Crate-private dealing helpers await DKG/reshare callers." - ) -)] pub mod seal; pub use activation::{ diff --git a/tinycloud-threshold/src/poly.rs b/tinycloud-threshold/src/poly.rs index 643deed7..561bd99b 100644 --- a/tinycloud-threshold/src/poly.rs +++ b/tinycloud-threshold/src/poly.rs @@ -16,7 +16,6 @@ pub(crate) fn lagrange( Ok(numerator.mul(&denominator.inverse()?)) } -#[cfg(test)] pub(crate) fn evaluate(coefficients: &[Scalar], x: &Scalar) -> Scalar { coefficients .iter() From 4f090b94e7518cde1e9d1a762853e5f36fe77ebd Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:32:42 +0000 Subject: [PATCH 05/15] test(threshold): keep the reshare fixture's master key in the DKG context check; Details: TC-2220 --- tinycloud-threshold/src/dkg_relay.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tinycloud-threshold/src/dkg_relay.rs b/tinycloud-threshold/src/dkg_relay.rs index f2861d52..97396476 100644 --- a/tinycloud-threshold/src/dkg_relay.rs +++ b/tinycloud-threshold/src/dkg_relay.rs @@ -1280,7 +1280,8 @@ fn local_recipient_key_and_context_are_checked_before_any_randomness() { ¤t, CeremonyKind::Reshare, Threshold::new(2, 3).unwrap(), - 12, + // Seed 11 matches the genesis fixture, so the reshare keeps its mpk. + 11, ); assert!(matches!( dkg_start(&reshare, idx(1), f.secret(1), &mut rng), From 705ff4d07c52626ab8addad0c9883e6876c62863 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:35:45 +0000 Subject: [PATCH 06/15] fix(threshold): satisfy clippy in DKG round outcome and relay tests; Details: TC-2220 --- tinycloud-threshold/src/dkg.rs | 4 ++++ tinycloud-threshold/src/dkg_relay.rs | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tinycloud-threshold/src/dkg.rs b/tinycloud-threshold/src/dkg.rs index 591b4a2e..5440fae0 100644 --- a/tinycloud-threshold/src/dkg.rs +++ b/tinycloud-threshold/src/dkg.rs @@ -859,6 +859,10 @@ redacted!( /// The outcome of consuming the common closed R5. Details: TC-2220 #[derive(Debug)] +#[allow( + clippy::large_enum_variant, + reason = "Returned once per ceremony and moved immediately; variants mirror the R5 outcome." +)] pub enum AfterBarrier { /// No extraction fault or upheld R5 complaint. Details: TC-2220 Finished(CandidateShare, DkgOutput), diff --git a/tinycloud-threshold/src/dkg_relay.rs b/tinycloud-threshold/src/dkg_relay.rs index 97396476..a3c1719c 100644 --- a/tinycloud-threshold/src/dkg_relay.rs +++ b/tinycloud-threshold/src/dkg_relay.rs @@ -1001,9 +1001,11 @@ fn selective_r4_reaches_the_common_barrier_and_is_reconstructed() { assert_eq!(run.transcript.rounds.len(), 6); } +type Mutation = fn(&mut Outbox); + #[test] fn bad_missing_and_malformed_r4_are_extraction_faults() { - let cases: [(&str, fn(&mut Outbox)); 5] = [ + let cases: [(&str, Mutation); 5] = [ ("bad", |out| { let forged = vanishing_feldman(&out.payload(3), &[]); out.replace(3, &forged); @@ -1412,7 +1414,7 @@ fn closed_rounds_reject_wrong_signers_cohorts_and_signatures() { let mut rng = f.member_rng(1); let (_, message) = dkg_start(&f.ceremony, idx(1), f.secret(1), &mut rng).unwrap(); let genuine = f.sign(1, &message); - assert!(close_for::(&f, &f.ceremony, 1, &h0, &[genuine.clone()]).is_ok()); + assert!(close_for::(&f, &f.ceremony, 1, &h0, std::slice::from_ref(&genuine)).is_ok()); let mut wrong_signer = genuine.clone(); wrong_signer.signature = f From a672197481f5ef9e1efd662411188cba8cbf6bdb Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:40:09 +0000 Subject: [PATCH 07/15] docs(threshold): record the genesis DKG layouts, rules and vectors; Details: TC-2220 --- tinycloud-threshold/README.md | 14 +- tinycloud-threshold/SPEC.md | 90 +- .../vectors/v1/dkg-genesis.json | 1003 +++++++++++++++++ 3 files changed, 1102 insertions(+), 5 deletions(-) create mode 100644 tinycloud-threshold/vectors/v1/dkg-genesis.json diff --git a/tinycloud-threshold/README.md b/tinycloud-threshold/README.md index 53879070..e619b56c 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.2 +## Implemented surface through S0.3 - 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). @@ -14,8 +14,10 @@ The crate is a workspace member with `publish = false`. No other workspace crate - Owner-authorized ceremony envelopes and closed rounds with signed recipient/dealer cohorts and retained authenticated payload-decode outcomes. - Externally anchored certificates, pinned succession and certificate-bound candidate shares. - 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. -`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)]`. DKG/redistribution rounds, HPKE dealings, 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 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). 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. @@ -35,7 +37,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 prints nine executed outcomes. 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 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`. ## Vectors @@ -49,6 +51,12 @@ TC_THRESHOLD_REGEN_VECTORS=1 cargo test -p tinycloud-threshold primitive_vectors `vectors/v1/cert-core.json` contains 18 certificate, lineage, share-binding, fault and activation cases. The certificate vector test replays these cases through the verification APIs with real test signatures. +`vectors/v1/dkg-genesis.json` contains six genesis DKG transcripts from fixed seeds: the signed ceremony statement, the initial transcript link, every owner closure and member message, and either the recomputed QUAL, disqualification reasons, reconstructed dealers, commitments, public shares and transcript hash, or the expected error. It lists the Ed25519 test keys for its DIDs. `dkg_vectors_regenerate_byte_for_byte_and_replay` regenerates the file in memory, compares its complete bytes and replays every case through `dkg_verify_transcript`. To update it deliberately: + +```sh +TC_THRESHOLD_REGEN_VECTORS=1 cargo test -p tinycloud-threshold dkg_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 4416ed2d..a6f425f5 100644 --- a/tinycloud-threshold/SPEC.md +++ b/tinycloud-threshold/SPEC.md @@ -219,7 +219,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.2 +## Implemented surface through S0.3 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: @@ -230,7 +230,9 @@ S0.1 implements §§3.1–3.6 and the typed parameters in §3.0. S0.2 adds the a - Fault verification checks certificate context, request/share binding and the member signature before classifying a failed share equation. - Readiness and activation APIs construct the defined preimages and verify every supplied receipt signature before quorum evaluation, receipt hashing or sorting. -The dealer remains test-only. DKG and redistribution round implementations in §§3.7–3.8, HPKE dealings in §3.10, durable storage/retirement and node-runtime integration are separate slices. The crate remains an unwired workspace member. +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.2 canonical byte appendix @@ -351,3 +353,87 @@ activation.bytes = TH(ACTIVATE, The owner signs exactly those 32 activation bytes. Unlike ceremony/closure statements, activation `SignedStatement.bytes` already contains the domain-separated signing preimage rather than a raw body. The default quorum is all `n` holders. `WithOutageBudget(b)` requires `b > 0` and checked `q = t+b <= n`; at least `q` unique verified members must be ready. The budget includes all unavailable ready signers. Receipts are signed installation statements; durable storage, monotonic activation state and retirement remain integration operations. + +## S0.3 genesis DKG appendix + +This appendix records the byte layouts and deterministic rules that members and `dkg_verify_transcript` apply to §3.7. 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 R6 = 6. Every DKG message uses the S0.2 member envelope with cohort `1` (authorized recipients); genesis has no dealer cohort. + +### Initial link and transcript + +```text +H0 = TH("TC-THRESHOLD-V1-DKG-START", authorized_ceremony_digest:32) +``` + +`dkg_initial_transcript(c)` returns `H0`. The tag is specific to this link and is not part of the §3.3 table; it is distinct from, and not a prefix of, every §3.3 tag. Closed R1 names `H0` as its previous hash; each later round names the S0.2 transcript update of the round before it. The DKG transcript hash is the transcript update after closed R5, or after closed R6 when R6 is required. + +Each member transition first requires the closed round's authorized-ceremony digest, round number and previous hash to equal the values the consuming state holds; otherwise it returns `InvalidRound` before reading any message. + +`DkgTranscript` is the list of raw round records: the exact owner closure (body and signature) and the exact member statements in owner order. `dkg_verify_transcript` first sums the byte lengths of every closure body, closure signature, member envelope and member signature and returns `LimitExceeded` above 4194304 bytes, before any signature check. It then verifies R1–R5 as closed rounds, verifies R6 exactly when the R5 rules require it, and rejects missing or additional records with `InvalidRound`. It derives the outcome from the authenticated records only. + +### Pedersen generator and dealing context + +`h = hash_to_curve_G2("pedersen-h", PEDERSEN_H DST)` as in §3.3. The commitment-vector hash used as HPKE AAD is: + +```text +TH(COMMITMENTS, t:u16 || C_0:96 || … || C_(t−1):96) +``` + +HPKE `info` is `DEALING || authorized_ceremony_digest:32 || 01 || dealer:u16 || recipient:u16`. The sealed plaintext is exactly `s_ij:32 || s′_ij:32`, both canonical scalars. + +Dealers draw randomness in this order: the t coefficients of f_i (constant first), then the t coefficients of f′_i, each as 64 bytes reduced mod r and redrawn if zero; then one HPKE encapsulation per recipient in index order. + +### Payloads + +```text +R1 deal = 01 || t:u16 || [C_k:96] || n:u16 || [(recipient:u16 || lp(sealed_dealing))] +R2 complaints = 01 || count:u16 || [dealer:u16] +R3 answers = 01 || count:u16 || [(recipient:u16 || s:32 || s′:32)] +R4 feldman = 01 || t:u16 || [A_k:96] +R5 extraction = 01 || count:u16 || [(dealer:u16 || s:32 || s′:32)] +R6 values = 01 || count:u16 || [(dealer:u16 || s:32 || s′:32)] +``` + +`sealed_dealing` is the §3.10 envelope `01 || enc:32 || lp(ciphertext)`. Every count is at most 16 and is checked before allocation; index lists are strictly increasing member indices 1..=16; point coefficients are canonical G2 encodings and individual coefficients may be the identity (§3.2 role table); scalars are canonical. Trailing bytes and other versions are rejected. A payload that fails this decoding is *malformed*. + +### Classifying a round + +Within a closed round, authenticated messages are grouped by sender. Messages with byte-identical envelopes are one message regardless of their signature bytes. Two messages from one sender with different envelope bytes are *conflicting*; this compares the signed content, including malformed payloads. A sender with no message is *missing*. + +### R1–R3 and QUAL + +For each member index i, in this order, the first applicable reason disqualifies dealer i: + +1. `MissingDeal`: no R1 from i. +2. `Equivocated { round: 1 }`: conflicting R1. +3. `MalformedDeal`: R1 is malformed, has a commitment count other than t, or its recipient list is not exactly 1..=n. +4. `Equivocated { round: 2 }`: conflicting R2 from i. +5. `Equivocated { round: 3 }`: conflicting R3 from i. +6. `Complaints`: at least t distinct complainers counted against i (below). +7. `InvalidAnswer`: i's decoded R3 contains any answer that fails `s·g2 + s′·h = Σ_k j^k·C_ik` at its recipient j, whether or not that answer was required. +8. `MissingAnswer`: a required answer is absent: i's R3 is missing or malformed, or omits a required recipient. + +R2 counting: a complaint by j against i counts once per distinct j, only from a non-conflicting, decodable R2 whose dealer indices are all at most n, and never when j = i. A malformed R2 supplies no complaints; a conflicting R2 supplies no complaints and disqualifies its sender as a dealer under rule 4. The required answers for dealer i are exactly the counted complainers against i. + +R3 rules: an R3 is used only if it decodes and all recipient indices are at most n; otherwise it is malformed. A missing or malformed R3 disqualifies i only when i has required answers. A valid answer that was not required is ignored and creates no complaint. A valid required answer replaces the complainer's private pair from i. + +QUAL is every dealer not disqualified, fixed after closed R3. If |QUAL| < t the ceremony returns `InsufficientQualified`. A member that lacks a Pedersen-valid pair from some QUAL dealer after R3 returns `LocalShareUnavailable`; this only arises when a member's own complaint is absent from closed R2. + +Members complain in R2 about every dealer whose valid R1 dealing to them fails to open, decode or satisfy the Pedersen relation. A dealer answers in R3 every counted complaint against it when its own R1 was accepted. Every member sends R2 and R3, possibly empty. + +### R4–R6 and output + +For each i in QUAL, an *extraction fault* is a missing, conflicting or malformed R4, or an R4 whose coefficient count is not t. R4 from a sender outside QUAL is ignored. + +Every member sends R5, possibly empty, listing each other QUAL dealer i whose valid R4 fails `s_ij·g2 = Σ_k j^k·A_ik` for its own pair. An R5 entry by sender j against dealer i is *upheld* when i ≠ j, i is in QUAL without an extraction fault, the pair satisfies the Pedersen relation for C_i at j, and the Feldman relation for A_i at j fails. Other entries are ignored. Missing, malformed and conflicting R5 messages count as empty lists. No member finishes before consuming closed R5. + +The *reconstructed* set is the extraction faults plus every dealer with an upheld R5 entry. If it is empty the ceremony finishes after R5. Otherwise every member sends R6 with its Pedersen-valid pair for each reconstructed dealer, and the ceremony finishes after R6. For each reconstructed dealer i, the values used are those from non-conflicting, decodable R6 messages whose entry for i satisfies the Pedersen relation at the sender index; the t lowest sender indices are interpolated to the coefficients of f_i, and A_ik = a_ik·g2 replaces i's published vector. Dealer i stays in QUAL. Fewer than t such values return `InsufficientRecovery`. Missing, malformed and conflicting R6 messages contribute no values. + +Output: A_k = Σ_{i∈QUAL} A_ik; mpk = A_0; pk_j = Σ_k j^k·A_k; identity mpk, A_(t−1) or pk_j is rejected (§3.2). The member's candidate scalar is Σ_{i∈QUAL} s_ij using R3 replacements. `DkgOutput` carries the public material, QUAL, the disqualification reasons, the reconstructed set and the transcript hash. + +### Scope + +Agreement among members and the verifier relies on the §3.0 owner relay closing each round once and delivering every conforming member's message. Round equivocation compares conflicting authenticated message content from the same sender; a different signature encoding alone is not equivocation. A relay that omits or substitutes messages produces the failure outcomes above, not agreement; the protocol does not implement reliable broadcast. + +### 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). diff --git a/tinycloud-threshold/vectors/v1/dkg-genesis.json b/tinycloud-threshold/vectors/v1/dkg-genesis.json new file mode 100644 index 00000000..b010575e --- /dev/null +++ b/tinycloud-threshold/vectors/v1/dkg-genesis.json @@ -0,0 +1,1003 @@ +{ + "anchor": { + "network": "tinycloud:test:certificates", + "owner": "did:key:tc2222-fixture-0" + }, + "cases": [ + { + "description": "All members follow the protocol; R5 is empty and no R6 round exists.", + "inputs": { + "ceremony": { + "body": "010161616161616161616161616161616161616161616161616161616161616161610000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d3000000200030000000000000000000000000000000000030001000000186469643a6b65793a7463323232322d666978747572652d31156f3db7c1a81bbf5e560ab3220408fe1e022a93e19b781609f7bafd10f779060002000000186469643a6b65793a7463323232322d666978747572652d322c012b5c4cd586196623b6f1cb9a47d81b31d15b42308774658961e26881d40b0003000000186469643a6b65793a7463323232322d666978747572652d33292d8377c411746c3c15e9a87d4e7ba9feaccebd703c9574b6c123defc74103c", + "signature": "06b4ec78f6b16462b777b83e661ebd1d06f1352011b45bb22f714ce08d9ffbacc7d4fdbe96fbba331659431d72cb4f85f4609f97e6c6652ee6f1b8c8923be20b" + }, + "ceremony_digest": "ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc576", + "initial_transcript": "13a43bac02ea3dd76a0a7e44f2b6fa49b68a23ff96ef71ee82344986710da711", + "members": 3, + "rounds": [ + { + "closure": { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc576010aebf81afb3c2fd60c5be289741b662e7f42225606b0b8bec23b396e28e51f3e13a43bac02ea3dd76a0a7e44f2b6fa49b68a23ff96ef71ee82344986710da711", + "signature": "8529daebcc8d107c1ca60e6fa27ae4816c9a7111de53fcb94a65ba09ed968cbc27c60c349a93e6333966a38bef3f4ccaf0ec7003ef1d11a6f4cf42cbc4cd9f00" + }, + "messages": [ + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760101000100000236010002b8cb9b05797274c12e70b8f63171d7d746d7d42971e6cce339a74eb54da283bf9ecc37a7e67c0cdbb17e6e4aafa6ef7411c74438257204a7e8f5472561ead2cbcd1f6830989b63663b3cfc0a0a83aa5a4e724e8a6e50ccd088c18ed281856eec823fa43511e29ba3187072a6c7eaa981bab2a929e9de3bd7cea9d5db9487a9cc8fae77fb931b9421a8a6fc643cf8c91c0624323e77afdb6b635496a0d78fe3a32fa02bc0d5508b414f8afda8bfec9cbbfcab78d3a76f34c6cd9a8f936a477f49000300010000007501c6e17f43a91de702f29a207a659fe500c19197c8af5723fb9315daaea317fc5d000000503495a04adb2deea3fbf73379ac99bfac168f24265b567d3bee7583c35cfe8b8b928caa7862c84026b8bcffb420f3fe4c537a6ed6a2b19df0394b1f3c6109e4390495b7284e5e348ee8fc90affdf0d97800020000007501b57b9821675b6c10277f51fe0ae007acc44774f55077d574f393bb99dac6135500000050952e744063b56ce9470d522e1075bf833353cadb38e5fca362c6c7413c88d0940985f6002be3fe8be23c8ee3e83a6189c5748f369a5c7e2d98101256d73abd0ba9940e00eea579578eb4906f87e1af6a000300000075018463dfc3ca9da6f954c05b0e58dc93f82b8a6f2f51e9e704576c5a94d47f0c2e0000005096abb821a4f401aa69635ad7edc78127fe7453c535b79758c8a9c5e159f6e59dff79c1c6674cf1db0b90db7ceb65e16c9ce57ab32b46dd575b21e6f1a4e2ae17f265edc4a01189ad7881df16d256c625", + "signature": "0e005122b3ae2b8dc479ac3c87a7013525e07bcc18239842816a28d07affc773ffb26d0ae6f180657413b9e717c2f237f72598c0f48c35bef85490090a8a2d05" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760101000200000236010002adc93f789714a0c2a761924f5c8b9282e77ca2271f0fd42a7490013f303e429b7ea7ce7e831a4c4d519849f5490650c2127c3dfbff34f4ff671e1f6e4d74d3dff500d4828fb519275d6d7345d0e55ce8e85c970f3b7f69997b1bfdc944709b839773b2018d711a1baa9b2243c89dab6f8dc7970b38731620c6999e41276ceb747f7eb7b88498a894ea37d998c6ee55f217a563e5e770da09fd8baa3c6bc01cca89ee7f2d0bc4e28964586eaf24dd24dae84d2b111e59e51f50fbec6e5dbdda5b0003000100000075015e0127cd2084859b2b95b8fc1ad88b1e2ccd76420bea31b5ce6d2a783fed990a00000050efa290a524e08dc0762d931f6573c83a727cc436fd2f64a1540cf7ecb617ea61b121eee92a2934ed20726987d66c9393a97f01d52eea233417423c40fe922871a01483baeda910eace20ed904681f7f3000200000075011e7d0d107fe482980fa32f2b362cb44032df727558bb76051c3f8a1ca056276a00000050bc749c6f49fb8af769af1da04778bbcf6ca38d4c6ebbf22c121124147f89e8842d59cef953a8a4974ff01b2bdc71f9dd73438fa4102f1dea02c422295d7b0e17d37be7c5577823e8350024e5f4cd5abf0003000000750155253cb203bf6cc5cc955141d62c4fdaf32b79f1d9dc4cc055a1ea6408d2ed6f000000505db8b47f231cd8e1830d90e449a2eba091b3937696930ee7cf9a02ec5b33883ab94e05022afb497fd90671ed3cab7736bcaa1e7735b2de63717095e30c723007073547ceac2d8861bafbe8d400900361", + "signature": "f25f590ccebcfc270974084f64a8bc3ccf9fb1b124be7f8d4cd44dd237214906f0803eabe001a16dc544f4e2d76dded1affa61c02c910edd8718a528cdbe7809" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760101000300000236010002944cc7dc866fb71180b8a3b06ba37169dbbc6fa4ac58cb629e618923d17b5b212e25ee65dc3625fecb716d4a2486cc7706ff423f0eb4b3cb77240b3d6e16f93b46d18e407f56d869841914c2dcb5c956dcb64b8b42d78cfcf946dcccdcc9795db20c4dc0dae9df1acfdc343682ce38a8d6943aa729b6c193acb33faec1af3e75cc9a87c6e61f605c3315391220bb52cd115bac2a0ec7b42a9d8db24e8d106fafe2eb1548a5050ce2b2cd0d895c7e26189312e947db2d8a11abb4d6aae1df8b730003000100000075014a34455acfa0b0e82e31d5dc6a30525278384f6a1677131c8a3a174d2744e2380000005071b3a41c15f74c8ae834124e4b1ed2c167371c8def6cfdcb0833c93f0a8c66d1d03030290b7fe12f6f302efde8be7944e51c46ce8e056c7dd3cba24dc2858a50a1c53969175e663ec2fecf8f800f7d490002000000750162c6ad6adba12d7bfd2adec882539849e67d9a4ea21d1b373b8fcc10aefafc67000000505ae370c1bca9e9af3a4226ca02037e9e1f392394660dc6fba75f61a621b85b2e97d7f7e07f365a3640e3458c068ec9579985ea9c61e59a993ace6c57621eb54967c01f1a7d3a9d73ce38738ec0d7c6ce00030000007501c614eb2a3234dfb44775ff69d8d9a189e15f2d4869397740db4579b5fb905a67000000503b32013b9d28b13d6a42acdf95d226f7706d00c27d8d53478bf25a4cf08d78e1b4c74c9121e052428e67fe76f7b13298b3eb9fe6e1f53625e02abc5b58425929f78c22d6c615079d2709f07c159e963c", + "signature": "1c07a7fe6cd10f1f911946b162948ba55f90842d08d37336588982856da68f53685db5187b6e7267b32f6490dbbe1702333f56b132c623ce872755ff4dd1cd02" + } + ] + }, + { + "closure": { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc576028b26f0948ed227976565776504f719c5debd598849617cd74a31158f6785a856d080233f299edea73b80a76dbab233e12214c1955e55b3c45961066d2dc91739", + "signature": "ff9922bccf415cd810fa87efc3eef15983a4338888cf01680e37958929c8ae0d03909c6942f4303f3c388528c14c643dcab074cd8a0f26ec2addc7d5686e9a08" + }, + "messages": [ + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760201000100000003010000", + "signature": "8f786d6d0b8b97bc2bfe295dd38f665ddeced09b7317f8d3ee037cd21c63b269af375b8fd48ceb73d6baeaa578d0a02b568d9d5ae8a06efc1a2be0c2e4befc09" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760201000200000003010000", + "signature": "810d90b56da243830327d0cb63a8b47736e9268f62384f63c686e052e83492812d02f962fe8f2435eea89b2d33fbb3833e9a6c1bbfa34d69ebe1c54de3826608" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760201000300000003010000", + "signature": "e186f4ecb49dfa3cf0e6d3281ed1526f0537dbfb1aa34b3195809c604a519b417c690f3a998d9242dd70e6ce07ab1f5c25c458f60d7f350ac6ceec5d21ee1706" + } + ] + }, + { + "closure": { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc57603e53cad9c41fc1ba68ce6dd8e05715d148b7ff100713c8a82cc43f8d5c7c11eec6d3c0b00e74df50c07808e1188ac65d14d73436b94ce83185f43399eef4736ba", + "signature": "265468f84d0a8ef43694a8143166ed030a5eab9f7b6d51d70ac3cfcefa67ed1415dee7361ad8ce8bd98d6fe70ca3d27763c102b3c2efa337433cc397d57d380e" + }, + "messages": [ + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760301000100000003010000", + "signature": "e26b56975b3fd57319b1a39473813de045e486681131d2151bcdefc18033b37e2d72f27c08c7ef5697d42355ade156553a9fe931baf67b520d3a0d115b20ce07" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760301000200000003010000", + "signature": "b345d1df29ae3e97bcfa169a9a4794615f78d62094dc9ec27b2531fdf92f8756fe72a5384e8a143cbd88e4936d606d26a237819f13c429c243880c0d759b2006" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760301000300000003010000", + "signature": "cf5bc0cd2648e0d4caacfb7fbaa6874028ffe0d5b994cc38922981fdabc70d4b9d5f6e03c9f66dca5ae19fec5a5eb65159c581bd7f7bff9cd45d475dd925040b" + } + ] + }, + { + "closure": { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc57604627ff7d0d52cdabe4432b6dd693993d9206fe1a41aca2e03f34f5e1eb86ca3ba1a6e1dc705568eaf71cdcddc1d5485820ce18d869a4fa223b7a302b853e0ddff", + "signature": "840ca0ca91eb71eec4652f3219dda3df785780c11388ee18a68c1a242e76abb5a345cf17e63ae764d01321cb055b549ee90505527c831bf2f7d09747c11ed102" + }, + "messages": [ + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc57604010001000000c3010002b992ff6fd5367bda9628627be657995c8dfb7260385a19a651dc59501a656d68f8e4918d1cb5562d75480c94906193cd16cbbb64750562ed0c98c62c78c57444e8e1a15b6ca2ec45563ed22a7220944a79e5f834e9cf2b4b664be128fc892a53b9e3cd2e5f84900704c995d56fbe76765d3732404704bd433e95abd356cfc094f62284ee2a7f93e04436c774fb003fdc0fca160d42ebabdb087da7ce1725c1a2332f691576b046037363a3f947dacbd0a5f1339ce14a05ad5da6bb2defaf4ca8", + "signature": "f44506ce0f858bb4b69f6cf0cce66439367c5420b841a6e11622d4c7351775406bfde504bb6f534890d118a1f021ac50f8e52e3286c43a591d08c0f56eca6900" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc57604010002000000c30100028f6a6c707cd945d7a8d5e0acd611027da1271ea730991ff95b1dbf55be655b34b1f5375bc802f061cc441b9b453156fc185e6365add2c2b291825491be92d06f251278a6252b9daa8021fb2528f92da055a3012521d19045719049c508831c1bade0f2c7f5a45b435198a8a9700a0120a05d445e3d301db9f62b0742c1f384eb9489591b8fda937cdd24801df71f151d101f7c5c1b779ffc92f3c67d46b2de1a526e86efb9c648057027a4d03572a9410a4acdbf55b386f672f7d0b565183b8d", + "signature": "f17e7458b133f2b73973c47149e2ab610e09c6e41e93dccf9c303e715f4b3a089d1edda270b2feaf6d88e7202d33bb2394195717ec276e897cbff99c2b1b5108" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc57604010003000000c3010002a805a08cc86b83c5d28e6f5c26112a4aed7290669a6105b90dab0c1fe7236cd7570522d5a88e1e1864eb1ec82c6568ec0b9c5e882e06da5bc7daad93b1001b78ccdd900d53688bfdb269166366a093ff9d8506182311b75c47e2b1d8db2ebbe1a6d8cbad099b0ad4671a3cf70fdbb48faf9aa42389dbcc6198afebced971ef0c595031c28727da27c343bef6d21c023114e3f7bf614a3ce524c971faffb093102e0dedaf43118774602c61b48c42e5aa5d8fa662c93d4fa367ecf6984823a82c", + "signature": "5440de821960437b66786335a033574c71e0cbf0706db60449fbb42713ca48ee074100c2c62b56efc1a782221d188250e6359b1542532d2054f0d5db3a1c5b0c" + } + ] + }, + { + "closure": { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc57605db2b6e5e9cdfa6f076b7b94fcb3bba670236e27656ac3939546935c072fce2a4972f8d63f27b895091f94cf92f86ec26ca6c42ecf7843686a2dd47c4f2a6ca83", + "signature": "d80d681eafdd6a5c47e73aa136c2e1e1d517628961c388e4d2e6ca507fa4f79bd88364900978ed8e86df34f56a4228134f86076de86e19d9e54cdeab030ada05" + }, + "messages": [ + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760501000100000003010000", + "signature": "25139cd9ab68fa245b6798c0d1713d5dda756756e20072d403cbddaef48198e740598a3794994bb8834d30bec2c8b3f3f440283aebb0ef9775bd792d8c10f40d" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760501000200000003010000", + "signature": "91d79791ad1f1d9f687a6e9408ed1365cb8a30e1cfb4e0fd060acc7365b04d96c2953972e02bd28855ffde6277e93968031a41b93649ff76cc1aa28e2072b505" + }, + { + "body": "01ec56614eda44fba6e64b1a7b0c58eef4213175fb6050c8d2c7c99c14d7ffc5760501000300000003010000", + "signature": "498c7ecc7a11d42535eaecf93817260672a12ee35a11776f9689b7b45646319d0459f826ed5464e398c3a08f275611b76930c627d944f049aed264f6a024d20e" + } + ] + } + ], + "threshold": 2 + }, + "name": "genesis_2of3", + "operation": "dkg_verify_transcript", + "outputs": { + "commitments": [ + "896cabab7453b2a12897f7664a0bd231ee54e1391820d3399cbaeb24dde62ad0170171cf64f2ca9cff582b56c54974ed119818836ffeb2e52ddaf2aa109a89ec4abad722533a668d5c9462563c3d0076163b19dd2a759e8094f677a21219764a", + "92a9b338245074d1b88282d7a4f7830caf860c84fc37c746cd236e1240cf5e96e14bc3925b7790e3a1d8fc3a6fba3b2f19d3996f1798788cedbb346a6009c62d99251fdb5ac81a5036e29ce1b5749152a83f9f82d94d38e0574a6d3cc0f8f248" + ], + "disqualified": [], + "mpk": "896cabab7453b2a12897f7664a0bd231ee54e1391820d3399cbaeb24dde62ad0170171cf64f2ca9cff582b56c54974ed119818836ffeb2e52ddaf2aa109a89ec4abad722533a668d5c9462563c3d0076163b19dd2a759e8094f677a21219764a", + "public_shares": [ + { + "member": 1, + "point": "b7da057b5b2cf058fe8a2cee59045adf09e4a4051ebfb982b5807ea29f338c6429029a4764ce22ca13b62ba1bc61a24313850bd80369707b56c5fec45ebf5a31b060b6c75ffcce34c1abccc6d7ff9c5d3cd4434859a78b108ed66c3d4a1a9887" + }, + { + "member": 2, + "point": "aed1a44dc8ef85a7c2ec8b63e03d2bf64cd709431c24b01d8712ee84167e2b742655607d872c6afb774b6c226d1c7d110665987dd280cc2471e3d22edfeff2a5aeeb7bb0fc802803e3f8012be80422b6a307af6e9c5f72568b5bca45fe9e5e33" + }, + { + "member": 3, + "point": "91964e1f115af8fb7f71751fea53d7b3783b8398b3cb56d2cbac73afb6dfe6ddf8aab3e8c977848a91fec27654776ed50addd59305895e675565fb10e8f769626b8c1b6a1830292cbf3276efd58497536bd8480247eb842c71b5793a701a6e90" + } + ], + "qual": [ + 1, + 2, + 3 + ], + "reconstructed": [], + "transcript_hash": "3e9150b05c5610427d76e12f46ab3b6347c465a5695aede6f6e9efce9947d7b3" + } + }, + { + "description": "Dealer 3's R4 vector passes only member 1's check; member 2's R5 complaint leads to R6 reconstruction of dealer 3.", + "inputs": { + "ceremony": { + "body": "010162626262626262626262626262626262626262626262626262626262626262620000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d3000000200030000000000000000000000000000000000030001000000186469643a6b65793a7463323232322d666978747572652d31862064d76002a77cf789bf424066eae5f176540cddb45caf025413492abd2a3e0002000000186469643a6b65793a7463323232322d666978747572652d320b75d6059b87d5920be94ce04a0e050d4bd8dddc565e85b5f5a4d9ed88218f500003000000186469643a6b65793a7463323232322d666978747572652d33e732fca52c12138bbf3060d5939e086583437f32e76f6c594a9c9d624981355c", + "signature": "c192c655b8322f7429a6ee5821a5a11869da863d17be234ac3879904ce6b6432a08580bc8a9c3939c1ca7ac7885a705997ecd425d5e947bb4de41e88d3f7ac08" + }, + "ceremony_digest": "d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb772", + "initial_transcript": "d74680fdfd166773ff0fb09f25945d93c5a857a9c8a4f1bee35a50f1769d00c8", + "members": 3, + "rounds": [ + { + "closure": { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb77201c01221bdceb89c2b8701fb49e2b32703e8631429435f5217a863719b3b0118a5d74680fdfd166773ff0fb09f25945d93c5a857a9c8a4f1bee35a50f1769d00c8", + "signature": "3f10190f7d2b91d69a0d3a8f1e3ca1cc02457a4a29e8634b6cb122797711be174d5748dc13ed7e7376d8e060ae04787e3266c81a42a747c609cba348cee32509" + }, + "messages": [ + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720101000100000236010002a5f198ee764bb4aa86b10531f2f04d8dc7f6f6c749f15323fffd56e241a79808fd21655df585124bae9b76a539e7d11a1801827a8c811a8dbea7f7c4ccb635a9d6d4e6146b3d8995715da14c0d23c7d941a24f5da51e9557d601665ed16919e3b091a0c6f5d3b719b7f8faccbbbee32cf8e9cc0deb78232e5ac2aa8c57ebfb54512816c542e60d391ba72b7f7825ed440f91c7459513ac2be3eb0b6cf4027e873e9da2b220f007ff7f1032293191a9d32acb7b26d50ae1593bd1791e9f84ecc30003000100000075017b87b1b2e23f648b3d7daf0abaf54268c4fe714cab9c9bdebe9a5be99bdc230800000050afc1d6430f640137baca65b44a63735c4c20b5a0d911f233ad08297026703f30e97afcdafe5a830ef8ecd9386bdc6b6c36e9aa62887ee47e2cd2f1852d342365623c8b5ba0eb54e49018e9ce2e8f9b0c000200000075015175806c4dd79c93d8c6b016c18c456d9421278ce893762ec721059f65784a640000005003bb4704f3eb7040eec0bacf71e44d8154e4850596005cac2d29246140acaf1bd1c1bc79fb4cb7605df0b3ee44c83d73b7575af848e0391b7e0a34dd86fd44b4d91873e3dc514e2be6789137a836a158000300000075017aa5fb2564aebda580eb8258db7a0df4845edcb47af5d14dcbb3eb5a0ed15e63000000505cd2f6c5172727661abe3e76715e36b261b0dca0c2a68a429f0dccc245970a6e2b5f4c498164428a0890575d90252e8a5240ff1c0d0e25be570335056fa0e5571c2f1d171d5c4e64c13ea753e5949468", + "signature": "742b4eb971b7436b9dcd113bce651b9a59cc2b89d9b665f619f5172de6e88f89703771623660d338124c0f781036af024efbc168b04472c3b54e8e6e13ed8e0c" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb772010100020000023601000299d6ce3b115c9a85766588d8fc51dad0edfab4d19656c6b8a7dd12b7013ec5f1db7f89390c683834746db9220912e6771933e09ea22cc9150566e8250f7b0ff4f0d623e13a1f53647f9c9af703c07de2b17acc6e1272d18b55203a1db7d8feb287b5d141712bfbc647814a42de63761ff1ebefa15b8540d62a5c5e4ffb1970d8010212acf7954885c45b1ba7fac5b9330a05d6c1168d70c2c47306a737fd43d4ede6dd0c85b702dfc0fce969b31c97788c226d146b20f91afca0a4288d0a36aa000300010000007501f3afc7027311727b94d63e968499163e1da7dc7fd75ffc5e7db61d1d0c7b865000000050d4fa70c415f79185eff18e192c7331567bb69b3b8f7420d2d9b44b634298ac4987733e287f68cd03373ba4bdcfac5a50959236f6ed4ab9a6975d6eddb49efb0d70a6ec7e9efec05ffef28f2a82a79fea00020000007501a19175e221e1805e0db24da3225cbbf9aea0253c03ab1bab163f1a7e2eeeae5c0000005095eb475bcf552036990920a494000f700fad1ccb7ea720a583f365631bb5724e9c3f931b788fb36b258f3a6e83953afd439e44a8452ac5c54577a5cdc10212cf47bb20fe5ddeabaa6d15a14d0c51079200030000007501d10ce4e8dfdb979d5f429cc9e9152708662a9c3099ef6f7ae8cbd4261a26bf3000000050fe910d7692e5d6258463b9f9f4a519c9264e9fadbf535d0595c771a6aaa4e52d8c2c8a73d336f4f262aeaa7f8790802810bb74a9bbdca6bbdf17998a1a508aa5e5fa418948e49108531ce31196ea609b", + "signature": "7105fbd5e876934bef72cfb5308d0e05eaada7177a5bd8d326f965bcf0235f99781ae6a4d18b2595117b017cc5753f163a6f1c257aea55ce15095d16678e5d0d" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb77201010003000002360100028aa7eb637c8d51ee07bb1800a29fe22126e6947301ed918a0aeb1aeca29eea531b31e4b8184dc7667c02290c9b6b06ef0332309fabb66cc3b19b04f37d8d551129bd1679806c6cd76028090b0e4e8be7e37ab4d99f1f8f4c4310cb2627cf2c358e34dd61a91bf90c16f27b3696f230f25aff11c822c3d01ac329b0d99e031d2921d99de0b053fe213f88a55de160f03a0740753d5105a67d805e6157445b27abfa6e9e03f225935d282c884dafda49f2097dcb313bfd563490355e22cb0d8f4600030001000000750126c4cef59d951b90e1af048d46ee74ab8f555c33f5c51114212e5b2209b38901000000501f29fad672014c1a5d8d82fa68c64d085b5e8a1cd06b1b25a7ae030737a7d424ac472fb396b7ea0c22a8fe3086a0e48135446eb4c980b381697bb12cdf88c7a0c24a0a63d9542c82f40db581fb94433800020000007501cbb859175fc326b3573a2383b54019013c419a724aee1a65622a2f252bd9697f00000050a42059c71fc99373f3debf03e22945e74470ed80a85f4dbb5d35a2db0df6ee98436c785fbc97a163621455a8893f6d749394d9c12c7bcf557502ad4df8fd98428adb500db3b2c1e6c8e450a2be63fc5900030000007501d1ef71aea01df7b1146ea12a58b679c5982bd728fd6c7075a55b47bc659fd52400000050ee9f62f53bfc304088ede2f66143c470d4d61d3b595850b45f6873f0bf50e12aed3686b7b035a1a468eea642944d666a64c6c93705823db8e91929fc4748cd3dc9ab14fbc48cde6829e1b9efe8d6963e", + "signature": "e077a208e758c10fddf16c33fd3ecdeac9f2d44df7406550f37a1bbb744f1b1064bd6a10e509de06c78f1a4a865e535e26657baf2fc298139b5450ad62a7b502" + } + ] + }, + { + "closure": { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb772021a6469d7fb78cdbe39dc164a11ac1a49ab3bf998ab08ed9ca43fb7f1b860c69713baa02bac396702a29487ef45eccf6c6886567436f033f521eb1d7b620ffcaf", + "signature": "fa74fe1076a3a2f0becb1c7767192f2396f5e51d19fa60396ee27d2f5b89d5f50a7b11a067062b3e74bb71b13388f1a2d5b0c14ea48ebea7e4a496843ee6880d" + }, + "messages": [ + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720201000100000003010000", + "signature": "0216d6ed0e091dc76fd0f404b69fa59f914ee32a51ac707a4eb76137c1747876c14423b43889460e2eeab4d842c3923e4917e1568c3383b3c48cc0f97afe7e0e" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720201000200000003010000", + "signature": "bedad677d8e081c24190360d3dd113070d3a6fa6ab4c994167898efd5a8863c201e5cd2a46f01489760e4743aff8b9a50b93059d177169bb52e44f937ca21d02" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720201000300000003010000", + "signature": "97a5447d6b66dc319d4f17b2e8f8669ce4e60fea833794d222b0993fec9d6c58949bad709319d9ebabf384e6f6984e2517c0d4180783aa6eed45ddd176f01d0d" + } + ] + }, + { + "closure": { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb772031b8eca6e0ee405f62f0d9d0b09e62cd2a58d3fcdd254fc275bdb22a7d06ea06cb5fd99ccdcb809eadf4e89373b2e497bde5418d2c36b923e52e3268dc7a6ecf4", + "signature": "b12a396a0c6aacfc827bc471c5702bf5b952b6b9d8769d5c3d72c93e6da71ea1ba698ebd75994b7b473a09bd3321ab20d2caed3965a27f52875b6ec65c49f60f" + }, + "messages": [ + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720301000100000003010000", + "signature": "afe3d546cad8713ed872c7ae49d4c5b892ce6f16cb1b9e98f873fa70987ef408a64a0b195d9fabd0a61be1735099e5dc0cf55d403e511221bfe5257d4a67e50f" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720301000200000003010000", + "signature": "ceaf0d61d7e9a08156af32457a84c6549bee3903b167bb02739bd659176397a70358fefbb5ffa2fbfab9fcfdb8977a3fe8da25faca6ca9ef7528153c58bf1805" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720301000300000003010000", + "signature": "3e10784a4eb2ff7fca72bf5d39747bfa2065f5ad64ff48662f5e5c8753ddb00e05141e64468a9ec67c8c476435bcc2491968644dcbf763b4cbc3e19585ab4d06" + } + ] + }, + { + "closure": { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb77204e9922ff6dba7b7a3da9e6ffc1a6575d1c3cab8ff542bb6280d9f9b3327c3d48c6b446bd0aee583e78c8df977cf49206a4612689251192118e2fe3291b4b7d35a", + "signature": "bf8f1e993ab3697949949e1cacb19793feb50bc9a4a315440433ad87bd20b31271df458ea6460e93c2d70b3c0be650b484a3347a51f8de0281b10ee5e0b12d01" + }, + "messages": [ + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb77204010001000000c3010002805516e9528d922bda3502cdc68b023f4fb1eb7ba208ec7921bb54e152fa4984c57f25b2113011c8a8ab99a0a4da00ed08e15ea796dde8ba1d61c31be4128c2f59c5197c2d9465c7865c8d9f37a2cf32ebdf4d6135ce2957f7421c68a833e6de99ca64116982443df98ceb1b42ca24a610fe3dc172d5c7dfe3d5ed72eb12e0dd4ef5b9db7c4de20827a76cc46b6d846f012104e274b4165eeb1404e5e6a7f229072d6bd937d17b28bcb50b27f4c9f844b76580f03b9368c24dd5cc882c8862f6", + "signature": "31f17887572d123f2e5d86383ae8080aff880b4485a3781492bcdf7c9c2494ed0bfc6c8e7dd7e5aa66c344ce9a4cd6a6cb06b7357f71d01285b2f8c1b1cff407" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb77204010002000000c3010002828ad929b5be62212508307cdb5bf003e86e63e880640264df9aa8edb56e0546569dc1064a1ab083e4ad5d97320d59e30e45eab6f1bbb1a9267eddcc23f081c311ae24a68f3505b824aa40325ac2ea6f23db61a6b0e5793b660bb49ba287ac1c922b02ae939a381ce9c0ede2167263f5870de83c854c81928c7fd73173f215de9516e30dc2a418a687eea27059ff697512ff130f86cadadc18b4d995e99e9fc266b3743cf28b6c8706fa3c2bbf3eda490e7d9f50b3f156b4bbd75f647243e36b", + "signature": "3be10fb9be1ac9902d369189f8432eb81da0ffac862745c5b987f9a3de272ace135f1d29a3b1e4bc0cbeed80264c1c41108a7f5f504f0aa341ec373c75e8570c" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb77204010003000000c3010002aefdebb8271056760540b28d8f80fd93fe927f9e8b447b8d787a4ba753c0fd7fc0cac5fee195679db1fa993377bda65d04af32fb441b021128c046a9b1538bb3725618a5fd2a1741625276299c4f3880532087a30ba025b24d8fc8b67050191d83204840053307cadf653661ba024a61452d1e9182856655da238001323341355516e3549bee77ca19300009737fd33806c7605e3fa103e7cd5b86185c2e82d4cba45e2ba63822fbf281b3d91d2633064248a2be3a8972d3c76fd019635449d3", + "signature": "45230f6ef87bb09c56abbb1394114c8e48e321adec90244ef032d4bb2a354b32737bbf88ffbbfd5ba91d6a33f6e5a561d762a224dc6b27a1bc523e817b21e30d" + } + ] + }, + { + "closure": { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb77205e7ab34a7e8e4d2e195960163d6a2e48903c471f64e2e1acef3fcaafb67c6eb68ae0b317b30a729444fe3cd8c585eb49f723dd768f47ea8d4b9e2b4ce9de2a724", + "signature": "1803d4706a1ed84560ac3f4b0abbd5bfc0327b46666dadf98f79ece3186aef86a70b27a1803f024569d87442b308dcbc300466a5a81de29514623c2a109b270f" + }, + "messages": [ + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720501000100000003010000", + "signature": "c5b749b20d4703403ebe2c1fe548ede97d24a5032e67ea6f7d06054707e1b7b2309118397850e12e0d69e9c4406f44df691a6b7e2272fe815c85ae4f7ba21f0f" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720501000200000045010001000351e7702fc3b5d1c23ec88db31827a5d7538dc706f03cd4231df946c24654f86440c1aacaff36ad9349d9dd20c2203bf58b33cf372c2ec81bfa4a028bb924745d", + "signature": "fb189e9fa80e9c8bf9c6e62977a27dfc1a39c140fc991bb4f2f9704f7a6f1089f07a6dbbbbd90492f026c872db5fef483ae2922a63e8e7e72470c73339c5dd02" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720501000300000003010000", + "signature": "977889930b126bc1abfa0e5759e9e5597a4c024503da1d200c998de6d2e4adb22acd890d99f2b2a9710dbb34e526a1e13ea314ca1c8c3264b98069a1e52ae405" + } + ] + }, + { + "closure": { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720690a2b2cc6a55221da9f60d27fd0edc07affa05f50b2cce1f6941cee0864c29cc90a284bbd15dea930232812bd30866e80a6b13af7a5a7e8cc2379febd3454521", + "signature": "5e05def3ba3597f6b601f24961749e72a3862bb2d0959e1f732e54ba4a19f006ed0b50e73ec29167e899183e5cdbbe261b14d3fa98eed2d3cab2126d259dc20e" + }, + "messages": [ + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb772060100010000004501000100032a20f656907c40e23ddad44693cccf608f7d0c37d45602bdd5dc69ef190ff33d5d73388d15631ff8ba887b4a31980a70c132cf604fc63a768307e0b44964f705", + "signature": "747eb3973a3a3d507428066c73a89360d74476eee77d5d9dedab65d92ffc32e42f6a0e3658b66c4b083a4a0c9e3bfc254788a735ff82002ce693d1543312e40d" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720601000200000045010001000351e7702fc3b5d1c23ec88db31827a5d7538dc706f03cd4231df946c24654f86440c1aacaff36ad9349d9dd20c2203bf58b33cf372c2ec81bfa4a028bb924745d", + "signature": "2ec3bb54dd14151760ef6bbd6efa1c84cc3c51c3409fb43e2aabf918f6c2e88e20de0c51314c9ec38cfa590cc492cba860ef600bf8ccb9965f1d1be2a6835606" + }, + { + "body": "01d6918020ea2fe4a8780e3b86b856d7f2deac804b8a9e3c2cedc7ca974c8eb7720601000300000045010001000305c042b5cd51e55a0c7c6f1792e0a448c3e0ddd30c254989661623967399fd8a24101d08e90a3b2dd92b3ef752a86d7a5534cf0e089755c1718c246328e3f1b5", + "signature": "11abaafa08bc83ec6d6ea615589fd75822588931c716b8f5dd6861fb6552996fcdd1b07aee0caad170860360ef601366fe214e07ac55b1a2ac4737db123bea0d" + } + ] + } + ], + "threshold": 2 + }, + "name": "genesis_2of3_selective_extraction", + "operation": "dkg_verify_transcript", + "outputs": { + "commitments": [ + "b63de12e3ff4ae606c874662385636db965833c2110fac9c7cb22cf3587e163477bb2938bf496c03a8cc69cd2197b2c2062d657ec0c743985f92d9b500b44a33a5c5a91706dea5ccf91afa0f3531d45ccd5d5505e6604f7d662d126e5e5774e7", + "99273f8a1338b93e08c4779ca3a6981198d966c67b4dd270712342b5bce0f06bc3061acbf32fdea67dc7c9d5c94b0c1504a458a3279f2d29b5e04e2af3cc64148153fbdfb1415d5278308a4eab8c23c69c851a52855141def3a3c774fb853096" + ], + "disqualified": [], + "mpk": "b63de12e3ff4ae606c874662385636db965833c2110fac9c7cb22cf3587e163477bb2938bf496c03a8cc69cd2197b2c2062d657ec0c743985f92d9b500b44a33a5c5a91706dea5ccf91afa0f3531d45ccd5d5505e6604f7d662d126e5e5774e7", + "public_shares": [ + { + "member": 1, + "point": "92312af844dcfaf1ec8b425e072321597ca144e88c88593ef8ab20081ab13cfeb0cdc3ec9fb95370ef86d5d0254a99f219cf18e26e3f89e0a272139a3a4f5953d4a38379cdb08ea81cca20fe75340cead1c606d0747d00e3084210d4aa54c580" + }, + { + "member": 2, + "point": "8ae9633ea7431859b491233f0c14b7505d254d87e191e7a6dd7cf08aa33d5b2ee0fa09de4d941edee5ebe6b90a5a59e91130a4880c1273c9635402d9fe3ffda578048ae8570695d12487cdd92d44457bf9ab66c5ba6571c370c3ea3578131fa5" + }, + { + "member": 3, + "point": "95f5429bdf077cb7802978f351cd79b1011088eab5a6a9bddd74bd7db8f824ca76acc3a056f068fadc881d5062ad11310dc8c755cce263b75d56afca5d5cb01dfd5528fed869bc2f45f81114d33723f4d348a2aab8406731d3b11a218fbfd3ba" + } + ], + "qual": [ + 1, + 2, + 3 + ], + "reconstructed": [ + 3 + ], + "transcript_hash": "19b6b99de67c25d936c50362dd0ad08efb19ecf482d29e3d05982ea33d47988e" + } + }, + { + "description": "Dealer 5's R4 passes only at members 4 and 5; member 4 files an invalid R5 complaint; members 4 and 5 send no R6.", + "inputs": { + "ceremony": { + "body": "010163636363636363636363636363636363636363636363636363636363636363630000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d3000000300050000000000000000000000000000000000050001000000186469643a6b65793a7463323232322d666978747572652d31fc07556c1b0394033f78ac77eac751a54424e2ece81ab90c0f2f5904c66bc57f0002000000186469643a6b65793a7463323232322d666978747572652d326937cecedf41d95aa7318daf80b12c7dc0ef2d728e19c321704f82e8f3500e670003000000186469643a6b65793a7463323232322d666978747572652d334c870e7760fab172f0758d751f2f551dda65e32412b3eecfd3d9dc7d3380ed350004000000186469643a6b65793a7463323232322d666978747572652d343c6a894ad08b8c4d371b01e2afaf3ee04fad9d62401b437175c64daf9e2a3c090005000000186469643a6b65793a7463323232322d666978747572652d3521911f82d2c7df6803df960e638b48abc4517ee45629ff1a2a4d0b79c5f4757b", + "signature": "25677afed79c84dfc92bf88f1df7e2d18bdbd2d51afba879c38d4e02eae3562813df8fc7fb7b6a5ebe94df328213f53f4642795f1e6912eb02a8d9cd1817ce09" + }, + "ceremony_digest": "6ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1", + "initial_transcript": "015b464ebae13bb31472d78086282ad4c8920529a6980fe72afeb17144e1ac80", + "members": 5, + "rounds": [ + { + "closure": { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1012a1d5281c60fdd141836ba74a49c97b044155e33e733b711e2dae2ad1dbaff1d015b464ebae13bb31472d78086282ad4c8920529a6980fe72afeb17144e1ac80", + "signature": "acaba6cd88e73ad73b932874aedeef2727aec3405a486889cf8c6d793a9340eb37f3d86c9fd4dd9bed35b52aaff303d147ee1e2f1f976a6fbf7f6f793867820c" + }, + "messages": [ + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1010100010000038c010003914cdfc1e3f575d4f8e65bea22ea24193216cb58e41bd4df4c9c693e6e6c1d99a1fb3106a1603e0e7143ca45cae343f102a4319bf32bb9ef6e3fa0732606b65bc113f3ada4f6e3dd454ad237d1637ac51b20c78b60481455296b0b21c29ea1b4a31f1635840c6e83e2e296275156db745f84e0af2f5bd0b2aaedc6a413d06330b39000466909be2ee3e3b3b7bcf8dff4020f32efd17279f968071c46eb5004b61d6afb1c97f87170330e904c304a25dc348f59f8ab158e6f38ef8abea1d54abb8065bb0d9a8d1b02172aa291bfc5d00ddc69a8375c0bd2a3ca1fc7616b7a660452225ef4a69bfdd1b8b4a2acc7faab160dae492d2225c7f43d65587adb59472d385df9815230ad319676c8464159a56cc9800bd2cad7c15157c5f1ee25461170000500010000007501fbdf8f1d1f935cc8d000e152239e9e44fb02c5c00a31fe142e9ff2ae96b2452b000000501f493723ecfba21c62ac82cc73c30e07c98533d40f7e3520754228017cb530e4316935782bb6f6385529efcace87afbe14141908c40fdaf640f82c1ba47dd1cb967badeadae80fb251551f014a2f0e80000200000075015b0e4777da377eef7bb5766fb4743bb2ec9cca45f5b7aae91c242e8573349b03000000506e5b19abf644255b398f768b14c5b9e592344253945011b8416d0f0d6dc41073f1909635421258ddeb61cea17e271bdbda03eaee10c6deff514bdc206f160e122f2c128926998b6a4b5fa931b4674c2700030000007501e24e22024085f2d86aa3421edcb6886c69c9f266eb8efd263d8d414b8771456200000050b93bce688aa3c9908534be0a11e1d8be6b638b28a98bd0c0140aca49d3f21b112825e162daa2c6133dca29ee232f0d6f267c1937df3a7ad99ab678323824b156760b5376e8ea8669401ba3f49f62b8e10004000000750193b5b5c50d51326df7621f69b4a01016b20d172c577f2a638741ab67d73260420000005033f2c9a1c09ebf75ee5ed44251468c9a1c8b6c3f1f3d2f1b4be2bdc0548e7076517103d36d9ebd9650bb059a07f46ebdcd2c2172d4d22b205246f4a42f9869e289ab5499d1aae529335c1f15cf2d4fd800050000007501a3a68b004b253b3534c011dc0297b5f96453bd094236edc2ce46f22e6ec27159000000500febd24734b51644c9e006761acf0cbc34c33fe2fb6f2444ed57d1ff621cfa2e897d029172ff4a15cae5f477397f0591e77eac82a2e61ccd2529a924e1cadf9c4e7f30dc4c4097a72f06352d5242148c", + "signature": "4f20fd537aaebc1f27cb8a844cb2543699b0bed3f6cfaa7805ee8d0a321c123d574b70091231ee53d78130172261b2b62a7659aabfa44e57ded92bcbad549801" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1010100020000038c01000385ba3e5e4ea0a0ffdeaa8caaf5a108d6c0209cb653662970adcb4bd841e87fd62586713e88125463a9ac42889d8029b10d882e46ab413f756dbbd71eeffe2cec64b4f012811c1f0690690ef6f1fba72f897b2565f5875626d4132950ca72a7658d44b9296ab9dc39253ea2f959fef0f8c31662d03de9a14c47e8f86fe77ee9873efb5b499c19800b74f50318bb31758e02ddcf6bd6e15162c247e8348d511605b533b4cf24912de8065f5c6767084eaa353cda7fb82c433b3eb41f05c4f463c0ad8ab9fe5ef3b1917ea5b4cb794f72eaa0777120ccae178af8bdd2a138bf165444620d377e467a3d952b84394c3144600596da86ce56d0eba94717574e852638198c7932c9693fdf9784c1acf11018ed23a52aef3987f709b0d54f42cdfef908000500010000007501bb0a09eaa9e5e26432f8d70c2308dadee4fb96908c77d717a6433d2059d1da4a000000502bea904c5f3c297a677812ee34190b15f0f1326cdad617567eca6416db5215575954a80ad6de116ba3ef9dc7f5d4ba50bece996f0b807ac0c828f3437258d3ab68269c0caffeb15d2e617f1cd5d172920002000000750161b52e2de261a4186dedf5dd7ce1e254e3a3625c697ecdfce125c9a0a9a1a71a000000505601b802937cff56f28dcd8156d99cc1864e37867ee76f1e23a96d0e65038f776352e9006685877234167c39bee46666c80aa680c5d76a2c5415a51c0c1e6aa07349aa31d2b3ca859e17968996a4f86100030000007501a68fd59c01271ea4102f680d2da8d1b8497babbe75e4c1b7c2fcf27d5b0e5347000000504f62bb2aafdb1b8d90ce10e0c80570721b2598a5f9e3abcf1578e20ba65db5f1198df5f52740c6a602c032c871692079d5208c8a39f9d4edac764bf200f5e3ba14f298a0d729f3d0fe4e34a2430176c700040000007501b85c8554ac4a9c19378040755acd0d54d6e457267c9ded86247f3afe523eb95f000000509b2e8cdc500516fb65aea9129707508549867d301886a417a6c0930c02374e78e5796eff44f82b7e84cee88eec6ed58ba967add7eaa4c5d164a48558dad65a717ae2f83b28ee25e33cb2ed52a0bd1f9200050000007501563fa8b59869b916eb9344f74529f5ca45046bc876db136310d2179c861df80b0000005015f5ee2f3c066e9feb1b78d526e3884ca3504b956a986b27bdd31c2453c73cd19a74dea1b6e9fa215892aed09a5c3cd9091d84cf5fb0c8aa27e836a302636be36780d2cef11aed62f5044b5e58033e77", + "signature": "679e5120ae3577575cc872fe284f7d50180c4bd6dc9a71c683283fb5201ab3bf18431d578fb6de74102518c30c57d093222781737c2dba5c272cb666ab062a03" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1010100030000038c010003adf9bdf65c88465398659a41c5756648d104073ec0798020bcb0bf9a852260cf841d2dba52759483d77514c9c6b534b204b4edad4af0c62cc14e2a60986a176dd28ad36d346036835268b31d19e1f00003ecae3e36c399363990bbe8958cf6a881977bc2c4d914f6aa11c6e30563577695e26fccd82ba52d2b04619d10d9c37b29af907ea0932a146ab1c6a4d575fcfe0058318d7a4ed2a83467ecfc5cb0193efc6d1e158bef5916a30c974048c2b73992d96fda66a8056304acc001de7d4cc0b8351dfbb7712b73090f54f8e781f5c6ad45355b573e6ede8affb580d777cc3a752ae46f6553d22efe6267f78e56ae080c6df27d1c4556c88a46713508f05cfc097c3d2d2b3bd9993155e2a8893c64ba2d467763ae8aedc192759345fd25af11000500010000007501ec5bba5063eb72ba8834f1f8d1a48216f061389bff75daf022543dc2b9b4861900000050d9db3a39ebec2a68796ca70fbe9d5e517511866f8a24eaafd7a4bcc72affdbac8fe069a6cba3176b65fedd7b933670076c6b1b5a888241df132e16bee505a29b3fdb603420b4520179bdc836bf8bd4ba000200000075014bde83446dd22901e8d6ef3b2ec3c28ecf82ac8453d0f22d2f2ca1b99921bd7a00000050cb510056d551dda7dddf511579fb8dc8cf48ec40e32b4ba9ef8dc3831992f253c15522573b1eff1bc86a833ec32183a1b58dace7407fc5b020069862f771fa2acda33841b10d0adfa715e452370cd448000300000075018da1338d6bb12d399851208cba9ead66f160880d3f9099aa2b17fc5761def57c000000506e825f66a096b886d665d022c0538a501df7eb5e5ed8ee23711042a1f6ee5b67e42953c802bd99c802317c9fd229c395c49098353e6924dfe3e8bcc34d832d0b88ef71dbd460a91b44966ac8492e6e7700040000007501dbf02fedf7f17357cc3b9129c8077cfbcd0bbd35132c71132256deed946d5f7100000050b6eee0378475cf4b21f2b01d3375f8057cf68e72d7aee33e5d84a88902e25720309fcc43f2587f8c8376a761e7300f7aaab1fad095d5d034d3b94d844149cd10d29779f18f19dcfd86b4daa7464404a60005000000750186d70d64afb459116a06319641cd22fbecf4764f66e7f6ee851812e74f90d70f0000005052ba114c53aa8e69456e3da5cb76f6ada31ffd8c1db0ae7ee68641fe47792419d387ce2153cfe74691440f2125e62ec22dcb989439272fd0fae476d0e3e27a53acd5d370b5bd2c3fecef2b11817cb643", + "signature": "a3171451fce0f499f99994070f771c5a4aaa2d1f11a844bff875ebf8c89da09adc6004c69190b2e0642a067038929d7701912a99a97bcd2bc20c39b087b6460f" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1010100040000038c010003a73efde7ff4936987055a3167a45e213a82d34fcdbfb002cf37aed4b142a039473470409436a53d8e79ac441fe1ecaaa080c2cb4540db983dbbbd9ff47fa138b6f422036b27708b4cacbef27817c82fac00b43535803069877efe94d3aa2d5b3963268a8d7b97598b07b91c077783f9a0ed01052d4227c565e19cd4475357c27575eda1fc0ef0bbf058d0e848887c6ec0135db236342168745a217cbe3c7cc4353554a5beeee02ff8a58c3e022a6354e4b177bb2b70819b575de4cd9bafa23efb4851077065fc23f02fff80e5c7bc6dea16855e11041d639fdb26edee885c60ef38095c96b212e029cbdcf2e4c1aaea80b31408c8fc8a3433d49a0e42515846a0723793944e50b50d57115d9636d8839de1b071cd013ac958116e5ef28e7522b000500010000007501d05669c88e331e640a1f2bdad4e6b4304fca3fbac8c66321243e0307499a8966000000500b59a0ea8af14364842e1e0d8e88d8fa1e71b783a54ef8cbddc695a2e412248d5f7d34eb3f31d02bad74c050aa253779ba325131637150ab9fb64887005e7c83a2e4dc11066dff6d230c7dccc2cb8363000200000075014bb04b95db5427cf4fcdac0aafe0f080c2a13ce4a7773794f38b1709d373d62f00000050f514b376685496a9c348b45c93b89f1191619b0cd938f42f1598a9ce90c27c6440773045ca942ad115f3ce4e89ab1b17ce0be846d985cd6cfad88268771ff3b4f094f7c4a1536fd515f8e4f796ff6d0c00030000007501185d962c42d3eee9c306086d7ad96229ac63ba21efac982a6e4095e3c5bca01a0000005072582be33f6f5598a4aac563d3b941a91ee84a4bd3f64b3e207a165061e5146c4b301940f6a9c6a6a03b986233f2922c9c166d2243a6a74ddf911ef71dd27dc477eebc048229a07a9b97f271d4d0e7ab0004000000750107c12c50ae9236f68d3e64c4618fa32b283658f2344e809e33fb801c3f178130000000503b492d0f45725c70d208b461d31593dd46499598797ee3128e27726e943cb54866cb540fb805da601939059c8354bcd76a0d12f693aeac2eb06967d4c244c9b95f18f94500c7e35f36a37c2596488bc700050000007501e67051451e1d55fad535a979f9158c336e08144d45b81d6b348c2540a6e05412000000509543be4046e2a1610633096230a290cea0915296a5aa347b365bd6b44c74b74afd8ba95c7a85ac5d1f72fc3f28d725946c37e3a87d9d23ac9c5f60b9debbec18b6d9a4409253abfb696a21fb435aac74", + "signature": "c93c94d855eed53833f659e4c400d74f181a66cfc4185345f5405e06d68a4acb206099f5e03585729c54bf1ca22354ac6a4ffb310f093ee6cf2b42863cc08c00" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1010100050000038c010003a5299847600c3abd35cf0058d64e096dac636ceba3e7bc174193708614bf205027ca1e0ffd402d03d1cff02d2403e18e0fe904991b305c24b330c37621cd9c3e2c702c3b28256aff2a41db732ae23a94e654d8f7fa49a52fe579dfdd906707d8b5a0b8c193bf078be791a9ccc1ea734273a48631b2f3edbc44819f4f4bea2004fe5270c15c9c43bbaf40e420b43c232101e7891e7b03cd1245b5e06d43123157dd5b30f4082d3c9556d4005001572cd7fd5f57da8956280c07d29ba6a8952b1ca6e0471c4620b264f03ea7602d741790135d11b65884ad2f314d275b085c7ccbe8e2cb6791594d0e2041c9e75b9c665f0e681751daabe3b360b6d7bc47955ffbdc26cad1088b797aff76f491765e6a1c4b2becc47282a7e0b958450fc7ff1ace00050001000000750187458407de235cf1645df66e38fea62ec407735178a0a2fb06e0de06c2e4e00200000050b460b1acab4bd88b82ebe389f6c2b6ccb60658e041ce39d0b026643b0d7fc0fec440a657a57763e25ed4cd568c3c830e3e6d009c23ab66958b1e7b878ae60f0fc33e0d7c328fff61a904dc7a5b764f5f00020000007501f04ff885215bd72b70daca5b4fe135fbeef9ad3d55e9294c821d0d183a823800000000501a5f4b991e7c76e3a62b7a721c8d090c862b2ec95d061be6f1fa09af1624734608f16331da02f93d0c6e42a02766d532037a45282503e19da69d2921d52f58224a912f91909f72e1935466588bb3db39000300000075013338313bc604faf013c9bd38963f1740967534b40b14f93bd32ec618a196da7800000050b2135ae0265601388ae5eb695d7f7cfbda71cb61cb08ceae10f19eab1c056fea556ff1d61b9423810a48fec9095d41b18f8e225edeeb5c35813d719a6b92a627f5d5aef442f734be80eb97d24ed49f2f00040000007501dc123512d15677a00359165e3b2d167f3539565584f3439fb33fc2254f8cab400000005062298be3cf40b0cd570efcde33bd087068488ec1965e1e51542d835238383843c08d0969e876f9f9ba69a42737daf4ad02d99707ffd223c4eeaa57c393efd834cee1a5adaa634fb76f388f71834b27610005000000750113899a40a729c3fb8b46bf096e52f55366f5517218e31d7a73fb19b910e3707f0000005016aa80ff299de371d642615e7c41a006bb3fa372739d09bde1f96664619a9a09c764d57615e2d38953ccf083fe22d5e0a83f383c93975206090aca9df786c8dd1413b973ba952b411b1903d026ea5bf2", + "signature": "61e8f34abfb65a714f2deee46c1a7b5328ad61e338bbed18ff257a7d68278462a0612971b6b140a6eaa2e6434f9e9005d44d26ffcf04db99f3aff34f8777a505" + } + ] + }, + { + "closure": { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1023c10720e7890f14e6dd6ac50a09f647f7cd1e286ac50a9efb59c197b732c5c73019552ec7dcd0d0bc6fc2885505071c0c15d7d990e6b3dd7a6bd4799d8670366", + "signature": "3fe02bfb6790592cdb327e4d9ca4ec5da4729e4c854a325df82a02203d615e8a98c339b6e131ce428fe78a8a766d2965029b0f3b41f9b20580fef0308b30c506" + }, + "messages": [ + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10201000100000003010000", + "signature": "7474c7775bd5b7a6a4b1fb25862faad2fc9c7d7de13b6028643c1d708a59bbeacb66f21710aee0ad670b554aa01d83477894cbde40e58d31fef894af7f65150d" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10201000200000003010000", + "signature": "11a9918a3a835ce2acfc88a53b47165805c90fec4c2a3c3a8b729832de3780d6da22aa3bd55171799cc27831d100b0d9cf7c751013f02a546417fb2ffb401402" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10201000300000003010000", + "signature": "16096e9588dfc247fb47e63cf82497b3809ba751309ebd9127feebbfe758c2720acea749a16437fca1129279ba6298feefe231850d9caaad47b41b676c5b480f" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10201000400000003010000", + "signature": "1dd3e9bdb7bfc648c73d3a3ff511d3333e2900703bb959833b19bbe8f8f8017cc7e0f31adaaf945211f280aa0bb25acdccaffe13f0f5868bd5c5eb7df7538e08" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10201000500000003010000", + "signature": "7836d4a4ab5df83e647f338ee8a8ec26d6f679e2c5aa12fb127b0b25887c4fd5d34c0f4c371f901cf7e770adb8233ff2369c192de70ce6a6f95a89e8f5ce200b" + } + ] + }, + { + "closure": { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10309fc819a065396defca0c0df7949efee3cb195384913b0815fe3c1ab89e1f630deb5bd511927d887f6cd0777395d8e7ae096f0a5f6d5ccec5d785f1d18b5ca44", + "signature": "4ccb756b93372a02345165aade98791b2629550223352d1a289ed9adf453fcac1d32ca0c6ee350a809bd812e3ab824e2179fe6941d0ee5f87898797bcd03f101" + }, + "messages": [ + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10301000100000003010000", + "signature": "e25e45de0eb17682da2ac2e3accc4bebf9078fea3ddda19c4d2c6fb9be4b693c22d81d16aad5c232c9562366a65ea1e7badafe2d49f7958a2ec3cd2c133f2c03" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10301000200000003010000", + "signature": "ae4b5cd93f2cc9bb47bb49bd3c6c07ec68423736e21a63422c402340bcd450af8e50b4e91260641c25e47d3ee20aa48f427ccbf386f77b79b3e674de645f610c" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10301000300000003010000", + "signature": "405d1de17b6153eb6c3ce61c443b1650c1104d99f005e929aff3667339d930a6bb09e8399cd745697ddfa0bb2a3b2a46169ef258e18eaf68c246f8a4ea19f40c" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10301000400000003010000", + "signature": "45f969f6b1a139ec06b623a902e5a89ffec1ad0e0e422a1ce0f8e0bfbd4a3efa618d0ab4742a29d814a19e007e9259b3b6010fceda9c8b56ae4c0bbb462b0509" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10301000500000003010000", + "signature": "3411e7f6559de533359add64ddedce654fa8b1c433a3e4434318161764fb899de6ab6264a4617005122699f584e82d44fd60aa19c17ed9079e9348a1c16dc60a" + } + ] + }, + { + "closure": { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1044d39edb7a1c54990e61bcdc2174cc7d830a41bda93b68ae24b410d88ed4da45370dc529cad93f96aaed2aa45d8c7bbdced7100af9249c8e349cad863d993c3a8", + "signature": "7331e46435cda56c066e798bf34c6f502d453bf7a6350dd01c8ffa5b164fa387e6ff7be2af378d678b4486023a788898eb72870ad4695300094de691e6407d01" + }, + "messages": [ + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10401000100000123010003a35c5e7296b36e8cc85f32436fca002c10b4bb4f4656e7aa24b460a654e606d6a8f89a15d72e2eadf3a14adbdf61e7ae192fd1acc48a0130905ea4baec2af80fc82384aba26ca6f06437208f4d6c08d88ddc7eb08d193693171d2e6885c691b5a3d6497012adde5dfb65a77f0edad1b3e4f8663dade66e0bd5788fa5ccdb3d6fef9ff5669165de46f468dd63245f627a162ec5414dbd99d6ea4ea78b812199853bc06aefe577bdee3ea5ac434c138f5657f248f2b3e417eeac62ab118b7d50b7a1af4180182ff24e5400859d91554d27acdd7740c4143bfcfd26233b9f3b5981cb33c397f17ea73f3a958b9808b4c0ba0cc2ac94c558007ad703c80d6e5b2ca037289b7763c80d224424856fd47f5d0f3bd73c4e8602169ad1945fce51c52417", + "signature": "1523ace14f56a8252459d02647d9e8f5b4186dabde209cb6efa87db90847c4d795967858c14811181617c885cb144ab085122f1b938844a96f2c4697d8ed9b0d" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10401000200000123010003935168bde52049dcded6d62b0172ff07beeca2a32bae97273823169cdf8f15ec6dd0a6c2f20d0d9d2f7e801541ac80110785062c4fbda64f7df627ff37fe469cd8ea2334ad7b5ffae20cbbd5f2388c1e4b95d456a6f9ae13d913269cdf881bfcac9cde3533025637ad87abc75d8df905d0c41272bbfc2f1060b99bd7e98d7b3f5d7db249775f3690f8b4ac0f60b297b603dac992ff533eda2af61f54e55752932108ed751cd76e78a82e8c928c710b73ad11323acb896af1781fb4fd24bc5082945a63fcef30ef8261e18cd768ce49013244965fffc01557158ca04e97250c6760ff4251d01624331e8a37f77587249819003854f17ac6b574a32cb8110adea0a8e17bd81ba66c68306030aefd5883e7fa003e0066015e764f566869519de98a", + "signature": "3aa745a99a34938fd785f665b879343c453b14ec7cf5eb31e80ceb1e66fa803ef95f8d334c6af73a75d36095db2b1fbefb80ea26e0d8c985e3b2aea7fb8f7205" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10401000300000123010003b7003ec95300ed39dea352981a4a5c8e05d8fc44fef8ab8530e88cbe5620dacd3ea86a5f0c68feae432c689b68b8f6110f17ebae478647e86389b4291f4863b40ec6c2ef8503664e92ec036055679e68c6db806af47b134b37b6f09e8480313da599d81cdd7e32dbc4511e11ce4ce9c304998b9338c475b10f2348d06b41e0d294fab8875105999e9353575b79f4fbd00cf1966462032b48b3628690cca7f70e8e97cfdd8b347da7f7fbdec83703963aff88b0d7eed3c21c696efd7e39cfcdc4af304a233c0c52cb55bc4d7b88c598854000af468c69a9d9a976e47a27762a740a233c46f004f86da2ecd46a4dd4e7df046be15250deba41afb59e255c29a7076c4c81e1b4e995c88c935703e8ca1a5e5e519ed06fd35d6ef43c6efaf6f5a596", + "signature": "437769f3d4f9b2e2e92845e9020f2cfbc5480e22b2e734fff2d78bb73579b9347882f0f385726b18f86dea23dc5299c3f4f9368d54b5646b5abdca4170f53905" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10401000400000123010003b2f9febfc6c9c484086c2c4da3c2d03de10cf1c500da345706d29c4824d30c81129eaac13819e36fb2928215eb64f4180829c92eec4a6b54ae6d4342178beb2e7beeae2f149839b329648dd34b1b2428187feff19969f76cc220f49f0d0a2211927c051c566ea268bb3f8ac9c06fffe4999eea2a3da9d0516316c38b7dc5559fe9226b5561362ba012a5237e8a03497e197b38e007d1e2e0f9dcdbbbfe2753a54b7aa4c9d2626f378b0b5a4c460a6fa781ca461ef297d41f9d891c17d1fe8682b4ea862c0523d43d584ddc7bfacfeeeb0433ded836719fcc6e1b9124d7df888196171873577f9f3c33bdc5cc077b4365191b9d610931f072956bc2ebf1f2f57e750c5c99daab5e613dc9c23e64a463a7ed6e4570ac9391beb653393fd5ea3ae7", + "signature": "d217fe7ee590224122e9105e280e9e02ff60f573e6d79b148c32237d434aa33554f3351a1daf176a73ac36c26f14026b207e6e34fe7e45dbcddf5a55677b2e09" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10401000500000123010003a5c3eb2da85dd092912538499c6bb751ccbcf65b80bb9fc98b0f657a86f1192a0d70d1c755af9f199c1d913d97fe1dc916a23296e3e1ede7d96f36ab3555489100086eaae81a40bac34278af7fbdd3e997a7a0c2e686e71ff6e734cab448d7caa5db7e6797372b44a8e22a81ad4ba397d5753f6b086494f7bb1b08fff68c1217da5c8e87c450462d1214c57d9598d86b1621b481a4808f8b64ef125ea0a6cbce3c511a97bfdbfe78824d6667dd3c3cb6078bd6ca632185f394c3350da53f5e758fc2cf7921f8c2d758793f098f18d091f2b6ea0e9ac6244e11afed2302a3b56d94490d722869148ff3efc94c470ecbbc083fdf305344a7e0b58dbfa2af96fe925787064d23e8101ee4b4d9a04849edac18f0e39d73ba9039067c678f86beb111", + "signature": "14d81625e84e25ad4b68f0158e122c211e787e70b8f349de5c252cd10cbee40bfb28d76e6f9b7aa80697b8f29c8877760a5365d8e1bea4192270b719e9e10908" + } + ] + }, + { + "closure": { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1051f7d67efae17d07d7326ffb8c186d52b0d35c82999b6b7ad25af2f4292c09a4053931a723ae3fbb61a39bb13aba21e88ec77a8bb6722f53f8d76048bb0733979", + "signature": "5cd49f2fedfd57dcacefe4e815eda7f43292619e3c389f6f3712950aa412c7dba749e1c4c0a3b6927fd9fa3739cc52431c168e9500cd7f31c4b3572ab5c8f604" + }, + "messages": [ + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a105010001000000450100010005210c13142328a04e497807ff567d35c47df3233a876bce66c3a882c21bb796e5704173d7becfe027b1b65fdd6886a2cea5298a376bc97f44c24dda873df4103e", + "signature": "ae0e3cd7042074a5c7211ff06d5d97735cb940b30ff6766fff256b0121b6be5699c38e25db85a3e5ba5d59bacab9b43327d50fe4f8f959bb37e1ea0e69838708" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10501000200000045010001000518a8a7e98021c65fe0a308a5ee6f15cd0ab03fd82c8b89af21d0f4d0b5a0df5b2a0cdeffed99206818fc5128315e610ee8b3756b1d5cfdf16884d578c0d8cc84", + "signature": "63124d54e544f28b4f291b2a1202ed22d71f55c34200544539625fcd3815958932c914fdb9caaa8db65d786555fdf397a430c881a1e333bf2368e82638bafe0f" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1050100030000004501000100051458ba171f4dc22fbffba10674ae95891c0a75db55c7a3a53f93568c394449762f64e113a2d6e5848eedc383fc22fb3def0f465583e9144021799fdf4c0c34fa", + "signature": "f2475f51328e484d501e070a03bef94727e6ebb6ed273b0936e2de7440924451652bf2df35bdf999940b76f0fb255f5e12bd17e70fc177d05d669e4dbf81f305" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a105010004000000450100010001000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000c", + "signature": "3de59c2e68081fe7064885cd97c89ff9114049ee62918d4d8c639496a138465dd9fd4e06c1006c31ea9600c4e434259d4692d994d0dab75748ed605fb82a980c" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10501000500000003010000", + "signature": "8b65e6a75cc6260b697872edc80c678cc7cf2850bd6480e73f413e3b17d37edc70dd3045cf1b39f5018c96f5d2d919c5ee5fab7e6c15a903ba05074cca2d0509" + } + ] + }, + { + "closure": { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1064af37b2cacf191656b09a2253040665d359c2f51eeb27e4664681f194d52ea4215e08dabd22c0b223ed31d310b7b0165d02dda5aa907e03ae17004a51b903d10", + "signature": "23b0ff783f224a7e5d3433d923a00d705127c7be9a05c9cc3faa2f676f2951e92a97e809fab708f7120e01abfd3232a235eec9581fbefff8718a17f45abf940c" + }, + "messages": [ + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a106010001000000450100010005210c13142328a04e497807ff567d35c47df3233a876bce66c3a882c21bb796e5704173d7becfe027b1b65fdd6886a2cea5298a376bc97f44c24dda873df4103e", + "signature": "14b0188f1d57a1306f0829410403ed96ac874cf2a8a88f5ea3758f502ca4b0c76e0f5cb7bb74b942b53036bdbd519d3b7f14ea850c92f28e7500500709302c0f" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a10601000200000045010001000518a8a7e98021c65fe0a308a5ee6f15cd0ab03fd82c8b89af21d0f4d0b5a0df5b2a0cdeffed99206818fc5128315e610ee8b3756b1d5cfdf16884d578c0d8cc84", + "signature": "1dfdebc12b258dcaf874417a6faa11fcfe7c43cd9e1d020b72c80c4dda24815627ea449ef07a60c117feddc09bf375081b37fc0f2d3b8f7a1e709d0275346f03" + }, + { + "body": "016ae418b46861033d191f2a9a499d98374ac133c5493608451f4a3cebf8f395a1060100030000004501000100051458ba171f4dc22fbffba10674ae95891c0a75db55c7a3a53f93568c394449762f64e113a2d6e5848eedc383fc22fb3def0f465583e9144021799fdf4c0c34fa", + "signature": "6b20bb2e0e6ab5dfcc816b8b1c0c5d9f371381e7964d7e9f095f7657d1815fc8384aa8b482be4c185e79f7afa1089d159dacc33d230a2471c5eb504bd1de6a0c" + } + ] + } + ], + "threshold": 3 + }, + "name": "genesis_3of5_two_faulty", + "operation": "dkg_verify_transcript", + "outputs": { + "commitments": [ + "a7bdfc4eed60f7cc3b1d0ded7a911f7fc2b12339a23208e1cc6fdaf65387f079ac6991ced78ef7bfa98cc4e9c73eb2830da5682ca9c7eae6326fc5e42699d15ff450ebeae477268e1541710817a6c1da37c5997f814482e102bbd1b033be3391", + "93a8dfa3e80b18b28fbdeaf34242b47eee9b2285175387f1020b98d2e08b28d7c7e45edf6b8f88dc683c64a60d23d336132753e89a188da4ccdc191823fc8eba60bbed6c784c3b48c41eb1bcba5b5973693f2b9badb7e44340689a1762c83c0d", + "84d353694c58b45c8286ff712017a5bb6d00dd35509d52d35a20a0c3d09d36bd1bc034fa34eaae0d15a9e76d1b574464031babbf8d25a8c18ece182e8adaacc884538d92432ca43fc4baf2b45d2480e414dab99a0e036542da73e42252d237b3" + ], + "disqualified": [], + "mpk": "a7bdfc4eed60f7cc3b1d0ded7a911f7fc2b12339a23208e1cc6fdaf65387f079ac6991ced78ef7bfa98cc4e9c73eb2830da5682ca9c7eae6326fc5e42699d15ff450ebeae477268e1541710817a6c1da37c5997f814482e102bbd1b033be3391", + "public_shares": [ + { + "member": 1, + "point": "a4cec6dcd968988bf03583727c518aa1e558bd554dcad286f848d433cae8494c60e9299ae2282b82e4668fa4868f4bce159aa98ce34060402fd310da51ad81b2a9a2c0b14375554c2296d9ef1184d207a086c4f6e52d608ce2f41ca70010dd51" + }, + { + "member": 2, + "point": "8543be2992e8e9e4925bfe57230a2ab4dd17e4a22bc965a6e93cd1f59c434ee51a077d3c30797c2df93b82b6acd4ddac04fff815db14e8be669509ae894427c57e06de80322c2b8f7c9c7dc87cc920132a61bc60c1dbfb9e3390bf37ea6fe3ff" + }, + { + "member": 3, + "point": "b32cbfdbf7da7de66c33231b5c2db65198ba39ee1046a0df364d313b970ce1852a4bef0d302491ca9bb45ba497f6e0e60526ad16c4f293f1eab5445d3b5abd38fd065bb04b28d90605fb789e9f057d3bcf969b96c3ee6cb019e066638c389096" + }, + { + "member": 4, + "point": "b9e2d193ac2a712ffce1d75da65c57ed04995c36d6a769b2632731cd7ea88f4a5747ca1db71f0c7d1ec3dad46745151a110f547715aee3c243fba6bb75ea5f8424455f2590bf6e0e6175a4e799e42be87fdb41f6d2b63afedcdb916ea6071e66" + }, + { + "member": 5, + "point": "a3519c35b4b956ee92f8e7170c005e9cb7efad87aeb66aae33a071d5dfa1505c93cbd4215a6e8ae55c61cd400d8b46a20658c0298066a2fe0e5b699f1a1de44af056a93ea7362014d50ed7365ea9447140d810220da38e20cb9de37e10a0a991" + } + ], + "qual": [ + 1, + 2, + 3, + 4, + 5 + ], + "reconstructed": [ + 5 + ], + "transcript_hash": "eacedea49628473d397d15f9103541746e5937999de9049a3725c62ba21f9998" + } + }, + { + "description": "Dealer 1's bad value to member 2 is answered; dealer 4 sends two R1 messages; dealer 5 does not answer member 1's complaint.", + "inputs": { + "ceremony": { + "body": "010164646464646464646464646464646464646464646464646464646464646464640000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d3000000300050000000000000000000000000000000000050001000000186469643a6b65793a7463323232322d666978747572652d31b8c91ecaf9cbe607a8ae4b7e09cddd2a5510f67dc8c5bce74988585b73d395210002000000186469643a6b65793a7463323232322d666978747572652d32e1f2072880b09c007f0ab3358b6584f0ab4db09442da8ec185cd9893998d09750003000000186469643a6b65793a7463323232322d666978747572652d338bb953c9d9eda57753ef1194359068a100ae78e13c0754017edbd52f8d4c64160004000000186469643a6b65793a7463323232322d666978747572652d34ea7c95d49be79ab0e89cd25cfa528be77c319f7a7afaccb8cc9f80bad42d7d3b0005000000186469643a6b65793a7463323232322d666978747572652d3569d8c78584d182d9ada31836b6f2f298d5e96534b5bea1381e47d02816b44734", + "signature": "43eba7360f09e41f6f46cb56201f66b6411d84848c28af5487c3318d52c43d49c4bbb1aacacddf0ba34395e3ff16364453c6378a6147f0cd92464839a2af8601" + }, + "ceremony_digest": "990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f", + "initial_transcript": "409b925ccb7b841e470e0fe673a0b66a219587e4d96672ba5136b03db620572f", + "members": 5, + "rounds": [ + { + "closure": { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f018311d8acb0e65386351f318608264b733f887ac0b4660db50759c6507272f94e409b925ccb7b841e470e0fe673a0b66a219587e4d96672ba5136b03db620572f", + "signature": "fc1e59d805c798a3cf40e2818bac639108464ea4a476a6698e642c0742094a7eff0310e4e2b49643eaea90cf48575b380027956a897b09392ce38bf06a75a40b" + }, + "messages": [ + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f010100010000038c01000388cdf455838f4371235e0b35300a49c7ba6058bff2879861434195533efa099af6b38b55deae2642892ab5dd2b2f4c411669a5a3f8b12064ea402220d92007d599026b335979465fc06ad6a6bb9f4da056a36545e5053308094a9e818cd9ced08f1b0eeff25ef896db95dc6f54b392f6d3c742552f30ad1ed556b417e693b5ea3abbccb1ac538f8b9acdd6dac455fda907ba023480283601b6221c26c2151b7c4d9d03b48e3a04ef89d638bbf610912c298623f2aecd703c17a890c358ad9d5a84cd2e241ab05e14843db24af64b148a4b85a695a1045d1dd7446b757e83784857a95026a0a64104941eda3170fb06ea0d2edbc9373ec562a8b7467bb5dc00e38306cd16efba54f3793f335ff0d4069e9c14ff2597c2ecd4415c4d7b4a5fb909000500010000007501123b0ca1e1a369376f23eb42c767dfd781a750615286cbc2d49e07ad6152005500000050760c5a84ab90eabf9d153fbc27a6888f6a18cef6d458a0158a6e4aa90320171c6c36d33349e83ee9a025dcd6889ac51475e38bd5080137f37b802590087886b007cbab181956a5a84ac86816b24de95d00020000007501e085ee426688d9a6fa94ae50b546c7f502d9d1664724f0f31d1feb35c95db63d000000504594e59787716862e65bba08cb69065774a8e4c63e0baae3abeef5b3816c0372010f5e7bd206162bbb30ac1046c1d82c6c53280ca62f6b1abfb11386251c2694f02ae9ce966651676b065a9f6ba0dee5000300000075013c1a38297d91329db8937d2874808789fc37c40cfb488c4ae37da0b9204e407800000050e878f15b8f6822811c7d05c0dbf76c7b5ffae65e95a0410a6ca04b2a23c3364ff37cae39b3c66e25f155900d219da014061fd7bdb5ad9f6b90548529ccdbeb79e9f87a8ff2e0663fa5cdf3199d6d6c06000400000075011c62d333fd85155f9169ee84bbed3697fb74948d8ab1da9d171fed335cc5540200000050403c8527afe27bcecfa7d2037b5414d4c3409b5834ced5c458c42e985edd2336d5d502b8e6cc7f5fd8c8a2d79bab6143c04e26767c15c5e89beb78d54ec2c9553a3d3daeb5f1ed2689c24739e217a7640005000000750157e5d7d2ec191c7967d3b4467eecf42682e8746231282fbea1c2d77d2e081e7f000000502227819a3b418e32223cbde7691d15697aef5d0b0004a0af0539595edd7675f36114b82760c5fdcb18406c197fad0b911fa7f42109f5742bff8544c4907f23c6767b73f9efb93cb0cb295a9a707cc363", + "signature": "b0e28c784411a410a4372ff8bf2c5b426d397ec3043e2d3c009ea0fdabe154661be760c55f4f70afa419fdd73d4a181ec0f6ca06f5bfd29dba51f3a46a1cd10d" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f010100020000038c010003aff927e243c315bee78487fc7d8d5390e55b2922b2158a3bb89d79f8788f534b0159ba0e843cc1ddae1f02976eec2a49062a3f9d1e36401a57334ffa215b2802a846f3c9de9ea206e124ef8423686eb2bd9fa0fcb9cfd3a9c1a9f7fd9b8900d9813282c5a748b530b0b186d7cb845f2307fa196cd33e4d3eb53d47798659bd28c8c4998143bd7dc7e15cc064fe7e248405b3c2ad565f3eea41ef23cb992b179bfb7db020ba24ba1fc38d7c741d955ce4b1acbd4e8faead6912c838b84d6329eeafa2fdbdc0fbcae2c44373562ccd04064a45ef1f73d0a5b7e6ae38d670f9fe520366cef4789a37b1091fab04485226cb05b8d9f944847d2ebc4fff9ad29d6f797dc0754fda12f57b6ad29297731dd37351af59b70116cc5d576a65eb2d1f2946000500010000007501cbc5ce1751bf8319335f87ea0e4da093a697e292ded058fc3ff087d152a7a448000000507c2a68448c1fd275ec6d85594e17c1de1d2b418f5776c3edb504ddd6254642d427de549cc57df30a40f84ecc70f646ab6725f0cc1cec24bdaeccac60ca38969bdece8173fffdc87cdf99aea6da9e8d3300020000007501f49e8209a73828b7e046161820bbb625a943d462339da69de6b3aa091fffd32b00000050056b6a2a421182a099e717809f5e4c7265fb44b241786145d2e57e5f1f8e36f7407daa759e364e3c815f6e5febca64dc4615e2dde1ecc7736988357461b1a190ca596d6bfaa8e87dd7d72c9e2758378300030000007501cd2fdcd6b3d66cb81845881c01051defb5dea499538133570f1b680d05fb4677000000506eaf2643e0680b7d7fb2f5104129a733e91e29a5147dd2df8b648a47a97c2d895b8ed712c31856cfc59ebd4e9c4b402b33ce4605098fa4e3e4fe388ab656073eb4d11664984071d2f9c10c0f7bbcbdfb00040000007501086fba7ffbd717bbb820be0995aa9cb706e1790f9da9e6207f20f35af10da10e0000005094255540e00e5c22d9e863355043d5b7bd6751c34916f8554ba24d5c8cbc4560383ec79fd1b2eab1ee755aa3fe1d858d27e9432e836258dc57f85d6cec96dcce3f6c396cc11ff842666f348d4e06ecc50005000000750163eecb77a7ae765ac035b7ed55ce79bcc8a17aa5cd8ab1039f5da3e1a6c3c5290000005042649a1e299939b620f622a1e3f05e3f9b8b3f7ea494aa38a3836e3ba4490a2c38f0c032506145b3185eb004e22f70e3cf23f7656136fede3740cf840248850f4d06c68b05863e3caffdd4f005aaf61f", + "signature": "edc4bcd8d73bd7ac3a095a6f56aaab455785ac157c76a4f8476915c7ed1c2d166fffd489836ccd65cc671d96549ca1fe331d9349ce703e754523937e52555f0c" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f010100030000038c010003adf725409c961f8690d2d0a9aa02041dcef0493ea913bd78874c0de8e4b2516b4984214ba77904a6c932adf9e9a0bf3901abf4d7c555b177bad273fa57d383cd38c7f34f1806f50d38042fe92bf23d8ec90405efdd3cc01c5a4b7d872f2f8989b25e7fcff21a78dae3b1d26ad3178da80325ff3d01fcfc53dd03bece6b84cf6da0fbc9f42685e4b8fff10ebc60aa732c0a89854bb79faf32cd84433bb5d8ea71fd51b135adde9a624b140ef8f87587299f8420cab99f3b6fdacabfab861d6a0a8535a8acb4c0fffb37134c865f32c4b950bdcf8648dc0e9eb83da8a25bb344712e14470a4637f5d38f1b8d5df72afa0518f2fe724c6e5b254c6ae0524fcc7323de307e27f701ce96ef2cfafe4964093d44647b349a92ac3124f4044886d009e8000500010000007501716c96eab3f9db0314926992c5c6a63c9882d32da8a4286574e9ee187ecfda6700000050df13ec76dd6947c52237e1e52a339b474532ecc587bf6b6b351b40fc31d67c02439666a862cb7d4209a2622932c3d3ed8997b584722fde4216e6188d8481dbc022546807e9087a7e98c28493f5c0f731000200000075014ffb986266457a597a0fb04679deefc57756d448af9ead5005a9da9b9ef509380000005041f769b8ac7c7f47b28d6d365bdcbc3e8291e678484636bf723aacccdefaa2ad6bf4d5346c82948f8890b97b272670d87bd96d1deb7ded0525011073f1018168cec1aee841088d7d33d8a510d7c5b2db00030000007501c3cd422e118cab9f712f9100b1a12b09892a6d5070594fc25b3626935d73e76900000050539fa749cbc6198fb8bfd55d2d1a7a05dd61239a997dc6682d9646af6643c4b19e4cfccc48e917d8676455bc7abd0061867809c71ede745e3e0d46fb477f6dde29160982d4abfa905e0d6326a684982c000400000075015af8f84690b6f5805348c0e4f25eb2cb2bc70b8d02a4392e079ea3f3d02bf22b0000005021301703dfee597ac5b30ae4c464a8591d9a4e0ca9a7ca55ba571a11e289acd359bbc011132fe0edd7867717505a57aa13955b10e00d44daf9a79adf84d097ea8c08f09316754352c9699a8b111e0a2e0005000000750136e7026278a52df83f21173c5b1d8fc2f9ce451a7b8421d524742347c1251c780000005038f8812cad8c0f01655bc954f797da53ec2191cc192a3a4d0a7d45a20c6a309ad47aa9f34cb76d76db5ba55160034c2f942c0dbdc896daffd173fabac9fbba605079e94b8e37340a24e794e2f23e1783", + "signature": "470c8f5811379323c1b265e0094a7d1d2cd367d81c8f11a4959e9e52f566d0dc2ffb7974b7a3131974a206cee3b794538f08c4e3c2cae9ab8c9e267b6246ca0b" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f010100040000038c010003a3ce985b8d89cf802f157a403f18b8d509a422a09f808bf010963be00a2562e1dd9b905e57e136f5c459fa4751e69b4f11568500ce1e55899a91d0bab065b0ec0d8d856eea351dd0f7f935dc7a82b9118c9798a311020817698246b1c13ecd39a11d80885fc3374ad55344735862f98b89d1506fc5be878d757a2a5c58bea74d80dadd0df83eaaab6fcd73a596b3a8d60c3096187a0275d830fa4ec0de5cf17e095ca2998bf73ca2252f54a60e5cfe3564168237bac470f328dfe6982e932446ab72a1353ffa80eaf10691bd4c195efe0a60d0934ac512fa4f63976728d57710577cd9e8fa99fa374f938194364910e516e64fd151c19ad0731cdd4f601ed33597ce0004097feb8c7f9781ae8294b4a492d3d9a37abf95ed7f486b92a9ee631700050001000000750114733b9dd0ab699176c63cfdd8177cd55302b82316aa400ac3a31cc9b27165000000005047e1a16588cbec0f125922665564b155dac9c5067eed2ee8b31c44bd9a7b5274d49c8af05e9e580d4cc860ca5eee16401967b02e07e842633fa7fa0d28dca7109f7f20c1c0dcab35222b78e82e827c98000200000075019ce795e79825be1eb564fa8cbca7a475346f7485c01937dbca6e520cf161ab5300000050b1aa0ecb152ae2d238956199e2c7fe73ea2e5a7614e13c213b6343e674b4a07a2af28b3efd921ee888101d311b3058d6aabb6663fa18b9b466ef4d1c2b9763c21937740e95026b4ac73af9afa534a43f00030000007501fa2a1d9ff30951c97bf9623c7440b79ebff7882f59cf961c2a096ec308adad33000000504c114a04efcd5189a61a8190bca2a9305d341874dc588b95841fc5a09e1753bdc057503db2b93da55a3ada335535823186d469061fa53ba30931b76b77afbdf9ffbcf94b54bf65eb455b88623792b07a0004000000750187d026361a2591afb90312193c8356e8b1eee2d557dc0655ff4ca1c140d7fc4c0000005022c2bc83f3d967093861649eb656b70dcf323b080e98b078098170464c7753a11550370b2ead468b75c86d30352c508b56709baae850990d7b9a39da4a8fc2d84e491b2bd9545ecae8489654f020798d000500000075010d2396399273baf4132c689761f9f98318d8240955ef15a68a9401df6ff0fd2900000050f1d4d32ca3f2ddf603905c41498b9834e87df9a749ffa6eded423c679e5e55cb38ef71eb438ee01f366eb04987a719db867169c9b25c2ade907aaa9ee174b19e5eb17c677434bd9926c412f77c293907", + "signature": "c443871f4bf12fca7c46937f883813f0aaa58e8e14fa08910ecd82fdf948c255712c2f64c1e31f1b789baa830deed23a7cefd5b85a190adf6d259f9257d3a305" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f010100050000038c0100038bd553a6f20bff7d9d0e75616661ebeec3baa711b87824441deb02d0c4515e0e36d7b4a8b2dc91cf3a597a088691001a03483ed7d52041e822940cf5f7d7d610b715e19048bf1bde5002451a85c236e31ba2327891c36d2d72ecd30b2b1cd0afa2a37d5cd881891965b7c4e36d173d9778c1da05484d7461a547ebd126ef319bc6dc47cf4175216f519eb8c70ff2b1e8106a8013e214b43f41056a61b95645344c151e715a5e35057f8e47ae74ca2883199a03b149b91c18b8e0acd2f035c078ae586dd9676220ff62854ab99cd44e830a21bf6c283cfca25beb9379610772086a2427029ec345aed052319441b52f71045a6a58abca836615e6be6ac4a3c6177015fc6be57ccc0159a7b5d8147e6e9f80c0b16abf0be706deca66a7f2a9cd0b000500010000007501c56d7f393ec168ba888d0d1d0e938f55aab8a343bca228c4d3a52c0896468105000000504e475bce0499ad2ddf867dda032d7e61459eef70c96f42e1dfe5c7939e337e4810a2a6bf10dc1d325852d18280544ebf2b8785b8448a93cb611437ec1394eb2ca26ea01c0bddb47ffb259529c0b8c9d4000200000075010858f0e4da21af5e56b36edfbbf86ae3ceaa4a0bae5b9ab014c1219f376587620000005018945e72d69b3d4e325a46bba8d3bb58077a08a41d67052bbd3ff9e6b85abc35bcec04a8d41974e516311f3354841891c6aae43f8bbcdc93d0c12edd98d4622b6ba07f5341ee1ad4530209f9c653d7f2000300000075016c2e48fac5042f19d9d15599d6edeb9350bf144f3117ed652ecb5613c3f7a4340000005060752c641a802f3ddf4604256ad45a68596cf83c31a59b8bce0e38c533f17abaec3b489b53b0f06c53d1f0d9d1109bef0f9a65704dc13f85ea2efdafd561bb28cceca36a815aff9987664df0e0bfe651000400000075011ba6e17f2522ee644f3c88fc7fbd61e36037910662ef1f9eda4a3a04eb0e9326000000507bcd31d5ea141d904d212bda6b56c89da0cf3a33078c9ef8852d0deb942565e978bbaa5b6f8995c9a59c624bfd09b9391f49117dc28fe98f0c55a56cbbf63ae062ad7a1a410d0a711d3c2ef518aa6d11000500000075011ce7fbd024ee698d39003d70decc4b2f868bb86f67bcea5dd88be6d7cc9aee2500000050a4b3889cb676f2d8c7e3ed5adc50e1da6a5c4699078371c291e2a0e12799cfea0be10ee86cd469892f66f99661db057c29579c15490603f801052daadfb5b286d9848156f549c2c854ebc12bd006783e", + "signature": "340460b7077316f3739a12172d441532ebcba9559e4cb92fd68bf872e5fe0a6f2bd3ca594e81f1918f06126935cf4b54b7dc59460145defe578bdf4ebe94bd06" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f010100040000038c010003a3ce985b8d89cf802f157a403f18b8d509a422a09f808bf010963be00a2562e1dd9b905e57e136f5c459fa4751e69b4f11568500ce1e55899a91d0bab065b0ec0d8d856eea351dd0f7f935dc7a82b9118c9798a311020817698246b1c13ecd39a11d80885fc3374ad55344735862f98b89d1506fc5be878d757a2a5c58bea74d80dadd0df83eaaab6fcd73a596b3a8d60c3096187a0275d830fa4ec0de5cf17e095ca2998bf73ca2252f54a60e5cfe3564168237bac470f328dfe6982e932446ab72a1353ffa80eaf10691bd4c195efe0a60d0934ac512fa4f63976728d57710577cd9e8fa99fa374f938194364910e516e64fd151c19ad0731cdd4f601ed33597ce0004097feb8c7f9781ae8294b4a492d3d9a37abf95ed7f486b92a9ee631700050001000000750114733b9dd0ab699176c63cfdd8177cd55302b82316aa400ac3a31cc9b27165000000005047e1a16588cbec0f125922665564b155dac9c5067eed2ee8b31c44bd9a7b5274d49c8af05e9e580d4cc860ca5eee16401967b02e07e842633fa7fa0d28dca7109f7f20c1c0dcab35222b78e82e827c98000200000075019ce795e79825be1eb564fa8cbca7a475346f7485c01937dbca6e520cf161ab5300000050b1aa0ecb152ae2d238956199e2c7fe73ea2e5a7614e13c213b6343e674b4a07a2af28b3efd921ee888101d311b3058d6aabb6663fa18b9b466ef4d1c2b9763c21937740e95026b4ac73af9afa534a43f000300000075014543fd3003e651685e66f3aae30f6b67beafd108381afd953384961a7ed6b5460000005064214d034ca6f0cb3aca756298ecdb87509da2a3c7363c44a72dbdbc4402f1cdd42b98a6bdf8a96ddc8a673477edffdbaf4f62a631241c74bb68edccd217a2b61b7abf1336cb717e4f39e848b2c7dca60004000000750187d026361a2591afb90312193c8356e8b1eee2d557dc0655ff4ca1c140d7fc4c0000005022c2bc83f3d967093861649eb656b70dcf323b080e98b078098170464c7753a11550370b2ead468b75c86d30352c508b56709baae850990d7b9a39da4a8fc2d84e491b2bd9545ecae8489654f020798d000500000075010d2396399273baf4132c689761f9f98318d8240955ef15a68a9401df6ff0fd2900000050f1d4d32ca3f2ddf603905c41498b9834e87df9a749ffa6eded423c679e5e55cb38ef71eb438ee01f366eb04987a719db867169c9b25c2ade907aaa9ee174b19e5eb17c677434bd9926c412f77c293907", + "signature": "6d5f9785192037e868856e9de04f19b651acab2888f5cb00238541e4c6d95f321d8bc31e00777cd6f52ebc26e9a60894bcaded9023b72e0960c5f8404eb35506" + } + ] + }, + { + "closure": { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f025b355322cee77b95899b5935b15164b82defbc68975aec6156f9f633d9225d23267882b2a396b546e0015423f7c62e0a1d9dd44ca69729b569d3f28d3708ae7d", + "signature": "da4768f8e65b2ac73429d936adeda5c5ba78ed8df61244e163b02e7aba77ef92cf85d33f02bdc0b50eb8bd3ad6c45d5879bd3d08c5c9b02f66dcd74f9e7a7d0d" + }, + "messages": [ + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f02010001000000050100010005", + "signature": "d516062f0c32c80d1dadce7396489d440a6643e517e313dcece6236656ee64a6f3e73b236b9d17ce510d2d31f70895144eb2308e4d30cfb889dcfcaa5924e800" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f02010002000000050100010001", + "signature": "4ca6ff6a374cc46324ae67da0f2435b72175f6934405a755cec699aa2e513d983178e0792fe73e1a888f8568a3b2434ad6c540902a9dac94a558879cdbe6e003" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0201000300000003010000", + "signature": "e3efc6f472514ed59a3a9ddfbdc13a2bdb6d43feb073f8520bfdc11a4b0512f2be14c58464737174378353fa35b130af484f7cc838334d241d4e13f3fb6bd005" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0201000400000003010000", + "signature": "c8cadd4c45cd6ecfe9cd743a1a1f23c49deacd90da6b21ca38cb0483fc7cac9469ff08575cede3adc98bdcd67e77680c75252b7894f3ae4a37157eb17ec8f105" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0201000500000003010000", + "signature": "68fba815d7629f135a507bc83a567ef6c8406e052554940217c418602a118c196d2d7a66579d96b2ea4435967e4264b10741ce0f835ef8c9e07908bd6b8acf0c" + } + ] + }, + { + "closure": { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f03c930813bfb6d40f67ce70a08f19b7976de96d935b156e66459f816bbdd35bca44d1402bfa1eb395adea06a33da96f0fa7fda5ea68ad6807ec9d23f8ae6f0b7f2", + "signature": "a3234083cbc1ba87645a56d7e257a03cd087370385c3c384b9c59286cc9e46c3806b81c395694fea635afccd33f3d39249c5650a77cbf49b24f8fa5a0f480a0e" + }, + "messages": [ + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f03010001000000450100010002233ab7d8a5e649d0cbd24bc811d2a0f8c3c4e73755bc6b9b5fb29f395679f01b3e24504df4987f4fc75e70902b5eff3354cfa56c7cd15f56410ce5dbe74b8b45", + "signature": "cd64cf620c8e0faeb368b43ed90912a370def98e6fa5fe7eb478c91b3e6fa552f68494ed66d49d9e362237d68e822c84a08bedfbf9ee7effeee9c8e2e354a800" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0301000200000003010000", + "signature": "f723c9938763f50da06b5c60066804aadb99320ebb2796e706e83520fe39670a9f5145ea057cdd52040a4111ee301e7f8650c148dd85056d6c633333b61c2e02" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0301000300000003010000", + "signature": "782ce81943895bb8d2f538c88e61c06f81180da658ba5306830bbefb70c09a43bf46dce1220785cacbb9779ef23f9410204cae67d35808d165118c2e0c721002" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0301000400000003010000", + "signature": "9882f3c171ed3aca9be71272d67b5f3ca49468e5b641678ffa34763bae3bada0e2ea6ee55424a3aff4e3892dcafc2e5bc5c4335e2fc57e98137b08f3a647450a" + } + ] + }, + { + "closure": { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f04c8988da1f3505a7cc808b39f89863979cc29c40b9c59790e52a0f04211eb965795c4737c42bbb9741c796dcffcb271a0b2f691e88b998b0f2aad7b2f5c3d58d1", + "signature": "1d23f71ac254b26c698604dc4e7da5b38ac9067df525793f6a24b827f467d318c36529519020bbc1cd8adb8ba51608621b8e764f4fedf15b65b220ce76ef1c0d" + }, + "messages": [ + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0401000100000123010003b0167276e94aa096d34876cdecb1783a3d4a9fc91b69142840f34dc0f005b8c0d6265c51c088281206621ac7d8b7743d0046f3a10f904732e5df9d1b18b2c0791f0d8686a6439bcd26f79b7c55f1f4b71eb183e449a54abc50ea12ce44c5410190eee8d35c9fc754bfb129d5cdd463f787df27ef8f8ff8c4e06b2245b2d91e5cd961137ba526ffd9948fb41ec7ce98a3174fb3e8fbf49cec998b96718020e36def493f0ffe0c62a1cb30c53e0ec621201a884e1e418f529adb808fa92dd4d8b58b718c10e1a85c4f4edb41cfe66822873c1c80f450766de451a10d2b38e314cb8bcf98eb0e9b5ece7a55888d8fc35c71185966a15d5ae9e0b1227dccf69bd7ef31af109708459ad0f5e6b6c99ce7a443526205ede5e39436c594f7cc507d748e", + "signature": "0a2220dd2321063104a38626648c4865477a472ea9ccca2871a6a813380cdc161cfb3e5dda481ae93a69302111aea7fe9dd3706d6a3763bb643fcf9d3bb31406" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0401000200000123010003a27fef503fd01ab056a98eee44d1c68903f249fe6d95cdacd6034ecd543c208d2b791efadb1277ece96c7573c6fef38e00d269c484d593a8a03a035194863ddcd7a7a2ec1574e85f4453b8749ac1781f047d6c3bb7d4fdff190c26b8ad87cdd8b2c72a0a1cf38015329d84aea7c29fbfc8d6080f34048fba84c41beb6d2cdc26f04f6aad587a502099666dac0a2cfefe0d243808b9d8862cdb987d82bca9ec80247fef17156dad53eab6019378c603593970846dd8d401d4fc492fb26bc269a68ac1aaaa2e23cc55080125ef4d33c3fe3fd63d3b85996e3f13171f47cb00e2c0f9ac347376f50f8282167bf64ebad8b91389f5316e8474f1997a4652ed72efa02d0252a56df6b929024042b725f66224f4cf1aee0c45506d6f48c1aa420bf55b", + "signature": "74b4dfe2984d04fa60f44c35921ed98d18d778e1df246661ee3931182ce591c75cfe5edb85963db492731099eb7e1c1fb45c34ef9af2cd332e8adcc0617a3f0a" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0401000300000123010003ad1acdfbb5cd2b6cc60fc639cc8f722419c7ef068e18f6bb163301cbcc8ace32d4aacdc7d21f12a75b2c0f0224e1b4c40afc3f29869874340d80c009e1b708e945a63bc3939a96c9f563a9f3f81f288b87c1a2bf7b9bea0cad212d563a5869279920b0ebd7645bf3e7dc9a16c13457d90f6b195d563016126e51ef297f52b30906b61dab2f1ff7a8ab023faa02554dac043c36bd03018ad043b0fc92b4fb6792751c31ef2694569f71c1fec8db6ec20d775f9e47cd1b01cb0d118e8c6676af7eadc30b94535a79e1b3f0f05e428c9bdc1e6e54eea52bba97ccb9d9cc9a964b766b8c34ba9d15715f0d19970812415d8413607349c2f4b37b6bb310d27c82129545bbd0cdb80359a0677447299f3c711aede745cd3ea83a1064ab542ae0307c90", + "signature": "6f50c471bb0a34bbe51b9e6356f6bcd8364bd9c065306a314995966494b670f33081279288ef74b11c09c6e5f88e9b0afca2198c6c9e92635c7a05763697b70b" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f04010004000001230100038635cbc8ffc41cc7c20bebae9c067414e1a1a85deb5ecd74e9184f7faa6d47fcdf447d842259da3b6b6f60a4f80d2cef0d775ec08808ea302669c114adc4ea945f9fc942810d26777b0a845d75c0e1cc3b859d3e7839e0caf91d86e7dbe6446b8148da42a74e7d41d87fca3041a21a38d9d5b4bfd274d6d91038ad30953afdc559f37cc004c4d3b26cd0d4c238319ebe0dcbb9fd1bb3fefeba658f4e2943a6fc57d3a957d74baf8788d619f0ec878fa59ff3b3d4563ca8ccc00805f5626e79f98b8faa7eae3efde894cb9119ed8cf3eb879bc22a764bb5b29764fe27e2c0bc41418871900e2ab0e1080469b6637c28a51755f02a4b1cac63dd5df772c43a8bd3c82ff7c7c97468eda54ec3438756e25e0c561a1c83366218e49ee5f4b7cd06d8", + "signature": "f62f643ba003c29727f5085dbf5546933cf6c44a6afd83fff04484bee7c368bf6ed984a7a5c3ea64a12806c1626416dbe39428f5c66c6fbdaec94d35e1e28d07" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0401000500000123010003b29634a594d14f3a9fe340c2fa7c14432a649e907cd0ac3844944b7aa08b0003529bd76e01eaa38578d3282478487fdb0d175ac61cd10257a7148917224d0a33a69ab54e1de33b96d5ca20c8f9cc393a7c11e7ef7b2777524db93d2292533a19ad431e79989f5c16e9cd2d4bb16847f9ab40b569fb0778c0c1ccbcdcfa66a3b21b5dbecb9340425d2589c340d3d5a76914041762bee3a2cc47a2ba90086ba5f00c96d362cc29e713bc7e216da10410e4421dec9211be6f5b7c82e6af1c64075a886a8621898ab6def078e51de6134b7bffcc635e0eab03f96cd28e01e662b082f1d761af913d4eebd1eb98270f985abc12da1aec13bab63e23f4da123134c715ac28f8db459d61ad20c9ab75b7565983382f9bbe20bb469f9c8aeb6c943c2e0e", + "signature": "543a667f58641e766ba8058becceeab2499b5c1dbae114af3d340402eda23f0c42a814cac199a8cfde082fccadbb91676887eed04ea1fa2c332f2ec37bfca70d" + } + ] + }, + { + "closure": { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f059ad07f77d78ea3175e18d4aa73b8bb88cbd8a4a64aeaf27dca4c6948b1ef36bf5f36f271dc2d4be8a910394c1bb5ddcaa85adf9eb936690e22702e7ddeeb814e", + "signature": "a911a83d45f1dceec40b87f6f0a14c293bde4447c8850db6121507157085df876f0376f84161458518759ce2a341a60e421264d514e8171572c9e2e04ce6bd0b" + }, + "messages": [ + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0501000100000003010000", + "signature": "f2c60fd60cf51286c8eb690bf5d54fd523e0e1500cf0db8ae2c3ee73039e6ff7dc104d302ad848430dd1c190eb59f5e0bdb38e227c39b9ffbd5f36bfc875c007" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0501000200000003010000", + "signature": "026a6182612b8ebf357196fe37cd6afe34ace83af7a110588ef3ca9b73874d24aacd89b7a41661e051fa0ac8261922dc30aa0e46fd7c03fbf7c42b86ea5bec0d" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0501000300000003010000", + "signature": "cb17551016ad6ff3f8c8ec555155be34107a7dce71bc54ad8f5e14db82498cac361ca5a6d5e6d43328ee61a1d548ec13390b1809f0bcba8ab58524425553d600" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0501000400000003010000", + "signature": "004594d3a5c0361924490b6c965553a2601c7c3fa02656e040ac0b4efb69f1c6cc44b459fc37b7417ef6609d9f34bb2680e329cbfd64f95501eff00f48c61701" + }, + { + "body": "01990e9b7976bfb2eafc5d9e7b836e4357d1f2c87df458369c9be47f6a6fc9838f0501000500000003010000", + "signature": "b8356d196655497dd9ed4b2cf757556cc0d3e0c88131182f1383e7bd4aba3ea59b1a11ddf998b3aa6feee2edf9d38c7e4b7f0be63469b0b8447bd3b67bfbbc01" + } + ] + } + ], + "threshold": 3 + }, + "name": "genesis_3of5_answers_and_disqualification", + "operation": "dkg_verify_transcript", + "outputs": { + "commitments": [ + "ad1c70db3dd7721a305956fd7fa8cdf8b4ee47c58ee148d8b5d8707a8d7bdc385834db885b4187ac08098bc1b23947380373b0fad351f47522061cf83c896a4a909a1e0902e00dade2da92fa00058dfca753c5ecec34042e0ee906e221390020", + "804bcbb4fa645da7db40ac6b3590a4426b7b00fd1281daf1890b4a4cf8a787cad996734186729c8beaa04eb89f31cbdc0801fd5c3e0c2d51acc64b263abee54f7d3a1a8a56e4254b5da3ffa3e29092cab2a396153f09da3a237ed6a2579b090b", + "b2bc9d89761e137547d6b2d0dbacbf12c1698cdc018b277e919934d29b4f28c415058148d7dd40b4e75b4779ae41947c15e0bc9ee04a891227eb4823ebf38d3ac001b84e5172108b5f1e1922e91df2f56f8dfadf88e911b0aeebfd6fc9ee8bc6" + ], + "disqualified": [ + { + "member": 4, + "reason": "Equivocated", + "round": 1 + }, + { + "member": 5, + "reason": "MissingAnswer" + } + ], + "mpk": "ad1c70db3dd7721a305956fd7fa8cdf8b4ee47c58ee148d8b5d8707a8d7bdc385834db885b4187ac08098bc1b23947380373b0fad351f47522061cf83c896a4a909a1e0902e00dade2da92fa00058dfca753c5ecec34042e0ee906e221390020", + "public_shares": [ + { + "member": 1, + "point": "a2aa686881d2ee061bec22cfe47e9d82b79f2782e3eab2ab480a2af790a639bcaef48262c67c4cb64a46eae720edaf6d0af835a84a30be8dda054ee69521326522752ab4b063ad292f13393386d9ea07077a40af6a85561507a7402bad5522f7" + }, + { + "member": 2, + "point": "b3ebf5da32173bcbef5eefc6a68257803d38615129ccd35196ebb7e8521a4aa2e3d7ce969afacc8ce0483fe7e8f02ee908729774c1a5f5d3440715be0ad3ccb0b7a786a67998bffe7fb74bd46c52b875ec4aa6e32a0b01784bec9ceed8c2b415" + }, + { + "member": 3, + "point": "92730453472319cc2186ef21161050d0726bf965be1c882daabb3a3ddfdd69983321a4a44d9ed823b8fb000aac059f9f003bb044cc7c9e37529272490b524fee41279dd0057e47b6ee0cda8cf02afe71b782b9de031b6740cef467d2536aeacd" + }, + { + "member": 4, + "point": "8fdd7c553a3ca1f48bd118cf69d5ae084d22fc15997278d8728d0a8a528566b6dd28388fc40fd12c6499ac071caf84fd0909c79c801bd47bd137171b5d513bb4305277cfd46f37f003c66e9575aa2bf6e07aff38c894cf9b2df3228afd41ef92" + }, + { + "member": 5, + "point": "b99537ef9bad53e5b84b9b6bf3b1985fa13f98965afeb527b956c9db0e55e2a0ad20ce7271c947d6642faa486c892b33127f03792d8060f73cd50eaa64e80fdb281463d276ea5c76effb62cdf1bb2daad5725588af673a288677bdc836eabf01" + } + ], + "qual": [ + 1, + 2, + 3 + ], + "reconstructed": [], + "transcript_hash": "6ac54abc7f9b37ab336655f7d51235385c46759e3b1d870962ae5a581da2e5b6" + } + }, + { + "description": "Only member 1 sends R6 for faulted dealer 3.", + "error": "InsufficientRecovery", + "inputs": { + "ceremony": { + "body": "010165656565656565656565656565656565656565656565656565656565656565650000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d3000000200030000000000000000000000000000000000030001000000186469643a6b65793a7463323232322d666978747572652d310c921bb2dbf9625d307b45a3e800e74c8cefaa2aaea59768b0a61b5dc34534370002000000186469643a6b65793a7463323232322d666978747572652d32ac435ad03587a0504b1f9ccd79f73f0f62697061ec6fb85d8624a0bb3fd906580003000000186469643a6b65793a7463323232322d666978747572652d331d63300c37aff03b791c0860100f7ccccafa0f3f37b0ea4a6517eedae5b1dd10", + "signature": "57a016b3e7ccb688954c0f7379a4c41285cc717d8cc72bb14950d6ca0e16bb410034de94818c30ead4a15698f028c289b440a05a00fe8a650e262d4c6e11150d" + }, + "ceremony_digest": "1c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d41415", + "initial_transcript": "48084f9935d10b2f5ee1b9b73ab2587160a62cce255db36cd2a17a08dbbb5ef8", + "members": 3, + "rounds": [ + { + "closure": { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d4141501c6f30b1bf560381ef2b38bb897fab7edcc7b05703a1f7013d2f080e62296cf4348084f9935d10b2f5ee1b9b73ab2587160a62cce255db36cd2a17a08dbbb5ef8", + "signature": "64e474d84973c73f79bf8b8ac1d846b6afd510e9729d7548b21fab3522a4e02851b11fb17e99307f6e89d22427db2ce4af8f0882d5d621b9805259db0b340c0f" + }, + "messages": [ + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d4141501010001000002360100028b7f73e22c610236c1c4a7fd83017ed5aae69034b3a6098108226987f02f4f992add7b661a87c801ada6411e5e75213214ec30f3ff8dbc403b834924c76b2eea3a2956d310b8b520bf12190a067dd18aeb36c6f885438ff57ed9aad7c2ef13d69638be7ae9c0ff03c6f99a20ef89838ee8f7f04f69c74098e0111143da26cded88d815361dbcab9604f925268485dd6211b1ab1caa158c074900c79b54609d0173a26270a0720d67ffa9fc1cca4388a65a0edd54ec0d3580ad10c850a1ad025800030001000000750139897e5abaa26e60e18ee4c1600fc57b4a57899645a28672a41ca8fbb06b6c220000005096087fab26730e82e18f0386b134559dd0493d28959ae91873b10c53ae20373b746d8ccb6a58c23006d2f5d62d8974cef126a15f0e629dcb7e5316db4f08cb211b591b2dc6e7067c6b0dfa2e806b40620002000000750104e14c60bcb8b9487002851de8a0c215f392bcc7489134f14b827425ce83e566000000502c040403243be459a93fc0f6d1a8e1b8ccb122a1c9c732ead1afb84f5f402239277d50c0c3c3b82feab23190e7531b6017598864c6cc49a74d5a8090ffc8910d8969542e9b403f8f21c24dcf6bcfa6a800030000007501d99ae42421eb3e4f50ed658dedf9aaca89def359036de63c400c0297af42721a00000050e0fca38ab63f8955b1c1dfebb5d1d50431515316a27e03dc8c69b1cb686eba08dd32855bf513709bdeb495308fddd785637d7f6d0c7f7eebd61c0fc661f3873dfaaa2ee3502abb783b5de4c44c6e872e", + "signature": "7d74cdbc59d9e1a9f6895c6eed0806f90b2c950649712eeb895860e11cfd82f9a71a23319c19a79c48ea78a432e0175892411b46c5d73e998f33c69eea4f800f" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d41415010100020000023601000283fc1d5ec5946f8273423656c2c8fbed5317a258e284d9e70f4ca4dbf2b722665156037d0baa9a3ea344d2a577f8d9410ba209338dfaf6f80c12615fe24116b6bb1f078dd3e0052cdda9283495353d8738b4fdf0807f12ead94dd78276bca80bb2df4da488eef77a2138126dbf6f6e4b8082ae922bfe4c5a9f1e90d0461e4bc09f82f5d49277eff4d408826a1f6cd0fc0c0e21abd82a9a0ba73e91cac98b6c7682e567dc927e6c6ceba88d89235252ec790afe7bf9d24cbfe699137f90918fdf00030001000000750141416d53372425a41e2603910678172b13d23fd9839cdb725c2c91f06285b7780000005003304b20cc47fe9e3c5cd3bc2958e2089766093480bb791e7df5a66e3ab9a219ff17742a1183db8e54dd129da25c66719ec3ffb567bb8d1bed44f8518bff7307504a3a84117009c13c74ced995a4b1e700020000007501443589d6bc64ccf6d5f066419755b0f595402ef0887192da0c22a51e4c57896d000000500d0888eb5e6b17e014fcda41f53d5f186de18730e4784540748e58f49808cd0f5093ac6b3985b6e2fb67c2dc5ddfc2a2f43d2823b349aaee862a3a74f9a4cd336be65c4bc7a573a68147f102dc30799d00030000007501dcc002d670e2a62ccb1db3612f0f95495f387f436b41ce37300e2c978e847f2b00000050237df70b42cd3cc630f8dbdd49f6be7ca4094f82724a1a8cce6e3c2800213596dbc1019abc943876f0105e9a20987697efe8fd5e8c8a9fd9c10ec5a7405844cec8d066add80dc6e116aa94d0a167c4e8", + "signature": "285c3b2325b28df3a804f96b62a45188b8fa481ec4eee33cae35e5f783ee65be763bd3c6606950df7adc242ba4434c3b39b27f3d3189c0065da49cb4a7d35d0d" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d41415010100030000023601000284309c9bddb323d9fa49bc88bc0a32cd4280cb0a1386fba2305a425d027a2a316e6865c28199dcbdd939a1bddfd3ffd4042efad86de9094b2259da8618f1f8c2ff1379948a6472e5c6b56f1594db9db8cb734448cdebbf4c2c0e3b0a5a787dc882e0ff6e5ab3187476647e16ab9e8f90cce144915f1889f19f14b0e2e4e1ac3b310cf780513fb83da2f2f18a3328efc71935468421fa1c54abfe46f7c958375177dc0b69cc37327b4a65d0a3b64a8cf83c95bbb584415e4c036ac9f6a7644279000300010000007501b6ac73b1f13c90789bc795579a5686335058f93da3cbc04831f76233f21c331800000050f658efeae776778d124271f3b76f6385fa2ee14d50bd3b9d667d6480f1c16987c8ccc982514e0ef5fb533f997a781e1a7da72bdcebf776d7a360d5805452b75f9df12452ac4eebb015c8accef01f985d000200000075015d54b20fc06904ca33a421e2131dff377ef4ef7d7f47c0322c202fec95a2d04300000050b0c684282af147c296685f1da0e46f0a0dad5686a9911dc815f2125954c545ab49472a9881b4a96c41177936f923c29c2442053ea406b2ff25e74386557e325e9e57cbdc760508805492e2b0a6782e2a00030000007501b3d53d386a204a5a4d39212720134217c73086e46bedb537da405c33bf70165800000050f003af692661cf5cb05c3bb99d2cb0eab7b57f8d37bcdd74420912da883aa8a163b3d3f9d052ded8f29046c31365751ffc1be64e65bb8ed8c5b5aadcdcf7aff91cb70878624a5a1d795f76b89f9b6408", + "signature": "5db283ad1ab1179bb92df2240b261d145275c074e9bf15490d7d5e1e023c50c690ed08c432d8a805d8f2090647c004dba53aff755ae0c5b8192d6ee92ce3510b" + } + ] + }, + { + "closure": { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d41415021de7ffe50abbf1981e1648b6f6cbb0a1e00cdfd430d51c1a43855a27d8a1441fa9cce62d5f0bb9a6af136739b8fb1692760fceb18d760af4be18663dd69301f2", + "signature": "bb9a3edb1427f77488df29257b7f7ece104cb53074ce3e59c5ba45db37a8d5b693143863b911b183c5e4eb65cb3a83bcb51518cd62e86cd8497dcb2fa0ca6008" + }, + "messages": [ + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d414150201000100000003010000", + "signature": "485ed633633a904fed7de9b58f44ab1f29495f9cf9b81161fef98f8b8089b74335354becaa78cb9103d294e4f3a7d8f4619fbebe2859879a9c1306a63ee1e005" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d414150201000200000003010000", + "signature": "f9ce768fbbe191fd3dc1548ce1bb330951cf9d6a365eeba1269db57162ed3f17aeb03f306df9849a82f2531b3f63855a8fae64677beae495f26667b8d29bfa0e" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d414150201000300000003010000", + "signature": "9ae683ae562d07328508128bcf70034e0207b0c51395b3a5d17cb7cae372555a37def9e7d3c8c04bb8a06c54e250e33194389445783e15c91d5fe260da92b60e" + } + ] + }, + { + "closure": { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d41415032d55bfaf92c87e93f6831adcbc26a612caf27b9dc48f0d2d35dbe2337886ffb2ed163db3ddf951122d8b558e7d4385a43230b74974f429c8469d151709cb477d", + "signature": "b9cb47913217740aee96a4a82dd77ecaa7143acb1b0ac9e9918bc019a8de9aeb853f60be189e199f616379abeadf3df9247e82e55102fee2d5e823defe48d003" + }, + "messages": [ + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d414150301000100000003010000", + "signature": "1a3344ad7ae5a1e0d81a0bc207f0fc8df226961a2c86e197ac4ed7619d7205addcb278ccb4785112d7ec595f174e987196b937c308cb4d2d510ec2ebedfe830c" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d414150301000200000003010000", + "signature": "23f3be1a5a9ea57dd0bfa7a848d0f3e118e0ccdf50a1cd4c09c7046e548a9f8f71dd4aceffc51c8c5dd7119f765656ba62240656994445e9e94cd4a30e0a6005" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d414150301000300000003010000", + "signature": "a2722c105706e1e506590a4cbb68d033ac1f3dca3ed1a6a1e461cd22ff4ef5ed703677a8649d0101e3d82d87b7b21eccc17e8defc99884ca131d453f49421c0f" + } + ] + }, + { + "closure": { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d4141504f92a59d5d08d421e9663832098f4ea9aae23919787066ccfe2a2a1c5124bc833a1bf55577047cd94e6e4cefd3179cd55c2eba44328d33d1b7565f2191a24141a", + "signature": "6832e6163d1c3d8ac2072165d3527514d789378adbaadd75cc8ab385dfb29d80c33a34c089e3afd7999bd0d1484f95b841695adc41bc820ba283d9c6acfb0302" + }, + "messages": [ + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d4141504010001000000c3010002a4b9c509d40eba7621b374897546f07b7e6bba9741e6eb6749dcb111cd303f350266de4bcfce1c5490590341d882ba6b08be1376dd4db14000810ce7bccf9283b7471eb2160ec01dcc1f7352101872926af2a38ce1aa729123ddfe64bbf077f78f27e4908459adc651e44805fa694d4dd9e6e2b3a3fcd85905011ba99fff1e468e12aab05dda627515a19d9e8e9659b804334bf2f8a5125667e04a39a3e61dda8eebea0997c826e49ccdc333f5917d662145b3095ba98b60e3443264c5ddb95c", + "signature": "28609df98bcdb31fef280a7a8258c45d342d64aa785ce46fa3be1e7123cc43547e3dcb36e444e3ec898840b2ee7bc114419d80a83492f880a2ba81a8640e4d00" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d4141504010002000000c3010002b0461f1a708cce2431475429ad5c0719f075fcd262f84c53f84c3b61941ce1b19e56b0688d6cc4b27de5f16a5474175412d2874e1b7a50d3a54306fcdc5d55cd6719c801b65317643c75b8f9c25543146f5bda4f4d869b250d1353abcfa614dd81cfe98d0e724f70f8e3b86d5dec063b120c4c150b3d878e873a38dcfd06a95e139db75995881a242814356c24e3948908a9ffc41648c6227a5bf549f3f339fe7329621d9a4ca0397b5589f860da196568252e0762445c31687159e44c948fff", + "signature": "3af233dacc0faa24a1aebf82017bf53cfa1d68b35de7d5101603ed8526db61ccefa3f67e718f78207dee659ba14aa5a55dbf7c5921481805e1a992ff5e6f3a02" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d4141504010003000000c3010002864401f8d8f39df9f4e06963beb81b75796f4c6e26a4c3233f27df0883ea69d0105345545db9907934e3decb563349f1043bb2686fc9466f010db7edc83afff5c80a7ca9e82cdd08f7db1232a705a9232ba3624d29fdc7e74bc235d24c7f8a8c828c4840fd1f4653eae38fbad5cca2ab3b60cbeb47753aeb44a4d5437f49a016986edf6859b2e02b991d6d582bd6c5630b7f1ed38202952dbadf14890650226e65044a33d6d57a0e10b090acd2230004c38e9c7e8257b49f28bd2214e79fc031", + "signature": "c3b1dc8e6971708ac73b78eac1d987d926320a744fb1adcd42bcdb6af4578fa36cc1eb4062a893037c716919d96376e69677e24c7d8c891b4f9dc7a01f8a150d" + } + ] + }, + { + "closure": { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d414150575f88a756daabb4c5002d729657b11b25d2377a380d70faf26487aa60ac710a04b7f6777042f095bb868428f4ea091b016d22046be535bf77ccff152aedde054", + "signature": "dd0991df7d5baf1ef058dcae5d06c7091ee43fb820090fa084efb35d9a21037aa76f27a8eaf00e4de16ca2f23ceea2e12b91fd6d95711c30156838d6ccf10c0b" + }, + "messages": [ + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d41415050100010000004501000100033fe8fca9d79ef29d8d8b3e204d1d18e3765d8a7ce178e2187873c47bdd65a6ae734627d98186db20d3136ae4c634db8e3f2b6e03c7c8e6d3c7cd046dd7acea27", + "signature": "cb31797456217d53ebcb44cb27ee96aa1e375c85dfd6a0311934e23ca5b371738c34e151e581e3b51cf536dbe715c9b9ad0e6a63f2586207faa1d3c6d559a203" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d41415050100020000004501000100037369ac3a7e7c1d2d17ab1a7930b4461f4cf8749b50c619c5564d14e530e5b05507120515425fbd61085261d1b447a48ff697a8153550916774a29cf82cdf0e78", + "signature": "17ee673c88aabdeadc934138a18db583b8769137dcd49d7d6869c5d19ed6757e5c91ca199cd165c6465247f2407761e8bb2d5d9ae083b2c6e9a77390bb7dc302" + }, + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d414150501000300000003010000", + "signature": "a1eb1d15a47433403ea49879fcfbc0b887c87ce2f107a3c8a70caf0a749791705c2adb907bb2c533af6ff0781494deaa0e6b091fc97b261b8e41590a3d423b07" + } + ] + }, + { + "closure": { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d4141506a830e64a83667fc6babeb527cdaf689a2a883644716d40f0620a09ceb6b7f8ab0a79901fcd03a2bf5979ef6ba3d132da441fa588e612664a71bed627db9d2ab0", + "signature": "7faf38f08dff4bb542b8109b141475fb30b1a004dd9fb7e098bff8c178be996244f2b5a8c34f2e5c011c43e8b52430a71dc48ec025bf1868cb7b4899b68e700f" + }, + "messages": [ + { + "body": "011c3a9e2b73a8533f86236d692740fbfac73ab53963ad346aa97d9562a7d41415060100010000004501000100033fe8fca9d79ef29d8d8b3e204d1d18e3765d8a7ce178e2187873c47bdd65a6ae734627d98186db20d3136ae4c634db8e3f2b6e03c7c8e6d3c7cd046dd7acea27", + "signature": "e7dddd9453e999c83946c905770633ed82e2acdd42902f301532e8262039c8cee9fe788b7fd05a79564b7c044c9573c78acb8fd161d55f415cfd0da6b6ac6802" + } + ] + } + ], + "threshold": 2 + }, + "name": "insufficient_reconstruction_values", + "operation": "dkg_verify_transcript" + }, + { + "description": "Only dealer 1 publishes R1.", + "error": "InsufficientQualified", + "inputs": { + "ceremony": { + "body": "010166666666666666666666666666666666666666666666666666666666666666660000001b74696e79636c6f75643a746573743a636572746966696361746573000000186469643a6b65793a7463323232322d666978747572652d3000000200030000000000000000000000000000000000030001000000186469643a6b65793a7463323232322d666978747572652d313e586833f35c91c9985444777fa6cc0c4d8fcd4c615c247bef3a42e43cd40f540002000000186469643a6b65793a7463323232322d666978747572652d32c1fb47d90b9d208657741fc05eccf533f891eabb5207b985fb008705c2e47c6f0003000000186469643a6b65793a7463323232322d666978747572652d3376780196f3874c7483661b5027b6b902397475e479fa6fcc187b296155165079", + "signature": "b0672727bf08c14e0df862f2d6588821def7de6c383063b07457678ed922e056c0ca8aff15d5f13e0d6f9c82efec218b6efcbfc44eed22d60950ff8146ba4209" + }, + "ceremony_digest": "3a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f28", + "initial_transcript": "7c081b8a20f4b5452efc8ebca5ecdcc2231c71a81ca36cf8e8ed4277eeffefe5", + "members": 3, + "rounds": [ + { + "closure": { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f28016547e2484058e47c45b10b3a1065a4d3054c3d70c6fad4f0387daac035bff85e7c081b8a20f4b5452efc8ebca5ecdcc2231c71a81ca36cf8e8ed4277eeffefe5", + "signature": "a02ce670e09c10ccfeb242eca00b7c6b813b999ac881376e269987cb0d25469b3102b8762af3ae8f87540f3af8d4e14c453f096831af306913b619e6aebde700" + }, + "messages": [ + { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f2801010001000002360100028515c51b01f575e1e7f7022285e3dd997a45652896de32669c29323f930792356b828dfce45794b754c732ec8c6df2b304b66a073ac4bb307179361be99ae7a0848404e724afb6094722897c1ed83cb637324a65f2a595e8464089c71eff2366b7089d1a108e4b122e5fd0c4cfa02b05465bcd0690a8402176119d37194be51901155c2c38c955224142a624b8320aab09d1bf8d430d5eed518bcbb5fb2b5bc59929f840e588049f9f3c11ad2b21946871b8dcca631699a22e1e9d184866fd8a000300010000007501fdb9c67a06c2c9629575bb17e7045d2b9a32f2d1c1be52aa580933673b75c04b000000508fd9ce372926e6e1d86846ecbf76cf7adc1204745a714178f7e23da7abce0479c4c54180fa93c111a13d90a09b327ef44e27123392a699fd939ce4a86a4e2dbf177878d9a632d86b0b1d871a65c9defb00020000007501e98a25d1b546dd01d713df1167d8bb0150e58805890e755e39bd9e38441dc94400000050d9938db759a732c1ce448956eb3705e7b2c0d76d678ff226c71ea7b56760fe3fd11c5bc81e79563622d7ccced1d40275ee6f77fe767c02b75e3b804d23490ef345ed77edc15262e2303931d2d1f383d200030000007501f2789c1de58910eb088d2b2fdcf88c23b85dfe0d988461b88b5560f6c4f74936000000504b750492f6c13c1f0e1c1f069241957394cbd41bf73c169441a2b6a95581eda7461d5a67bcd25e3f861d143dc411fbd328e51750621a8016d37205951ee0e71ceff8dfcbedf8774802bb964966192b3a", + "signature": "67cb61dad6e7eba47727abeac98c81aaa73304497da60e5b241711d640211818d4cc7ca61681fe253be44bcb8112f3d34c0d2d89991493fb46a57c66563ddb0e" + } + ] + }, + { + "closure": { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f2802f6f61bf62dc6e4adf25e3e6628e605490efaf5d39c07a1065d9860c21532a987e23292e7aefc8ea23bd1af23c52109b745ec92dcc9947af3dddde751ed9307ec", + "signature": "c74a79f9d42e3fe02220184fb53ffb69ddc42fed1c71cf0d4aca77f2611496294bc2c67165fbe20860a35092fbc12708e3d924f532b0908942df1a2bf4eeb30c" + }, + "messages": [ + { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f280201000100000003010000", + "signature": "bbf53931c7c6f402fe1f287d26a9a3c41e79ac08b2a80ef05775481200bf3a7105d1a77d5975e9fbb5510a10ae9d5eb528e134d10c03daf195be75d2ca6ccd00" + }, + { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f280201000200000003010000", + "signature": "840c096cfd990a5816a2965b0c0885973c5e7f9e2cc09403ee41a262d7012657eb3eae77799e53d9b099dd609bb0298c87c6248aec815599f1793fb885d35a0c" + }, + { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f280201000300000003010000", + "signature": "dd42e9e11fefe89383a2ea7da0d73f966a01a14ea2ca23711cd17f79e0504c5670bb46d8a83685ce1e6b5342796b839c041ee7e4d5de5cedadd9f9a97337ba07" + } + ] + }, + { + "closure": { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f28035a4209392b39b58ecf527da78c6e00731ba652262cdb87604e93a5ea569dd28ec340ee234358e9145d0b3c801417fb6dd82c273a05a6b4286aec25a45b92bf89", + "signature": "72cfdf1fa6cc20c259120b2c29af5cdec8c04cc610a4108473756839f3b8b8f9506c49317d94f309b19a2e220797480b4be8250bd5ef6e855c503f9058a2030c" + }, + "messages": [ + { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f280301000100000003010000", + "signature": "aea1b9cd0f4615bb7f89c3e4dfac56d4b90932b7d2723419e3763516bee282ea2be460e42b6e1589469180a8538fa6c2bd3f84f40e6f8eb90dd3f608f1fab601" + }, + { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f280301000200000003010000", + "signature": "e2faac937923c60804b3e8611c783974b77d6422b79b1b0337773465e83f90ed6d548c9f0cda28dc65abe5e5c4e1d00038ba1c06ca85c4b02fd71db2cb36760f" + }, + { + "body": "013a418842a9461bcecd458ce346fb3d08d402e50bb0a56facbdf0652ae7562f280301000300000003010000", + "signature": "dd59d39746b76f87699344c797d56a6896d97181cb67f3c0832e819749cea48e14c70b98e491f6567b988596e0b5bbba7af4446f787cce696af8387690019b00" + } + ] + } + ], + "threshold": 2 + }, + "name": "insufficient_qualified_dealers", + "operation": "dkg_verify_transcript" + } + ], + "dst": { + "ceremony_auth": "TC-THRESHOLD-V1-CEREMONY-AUTH", + "ceremony_msg": "TC-THRESHOLD-V1-CEREMONY-MSG", + "commitments": "TC-THRESHOLD-V1-COMMITMENTS", + "dealing": "TC-THRESHOLD-V1-DEALING", + "dkg_start": "TC-THRESHOLD-V1-DKG-START", + "pedersen_h": "TC-THRESHOLD-V1-PEDERSEN-H_BLS12381G2_XMD:SHA-256_SSWU_RO_", + "pedersen_h_message": "pedersen-h", + "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": [ + 97, + 98, + 99, + 100, + 101, + 102 + ] + }, + "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" + }, + { + "did": "did:key:tc2222-fixture-5", + "ed25519_public_key": "6e7a1cdd29b0b78fd13af4c5598feff4ef2a97166e3ca6f2e4fbfccd80505bf1" + } + ], + "spec_sections": [ + "3.2", + "3.3", + "3.7", + "3.10", + "S0.3 DKG appendix" + ], + "suite": "BLS12-381 G2 Pedersen/Feldman GJKR New-DKG; RFC 9180 base mode DHKEM(X25519, HKDF-SHA256)/HKDF-SHA256/ChaCha20-Poly1305 dealings; Ed25519 test signatures" +} From 82ab38cbbdb64005eff7d4c694c99fb12cb0ce4f Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:49:59 +0000 Subject: [PATCH 08/15] test(threshold): cover DKG ceremony binding and cohort index rules, enumerate fault patterns; Details: TC-2220 --- tinycloud-threshold/src/dkg_relay.rs | 175 ++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 29 deletions(-) diff --git a/tinycloud-threshold/src/dkg_relay.rs b/tinycloud-threshold/src/dkg_relay.rs index a3c1719c..9dfd0368 100644 --- a/tinycloud-threshold/src/dkg_relay.rs +++ b/tinycloud-threshold/src/dkg_relay.rs @@ -5,7 +5,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use proptest::prelude::*; use rand_chacha::ChaCha20Rng; use rand_core::{CryptoRng, RngCore, SeedableRng}; use serde_json::{json, Value}; @@ -940,6 +939,66 @@ fn valid_unsolicited_answer_is_ignored() { assert!(output.disqualified.is_empty()); } +#[test] +fn r2_naming_a_dealer_outside_the_cohort_supplies_no_complaints() { + // Member 3's R2 names dealer 1 and index 4 > n. The list is out of cohort, + // so it creates no required answer and dealer 1's missing R3 is harmless. + let run = run(2, 3, 0x5c, |out| match out.round() { + 2 => out.replace( + 3, + &DkgComplaints { + dealers: vec![idx(1), idx(4)], + } + .encode(), + ), + 3 => out.remove(1), + _ => {} + }); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!(output.qual, members(&[1, 2, 3])); + assert!(output.disqualified.is_empty()); +} + +#[test] +fn r3_naming_a_recipient_outside_the_cohort_is_malformed() { + // Dealer 2 publishes an unrequested R3 entry for index 4 > n: malformed, + // and without required answers it does not disqualify. Dealer 1 answers + // member 3's complaint validly but adds an index-4 entry: malformed, so the + // required answer counts as absent. + let run = run(2, 3, 0x5d, |out| match out.round() { + 2 => out.replace( + 3, + &DkgComplaints { + dealers: vec![idx(1)], + } + .encode(), + ), + 3 => { + let mut answers = DkgAnswers::decode(&out.payload(1)).unwrap(); + assert_eq!(answers.answers.len(), 1); + answers.answers.push((idx(4), pair(5, 6))); + out.replace(1, &answers.encode()); + let unrequested = DkgAnswers { + answers: vec![(idx(4), pair(5, 6))], + }; + out.replace(2, &unrequested.encode()); + } + _ => {} + }); + // Assert the verifier's outcome directly, so a counted out-of-cohort entry + // fails here as a disqualification mismatch rather than inside a helper. + assert_eq!( + run.verify().map(|o| o.disqualified), + Ok(BTreeMap::from([(idx(1), DisqualifyReason::MissingAnswer)])) + ); + let output = agreed(&run, &[1, 2, 3]); + assert_eq!( + output.disqualified, + BTreeMap::from([(idx(1), DisqualifyReason::MissingAnswer)]) + ); + assert_eq!(output.qual, members(&[2, 3])); +} + #[test] fn relay_omitted_complaint_fails_only_the_affected_member() { // Failure scenario outside the trusted-relay liveness model. @@ -1407,6 +1466,29 @@ fn transitions_refuse_other_ceremonies_rounds_and_histories() { assert!(answered.remove(&2).unwrap().extract(&chained).is_ok()); } +#[test] +fn transitions_refuse_a_foreign_ceremony_closure_on_this_history() { + // Same owner, round and previous link (this ceremony's H0); only the + // authorized-ceremony digest differs. + let f = fixture(2, 3, 0x5a); + let other = fixture(2, 3, 0x5b); + let h0 = dkg_initial_transcript(&f.ceremony); + let other_r1: Vec<_> = (1..=3) + .map(|i| { + let mut rng = other.member_rng(i); + let (_, m) = dkg_start(&other.ceremony, idx(i), other.secret(i), &mut rng).unwrap(); + other.sign(i, &m) + }) + .collect(); + let foreign = close_for::(&other, &other.ceremony, 1, &h0, &other_r1).unwrap(); + assert_eq!(foreign.previous_hash(), h0); + assert_eq!(foreign.round(), 1); + assert_ne!(foreign.ceremony_digest(), f.ceremony.digest()); + let mut rng = f.member_rng(1); + let (state, _) = dkg_start(&f.ceremony, idx(1), f.secret(1), &mut rng).unwrap(); + assert!(matches!(state.complain(&foreign), Err(Error::InvalidRound))); +} + #[test] fn closed_rounds_reject_wrong_signers_cohorts_and_signatures() { let f = fixture(2, 3, 0x53); @@ -1594,36 +1676,71 @@ fn misbehave(out: &mut Outbox, m: u16, behaviour: u8, target: u16) { } } -proptest! { - #![proptest_config(ProptestConfig::with_cases(12))] +/// The `misbehave` model has 15 behaviours per faulty member. +const BEHAVIOURS: u8 = 15; +/// Fixed fixture seeds; each shape is enumerated under both. +const ENUMERATION_SEEDS: [u8; 2] = [0x01, 0x9c]; + +// Coverage: the enumeration below runs every behaviour assignment of the +// in-tree `misbehave` model to the last t−1 members, for 2-of-3 and 3-of-5, +// under two fixed seeds: 2 × (15 + 15 × 15) = 480 ceremonies. It covers this +// model only, with honest messages always delivered by the relay; it is not a +// claim about arbitrary faulty behaviour, other seeds or other parameters. + +/// One within-budget run: faulty members are the last t−1 indices; every honest +/// member and the verifier agree, honest dealers stay in QUAL, and mpk is the sum +/// of the QUAL dealers' committed constants. +fn within_budget_agreement(t: u16, n: u16, seed: u8, behaviours: &[u8]) { + let honest_members: Vec = (1..=n - (t - 1)).collect(); + let faulty: Vec = (n - (t - 1) + 1..=n).collect(); + assert_eq!(faulty.len(), behaviours.len()); + let mut constants_seen = BTreeMap::new(); + let run = run(t, n, seed, |out| { + if out.round() == 4 { + constants(out, &mut constants_seen); + } + for (m, behaviour) in faulty.iter().zip(behaviours) { + misbehave(out, *m, *behaviour, 1); + } + }); + let output = agreed(&run, &honest_members); + let faulty_set = members(&faulty); + assert!( + members(&honest_members).is_subset(&output.qual), + "{behaviours:?}" + ); + assert!( + output.disqualified.keys().all(|i| faulty_set.contains(i)), + "{behaviours:?}" + ); + assert!( + output.reconstructed.is_subset(&faulty_set), + "{behaviours:?}" + ); + assert_eq!( + output.public.master_public_key().encode(), + genuine_mpk(&constants_seen, &output.qual), + "{behaviours:?}" + ); +} + +#[test] +fn every_within_budget_fault_pattern_finishes_with_identical_outcomes_2of3() { + for seed in ENUMERATION_SEEDS { + for behaviour in 0..BEHAVIOURS { + within_budget_agreement(2, 3, seed, &[behaviour]); + } + } +} - #[test] - fn within_budget_fault_patterns_finish_with_identical_outcomes( - shape in 0usize..2, - seed in any::(), - behaviours in proptest::collection::vec(0u8..15, 2), - ) { - let (t, n) = [(2u16, 3u16), (3, 5)][shape]; - let honest_members: Vec = (1..=n - (t - 1)).collect(); - let faulty: Vec = (n - (t - 1) + 1..=n).collect(); - let mut constants_seen = BTreeMap::new(); - let run = run(t, n, seed, |out| { - if out.round() == 4 { - constants(out, &mut constants_seen); - } - for (k, m) in faulty.iter().enumerate() { - misbehave(out, *m, behaviours[k], 1); +#[test] +fn every_within_budget_fault_pattern_finishes_with_identical_outcomes_3of5() { + for seed in ENUMERATION_SEEDS { + for first in 0..BEHAVIOURS { + for second in 0..BEHAVIOURS { + within_budget_agreement(3, 5, seed, &[first, second]); } - }); - let output = agreed(&run, &honest_members); - let faulty_set = members(&faulty); - prop_assert!(members(&honest_members).is_subset(&output.qual)); - prop_assert!(output.disqualified.keys().all(|i| faulty_set.contains(i))); - prop_assert!(output.reconstructed.is_subset(&faulty_set)); - prop_assert_eq!( - output.public.master_public_key().encode(), - genuine_mpk(&constants_seen, &output.qual) - ); + } } } From 1b9bccdb24b86a888a4a77c371c5b800fb380f86 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:46:32 +0000 Subject: [PATCH 09/15] test(threshold): specify authorized redistribution rounds and signed relay scenarios; Details: TC-2221 --- tinycloud-threshold/src/checkpoint.rs | 145 ++ tinycloud-threshold/src/reshare.rs | 168 ++ tinycloud-threshold/src/reshare_relay.rs | 2201 ++++++++++++++++++++++ 3 files changed, 2514 insertions(+) create mode 100644 tinycloud-threshold/src/reshare.rs create mode 100644 tinycloud-threshold/src/reshare_relay.rs diff --git a/tinycloud-threshold/src/checkpoint.rs b/tinycloud-threshold/src/checkpoint.rs index d4e388c0..9d7b0e96 100644 --- a/tinycloud-threshold/src/checkpoint.rs +++ b/tinycloud-threshold/src/checkpoint.rs @@ -690,3 +690,148 @@ 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/reshare.rs b/tinycloud-threshold/src/reshare.rs new file mode 100644 index 00000000..f95f7f44 --- /dev/null +++ b/tinycloud-threshold/src/reshare.rs @@ -0,0 +1,168 @@ +//! 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. + +#[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..07457ddb --- /dev/null +++ b/tinycloud-threshold/src/reshare_relay.rs @@ -0,0 +1,2201 @@ +//! 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 (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 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])); +} + +// ------------------------------------------------------- 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()), + } + } +} From 6e5e92870c7e1a45b63d699249565f714fb18334 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:46:32 +0000 Subject: [PATCH 10/15] build(threshold): wire the redistribution module and its test relay; Details: TC-2221 --- tinycloud-threshold/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) 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; From 501608794d0483242221c87295def7b7404d3bb0 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:56:51 +0000 Subject: [PATCH 11/15] feat(threshold): add authorized redistribution over owner-closed rounds; Details: TC-2221 --- tinycloud-threshold/src/reshare.rs | 751 +++++++++++++++++++++++++++++ 1 file changed, 751 insertions(+) diff --git a/tinycloud-threshold/src/reshare.rs b/tinycloud-threshold/src/reshare.rs index f95f7f44..848dfeee 100644 --- a/tinycloud-threshold/src/reshare.rs +++ b/tinycloud-threshold/src/reshare.rs @@ -6,6 +6,757 @@ //! 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::*; From 77ecc9c82f079693a30030cac45402e7496a313e Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:56:51 +0000 Subject: [PATCH 12/15] docs(threshold): record the redistribution layouts and rules; Details: TC-2221 --- tinycloud-threshold/README.md | 13 +++++-- tinycloud-threshold/SPEC.md | 68 +++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/tinycloud-threshold/README.md b/tinycloud-threshold/README.md index e619b56c..bd5888a8 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. @@ -37,7 +38,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 @@ -57,6 +58,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 a6f425f5..c7fca61f 100644 --- a/tinycloud-threshold/SPEC.md +++ b/tinycloud-threshold/SPEC.md @@ -219,7 +219,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: @@ -232,7 +232,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 @@ -437,3 +439,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`. 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). From 65e49139c56e7496490b8a95ab0ba3440138f561 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:08:41 +0000 Subject: [PATCH 13/15] style(threshold): apply rustfmt to the redistribution files; Details: TC-2221 --- tinycloud-threshold/src/checkpoint.rs | 20 ++- tinycloud-threshold/src/reshare.rs | 18 +-- tinycloud-threshold/src/reshare_relay.rs | 169 ++++++++++++++++++----- 3 files changed, 157 insertions(+), 50 deletions(-) diff --git a/tinycloud-threshold/src/checkpoint.rs b/tinycloud-threshold/src/checkpoint.rs index 9d7b0e96..dac5a52f 100644 --- a/tinycloud-threshold/src/checkpoint.rs +++ b/tinycloud-threshold/src/checkpoint.rs @@ -768,10 +768,20 @@ fn checkpoint_2of3_reshare() { 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()); + 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), + 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"); @@ -800,7 +810,11 @@ fn checkpoint_2of3_reshare() { Err(ShareError::ContextMismatch) ); assert!(matches!( - combine(next.cert.public_key_set(), &new_req, &[new_issued, old_issued]), + 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"); diff --git a/tinycloud-threshold/src/reshare.rs b/tinycloud-threshold/src/reshare.rs index 848dfeee..f21a04a4 100644 --- a/tinycloud-threshold/src/reshare.rs +++ b/tinycloud-threshold/src/reshare.rs @@ -276,11 +276,11 @@ impl Public { } let old = current.key_context().threshold(); if c.kind() == CeremonyKind::Refresh { - let same_directory = c - .recipients() + let same_directory = c.recipients().iter().map(|r| (r.index, &r.node)).eq(current + .body() + .members() .iter() - .map(|r| (r.index, &r.node)) - .eq(current.body().members().iter().map(|m| (m.index, &m.node))); + .map(|m| (m.index, &m.node))); if c.threshold() != old || !same_directory { return Err(Error::InvalidCeremony); } @@ -319,7 +319,10 @@ impl Public { 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 + .commitments + .last() + .is_some_and(|top| !top.is_identity()) && deal .dealings .iter() @@ -850,10 +853,7 @@ mod tests { } assert_eq!(bytes, expected); assert_eq!(ReshareAnswers::decode(&bytes).unwrap().encode(), bytes); - assert_eq!( - ReshareAnswers::decode(&[1, 0, 0]).unwrap().answers.len(), - 0 - ); + assert_eq!(ReshareAnswers::decode(&[1, 0, 0]).unwrap().answers.len(), 0); rejects_every_truncation_and_trailing_byte::(&bytes); let mut noncanonical = expected.clone(); diff --git a/tinycloud-threshold/src/reshare_relay.rs b/tinycloud-threshold/src/reshare_relay.rs index 07457ddb..d227da0d 100644 --- a/tinycloud-threshold/src/reshare_relay.rs +++ b/tinycloud-threshold/src/reshare_relay.rs @@ -27,8 +27,8 @@ use crate::{ checkpoint::{authorize, identity}, curve::{Scalar, G2}, derive::{ - combine, decrypt_derived_key, issue_share, verify_share, DecryptRequest, - EncryptedKeyShare, KeyShare, TransportSecretKey, + combine, decrypt_derived_key, issue_share, verify_share, DecryptRequest, EncryptedKeyShare, + KeyShare, TransportSecretKey, }, dkg::{dkg_initial_transcript, DkgRoundRecord}, dkg_relay::commitment_hash, @@ -132,7 +132,8 @@ pub(crate) fn dealer_genesis(t: u16, n: u16) -> (VerifiedCertificate, Vec( 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)); + 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); @@ -565,7 +570,10 @@ pub(crate) fn agreed(run: &Run, receivers: &[u16]) -> ReshareOutput { .master_public_key() .encode() ); - assert_eq!(verified.public.threshold(), run.fixture.ceremony.threshold()); + 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 (candidate, output) = run.outcomes[j].as_ref().unwrap(); @@ -630,7 +638,11 @@ pub(crate) fn install(run: Run, output: &ReshareOutput) -> Next { } /// 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) { +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( @@ -872,7 +884,14 @@ fn genuine_2of3_to_3of4_preserves_the_master_key_and_binds_every_candidate() { 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), + unwraps_through_every_subset( + &next.cert, + &next.shares, + "records/reshare", + &data, + &ct, + 0x82 + ), 4 ); } @@ -933,7 +952,14 @@ fn dkg_genesis_reshare_chain_keeps_the_master_key_through_refresh() { Threshold::new(t, n).unwrap() ); assert_eq!( - unwraps_through_every_subset(&next.cert, &next.shares, "records/chain", &data, &ct, seed), + unwraps_through_every_subset( + &next.cert, + &next.shares, + "records/chain", + &data, + &ct, + seed + ), subsets ); current = next.cert; @@ -1051,7 +1077,15 @@ fn within_the_secrecy_budget_but_below_t_valid_dealers_refuses_and_keeps_the_old 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 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); @@ -1102,12 +1136,12 @@ fn a_member_of_both_cohorts_signs_distinct_roles() { .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 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()), + signature: f.signers.sign( + &Signers::did(1), + &ceremony_message_preimage(&bytes).unwrap(), + ), bytes, }; assert!(matches!( @@ -1180,7 +1214,11 @@ fn one_recipient_bad_value_is_replaced_by_a_valid_answer() { 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::>(), + published + .answers + .iter() + .map(|(j, _)| *j) + .collect::>(), dealers(&[2]) ); } @@ -1196,7 +1234,10 @@ fn invalid_answer_excludes_the_dealer_and_the_next_dealer_is_selected() { _ => {} }); let output = agreed(&run, &[1, 2, 3]); - assert_eq!(output.excluded, excluded(&[(1, ExcludeReason::InvalidAnswer)])); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::InvalidAnswer)]) + ); assert_eq!(output.dealers, dealers(&[2, 3])); } @@ -1211,7 +1252,10 @@ fn missing_r3_excludes_only_with_required_answers() { _ => {} }); let output = agreed(&run, &[1, 2, 3]); - assert_eq!(output.excluded, excluded(&[(1, ExcludeReason::MissingAnswer)])); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingAnswer)]) + ); assert_eq!(output.dealers, dealers(&[2, 3])); } @@ -1226,7 +1270,10 @@ fn malformed_r3_excludes_only_with_required_answers() { _ => {} }); let output = agreed(&run, &[1, 2, 3]); - assert_eq!(output.excluded, excluded(&[(1, ExcludeReason::MissingAnswer)])); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingAnswer)]) + ); assert_eq!(output.dealers, dealers(&[2, 3])); } @@ -1238,7 +1285,10 @@ fn invalid_unsolicited_answer_excludes_its_dealer() { } }); let output = agreed(&run, &[1, 2, 3]); - assert_eq!(output.excluded, excluded(&[(2, ExcludeReason::InvalidAnswer)])); + assert_eq!( + output.excluded, + excluded(&[(2, ExcludeReason::InvalidAnswer)]) + ); assert_eq!(output.dealers, dealers(&[1, 3])); } @@ -1288,7 +1338,10 @@ fn r3_naming_a_recipient_outside_the_new_cohort_is_malformed() { _ => {} }); let output = agreed(&run, &[1, 2, 3]); - assert_eq!(output.excluded, excluded(&[(1, ExcludeReason::MissingAnswer)])); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingAnswer)]) + ); assert_eq!(output.dealers, dealers(&[2, 3])); } @@ -1341,7 +1394,11 @@ fn duplicate_complaints_count_once() { }); let published = ReshareAnswers::decode(run.answers[&1].as_ref().unwrap()).unwrap(); assert_eq!( - published.answers.iter().map(|(j, _)| *j).collect::>(), + published + .answers + .iter() + .map(|(j, _)| *j) + .collect::>(), dealers(&[4, 5]) ); let output = agreed(&run, &[1, 2, 3, 4, 5]); @@ -1441,7 +1498,10 @@ fn missing_and_equivocated_r1_exclude() { } }); let output = agreed(&missing, &[1, 2, 3]); - assert_eq!(output.excluded, excluded(&[(1, ExcludeReason::MissingDeal)])); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingDeal)]) + ); assert_eq!(output.dealers, dealers(&[2, 3])); let equivocated = run(Reshare, 2, 3, 0xa8, |out| { @@ -1486,7 +1546,10 @@ fn dealers_are_the_first_t_in_closed_round_order() { }); let output = agreed(&skipped, &[1, 2, 3]); assert_eq!(output.dealers, dealers(&[1, 2])); - assert_eq!(output.excluded, excluded(&[(3, ExcludeReason::MalformedDeal)])); + assert_eq!( + output.excluded, + excluded(&[(3, ExcludeReason::MalformedDeal)]) + ); } // --------------------------------------------------------- wrong cohorts @@ -1505,7 +1568,10 @@ fn wrong_cohort_r1_is_ignored_without_blame() { } }); let output = agreed(&run, &[1, 2, 3]); - assert_eq!(output.excluded, excluded(&[(1, ExcludeReason::MissingDeal)])); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingDeal)]) + ); assert_eq!(output.dealers, dealers(&[2, 3])); } @@ -1541,7 +1607,10 @@ fn wrong_cohort_r3_is_ignored_without_blame() { _ => {} }); let output = agreed(&run, &[1, 2, 3]); - assert_eq!(output.excluded, excluded(&[(1, ExcludeReason::MissingAnswer)])); + assert_eq!( + output.excluded, + excluded(&[(1, ExcludeReason::MissingAnswer)]) + ); assert_eq!(output.dealers, dealers(&[2, 3])); } @@ -1681,7 +1750,9 @@ fn transitions_refuse_other_ceremonies_rounds_and_histories() { }; 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 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)) @@ -1710,9 +1781,14 @@ fn transitions_refuse_other_ceremonies_rounds_and_histories() { receiver(1).complain(&rebased), Err(Error::InvalidRound) )); - let dkg_link = - close_for::(&f, &f.ceremony, 1, &dkg_initial_transcript(&f.ceremony), &r1) - .unwrap(); + 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) @@ -1751,7 +1827,13 @@ fn transitions_refuse_other_ceremonies_rounds_and_histories() { Err(Error::InvalidRound) )); let r3: Vec<_> = (1..=3) - .map(|i| f.sign(Current, i, &deal(&f, i).0.answer(&closed1, &closed2).unwrap())) + .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(); @@ -1776,8 +1858,13 @@ fn transitions_refuse_other_ceremonies_rounds_and_histories() { 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 (_, 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 { @@ -1808,7 +1895,10 @@ fn verifier_recomputes_from_authenticated_records_only() { 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); + 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()); @@ -1822,10 +1912,13 @@ fn verifier_recomputes_from_authenticated_records_only() { 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() - ); + 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(), From 45341d995a1f535483a2461385b20da25055d076 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:12:09 +0000 Subject: [PATCH 14/15] test(threshold): add fixed-seed redistribution vectors; Details: TC-2221 --- tinycloud-threshold/vectors/v1/reshare.json | 583 ++++++++++++++++++++ 1 file changed, 583 insertions(+) create mode 100644 tinycloud-threshold/vectors/v1/reshare.json 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" +} From a429c480e69eae1cff2f71a7c43a3db3a33ce069 Mon Sep 17 00:00:00 2001 From: "tinycloud-bot[bot]" <348376441+tinycloud-bot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:30:52 +0000 Subject: [PATCH 15/15] test(threshold): cover signature encodings, old-cohort complaints and accepted-commitment answers; Details: TC-2221 --- tinycloud-threshold/SPEC.md | 2 +- tinycloud-threshold/src/reshare_relay.rs | 141 +++++++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/tinycloud-threshold/SPEC.md b/tinycloud-threshold/SPEC.md index c7fca61f..07d52853 100644 --- a/tinycloud-threshold/SPEC.md +++ b/tinycloud-threshold/SPEC.md @@ -496,7 +496,7 @@ A dealer answers in R3 only when its R1 passes the public checks and carries its ### 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`. 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`. +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 diff --git a/tinycloud-threshold/src/reshare_relay.rs b/tinycloud-threshold/src/reshare_relay.rs index d227da0d..50788cc3 100644 --- a/tinycloud-threshold/src/reshare_relay.rs +++ b/tinycloud-threshold/src/reshare_relay.rs @@ -576,6 +576,12 @@ pub(crate) fn agreed(run: &Run, receivers: &[u16]) -> ReshareOutput { ); 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)); @@ -1419,6 +1425,38 @@ fn r2_naming_a_dealer_outside_the_old_cohort_supplies_no_complaints() { 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. @@ -1881,6 +1919,109 @@ fn retry_under_a_new_ceremony_cannot_reuse_an_earlier_dealing() { 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]