Skip to content

Conversation

chiulam
Copy link

@chiulam chiulam commented Aug 27, 2025

Add Ethereum secp256k1 address support

Summary

This PR adds support for Ethereum-style secp256k1 addresses in CosmJS, enabling compatibility with Ethereum-derived address formats in Cosmos chains.

Changes

  • Added rawEthSecp256k1PubkeyToRawAddress() function to convert compressed Ethereum secp256k1 public keys to raw addresses
  • Updated pubkeyToRawAddress() to handle EthSecp256k1Pubkey type
  • Added support for keccak256 hashing for Ethereum address derivation
  • Imported necessary dependencies (keccak256, Secp256k1) from @cosmjs/crypto

Technical Details

  • Ethereum addresses are derived by taking the keccak256 hash of the uncompressed public key (without the 0x04 prefix) and using the last 20 bytes
  • The implementation follows Ethereum's address derivation standard while maintaining compatibility with existing Cosmos address formats
  • Added proper validation for compressed public key length (33 bytes)

Breaking Changes

None - this is a purely additive change that maintains backward compatibility.

@chiulam chiulam marked this pull request as draft August 27, 2025 08:34
allthatjazzleo and others added 6 commits August 27, 2025 16:37
- Implemented encoding for EthSecp256k1 public keys in amino format.
- Added functions to encode and decode EthSecp256k1 signatures.
- Updated pubkey type definitions to include EthSecp256k1.
- Enhanced signing clients to handle EthSecp256k1 accounts.
- Created DirectEthSecp256k1HdWallet for managing EthSecp256k1 keypairs.
- Added tests for DirectEthSecp256k1HdWallet and DirectEthSecp256k1Wallet functionalities.
@chiulam chiulam force-pushed the feat/support-cosmos-evm branch from 6e044f9 to ce7e8b9 Compare August 27, 2025 08:39
@chiulam chiulam marked this pull request as ready for review August 27, 2025 08:40
}
const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey));
let pubkey;
if (accountFromSigner.algo == "eth_secp256k1" || accountFromSigner.algo == "ethsecp256k1" ) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to add a comment here to explain why you need eth_secp256k1 and ethsecp256k1.

@chiulam chiulam requested a review from BigtoC August 27, 2025 09:58
}

export function isEthSecp256k1Pubkey(pubkey: Pubkey): pubkey is EthSecp256k1Pubkey {
return (pubkey as EthSecp256k1Pubkey).type === "os/PubKeyEthSecp256k1";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a typo? os/PubKeyEthSecp256k1

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

What does "os/" mean in this context?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a legacy from evmOS?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems so. That is the earliest example of it I can see publicly within github.

* @param account The account data from a signer
* @returns The amino-encoded pubkey (EthSecp256k1Pubkey or Secp256k1Pubkey)
*/
export function getAminoPubkey(account: AccountData): any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no good reason for the return type here to be any.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the code. Please have a look.

Copy link
Contributor

@dynst dynst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function isn't updated:

export function isSinglePubkey(pubkey: Pubkey): pubkey is SinglePubkey {
const singPubkeyTypes: string[] = [pubkeyType.ed25519, pubkeyType.secp256k1, pubkeyType.sr25519];
return singPubkeyTypes.includes(pubkey.type);
}

@BigtoC
Copy link

BigtoC commented Sep 2, 2025

This function isn't updated:

export function isSinglePubkey(pubkey: Pubkey): pubkey is SinglePubkey {
const singPubkeyTypes: string[] = [pubkeyType.ed25519, pubkeyType.secp256k1, pubkeyType.sr25519];
return singPubkeyTypes.includes(pubkey.type);
}

I have updated the code. Please have a look.

Copy link
Member

@webmaster128 webmaster128 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, looks pretty nice. Glad to see that the quality of evm signing imrpoved a lot on the chain side of things which makes it smooth to integrate.

return ripemd160(sha256(pubkeyData));
}

export function rawEthSecp256k1PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Array {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An "raw Eth Secp256k1 Pubkey" is uncompressed by convention. All Ethereum ecosystem uses uncompressed pubkeys. So either the implementation is wrong but maybe it is a question of findung a better name for the function

I am wrong I guess and compressed pubkeys are used consostently in Cosmos, also evm code. Any thoughts on that matter?

Copy link

@BigtoC BigtoC Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the codes in cosmos/evmethsecp256k1.go, the PubKey() method returns a 33-byte compressed pubkey.

But when it derives an address, the Address() method first decompresses the pubkey, then uses the uncompressed pubkey to derive an address.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that helps. Sounds like a reasonable approach for the Cosmos Stack

// Prefixes listed here: https://github.com/tendermint/tendermint/blob/d419fffe18531317c28c29a292ad7d253f6cafdf/docs/spec/blockchain/encoding.md#public-key-cryptography
// Last bytes is varint-encoded length prefix
const pubkeyAminoPrefixSecp256k1 = fromHex("eb5ae987" + "21" /* fixed length */);
const pubkeyAminoPrefixEthSecp256k1 = fromHex("5D7423DF" + "21" /* fixed length */);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sweet, we now got a new Amino prefix for that? Could you add a reference to any documentation or source code where this is coming from?

This was a major blocker in earlier attempts to add EVM support to CosmJS

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prove pubkeyAminoPrefixEthSecp256k1 is correct

1. Reproduce the existing prefix to prove my method is correct

According to the Amino document, I compute the below result, which matches the existing pubkeyAminoPrefixSecp256k1.
image

2. Use the same method to compute pubkeyAminoPrefixEthSecp256k1

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow this document you found there is amazing. I did not know that there was an algorithm for calculating those prefixes. Super nice, thanks!

}

export function isEthSecp256k1Pubkey(pubkey: Pubkey): pubkey is EthSecp256k1Pubkey {
return (pubkey as EthSecp256k1Pubkey).type === "os/PubKeyEthSecp256k1";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

What does "os/" mean in this context?


export interface EthSecp256k1Pubkey extends SinglePubkey {
readonly type: "os/PubKeyEthSecp256k1";
readonly value: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: is this a compressed or uncompressed encoding of the pubkey? In earlier versions of Ethermint this was uncompressed (but barely documented). Now looking at the code above it seems to be compressed.

secp256k1: "tendermint/PubKeySecp256k1" as const,
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 */
secp256k1: "tendermint/PubKeySecp256k1" as const,
/** @see https://github.com/cosmos/evm/blob/main/crypto/ethsecp256k1/ethsecp256k1.go#L36 */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** @see https://github.com/cosmos/evm/blob/main/crypto/ethsecp256k1/ethsecp256k1.go#L36 */
/** @see https://github.com/cosmos/evm/blob/v1.0.0-rc2/crypto/ethsecp256k1/ethsecp256k1.go#L35-L36 */

data: toBase64(encryptedData),
};
return JSON.stringify(out);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please remove all references to kdf/serialize/deserialize from the newly added wallet? I know it is there for consistency but we will remove that functionality sooninsh and don't want new users to start with it. See #1796

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked, and the serialize/deserialize functions are only referenced in test codes.

/**
* A wallet that holds a single secp256k1 keypair.
*
* If you want to work with BIP39 mnemonics and multiple accounts, use DirectSecp256k1HdWallet.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* If you want to work with BIP39 mnemonics and multiple accounts, use DirectSecp256k1HdWallet.
* If you want to work with BIP39 mnemonics and multiple accounts, use DirectEthSecp256k1HdWallet.

value: Uint8Array.from(CosmosCryptoSecp256k1Pubkey.encode(pubkeyProto).finish()),
});
} else if (isEthSecp256k1Pubkey(pubkey)) {
const pubkeyProto = CosmosCryptoSecp256k1Pubkey.fromPartial({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const pubkeyProto = CosmosCryptoSecp256k1Pubkey.fromPartial({
// Note: This code block is hacky because we should use the correct EVM proto type
// https://github.com/cosmos/evm/blob/v1.0.0-rc2/proto/cosmos/evm/crypto/v1/ethsecp256k1/keys.proto#L12-L17.
// However, we do not have that available here or in cosmjs-types yet and the classic secp256k1 pubkey has the same structure except for documentation and annotations:
// https://github.com/cosmos/cosmos-sdk/blob/v0.53.4/proto/cosmos/crypto/secp256k1/keys.proto#L14-L30
//
// Actually this type is so simple we should just have an Anybuf for TS instead of code generation (https://github.com/webmaster128/anybuf)
const pubkeyProto = CosmosCryptoSecp256k1Pubkey.fromPartial({

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just adds a comment, no code change

}

export function isEthSecp256k1Pubkey(pubkey: Pubkey): pubkey is EthSecp256k1Pubkey {
return (pubkey as EthSecp256k1Pubkey).type === "os/PubKeyEthSecp256k1";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should just use pubkey.type here, there's no reason to cast it before the check.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also remove type casting in this line? https://github.com/cosmos/cosmjs/blob/main/packages/amino/src/pubkeys.ts#L24

return (pubkey as Secp256k1Pubkey).type === "tendermint/PubKeySecp256k1";

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! #1847

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants