From 2e0a054a193553065e806407ef356804c29fbeef Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 5 Jun 2026 00:31:15 -0400 Subject: [PATCH 1/4] Support Taproot tweaked signer rounds --- pkg/tbtc/signer/src/api.rs | 6 + pkg/tbtc/signer/src/engine.rs | 352 +++++++++++++++++++++++++++++++++- pkg/tbtc/signer/src/lib.rs | 10 + 3 files changed, 359 insertions(+), 9 deletions(-) diff --git a/pkg/tbtc/signer/src/api.rs b/pkg/tbtc/signer/src/api.rs index 98441d905a..5ffcf6f71e 100644 --- a/pkg/tbtc/signer/src/api.rs +++ b/pkg/tbtc/signer/src/api.rs @@ -29,6 +29,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 +63,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 +74,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..a0cb3f9669 100644 --- a/pkg/tbtc/signer/src/engine.rs +++ b/pkg/tbtc/signer/src/engine.rs @@ -4250,6 +4250,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 +4342,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 ) @@ -5276,7 +5305,7 @@ fn enforce_bootstrap_dealer_dkg_disabled_in_production( Ok(()) } -pub fn start_sign_round(request: StartSignRoundRequest) -> Result { +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,6 +5323,8 @@ pub fn start_sign_round(request: StartSignRoundRequest) -> Result Result Result Result, ) -> Result { let mut commitments = BTreeMap::new(); let mut own_nonces = None; @@ -5693,8 +5728,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 +5753,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 +5764,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 +5780,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 +5852,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 +5927,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 +6040,19 @@ 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 signature_bytes = signature.serialize().map_err(|e| { EngineError::Internal(format!("failed to serialize aggregate signature: {e}")) })?; @@ -6606,6 +6671,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, @@ -7951,6 +8017,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, }, @@ -8004,6 +8071,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, @@ -8064,6 +8132,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 +8154,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), @@ -8174,6 +8244,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 +8265,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), @@ -8255,6 +8327,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 +8349,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), @@ -8389,6 +8463,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 +8485,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), @@ -8555,6 +8631,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 +8754,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, @@ -8857,6 +8935,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, @@ -8904,6 +8983,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, @@ -8955,6 +9035,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, @@ -8999,6 +9080,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 +9100,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, @@ -9075,6 +9158,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 +9180,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 +9610,7 @@ mod tests { request_session_id, key_group, message_hex, + None, signing_participants_fingerprint, Some(&lowercase_attempt_context), ); @@ -9532,6 +9618,7 @@ mod tests { request_session_id, key_group, message_hex, + None, signing_participants_fingerprint, Some(&uppercase_attempt_context), ); @@ -9545,6 +9632,7 @@ mod tests { request_session_id, key_group, message_hex, + None, signing_participants_fingerprint, Some(&different_attempt_context), ); @@ -9554,6 +9642,7 @@ mod tests { request_session_id, key_group, message_hex, + None, signing_participants_fingerprint, None, ); @@ -9668,6 +9757,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, @@ -9724,6 +9814,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, @@ -9775,6 +9866,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, @@ -9818,6 +9910,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, @@ -9867,6 +9960,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, @@ -9916,6 +10010,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, @@ -9965,6 +10060,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, @@ -10026,6 +10122,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, @@ -10074,6 +10171,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, @@ -10127,6 +10225,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, @@ -10180,6 +10279,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 +10293,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, @@ -10234,6 +10335,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 +10345,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 { @@ -10301,6 +10404,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 +10414,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 { @@ -10364,6 +10469,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 +10481,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 { @@ -10431,6 +10538,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 +10550,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, @@ -10484,6 +10593,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 +10607,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, @@ -10544,6 +10655,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 +10669,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, @@ -10604,6 +10717,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 +10732,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 +10758,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, @@ -10691,6 +10807,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 +10824,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), @@ -10753,6 +10871,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 +10886,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 +10900,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, @@ -10831,6 +10952,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 +10970,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), @@ -10895,6 +11018,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 +11033,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), @@ -10956,6 +11081,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 +11099,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), @@ -11020,6 +11147,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 +11169,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), @@ -11092,6 +11221,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 +11243,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), @@ -11168,6 +11299,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 +11321,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), @@ -11240,6 +11373,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 +11395,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), @@ -11308,6 +11443,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 +11454,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(), @@ -11372,6 +11509,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 +11521,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 +11552,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 +11581,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 { @@ -11489,6 +11630,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 +11672,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 +11686,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 +11715,142 @@ mod tests { .expect("signature verification"); } + #[test] + fn finalize_aggregates_real_taproot_tweaked_contributions() { + use frost::keys::Tweak; + + 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, + }; + + 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 finalize_aggregates_real_threshold_subset_outside_bootstrap_mode() { let _guard = lock_test_state(); @@ -11600,6 +11881,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 +11923,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(), @@ -11771,6 +12055,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 +12087,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 +12105,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(), @@ -11867,6 +12154,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 +12186,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(), @@ -11961,6 +12251,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 +12260,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, @@ -12464,6 +12756,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 +12769,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 { @@ -12700,6 +12994,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, @@ -12766,6 +13061,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, @@ -12840,6 +13136,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, @@ -12908,6 +13205,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, @@ -13079,6 +13377,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, @@ -13147,6 +13446,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, @@ -13215,6 +13515,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, @@ -13264,6 +13565,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 +13574,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 +13608,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 { @@ -13449,6 +13753,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 +13778,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 { @@ -13544,6 +13850,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 +13872,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 { @@ -13623,6 +13931,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 +13956,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 { @@ -13722,6 +14032,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 +14054,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 { @@ -13808,6 +14120,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 +14129,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 +14150,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, }) @@ -13912,6 +14227,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 +14236,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 +14257,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, }) @@ -14021,6 +14339,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 +14351,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, @@ -14072,6 +14392,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 +14404,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, @@ -14117,6 +14439,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 +14448,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 +14464,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 { @@ -14187,6 +14512,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 +14521,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 +14538,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 { @@ -14375,6 +14703,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 +14712,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 { @@ -14606,6 +14936,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 +14945,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 { @@ -14680,6 +15012,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 +15021,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 { diff --git a/pkg/tbtc/signer/src/lib.rs b/pkg/tbtc/signer/src/lib.rs index 9835e0d1c1..020afb8fcc 100644 --- a/pkg/tbtc/signer/src/lib.rs +++ b/pkg/tbtc/signer/src/lib.rs @@ -729,6 +729,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, @@ -786,6 +787,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 +801,7 @@ mod tests { let finalize = FinalizeSignRoundRequest { session_id: "session-sign".to_string(), + taproot_merkle_root_hex: None, attempt_context: None, round_contributions: vec![ RoundContribution { @@ -862,6 +865,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 +879,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 { @@ -927,6 +932,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 +945,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, @@ -983,6 +990,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 +1002,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 +1036,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, From 3a259ecc271fa1e356b7d9c4bdc498e8908e7cf4 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 5 Jun 2026 11:44:46 -0400 Subject: [PATCH 2/4] Support seeded tbtc-signer DKG --- pkg/tbtc/signer/src/api.rs | 2 + pkg/tbtc/signer/src/engine.rs | 137 +++++++++++++++++++++++++++++++++- pkg/tbtc/signer/src/lib.rs | 93 +++++++++++++++++++++++ 3 files changed, 230 insertions(+), 2 deletions(-) diff --git a/pkg/tbtc/signer/src/api.rs b/pkg/tbtc/signer/src/api.rs index 5ffcf6f71e..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)] diff --git a/pkg/tbtc/signer/src/engine.rs b/pkg/tbtc/signer/src/engine.rs index a0cb3f9669..51015aa7bb 100644 --- a/pkg/tbtc/signer/src/engine.rs +++ b/pkg/tbtc/signer/src/engine.rs @@ -5194,8 +5194,8 @@ 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(), &request_fingerprint)?; let keygen_rng = ZeroizingChaCha20Rng::from_seed(keygen_rng_seed); keygen_rng_seed.zeroize(); @@ -5305,6 +5305,30 @@ fn enforce_bootstrap_dealer_dkg_disabled_in_production( Ok(()) } +fn development_dealer_dkg_seed( + dkg_seed_hex: Option<&str>, + request_fingerprint: &str, +) -> Result<[u8; 32], EngineError> { + let (seed_source, seed_hex) = match dkg_seed_hex { + Some(seed) => ("DKG seed", seed), + None => ("DKG request fingerprint", request_fingerprint), + }; + + let seed = hex::decode(seed_hex) + .map_err(|e| EngineError::Internal(format!("failed to decode {seed_source}: {e}")))?; + if seed.len() != 32 { + return Err(EngineError::Internal(format!( + "{seed_source} 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 = @@ -6662,6 +6686,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -6887,6 +6912,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("production profile should reject bootstrap dealer DKG"); @@ -6924,6 +6950,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("missing/empty profile should reject bootstrap dealer DKG"); @@ -6974,6 +7001,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected provenance gate rejection"); @@ -7044,6 +7072,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }); assert!(result.is_ok(), "expected signed attestation acceptance"); @@ -7087,6 +7116,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected missing signature rejection"); @@ -7149,6 +7179,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected signature verification rejection"); @@ -7202,6 +7233,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected attestation expiry rejection"); @@ -7255,6 +7287,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected attestation missing expiry rejection"); @@ -7311,6 +7344,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected attestation expiry too far rejection"); @@ -7373,6 +7407,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected trust-root mismatch rejection"); @@ -7426,6 +7461,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected runtime version mismatch rejection"); @@ -7479,6 +7515,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected status mismatch rejection"); @@ -7525,6 +7562,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected invalid trust root rejection"); @@ -7572,6 +7610,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected session_id validation rejection"); @@ -7608,6 +7647,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected admission policy rejection"); @@ -7641,6 +7681,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected admission policy config rejection"); @@ -7679,6 +7720,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected admission policy config rejection"); @@ -7988,6 +8030,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected run_dkg provenance gate rejection"); assert!(matches!( @@ -8063,6 +8106,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8122,6 +8166,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8234,6 +8279,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8317,6 +8363,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8379,6 +8426,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected auto-quarantine rejection"); let EngineError::QuarantinePolicyRejected { reason_code, .. } = err else { @@ -8410,6 +8458,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("allowlisted operator should bypass quarantine rejection"); @@ -8453,6 +8502,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8521,6 +8571,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect_err("expected quarantine rejection after reload"); let EngineError::QuarantinePolicyRejected { reason_code, .. } = err else { @@ -8555,6 +8606,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8927,6 +8979,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -8973,6 +9026,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9024,6 +9078,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9070,6 +9125,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9148,6 +9204,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9749,6 +9806,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9800,6 +9858,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed non-production dkg"); @@ -9856,6 +9915,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9898,6 +9958,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9948,6 +10009,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -9998,6 +10060,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10048,6 +10111,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10098,6 +10162,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10160,6 +10225,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10209,6 +10275,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10263,6 +10330,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10325,6 +10393,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10394,6 +10463,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10459,6 +10529,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10528,6 +10599,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10583,6 +10655,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10645,6 +10718,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10707,6 +10781,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10797,6 +10872,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10861,6 +10937,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -10942,6 +11019,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11008,6 +11086,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11071,6 +11150,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11137,6 +11217,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11211,6 +11292,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11289,6 +11371,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11363,6 +11446,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11433,6 +11517,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11499,6 +11584,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("run dkg"); @@ -11622,6 +11708,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -11739,6 +11826,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let taproot_merkle_root_hex = @@ -11873,6 +11961,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -11969,6 +12058,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; run_dkg(run_dkg_request).expect("run dkg"); @@ -12047,6 +12137,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -12146,6 +12237,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -12243,6 +12335,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -12300,6 +12393,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let mut request_b = request_a.clone(); request_b.participants.push(crate::api::DkgParticipant { @@ -12429,6 +12523,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; run_dkg(request_a.clone()).expect("initial run dkg"); @@ -12447,6 +12542,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 { @@ -12485,6 +12581,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(); @@ -12527,6 +12624,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let mut retry_request = request.clone(); retry_request.participants.reverse(); @@ -12748,6 +12846,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -12986,6 +13085,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13053,6 +13153,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13125,6 +13226,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13194,6 +13296,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13262,6 +13365,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; run_dkg(existing_request).expect("seed existing persisted session"); @@ -13278,6 +13382,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; set_persist_fault_injection_for_tests( @@ -13326,6 +13431,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("post-fault recovery run dkg"); @@ -13353,6 +13459,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13423,6 +13530,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13492,6 +13600,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13557,6 +13666,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13664,6 +13774,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; run_dkg(existing_request).expect("seed existing persisted session"); @@ -13680,6 +13791,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; set_persist_fault_injection_for_tests( @@ -13745,6 +13857,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13834,6 +13947,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -13923,6 +14037,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14022,6 +14137,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14112,6 +14228,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14219,6 +14336,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14331,6 +14449,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14384,6 +14503,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14431,6 +14551,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14504,6 +14625,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -14656,6 +14778,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(dkg_request.clone()).expect("run dkg"); @@ -14696,6 +14819,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 { @@ -14765,6 +14889,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("post-restart run dkg"); assert!(!new_session_result.key_group.is_empty()); @@ -14928,6 +15053,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -15004,6 +15130,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let dkg_result = run_dkg(run_dkg_request).expect("run dkg"); @@ -15109,6 +15236,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed persisted state"); @@ -15188,6 +15316,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed persisted state"); @@ -15345,6 +15474,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed persisted encrypted state"); @@ -15434,6 +15564,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed encrypted state file"); @@ -15472,6 +15603,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }) .expect("seed encrypted state file"); @@ -15724,6 +15856,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 020afb8fcc..a403d84fb4 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,91 @@ mod tests { assert_eq!(first_payload, second_payload); } + #[test] + fn run_dkg_is_deterministic_for_identical_request_after_engine_reset() { + let _guard = crate::engine::lock_test_state(); + crate::engine::reset_for_tests(); + + let request = RunDkgRequest { + session_id: "session-deterministic".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); + assert_eq!(first_payload, second_payload); + } + + #[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 +552,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let mut request_b = request_a.clone(); @@ -537,6 +624,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 +795,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); @@ -774,6 +863,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let (dkg_status, dkg_payload) = call_ffi(&dkg, frost_tbtc_run_dkg); @@ -852,6 +942,7 @@ mod tests { }, ], threshold: 2, + dkg_seed_hex: None, }; let (dkg_status, dkg_payload) = call_ffi(&dkg, frost_tbtc_run_dkg); @@ -921,6 +1012,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); @@ -979,6 +1071,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); From 4c9c6547cc5b72c66cf54aa82bb79f9fd544af7c Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 5 Jun 2026 17:03:57 -0400 Subject: [PATCH 3/4] Reuse signer rounds across member identifiers --- pkg/tbtc/signer/src/engine.rs | 673 +++++++++++++++++++++++----------- 1 file changed, 462 insertions(+), 211 deletions(-) diff --git a/pkg/tbtc/signer/src/engine.rs b/pkg/tbtc/signer/src/engine.rs index 51015aa7bb..d4d51441e3 100644 --- a/pkg/tbtc/signer/src/engine.rs +++ b/pkg/tbtc/signer/src/engine.rs @@ -5353,6 +5353,7 @@ pub fn start_sign_round(mut request: StartSignRoundRequest) -> Result Result Result>(); + 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(); From 8f5aec7abd2130bc566f95dd47858f4e19ae540b Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 5 Jun 2026 23:42:20 -0400 Subject: [PATCH 4/4] Harden Taproot signer aggregation --- pkg/tbtc/signer/src/engine.rs | 70 +++++++++++++++++++++++++++-------- pkg/tbtc/signer/src/lib.rs | 13 +++++-- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/pkg/tbtc/signer/src/engine.rs b/pkg/tbtc/signer/src/engine.rs index d4d51441e3..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; @@ -5194,8 +5195,7 @@ pub fn run_dkg(request: RunDkgRequest) -> Result { .map(|identifier| participant_identifier_to_frost_identifier(*identifier)) .collect::, _>>()?; - let mut keygen_rng_seed = - development_dealer_dkg_seed(request.dkg_seed_hex.as_deref(), &request_fingerprint)?; + 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(); @@ -5305,20 +5305,18 @@ fn enforce_bootstrap_dealer_dkg_disabled_in_production( Ok(()) } -fn development_dealer_dkg_seed( - dkg_seed_hex: Option<&str>, - request_fingerprint: &str, -) -> Result<[u8; 32], EngineError> { - let (seed_source, seed_hex) = match dkg_seed_hex { - Some(seed) => ("DKG seed", seed), - None => ("DKG request fingerprint", request_fingerprint), +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 {seed_source}: {e}")))?; + .map_err(|e| EngineError::Internal(format!("failed to decode DKG seed: {e}")))?; if seed.len() != 32 { return Err(EngineError::Internal(format!( - "{seed_source} decoded to [{}] bytes, expected 32", + "DKG seed decoded to [{}] bytes, expected 32", seed.len() ))); } @@ -6101,6 +6099,24 @@ pub fn finalize_sign_round( .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}")) })?; @@ -11828,8 +11844,6 @@ mod tests { #[test] fn finalize_aggregates_real_taproot_tweaked_contributions() { - use frost::keys::Tweak; - let _guard = lock_test_state(); reset_for_tests(); @@ -11963,6 +11977,34 @@ mod tests { ); } + #[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(); @@ -12158,8 +12200,6 @@ mod tests { #[test] fn start_sign_round_allows_taproot_threshold_subset_members_for_same_active_round() { - use frost::keys::Tweak; - let _guard = lock_test_state(); reset_for_tests(); diff --git a/pkg/tbtc/signer/src/lib.rs b/pkg/tbtc/signer/src/lib.rs index a403d84fb4..e292e4f83f 100644 --- a/pkg/tbtc/signer/src/lib.rs +++ b/pkg/tbtc/signer/src/lib.rs @@ -450,12 +450,12 @@ mod tests { } #[test] - fn run_dkg_is_deterministic_for_identical_request_after_engine_reset() { + 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-deterministic".to_string(), + session_id: "session-unseeded-entropy".to_string(), participants: vec![ DkgParticipant { identifier: 1, @@ -480,7 +480,14 @@ mod tests { assert_eq!(status_first, 0); assert_eq!(status_second, 0); - assert_eq!(first_payload, second_payload); + + 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]