diff --git a/CHANGELOG.md b/CHANGELOG.md index debc0a4348..22dd4a2309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,10 @@ and this project adheres to - @cosmjs/proto-signing: Export `DirectSecp256k1HdWalletOptions` interface. - @cosmjs/proto-signing: Add `bip39Password` option to `DirectSecp256k1HdWallet` options. +- @cosmjs/amino: Add `rawEd25519PubkeyToRawAddress` helper function. +- @cosmjs/tendermint-rpc: Add `pubkeyToAddress`, `pubkeyToRawAddress`, + `rawEd25519PubkeyToRawAddress`, and `rawSecp256k1PubkeyToRawAddress` helper + functions. ### Changed diff --git a/packages/amino/src/addresses.ts b/packages/amino/src/addresses.ts index 133f8f8b29..4ecedf4b9b 100644 --- a/packages/amino/src/addresses.ts +++ b/packages/amino/src/addresses.ts @@ -6,6 +6,13 @@ import { Bech32, fromBase64 } from "@cosmjs/encoding"; import { encodeAminoPubkey } from "./encoding"; import { isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, Pubkey } from "./pubkeys"; +export function rawEd25519PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Array { + if (pubkeyData.length !== 32) { + throw new Error(`Invalid Ed25519 pubkey length: ${pubkeyData.length}`); + } + return sha256(pubkeyData).slice(0, 20); +} + export function rawSecp256k1PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Array { if (pubkeyData.length !== 33) { throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyData.length}`); @@ -20,10 +27,7 @@ export function pubkeyToRawAddress(pubkey: Pubkey): Uint8Array { return rawSecp256k1PubkeyToRawAddress(pubkeyData); } else if (isEd25519Pubkey(pubkey)) { const pubkeyData = fromBase64(pubkey.value); - if (pubkeyData.length !== 32) { - throw new Error(`Invalid Ed25519 pubkey length: ${pubkeyData.length}`); - } - return sha256(pubkeyData).slice(0, 20); + return rawEd25519PubkeyToRawAddress(pubkeyData); } else if (isMultisigThresholdPubkey(pubkey)) { // https://github.com/tendermint/tendermint/blob/38b401657e4ad7a7eeb3c30a3cbf512037df3740/crypto/multisig/threshold_pubkey.go#L71-L74 const pubkeyData = encodeAminoPubkey(pubkey); diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index cbe92581d3..42d9720c6c 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -1,4 +1,9 @@ -export { pubkeyToAddress, pubkeyToRawAddress, rawSecp256k1PubkeyToRawAddress } from "./addresses"; +export { + pubkeyToAddress, + pubkeyToRawAddress, + rawEd25519PubkeyToRawAddress, + rawSecp256k1PubkeyToRawAddress, +} from "./addresses"; export { Coin, coin, coins, parseCoins } from "./coins"; export { decodeAminoPubkey, diff --git a/packages/tendermint-rpc/src/addresses.spec.ts b/packages/tendermint-rpc/src/addresses.spec.ts new file mode 100644 index 0000000000..f75c28fd2d --- /dev/null +++ b/packages/tendermint-rpc/src/addresses.spec.ts @@ -0,0 +1,35 @@ +import { fromHex } from "@cosmjs/encoding"; + +import { pubkeyToAddress, pubkeyToRawAddress } from "./addresses"; + +// Test values from https://github.com/informalsystems/tendermint-rs/blob/v0.18.1/tendermint/src/account.rs#L153-L192 + +describe("addresses", () => { + describe("pubkeyToRawAddress", () => { + it("works for Secp256k1", () => { + const pubkey = fromHex("02950E1CDFCB133D6024109FD489F734EEB4502418E538C28481F22BCE276F248C"); + expect(pubkeyToRawAddress("secp256k1", pubkey)).toEqual( + fromHex("7C2BB42A8BE69791EC763E51F5A49BCD41E82237"), + ); + }); + + it("works for Ed25519", () => { + const pubkey = fromHex("14253D61EF42D166D02E68D540D07FDF8D65A9AF0ACAA46302688E788A8521E2"); + expect(pubkeyToRawAddress("ed25519", pubkey)).toEqual( + fromHex("0CDA3F47EF3C4906693B170EF650EB968C5F4B2C"), + ); + }); + }); + + describe("pubkeyToAddress", () => { + it("works for Secp256k1", () => { + const pubkey = fromHex("02950E1CDFCB133D6024109FD489F734EEB4502418E538C28481F22BCE276F248C"); + expect(pubkeyToAddress("secp256k1", pubkey)).toEqual("7C2BB42A8BE69791EC763E51F5A49BCD41E82237"); + }); + + it("works for Ed25519", () => { + const pubkey = fromHex("14253D61EF42D166D02E68D540D07FDF8D65A9AF0ACAA46302688E788A8521E2"); + expect(pubkeyToAddress("ed25519", pubkey)).toEqual("0CDA3F47EF3C4906693B170EF650EB968C5F4B2C"); + }); + }); +}); diff --git a/packages/tendermint-rpc/src/addresses.ts b/packages/tendermint-rpc/src/addresses.ts new file mode 100644 index 0000000000..24c5d429e5 --- /dev/null +++ b/packages/tendermint-rpc/src/addresses.ts @@ -0,0 +1,33 @@ +import { ripemd160, sha256 } from "@cosmjs/crypto"; +import { toHex } from "@cosmjs/encoding"; + +export function rawEd25519PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Array { + if (pubkeyData.length !== 32) { + throw new Error(`Invalid Ed25519 pubkey length: ${pubkeyData.length}`); + } + return sha256(pubkeyData).slice(0, 20); +} + +export function rawSecp256k1PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Array { + if (pubkeyData.length !== 33) { + throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyData.length}`); + } + return ripemd160(sha256(pubkeyData)); +} + +// For secp256k1 this assumes we already have a compressed pubkey. +export function pubkeyToRawAddress(type: "ed25519" | "secp256k1", data: Uint8Array): Uint8Array { + switch (type) { + case "ed25519": + return rawEd25519PubkeyToRawAddress(data); + case "secp256k1": + return rawSecp256k1PubkeyToRawAddress(data); + default: + // Keep this case here to guard against new types being added but not handled + throw new Error(`Pubkey type ${type} not supported`); + } +} + +export function pubkeyToAddress(type: "ed25519" | "secp256k1", data: Uint8Array): string { + return toHex(pubkeyToRawAddress(type, data)).toUpperCase(); +} diff --git a/packages/tendermint-rpc/src/index.ts b/packages/tendermint-rpc/src/index.ts index 10c7e62e4d..1bcb3e6a3b 100644 --- a/packages/tendermint-rpc/src/index.ts +++ b/packages/tendermint-rpc/src/index.ts @@ -1,3 +1,9 @@ +export { + pubkeyToAddress, + pubkeyToRawAddress, + rawEd25519PubkeyToRawAddress, + rawSecp256k1PubkeyToRawAddress, +} from "./addresses"; export { adaptor33, adaptor34,