Skip to content
5 changes: 5 additions & 0 deletions client/keys/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ func printCreate(cmd *cobra.Command, info keyring.Info, showMnemonic bool, mnemo
return err
}

out, err = keyring.PopulateEvmAddrIfApplicable(info, out)
if err != nil {
return err
}

if showMnemonic {
out.Mnemonic = mnemonic
}
Expand Down
63 changes: 63 additions & 0 deletions client/keys/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,66 @@ func TestAddRecoverFileBackend(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "keyname1", info.GetName())
}

func Test_runAddCmdJSONEvmAddress(t *testing.T) {
cmd := AddKeyCommand()
cmd.Flags().AddFlagSet(Commands("home").PersistentFlags())

kbHome := t.TempDir()
mockIn := testutil.ApplyMockIODiscardOutErr(cmd)

clientCtx := client.Context{}.WithKeyringDir(kbHome).WithInput(mockIn)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)

b := bytes.NewBufferString("")
cmd.SetOut(b)

cmd.SetArgs([]string{
"test-evm-key",
fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome),
fmt.Sprintf("--%s=%s", cli.OutputFlag, OutputFormatJSON),
fmt.Sprintf("--%s=%s", flags.FlagKeyAlgorithm, string(hd.Secp256k1Type)),
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest),
})

require.NoError(t, cmd.ExecuteContext(ctx))

// Check that the JSON output contains an EVM address
output, err := ioutil.ReadAll(b)
require.NoError(t, err)

outputStr := string(output)
require.Contains(t, outputStr, `"evm_address"`)
require.Contains(t, outputStr, `"0x`)
require.NotContains(t, outputStr, `"evm_address":""`)
require.NotContains(t, outputStr, `"evm_address":null`)
}

func Test_PopulateEvmAddrError_JSONOutput(t *testing.T) {
// This test verifies that if PopulateEvmAddrIfApplicable returns an error,
// the add command properly handles it and returns the error

cmd := AddKeyCommand()
cmd.Flags().AddFlagSet(Commands("home").PersistentFlags())

kbHome := t.TempDir()
mockIn := testutil.ApplyMockIODiscardOutErr(cmd)

clientCtx := client.Context{}.WithKeyringDir(kbHome).WithInput(mockIn)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)

// Create a key with sr25519 algorithm - this should fail when PopulateEvmAddrIfApplicable
// tries to parse the sr25519 private key as secp256k1
cmd.SetArgs([]string{
"test-sr25519-key",
fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome),
fmt.Sprintf("--%s=%s", cli.OutputFlag, OutputFormatJSON),
fmt.Sprintf("--%s=%s", flags.FlagKeyAlgorithm, string(hd.Sr25519Type)),
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest),
})

// This should fail because sr25519 keys can't be used to generate EVM addresses
err := cmd.ExecuteContext(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), "unmarshal to types.PrivKey failed")
}
36 changes: 22 additions & 14 deletions crypto/keyring/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,28 @@ func MkAccKeysOutput(infos []Info) ([]KeyOutput, error) {
}

func PopulateEvmAddrIfApplicable(info Info, o KeyOutput) (KeyOutput, error) {
localInfo, ok := info.(LocalInfo)
if ok {
// Only works with secp256k1 algo
priv, err := legacy.PrivKeyFromBytes([]byte(localInfo.PrivKeyArmor))
if err != nil {
return o, err
}
privHex := hex.EncodeToString(priv.Bytes())
privKey, err := crypto.HexToECDSA(privHex)
if err != nil {
return o, err
}
o.EvmAddress = crypto.PubkeyToAddress(privKey.PublicKey).Hex()
} else {
var localInfo LocalInfo
switch v := info.(type) {
case LocalInfo:
localInfo = v
case *LocalInfo:
localInfo = *v
default:
return o, nil // non-local key – nothing to do
}

// Only secp256k1 keys produce an EVM address
priv, err := legacy.PrivKeyFromBytes([]byte(localInfo.PrivKeyArmor))
if err != nil {
return o, err
}

privHex := hex.EncodeToString(priv.Bytes())
privKey, err := crypto.HexToECDSA(privHex)
if err != nil {
return o, err
}

o.EvmAddress = crypto.PubkeyToAddress(privKey.PublicKey).Hex()
return o, nil
}
113 changes: 113 additions & 0 deletions crypto/keyring/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (

"github.com/stretchr/testify/require"

"github.com/cosmos/cosmos-sdk/codec/legacy"
"github.com/cosmos/cosmos-sdk/crypto/hd"
kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/crypto/types"
Expand Down Expand Up @@ -45,3 +47,114 @@ func TestMkAccKeyOutputForSr25519(t *testing.T) {
require.NoError(t, err)
require.Equal(t, expectedOutput, out)
}

func TestPopulateEvmAddrIfApplicable(t *testing.T) {
sk := secp256k1.GenPrivKey()
pubKey := sk.PubKey()

// PrivKeyArmor should contain amino-encoded private key bytes
aminoBytes, err := legacy.Cdc.Marshal(sk)
require.NoError(t, err)
privKeyArmor := string(aminoBytes)

tests := []struct {
name string
info Info
input KeyOutput
expectError bool
expectEvm bool
}{
{
name: "LocalInfo pointer case - should populate EVM address",
info: &LocalInfo{
Name: "test-key",
PubKey: pubKey,
PrivKeyArmor: privKeyArmor,
Algo: hd.Secp256k1Type,
},
input: KeyOutput{
Name: "test-key",
Type: "local",
Address: sdk.AccAddress(pubKey.Address()).String(),
PubKey: "",
},
expectError: false,
expectEvm: true,
},
{
name: "LocalInfo value case - should populate EVM address",
info: LocalInfo{
Name: "test-key",
PubKey: pubKey,
PrivKeyArmor: privKeyArmor,
Algo: hd.Secp256k1Type,
},
input: KeyOutput{
Name: "test-key",
Type: "local",
Address: sdk.AccAddress(pubKey.Address()).String(),
PubKey: "",
},
expectError: false,
expectEvm: true,
},
{
name: "Non-LocalInfo case - should return unchanged",
info: &multiInfo{
Name: "multi-key",
PubKey: pubKey,
Threshold: 1,
},
input: KeyOutput{
Name: "multi-key",
Type: "multi",
Address: sdk.AccAddress(pubKey.Address()).String(),
EvmAddress: "0x1234567890123456789012345678901234567890",
PubKey: "",
},
expectError: false,
expectEvm: false,
},
{
name: "Invalid private key armor - should return error",
info: &LocalInfo{
Name: "bad-key",
PubKey: pubKey,
PrivKeyArmor: "invalid-armor",
Algo: hd.Secp256k1Type,
},
input: KeyOutput{
Name: "bad-key",
Type: "local",
Address: sdk.AccAddress(pubKey.Address()).String(),
PubKey: "",
},
expectError: true,
expectEvm: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := PopulateEvmAddrIfApplicable(tt.info, tt.input)

if tt.expectError {
require.Error(t, err)
return
}

require.NoError(t, err)
require.Equal(t, tt.input.Name, result.Name)
require.Equal(t, tt.input.Type, result.Type)
require.Equal(t, tt.input.Address, result.Address)

if tt.expectEvm {
require.NotEmpty(t, result.EvmAddress)
require.Len(t, result.EvmAddress, 42) // 0x + 40 hex chars
require.True(t, result.EvmAddress[:2] == "0x")
} else {
require.Equal(t, tt.input.EvmAddress, result.EvmAddress)
}
})
}
}
Loading