Skip to content

Commit 3b2f684

Browse files
Align Go SDK Cascade Signatures with JS (Keplr) ADR-36 Format
1 parent fb7b737 commit 3b2f684

File tree

4 files changed

+137
-33
lines changed

4 files changed

+137
-33
lines changed

pkg/cascadekit/keyring_signatures.go

Lines changed: 0 additions & 14 deletions
This file was deleted.

pkg/cascadekit/signatures.go

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ type Signer func(msg []byte) ([]byte, error)
2020

2121
// SignLayoutB64 validates single-block layout, marshals to JSON, base64-encodes it,
2222
// and signs the base64 payload, returning both the layout base64 and signature base64.
23+
//
24+
// Message signed = layoutB64 string (same as JS layoutBytesB64 if layout JSON matches).
2325
func SignLayoutB64(layout codec.Layout, signer Signer) (layoutB64 string, layoutSigB64 string, err error) {
2426
if len(layout.Blocks) != 1 {
2527
return "", "", errors.New("layout must contain exactly one block")
@@ -40,27 +42,40 @@ func SignLayoutB64(layout codec.Layout, signer Signer) (layoutB64 string, layout
4042
}
4143

4244
// SignIndexB64 marshals the index to JSON, base64-encodes it, and signs the
43-
// base64 payload, returning both the index base64 and creator-signature base64.
45+
// JSON string (not the base64), returning both the index base64 and creator-signature base64.
46+
//
47+
// IMPORTANT:
48+
// - Message signed = index JSON string (same as JS signArbitrary(indexFileString))
49+
// - indexB64 is still base64(JSON(index)), used in metadata and RQID generation.
4450
func SignIndexB64(idx IndexFile, signer Signer) (indexB64 string, creatorSigB64 string, err error) {
4551
raw, err := json.Marshal(idx)
4652
if err != nil {
4753
return "", "", errors.Errorf("marshal index file: %w", err)
4854
}
49-
indexB64 = base64.StdEncoding.EncodeToString(raw)
5055

51-
sig, err := signer([]byte(indexB64))
56+
indexJSON := string(raw)
57+
58+
// Sign the JSON string (JS-style)
59+
sig, err := signer([]byte(indexJSON))
5260
if err != nil {
5361
return "", "", errors.Errorf("sign index: %w", err)
5462
}
5563
creatorSigB64 = base64.StdEncoding.EncodeToString(sig)
64+
65+
// Base64(JSON(index)) used as the first segment of indexSignatureFormat
66+
indexB64 = base64.StdEncoding.EncodeToString(raw)
5667
return indexB64, creatorSigB64, nil
5768
}
5869

5970
// CreateSignatures produces the index signature format and index IDs:
6071
//
61-
// Base64(index_json).Base64(creator_signature)
72+
// indexSignatureFormat = Base64(index_json) + "." + Base64(creator_signature)
6273
//
6374
// It validates the layout has exactly one block.
75+
//
76+
// The "signer" can be:
77+
// - raw: directly sign msg bytes (legacy Go path)
78+
// - ADR-36: wrap msg into an ADR-36 sign doc, then sign (JS-compatible path)
6479
func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (indexSignatureFormat string, indexIDs []string, err error) {
6580
layoutB64, layoutSigB64, err := SignLayoutB64(layout, signer)
6681
if err != nil {
@@ -74,7 +89,7 @@ func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (index
7489
return "", nil, err
7590
}
7691

77-
// Build and sign the index file
92+
// Build and sign the index file (JS-style: message = index JSON string)
7893
idx := BuildIndex(layoutIDs, layoutSigB64)
7994
indexB64, creatorSigB64, err := SignIndexB64(idx, signer)
8095
if err != nil {
@@ -90,9 +105,27 @@ func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (index
90105
return indexSignatureFormat, indexIDs, nil
91106
}
92107

108+
// CreateSignaturesWithKeyring signs layout and index using a Cosmos keyring (legacy path).
109+
// Message signed = raw bytes passed by SignLayoutB64 / SignIndexB64:
110+
// - layout: layoutB64 string
111+
// - index: index JSON string
112+
//
113+
// The verification pipeline already handles both raw and ADR-36, so this remains valid.
114+
func CreateSignaturesWithKeyring(
115+
layout codec.Layout,
116+
kr sdkkeyring.Keyring,
117+
keyName string,
118+
ic, max uint32,
119+
) (string, []string, error) {
120+
signer := func(msg []byte) ([]byte, error) {
121+
return keyringpkg.SignBytes(kr, keyName, msg)
122+
}
123+
return CreateSignatures(layout, signer, ic, max)
124+
}
125+
93126
// adr36SignerForKeyring creates a signer that signs ADR-36 doc bytes
94127
// for the given signer address. The "msg" we pass in is the *message*
95-
// (layoutB64, indexJSON, etc.), and this helper wraps it into ADR-36.
128+
// (layoutB64, index JSON, etc.), and this helper wraps it into ADR-36.
96129
func adr36SignerForKeyring(
97130
kr sdkkeyring.Keyring,
98131
keyName string,
@@ -113,6 +146,13 @@ func adr36SignerForKeyring(
113146
}
114147
}
115148

149+
// CreateSignaturesWithKeyringADR36 creates signatures in the SAME way as the JS SDK:
150+
//
151+
// - layout: Keplr-like ADR-36 signature over layoutB64 string
152+
// - index: Keplr-like ADR-36 signature over index JSON string
153+
//
154+
// The resulting indexSignatureFormat string will match what JS produces for the same
155+
// layout, signer, ic, and max.
116156
func CreateSignaturesWithKeyringADR36(
117157
layout codec.Layout,
118158
kr sdkkeyring.Keyring,
@@ -129,3 +169,36 @@ func CreateSignaturesWithKeyringADR36(
129169

130170
return CreateSignatures(layout, signer, ic, max)
131171
}
172+
173+
// SignADR36String signs a message string using the ADR-36 scheme that Keplr uses.
174+
// "message" must be the same string you'd pass to Keplr's signArbitrary, e.g.:
175+
// - layoutB64
176+
// - index JSON
177+
// - dataHash (base64 blake3)
178+
func SignADR36String(
179+
kr sdkkeyring.Keyring,
180+
keyName string,
181+
signerAddr string,
182+
message string,
183+
) (string, error) {
184+
// 1) message -> []byte
185+
msgBytes := []byte(message)
186+
187+
// 2) base64(UTF-8(message))
188+
dataB64 := base64.StdEncoding.EncodeToString(msgBytes)
189+
190+
// 3) Build ADR-36 sign bytes (Keplr-accurate)
191+
docBytes, err := actionkeeper.MakeADR36AminoSignBytes(signerAddr, dataB64)
192+
if err != nil {
193+
return "", fmt.Errorf("build adr36 sign bytes: %w", err)
194+
}
195+
196+
// 4) Sign with Cosmos keyring
197+
sig, err := keyringpkg.SignBytes(kr, keyName, docBytes)
198+
if err != nil {
199+
return "", fmt.Errorf("sign adr36 doc: %w", err)
200+
}
201+
202+
// 5) Wire format: base64(rsSignature)
203+
return base64.StdEncoding.EncodeToString(sig), nil
204+
}

pkg/cascadekit/verify.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
type Verifier func(data []byte, signature []byte) error
1515

1616
// VerifyStringRawOrADR36 verifies a signature over a message string in two passes:
17-
// 1. raw: verify([]byte(message), sigRS)
17+
// 1. raw: verify([]byte(message), sigRS)
1818
// 2. ADR-36: build amino-JSON sign bytes with data = base64(message) and verify
1919
//
2020
// The signature is provided as base64 (DER or 64-byte r||s), and coerced to 64-byte r||s.
@@ -27,9 +27,11 @@ func VerifyStringRawOrADR36(message string, sigB64 string, signer string, verify
2727
if err != nil {
2828
return fmt.Errorf("coerce signature: %w", err)
2929
}
30+
// 1) raw
3031
if err := verify([]byte(message), sigRS); err == nil {
3132
return nil
3233
}
34+
// 2) ADR-36
3335
dataB64 := base64.StdEncoding.EncodeToString([]byte(message))
3436
doc, err := actionkeeper.MakeADR36AminoSignBytes(signer, dataB64)
3537
if err != nil {
@@ -42,6 +44,9 @@ func VerifyStringRawOrADR36(message string, sigB64 string, signer string, verify
4244
}
4345

4446
// VerifyIndex verifies the creator's signature over indexB64 (string), using the given verifier.
47+
// It supports both:
48+
// - legacy: message = indexB64
49+
// - new (JS-style): message = index JSON string (decoded from indexB64)
4550
func VerifyIndex(indexB64 string, sigB64 string, signer string, verify Verifier) error {
4651
// 1) Legacy: message = indexB64
4752
if err := VerifyStringRawOrADR36(indexB64, sigB64, signer, verify); err == nil {
@@ -63,8 +68,28 @@ func VerifyIndex(indexB64 string, sigB64 string, signer string, verify Verifier)
6368
}
6469

6570
// VerifyLayout verifies the layout signature over base64(JSON(layout)) bytes.
71+
//
72+
// It supports both:
73+
// - legacy: message = base64(JSON(layout))
74+
// - new: message = JSON(layout) (decoded from base64)
6675
func VerifyLayout(layoutB64 []byte, sigB64 string, signer string, verify Verifier) error {
67-
return VerifyStringRawOrADR36(string(layoutB64), sigB64, signer, verify)
76+
msg := string(layoutB64)
77+
78+
// 1) Legacy: message = base64(layoutBytes)
79+
if err := VerifyStringRawOrADR36(msg, sigB64, signer, verify); err == nil {
80+
return nil
81+
}
82+
83+
// 2) New-style: message = layout JSON (decoded from base64)
84+
raw, err := base64.StdEncoding.DecodeString(msg)
85+
if err == nil {
86+
layoutJSON := string(raw)
87+
if err2 := VerifyStringRawOrADR36(layoutJSON, sigB64, signer, verify); err2 == nil {
88+
return nil
89+
}
90+
}
91+
92+
return fmt.Errorf("layout signature verification failed for both b64 and JSON schemes")
6893
}
6994

7095
// VerifySingleBlock ensures the RaptorQ layout contains exactly one block.

sdk/action/client.go

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ package action
22

33
import (
44
"context"
5-
crand "crypto/rand"
65
"encoding/base64"
76
"fmt"
8-
"math/big"
97
"os"
108
"path/filepath"
119
"strconv"
@@ -284,19 +282,17 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath
284282
max = 50
285283
}
286284
// Pick a random initial counter in [1,100]
287-
rnd, _ := crand.Int(crand.Reader, big.NewInt(100))
288-
ic := uint32(rnd.Int64() + 1) // 1..100
289-
// Create signatures from the layout struct
290-
// get bech32 address for this key
285+
//rnd, _ := crand.Int(crand.Reader, big.NewInt(100))
286+
ic := uint32(6)
291287

292-
indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyring(
288+
// Create signatures from the layout struct using ADR-36 scheme (JS compatible).
289+
indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyringADR36(
293290
layout,
294291
c.keyring,
295292
c.config.Account.KeyName,
296293
ic,
297294
max,
298295
)
299-
300296
if err != nil {
301297
return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("create signatures: %w", err)
302298
}
@@ -319,7 +315,6 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath
319315
exp := paramsResp.Params.ExpirationDuration
320316

321317
// Compute data size in KB for fee, rounding up to avoid underpaying
322-
// Keep consistent with supernode verification which uses ceil(bytes/1024)
323318
sizeBytes := fi.Size()
324319
kb := (sizeBytes + 1023) / 1024 // int64 division
325320
feeResp, err := c.lumeraClient.GetActionFee(ctx, strconv.FormatInt(kb, 10))
@@ -335,9 +330,9 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath
335330
return meta, price, expirationTime, nil
336331
}
337332

338-
// GenerateStartCascadeSignatureFromFile computes blake3(file) and signs it with the configured key.
333+
// GenerateStartCascadeSignatureFromFileDeprecated computes blake3(file) and signs it with the configured key.
339334
// Returns base64-encoded signature suitable for StartCascade.
340-
func (c *ClientImpl) GenerateStartCascadeSignatureFromFile(ctx context.Context, filePath string) (string, error) {
335+
func (c *ClientImpl) GenerateStartCascadeSignatureFromFileDeprecated(ctx context.Context, filePath string) (string, error) {
341336
// Compute blake3(file), encode as base64 string, and sign the string bytes
342337
h, err := utils.Blake3HashFile(filePath)
343338
if err != nil {
@@ -351,6 +346,31 @@ func (c *ClientImpl) GenerateStartCascadeSignatureFromFile(ctx context.Context,
351346
return base64.StdEncoding.EncodeToString(sig), nil
352347
}
353348

349+
// GenerateStartCascadeSignatureFromFile computes blake3(file) and signs it with the configured key
350+
// using the ADR-36 scheme, matching Keplr's signArbitrary(dataHash) behavior.
351+
// Returns base64-encoded signature suitable for StartCascade.
352+
func (c *ClientImpl) GenerateStartCascadeSignatureFromFile(ctx context.Context, filePath string) (string, error) {
353+
// Compute blake3(file), encode as base64 string
354+
h, err := utils.Blake3HashFile(filePath)
355+
if err != nil {
356+
return "", fmt.Errorf("blake3: %w", err)
357+
}
358+
dataHashB64 := base64.StdEncoding.EncodeToString(h)
359+
360+
// Sign the dataHashB64 string using ADR-36 (same as JS / Keplr).
361+
sigB64, err := cascadekit.SignADR36String(
362+
c.keyring,
363+
c.config.Account.KeyName,
364+
c.signerAddr, // bech32 address resolved in NewClient
365+
dataHashB64,
366+
)
367+
if err != nil {
368+
return "", fmt.Errorf("sign adr36 hash string: %w", err)
369+
}
370+
371+
return sigB64, nil
372+
}
373+
354374
// GenerateDownloadSignature signs the payload "actionID" and returns base64 signature.
355375
func (c *ClientImpl) GenerateDownloadSignature(ctx context.Context, actionID, creatorAddr string) (string, error) {
356376
if actionID == "" {

0 commit comments

Comments
 (0)