diff --git a/CHANGELOG.md b/CHANGELOG.md index 7285afae..7e3e53c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ and this project adheres to `SigningStargateClient`. - @cosmjs/amino: New package created that contains the shared amino signing functionality for @cosmjs/launchpad and @cosmjs/stargate. +- @cosmjs/amino: Split public key interfaces into `Pubkey`, `SinglePubkey` and + `Secp256k1Pubkey` where `Pubkey` is a generalization of the old `PubKey` that + supported nested pubkeys for multisig. `SinglePubkey` is the old `PubKey` in + which the `value` is a base64 encoded string. And `Secp256k1Pubkey` is a + single secp256k1 pubkey. ### Changed diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 02c6900d..e3f60f56 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -7,9 +7,9 @@ import { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; -import { PubKey } from "./pubkeys"; +import { Pubkey } from "./pubkeys"; -describe("pubkey", () => { +describe("encoding", () => { describe("encodeSecp256k1Pubkey", () => { it("encodes a compresed pubkey", () => { const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); @@ -85,7 +85,7 @@ describe("pubkey", () => { describe("encodeAminoPubkey", () => { it("works for secp256k1", () => { - const pubkey: PubKey = { + const pubkey: Pubkey = { type: "tendermint/PubKeySecp256k1", value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", }; @@ -98,7 +98,7 @@ describe("pubkey", () => { it("works for ed25519", () => { // Decoded from http://localhost:26657/validators // Encoded from `corald tendermint show-validator` - const pubkey: PubKey = { + const pubkey: Pubkey = { type: "tendermint/PubKeyEd25519", value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", }; @@ -111,7 +111,7 @@ describe("pubkey", () => { describe("encodeBech32Pubkey", () => { it("works for secp256k1", () => { - const pubkey: PubKey = { + const pubkey: Pubkey = { type: "tendermint/PubKeySecp256k1", value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", }; @@ -123,7 +123,7 @@ describe("pubkey", () => { it("works for ed25519", () => { // Decoded from http://localhost:26657/validators // Encoded from `corald tendermint show-validator` - const pubkey: PubKey = { + const pubkey: Pubkey = { type: "tendermint/PubKeyEd25519", value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", }; diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index cc6297a0..58ba9202 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -1,9 +1,9 @@ import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; import { arrayContentEquals } from "@cosmjs/utils"; -import { PubKey, pubkeyType } from "./pubkeys"; +import { Pubkey, pubkeyType, Secp256k1Pubkey } from "./pubkeys"; -export function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey { +export function encodeSecp256k1Pubkey(pubkey: Uint8Array): Secp256k1Pubkey { 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"); } @@ -24,7 +24,7 @@ const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length; /** * Decodes a pubkey in the Amino binary format to a type/value object. */ -export function decodeAminoPubkey(data: Uint8Array): PubKey { +export function decodeAminoPubkey(data: Uint8Array): Pubkey { const aminoPrefix = data.slice(0, pubkeyAminoPrefixLength); const rest = data.slice(pubkeyAminoPrefixLength); if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSecp256k1)) { @@ -62,7 +62,7 @@ export function decodeAminoPubkey(data: Uint8Array): PubKey { * * @param bechEncoded the bech32 encoded pubkey */ -export function decodeBech32Pubkey(bechEncoded: string): PubKey { +export function decodeBech32Pubkey(bechEncoded: string): Pubkey { const { data } = Bech32.decode(bechEncoded); return decodeAminoPubkey(data); } @@ -70,7 +70,7 @@ export function decodeBech32Pubkey(bechEncoded: string): PubKey { /** * Encodes a public key to binary Amino. */ -export function encodeAminoPubkey(pubkey: PubKey): Uint8Array { +export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { let aminoPrefix: Uint8Array; switch (pubkey.type) { // Note: please don't add cases here without writing additional unit tests @@ -92,6 +92,6 @@ export function encodeAminoPubkey(pubkey: PubKey): Uint8Array { * @param pubkey the public key to encode * @param prefix the bech32 prefix (human readable part) */ -export function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string { +export function encodeBech32Pubkey(pubkey: Pubkey, prefix: string): string { return Bech32.encode(prefix, encodeAminoPubkey(pubkey)); } diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index 7f01fc17..5469771a 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -5,4 +5,4 @@ export { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; -export { PubKey, pubkeyType } from "./pubkeys"; +export { Pubkey, Secp256k1Pubkey, SinglePubkey, isSinglePubkey, pubkeyType } from "./pubkeys"; diff --git a/packages/amino/src/pubkeys.spec.ts b/packages/amino/src/pubkeys.spec.ts new file mode 100644 index 00000000..858c0815 --- /dev/null +++ b/packages/amino/src/pubkeys.spec.ts @@ -0,0 +1,44 @@ +import { isSinglePubkey } from "./pubkeys"; + +describe("pubkeys", () => { + const pubkeyEd25519 = { + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }; + const pubkeySecp256k1 = { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }; + const pubkeyMultisigThreshold = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "3", + pubkeys: [ + { + type: "tendermint/PubKeySecp256k1", + value: "A4KZH7VSRwW/6RTExROivRYKsQP63LnGcBlXFo+eKGpQ", + }, + { + type: "tendermint/PubKeySecp256k1", + value: "A8/Cq4VigOnDgl6RSdcx97fjrdCo/qwAX6C34n7ZDZLs", + }, + { + type: "tendermint/PubKeySecp256k1", + value: "ApKgZuwy03xgdRnXqG6yEHATomsWDOPacy7nbpsuUCSS", + }, + { + type: "tendermint/PubKeySecp256k1", + value: "Aptm8E3WSSFS0RTAIUW+bLi/slYnTEE+h4qPTG28CHfq", + }, + ], + }, + }; + + describe("isSinglePubkey", () => { + it("works", () => { + expect(isSinglePubkey(pubkeyEd25519)).toEqual(true); + expect(isSinglePubkey(pubkeySecp256k1)).toEqual(true); + expect(isSinglePubkey(pubkeyMultisigThreshold)).toEqual(false); + }); + }); +}); diff --git a/packages/amino/src/pubkeys.ts b/packages/amino/src/pubkeys.ts index 3036afcf..e538e657 100644 --- a/packages/amino/src/pubkeys.ts +++ b/packages/amino/src/pubkeys.ts @@ -1,10 +1,13 @@ -export interface PubKey { +export interface Pubkey { // type is one of the strings defined in pubkeyType // I don't use a string literal union here as that makes trouble with json test data: // https://github.com/cosmos/cosmjs/pull/44#pullrequestreview-353280504 readonly type: string; - // Value field is base64-encoded in all cases - // Note: if type is Secp256k1, this must contain a COMPRESSED pubkey - to encode from bcp/keycontrol land, you must compress it first + readonly value: any; +} + +export interface Secp256k1Pubkey extends SinglePubkey { + readonly type: "tendermint/PubKeySecp256k1"; readonly value: string; } @@ -16,3 +19,26 @@ export const pubkeyType = { /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */ sr25519: "tendermint/PubKeySr25519" as const, }; + +/** + * A pubkey which contains the data directly without further nesting. + * + * You can think of this as a non-multisig pubkey. + */ +export interface SinglePubkey extends Pubkey { + // type is one of the strings defined in pubkeyType + // I don't use a string literal union here as that makes trouble with json test data: + // https://github.com/cosmos/cosmjs/pull/44#pullrequestreview-353280504 + readonly type: string; + /** + * The base64 encoding of the Amino binary encoded pubkey. + * + * Note: if type is Secp256k1, this must contain a 33 bytes compressed pubkey. + */ + readonly value: string; +} + +export function isSinglePubkey(pubkey: Pubkey): pubkey is SinglePubkey { + const singPubkeyTypes: string[] = [pubkeyType.ed25519, pubkeyType.secp256k1, pubkeyType.sr25519]; + return singPubkeyTypes.includes(pubkey.type); +} diff --git a/packages/launchpad/src/address.ts b/packages/launchpad/src/address.ts index a35b2a0e..adfc43a0 100644 --- a/packages/launchpad/src/address.ts +++ b/packages/launchpad/src/address.ts @@ -1,4 +1,4 @@ -import { PubKey, pubkeyType } from "@cosmjs/amino"; +import { pubkeyType, SinglePubkey } from "@cosmjs/amino"; import { ripemd160, sha256 } from "@cosmjs/crypto"; import { Bech32, fromBase64 } from "@cosmjs/encoding"; @@ -13,7 +13,7 @@ export function rawSecp256k1PubkeyToAddress(pubkeyRaw: Uint8Array, prefix: strin // See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography // This assumes we already have a cosmos-compressed pubkey -export function pubkeyToAddress(pubkey: PubKey, prefix: string): string { +export function pubkeyToAddress(pubkey: SinglePubkey, prefix: string): string { const pubkeyBytes = fromBase64(pubkey.value); switch (pubkey.type) { case pubkeyType.secp256k1: { diff --git a/packages/launchpad/src/cosmosclient.ts b/packages/launchpad/src/cosmosclient.ts index d49e018b..9bb2764e 100644 --- a/packages/launchpad/src/cosmosclient.ts +++ b/packages/launchpad/src/cosmosclient.ts @@ -1,4 +1,4 @@ -import { PubKey } from "@cosmjs/amino"; +import { Pubkey } from "@cosmjs/amino"; import { sha256 } from "@cosmjs/crypto"; import { fromBase64, fromHex, toHex } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; @@ -24,7 +24,7 @@ export interface Account { /** Bech32 account address */ readonly address: string; readonly balance: readonly Coin[]; - readonly pubkey: PubKey | undefined; + readonly pubkey: Pubkey | undefined; readonly accountNumber: number; readonly sequence: number; } diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index 46b2e770..ea1120b5 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -6,7 +6,7 @@ export { encodeBech32Pubkey, encodeSecp256k1Pubkey, pubkeyType, - PubKey, + SinglePubkey as PubKey, } from "@cosmjs/amino"; import * as logs from "./logs"; diff --git a/packages/launchpad/src/lcdapi/auth.ts b/packages/launchpad/src/lcdapi/auth.ts index 009c91ba..947ff1da 100644 --- a/packages/launchpad/src/lcdapi/auth.ts +++ b/packages/launchpad/src/lcdapi/auth.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { PubKey } from "@cosmjs/amino"; +import { Pubkey } from "@cosmjs/amino"; import { Coin } from "../coins"; import { LcdClient } from "./lcdclient"; @@ -26,7 +26,7 @@ export interface BaseAccount { * [1]: https://github.com/cosmos/cosmos-sdk/pull/5280 * [2]: https://github.com/cosmos/cosmos-sdk/pull/6749 */ - readonly public_key: string | PubKey | null; + readonly public_key: string | Pubkey | null; /** * The account number assigned by the blockchain. * diff --git a/packages/launchpad/src/lcdapi/utils.spec.ts b/packages/launchpad/src/lcdapi/utils.spec.ts index 848ea00c..646eee1c 100644 --- a/packages/launchpad/src/lcdapi/utils.spec.ts +++ b/packages/launchpad/src/lcdapi/utils.spec.ts @@ -1,4 +1,4 @@ -import { PubKey } from "@cosmjs/amino"; +import { Pubkey } from "@cosmjs/amino"; import { normalizePubkey, uint64ToNumber, uint64ToString } from "./utils"; @@ -85,7 +85,7 @@ describe("utils", () => { }); it("passes PubKey unchanged", () => { - const original: PubKey = { + const original: Pubkey = { type: "tendermint/PubKeySecp256k1", value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", }; diff --git a/packages/launchpad/src/lcdapi/utils.ts b/packages/launchpad/src/lcdapi/utils.ts index 1fd35af3..558033dc 100644 --- a/packages/launchpad/src/lcdapi/utils.ts +++ b/packages/launchpad/src/lcdapi/utils.ts @@ -1,4 +1,4 @@ -import { decodeBech32Pubkey, PubKey } from "@cosmjs/amino"; +import { decodeBech32Pubkey, Pubkey } from "@cosmjs/amino"; import { Uint64 } from "@cosmjs/math"; /** @@ -29,7 +29,7 @@ export function uint64ToString(input: number | string): string { * * Returns null when unset. */ -export function normalizePubkey(input: string | PubKey | null): PubKey | null { +export function normalizePubkey(input: string | Pubkey | null): Pubkey | null { if (!input) return null; if (typeof input === "string") return decodeBech32Pubkey(input); return input; diff --git a/packages/launchpad/src/types.ts b/packages/launchpad/src/types.ts index 7cccf774..bb674bc1 100644 --- a/packages/launchpad/src/types.ts +++ b/packages/launchpad/src/types.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { PubKey } from "@cosmjs/amino"; +import { Pubkey } from "@cosmjs/amino"; import { Coin } from "./coins"; @@ -9,6 +9,6 @@ export interface StdFee { } export interface StdSignature { - readonly pub_key: PubKey; + readonly pub_key: Pubkey; readonly signature: string; } diff --git a/packages/proto-signing/src/pubkey.ts b/packages/proto-signing/src/pubkey.ts index e44d3b80..faa88ac4 100644 --- a/packages/proto-signing/src/pubkey.ts +++ b/packages/proto-signing/src/pubkey.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { encodeSecp256k1Pubkey, PubKey as AminoPubKey } from "@cosmjs/amino"; +import { encodeSecp256k1Pubkey, SinglePubkey as AminoPubKey } from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; import { PubKey } from "./codec/cosmos/crypto/secp256k1/keys"; diff --git a/packages/stargate/src/accounts.ts b/packages/stargate/src/accounts.ts index e6594b35..04ab739b 100644 --- a/packages/stargate/src/accounts.ts +++ b/packages/stargate/src/accounts.ts @@ -1,4 +1,4 @@ -import { PubKey } from "@cosmjs/amino"; +import { Pubkey } from "@cosmjs/amino"; import { Uint64 } from "@cosmjs/math"; import { decodePubkey } from "@cosmjs/proto-signing"; import { assert } from "@cosmjs/utils"; @@ -16,7 +16,7 @@ import { Any } from "./codec/google/protobuf/any"; export interface Account { /** Bech32 account address */ readonly address: string; - readonly pubkey: PubKey | null; + readonly pubkey: Pubkey | null; readonly accountNumber: number; readonly sequence: number; }