Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f80d43d
feat: add support for EthSecp256k1 public keys and signatures
allthatjazzleo Jul 7, 2025
36d2e10
feat: add DirectEthSecp256k1 wallet exports to index
allthatjazzleo Jul 7, 2025
f811448
feat: add EthSecp256k1Pubkey to pubkeyToRawAddress function
allthatjazzleo Jul 8, 2025
c640c39
feat: support both eth_secp256k1 and ethsecp256k1 algorithms in pubke…
chiulam Aug 27, 2025
a489a7b
feat: support ethsecp256k1 algorithm in SigningStargateClient for pub…
chiulam Aug 27, 2025
ce7e8b9
refactor: consolidate crypto imports in addresses.ts
chiulam Aug 27, 2025
2b9d6ac
feat: add utility functions for Ethereum secp256k1 account handling a…
chiulam Aug 27, 2025
b303742
feat: add ethsecp256k1 to supported pubkey types in isSinglePubkey fu…
BigtoC Sep 1, 2025
d15842c
feat: add support for ethsecp256k1 pubkey type in encoding
BigtoC Sep 1, 2025
8ff0e05
chore: Specific getAminoPubkey return type instead of any
BigtoC Sep 1, 2025
eaa0ebf
Merge pull request #1 from BigtoC/fix/add-missing-ethsecp256k1-pubkey…
chiulam Sep 2, 2025
54b103c
test: add encoding and decoding functions for EthSecp256k1 signatures…
BigtoC Oct 9, 2025
bbffd36
refactor: remove eslint disable for naming convention in signingcosmw…
BigtoC Oct 9, 2025
5b4a48a
fix: add missing 'ethsecp256k1' algorithm type in Algo definition
BigtoC Oct 9, 2025
fada481
fix: update ethsecp256k1 references and class name in wallet document…
BigtoC Oct 9, 2025
03fa926
doc: Add comments for the code block
BigtoC Oct 9, 2025
17ad7ba
rollback .pnp.cjs
BigtoC Oct 9, 2025
5fee884
chore: lint fix
BigtoC Oct 9, 2025
2949808
fix: update getAminoPubkey return type and import missing pubkey types
BigtoC Oct 9, 2025
e77ae7a
Merge branch 'feat/support-cosmos-evm' into fix/add-missing-codes-and…
BigtoC Oct 9, 2025
5636a48
Merge pull request #2 from BigtoC/fix/add-missing-codes-and-update-fr…
chiulam Oct 9, 2025
4a5fd80
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions packages/amino/src/addresses.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography

import { ripemd160, sha256 } from "@cosmjs/crypto";
import { keccak256, ripemd160, Secp256k1, sha256 } from "@cosmjs/crypto";
import { fromBase64, toBech32 } from "@cosmjs/encoding";

import { encodeAminoPubkey } from "./encoding";
import { isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, Pubkey } from "./pubkeys";
import { isEd25519Pubkey, isEthSecp256k1Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, Pubkey } from "./pubkeys";

export function rawEd25519PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Array {
if (pubkeyData.length !== 32) {
Expand All @@ -20,11 +20,24 @@ export function rawSecp256k1PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Arr
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

if (pubkeyData.length !== 33) {
throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyData.length}`);
}
const uncompressed = Secp256k1.uncompressPubkey(pubkeyData);
const pubkeyWithoutPrefix = uncompressed.slice(1);
const hash = keccak256(pubkeyWithoutPrefix);
return hash.slice(-20);
}

// For secp256k1 this assumes we already have a compressed pubkey.
export function pubkeyToRawAddress(pubkey: Pubkey): Uint8Array {
if (isSecp256k1Pubkey(pubkey)) {
const pubkeyData = fromBase64(pubkey.value);
return rawSecp256k1PubkeyToRawAddress(pubkeyData);
} else if (isEthSecp256k1Pubkey(pubkey)) {
const pubkeyData = fromBase64(pubkey.value);
return rawEthSecp256k1PubkeyToRawAddress(pubkeyData);
} else if (isEd25519Pubkey(pubkey)) {
const pubkeyData = fromBase64(pubkey.value);
return rawEd25519PubkeyToRawAddress(pubkeyData);
Expand Down
15 changes: 15 additions & 0 deletions packages/amino/src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { arrayContentStartsWith } from "@cosmjs/utils";

import {
Ed25519Pubkey,
EthSecp256k1Pubkey,
isEd25519Pubkey,
isMultisigThresholdPubkey,
isSecp256k1Pubkey,
Expand Down Expand Up @@ -41,6 +42,20 @@ export function encodeEd25519Pubkey(pubkey: Uint8Array): Ed25519Pubkey {
};
}

/**
* Takes a EthSecp256k1 public key as raw bytes and returns the Amino JSON
* representation of it (the type/value wrapper object).
*/
export function encodeEthSecp256k1Pubkey(pubkey: Uint8Array): EthSecp256k1Pubkey {
if (pubkey.length !== 33 || (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)) {
throw new Error("Public key must be compressed secp256k1, i.e. 33 bytes starting with 0x02 or 0x03");
}
return {
type: pubkeyType.ethsecp256k1,
value: toBase64(pubkey),
};
}

// As discussed in https://github.com/binance-chain/javascript-sdk/issues/163
// 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
Expand Down
6 changes: 5 additions & 1 deletion packages/amino/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
pubkeyToRawAddress,
rawEd25519PubkeyToRawAddress,
rawSecp256k1PubkeyToRawAddress,
rawEthSecp256k1PubkeyToRawAddress,
} from "./addresses";
export type { Coin } from "./coins";
export { addCoins, coin, coins, parseCoins } from "./coins";
Expand All @@ -13,19 +14,22 @@ export {
encodeBech32Pubkey,
encodeEd25519Pubkey,
encodeSecp256k1Pubkey,
encodeEthSecp256k1Pubkey,
} from "./encoding";
export { createMultisigThresholdPubkey } from "./multisig";
export { omitDefault } from "./omitdefault";
export { makeCosmoshubPath } from "./paths";
export type {
Ed25519Pubkey,
EthSecp256k1Pubkey,
MultisigThresholdPubkey,
Pubkey,
Secp256k1Pubkey,
SinglePubkey,
} from "./pubkeys";
export {
isEd25519Pubkey,
isEthSecp256k1Pubkey,
isMultisigThresholdPubkey,
isSecp256k1Pubkey,
isSinglePubkey,
Expand All @@ -37,7 +41,7 @@ export {
Secp256k1HdWallet,
} from "./secp256k1hdwallet";
export { Secp256k1Wallet } from "./secp256k1wallet";
export { type StdSignature, decodeSignature, encodeSecp256k1Signature } from "./signature";
export { type StdSignature, decodeSignature, encodeEthSecp256k1Signature, encodeSecp256k1Signature } from "./signature";
export type { AminoMsg, StdFee, StdSignDoc } from "./signdoc";
export { makeSignDoc, serializeSignDoc } from "./signdoc";
export type { AccountData, Algo, AminoSignResponse, OfflineAminoSigner } from "./signer";
Expand Down
15 changes: 13 additions & 2 deletions packages/amino/src/pubkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,21 @@ export function isSecp256k1Pubkey(pubkey: Pubkey): pubkey is Secp256k1Pubkey {
return (pubkey as Secp256k1Pubkey).type === "tendermint/PubKeySecp256k1";
}

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.

}

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.

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

}

export const pubkeyType = {
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */
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 */

ethsecp256k1: "os/PubKeyEthSecp256k1" as const,
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */
ed25519: "tendermint/PubKeyEd25519" as const,
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */
sr25519: "tendermint/PubKeySr25519" as const,
Expand Down
26 changes: 25 additions & 1 deletion packages/amino/src/signature.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { fromBase64, toBase64 } from "@cosmjs/encoding";

import { encodeSecp256k1Pubkey } from "./encoding";
import { encodeEthSecp256k1Pubkey, encodeSecp256k1Pubkey } from "./encoding";
import { Pubkey, pubkeyType } from "./pubkeys";

export interface StdSignature {
Expand All @@ -28,6 +28,25 @@ export function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Arr
};
}

/**
* Takes a binary pubkey and signature to create a signature object
*
* @param pubkey a compressed secp256k1 public key
* @param signature a 64 byte fixed length representation of secp256k1 signature components r and s
*/
export function encodeEthSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature {
if (signature.length !== 64) {
throw new Error(
"Signature must be 64 bytes long. Cosmos SDK uses a 2x32 byte fixed length encoding for the secp256k1 signature integers r and s.",
);
}

return {
pub_key: encodeEthSecp256k1Pubkey(pubkey),
signature: toBase64(signature),
};
}

export function decodeSignature(signature: StdSignature): {
readonly pubkey: Uint8Array;
readonly signature: Uint8Array;
Expand All @@ -39,6 +58,11 @@ export function decodeSignature(signature: StdSignature): {
pubkey: fromBase64(signature.pub_key.value),
signature: fromBase64(signature.signature),
};
case pubkeyType.ethsecp256k1:
return {
pubkey: fromBase64(signature.pub_key.value),
signature: fromBase64(signature.signature),
};
default:
throw new Error("Unsupported pubkey type");
}
Expand Down
17 changes: 14 additions & 3 deletions packages/cosmwasm-stargate/src/signingcosmwasmclient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { encodeSecp256k1Pubkey, makeSignDoc as makeSignDocAmino } from "@cosmjs/amino";
/* eslint-disable @typescript-eslint/naming-convention */
import { encodeEthSecp256k1Pubkey, encodeSecp256k1Pubkey, makeSignDoc as makeSignDocAmino } from "@cosmjs/amino";
import { sha256 } from "@cosmjs/crypto";
import { fromBase64, toHex, toUtf8 } from "@cosmjs/encoding";
import { Int53, Uint53 } from "@cosmjs/math";
Expand Down Expand Up @@ -703,7 +704,12 @@ export class SigningCosmWasmClient extends CosmWasmClient {
if (!accountFromSigner) {
throw new Error("Failed to retrieve account from signer");
}
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.

pubkey = encodePubkey(encodeEthSecp256k1Pubkey(accountFromSigner.pubkey));
} else {
pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey));
}
const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON;
const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg));
const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence, timeoutHeight);
Expand Down Expand Up @@ -749,7 +755,12 @@ export class SigningCosmWasmClient extends CosmWasmClient {
if (!accountFromSigner) {
throw new Error("Failed to retrieve account from signer");
}
const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey));
let pubkey;
if (accountFromSigner.algo == "eth_secp256k1" || accountFromSigner.algo == "ethsecp256k1" ) {
pubkey = encodePubkey(encodeEthSecp256k1Pubkey(accountFromSigner.pubkey));
} else {
pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey));
}
const txBody: TxBodyEncodeObject = {
typeUrl: "/cosmos.tx.v1beta1.TxBody",
value: {
Expand Down
Loading