diff --git a/pkg/tbtc/signer/src/api.rs b/pkg/tbtc/signer/src/api.rs index 98441d905a..e95d63499a 100644 --- a/pkg/tbtc/signer/src/api.rs +++ b/pkg/tbtc/signer/src/api.rs @@ -11,6 +11,8 @@ pub struct RunDkgRequest { pub session_id: String, pub participants: Vec, pub threshold: u16, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub dkg_seed_hex: Option, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] @@ -29,6 +31,8 @@ pub struct StartSignRoundRequest { pub message_hex: String, pub key_group: String, #[serde(default, skip_serializing_if = "Option::is_none")] + pub taproot_merkle_root_hex: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub signing_participants: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub attempt_context: Option, @@ -61,6 +65,8 @@ pub struct RoundState { pub required_contributions: u16, pub message_digest_hex: String, #[serde(default, skip_serializing_if = "Option::is_none")] + pub taproot_merkle_root_hex: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub signing_participants: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub attempt_transition_telemetry: Option, @@ -70,6 +76,8 @@ pub struct RoundState { #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct FinalizeSignRoundRequest { pub session_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub taproot_merkle_root_hex: Option, pub round_contributions: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub attempt_context: Option, diff --git a/pkg/tbtc/signer/src/engine.rs b/pkg/tbtc/signer/src/engine.rs index 4c9462a7f7..58a3d059db 100644 --- a/pkg/tbtc/signer/src/engine.rs +++ b/pkg/tbtc/signer/src/engine.rs @@ -24,6 +24,7 @@ use std::str::FromStr; use std::sync::{mpsc, Mutex, OnceLock}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use frost::keys::Tweak; use frost_secp256k1_tr as frost; use rand_chacha::rand_core::{CryptoRng, Error as RandCoreError, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -4250,6 +4251,32 @@ fn canonicalize_refresh_shares_request_for_fingerprint( canonical_request } +fn canonicalize_taproot_merkle_root_hex( + taproot_merkle_root_hex: &mut Option, +) -> Result, EngineError> { + let Some(raw_taproot_merkle_root_hex) = taproot_merkle_root_hex.as_mut() else { + return Ok(None); + }; + + let normalized_taproot_merkle_root_hex = + raw_taproot_merkle_root_hex.trim().to_ascii_lowercase(); + let taproot_merkle_root_bytes = + hex::decode(&normalized_taproot_merkle_root_hex).map_err(|_| { + EngineError::Validation("taproot_merkle_root_hex must be valid hex".to_string()) + })?; + if taproot_merkle_root_bytes.len() != 32 { + return Err(EngineError::Validation( + "taproot_merkle_root_hex must decode to 32 bytes".to_string(), + )); + } + + let mut taproot_merkle_root = [0_u8; 32]; + taproot_merkle_root.copy_from_slice(&taproot_merkle_root_bytes); + *raw_taproot_merkle_root_hex = normalized_taproot_merkle_root_hex; + + Ok(Some(taproot_merkle_root)) +} + fn truthy_env_flag(raw_value: &str) -> bool { matches!( raw_value.trim().to_ascii_lowercase().as_str(), @@ -4316,16 +4343,19 @@ fn derive_round_id( session_id: &str, key_group: &str, message_hex: &str, + taproot_merkle_root_hex: Option<&str>, signing_participants_fingerprint: &str, attempt_context: Option<&AttemptContext>, ) -> String { let attempt_id_component = round_attempt_id_component(attempt_context); + let taproot_merkle_root_component = taproot_merkle_root_hex.unwrap_or("no-taproot-merkle-root"); hash_hex( format!( - "round:{}:{}:{}:{}:{}", + "round:{}:{}:{}:{}:{}:{}", session_id, key_group, message_hex, + taproot_merkle_root_component, signing_participants_fingerprint, attempt_id_component ) @@ -5165,8 +5195,7 @@ pub fn run_dkg(request: RunDkgRequest) -> Result { .map(|identifier| participant_identifier_to_frost_identifier(*identifier)) .collect::, _>>()?; - let mut keygen_rng_seed = [0u8; 32]; - OsRng.fill_bytes(&mut keygen_rng_seed); + let mut keygen_rng_seed = development_dealer_dkg_seed(request.dkg_seed_hex.as_deref())?; let keygen_rng = ZeroizingChaCha20Rng::from_seed(keygen_rng_seed); keygen_rng_seed.zeroize(); @@ -5276,7 +5305,29 @@ fn enforce_bootstrap_dealer_dkg_disabled_in_production( Ok(()) } -pub fn start_sign_round(request: StartSignRoundRequest) -> Result { +fn development_dealer_dkg_seed(dkg_seed_hex: Option<&str>) -> Result<[u8; 32], EngineError> { + let Some(seed_hex) = dkg_seed_hex else { + let mut seed = [0_u8; 32]; + OsRng.fill_bytes(&mut seed); + return Ok(seed); + }; + + let seed = hex::decode(seed_hex) + .map_err(|e| EngineError::Internal(format!("failed to decode DKG seed: {e}")))?; + if seed.len() != 32 { + return Err(EngineError::Internal(format!( + "DKG seed decoded to [{}] bytes, expected 32", + seed.len() + ))); + } + + let mut output = [0u8; 32]; + output.copy_from_slice(&seed); + + Ok(output) +} + +pub fn start_sign_round(mut request: StartSignRoundRequest) -> Result { record_hardening_telemetry(|telemetry| { telemetry.start_sign_round_calls_total = telemetry.start_sign_round_calls_total.saturating_add(1); @@ -5294,10 +5345,13 @@ pub fn start_sign_round(request: StartSignRoundRequest) -> Result Result Result, ) -> Result { let mut commitments = BTreeMap::new(); let mut own_nonces = None; @@ -5693,8 +5774,16 @@ fn build_real_signature_share_contribution( })?; let signing_package = frost::SigningPackage::new(commitments, message_bytes); - let signature_share_result = - frost::round2::sign(&signing_package, &own_nonces, own_key_package); + let signature_share_result = if let Some(taproot_merkle_root) = taproot_merkle_root { + frost::round2::sign_with_tweak( + &signing_package, + &own_nonces, + own_key_package, + Some(taproot_merkle_root.as_slice()), + ) + } else { + frost::round2::sign(&signing_package, &own_nonces, own_key_package) + }; own_nonces.zeroize(); let signature_share = signature_share_result .map_err(|e| EngineError::Internal(format!("failed to create signature share: {e}")))?; @@ -5710,7 +5799,7 @@ fn build_real_signature_share_contribution( } pub fn finalize_sign_round( - request: FinalizeSignRoundRequest, + mut request: FinalizeSignRoundRequest, bootstrap_mode_enabled: bool, ) -> Result { record_hardening_telemetry(|telemetry| { @@ -5721,6 +5810,8 @@ pub fn finalize_sign_round( enforce_provenance_gate()?; validate_session_id(&request.session_id)?; let strict_roast_mode_enabled = roast_strict_mode_enabled(); + let finalize_taproot_merkle_root = + canonicalize_taproot_merkle_root_hex(&mut request.taproot_merkle_root_hex)?; let request_fingerprint = { let mut canonical_attempt_context = request.attempt_context.clone(); @@ -5735,6 +5826,7 @@ pub fn finalize_sign_round( fingerprint(&FinalizeSignRoundRequest { session_id: request.session_id.clone(), + taproot_merkle_root_hex: request.taproot_merkle_root_hex.clone(), round_contributions: canonical_contributions, attempt_context: canonical_attempt_context, })? @@ -5806,6 +5898,11 @@ pub fn finalize_sign_round( .ok_or_else(|| EngineError::SignRoundNotStarted { session_id: request.session_id.clone(), })?; + if request.taproot_merkle_root_hex != round_state.taproot_merkle_root_hex { + return Err(EngineError::Validation( + "taproot_merkle_root_hex does not match active signing round".to_string(), + )); + } if signing_policy_firewall_enforced() { let sign_message_hex = session .sign_message_bytes @@ -5876,6 +5973,11 @@ pub fn finalize_sign_round( session_id: request.session_id, }); } + if is_synthetic && round_state.taproot_merkle_root_hex.is_some() { + return Err(EngineError::Validation( + "synthetic contributions do not support taproot tweaked signing".to_string(), + )); + } let signature_result = if is_synthetic { build_bootstrap_synthetic_signature_result( @@ -5984,10 +6086,37 @@ pub fn finalize_sign_round( } let signing_package = frost::SigningPackage::new(commitments, sign_message_bytes); - let signature = - frost::aggregate(&signing_package, &signature_shares, dkg_public_key_package).map_err( - |e| EngineError::Validation(format!("failed to aggregate signature shares: {e}")), - )?; + let signature = if let Some(taproot_merkle_root) = finalize_taproot_merkle_root.as_ref() { + frost::aggregate_with_tweak( + &signing_package, + &signature_shares, + dkg_public_key_package, + Some(taproot_merkle_root.as_slice()), + ) + } else { + frost::aggregate(&signing_package, &signature_shares, dkg_public_key_package) + } + .map_err(|e| { + EngineError::Validation(format!("failed to aggregate signature shares: {e}")) + })?; + + let verification_key_package = + if let Some(taproot_merkle_root) = finalize_taproot_merkle_root.as_ref() { + dkg_public_key_package + .clone() + .tweak(Some(taproot_merkle_root.as_slice())) + } else { + dkg_public_key_package.clone() + }; + verification_key_package + .verifying_key() + .verify(sign_message_bytes, &signature) + .map_err(|e| { + EngineError::Validation(format!( + "aggregate signature failed self-verification: {e}" + )) + })?; + let signature_bytes = signature.serialize().map_err(|e| { EngineError::Internal(format!("failed to serialize aggregate signature: {e}")) })?; @@ -6597,6 +6726,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -6606,6 +6736,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -6821,6 +6952,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("production profile should reject bootstrap dealer DKG"); @@ -6858,6 +6990,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("missing/empty profile should reject bootstrap dealer DKG"); @@ -6908,6 +7041,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected provenance gate rejection"); @@ -6978,6 +7112,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }); assert!(result.is_ok(), "expected signed attestation acceptance"); @@ -7021,6 +7156,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected missing signature rejection"); @@ -7083,6 +7219,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected signature verification rejection"); @@ -7136,6 +7273,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected attestation expiry rejection"); @@ -7189,6 +7327,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected attestation missing expiry rejection"); @@ -7245,6 +7384,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected attestation expiry too far rejection"); @@ -7307,6 +7447,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected trust-root mismatch rejection"); @@ -7360,6 +7501,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected runtime version mismatch rejection"); @@ -7413,6 +7555,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected status mismatch rejection"); @@ -7459,6 +7602,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected invalid trust root rejection"); @@ -7506,6 +7650,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected session_id validation rejection"); @@ -7542,6 +7687,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected admission policy rejection"); @@ -7575,6 +7721,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected admission policy config rejection"); @@ -7613,6 +7760,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected admission policy config rejection"); @@ -7922,6 +8070,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected run_dkg provenance gate rejection"); assert!(matches!( @@ -7951,6 +8100,7 @@ mod tests { let finalize_err = finalize_sign_round( FinalizeSignRoundRequest { session_id: "session-metrics-provenance-finalize".to_string(), + taproot_merkle_root_hex: None, round_contributions: vec![], attempt_context: None, }, @@ -7996,6 +8146,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8004,6 +8155,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -8054,6 +8206,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8064,6 +8217,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2, 3]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -8085,6 +8239,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -8164,6 +8319,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8174,6 +8330,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2, 3]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -8194,6 +8351,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2, 3]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -8245,6 +8403,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8255,6 +8414,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2, 3]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -8276,6 +8436,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -8305,6 +8466,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected auto-quarantine rejection"); let EngineError::QuarantinePolicyRejected { reason_code, .. } = err else { @@ -8336,6 +8498,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("allowlisted operator should bypass quarantine rejection"); @@ -8379,6 +8542,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8389,6 +8553,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2, 3]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -8410,6 +8575,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -8445,6 +8611,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected quarantine rejection after reload"); let EngineError::QuarantinePolicyRejected { reason_code, .. } = err else { @@ -8479,6 +8646,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8555,6 +8723,7 @@ mod tests { key_group: post_rekey_status .continuity_reference_key_group .expect("continuity reference key group"), + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -8677,6 +8846,7 @@ mod tests { let finalize_err = finalize_sign_round( FinalizeSignRoundRequest { session_id: round_state.session_id.clone(), + taproot_merkle_root_hex: None, round_contributions: vec![ RoundContribution { identifier: 1, @@ -8849,6 +9019,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8857,6 +9028,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -8894,6 +9066,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8904,6 +9077,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -8944,6 +9118,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8955,6 +9130,7 @@ mod tests { member_identifier: 1, message_hex, key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -8989,6 +9165,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8999,6 +9176,7 @@ mod tests { member_identifier: 1, message_hex, key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -9018,6 +9196,7 @@ mod tests { let err = finalize_sign_round( FinalizeSignRoundRequest { session_id: session_id.to_string(), + taproot_merkle_root_hex: None, round_contributions: vec![ RoundContribution { identifier: 1, @@ -9065,6 +9244,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9075,6 +9255,7 @@ mod tests { member_identifier: 1, message_hex, key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -9096,6 +9277,7 @@ mod tests { let err = finalize_sign_round( FinalizeSignRoundRequest { session_id: session_id.to_string(), + taproot_merkle_root_hex: None, round_contributions: vec![ RoundContribution { identifier: 1, @@ -9525,6 +9707,7 @@ mod tests { request_session_id, key_group, message_hex, + None, signing_participants_fingerprint, Some(&lowercase_attempt_context), ); @@ -9532,6 +9715,7 @@ mod tests { request_session_id, key_group, message_hex, + None, signing_participants_fingerprint, Some(&uppercase_attempt_context), ); @@ -9545,6 +9729,7 @@ mod tests { request_session_id, key_group, message_hex, + None, signing_participants_fingerprint, Some(&different_attempt_context), ); @@ -9554,6 +9739,7 @@ mod tests { request_session_id, key_group, message_hex, + None, signing_participants_fingerprint, None, ); @@ -9660,6 +9846,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9668,6 +9855,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: None, attempt_transition_evidence: None, @@ -9710,6 +9898,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed non-production dkg"); @@ -9724,6 +9913,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: None, attempt_transition_evidence: None, @@ -9765,6 +9955,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9775,6 +9966,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -9806,6 +9998,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9818,6 +10011,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -9855,6 +10049,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9867,6 +10062,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -9904,6 +10100,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9916,6 +10113,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -9953,6 +10151,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9965,6 +10164,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -10002,6 +10202,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10026,6 +10227,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(invalid_attempt_context), attempt_transition_evidence: None, @@ -10063,6 +10265,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10074,6 +10277,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -10111,6 +10315,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10127,6 +10332,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -10164,6 +10370,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10180,6 +10387,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(uppercase_attempt_context), attempt_transition_evidence: None, @@ -10193,6 +10401,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![2, 1]), attempt_context: Some(lowercase_attempt_context), attempt_transition_evidence: None, @@ -10224,6 +10433,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10234,6 +10444,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -10243,6 +10454,7 @@ mod tests { let err = finalize_sign_round( FinalizeSignRoundRequest { session_id: session_id.to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -10291,6 +10503,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10301,6 +10514,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -10310,6 +10524,7 @@ mod tests { let signature_result = finalize_sign_round( FinalizeSignRoundRequest { session_id: session_id.to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -10354,6 +10569,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10364,6 +10580,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -10375,6 +10592,7 @@ mod tests { let signature_result = finalize_sign_round( FinalizeSignRoundRequest { session_id: session_id.to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -10421,6 +10639,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10431,6 +10650,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -10442,6 +10662,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: None, attempt_transition_evidence: None, @@ -10474,6 +10695,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10484,6 +10706,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: None, @@ -10497,6 +10720,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -10534,6 +10758,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10544,6 +10769,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -10557,6 +10783,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: None, @@ -10594,6 +10821,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10604,6 +10832,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -10618,6 +10847,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -10643,6 +10873,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(stale_attempt), attempt_transition_evidence: None, @@ -10681,6 +10912,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10691,6 +10923,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -10707,6 +10940,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -10743,6 +10977,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10753,6 +10988,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one.clone()), attempt_transition_evidence: None, @@ -10767,6 +11003,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -10780,6 +11017,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -10821,6 +11059,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10831,6 +11070,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -10848,6 +11088,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(invalid_transition_evidence), @@ -10885,6 +11126,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10895,6 +11137,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -10909,6 +11152,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_three), attempt_transition_evidence: Some(transition_evidence), @@ -10946,6 +11190,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10956,6 +11201,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -10973,6 +11219,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -11010,6 +11257,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11020,6 +11268,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -11041,6 +11290,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -11082,6 +11332,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11092,6 +11343,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2, 3]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -11113,6 +11365,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -11158,6 +11411,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11168,6 +11422,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2, 3]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -11189,6 +11444,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -11230,6 +11486,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11240,6 +11497,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2, 3]), attempt_context: Some(attempt_one), attempt_transition_evidence: None, @@ -11261,6 +11519,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_two), attempt_transition_evidence: Some(transition_evidence), @@ -11298,6 +11557,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11308,6 +11568,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(start_attempt), attempt_transition_evidence: None, @@ -11318,6 +11579,7 @@ mod tests { let err = finalize_sign_round( FinalizeSignRoundRequest { session_id: session_id.to_string(), + taproot_merkle_root_hex: None, attempt_context: Some(mismatched_attempt), round_contributions: vec![ round_state.own_contribution.clone(), @@ -11362,6 +11624,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11372,6 +11635,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(start_attempt), attempt_transition_evidence: None, @@ -11383,6 +11647,7 @@ mod tests { let err = finalize_sign_round( FinalizeSignRoundRequest { session_id: session_id.to_string(), + taproot_merkle_root_hex: None, attempt_context: Some(stale_attempt), round_contributions: vec![ round_state.own_contribution.clone(), @@ -11413,6 +11678,7 @@ mod tests { let request = FinalizeSignRoundRequest { session_id: "session-synthetic-rejected".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -11441,6 +11707,7 @@ mod tests { let request = FinalizeSignRoundRequest { session_id: "session-synthetic-accepted".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -11481,6 +11748,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -11489,6 +11757,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -11530,6 +11799,7 @@ mod tests { &member_two_request, &round_state.round_id, &hex::decode(&member_two_request.message_hex).expect("message decode"), + None, ) .expect("member two contribution"); let member_three_request = StartSignRoundRequest { @@ -11543,11 +11813,13 @@ mod tests { &member_three_request, &round_state.round_id, &hex::decode(&member_three_request.message_hex).expect("message decode"), + None, ) .expect("member three contribution"); let finalize_request = FinalizeSignRoundRequest { session_id: "session-real-finalize".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ round_state.own_contribution.clone(), @@ -11570,6 +11842,169 @@ mod tests { .expect("signature verification"); } + #[test] + fn finalize_aggregates_real_taproot_tweaked_contributions() { + let _guard = lock_test_state(); + reset_for_tests(); + + let run_dkg_request = RunDkgRequest { + session_id: "session-real-taproot-tweak".to_string(), + participants: vec![ + crate::api::DkgParticipant { + identifier: 1, + public_key_hex: "02aa".to_string(), + }, + crate::api::DkgParticipant { + identifier: 2, + public_key_hex: "02bb".to_string(), + }, + crate::api::DkgParticipant { + identifier: 3, + public_key_hex: "02cc".to_string(), + }, + ], + threshold: 2, + dkg_seed_hex: None, + }; + + let taproot_merkle_root_hex = + "37a57b86de2819d2b72a173df46238a7ad295ea1485d3b40e9415daa82b4fdcb"; + let taproot_merkle_root_bytes = + hex::decode(taproot_merkle_root_hex).expect("taproot merkle root"); + let mut taproot_merkle_root = [0_u8; 32]; + taproot_merkle_root.copy_from_slice(&taproot_merkle_root_bytes); + + let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); + let start_request = StartSignRoundRequest { + session_id: "session-real-taproot-tweak".to_string(), + member_identifier: 1, + message_hex: "deadbeef".to_string(), + key_group: dkg_result.key_group, + taproot_merkle_root_hex: Some(taproot_merkle_root_hex.to_string()), + signing_participants: None, + attempt_context: None, + attempt_transition_evidence: None, + }; + let round_state = start_sign_round(start_request.clone()).expect("start sign round"); + assert_eq!( + round_state.taproot_merkle_root_hex.as_deref(), + Some(taproot_merkle_root_hex) + ); + let signing_participants = round_state + .signing_participants + .clone() + .expect("round signing participants"); + + let (dkg_key_packages, dkg_public_key_package, sign_message_bytes) = { + let guard = state().expect("engine state").lock().expect("engine lock"); + let session = guard + .sessions + .get(&start_request.session_id) + .expect("session state"); + + ( + session.dkg_key_packages.clone().expect("dkg key packages"), + session + .dkg_public_key_package + .clone() + .expect("dkg public key package"), + session + .sign_message_bytes + .clone() + .expect("sign message bytes"), + ) + }; + + let member_two_request = StartSignRoundRequest { + member_identifier: 2, + attempt_transition_evidence: None, + ..start_request.clone() + }; + let member_two_contribution = build_real_signature_share_contribution( + &dkg_key_packages, + &signing_participants, + &member_two_request, + &round_state.round_id, + &hex::decode(&member_two_request.message_hex).expect("message decode"), + Some(&taproot_merkle_root), + ) + .expect("member two contribution"); + let member_three_request = StartSignRoundRequest { + member_identifier: 3, + attempt_transition_evidence: None, + ..member_two_request.clone() + }; + let member_three_contribution = build_real_signature_share_contribution( + &dkg_key_packages, + &signing_participants, + &member_three_request, + &round_state.round_id, + &hex::decode(&member_three_request.message_hex).expect("message decode"), + Some(&taproot_merkle_root), + ) + .expect("member three contribution"); + + let finalize_request = FinalizeSignRoundRequest { + session_id: "session-real-taproot-tweak".to_string(), + taproot_merkle_root_hex: Some(taproot_merkle_root_hex.to_string()), + attempt_context: None, + round_contributions: vec![ + round_state.own_contribution.clone(), + member_two_contribution, + member_three_contribution, + ], + }; + + let result = finalize_sign_round(finalize_request, false).expect("finalize"); + + assert_eq!(result.round_id, round_state.round_id); + let signature_bytes = hex::decode(&result.signature_hex).expect("signature decode"); + assert_eq!(signature_bytes.len(), 64); + let signature = frost::Signature::deserialize(&signature_bytes).expect("signature parse"); + let tweaked_public_key_package = dkg_public_key_package + .clone() + .tweak(Some(taproot_merkle_root.as_slice())); + tweaked_public_key_package + .verifying_key() + .verify(&sign_message_bytes, &signature) + .expect("tweaked signature verification"); + assert!( + dkg_public_key_package + .verifying_key() + .verify(&sign_message_bytes, &signature) + .is_err(), + "tweaked signature must not verify under the untweaked key" + ); + } + + #[test] + fn taproot_tweak_matches_cross_repo_deposit_fixture() { + let internal_key = + hex::decode("022336f65004d8f122f1fe947ebd009a8b4add3a0d937356d568e30f7fcc2e4008") + .expect("decode compressed internal key"); + let verifying_key = + frost::VerifyingKey::deserialize(&internal_key).expect("deserialize verifying key"); + let public_key_package = frost::keys::PublicKeyPackage::new( + BTreeMap::::new(), + verifying_key, + Some(1), + ); + + let merkle_root = + hex::decode("3d6f9a2fea1de0a6c260d1fbc0343c9b2ed84307e6a7231139b78438448ee8c0") + .expect("decode taproot merkle root"); + let tweaked_public_key = public_key_package + .tweak(Some(merkle_root.as_slice())) + .verifying_key() + .serialize() + .expect("serialize tweaked verifying key"); + + assert_eq!( + hex::encode(&tweaked_public_key[1..]), + "90e7ce2b6cd476b7a1c2c7f6585c3fd0eae4379a508e981ed422b3e28b9ae8c2" + ); + } + #[test] fn finalize_aggregates_real_threshold_subset_outside_bootstrap_mode() { let _guard = lock_test_state(); @@ -11592,6 +12027,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -11600,6 +12036,7 @@ mod tests { member_identifier: 1, message_hex: "cafef00d".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: None, attempt_transition_evidence: None, @@ -11641,11 +12078,13 @@ mod tests { &member_two_request, &round_state.round_id, &hex::decode(&member_two_request.message_hex).expect("message decode"), + None, ) .expect("member two contribution"); let finalize_request = FinalizeSignRoundRequest { session_id: "session-real-threshold-subset".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ round_state.own_contribution.clone(), @@ -11667,6 +12106,231 @@ mod tests { .expect("signature verification"); } + #[test] + fn start_sign_round_allows_distinct_members_for_same_active_round() { + let _guard = lock_test_state(); + reset_for_tests(); + + let run_dkg_request = RunDkgRequest { + session_id: "session-real-multi-member-process".to_string(), + participants: vec![ + crate::api::DkgParticipant { + identifier: 1, + public_key_hex: "02aa".to_string(), + }, + crate::api::DkgParticipant { + identifier: 2, + public_key_hex: "02bb".to_string(), + }, + ], + threshold: 2, + dkg_seed_hex: None, + }; + + let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); + let start_request = StartSignRoundRequest { + session_id: "session-real-multi-member-process".to_string(), + member_identifier: 1, + message_hex: "baddcafe".to_string(), + key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, + signing_participants: Some(vec![1, 2]), + attempt_context: None, + attempt_transition_evidence: None, + }; + let first_round_state = + start_sign_round(start_request.clone()).expect("first member start sign round"); + + let second_round_state = start_sign_round(StartSignRoundRequest { + member_identifier: 2, + ..start_request.clone() + }) + .expect("second member start sign round"); + + assert_eq!(first_round_state.session_id, second_round_state.session_id); + assert_eq!(first_round_state.round_id, second_round_state.round_id); + assert_eq!(first_round_state.required_contributions, 2); + assert_eq!(second_round_state.required_contributions, 2); + assert_eq!(first_round_state.own_contribution.identifier, 1); + assert_eq!(second_round_state.own_contribution.identifier, 2); + assert_ne!( + first_round_state.own_contribution.signature_share_hex, + second_round_state.own_contribution.signature_share_hex + ); + + let (dkg_public_key_package, sign_message_bytes) = { + let guard = state().expect("engine state").lock().expect("engine lock"); + let session = guard + .sessions + .get(&start_request.session_id) + .expect("session state"); + + ( + session + .dkg_public_key_package + .clone() + .expect("dkg public key package"), + session + .sign_message_bytes + .clone() + .expect("sign message bytes"), + ) + }; + + let finalize_request = FinalizeSignRoundRequest { + session_id: start_request.session_id, + taproot_merkle_root_hex: None, + attempt_context: None, + round_contributions: vec![ + first_round_state.own_contribution, + second_round_state.own_contribution, + ], + }; + + let result = finalize_sign_round(finalize_request, false).expect("finalize"); + + assert_eq!(result.round_id, first_round_state.round_id); + let signature_bytes = hex::decode(&result.signature_hex).expect("signature decode"); + let signature = frost::Signature::deserialize(&signature_bytes).expect("signature parse"); + dkg_public_key_package + .verifying_key() + .verify(&sign_message_bytes, &signature) + .expect("signature verification"); + } + + #[test] + fn start_sign_round_allows_taproot_threshold_subset_members_for_same_active_round() { + let _guard = lock_test_state(); + reset_for_tests(); + + let participants = (1_u16..=100) + .map(|identifier| crate::api::DkgParticipant { + identifier, + public_key_hex: format!("02{identifier:02x}"), + }) + .collect::>(); + let signing_participants = vec![ + 2, 3, 4, 8, 11, 13, 14, 17, 19, 21, 22, 25, 27, 29, 30, 31, 32, 33, 35, 37, 38, 39, 42, + 44, 45, 48, 50, 51, 52, 53, 57, 58, 60, 61, 63, 64, 65, 67, 68, 73, 76, 77, 80, 81, 84, + 86, 87, 88, 90, 94, 96, + ]; + let taproot_merkle_root_hex = + "37a57b86de2819d2b72a173df46238a7ad295ea1485d3b40e9415daa82b4fdcb"; + + let dkg_result = run_dkg(RunDkgRequest { + session_id: "session-real-taproot-multi-member-process".to_string(), + participants, + threshold: 51, + dkg_seed_hex: None, + }) + .expect("run dkg"); + + let first_request = StartSignRoundRequest { + session_id: "session-real-taproot-multi-member-process".to_string(), + member_identifier: 86, + message_hex: "ac692bb7fddf3f7e1e050a83cf3ffb6e8e69888ce980281aa39da169525750ef" + .to_string(), + key_group: dkg_result.key_group, + taproot_merkle_root_hex: Some(taproot_merkle_root_hex.to_string()), + signing_participants: Some(signing_participants.clone()), + attempt_context: None, + attempt_transition_evidence: None, + }; + + let first_round_state = + start_sign_round(first_request.clone()).expect("first member start sign round"); + assert_eq!(first_round_state.required_contributions, 51); + assert_eq!( + first_round_state.signing_participants.as_deref(), + Some(signing_participants.as_slice()) + ); + + let mut contributions = vec![first_round_state.own_contribution.clone()]; + for member_identifier in [76_u16, 39, 53, 3] { + let round_state = start_sign_round(StartSignRoundRequest { + member_identifier, + ..first_request.clone() + }) + .expect("next member start sign round"); + + assert_eq!(round_state.session_id, first_round_state.session_id); + assert_eq!(round_state.round_id, first_round_state.round_id); + assert_eq!(round_state.required_contributions, 51); + assert_eq!(round_state.own_contribution.identifier, member_identifier); + contributions.push(round_state.own_contribution); + } + + let (dkg_key_packages, dkg_public_key_package, sign_message_bytes) = { + let guard = state().expect("engine state").lock().expect("engine lock"); + let session = guard + .sessions + .get(&first_request.session_id) + .expect("session state"); + + ( + session.dkg_key_packages.clone().expect("dkg key packages"), + session + .dkg_public_key_package + .clone() + .expect("dkg public key package"), + session + .sign_message_bytes + .clone() + .expect("sign message bytes"), + ) + }; + let taproot_merkle_root_bytes = + hex::decode(taproot_merkle_root_hex).expect("taproot merkle root"); + let mut taproot_merkle_root = [0_u8; 32]; + taproot_merkle_root.copy_from_slice(&taproot_merkle_root_bytes); + + for member_identifier in signing_participants + .iter() + .copied() + .filter(|identifier| ![86_u16, 76, 39, 53, 3].contains(identifier)) + .take(46) + { + let member_request = StartSignRoundRequest { + member_identifier, + ..first_request.clone() + }; + contributions.push( + build_real_signature_share_contribution( + &dkg_key_packages, + signing_participants.as_slice(), + &member_request, + &first_round_state.round_id, + &sign_message_bytes, + Some(&taproot_merkle_root), + ) + .expect("additional contribution"), + ); + } + assert_eq!(contributions.len(), 51); + + let result = finalize_sign_round( + FinalizeSignRoundRequest { + session_id: first_request.session_id, + taproot_merkle_root_hex: Some(taproot_merkle_root_hex.to_string()), + attempt_context: None, + round_contributions: contributions, + }, + false, + ) + .expect("finalize"); + + assert_eq!(result.round_id, first_round_state.round_id); + let signature_bytes = hex::decode(&result.signature_hex).expect("signature decode"); + let signature = frost::Signature::deserialize(&signature_bytes).expect("signature parse"); + let tweaked_public_key_package = dkg_public_key_package + .clone() + .tweak(Some(taproot_merkle_root.as_slice())); + tweaked_public_key_package + .verifying_key() + .verify(&sign_message_bytes, &signature) + .expect("tweaked signature verification"); + } + #[test] fn deterministic_round_nonce_and_commitment_is_message_bound() { let _guard = lock_test_state(); @@ -11685,6 +12349,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; run_dkg(run_dkg_request).expect("run dkg"); @@ -11763,6 +12428,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -11771,6 +12437,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: None, attempt_transition_evidence: None, @@ -11802,6 +12469,7 @@ mod tests { &member_two_request, &round_state.round_id, &hex::decode(&member_two_request.message_hex).expect("message decode"), + None, ) .expect("member two contribution"); @@ -11819,6 +12487,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-message-tamper".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ round_state.own_contribution.clone(), @@ -11859,6 +12528,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -11867,6 +12537,7 @@ mod tests { member_identifier: 1, message_hex: "b16b00b5".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -11898,11 +12569,13 @@ mod tests { &member_two_request, &round_state.round_id, &hex::decode(&member_two_request.message_hex).expect("message decode"), + None, ) .expect("member two contribution"); let finalize_request = FinalizeSignRoundRequest { session_id: "session-real-contributor-set-mismatch".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ round_state.own_contribution.clone(), @@ -11953,6 +12626,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -11961,6 +12635,7 @@ mod tests { member_identifier: 1, message_hex: "facefeed".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: None, attempt_transition_evidence: None, @@ -11969,6 +12644,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-real-outside-signing-cohort".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ round_state.own_contribution, @@ -12008,6 +12684,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let mut request_b = request_a.clone(); request_b.participants.push(crate::api::DkgParticipant { @@ -12137,6 +12814,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; run_dkg(request_a.clone()).expect("initial run dkg"); @@ -12155,6 +12833,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let err = run_dkg(request_b).expect_err("expected session cap rejection"); let EngineError::Internal(message) = err else { @@ -12193,6 +12872,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let mut request_b = request_a.clone(); request_b.session_id = "session-secret-entropy-b".to_string(); @@ -12235,6 +12915,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let mut retry_request = request.clone(); retry_request.participants.reverse(); @@ -12456,6 +13137,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -12464,6 +13146,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -12476,6 +13159,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-persisted-idempotency".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -12692,6 +13376,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -12700,6 +13385,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -12758,6 +13444,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -12766,6 +13453,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -12829,6 +13517,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -12840,6 +13529,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -12897,6 +13587,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -12908,6 +13599,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -12964,6 +13656,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; run_dkg(existing_request).expect("seed existing persisted session"); @@ -12980,6 +13673,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; set_persist_fault_injection_for_tests( @@ -13028,6 +13722,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("post-fault recovery run dkg"); @@ -13055,6 +13750,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13079,6 +13775,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -13124,6 +13821,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13147,6 +13845,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -13192,6 +13891,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13215,6 +13915,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context), attempt_transition_evidence: None, @@ -13256,6 +13957,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13264,6 +13966,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -13272,6 +13975,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-consumed-round".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -13305,6 +14009,7 @@ mod tests { let round_only_replay_request = FinalizeSignRoundRequest { session_id: finalize_request.session_id.clone(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -13360,6 +14065,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; run_dkg(existing_request).expect("seed existing persisted session"); @@ -13376,6 +14082,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; set_persist_fault_injection_for_tests( @@ -13441,6 +14148,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13449,6 +14157,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -13473,6 +14182,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-consumed-request-capacity".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -13528,6 +14238,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13544,6 +14255,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(uppercase_attempt_context.clone()), attempt_transition_evidence: None, @@ -13565,6 +14277,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: session_id.to_string(), + taproot_merkle_root_hex: None, attempt_context: Some(uppercase_attempt_context), round_contributions: vec![ RoundContribution { @@ -13615,6 +14328,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13623,6 +14337,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -13647,6 +14362,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-consumed-round-capacity".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -13712,6 +14428,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13722,6 +14439,7 @@ mod tests { member_identifier: 1, message_hex: message_hex.to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: Some(attempt_context.clone()), attempt_transition_evidence: None, @@ -13743,6 +14461,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: session_id.to_string(), + taproot_merkle_root_hex: None, attempt_context: Some(attempt_context), round_contributions: vec![ RoundContribution { @@ -13800,6 +14519,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13808,6 +14528,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -13816,6 +14537,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-consumed-request-fingerprint".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -13836,6 +14558,7 @@ mod tests { }); let expected_request_fingerprint = fingerprint(&FinalizeSignRoundRequest { session_id: finalize_request.session_id.clone(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: canonical_contributions, }) @@ -13904,6 +14627,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13912,6 +14636,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -13920,6 +14645,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-consumed-request-fingerprint-restart".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -13940,6 +14666,7 @@ mod tests { }); let expected_request_fingerprint = fingerprint(&FinalizeSignRoundRequest { session_id: finalize_request.session_id.clone(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: canonical_contributions, }) @@ -14013,6 +14740,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14021,6 +14749,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![3, 1, 2]), attempt_context: None, attempt_transition_evidence: None, @@ -14032,6 +14761,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![2, 3, 1]), attempt_context: None, attempt_transition_evidence: None, @@ -14064,6 +14794,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14072,6 +14803,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![3, 1, 2]), attempt_context: None, attempt_transition_evidence: None, @@ -14083,6 +14815,7 @@ mod tests { member_identifier: 1, message_hex: "cafebabe".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![2, 3, 1]), attempt_context: None, attempt_transition_evidence: None, @@ -14109,6 +14842,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14117,6 +14851,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -14125,6 +14860,7 @@ mod tests { let first_finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-reordered-idempotency".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -14140,6 +14876,7 @@ mod tests { let second_finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-reordered-idempotency".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -14179,6 +14916,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14187,6 +14925,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -14195,6 +14934,7 @@ mod tests { let first_finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-canonicalization-conflict".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -14211,6 +14951,7 @@ mod tests { let second_finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-canonicalization-conflict".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -14328,6 +15069,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(dkg_request.clone()).expect("run dkg"); @@ -14368,6 +15110,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let finalize_dkg_result = run_dkg(finalize_dkg_request).expect("run finalize dkg"); let start_request = StartSignRoundRequest { @@ -14375,6 +15118,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: finalize_dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -14383,6 +15127,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-restart-finalize".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -14435,6 +15180,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("post-restart run dkg"); assert!(!new_session_result.key_group.is_empty()); @@ -14598,6 +15344,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14606,6 +15353,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -14614,6 +15362,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-clears-signing-material".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -14672,6 +15421,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14680,6 +15430,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -14688,6 +15439,7 @@ mod tests { let finalize_request = FinalizeSignRoundRequest { session_id: "session-finalize-purge-persist-reload".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -14775,6 +15527,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed persisted state"); @@ -14854,6 +15607,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed persisted state"); @@ -15011,6 +15765,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed persisted encrypted state"); @@ -15100,6 +15855,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed encrypted state file"); @@ -15138,6 +15894,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed encrypted state file"); @@ -15390,6 +16147,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed encrypted state file"); diff --git a/pkg/tbtc/signer/src/lib.rs b/pkg/tbtc/signer/src/lib.rs index 9835e0d1c1..e292e4f83f 100644 --- a/pkg/tbtc/signer/src/lib.rs +++ b/pkg/tbtc/signer/src/lib.rs @@ -438,6 +438,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let (status_first, first_payload) = call_ffi(&request, frost_tbtc_run_dkg); @@ -448,6 +449,98 @@ mod tests { assert_eq!(first_payload, second_payload); } + #[test] + fn run_dkg_uses_fresh_entropy_for_unseeded_request_after_engine_reset() { + let _guard = crate::engine::lock_test_state(); + crate::engine::reset_for_tests(); + + let request = RunDkgRequest { + session_id: "session-unseeded-entropy".to_string(), + participants: vec![ + DkgParticipant { + identifier: 1, + public_key_hex: "02aa".to_string(), + }, + DkgParticipant { + identifier: 2, + public_key_hex: "02bb".to_string(), + }, + DkgParticipant { + identifier: 3, + public_key_hex: "02cc".to_string(), + }, + ], + threshold: 2, + dkg_seed_hex: None, + }; + + let (status_first, first_payload) = call_ffi(&request, frost_tbtc_run_dkg); + crate::engine::reset_for_tests(); + let (status_second, second_payload) = call_ffi(&request, frost_tbtc_run_dkg); + + assert_eq!(status_first, 0); + assert_eq!(status_second, 0); + + let result_first: crate::api::DkgResult = + serde_json::from_slice(&first_payload).expect("decode first DKG result"); + let result_second: crate::api::DkgResult = + serde_json::from_slice(&second_payload).expect("decode second DKG result"); + + assert_eq!(result_first.session_id, result_second.session_id); + assert_ne!(result_first.key_group, result_second.key_group); + } + + #[test] + fn run_dkg_uses_explicit_seed_across_distinct_sessions() { + let _guard = crate::engine::lock_test_state(); + crate::engine::reset_for_tests(); + + let participants = vec![ + DkgParticipant { + identifier: 1, + public_key_hex: "02aa".to_string(), + }, + DkgParticipant { + identifier: 2, + public_key_hex: "02bb".to_string(), + }, + DkgParticipant { + identifier: 3, + public_key_hex: "02cc".to_string(), + }, + ]; + let dkg_seed_hex = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; + + let request_a = RunDkgRequest { + session_id: "session-seeded-a".to_string(), + participants: participants.clone(), + threshold: 2, + dkg_seed_hex: Some(dkg_seed_hex.to_string()), + }; + let (status_a, payload_a) = call_ffi(&request_a, frost_tbtc_run_dkg); + + crate::engine::reset_for_tests(); + + let request_b = RunDkgRequest { + session_id: "session-seeded-b".to_string(), + participants, + threshold: 2, + dkg_seed_hex: Some(dkg_seed_hex.to_string()), + }; + let (status_b, payload_b) = call_ffi(&request_b, frost_tbtc_run_dkg); + + assert_eq!(status_a, 0); + assert_eq!(status_b, 0); + + let result_a: crate::api::DkgResult = + serde_json::from_slice(&payload_a).expect("decode first DKG result"); + let result_b: crate::api::DkgResult = + serde_json::from_slice(&payload_b).expect("decode second DKG result"); + + assert_ne!(result_a.session_id, result_b.session_id); + assert_eq!(result_a.key_group, result_b.key_group); + } + #[test] fn run_dkg_rejects_conflicting_repeat_request_for_same_session() { let _guard = crate::engine::lock_test_state(); @@ -466,6 +559,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let mut request_b = request_a.clone(); @@ -537,6 +631,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let (dkg_status, _) = call_ffi(&dkg_request, frost_tbtc_run_dkg); assert_eq!(dkg_status, 0); @@ -707,6 +802,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let (dkg_status, dkg_payload) = call_ffi(&dkg_request, frost_tbtc_run_dkg); assert_eq!(dkg_status, 0); @@ -729,6 +825,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -773,6 +870,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let (dkg_status, dkg_payload) = call_ffi(&dkg, frost_tbtc_run_dkg); @@ -786,6 +884,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -799,6 +898,7 @@ mod tests { let finalize = FinalizeSignRoundRequest { session_id: "session-sign".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -849,6 +949,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let (dkg_status, dkg_payload) = call_ffi(&dkg, frost_tbtc_run_dkg); @@ -862,6 +963,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -875,6 +977,7 @@ mod tests { let finalize = FinalizeSignRoundRequest { session_id: "session-sign-bootstrap-disabled".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -916,6 +1019,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let (dkg_status, dkg_payload) = call_ffi(&dkg, frost_tbtc_run_dkg); assert_eq!(dkg_status, 0); @@ -927,6 +1031,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group.clone(), + taproot_merkle_root_hex: None, signing_participants: Some(vec![1, 2]), attempt_context: None, attempt_transition_evidence: None, @@ -939,6 +1044,7 @@ mod tests { member_identifier: 1, message_hex: "cafebabe".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: Some(vec![2, 1]), attempt_context: None, attempt_transition_evidence: None, @@ -972,6 +1078,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let (dkg_status, dkg_payload) = call_ffi(&dkg, frost_tbtc_run_dkg); assert_eq!(dkg_status, 0); @@ -983,6 +1090,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: dkg_result.key_group, + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None, @@ -994,6 +1102,7 @@ mod tests { let finalize = FinalizeSignRoundRequest { session_id: "session-sign-finalized".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -1027,6 +1136,7 @@ mod tests { member_identifier: 1, message_hex: "deadbeef".to_string(), key_group: "missing".to_string(), + taproot_merkle_root_hex: None, signing_participants: None, attempt_context: None, attempt_transition_evidence: None,