diff --git a/pkg/cascadekit/keyring_signatures.go b/pkg/cascadekit/keyring_signatures.go deleted file mode 100644 index 968af4b..0000000 --- a/pkg/cascadekit/keyring_signatures.go +++ /dev/null @@ -1,14 +0,0 @@ -package cascadekit - -import ( - "github.com/LumeraProtocol/supernode/v2/pkg/codec" - keyringpkg "github.com/LumeraProtocol/supernode/v2/pkg/keyring" - cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" -) - -// CreateSignaturesWithKeyring signs layout and index using a Cosmos keyring. -// These helpers centralize keyring-backed signing for clarity. -func CreateSignaturesWithKeyring(layout codec.Layout, kr cosmoskeyring.Keyring, keyName string, ic, max uint32) (string, []string, error) { - signer := func(msg []byte) ([]byte, error) { return keyringpkg.SignBytes(kr, keyName, msg) } - return CreateSignatures(layout, signer, ic, max) -} diff --git a/pkg/cascadekit/signatures.go b/pkg/cascadekit/signatures.go index 2c8bc1f..2d67fe9 100644 --- a/pkg/cascadekit/signatures.go +++ b/pkg/cascadekit/signatures.go @@ -20,6 +20,8 @@ type Signer func(msg []byte) ([]byte, error) // SignLayoutB64 validates single-block layout, marshals to JSON, base64-encodes it, // and signs the base64 payload, returning both the layout base64 and signature base64. +// +// Message signed = layoutB64 string (same as JS layoutBytesB64 if layout JSON matches). func SignLayoutB64(layout codec.Layout, signer Signer) (layoutB64 string, layoutSigB64 string, err error) { if len(layout.Blocks) != 1 { return "", "", errors.New("layout must contain exactly one block") @@ -40,27 +42,40 @@ func SignLayoutB64(layout codec.Layout, signer Signer) (layoutB64 string, layout } // SignIndexB64 marshals the index to JSON, base64-encodes it, and signs the -// base64 payload, returning both the index base64 and creator-signature base64. +// JSON string (not the base64), returning both the index base64 and creator-signature base64. +// +// IMPORTANT: +// - Message signed = index JSON string (same as JS signArbitrary(indexFileString)) +// - indexB64 is still base64(JSON(index)), used in metadata and RQID generation. func SignIndexB64(idx IndexFile, signer Signer) (indexB64 string, creatorSigB64 string, err error) { raw, err := json.Marshal(idx) if err != nil { return "", "", errors.Errorf("marshal index file: %w", err) } - indexB64 = base64.StdEncoding.EncodeToString(raw) - sig, err := signer([]byte(indexB64)) + indexJSON := string(raw) + + // Sign the JSON string (JS-style) + sig, err := signer([]byte(indexJSON)) if err != nil { return "", "", errors.Errorf("sign index: %w", err) } creatorSigB64 = base64.StdEncoding.EncodeToString(sig) + + // Base64(JSON(index)) used as the first segment of indexSignatureFormat + indexB64 = base64.StdEncoding.EncodeToString(raw) return indexB64, creatorSigB64, nil } // CreateSignatures produces the index signature format and index IDs: // -// Base64(index_json).Base64(creator_signature) +// indexSignatureFormat = Base64(index_json) + "." + Base64(creator_signature) // // It validates the layout has exactly one block. +// +// The "signer" can be: +// - raw: directly sign msg bytes (legacy Go path) +// - ADR-36: wrap msg into an ADR-36 sign doc, then sign (JS-compatible path) func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (indexSignatureFormat string, indexIDs []string, err error) { layoutB64, layoutSigB64, err := SignLayoutB64(layout, signer) if err != nil { @@ -74,7 +89,7 @@ func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (index return "", nil, err } - // Build and sign the index file + // Build and sign the index file (JS-style: message = index JSON string) idx := BuildIndex(layoutIDs, layoutSigB64) indexB64, creatorSigB64, err := SignIndexB64(idx, signer) if err != nil { @@ -90,9 +105,27 @@ func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (index return indexSignatureFormat, indexIDs, nil } +// CreateSignaturesWithKeyring signs layout and index using a Cosmos keyring (legacy path). +// Message signed = raw bytes passed by SignLayoutB64 / SignIndexB64: +// - layout: layoutB64 string +// - index: index JSON string +// +// The verification pipeline already handles both raw and ADR-36, so this remains valid. +func CreateSignaturesWithKeyring( + layout codec.Layout, + kr sdkkeyring.Keyring, + keyName string, + ic, max uint32, +) (string, []string, error) { + signer := func(msg []byte) ([]byte, error) { + return keyringpkg.SignBytes(kr, keyName, msg) + } + return CreateSignatures(layout, signer, ic, max) +} + // adr36SignerForKeyring creates a signer that signs ADR-36 doc bytes // for the given signer address. The "msg" we pass in is the *message* -// (layoutB64, indexJSON, etc.), and this helper wraps it into ADR-36. +// (layoutB64, index JSON, etc.), and this helper wraps it into ADR-36. func adr36SignerForKeyring( kr sdkkeyring.Keyring, keyName string, @@ -113,6 +146,13 @@ func adr36SignerForKeyring( } } +// CreateSignaturesWithKeyringADR36 creates signatures in the SAME way as the JS SDK: +// +// - layout: Keplr-like ADR-36 signature over layoutB64 string +// - index: Keplr-like ADR-36 signature over index JSON string +// +// The resulting indexSignatureFormat string will match what JS produces for the same +// layout, signer, ic, and max. func CreateSignaturesWithKeyringADR36( layout codec.Layout, kr sdkkeyring.Keyring, @@ -129,3 +169,36 @@ func CreateSignaturesWithKeyringADR36( return CreateSignatures(layout, signer, ic, max) } + +// SignADR36String signs a message string using the ADR-36 scheme that Keplr uses. +// "message" must be the same string you'd pass to Keplr's signArbitrary, e.g.: +// - layoutB64 +// - index JSON +// - dataHash (base64 blake3) +func SignADR36String( + kr sdkkeyring.Keyring, + keyName string, + signerAddr string, + message string, +) (string, error) { + // 1) message -> []byte + msgBytes := []byte(message) + + // 2) base64(UTF-8(message)) + dataB64 := base64.StdEncoding.EncodeToString(msgBytes) + + // 3) Build ADR-36 sign bytes (Keplr-accurate) + docBytes, err := actionkeeper.MakeADR36AminoSignBytes(signerAddr, dataB64) + if err != nil { + return "", fmt.Errorf("build adr36 sign bytes: %w", err) + } + + // 4) Sign with Cosmos keyring + sig, err := keyringpkg.SignBytes(kr, keyName, docBytes) + if err != nil { + return "", fmt.Errorf("sign adr36 doc: %w", err) + } + + // 5) Wire format: base64(rsSignature) + return base64.StdEncoding.EncodeToString(sig), nil +} diff --git a/pkg/cascadekit/verify.go b/pkg/cascadekit/verify.go index 7d9b752..4c1aced 100644 --- a/pkg/cascadekit/verify.go +++ b/pkg/cascadekit/verify.go @@ -14,7 +14,7 @@ import ( type Verifier func(data []byte, signature []byte) error // VerifyStringRawOrADR36 verifies a signature over a message string in two passes: -// 1. raw: verify([]byte(message), sigRS) +// 1. raw: verify([]byte(message), sigRS) // 2. ADR-36: build amino-JSON sign bytes with data = base64(message) and verify // // 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 if err != nil { return fmt.Errorf("coerce signature: %w", err) } + // 1) raw if err := verify([]byte(message), sigRS); err == nil { return nil } + // 2) ADR-36 dataB64 := base64.StdEncoding.EncodeToString([]byte(message)) doc, err := actionkeeper.MakeADR36AminoSignBytes(signer, dataB64) if err != nil { @@ -42,6 +44,9 @@ func VerifyStringRawOrADR36(message string, sigB64 string, signer string, verify } // VerifyIndex verifies the creator's signature over indexB64 (string), using the given verifier. +// It supports both: +// - legacy: message = indexB64 +// - new (JS-style): message = index JSON string (decoded from indexB64) func VerifyIndex(indexB64 string, sigB64 string, signer string, verify Verifier) error { // 1) Legacy: message = indexB64 if err := VerifyStringRawOrADR36(indexB64, sigB64, signer, verify); err == nil { @@ -63,8 +68,28 @@ func VerifyIndex(indexB64 string, sigB64 string, signer string, verify Verifier) } // VerifyLayout verifies the layout signature over base64(JSON(layout)) bytes. +// +// It supports both: +// - legacy: message = base64(JSON(layout)) +// - new: message = JSON(layout) (decoded from base64) func VerifyLayout(layoutB64 []byte, sigB64 string, signer string, verify Verifier) error { - return VerifyStringRawOrADR36(string(layoutB64), sigB64, signer, verify) + msg := string(layoutB64) + + // 1) Legacy: message = base64(layoutBytes) + if err := VerifyStringRawOrADR36(msg, sigB64, signer, verify); err == nil { + return nil + } + + // 2) New-style: message = layout JSON (decoded from base64) + raw, err := base64.StdEncoding.DecodeString(msg) + if err == nil { + layoutJSON := string(raw) + if err2 := VerifyStringRawOrADR36(layoutJSON, sigB64, signer, verify); err2 == nil { + return nil + } + } + + return fmt.Errorf("layout signature verification failed for both b64 and JSON schemes") } // VerifySingleBlock ensures the RaptorQ layout contains exactly one block. diff --git a/sdk/action/client.go b/sdk/action/client.go index 741adbd..09eea74 100644 --- a/sdk/action/client.go +++ b/sdk/action/client.go @@ -2,10 +2,8 @@ package action import ( "context" - crand "crypto/rand" "encoding/base64" "fmt" - "math/big" "os" "path/filepath" "strconv" @@ -284,19 +282,17 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath max = 50 } // Pick a random initial counter in [1,100] - rnd, _ := crand.Int(crand.Reader, big.NewInt(100)) - ic := uint32(rnd.Int64() + 1) // 1..100 - // Create signatures from the layout struct - // get bech32 address for this key + //rnd, _ := crand.Int(crand.Reader, big.NewInt(100)) + ic := uint32(6) - indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyring( + // Create signatures from the layout struct using ADR-36 scheme (JS compatible). + indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyringADR36( layout, c.keyring, c.config.Account.KeyName, ic, max, ) - if err != nil { return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("create signatures: %w", err) } @@ -319,7 +315,6 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath exp := paramsResp.Params.ExpirationDuration // Compute data size in KB for fee, rounding up to avoid underpaying - // Keep consistent with supernode verification which uses ceil(bytes/1024) sizeBytes := fi.Size() kb := (sizeBytes + 1023) / 1024 // int64 division feeResp, err := c.lumeraClient.GetActionFee(ctx, strconv.FormatInt(kb, 10)) @@ -335,9 +330,9 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath return meta, price, expirationTime, nil } -// GenerateStartCascadeSignatureFromFile computes blake3(file) and signs it with the configured key. +// GenerateStartCascadeSignatureFromFileDeprecated computes blake3(file) and signs it with the configured key. // Returns base64-encoded signature suitable for StartCascade. -func (c *ClientImpl) GenerateStartCascadeSignatureFromFile(ctx context.Context, filePath string) (string, error) { +func (c *ClientImpl) GenerateStartCascadeSignatureFromFileDeprecated(ctx context.Context, filePath string) (string, error) { // Compute blake3(file), encode as base64 string, and sign the string bytes h, err := utils.Blake3HashFile(filePath) if err != nil { @@ -351,6 +346,31 @@ func (c *ClientImpl) GenerateStartCascadeSignatureFromFile(ctx context.Context, return base64.StdEncoding.EncodeToString(sig), nil } +// GenerateStartCascadeSignatureFromFile computes blake3(file) and signs it with the configured key +// using the ADR-36 scheme, matching Keplr's signArbitrary(dataHash) behavior. +// Returns base64-encoded signature suitable for StartCascade. +func (c *ClientImpl) GenerateStartCascadeSignatureFromFile(ctx context.Context, filePath string) (string, error) { + // Compute blake3(file), encode as base64 string + h, err := utils.Blake3HashFile(filePath) + if err != nil { + return "", fmt.Errorf("blake3: %w", err) + } + dataHashB64 := base64.StdEncoding.EncodeToString(h) + + // Sign the dataHashB64 string using ADR-36 (same as JS / Keplr). + sigB64, err := cascadekit.SignADR36String( + c.keyring, + c.config.Account.KeyName, + c.signerAddr, // bech32 address resolved in NewClient + dataHashB64, + ) + if err != nil { + return "", fmt.Errorf("sign adr36 hash string: %w", err) + } + + return sigB64, nil +} + // GenerateDownloadSignature signs the payload "actionID" and returns base64 signature. func (c *ClientImpl) GenerateDownloadSignature(ctx context.Context, actionID, creatorAddr string) (string, error) { if actionID == "" {