Skip to content

Commit b84ceee

Browse files
Add ADR-36 Signature Support and Dual-Mode Index Verification for JS SDK Compatibility (#231)
1 parent c5ff6dd commit b84ceee

File tree

10 files changed

+150
-16
lines changed

10 files changed

+150
-16
lines changed

pkg/cascadekit/signatures.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import (
77

88
"github.com/LumeraProtocol/supernode/v2/pkg/codec"
99
"github.com/LumeraProtocol/supernode/v2/pkg/errors"
10+
11+
actionkeeper "github.com/LumeraProtocol/lumera/x/action/v1/keeper"
12+
13+
keyringpkg "github.com/LumeraProtocol/supernode/v2/pkg/keyring"
14+
15+
sdkkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring"
1016
)
1117

1218
// Signer is a function that signs the provided message and returns the raw signature bytes.
@@ -83,3 +89,43 @@ func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (index
8389
}
8490
return indexSignatureFormat, indexIDs, nil
8591
}
92+
93+
// adr36SignerForKeyring creates a signer that signs ADR-36 doc bytes
94+
// 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.
96+
func adr36SignerForKeyring(
97+
kr sdkkeyring.Keyring,
98+
keyName string,
99+
signerAddr string,
100+
) Signer {
101+
return func(msg []byte) ([]byte, error) {
102+
// msg is the cleartext message we want to sign (e.g., layoutB64 or index JSON string)
103+
dataB64 := base64.StdEncoding.EncodeToString(msg)
104+
105+
// Build ADR-36 sign bytes: signerAddr + base64(message)
106+
doc, err := actionkeeper.MakeADR36AminoSignBytes(signerAddr, dataB64)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
// Now sign the ADR-36 doc bytes with the keyring (direct secp256k1)
112+
return keyringpkg.SignBytes(kr, keyName, doc)
113+
}
114+
}
115+
116+
func CreateSignaturesWithKeyringADR36(
117+
layout codec.Layout,
118+
kr sdkkeyring.Keyring,
119+
keyName string,
120+
ic, max uint32,
121+
) (string, []string, error) {
122+
// Resolve signer bech32 address from keyring
123+
addr, err := keyringpkg.GetAddress(kr, keyName)
124+
if err != nil {
125+
return "", nil, fmt.Errorf("resolve signer address: %w", err)
126+
}
127+
128+
signer := adr36SignerForKeyring(kr, keyName, addr.String())
129+
130+
return CreateSignatures(layout, signer, ic, max)
131+
}

pkg/cascadekit/verify.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,23 @@ func VerifyStringRawOrADR36(message string, sigB64 string, signer string, verify
4343

4444
// VerifyIndex verifies the creator's signature over indexB64 (string), using the given verifier.
4545
func VerifyIndex(indexB64 string, sigB64 string, signer string, verify Verifier) error {
46-
return VerifyStringRawOrADR36(indexB64, sigB64, signer, verify)
46+
// 1) Legacy: message = indexB64
47+
if err := VerifyStringRawOrADR36(indexB64, sigB64, signer, verify); err == nil {
48+
return nil
49+
}
50+
51+
// 2) JS-style: message = index JSON string (decoded from indexB64)
52+
raw, err := base64.StdEncoding.DecodeString(indexB64)
53+
if err != nil {
54+
return fmt.Errorf("invalid indexB64: %w", err)
55+
}
56+
indexJSON := string(raw)
57+
58+
if err := VerifyStringRawOrADR36(indexJSON, sigB64, signer, verify); err == nil {
59+
return nil
60+
}
61+
62+
return fmt.Errorf("signature verification failed for both legacy and ADR-36 index schemes")
4763
}
4864

4965
// VerifyLayout verifies the layout signature over base64(JSON(layout)) bytes.

pkg/keyring/keyring.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,11 @@ func SignBytes(kr sdkkeyring.Keyring, name string, bz []byte) ([]byte, error) {
173173
sig, _, err := kr.SignByAddress(addr, bz, signing.SignMode_SIGN_MODE_DIRECT)
174174
return sig, err
175175
}
176+
177+
func GetAddress(kr sdkkeyring.Keyring, name string) (types.AccAddress, error) {
178+
rec, err := kr.Key(name)
179+
if err != nil {
180+
return nil, err
181+
}
182+
return rec.GetAddress()
183+
}

pkg/net/grpc/client/client.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/LumeraProtocol/supernode/v2/pkg/logtrace"
1919
ltc "github.com/LumeraProtocol/supernode/v2/pkg/net/credentials"
2020
"github.com/LumeraProtocol/supernode/v2/pkg/random"
21+
"github.com/LumeraProtocol/supernode/v2/sdk/log"
2122
)
2223

2324
const (
@@ -29,7 +30,7 @@ const (
2930

3031
const (
3132
defaultTimeout = 30 * time.Second
32-
defaultConnWaitTime = 10 * time.Second
33+
defaultConnWaitTime = 12 * time.Second
3334
defaultRetryWaitTime = 1 * time.Second
3435
maxRetries = 3
3536

@@ -69,6 +70,7 @@ type Client struct {
6970
creds credentials.TransportCredentials
7071
builder DialOptionBuilder
7172
connHandler ConnectionHandler
73+
logger log.Logger
7274
}
7375

7476
// ClientOptions contains options for creating a new client
@@ -97,6 +99,8 @@ type ClientOptions struct {
9799
UserAgent string // User-Agent header value for all requests
98100
Authority string // Value to use as the :authority pseudo-header
99101
MinConnectTimeout time.Duration // Minimum time to attempt connection before failing
102+
103+
Logger log.Logger
100104
}
101105

102106
// Exponential backoff configuration
@@ -213,16 +217,36 @@ var waitForConnection = func(ctx context.Context, conn ClientConn, timeout time.
213217

214218
for {
215219
state := conn.GetState()
220+
221+
// Debug log for every state observation
222+
logtrace.Debug(timeoutCtx, "gRPC connection state",
223+
logtrace.Fields{
224+
"state": state.String(),
225+
},
226+
)
227+
216228
switch state {
217229
case connectivity.Ready:
230+
logtrace.Debug(timeoutCtx, "gRPC connection is READY", nil)
218231
return nil
232+
219233
case connectivity.Shutdown:
234+
logtrace.Error(timeoutCtx, "gRPC connection is SHUTDOWN", nil)
220235
return fmt.Errorf("grpc connection is shutdown")
236+
221237
case connectivity.TransientFailure:
238+
logtrace.Error(timeoutCtx, "gRPC connection in TRANSIENT_FAILURE", nil)
222239
return fmt.Errorf("grpc connection is in transient failure")
240+
223241
default:
224-
// For Idle and Connecting states, wait for state change
242+
// Idle / Connecting wait for state change
225243
if !conn.WaitForStateChange(timeoutCtx, state) {
244+
logtrace.Error(timeoutCtx, "Timeout waiting for gRPC connection state change",
245+
logtrace.Fields{
246+
"last_state": state.String(),
247+
"timeout": timeout.String(),
248+
},
249+
)
226250
return fmt.Errorf("timeout waiting for grpc connection state change")
227251
}
228252
}

sdk/action/client.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,15 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath
276276
rnd, _ := crand.Int(crand.Reader, big.NewInt(100))
277277
ic := uint32(rnd.Int64() + 1) // 1..100
278278
// Create signatures from the layout struct
279-
indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyring(layout, c.keyring, c.config.Account.KeyName, ic, max)
280-
if err != nil {
281-
return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("create signatures: %w", err)
282-
}
283-
279+
// get bech32 address for this key
280+
281+
indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyringADR36(
282+
layout,
283+
c.keyring,
284+
c.config.Account.KeyName,
285+
ic,
286+
max,
287+
)
284288
// Compute data hash (blake3) as base64 using a streaming file hash to avoid loading entire file
285289
h, err := utils.Blake3HashFile(filePath)
286290
if err != nil {

sdk/adapters/lumera/adapter.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"sort"
7+
"strings"
78
"time"
89

910
"github.com/LumeraProtocol/supernode/v2/sdk/log"
@@ -404,8 +405,8 @@ func toSdkSupernodes(resp *sntypes.QueryGetTopSuperNodesForBlockResponse) []Supe
404405
}
405406

406407
result = append(result, Supernode{
407-
CosmosAddress: sn.SupernodeAccount,
408-
GrpcEndpoint: ipAddress,
408+
CosmosAddress: strings.TrimSpace(sn.SupernodeAccount),
409+
GrpcEndpoint: strings.TrimSpace(ipAddress),
409410
State: SUPERNODE_STATE_ACTIVE,
410411
})
411412
}

sdk/adapters/lumera/types.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,16 @@ type Supernode struct {
4242
GrpcEndpoint string // Network endpoint for gRPC communication
4343
State SUPERNODE_STATE // Current state of the supernode
4444
}
45+
46+
func (s Supernodes) String() string {
47+
result := "["
48+
for i, sn := range s {
49+
result += sn.CosmosAddress + "@" + sn.GrpcEndpoint
50+
51+
if i < len(s)-1 {
52+
result += ", "
53+
}
54+
}
55+
result += "]"
56+
return result
57+
}

sdk/net/factory.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func NewClientFactory(ctx context.Context, logger log.Logger, keyring keyring.Ke
4444
// Increase per-stream window to provide headroom for first data chunk + events
4545
opts.InitialWindowSize = 12 * 1024 * 1024 // 8MB per-stream window
4646
opts.InitialConnWindowSize = 64 * 1024 * 1024 // 64MB per-connection window
47+
opts.Logger = logger
4748

4849
return &ClientFactory{
4950
logger: logger,

sdk/net/impl.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ func NewSupernodeClient(ctx context.Context, logger log.Logger, keyring keyring.
7676
targetSupernode.GrpcEndpoint,
7777
)
7878

79-
logger.Info(ctx, "Connecting to supernode securely", "endpoint", targetSupernode.GrpcEndpoint, "target_id", targetSupernode.CosmosAddress, "local_id", factoryConfig.LocalCosmosAddress, "peer_type", factoryConfig.PeerType)
79+
logger.Debug(ctx, "Preparing to connect to supernode securely",
80+
"endpoint", targetSupernode.GrpcEndpoint, "target_id", targetSupernode.CosmosAddress,
81+
"local_id", factoryConfig.LocalCosmosAddress, "peer_type", factoryConfig.PeerType)
8082

8183
// Use provided client options or defaults
8284
options := clientOptions
@@ -93,7 +95,7 @@ func NewSupernodeClient(ctx context.Context, logger log.Logger, keyring keyring.
9395
targetSupernode.CosmosAddress, err)
9496
}
9597

96-
logger.Info(ctx, "Connected to supernode securely", "address", targetSupernode.CosmosAddress, "endpoint", targetSupernode.GrpcEndpoint)
98+
logger.Debug(ctx, "Connected to supernode securely", "address", targetSupernode.CosmosAddress, "endpoint", targetSupernode.GrpcEndpoint)
9799

98100
// Create service clients
99101
cascadeClient := supernodeservice.NewCascadeAdapter(
@@ -116,9 +118,7 @@ func (c *supernodeClient) RegisterCascade(ctx context.Context, in *supernodeserv
116118
if err != nil {
117119
return nil, fmt.Errorf("cascade registration failed: %w", err)
118120
}
119-
120-
c.logger.Info(ctx, "Cascade registered successfully",
121-
"actionID", in.ActionID, "taskId", in.TaskId)
121+
c.logger.Info(ctx, "Cascade Registration request sent to supernode", "actionID", in.ActionID, "taskId", in.TaskId)
122122

123123
return resp, nil
124124
}
@@ -140,7 +140,7 @@ func (c *supernodeClient) GetSupernodeStatus(ctx context.Context) (*pb.StatusRes
140140
return nil, fmt.Errorf("failed to get supernode status: %w", err)
141141
}
142142

143-
c.logger.Debug(ctx, "Supernode status retrieved successfully")
143+
c.logger.Debug(ctx, "Supernode status retrieved successfully", "response", resp.String())
144144
return resp, nil
145145
}
146146

sdk/task/cascade.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ func (t *CascadeTask) Run(ctx context.Context) error {
4646
return err
4747
}
4848

49+
t.logger.Debug(ctx, "Fetched supernodes",
50+
map[string]interface{}{
51+
"count": len(supernodes),
52+
"list": supernodes.String(),
53+
},
54+
)
55+
4956
// 2 - Pre-filter: balance & health concurrently -> XOR rank, then hand over
5057
originalCount := len(supernodes)
5158
supernodes, preClients := t.filterEligibleSupernodesParallel(ctx, supernodes)
@@ -161,9 +168,23 @@ func (t *CascadeTask) attemptRegistration(ctx context.Context, _ int, sn lumera.
161168
// Use ctx directly; per-phase timers are applied inside the adapter
162169
resp, err := client.RegisterCascade(ctx, req)
163170
if err != nil {
171+
t.logger.Error(ctx, "RegisterCascade RPC failed",
172+
map[string]interface{}{
173+
"supernode": sn.GrpcEndpoint,
174+
"address": sn.CosmosAddress,
175+
"error": err.Error(),
176+
},
177+
)
164178
return fmt.Errorf("upload to %s: %w", sn.CosmosAddress, err)
165179
}
166180
if !resp.Success {
181+
t.logger.Error(ctx, "RegisterCascade RPC rejected",
182+
map[string]interface{}{
183+
"supernode": sn.GrpcEndpoint,
184+
"address": sn.CosmosAddress,
185+
"message": resp.Message,
186+
},
187+
)
167188
return fmt.Errorf("upload rejected by %s: %s", sn.CosmosAddress, resp.Message)
168189
}
169190

0 commit comments

Comments
 (0)