Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pkg/tbtc/signer/include/frost_tbtc.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ TbtcSignerResult frost_tbtc_interactive_session_open(const uint8_t* request_ptr,
TbtcSignerResult frost_tbtc_interactive_round1(const uint8_t* request_ptr, size_t request_len);
TbtcSignerResult frost_tbtc_interactive_round2(const uint8_t* request_ptr, size_t request_len);
TbtcSignerResult frost_tbtc_interactive_session_abort(const uint8_t* request_ptr, size_t request_len);
/*
* Coordinator-side aggregation: verifies each collected signature share
* against its verifying share (resolved from the session's DKG state) and
* returns the aggregated BIP-340 signature. Operates on public material
* only - no secret crosses here. On any verification failure it fails
* closed with the generic `validation_error` code and returns no
* signature; per-member attributable blame (a structured culprit list)
* is intentionally NOT emitted yet - it requires the signed-package
* envelope binding added in Phase 7.2b, without which the attribution
* would be forgeable by a coordinator using a mismatched package/root.
*/
TbtcSignerResult frost_tbtc_interactive_aggregate(const uint8_t* request_ptr, size_t request_len);

#ifdef __cplusplus
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/tbtc/signer/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,33 @@ pub struct InteractiveRound2Result {
pub signature_share_hex: String,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveAggregateRequest {
pub session_id: String,
pub attempt_id: String,
/// The signing package the shares were produced over (carries the
/// message and the chosen subset's commitments).
pub signing_package_hex: String,
/// The collected signature shares from the responsive subset. Each
/// is verified against the member's verifying share (resolved from
/// the session's DKG public key package) before aggregation; an
/// invalid share fails the call closed with `validation_error` and
/// no signature. Per-member attributable blame (a culprit list) is
/// deferred to Phase 7.2b, where the signed-package envelopes bind
/// what each member signed and make the attribution unforgeable.
pub signature_shares: Vec<NativeFrostSignatureShare>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub taproot_merkle_root_hex: Option<String>,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveAggregateResult {
pub session_id: String,
pub attempt_id: String,
/// The aggregated BIP-340 Schnorr signature, hex-encoded.
pub signature_hex: String,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveSessionAbortRequest {
pub session_id: String,
Expand Down Expand Up @@ -619,13 +646,21 @@ pub struct SignerHardeningMetricsResult {
#[serde(default)]
pub interactive_session_abort_success_total: u64,
#[serde(default)]
pub interactive_aggregate_calls_total: u64,
#[serde(default)]
pub interactive_aggregate_success_total: u64,
#[serde(default)]
pub interactive_round1_latency_p95_ms: u64,
#[serde(default)]
pub interactive_round1_latency_samples: u64,
#[serde(default)]
pub interactive_round2_latency_p95_ms: u64,
#[serde(default)]
pub interactive_round2_latency_samples: u64,
#[serde(default)]
pub interactive_aggregate_latency_p95_ms: u64,
#[serde(default)]
pub interactive_aggregate_latency_samples: u64,
pub last_updated_unix: u64,
}

Expand Down
128 changes: 128 additions & 0 deletions pkg/tbtc/signer/src/engine/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,134 @@ pub fn interactive_round2(
})
}

pub fn interactive_aggregate(
request: InteractiveAggregateRequest,
) -> Result<InteractiveAggregateResult, EngineError> {
record_hardening_telemetry(|telemetry| {
telemetry.interactive_aggregate_calls_total = telemetry
.interactive_aggregate_calls_total
.saturating_add(1);
});
let _latency_guard =
HardeningOperationLatencyGuard::new(HardeningOperation::InteractiveAggregate);
enforce_provenance_gate()?;
validate_session_id(&request.session_id)?;
let attempt_id = canonical_attempt_id(&request.attempt_id);

let mut signing_package_bytes = decode_hex_field(
"InteractiveAggregate",
"signing_package_hex",
&request.signing_package_hex,
)?;
let signing_package_result = frost::SigningPackage::deserialize(&signing_package_bytes);
signing_package_bytes.zeroize();
let signing_package = signing_package_result.map_err(|e| {
EngineError::Validation(format!(
"InteractiveAggregate: invalid signing package: {e}"
))
})?;
let signature_shares =
decode_signature_share_map("InteractiveAggregate", &request.signature_shares)?;
let mut taproot_merkle_root_hex = request.taproot_merkle_root_hex.clone();
let taproot_merkle_root = canonicalize_taproot_merkle_root_hex(&mut taproot_merkle_root_hex)?;

let mut guard = state()?
.lock()
.map_err(|_| EngineError::Internal("engine lock poisoned".to_string()))?;
// Aggregate takes the engine lock like every other interactive entry
// point, so it sweeps expired interactive state too: the TTL
// guarantee (a nonce handle gone within the TTL of inactivity) must
// hold even when the only post-expiry traffic is aggregate calls.
sweep_expired_interactive_state(&mut guard);

// Resolve the group's public key package (the verifying shares used
// to check each contribution) from the session's own DKG state, not
// the request - consistent with the no-secret-on-the-FFI discipline
// and so a caller cannot substitute verifying material. The session
// must exist with completed DKG.
let public_key_package = {
let session = guard.sessions.get(&request.session_id).ok_or_else(|| {
EngineError::SessionNotFound {
session_id: request.session_id.clone(),
}
})?;
if session.dkg_result.is_none() {
return Err(EngineError::DkgNotReady {
session_id: request.session_id.clone(),
});
}
session
.dkg_public_key_package
.as_ref()
.ok_or_else(|| {
EngineError::Internal("missing DKG public key package cache".to_string())
})?
.clone()
};
drop(guard);

// Aggregation uses only public material (commitments, shares,
// verifying shares), so no policy gate runs here - the secret-bearing
// step is each signer's Round2, where lifecycle/quarantine/firewall
// were already enforced (including the full-subset quarantine check).
//
// frost verifies every share and can name which failed, but this path
// does NOT surface those as attributable member blame: the engine
// cannot yet bind these public inputs (signing package, taproot root)
// to what each member actually signed at Round2, so a coordinator
// aggregating against a different package/root would make honest
// shares fail and frame their members. Attributable blame waits for
// the signed-package envelopes (Phase 7.2b, frozen spec section 6),
// which prove what each member signed. Until then a verification
// failure is a generic fail-closed error: no signature, no blame.
let verification_key_package = match taproot_merkle_root.as_ref() {
Some(root) => public_key_package.clone().tweak(Some(root.as_slice())),
None => public_key_package.clone(),
};

let aggregate_result = match taproot_merkle_root.as_ref() {
Some(root) => frost::aggregate_with_tweak(
&signing_package,
&signature_shares,
&public_key_package,
Some(root.as_slice()),
),
None => frost::aggregate(&signing_package, &signature_shares, &public_key_package),
};
let signature = aggregate_result.map_err(|error| {
EngineError::Validation(format!(
"InteractiveAggregate: failed to aggregate: {error}"
))
})?;

// Self-verify the aggregate against the (tweaked) group verifying
// key before releasing it, matching the coarse finalize path.
verification_key_package
.verifying_key()
.verify(signing_package.message().as_slice(), &signature)
.map_err(|e| {
EngineError::Validation(format!(
"InteractiveAggregate: aggregate signature failed self-verification: {e}"
))
})?;

let signature_bytes = signature
.serialize()
.map_err(|e| EngineError::Internal(format!("failed to serialize aggregate: {e}")))?;

record_hardening_telemetry(|telemetry| {
telemetry.interactive_aggregate_success_total = telemetry
.interactive_aggregate_success_total
.saturating_add(1);
});

Ok(InteractiveAggregateResult {
session_id: request.session_id,
attempt_id,
signature_hex: hex::encode(signature_bytes),
})
}

pub fn interactive_session_abort(
request: InteractiveSessionAbortRequest,
) -> Result<InteractiveSessionAbortResult, EngineError> {
Expand Down
25 changes: 13 additions & 12 deletions pkg/tbtc/signer/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,19 @@ use crate::api::{
DkgPart2Request, DkgPart2Result, DkgPart3Request, DkgPart3Result, DkgResult, DkgRound1Package,
DkgRound2Package, FinalizeSignRoundRequest, GenerateNoncesAndCommitmentsRequest,
GenerateNoncesAndCommitmentsResult, InitSignerConfigRequest, InitSignerConfigResult,
InteractiveRound1Request, InteractiveRound1Result, InteractiveRound2Request,
InteractiveRound2Result, InteractiveSessionAbortRequest, InteractiveSessionAbortResult,
InteractiveSessionOpenRequest, InteractiveSessionOpenResult, NativeFrostCommitment,
NativeFrostKeyPackage, NativeFrostPublicKeyPackage, NativeFrostSignatureShare,
NewSigningPackageRequest, NewSigningPackageResult, PromoteCanaryRequest, PromoteCanaryResult,
QuarantineStatusRequest, QuarantineStatusResult, RefreshCadenceStatusRequest,
RefreshCadenceStatusResult, RefreshSharesRequest, RefreshSharesResult,
RoastLivenessPolicyResult, RollbackCanaryRequest, RollbackCanaryResult, RoundContribution,
RoundState, RunDkgRequest, ShareMaterial, SignShareRequest, SignShareResult, SignatureResult,
SignerHardeningMetricsResult, StartSignRoundRequest, TransactionResult, TranscriptAuditRecord,
TranscriptAuditRequest, TranscriptAuditResult, TriggerEmergencyRekeyRequest,
TriggerEmergencyRekeyResult, VerifyBlameProofRequest,
InteractiveAggregateRequest, InteractiveAggregateResult, InteractiveRound1Request,
InteractiveRound1Result, InteractiveRound2Request, InteractiveRound2Result,
InteractiveSessionAbortRequest, InteractiveSessionAbortResult, InteractiveSessionOpenRequest,
InteractiveSessionOpenResult, NativeFrostCommitment, NativeFrostKeyPackage,
NativeFrostPublicKeyPackage, NativeFrostSignatureShare, NewSigningPackageRequest,
NewSigningPackageResult, PromoteCanaryRequest, PromoteCanaryResult, QuarantineStatusRequest,
QuarantineStatusResult, RefreshCadenceStatusRequest, RefreshCadenceStatusResult,
RefreshSharesRequest, RefreshSharesResult, RoastLivenessPolicyResult, RollbackCanaryRequest,
RollbackCanaryResult, RoundContribution, RoundState, RunDkgRequest, ShareMaterial,
SignShareRequest, SignShareResult, SignatureResult, SignerHardeningMetricsResult,
StartSignRoundRequest, TransactionResult, TranscriptAuditRecord, TranscriptAuditRequest,
TranscriptAuditResult, TriggerEmergencyRekeyRequest, TriggerEmergencyRekeyResult,
VerifyBlameProofRequest,
};
use crate::errors::EngineError;
use crate::go_math_rand::select_coordinator_identifier;
Expand Down
22 changes: 20 additions & 2 deletions pkg/tbtc/signer/src/engine/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,16 @@ pub(crate) struct HardeningTelemetryState {
pub(crate) interactive_round2_success_total: u64,
pub(crate) interactive_session_abort_calls_total: u64,
pub(crate) interactive_session_abort_success_total: u64,
pub(crate) interactive_aggregate_calls_total: u64,
pub(crate) interactive_aggregate_success_total: u64,
pub(crate) run_dkg_latency: HardeningLatencyTracker,
pub(crate) start_sign_round_latency: HardeningLatencyTracker,
pub(crate) build_taproot_tx_latency: HardeningLatencyTracker,
pub(crate) finalize_sign_round_latency: HardeningLatencyTracker,
pub(crate) refresh_shares_latency: HardeningLatencyTracker,
pub(crate) interactive_round1_latency: HardeningLatencyTracker,
pub(crate) interactive_round2_latency: HardeningLatencyTracker,
pub(crate) interactive_aggregate_latency: HardeningLatencyTracker,
pub(crate) last_updated_unix: u64,
}

Expand All @@ -87,10 +90,11 @@ pub(crate) enum HardeningOperation {
FinalizeSignRound,
RefreshShares,
// Interactive Open/Abort are O(1) registry mutations and record
// call/success counters only; the two cryptographic rounds get
// latency tracking.
// call/success counters only; the cryptographic rounds and the
// aggregation get latency tracking.
InteractiveRound1,
InteractiveRound2,
InteractiveAggregate,
}

pub(crate) struct HardeningOperationLatencyGuard {
Expand Down Expand Up @@ -155,6 +159,9 @@ pub(crate) fn record_hardening_operation_latency(operation: HardeningOperation,
HardeningOperation::InteractiveRound2 => {
telemetry.interactive_round2_latency.record(duration_ms)
}
HardeningOperation::InteractiveAggregate => {
telemetry.interactive_aggregate_latency.record(duration_ms)
}
});
}

Expand Down Expand Up @@ -209,10 +216,14 @@ pub fn hardening_metrics() -> SignerHardeningMetricsResult {
interactive_round2_success_total: 0,
interactive_session_abort_calls_total: 0,
interactive_session_abort_success_total: 0,
interactive_aggregate_calls_total: 0,
interactive_aggregate_success_total: 0,
interactive_round1_latency_p95_ms: 0,
interactive_round1_latency_samples: 0,
interactive_round2_latency_p95_ms: 0,
interactive_round2_latency_samples: 0,
interactive_aggregate_latency_p95_ms: 0,
interactive_aggregate_latency_samples: 0,
last_updated_unix: 0,
};

Expand Down Expand Up @@ -274,6 +285,9 @@ pub fn hardening_metrics() -> SignerHardeningMetricsResult {
telemetry.interactive_session_abort_calls_total;
result.interactive_session_abort_success_total =
telemetry.interactive_session_abort_success_total;
result.interactive_aggregate_calls_total = telemetry.interactive_aggregate_calls_total;
result.interactive_aggregate_success_total =
telemetry.interactive_aggregate_success_total;
result.interactive_round1_latency_p95_ms =
telemetry.interactive_round1_latency.p95_ms();
result.interactive_round1_latency_samples =
Expand All @@ -282,6 +296,10 @@ pub fn hardening_metrics() -> SignerHardeningMetricsResult {
telemetry.interactive_round2_latency.p95_ms();
result.interactive_round2_latency_samples =
telemetry.interactive_round2_latency.sample_count();
result.interactive_aggregate_latency_p95_ms =
telemetry.interactive_aggregate_latency.p95_ms();
result.interactive_aggregate_latency_samples =
telemetry.interactive_aggregate_latency.sample_count();
result.last_updated_unix = telemetry.last_updated_unix;
}
Err(error) => {
Expand Down
Loading
Loading