From e8d01266132e7eff5d9f6301f41b54020cac0f73 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 13 Oct 2022 14:30:19 +0200 Subject: [PATCH 1/3] Add encodeEd25519Pubkey --- CHANGELOG.md | 2 ++ packages/amino/src/encoding.spec.ts | 21 +++++++++++++++++++++ packages/amino/src/encoding.ts | 19 +++++++++++++++++++ packages/amino/src/index.ts | 1 + 4 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c1fad215c..2ce58ea288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to ### Added +- @cosmjs/amino: Add `encodeEd25519Pubkey` analogue to the existing + `encodeSecp256k1Pubkey`. - @cosmjs/utils: Add `isDefined` which checks for `undefined` in a TypeScript-friendly way. diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 3813e6ca33..0a056e9826 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -1,3 +1,4 @@ +import { Random } from "@cosmjs/crypto"; import { fromBase64, fromBech32, fromHex } from "@cosmjs/encoding"; import { @@ -5,6 +6,7 @@ import { decodeBech32Pubkey, encodeAminoPubkey, encodeBech32Pubkey, + encodeEd25519Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; import { Pubkey } from "./pubkeys"; @@ -37,6 +39,25 @@ describe("encoding", () => { }); }); + describe("encodeEd25519Pubkey", () => { + it("encodes a compressed pubkey", () => { + const pubkey = fromBase64("ICKLJPyWYIF35GpOclg0gu957WYJe4PHzyn2scCZoek="); + expect(encodeEd25519Pubkey(pubkey)).toEqual({ + type: "tendermint/PubKeyEd25519", + value: "ICKLJPyWYIF35GpOclg0gu957WYJe4PHzyn2scCZoek=", + }); + }); + + it("throws for wrong pubkey lengths", () => { + expect(() => encodeEd25519Pubkey(Random.getBytes(31))).toThrowError( + /ed25519 public key must be 32 bytes long/i, + ); + expect(() => encodeEd25519Pubkey(Random.getBytes(64))).toThrowError( + /ed25519 public key must be 32 bytes long/i, + ); + }); + }); + describe("decodeAminoPubkey", () => { it("works for secp256k1", () => { const amino = fromBech32( diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index fa0b35ba7e..0b069bcaea 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -3,6 +3,7 @@ import { Uint53 } from "@cosmjs/math"; import { arrayContentStartsWith } from "@cosmjs/utils"; import { + Ed25519Pubkey, isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, @@ -12,6 +13,10 @@ import { Secp256k1Pubkey, } from "./pubkeys"; +/** + * Takes a Secp256k1 public key as raw bytes and returns the Amino JSON + * representation of it (the type/value wrapper object). + */ 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"); @@ -22,6 +27,20 @@ export function encodeSecp256k1Pubkey(pubkey: Uint8Array): Secp256k1Pubkey { }; } +/** + * Takes an Edd25519 public key as raw bytes and returns the Amino JSON + * representation of it (the type/value wrapper object). + */ +export function encodeEd25519Pubkey(pubkey: Uint8Array): Ed25519Pubkey { + if (pubkey.length !== 32) { + throw new Error("Ed25519 public key must be 32 bytes long"); + } + return { + type: pubkeyType.ed25519, + 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 diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index d273be775a..2edbb2db94 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -10,6 +10,7 @@ export { decodeBech32Pubkey, encodeAminoPubkey, encodeBech32Pubkey, + encodeEd25519Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; export { createMultisigThresholdPubkey } from "./multisig"; From 3067ab09ed1a8919dc9d14d11c5f3876cbd4ab6b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 13 Oct 2022 14:34:32 +0200 Subject: [PATCH 2/3] Add Ed25519 support to encodePubkey/anyToSinglePubkey --- CHANGELOG.md | 2 ++ packages/proto-signing/src/index.ts | 2 +- packages/proto-signing/src/pubkey.spec.ts | 14 +++++++++++ packages/proto-signing/src/pubkey.ts | 30 +++++++++++++++++------ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce58ea288..fbf0aaaffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to - @cosmjs/amino: Add `encodeEd25519Pubkey` analogue to the existing `encodeSecp256k1Pubkey`. +- @cosmjs/proto-signing: Add Ed25519 support to `anyToSinglePubkey` and make + export `anyToSinglePubkey`. - @cosmjs/utils: Add `isDefined` which checks for `undefined` in a TypeScript-friendly way. diff --git a/packages/proto-signing/src/index.ts b/packages/proto-signing/src/index.ts index 1993b90a42..c34d0ff705 100644 --- a/packages/proto-signing/src/index.ts +++ b/packages/proto-signing/src/index.ts @@ -8,7 +8,7 @@ export { } from "./directsecp256k1hdwallet"; export { DirectSecp256k1Wallet } from "./directsecp256k1wallet"; export { makeCosmoshubPath } from "./paths"; -export { decodePubkey, encodePubkey } from "./pubkey"; +export { anyToSinglePubkey, decodePubkey, encodePubkey } from "./pubkey"; export { DecodeObject, EncodeObject, diff --git a/packages/proto-signing/src/pubkey.spec.ts b/packages/proto-signing/src/pubkey.spec.ts index fe7f9d56e1..5d5f190eaa 100644 --- a/packages/proto-signing/src/pubkey.spec.ts +++ b/packages/proto-signing/src/pubkey.spec.ts @@ -8,6 +8,9 @@ describe("pubkey", () => { const defaultPubkeyBase64 = "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"; const defaultPubkeyBytes = fromBase64(defaultPubkeyBase64); const defaultPubkeyProtoBytes = Uint8Array.from([0x0a, defaultPubkeyBytes.length, ...defaultPubkeyBytes]); + const ed25519PubkeyBase64 = "kEX3edqZB+HdCV92TPS7ePX0DtP62GWIjmrveZ5pnaQ="; + const ed25519PubkeyBytes = fromBase64(ed25519PubkeyBase64); + const ed25519PubkeyProtoBytes = Uint8Array.from([0x0a, ed25519PubkeyBytes.length, ...ed25519PubkeyBytes]); describe("encodePubkey", () => { it("works for secp256k1", () => { @@ -41,6 +44,17 @@ describe("pubkey", () => { }); }); + it("works for ed25519", () => { + const pubkey = { + typeUrl: "/cosmos.crypto.ed25519.PubKey", + value: ed25519PubkeyProtoBytes, + }; + expect(decodePubkey(pubkey)).toEqual({ + type: "tendermint/PubKeyEd25519", + value: ed25519PubkeyBase64, + }); + }); + it("throws for unsupported pubkey types", () => { const pubkey = { typeUrl: "/cosmos.crypto.unknown.PubKey", diff --git a/packages/proto-signing/src/pubkey.ts b/packages/proto-signing/src/pubkey.ts index 9af1af6158..38b3b2608a 100644 --- a/packages/proto-signing/src/pubkey.ts +++ b/packages/proto-signing/src/pubkey.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { + encodeEd25519Pubkey, encodeSecp256k1Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, @@ -9,18 +10,19 @@ import { } from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; +import { PubKey as CosmosCryptoEd25519Pubkey } from "cosmjs-types/cosmos/crypto/ed25519/keys"; import { LegacyAminoPubKey } from "cosmjs-types/cosmos/crypto/multisig/keys"; -import { PubKey } from "cosmjs-types/cosmos/crypto/secp256k1/keys"; +import { PubKey as CosmosCryptoSecp256k1Pubkey } from "cosmjs-types/cosmos/crypto/secp256k1/keys"; import { Any } from "cosmjs-types/google/protobuf/any"; export function encodePubkey(pubkey: Pubkey): Any { if (isSecp256k1Pubkey(pubkey)) { - const pubkeyProto = PubKey.fromPartial({ + const pubkeyProto = CosmosCryptoSecp256k1Pubkey.fromPartial({ key: fromBase64(pubkey.value), }); return Any.fromPartial({ typeUrl: "/cosmos.crypto.secp256k1.PubKey", - value: Uint8Array.from(PubKey.encode(pubkeyProto).finish()), + value: Uint8Array.from(CosmosCryptoSecp256k1Pubkey.encode(pubkeyProto).finish()), }); } else if (isMultisigThresholdPubkey(pubkey)) { const pubkeyProto = LegacyAminoPubKey.fromPartial({ @@ -36,12 +38,23 @@ export function encodePubkey(pubkey: Pubkey): Any { } } -function decodeSinglePubkey(pubkey: Any): SinglePubkey { +/** + * Decodes a single pubkey (i.e. not a multisig pubkey) from `Any` into + * `SinglePubkey`. + * + * In most cases you probably want to use `decodePubkey`, but `anyToSinglePubkey` + * might be preferred in CosmJS 0.29.x due to https://github.com/cosmos/cosmjs/issues/1289. + */ +export function anyToSinglePubkey(pubkey: Any): SinglePubkey { switch (pubkey.typeUrl) { case "/cosmos.crypto.secp256k1.PubKey": { - const { key } = PubKey.decode(pubkey.value); + const { key } = CosmosCryptoSecp256k1Pubkey.decode(pubkey.value); return encodeSecp256k1Pubkey(key); } + case "/cosmos.crypto.ed25519.PubKey": { + const { key } = CosmosCryptoEd25519Pubkey.decode(pubkey.value); + return encodeEd25519Pubkey(key); + } default: throw new Error(`Pubkey type_url ${pubkey.typeUrl} not recognized as single public key type`); } @@ -53,8 +66,9 @@ export function decodePubkey(pubkey?: Any | null): Pubkey | null { } switch (pubkey.typeUrl) { - case "/cosmos.crypto.secp256k1.PubKey": { - return decodeSinglePubkey(pubkey); + case "/cosmos.crypto.secp256k1.PubKey": + case "/cosmos.crypto.ed25519.PubKey": { + return anyToSinglePubkey(pubkey); } case "/cosmos.crypto.multisig.LegacyAminoPubKey": { const { threshold, publicKeys } = LegacyAminoPubKey.decode(pubkey.value); @@ -62,7 +76,7 @@ export function decodePubkey(pubkey?: Any | null): Pubkey | null { type: "tendermint/PubKeyMultisigThreshold", value: { threshold: threshold.toString(), - pubkeys: publicKeys.map(decodeSinglePubkey), + pubkeys: publicKeys.map(anyToSinglePubkey), }, }; return out; From 472f78a4f6cbf2e3a156aa7a928290cb8ba66469 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 13 Oct 2022 14:57:16 +0200 Subject: [PATCH 3/3] Add ed25519 support to encodePubkey --- CHANGELOG.md | 4 ++-- packages/proto-signing/src/pubkey.spec.ts | 10 ++++++++++ packages/proto-signing/src/pubkey.ts | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf0aaaffe..50d7dd73b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ and this project adheres to - @cosmjs/amino: Add `encodeEd25519Pubkey` analogue to the existing `encodeSecp256k1Pubkey`. -- @cosmjs/proto-signing: Add Ed25519 support to `anyToSinglePubkey` and make - export `anyToSinglePubkey`. +- @cosmjs/proto-signing: Add Ed25519 support to `encodePubkey` and + `anyToSinglePubkey`. Export `anyToSinglePubkey`. - @cosmjs/utils: Add `isDefined` which checks for `undefined` in a TypeScript-friendly way. diff --git a/packages/proto-signing/src/pubkey.spec.ts b/packages/proto-signing/src/pubkey.spec.ts index 5d5f190eaa..6dea27f3e1 100644 --- a/packages/proto-signing/src/pubkey.spec.ts +++ b/packages/proto-signing/src/pubkey.spec.ts @@ -23,6 +23,16 @@ describe("pubkey", () => { ); }); + it("works for ed25519", () => { + const pubkey = { type: "tendermint/PubKeyEd25519", value: ed25519PubkeyBase64 }; + expect(encodePubkey(pubkey)).toEqual( + Any.fromPartial({ + typeUrl: "/cosmos.crypto.ed25519.PubKey", + value: ed25519PubkeyProtoBytes, + }), + ); + }); + it("throws for unsupported pubkey types", () => { const pubkey = { type: "tendermint/PubKeyUnknown", diff --git a/packages/proto-signing/src/pubkey.ts b/packages/proto-signing/src/pubkey.ts index 38b3b2608a..8a07980d51 100644 --- a/packages/proto-signing/src/pubkey.ts +++ b/packages/proto-signing/src/pubkey.ts @@ -2,6 +2,7 @@ import { encodeEd25519Pubkey, encodeSecp256k1Pubkey, + isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, MultisigThresholdPubkey, @@ -15,6 +16,12 @@ import { LegacyAminoPubKey } from "cosmjs-types/cosmos/crypto/multisig/keys"; import { PubKey as CosmosCryptoSecp256k1Pubkey } from "cosmjs-types/cosmos/crypto/secp256k1/keys"; import { Any } from "cosmjs-types/google/protobuf/any"; +/** + * Takes a pubkey in the Amino JSON object style (type/value wrapper) + * and convertes it into a protobuf `Any`. + * + * This is the reverse operation to `decodePubkey`. + */ export function encodePubkey(pubkey: Pubkey): Any { if (isSecp256k1Pubkey(pubkey)) { const pubkeyProto = CosmosCryptoSecp256k1Pubkey.fromPartial({ @@ -24,6 +31,14 @@ export function encodePubkey(pubkey: Pubkey): Any { typeUrl: "/cosmos.crypto.secp256k1.PubKey", value: Uint8Array.from(CosmosCryptoSecp256k1Pubkey.encode(pubkeyProto).finish()), }); + } else if (isEd25519Pubkey(pubkey)) { + const pubkeyProto = CosmosCryptoEd25519Pubkey.fromPartial({ + key: fromBase64(pubkey.value), + }); + return Any.fromPartial({ + typeUrl: "/cosmos.crypto.ed25519.PubKey", + value: Uint8Array.from(CosmosCryptoEd25519Pubkey.encode(pubkeyProto).finish()), + }); } else if (isMultisigThresholdPubkey(pubkey)) { const pubkeyProto = LegacyAminoPubKey.fromPartial({ threshold: Uint53.fromString(pubkey.value.threshold).toNumber(),