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
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ typedef TbtcSignerResult (*tbtc_interactive_session_abort_fn)(
const uint8_t* request_ptr,
size_t request_len
);
typedef TbtcSignerResult (*tbtc_interactive_aggregate_fn)(
const uint8_t* request_ptr,
size_t request_len
);
typedef TbtcSignerResult (*tbtc_start_sign_round_fn)(
const uint8_t* request_ptr,
size_t request_len
Expand Down Expand Up @@ -269,6 +273,18 @@ static TbtcSignerResult tbtc_signer_interactive_session_abort(const uint8_t* req
return interactive_session_abort(request_ptr, request_len);
}

static TbtcSignerResult tbtc_signer_interactive_aggregate(const uint8_t* request_ptr, size_t request_len) {
tbtc_interactive_aggregate_fn interactive_aggregate = (tbtc_interactive_aggregate_fn)dlsym(
RTLD_DEFAULT,
"frost_tbtc_interactive_aggregate"
);
if (interactive_aggregate == NULL) {
return unavailable_tbtc_signer_result();
}

return interactive_aggregate(request_ptr, request_len);
}

static TbtcSignerResult tbtc_signer_start_sign_round(const uint8_t* request_ptr, size_t request_len) {
tbtc_start_sign_round_fn start_sign_round = (tbtc_start_sign_round_fn)dlsym(
RTLD_DEFAULT,
Expand Down Expand Up @@ -3085,3 +3101,150 @@ func callBuildTaggedTBTCSignerInteractiveSessionAbort(requestPayload []byte) ([]
},
)
}

// ----------------------------------------------------------------------------
// Phase 7.3 interactive aggregation bridge.
//
// Aggregates the responsive subset's signature shares for an interactive
// attempt into the BIP-340 signature. The engine resolves the verifying
// material from the session's own DKG state (no public key package crosses
// here). On a share-verification failure it returns the candidate culprits in
// the error payload; InteractiveAggregate surfaces them as a typed
// InteractiveAggregateShareVerificationError for the Go host's envelope-bound
// blame adjudication. Additive: no Go caller yet.
// ----------------------------------------------------------------------------

type buildTaggedTBTCSignerInteractiveAggregateRequest struct {
SessionID string `json:"session_id"`
AttemptID string `json:"attempt_id"`
SigningPackageHex string `json:"signing_package_hex"`
SignatureShares []buildTaggedTBTCSignerNativeFROSTSignatureShare `json:"signature_shares"`
TaprootMerkleRootHex *string `json:"taproot_merkle_root_hex,omitempty"`
}

type buildTaggedTBTCSignerInteractiveAggregateResponse struct {
SessionID string `json:"session_id"`
AttemptID string `json:"attempt_id"`
SignatureHex string `json:"signature_hex"`
}

func (bttse *buildTaggedTBTCSignerEngine) InteractiveAggregate(
sessionID string,
attemptID string,
signingPackage []byte,
signatureShares []nativeFROSTSignatureShare,
taprootMerkleRoot *[32]byte,
) (signature []byte, err error) {
requestPayload, err := buildTaggedTBTCSignerInteractiveAggregateRequestPayload(
sessionID,
attemptID,
signingPackage,
signatureShares,
taprootMerkleRoot,
)
if err != nil {
return nil, err
}

responsePayload, err := callBuildTaggedTBTCSignerInteractiveAggregate(requestPayload)
if err != nil {
// Surface a share-verification failure as the typed error carrying the
// candidate culprits; any other error passes through unchanged.
return nil, interpretInteractiveAggregateError(sessionID, attemptID, err)
}

return decodeBuildTaggedTBTCSignerInteractiveAggregateResponse(responsePayload)
}

func buildTaggedTBTCSignerInteractiveAggregateRequestPayload(
sessionID string,
attemptID string,
signingPackage []byte,
signatureShares []nativeFROSTSignatureShare,
taprootMerkleRoot *[32]byte,
) ([]byte, error) {
if sessionID == "" {
return nil, buildTaggedTBTCSignerOperationError("InteractiveAggregate", "session ID is empty")
}
if attemptID == "" {
return nil, buildTaggedTBTCSignerOperationError("InteractiveAggregate", "attempt ID is empty")
}
if len(signingPackage) == 0 {
return nil, buildTaggedTBTCSignerOperationError("InteractiveAggregate", "signing package is empty")
}
if len(signatureShares) == 0 {
return nil, buildTaggedTBTCSignerOperationError("InteractiveAggregate", "signature shares are empty")
}

requestShares := make(
[]buildTaggedTBTCSignerNativeFROSTSignatureShare,
0,
len(signatureShares),
)
for i, signatureShare := range signatureShares {
if signatureShare.Identifier == "" {
return nil, buildTaggedTBTCSignerOperationError(
"InteractiveAggregate",
fmt.Sprintf("signature share [%d] identifier is empty", i),
)
}
if len(signatureShare.Data) == 0 {
return nil, buildTaggedTBTCSignerOperationError(
"InteractiveAggregate",
fmt.Sprintf("signature share [%d] data is empty", i),
)
}
requestShares = append(
requestShares,
buildTaggedTBTCSignerNativeFROSTSignatureShare{
Identifier: signatureShare.Identifier,
DataHex: hex.EncodeToString(signatureShare.Data),
},
)
}

var taprootMerkleRootHex *string
if taprootMerkleRoot != nil {
encoded := hex.EncodeToString(taprootMerkleRoot[:])
taprootMerkleRootHex = &encoded
}

return buildTaggedTBTCSignerMarshalRequest(
"InteractiveAggregate",
buildTaggedTBTCSignerInteractiveAggregateRequest{
SessionID: sessionID,
AttemptID: attemptID,
SigningPackageHex: hex.EncodeToString(signingPackage),
SignatureShares: requestShares,
TaprootMerkleRootHex: taprootMerkleRootHex,
},
)
}

func decodeBuildTaggedTBTCSignerInteractiveAggregateResponse(
responsePayload []byte,
) ([]byte, error) {
var response buildTaggedTBTCSignerInteractiveAggregateResponse
if err := json.Unmarshal(responsePayload, &response); err != nil {
return nil, buildTaggedTBTCSignerOperationError(
"InteractiveAggregate",
fmt.Sprintf("cannot decode response payload: %v", err),
)
}

return buildTaggedTBTCSignerDecodeHexField(
"InteractiveAggregate",
"response signature",
response.SignatureHex,
)
}

func callBuildTaggedTBTCSignerInteractiveAggregate(requestPayload []byte) ([]byte, error) {
return callBuildTaggedTBTCSignerOperation(
"InteractiveAggregate",
requestPayload,
func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult {
return C.tbtc_signer_interactive_aggregate(requestPtr, requestLen)
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -1796,3 +1796,143 @@ func TestDecodeBuildTaggedTBTCSignerInteractiveResponses_RejectMalformed(t *test
t.Fatal("open: expected a response missing session/attempt ids to be rejected")
}
}

func TestBuildTaggedTBTCSignerInteractiveAggregateRequestPayload(t *testing.T) {
shares := []nativeFROSTSignatureShare{
{Identifier: "id-1", Data: []byte{0xaa}},
{Identifier: "id-2", Data: []byte{0xbb}},
}

payload, err := buildTaggedTBTCSignerInteractiveAggregateRequestPayload(
"session-1", "attempt-1", []byte{0xde, 0xad}, shares, nil,
)
if err != nil {
t.Fatalf("unexpected payload build error: [%v]", err)
}

var request buildTaggedTBTCSignerInteractiveAggregateRequest
if err := json.Unmarshal(payload, &request); err != nil {
t.Fatalf("cannot decode request payload: [%v]", err)
}
if request.SessionID != "session-1" || request.AttemptID != "attempt-1" {
t.Fatalf("unexpected session/attempt: [%+v]", request)
}
if request.SigningPackageHex != "dead" {
t.Fatalf("unexpected signing package hex: [%s]", request.SigningPackageHex)
}
if len(request.SignatureShares) != 2 ||
request.SignatureShares[0].Identifier != "id-1" ||
request.SignatureShares[0].DataHex != "aa" ||
request.SignatureShares[1].DataHex != "bb" {
t.Fatalf("unexpected signature shares: [%+v]", request.SignatureShares)
}

rejections := map[string]struct {
sessionID string
attemptID string
pkg []byte
shares []nativeFROSTSignatureShare
}{
"empty session": {"", "a", []byte{0x1}, shares},
"empty attempt": {"s", "", []byte{0x1}, shares},
"empty signing package": {"s", "a", nil, shares},
"no shares": {"s", "a", []byte{0x1}, nil},
"share missing data": {"s", "a", []byte{0x1}, []nativeFROSTSignatureShare{{Identifier: "id-1"}}},
}
for name, r := range rejections {
t.Run(name, func(t *testing.T) {
if _, err := buildTaggedTBTCSignerInteractiveAggregateRequestPayload(
r.sessionID, r.attemptID, r.pkg, r.shares, nil,
); err == nil {
t.Fatal("expected invalid input to be rejected")
}
})
}
}

func TestDecodeBuildTaggedTBTCSignerInteractiveAggregateResponse(t *testing.T) {
signature, err := decodeBuildTaggedTBTCSignerInteractiveAggregateResponse(
[]byte(`{"session_id":"s","attempt_id":"a","signature_hex":"cafe"}`),
)
if err != nil {
t.Fatalf("unexpected decode error: [%v]", err)
}
if hex.EncodeToString(signature) != "cafe" {
t.Fatalf("unexpected signature: [%x]", signature)
}

if _, err := decodeBuildTaggedTBTCSignerInteractiveAggregateResponse([]byte("not json")); err == nil {
t.Fatal("expected malformed JSON to be rejected")
}
}

// The aggregate_share_verification_failed error must surface as the typed error
// carrying the candidate culprits (so the Go host can adjudicate envelope-bound
// blame over them), with the session/attempt filled from the caller's request.
func TestInterpretInteractiveAggregateError_ShareVerificationFailure(t *testing.T) {
structured := &buildTaggedTBTCSignerStructuredError{
Code: interactiveAggregateShareVerificationFailedCode,
Message: "shares failed verification",
CandidateCulprits: []uint16{2, 3},
}
// Wrap exactly as the bridge call helper does (double %w).
wrapped := fmt.Errorf(
"%w: tbtc-signer bridge operation [InteractiveAggregate] failed: [%w]",
ErrNativeBridgeOperationFailed,
structured,
)

err := interpretInteractiveAggregateError("session-1", "attempt-1", wrapped)

var aggErr *InteractiveAggregateShareVerificationError
if !errors.As(err, &aggErr) {
t.Fatalf("expected InteractiveAggregateShareVerificationError, got: [%v]", err)
}
if aggErr.SessionID != "session-1" || aggErr.AttemptID != "attempt-1" {
t.Fatalf("unexpected session/attempt: [%+v]", aggErr)
}
if len(aggErr.CandidateCulprits) != 2 ||
aggErr.CandidateCulprits[0] != 2 ||
aggErr.CandidateCulprits[1] != 3 {
t.Fatalf("unexpected candidate culprits: [%v]", aggErr.CandidateCulprits)
}
}

func TestInterpretInteractiveAggregateError_OtherErrorPassesThrough(t *testing.T) {
structured := &buildTaggedTBTCSignerStructuredError{Code: "some_other_error", Message: "boom"}
wrapped := fmt.Errorf(
"%w: tbtc-signer bridge operation [InteractiveAggregate] failed: [%w]",
ErrNativeBridgeOperationFailed,
structured,
)

err := interpretInteractiveAggregateError("s", "a", wrapped)

var aggErr *InteractiveAggregateShareVerificationError
if errors.As(err, &aggErr) {
t.Fatal("a non-share-verification error must not become the typed culprit error")
}
if !errors.Is(err, ErrNativeBridgeOperationFailed) {
t.Fatalf("expected the original wrapped error to pass through, got: [%v]", err)
}
}

func TestBuildTaggedTBTCSignerErrorPayload_CandidateCulprits(t *testing.T) {
structured := buildTaggedTBTCSignerErrorPayload([]byte(
`{"code":"aggregate_share_verification_failed","message":"x","candidate_culprits":[2,3]}`,
))
if structured.Code != interactiveAggregateShareVerificationFailedCode {
t.Fatalf("unexpected code: [%s]", structured.Code)
}
if len(structured.CandidateCulprits) != 2 ||
structured.CandidateCulprits[0] != 2 ||
structured.CandidateCulprits[1] != 3 {
t.Fatalf("unexpected candidate culprits: [%v]", structured.CandidateCulprits)
}

// A non-culprit error decodes with an empty culprit list.
plain := buildTaggedTBTCSignerErrorPayload([]byte(`{"code":"validation_error","message":"x"}`))
if len(plain.CandidateCulprits) != 0 {
t.Fatalf("expected no culprits for a non-culprit error, got: [%v]", plain.CandidateCulprits)
}
}
Loading
Loading