Skip to content

Commit 65f3963

Browse files
committed
feat(frost/signing): RFC-21 Phase 1B -- optional AttemptContextHash field
Extends the three FROST/tbtc-signer protocol message types with an optional 32-byte AttemptContextHash field that binds the message to a specific RFC-21 AttemptContext (introduced in Phase 1A). * nativeFROSTRoundOneCommitmentMessage * nativeFROSTRoundTwoSignatureShareMessage * buildTaggedTBTCSignerRoundContributionMessage Migration contract (Phase 1B intentionally limited): * Field uses omitempty -- absent on the wire when the sender has not bound the message to a context. Old peers continue to interop. * Receiver-side Unmarshal validates length-when-present (must be exactly AttemptContextHashFieldLength = 32) but does not yet match against the locally-computed context. Higher-level acceptance lands in a later RFC-21 phase behind a build tag. * Shared helpers in attempt_context_binding.go convert between the on-wire []byte form and the canonical [32]byte hash form. Senders use SetAttemptContextHash; receivers use GetAttemptContextHash to get the hash + presence flag. Equal-or-reject is extended to compare AttemptContextHash bytewise, so a peer that retransmits the same contribution but mutates the binding mid-stream triggers the existing first-write-wins reject path (introduced in PR #3959). 17 new tests cover: length validation; array<->slice round-trip without caller aliasing; per-message marshal/unmarshal round-trip with field absent and present; backward compatibility with pre-Phase-1B JSON; wrong-length rejection; equal-or-reject sensitivity to the new field. All pass under `go test -tags 'frost_native frost_tbtc_signer' ./pkg/frost/...` plus the pkg/tbtc regression subset. Refs RFC-21 (docs/rfc/rfc-21-*); stacked on Phase 1A (#3963).
1 parent cb26865 commit 65f3963

4 files changed

Lines changed: 502 additions & 1 deletion

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package signing
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/keep-network/keep-core/pkg/frost/roast/attempt"
7+
)
8+
9+
// AttemptContextHashFieldLength is the on-wire byte length of the
10+
// optional AttemptContextHash field carried by the FROST/tbtc-signer
11+
// protocol messages. The field is the canonical SHA-256 hash of the
12+
// AttemptContext (see pkg/frost/roast/attempt), so 32 bytes.
13+
const AttemptContextHashFieldLength = attempt.MessageDigestLength
14+
15+
// validateAttemptContextHashField checks the length invariant for the
16+
// optional AttemptContextHash field on protocol messages. An absent
17+
// field (nil or zero-length slice) is valid; a present field must
18+
// match AttemptContextHashFieldLength exactly.
19+
//
20+
// This is the only validation Phase 1B performs on the field. Higher-
21+
// level acceptance (the receiver-side check that the hash matches the
22+
// locally-computed AttemptContext) lands in a later RFC-21 phase
23+
// behind a build tag, since enabling it requires honest peers to have
24+
// rolled out the new field first.
25+
func validateAttemptContextHashField(field []byte) error {
26+
if len(field) == 0 {
27+
return nil
28+
}
29+
if len(field) != AttemptContextHashFieldLength {
30+
return fmt.Errorf(
31+
"attempt context hash field has wrong length [%d], expected [%d] or absent",
32+
len(field),
33+
AttemptContextHashFieldLength,
34+
)
35+
}
36+
return nil
37+
}
38+
39+
// attemptContextHashFieldFromArray converts a fixed-size 32-byte hash
40+
// into the slice form used on the wire. Returns a fresh slice so the
41+
// caller's array cannot be mutated through the returned reference.
42+
func attemptContextHashFieldFromArray(
43+
hash [AttemptContextHashFieldLength]byte,
44+
) []byte {
45+
out := make([]byte, AttemptContextHashFieldLength)
46+
copy(out, hash[:])
47+
return out
48+
}
49+
50+
// attemptContextHashFieldToArray converts a wire-form slice back to
51+
// a fixed-size 32-byte hash plus a presence flag. Returns
52+
// (zeroArray, false) when the field is absent. Caller has already
53+
// validated length via validateAttemptContextHashField; this function
54+
// trusts that invariant and panics on violation.
55+
func attemptContextHashFieldToArray(
56+
field []byte,
57+
) ([AttemptContextHashFieldLength]byte, bool) {
58+
var out [AttemptContextHashFieldLength]byte
59+
if len(field) == 0 {
60+
return out, false
61+
}
62+
if len(field) != AttemptContextHashFieldLength {
63+
panic(fmt.Sprintf(
64+
"attemptContextHashFieldToArray called with wrong-length field [%d]",
65+
len(field),
66+
))
67+
}
68+
copy(out[:], field)
69+
return out, true
70+
}

0 commit comments

Comments
 (0)