diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 0a056e9826..21be591f93 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -7,6 +7,7 @@ import { encodeAminoPubkey, encodeBech32Pubkey, encodeEd25519Pubkey, + encodeEthSecp256k1Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; import { Pubkey } from "./pubkeys"; @@ -58,6 +59,25 @@ describe("encoding", () => { }); }); + describe("encodeEthSecp256k1Pubkey", () => { + it("encodes a compressed pubkey", () => { + const pubkey = fromBase64("Ay+1uNze+glFQM+T05EfzL8fQ1Y/wqO8K7q6tUM3BGin"); + expect(encodeEthSecp256k1Pubkey(pubkey)).toEqual({ + type: "tendermint/PubKeyEthSecp256k1", + value: "Ay+1uNze+glFQM+T05EfzL8fQ1Y/wqO8K7q6tUM3BGin", + }); + }); + + it("throws for uncompressed public keys", () => { + const pubkey = fromBase64( + "BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=", + ); + expect(() => encodeEthSecp256k1Pubkey(pubkey)).toThrowError( + /public key must be compressed ethsecp256k1/i, + ); + }); + }); + describe("decodeAminoPubkey", () => { it("works for secp256k1", () => { const amino = fromBech32( @@ -69,6 +89,18 @@ describe("encoding", () => { }); }); + // @todo: find a way to bypass the identical prefix for secp256k1 and ethsecp256k1 + // in @amino/encoding:decodeAminoPubkey + // it("works for ethsecp256k1", () => { + // const amino = fromBech32( + // "evmospub1addwnpepqvhmtwxummaqj32qe7fa8yglejl37s6k8lp280pth2at2sehq352w6wm5v0", + // ).data; + // expect(decodeAminoPubkey(amino)).toEqual({ + // type: "tendermint/PubKeyEthSecp256k1", + // value: "Ay+1uNze+glFQM+T05EfzL8fQ1Y/wqO8K7q6tUM3BGin", + // }); + // }); + it("works for ed25519", () => { // Encoded from `corald tendermint show-validator` // Decoded from http://localhost:26657/validators @@ -143,6 +175,17 @@ describe("encoding", () => { }); }); + // @todo: find a way to bypass the identical prefix for secp256k1 and ethsecp256k1 + // in @amino/encoding:decodeAminoPubkey + // it("works for ethsecp256k1", () => { + // expect( + // decodeBech32Pubkey("evmospub1addwnpepqvhmtwxummaqj32qe7fa8yglejl37s6k8lp280pth2at2sehq352w6wm5v0"), + // ).toEqual({ + // type: "tendermint/PubKeyEthSecp256k1", + // value: "Ay+1uNze+glFQM+T05EfzL8fQ1Y/wqO8K7q6tUM3BGin", + // }); + // }); + it("works for enigma pubkey", () => { expect( decodeBech32Pubkey("enigmapub1addwnpepqw5k9p439nw0zpg2aundx4umwx4nw233z5prpjqjv5anl5grmnchzp2xwvv"), @@ -183,6 +226,17 @@ describe("encoding", () => { expect(encodeAminoPubkey(pubkey)).toEqual(expected); }); + it("works for ethsecp256k1", () => { + const pubkey: Pubkey = { + type: "tendermint/PubKeyEthSecp256k1", + value: "Ay+1uNze+glFQM+T05EfzL8fQ1Y/wqO8K7q6tUM3BGin", + }; + const expected = fromBech32( + "evmospub1addwnpepqvhmtwxummaqj32qe7fa8yglejl37s6k8lp280pth2at2sehq352w6wm5v0", + ).data; + expect(encodeAminoPubkey(pubkey)).toEqual(expected); + }); + it("works for ed25519", () => { // Decoded from http://localhost:26657/validators // Encoded from `corald tendermint show-validator` @@ -208,6 +262,16 @@ describe("encoding", () => { ); }); + it("works for ethsecp256k1", () => { + const pubkey: Pubkey = { + type: "tendermint/PubKeyEthSecp256k1", + value: "Ay+1uNze+glFQM+T05EfzL8fQ1Y/wqO8K7q6tUM3BGin", + }; + expect(encodeBech32Pubkey(pubkey, "evmospub")).toEqual( + "evmospub1addwnpepqvhmtwxummaqj32qe7fa8yglejl37s6k8lp280pth2at2sehq352w6wm5v0", + ); + }); + it("works for ed25519", () => { // Decoded from http://localhost:26657/validators // Encoded from `corald tendermint show-validator` diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index 0b069bcaea..3b2c394146 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -4,7 +4,9 @@ import { arrayContentStartsWith } from "@cosmjs/utils"; import { Ed25519Pubkey, + EthSecp256k1Pubkey, isEd25519Pubkey, + isEthSecp256k1Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, MultisigThresholdPubkey, @@ -27,6 +29,20 @@ export function encodeSecp256k1Pubkey(pubkey: Uint8Array): Secp256k1Pubkey { }; } +/** + * Takes an 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 ethsecp256k1, i.e. 33 bytes starting with 0x02 or 0x03"); + } + return { + type: pubkeyType.ethsecp256k1, + value: toBase64(pubkey), + }; +} + /** * Takes an Edd25519 public key as raw bytes and returns the Amino JSON * representation of it (the type/value wrapper object). @@ -45,6 +61,7 @@ export function encodeEd25519Pubkey(pubkey: Uint8Array): Ed25519Pubkey { // 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("eb5ae987" + "21" /* fixed length */); const pubkeyAminoPrefixEd25519 = fromHex("1624de64" + "20" /* fixed length */); const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005" + "20" /* fixed length */); /** See https://github.com/tendermint/tendermint/commit/38b401657e4ad7a7eeb3c30a3cbf512037df3740 */ @@ -52,6 +69,7 @@ const pubkeyAminoPrefixMultisigThreshold = fromHex("22c1f7e2" /* variable length /** * Decodes a pubkey in the Amino binary format to a type/value object. + * @todo: find a clean way to distinct Secp256k1 and EthSecp256k1 (has the same prefix) */ export function decodeAminoPubkey(data: Uint8Array): Pubkey { if (arrayContentStartsWith(data, pubkeyAminoPrefixSecp256k1)) { @@ -63,6 +81,15 @@ export function decodeAminoPubkey(data: Uint8Array): Pubkey { type: pubkeyType.secp256k1, value: toBase64(rest), }; + } else if (arrayContentStartsWith(data, pubkeyAminoPrefixEthSecp256k1)) { + const rest = data.slice(pubkeyAminoPrefixEthSecp256k1.length); + if (rest.length !== 33) { + throw new Error("Invalid rest data length. Expected 33 bytes (compressed ethsecp256k1 pubkey)."); + } + return { + type: pubkeyType.ethsecp256k1, + value: toBase64(rest), + }; } else if (arrayContentStartsWith(data, pubkeyAminoPrefixEd25519)) { const rest = data.slice(pubkeyAminoPrefixEd25519.length); if (rest.length !== 32) { @@ -207,6 +234,8 @@ export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { return new Uint8Array([...pubkeyAminoPrefixEd25519, ...fromBase64(pubkey.value)]); } else if (isSecp256k1Pubkey(pubkey)) { return new Uint8Array([...pubkeyAminoPrefixSecp256k1, ...fromBase64(pubkey.value)]); + } else if (isEthSecp256k1Pubkey(pubkey)) { + return new Uint8Array([...pubkeyAminoPrefixEthSecp256k1, ...fromBase64(pubkey.value)]); } else { throw new Error("Unsupported pubkey type"); } diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index 2edbb2db94..c9a220ff30 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -11,13 +11,16 @@ export { encodeAminoPubkey, encodeBech32Pubkey, encodeEd25519Pubkey, + encodeEthSecp256k1Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; export { createMultisigThresholdPubkey } from "./multisig"; export { makeCosmoshubPath } from "./paths"; export { Ed25519Pubkey, + EthSecp256k1Pubkey, isEd25519Pubkey, + isEthSecp256k1Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, isSinglePubkey, diff --git a/packages/amino/src/pubkeys.ts b/packages/amino/src/pubkeys.ts index d61bfa3c39..c9c6a63f64 100644 --- a/packages/amino/src/pubkeys.ts +++ b/packages/amino/src/pubkeys.ts @@ -24,10 +24,20 @@ export function isSecp256k1Pubkey(pubkey: Pubkey): pubkey is Secp256k1Pubkey { return (pubkey as Secp256k1Pubkey).type === "tendermint/PubKeySecp256k1"; } +export interface EthSecp256k1Pubkey extends SinglePubkey { + readonly type: "tendermint/PubKeyEthSecp256k1"; + readonly value: string; +} + +export function isEthSecp256k1Pubkey(pubkey: Pubkey): pubkey is EthSecp256k1Pubkey { + return (pubkey as EthSecp256k1Pubkey).type === "tendermint/PubKeyEthSecp256k1"; +} + 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, + ethsecp256k1: "tendermint/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, diff --git a/packages/proto-signing/src/pubkey.ts b/packages/proto-signing/src/pubkey.ts index 8a07980d51..721c0a8046 100644 --- a/packages/proto-signing/src/pubkey.ts +++ b/packages/proto-signing/src/pubkey.ts @@ -3,6 +3,7 @@ import { encodeEd25519Pubkey, encodeSecp256k1Pubkey, isEd25519Pubkey, + isEthSecp256k1Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, MultisigThresholdPubkey, @@ -39,6 +40,14 @@ export function encodePubkey(pubkey: Pubkey): Any { typeUrl: "/cosmos.crypto.ed25519.PubKey", value: Uint8Array.from(CosmosCryptoEd25519Pubkey.encode(pubkeyProto).finish()), }); + } else if (isEthSecp256k1Pubkey(pubkey)) { + const pubkeyProto = CosmosCryptoSecp256k1Pubkey.fromPartial({ + key: fromBase64(pubkey.value), + }); + return Any.fromPartial({ + typeUrl: "/ethermint.crypto.v1.ethsecp256k1.PubKey", + value: Uint8Array.from(CosmosCryptoSecp256k1Pubkey.encode(pubkeyProto).finish()), + }); } else if (isMultisigThresholdPubkey(pubkey)) { const pubkeyProto = LegacyAminoPubKey.fromPartial({ threshold: Uint53.fromString(pubkey.value.threshold).toNumber(), @@ -62,14 +71,15 @@ export function encodePubkey(pubkey: Pubkey): Any { */ export function anyToSinglePubkey(pubkey: Any): SinglePubkey { switch (pubkey.typeUrl) { - case "/cosmos.crypto.secp256k1.PubKey": { - const { key } = CosmosCryptoSecp256k1Pubkey.decode(pubkey.value); - return encodeSecp256k1Pubkey(key); - } case "/cosmos.crypto.ed25519.PubKey": { const { key } = CosmosCryptoEd25519Pubkey.decode(pubkey.value); return encodeEd25519Pubkey(key); } + case "/ethermint.crypto.v1.ethsecp256k1.PubKey": + case "/cosmos.crypto.secp256k1.PubKey": { + const {key} = CosmosCryptoSecp256k1Pubkey.decode(pubkey.value); + return encodeSecp256k1Pubkey(key); + } default: throw new Error(`Pubkey type_url ${pubkey.typeUrl} not recognized as single public key type`); } @@ -81,6 +91,7 @@ export function decodePubkey(pubkey?: Any | null): Pubkey | null { } switch (pubkey.typeUrl) { + case "/ethermint.crypto.v1.ethsecp256k1.PubKey": case "/cosmos.crypto.secp256k1.PubKey": case "/cosmos.crypto.ed25519.PubKey": { return anyToSinglePubkey(pubkey); diff --git a/packages/stargate/src/accounts.ts b/packages/stargate/src/accounts.ts index 8b5d74a1dd..f098054a80 100644 --- a/packages/stargate/src/accounts.ts +++ b/packages/stargate/src/accounts.ts @@ -59,6 +59,11 @@ export function accountFromAny(input: Any): Account { assert(baseAccount); return accountFromBaseAccount(baseAccount); } + case "/ethermint.types.v1.EthAccount": { + const baseAccount = ModuleAccount.decode(value).baseAccount; + assert(baseAccount); + return accountFromBaseAccount(baseAccount); + } // vesting diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index da1ba93f78..161ddbbb00 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -1,4 +1,9 @@ -import { encodeSecp256k1Pubkey, makeSignDoc as makeSignDocAmino, StdFee } from "@cosmjs/amino"; +import { + encodeEthSecp256k1Pubkey, + encodeSecp256k1Pubkey, + makeSignDoc as makeSignDocAmino, + StdFee, +} from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; import { Int53, Uint53 } from "@cosmjs/math"; import { @@ -346,7 +351,11 @@ export class SigningStargateClient extends StargateClient { if (!accountFromSigner) { throw new Error("Failed to retrieve account from signer"); } - const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); + const pubkey = encodePubkey( + chainId.startsWith("evmos_9001-") + ? encodeEthSecp256k1Pubkey(accountFromSigner.pubkey) + : 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); @@ -391,7 +400,11 @@ export class SigningStargateClient extends StargateClient { if (!accountFromSigner) { throw new Error("Failed to retrieve account from signer"); } - const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); + const pubkey = encodePubkey( + chainId.startsWith("evmos_9001-") + ? encodeEthSecp256k1Pubkey(accountFromSigner.pubkey) + : encodeSecp256k1Pubkey(accountFromSigner.pubkey), + ); const txBodyEncodeObject: TxBodyEncodeObject = { typeUrl: "/cosmos.tx.v1beta1.TxBody", value: {