diff --git a/pkg/keyring/keyring.go b/pkg/keyring/keyring.go index 1a116041..76fb1f67 100644 --- a/pkg/keyring/keyring.go +++ b/pkg/keyring/keyring.go @@ -219,3 +219,15 @@ func GetAddress(kr sdkkeyring.Keyring, name string) (types.AccAddress, error) { } return rec.GetAddress() } + +func GetBech32Address(kr sdkkeyring.Keyring, name, hrp string) (string, error) { + rec, err := kr.Key(name) + if err != nil { + return "", err + } + addr, err := rec.GetAddress() + if err != nil { + return "", err + } + return types.Bech32ifyAddressBytes(hrp, addr.Bytes()) +} diff --git a/pkg/keyring/keyring_test.go b/pkg/keyring/keyring_test.go new file mode 100644 index 00000000..828999be --- /dev/null +++ b/pkg/keyring/keyring_test.go @@ -0,0 +1,44 @@ +package keyring + +import ( + "bytes" + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/hd" + sdkkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestGetBech32Address(t *testing.T) { + reg := codectypes.NewInterfaceRegistry() + cryptocodec.RegisterInterfaces(reg) + cdc := codec.NewProtoCodec(reg) + kr := sdkkeyring.NewInMemory(cdc) + + rec, _, err := kr.NewMnemonic("test", sdkkeyring.English, DefaultHDPath, DefaultBIP39Passphrase, hd.Secp256k1) + if err != nil { + t.Fatalf("new mnemonic: %v", err) + } + addr, err := rec.GetAddress() + if err != nil { + t.Fatalf("get address: %v", err) + } + + got, err := GetBech32Address(kr, "test", "lumera") + if err != nil { + t.Fatalf("get bech32 address: %v", err) + } + if got == "" { + t.Fatal("empty bech32 address") + } + bz, err := sdk.GetFromBech32(got, "lumera") + if err != nil { + t.Fatalf("decode bech32: %v", err) + } + if !bytes.Equal(bz, addr.Bytes()) { + t.Fatalf("decoded address mismatch") + } +} diff --git a/sdk/action/client.go b/sdk/action/client.go index 63f56762..ec7c45c1 100644 --- a/sdk/action/client.go +++ b/sdk/action/client.go @@ -23,7 +23,7 @@ import ( actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" "github.com/LumeraProtocol/supernode/v2/pkg/cascadekit" "github.com/LumeraProtocol/supernode/v2/pkg/codec" - keyringpkg "github.com/LumeraProtocol/supernode/v2/pkg/keyring" + snkeyring "github.com/LumeraProtocol/supernode/v2/pkg/keyring" "github.com/LumeraProtocol/supernode/v2/pkg/logtrace" "github.com/LumeraProtocol/supernode/v2/pkg/utils" "github.com/cosmos/cosmos-sdk/crypto/keyring" @@ -68,6 +68,7 @@ type ClientImpl struct { // Verify interface compliance at compile time var _ Client = (*ClientImpl)(nil) +var newLumeraAdapter = lumera.NewAdapter // NewClient creates a new action client func NewClient(ctx context.Context, config config.Config, logger log.Logger) (Client, error) { @@ -75,13 +76,14 @@ func NewClient(ctx context.Context, config config.Config, logger log.Logger) (Cl logger = log.NewNoopLogger() } - addr, err := keyringpkg.GetAddress(config.Account.Keyring, config.Account.KeyName) + // Enforce Lumera HRP for secure transport identity, independent of global SDK config. + addr, err := snkeyring.GetBech32Address(config.Account.Keyring, config.Account.KeyName, snkeyring.AccountAddressPrefix) if err != nil { return nil, fmt.Errorf("resolve signer address: %w", err) } // Create lumera client once - lumeraClient, err := lumera.NewAdapter(ctx, + lumeraClient, err := newLumeraAdapter(ctx, lumera.ConfigParams{ GRPCAddr: config.Lumera.GRPCAddr, ChainID: config.Lumera.ChainID, @@ -105,7 +107,7 @@ func NewClient(ctx context.Context, config config.Config, logger log.Logger) (Cl logger: logger, keyring: config.Account.Keyring, lumeraClient: lumeraClient, - signerAddr: addr.String(), + signerAddr: addr, }, nil } @@ -361,7 +363,7 @@ func (c *ClientImpl) GenerateStartCascadeSignatureFromFileDeprecated(ctx context return "", fmt.Errorf("blake3: %w", err) } dataHashB64 := base64.StdEncoding.EncodeToString(h) - sig, err := keyringpkg.SignBytes(c.keyring, c.config.Account.KeyName, []byte(dataHashB64)) + sig, err := snkeyring.SignBytes(c.keyring, c.config.Account.KeyName, []byte(dataHashB64)) if err != nil { return "", fmt.Errorf("sign hash string: %w", err) } @@ -441,7 +443,7 @@ func (c *ClientImpl) GenerateDownloadSignature(ctx context.Context, actionID, cr c.logger.Info(ctx, "Signing download with ICA signer", "signer", signerAddr, "key_name", keyName) } // Sign only the actionID using raw bytes so verification can succeed without ADR-36 signer context. - sig, err := keyringpkg.SignBytes(c.keyring, keyName, []byte(actionID)) + sig, err := snkeyring.SignBytes(c.keyring, keyName, []byte(actionID)) if err != nil { return "", fmt.Errorf("sign download payload: %w", err) } diff --git a/sdk/action/client_test.go b/sdk/action/client_test.go new file mode 100644 index 00000000..c9013953 --- /dev/null +++ b/sdk/action/client_test.go @@ -0,0 +1,71 @@ +package action + +import ( + "context" + "testing" + + snkeyring "github.com/LumeraProtocol/supernode/v2/pkg/keyring" + sdkconfig "github.com/LumeraProtocol/supernode/v2/sdk/config" + "github.com/LumeraProtocol/supernode/v2/sdk/log" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/hd" + sdkkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/LumeraProtocol/supernode/v2/sdk/adapters/lumera" +) + +func TestNewClientUsesLumeraBech32(t *testing.T) { + kr := newTestKeyring(t) + rec, _, err := kr.NewMnemonic("test", sdkkeyring.English, snkeyring.DefaultHDPath, snkeyring.DefaultBIP39Passphrase, hd.Secp256k1) + if err != nil { + t.Fatalf("new mnemonic: %v", err) + } + addr, err := rec.GetAddress() + if err != nil { + t.Fatalf("get address: %v", err) + } + lumeraAddr, err := sdk.Bech32ifyAddressBytes(snkeyring.AccountAddressPrefix, addr.Bytes()) + if err != nil { + t.Fatalf("bech32ify lumera: %v", err) + } + cosmosAddr, err := sdk.Bech32ifyAddressBytes("cosmos", addr.Bytes()) + if err != nil { + t.Fatalf("bech32ify cosmos: %v", err) + } + + origAdapter := newLumeraAdapter + defer func() { newLumeraAdapter = origAdapter }() + newLumeraAdapter = func(ctx context.Context, cfg lumera.ConfigParams, logger log.Logger) (lumera.Client, error) { + return nil, nil + } + + cfg := sdkconfig.Config{ + Account: sdkconfig.AccountConfig{KeyName: "test", Keyring: kr}, + Lumera: sdkconfig.LumeraConfig{GRPCAddr: "127.0.0.1:1", ChainID: "lumera-test"}, + } + client, err := NewClient(context.Background(), cfg, log.NewNoopLogger()) + if err != nil { + t.Fatalf("new client: %v", err) + } + impl, ok := client.(*ClientImpl) + if !ok { + t.Fatalf("expected *ClientImpl, got %T", client) + } + if impl.signerAddr != lumeraAddr { + t.Fatalf("signer address mismatch: got %s want %s", impl.signerAddr, lumeraAddr) + } + if impl.signerAddr == cosmosAddr { + t.Fatalf("signer address not normalized: got %s", impl.signerAddr) + } +} + +func newTestKeyring(t *testing.T) sdkkeyring.Keyring { + t.Helper() + reg := codectypes.NewInterfaceRegistry() + cryptocodec.RegisterInterfaces(reg) + cdc := codec.NewProtoCodec(reg) + return sdkkeyring.NewInMemory(cdc) +} diff --git a/sdk/net/factory.go b/sdk/net/factory.go index b0356997..7af48dc9 100644 --- a/sdk/net/factory.go +++ b/sdk/net/factory.go @@ -9,7 +9,7 @@ import ( "github.com/LumeraProtocol/supernode/v2/sdk/adapters/lumera" "github.com/LumeraProtocol/supernode/v2/sdk/log" - keyringpkg "github.com/LumeraProtocol/supernode/v2/pkg/keyring" + snkeyring "github.com/LumeraProtocol/supernode/v2/pkg/keyring" "github.com/cosmos/cosmos-sdk/crypto/keyring" ) @@ -35,7 +35,8 @@ func NewClientFactory(ctx context.Context, logger log.Logger, keyring keyring.Ke logger = log.NewNoopLogger() } - addr, err := keyringpkg.GetAddress(keyring, config.KeyName) + // Enforce Lumera HRP for secure transport identity, independent of global SDK config. + addr, err := snkeyring.GetBech32Address(keyring, config.KeyName, snkeyring.AccountAddressPrefix) if err != nil { logger.Error(ctx, "failed to resolve signer address from keyring", map[string]interface{}{"key_name": config.KeyName, "error": err.Error()}, @@ -60,7 +61,7 @@ func NewClientFactory(ctx context.Context, logger log.Logger, keyring keyring.Ke clientOptions: opts, config: config, lumeraClient: lumeraClient, - signerAddr: addr.String(), + signerAddr: addr, }, nil } diff --git a/sdk/net/factory_test.go b/sdk/net/factory_test.go new file mode 100644 index 00000000..e4f9c7eb --- /dev/null +++ b/sdk/net/factory_test.go @@ -0,0 +1,88 @@ +package net + +import ( + "context" + "testing" + + snkeyring "github.com/LumeraProtocol/supernode/v2/pkg/keyring" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/hd" + sdkkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func newTestKeyring(t *testing.T) sdkkeyring.Keyring { + t.Helper() + reg := codectypes.NewInterfaceRegistry() + cryptocodec.RegisterInterfaces(reg) + cdc := codec.NewProtoCodec(reg) + return sdkkeyring.NewInMemory(cdc) +} + +func TestNewClientFactoryUsesLumeraBech32(t *testing.T) { + kr := newTestKeyring(t) + rec, _, err := kr.NewMnemonic("test", sdkkeyring.English, snkeyring.DefaultHDPath, snkeyring.DefaultBIP39Passphrase, hd.Secp256k1) + if err != nil { + t.Fatalf("new mnemonic: %v", err) + } + addr, err := rec.GetAddress() + if err != nil { + t.Fatalf("get address: %v", err) + } + expected, err := sdk.Bech32ifyAddressBytes(snkeyring.AccountAddressPrefix, addr.Bytes()) + if err != nil { + t.Fatalf("bech32ify: %v", err) + } + + factory, err := NewClientFactory(context.Background(), nil, kr, nil, FactoryConfig{KeyName: "test"}) + if err != nil { + t.Fatalf("new client factory: %v", err) + } + if factory == nil { + t.Fatal("expected factory") + } + if factory.signerAddr != expected { + t.Fatalf("signer address mismatch: got %s want %s", factory.signerAddr, expected) + } +} + +func TestNewClientFactoryNormalizesNonLumeraPrefix(t *testing.T) { + kr := newTestKeyring(t) + rec, _, err := kr.NewMnemonic("test", sdkkeyring.English, snkeyring.DefaultHDPath, snkeyring.DefaultBIP39Passphrase, hd.Secp256k1) + if err != nil { + t.Fatalf("new mnemonic: %v", err) + } + addr, err := rec.GetAddress() + if err != nil { + t.Fatalf("get address: %v", err) + } + cosmosAddr, err := sdk.Bech32ifyAddressBytes("cosmos", addr.Bytes()) + if err != nil { + t.Fatalf("bech32ify cosmos: %v", err) + } + expected, err := sdk.Bech32ifyAddressBytes(snkeyring.AccountAddressPrefix, addr.Bytes()) + if err != nil { + t.Fatalf("bech32ify lumera: %v", err) + } + + factory, err := NewClientFactory(context.Background(), nil, kr, nil, FactoryConfig{KeyName: "test"}) + if err != nil { + t.Fatalf("new client factory: %v", err) + } + if factory.signerAddr == cosmosAddr { + t.Fatalf("signer address was not normalized: got %s", factory.signerAddr) + } + if factory.signerAddr != expected { + t.Fatalf("signer address mismatch: got %s want %s", factory.signerAddr, expected) + } +} + +func TestNewClientFactoryMissingKey(t *testing.T) { + kr := newTestKeyring(t) + _, err := NewClientFactory(context.Background(), nil, kr, nil, FactoryConfig{KeyName: "missing"}) + if err == nil { + t.Fatal("expected error") + } +} diff --git a/sdk/net/impl.go b/sdk/net/impl.go index b54a08bb..8ce683b8 100644 --- a/sdk/net/impl.go +++ b/sdk/net/impl.go @@ -14,7 +14,7 @@ import ( "github.com/LumeraProtocol/supernode/v2/sdk/log" pb "github.com/LumeraProtocol/supernode/v2/gen/supernode" - keyringpkg "github.com/LumeraProtocol/supernode/v2/pkg/keyring" + snkeyring "github.com/LumeraProtocol/supernode/v2/pkg/keyring" "github.com/cosmos/cosmos-sdk/crypto/keyring" "google.golang.org/grpc" "google.golang.org/grpc/health/grpc_health_v1" @@ -33,6 +33,7 @@ var _ SupernodeClient = (*supernodeClient)(nil) // ensure ALTS protocols are registered once per process var registerALTSOnce sync.Once +var newClientCreds = ltc.NewClientCreds // NewSupernodeClient creates a new supernode client func NewSupernodeClient(ctx context.Context, logger log.Logger, keyring keyring.Keyring, @@ -55,16 +56,17 @@ func NewSupernodeClient(ctx context.Context, logger log.Logger, keyring keyring. factoryConfig.PeerType = securekeyx.Simplenode } - addr, err := keyringpkg.GetAddress(keyring, factoryConfig.KeyName) + // Enforce Lumera HRP for secure transport identity, independent of global SDK config. + addr, err := snkeyring.GetBech32Address(keyring, factoryConfig.KeyName, snkeyring.AccountAddressPrefix) if err != nil { return nil, fmt.Errorf("resolve signer address: %w", err) } // Create client credentials - clientCreds, err := ltc.NewClientCreds(<c.ClientOptions{ + clientCreds, err := newClientCreds(<c.ClientOptions{ CommonOptions: ltc.CommonOptions{ Keyring: keyring, - LocalIdentity: addr.String(), + LocalIdentity: addr, PeerType: factoryConfig.PeerType, Validator: lumeraClient, }, @@ -81,7 +83,7 @@ func NewSupernodeClient(ctx context.Context, logger log.Logger, keyring keyring. logger.Debug(ctx, "Preparing to connect to supernode securely", "endpoint", targetSupernode.GrpcEndpoint, "target_id", targetSupernode.CosmosAddress, - "local_id", addr.String(), "peer_type", factoryConfig.PeerType) + "local_id", addr, "peer_type", factoryConfig.PeerType) // Use provided client options or defaults options := clientOptions diff --git a/sdk/net/impl_test.go b/sdk/net/impl_test.go new file mode 100644 index 00000000..3f359bc9 --- /dev/null +++ b/sdk/net/impl_test.go @@ -0,0 +1,51 @@ +package net + +import ( + "context" + "fmt" + "testing" + + snkeyring "github.com/LumeraProtocol/supernode/v2/pkg/keyring" + ltc "github.com/LumeraProtocol/supernode/v2/pkg/net/credentials" + "github.com/LumeraProtocol/supernode/v2/sdk/adapters/lumera" + "github.com/LumeraProtocol/supernode/v2/sdk/log" + "github.com/cosmos/cosmos-sdk/crypto/hd" + sdkkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + grpcCreds "google.golang.org/grpc/credentials" +) + +func TestNewSupernodeClientUsesLumeraBech32(t *testing.T) { + kr := newTestKeyring(t) + rec, _, err := kr.NewMnemonic("test", sdkkeyring.English, snkeyring.DefaultHDPath, snkeyring.DefaultBIP39Passphrase, hd.Secp256k1) + if err != nil { + t.Fatalf("new mnemonic: %v", err) + } + addr, err := rec.GetAddress() + if err != nil { + t.Fatalf("get address: %v", err) + } + expected, err := sdk.Bech32ifyAddressBytes(snkeyring.AccountAddressPrefix, addr.Bytes()) + if err != nil { + t.Fatalf("bech32ify: %v", err) + } + + origCreds := newClientCreds + defer func() { newClientCreds = origCreds }() + var got string + newClientCreds = func(opts *ltc.ClientOptions) (grpcCreds.TransportCredentials, error) { + got = opts.CommonOptions.LocalIdentity + return nil, fmt.Errorf("stub creds error") + } + + _, err = NewSupernodeClient(context.Background(), log.NewNoopLogger(), kr, FactoryConfig{KeyName: "test"}, lumera.Supernode{}, nil, nil) + if err == nil { + t.Fatal("expected error") + } + if got == "" { + t.Fatal("expected local identity to be captured") + } + if got != expected { + t.Fatalf("local identity mismatch: got %s want %s", got, expected) + } +} diff --git a/sdk/task/helpers.go b/sdk/task/helpers.go index ab6beb2b..3e2a4ff3 100644 --- a/sdk/task/helpers.go +++ b/sdk/task/helpers.go @@ -111,8 +111,7 @@ func (m *ManagerImpl) validateSignature(ctx context.Context, action lumera.Actio return fmt.Errorf("signature validation failed: %w", verifyErr) } -// - +// validateDownloadAction checks if the specified action is valid for downloading. func (m *ManagerImpl) validateDownloadAction(ctx context.Context, actionID string) (lumera.Action, error) { action, err := m.lumeraClient.GetAction(ctx, actionID) if err != nil { @@ -125,8 +124,8 @@ func (m *ManagerImpl) validateDownloadAction(ctx context.Context, actionID strin } // Check action state - if action.State != lumera.ACTION_STATE_DONE { - return lumera.Action{}, fmt.Errorf("action is in %s state, expected DONE", action.State) + if action.State != lumera.ACTION_STATE_DONE && action.State != lumera.ACTION_STATE_APPROVED { + return lumera.Action{}, fmt.Errorf("action is in %s state, expected DONE or APPROVED", action.State) } return action, nil