From f8cf23766cf4cf2f00acc5b12b03ad4fa61101a5 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 13:37:18 +0100 Subject: [PATCH 01/25] Pull out pubkey functionality into @cosmjs/amino --- CHANGELOG.md | 2 + packages/amino/.eslintignore | 8 ++ packages/amino/.gitignore | 3 + packages/amino/.nycrc.yml | 1 + packages/amino/README.md | 12 ++ packages/amino/jasmine-testrunner.js | 39 +++++ packages/amino/karma.conf.js | 47 ++++++ packages/amino/nonces/README.txt | 1 + packages/amino/package.json | 48 +++++++ packages/amino/src/encoding.spec.ts | 135 ++++++++++++++++++ packages/amino/src/encoding.ts | 97 +++++++++++++ packages/amino/src/index.ts | 8 ++ packages/amino/src/pubkeys.ts | 18 +++ packages/amino/tsconfig.json | 11 ++ packages/amino/typedoc.js | 11 ++ packages/amino/webpack.web.config.js | 17 +++ packages/cosmwasm-stargate/package.json | 1 + .../src/signingcosmwasmclient.ts | 2 +- packages/launchpad/package.json | 1 + packages/launchpad/src/address.ts | 3 +- packages/launchpad/src/cosmosclient.ts | 2 +- packages/launchpad/src/index.ts | 20 +-- packages/launchpad/src/lcdapi/auth.ts | 3 +- packages/launchpad/src/lcdapi/utils.spec.ts | 3 +- packages/launchpad/src/lcdapi/utils.ts | 4 +- packages/launchpad/src/pubkey.spec.ts | 135 ------------------ packages/launchpad/src/pubkey.ts | 97 ------------- packages/launchpad/src/signature.ts | 4 +- packages/launchpad/src/types.ts | 21 +-- packages/proto-signing/package.json | 1 + packages/proto-signing/src/pubkey.ts | 6 +- packages/stargate/package.json | 1 + packages/stargate/src/accounts.ts | 2 +- packages/stargate/src/aminotypes.spec.ts | 2 +- packages/stargate/src/aminotypes.ts | 3 +- .../stargate/src/signingstargateclient.ts | 2 +- 36 files changed, 493 insertions(+), 278 deletions(-) create mode 100644 packages/amino/.eslintignore create mode 100644 packages/amino/.gitignore create mode 120000 packages/amino/.nycrc.yml create mode 100644 packages/amino/README.md create mode 100755 packages/amino/jasmine-testrunner.js create mode 100644 packages/amino/karma.conf.js create mode 100644 packages/amino/nonces/README.txt create mode 100644 packages/amino/package.json create mode 100644 packages/amino/src/encoding.spec.ts create mode 100644 packages/amino/src/encoding.ts create mode 100644 packages/amino/src/index.ts create mode 100644 packages/amino/src/pubkeys.ts create mode 100644 packages/amino/tsconfig.json create mode 100644 packages/amino/typedoc.js create mode 100644 packages/amino/webpack.web.config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a6e5fb..7285afae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to connection tx, as well as Tendermint. - @cosmjs/stargate: Add support for IBC message types in `SigningStargateClient`. +- @cosmjs/amino: New package created that contains the shared amino signing + functionality for @cosmjs/launchpad and @cosmjs/stargate. ### Changed diff --git a/packages/amino/.eslintignore b/packages/amino/.eslintignore new file mode 100644 index 00000000..f373a53f --- /dev/null +++ b/packages/amino/.eslintignore @@ -0,0 +1,8 @@ +node_modules/ + +build/ +custom_types/ +dist/ +docs/ +generated/ +types/ diff --git a/packages/amino/.gitignore b/packages/amino/.gitignore new file mode 100644 index 00000000..68bf3735 --- /dev/null +++ b/packages/amino/.gitignore @@ -0,0 +1,3 @@ +build/ +dist/ +docs/ diff --git a/packages/amino/.nycrc.yml b/packages/amino/.nycrc.yml new file mode 120000 index 00000000..1f95ac55 --- /dev/null +++ b/packages/amino/.nycrc.yml @@ -0,0 +1 @@ +../../.nycrc.yml \ No newline at end of file diff --git a/packages/amino/README.md b/packages/amino/README.md new file mode 100644 index 00000000..31c09c37 --- /dev/null +++ b/packages/amino/README.md @@ -0,0 +1,12 @@ +# @cosmjs/amino + +[![npm version](https://img.shields.io/npm/v/@cosmjs/amino.svg)](https://www.npmjs.com/package/@cosmjs/amino) + +Helpers for Amino based signing which are shared between @cosmjs/launchpad and +@cosmjs/stargate. + +## License + +This package is part of the cosmjs repository, licensed under the Apache License +2.0 (see [NOTICE](https://github.com/cosmos/cosmjs/blob/main/NOTICE) and +[LICENSE](https://github.com/cosmos/cosmjs/blob/main/LICENSE)). diff --git a/packages/amino/jasmine-testrunner.js b/packages/amino/jasmine-testrunner.js new file mode 100755 index 00000000..6558a133 --- /dev/null +++ b/packages/amino/jasmine-testrunner.js @@ -0,0 +1,39 @@ +#!/usr/bin/env node +/* eslint-disable @typescript-eslint/naming-convention */ + +if (process.env.SES_ENABLED) { + require("ses/lockdown"); + // eslint-disable-next-line no-undef + lockdown(); +} + +require("source-map-support").install(); +const defaultSpecReporterConfig = require("../../jasmine-spec-reporter.config.json"); + +// setup Jasmine +const Jasmine = require("jasmine"); +const jasmine = new Jasmine(); +jasmine.loadConfig({ + spec_dir: "build", + spec_files: ["**/*.spec.js"], + helpers: [], + random: false, + seed: null, + stopSpecOnExpectationFailure: false, +}); +jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 1000; + +// setup reporter +const { SpecReporter } = require("jasmine-spec-reporter"); +const reporter = new SpecReporter({ + ...defaultSpecReporterConfig, + spec: { + ...defaultSpecReporterConfig.spec, + displaySuccessful: !process.argv.includes("--quiet"), + }, +}); + +// initialize and execute +jasmine.env.clearReporters(); +jasmine.addReporter(reporter); +jasmine.execute(); diff --git a/packages/amino/karma.conf.js b/packages/amino/karma.conf.js new file mode 100644 index 00000000..006da5fe --- /dev/null +++ b/packages/amino/karma.conf.js @@ -0,0 +1,47 @@ +module.exports = function (config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: ".", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["jasmine"], + + // list of files / patterns to load in the browser + files: ["dist/web/tests.js"], + + client: { + jasmine: { + random: false, + timeoutInterval: 15000, + }, + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["progress", "kjhtml"], + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ["Firefox"], + + browserNoActivityTimeout: 90000, + + // Keep brower open for debugging. This is overridden by yarn scripts + singleRun: false, + }); +}; diff --git a/packages/amino/nonces/README.txt b/packages/amino/nonces/README.txt new file mode 100644 index 00000000..092fe732 --- /dev/null +++ b/packages/amino/nonces/README.txt @@ -0,0 +1 @@ +Directory used to trigger lerna package updates for all packages diff --git a/packages/amino/package.json b/packages/amino/package.json new file mode 100644 index 00000000..bc9cb37d --- /dev/null +++ b/packages/amino/package.json @@ -0,0 +1,48 @@ +{ + "name": "@cosmjs/amino", + "version": "0.25.0-alpha.0", + "description": "Helpers for Amino based signing which are shared between @cosmjs/launchpad and @cosmjs/stargate.", + "contributors": [ + "Simon Warta " + ], + "license": "Apache-2.0", + "main": "build/index.js", + "types": "build/index.d.ts", + "files": [ + "build/", + "*.md", + "!*.spec.*", + "!**/testdata/" + ], + "repository": { + "type": "git", + "url": "https://github.com/cosmos/cosmjs/tree/main/packages/amino" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "docs": "typedoc --options typedoc.js", + "lint": "eslint --max-warnings 0 \"**/*.{js,ts}\"", + "lint-fix": "eslint --max-warnings 0 \"**/*.{js,ts}\" --fix", + "format": "prettier --write --loglevel warn \"./src/**/*.ts\"", + "format-text": "prettier --write --prose-wrap always --print-width 80 \"./*.md\"", + "test-node": "node jasmine-testrunner.js", + "test-edge": "yarn pack-web && karma start --single-run --browsers Edge", + "test-firefox": "yarn pack-web && karma start --single-run --browsers Firefox", + "test-chrome": "yarn pack-web && karma start --single-run --browsers ChromeHeadless", + "test-safari": "yarn pack-web && karma start --single-run --browsers Safari", + "test": "yarn build-or-skip && yarn test-node", + "coverage": "nyc --reporter=text --reporter=lcov yarn test --quiet", + "prebuild": "shx rm -rf ./build", + "build": "tsc", + "build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build", + "pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js" + }, + "dependencies": { + "@cosmjs/encoding": "^0.25.0-alpha.0", + "@cosmjs/utils": "^0.25.0-alpha.0" + }, + "devDependencies": { + } +} diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts new file mode 100644 index 00000000..02c6900d --- /dev/null +++ b/packages/amino/src/encoding.spec.ts @@ -0,0 +1,135 @@ +import { Bech32, fromBase64 } from "@cosmjs/encoding"; + +import { + decodeAminoPubkey, + decodeBech32Pubkey, + encodeAminoPubkey, + encodeBech32Pubkey, + encodeSecp256k1Pubkey, +} from "./encoding"; +import { PubKey } from "./pubkeys"; + +describe("pubkey", () => { + describe("encodeSecp256k1Pubkey", () => { + it("encodes a compresed pubkey", () => { + const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); + expect(encodeSecp256k1Pubkey(pubkey)).toEqual({ + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }); + }); + + it("throws for uncompressed public keys", () => { + const pubkey = fromBase64( + "BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=", + ); + expect(() => encodeSecp256k1Pubkey(pubkey)).toThrowError(/public key must be compressed secp256k1/i); + }); + }); + + describe("decodeAminoPubkey", () => { + it("works for secp256k1", () => { + const amino = Bech32.decode( + "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5", + ).data; + expect(decodeAminoPubkey(amino)).toEqual({ + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }); + }); + + it("works for ed25519", () => { + // Encoded from `corald tendermint show-validator` + // Decoded from http://localhost:26657/validators + const amino = Bech32.decode( + "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", + ).data; + expect(decodeAminoPubkey(amino)).toEqual({ + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }); + }); + }); + + describe("decodeBech32Pubkey", () => { + it("works", () => { + expect( + decodeBech32Pubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"), + ).toEqual({ + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }); + }); + + it("works for enigma pubkey", () => { + expect( + decodeBech32Pubkey("enigmapub1addwnpepqw5k9p439nw0zpg2aundx4umwx4nw233z5prpjqjv5anl5grmnchzp2xwvv"), + ).toEqual({ + type: "tendermint/PubKeySecp256k1", + value: "A6lihrEs3PEFCu8m01ebcas3KjEVAjDIEmU7P9ED3PFx", + }); + }); + + it("works for ed25519", () => { + // Encoded from `corald tendermint show-validator` + // Decoded from http://localhost:26657/validators + const decoded = decodeBech32Pubkey( + "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", + ); + expect(decoded).toEqual({ + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }); + }); + }); + + describe("encodeAminoPubkey", () => { + it("works for secp256k1", () => { + const pubkey: PubKey = { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }; + const expected = Bech32.decode( + "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5", + ).data; + expect(encodeAminoPubkey(pubkey)).toEqual(expected); + }); + + it("works for ed25519", () => { + // Decoded from http://localhost:26657/validators + // Encoded from `corald tendermint show-validator` + const pubkey: PubKey = { + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }; + const expected = Bech32.decode( + "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", + ).data; + expect(encodeAminoPubkey(pubkey)).toEqual(expected); + }); + }); + + describe("encodeBech32Pubkey", () => { + it("works for secp256k1", () => { + const pubkey: PubKey = { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }; + expect(encodeBech32Pubkey(pubkey, "cosmospub")).toEqual( + "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5", + ); + }); + + it("works for ed25519", () => { + // Decoded from http://localhost:26657/validators + // Encoded from `corald tendermint show-validator` + const pubkey: PubKey = { + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }; + expect(encodeBech32Pubkey(pubkey, "coralvalconspub")).toEqual( + "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", + ); + }); + }); +}); diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts new file mode 100644 index 00000000..cc6297a0 --- /dev/null +++ b/packages/amino/src/encoding.ts @@ -0,0 +1,97 @@ +import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; +import { arrayContentEquals } from "@cosmjs/utils"; + +import { PubKey, pubkeyType } from "./pubkeys"; + +export function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey { + 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.secp256k1, + 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 +const pubkeyAminoPrefixSecp256k1 = fromHex("eb5ae98721"); +const pubkeyAminoPrefixEd25519 = fromHex("1624de6420"); +const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005"); +const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length; + +/** + * Decodes a pubkey in the Amino binary format to a type/value object. + */ +export function decodeAminoPubkey(data: Uint8Array): PubKey { + const aminoPrefix = data.slice(0, pubkeyAminoPrefixLength); + const rest = data.slice(pubkeyAminoPrefixLength); + if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSecp256k1)) { + if (rest.length !== 33) { + throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); + } + return { + type: pubkeyType.secp256k1, + value: toBase64(rest), + }; + } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixEd25519)) { + if (rest.length !== 32) { + throw new Error("Invalid rest data length. Expected 32 bytes (Ed25519 pubkey)."); + } + return { + type: pubkeyType.ed25519, + value: toBase64(rest), + }; + } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSr25519)) { + if (rest.length !== 32) { + throw new Error("Invalid rest data length. Expected 32 bytes (Sr25519 pubkey)."); + } + return { + type: pubkeyType.sr25519, + value: toBase64(rest), + }; + } else { + throw new Error("Unsupported Pubkey type. Amino prefix: " + toHex(aminoPrefix)); + } +} + +/** + * Decodes a bech32 pubkey to Amino binary, which is then decoded to a type/value object. + * The bech32 prefix is ignored and discareded. + * + * @param bechEncoded the bech32 encoded pubkey + */ +export function decodeBech32Pubkey(bechEncoded: string): PubKey { + const { data } = Bech32.decode(bechEncoded); + return decodeAminoPubkey(data); +} + +/** + * Encodes a public key to binary Amino. + */ +export function encodeAminoPubkey(pubkey: PubKey): Uint8Array { + let aminoPrefix: Uint8Array; + switch (pubkey.type) { + // Note: please don't add cases here without writing additional unit tests + case pubkeyType.secp256k1: + aminoPrefix = pubkeyAminoPrefixSecp256k1; + break; + case pubkeyType.ed25519: + aminoPrefix = pubkeyAminoPrefixEd25519; + break; + default: + throw new Error("Unsupported pubkey type"); + } + return new Uint8Array([...aminoPrefix, ...fromBase64(pubkey.value)]); +} + +/** + * Encodes a public key to binary Amino and then to bech32. + * + * @param pubkey the public key to encode + * @param prefix the bech32 prefix (human readable part) + */ +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 new file mode 100644 index 00000000..7f01fc17 --- /dev/null +++ b/packages/amino/src/index.ts @@ -0,0 +1,8 @@ +export { + decodeAminoPubkey, + decodeBech32Pubkey, + encodeAminoPubkey, + encodeBech32Pubkey, + encodeSecp256k1Pubkey, +} from "./encoding"; +export { PubKey, pubkeyType } from "./pubkeys"; diff --git a/packages/amino/src/pubkeys.ts b/packages/amino/src/pubkeys.ts new file mode 100644 index 00000000..3036afcf --- /dev/null +++ b/packages/amino/src/pubkeys.ts @@ -0,0 +1,18 @@ +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: string; +} + +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 */ + 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/amino/tsconfig.json b/packages/amino/tsconfig.json new file mode 100644 index 00000000..df66add1 --- /dev/null +++ b/packages/amino/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "build", + "rootDir": "src" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/amino/typedoc.js b/packages/amino/typedoc.js new file mode 100644 index 00000000..ffe4be64 --- /dev/null +++ b/packages/amino/typedoc.js @@ -0,0 +1,11 @@ +const packageJson = require("./package.json"); + +module.exports = { + entryPoints: ["./src"], + out: "docs", + exclude: "**/*.spec.ts", + name: `${packageJson.name} Documentation`, + readme: "README.md", + excludeExternals: true, + excludePrivate: true, +}; diff --git a/packages/amino/webpack.web.config.js b/packages/amino/webpack.web.config.js new file mode 100644 index 00000000..9d5836a8 --- /dev/null +++ b/packages/amino/webpack.web.config.js @@ -0,0 +1,17 @@ +const glob = require("glob"); +const path = require("path"); + +const target = "web"; +const distdir = path.join(__dirname, "dist", "web"); + +module.exports = [ + { + // bundle used for Karma tests + target: target, + entry: glob.sync("./build/**/*.spec.js"), + output: { + path: distdir, + filename: "tests.js", + }, + }, +]; diff --git a/packages/cosmwasm-stargate/package.json b/packages/cosmwasm-stargate/package.json index 8261f247..fa4945e3 100644 --- a/packages/cosmwasm-stargate/package.json +++ b/packages/cosmwasm-stargate/package.json @@ -42,6 +42,7 @@ "postdefine-proto": "prettier --write \"src/codec/**/*.ts\"" }, "dependencies": { + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/cosmwasm-launchpad": "^0.25.0-alpha.0", "@cosmjs/crypto": "^0.25.0-alpha.0", "@cosmjs/encoding": "^0.25.0-alpha.0", diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index e4d12d71..7b008c5e 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { encodeSecp256k1Pubkey } from "@cosmjs/amino"; import { ChangeAdminResult, CosmWasmFeeTable, @@ -16,7 +17,6 @@ import { buildFeeTable, Coin, CosmosFeeTable, - encodeSecp256k1Pubkey, GasLimits, GasPrice, logs, diff --git a/packages/launchpad/package.json b/packages/launchpad/package.json index 0c8a4bfa..d6ed8030 100644 --- a/packages/launchpad/package.json +++ b/packages/launchpad/package.json @@ -39,6 +39,7 @@ "pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js" }, "dependencies": { + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/crypto": "^0.25.0-alpha.0", "@cosmjs/encoding": "^0.25.0-alpha.0", "@cosmjs/math": "^0.25.0-alpha.0", diff --git a/packages/launchpad/src/address.ts b/packages/launchpad/src/address.ts index 87fede26..a35b2a0e 100644 --- a/packages/launchpad/src/address.ts +++ b/packages/launchpad/src/address.ts @@ -1,8 +1,7 @@ +import { PubKey, pubkeyType } from "@cosmjs/amino"; import { ripemd160, sha256 } from "@cosmjs/crypto"; import { Bech32, fromBase64 } from "@cosmjs/encoding"; -import { PubKey, pubkeyType } from "./types"; - export function rawSecp256k1PubkeyToAddress(pubkeyRaw: Uint8Array, prefix: string): string { if (pubkeyRaw.length !== 33) { throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyRaw.length}`); diff --git a/packages/launchpad/src/cosmosclient.ts b/packages/launchpad/src/cosmosclient.ts index 79194bc9..d49e018b 100644 --- a/packages/launchpad/src/cosmosclient.ts +++ b/packages/launchpad/src/cosmosclient.ts @@ -1,3 +1,4 @@ +import { PubKey } from "@cosmjs/amino"; import { sha256 } from "@cosmjs/crypto"; import { fromBase64, fromHex, toHex } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; @@ -13,7 +14,6 @@ import { } from "./lcdapi"; import { Log, parseLogs } from "./logs"; import { StdTx, WrappedStdTx } from "./tx"; -import { PubKey } from "./types"; export interface GetSequenceResult { readonly accountNumber: number; diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index dc7a11d0..46b2e770 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -1,3 +1,14 @@ +// Re-exports for backwards compatibility +export { + decodeAminoPubkey, + decodeBech32Pubkey, + encodeAminoPubkey, + encodeBech32Pubkey, + encodeSecp256k1Pubkey, + pubkeyType, + PubKey, +} from "@cosmjs/amino"; + import * as logs from "./logs"; export { logs }; @@ -125,19 +136,12 @@ export { MsgWithdrawValidatorCommission, } from "./msgs"; export { makeCosmoshubPath } from "./paths"; -export { - decodeAminoPubkey, - decodeBech32Pubkey, - encodeAminoPubkey, - encodeBech32Pubkey, - encodeSecp256k1Pubkey, -} from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { AccountData, Algo, AminoSignResponse, OfflineSigner } from "./signer"; export { CosmosFeeTable, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, isWrappedStdTx, makeStdTx, CosmosSdkTx, StdTx, WrappedStdTx, WrappedTx } from "./tx"; -export { pubkeyType, PubKey, StdFee, StdSignature } from "./types"; +export { StdFee, StdSignature } from "./types"; export { executeKdf, KdfConfiguration } from "./wallet"; export { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; export { Secp256k1Wallet } from "./secp256k1wallet"; diff --git a/packages/launchpad/src/lcdapi/auth.ts b/packages/launchpad/src/lcdapi/auth.ts index 6b316466..009c91ba 100644 --- a/packages/launchpad/src/lcdapi/auth.ts +++ b/packages/launchpad/src/lcdapi/auth.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { PubKey } from "@cosmjs/amino"; + import { Coin } from "../coins"; -import { PubKey } from "../types"; import { LcdClient } from "./lcdclient"; /** diff --git a/packages/launchpad/src/lcdapi/utils.spec.ts b/packages/launchpad/src/lcdapi/utils.spec.ts index 27b370a9..848ea00c 100644 --- a/packages/launchpad/src/lcdapi/utils.spec.ts +++ b/packages/launchpad/src/lcdapi/utils.spec.ts @@ -1,4 +1,5 @@ -import { PubKey } from "../types"; +import { PubKey } from "@cosmjs/amino"; + import { normalizePubkey, uint64ToNumber, uint64ToString } from "./utils"; describe("utils", () => { diff --git a/packages/launchpad/src/lcdapi/utils.ts b/packages/launchpad/src/lcdapi/utils.ts index 5f797746..1fd35af3 100644 --- a/packages/launchpad/src/lcdapi/utils.ts +++ b/packages/launchpad/src/lcdapi/utils.ts @@ -1,8 +1,6 @@ +import { decodeBech32Pubkey, PubKey } from "@cosmjs/amino"; import { Uint64 } from "@cosmjs/math"; -import { decodeBech32Pubkey } from "../pubkey"; -import { PubKey } from "../types"; - /** * Converts an integer expressed as number or string to a number. * Throws if input is not a valid uint64 or if the value exceeds MAX_SAFE_INTEGER. diff --git a/packages/launchpad/src/pubkey.spec.ts b/packages/launchpad/src/pubkey.spec.ts index 9576239b..e69de29b 100644 --- a/packages/launchpad/src/pubkey.spec.ts +++ b/packages/launchpad/src/pubkey.spec.ts @@ -1,135 +0,0 @@ -import { Bech32, fromBase64 } from "@cosmjs/encoding"; - -import { - decodeAminoPubkey, - decodeBech32Pubkey, - encodeAminoPubkey, - encodeBech32Pubkey, - encodeSecp256k1Pubkey, -} from "./pubkey"; -import { PubKey } from "./types"; - -describe("pubkey", () => { - describe("encodeSecp256k1Pubkey", () => { - it("encodes a compresed pubkey", () => { - const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); - expect(encodeSecp256k1Pubkey(pubkey)).toEqual({ - type: "tendermint/PubKeySecp256k1", - value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", - }); - }); - - it("throws for uncompressed public keys", () => { - const pubkey = fromBase64( - "BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=", - ); - expect(() => encodeSecp256k1Pubkey(pubkey)).toThrowError(/public key must be compressed secp256k1/i); - }); - }); - - describe("decodeAminoPubkey", () => { - it("works for secp256k1", () => { - const amino = Bech32.decode( - "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5", - ).data; - expect(decodeAminoPubkey(amino)).toEqual({ - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }); - }); - - it("works for ed25519", () => { - // Encoded from `corald tendermint show-validator` - // Decoded from http://localhost:26657/validators - const amino = Bech32.decode( - "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", - ).data; - expect(decodeAminoPubkey(amino)).toEqual({ - type: "tendermint/PubKeyEd25519", - value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", - }); - }); - }); - - describe("decodeBech32Pubkey", () => { - it("works", () => { - expect( - decodeBech32Pubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"), - ).toEqual({ - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }); - }); - - it("works for enigma pubkey", () => { - expect( - decodeBech32Pubkey("enigmapub1addwnpepqw5k9p439nw0zpg2aundx4umwx4nw233z5prpjqjv5anl5grmnchzp2xwvv"), - ).toEqual({ - type: "tendermint/PubKeySecp256k1", - value: "A6lihrEs3PEFCu8m01ebcas3KjEVAjDIEmU7P9ED3PFx", - }); - }); - - it("works for ed25519", () => { - // Encoded from `corald tendermint show-validator` - // Decoded from http://localhost:26657/validators - const decoded = decodeBech32Pubkey( - "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", - ); - expect(decoded).toEqual({ - type: "tendermint/PubKeyEd25519", - value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", - }); - }); - }); - - describe("encodeAminoPubkey", () => { - it("works for secp256k1", () => { - const pubkey: PubKey = { - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }; - const expected = Bech32.decode( - "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5", - ).data; - expect(encodeAminoPubkey(pubkey)).toEqual(expected); - }); - - it("works for ed25519", () => { - // Decoded from http://localhost:26657/validators - // Encoded from `corald tendermint show-validator` - const pubkey: PubKey = { - type: "tendermint/PubKeyEd25519", - value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", - }; - const expected = Bech32.decode( - "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", - ).data; - expect(encodeAminoPubkey(pubkey)).toEqual(expected); - }); - }); - - describe("encodeBech32Pubkey", () => { - it("works for secp256k1", () => { - const pubkey: PubKey = { - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }; - expect(encodeBech32Pubkey(pubkey, "cosmospub")).toEqual( - "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5", - ); - }); - - it("works for ed25519", () => { - // Decoded from http://localhost:26657/validators - // Encoded from `corald tendermint show-validator` - const pubkey: PubKey = { - type: "tendermint/PubKeyEd25519", - value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", - }; - expect(encodeBech32Pubkey(pubkey, "coralvalconspub")).toEqual( - "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", - ); - }); - }); -}); diff --git a/packages/launchpad/src/pubkey.ts b/packages/launchpad/src/pubkey.ts index bebc3c62..e69de29b 100644 --- a/packages/launchpad/src/pubkey.ts +++ b/packages/launchpad/src/pubkey.ts @@ -1,97 +0,0 @@ -import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; -import { arrayContentEquals } from "@cosmjs/utils"; - -import { PubKey, pubkeyType } from "./types"; - -export function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey { - 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.secp256k1, - 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 -const pubkeyAminoPrefixSecp256k1 = fromHex("eb5ae98721"); -const pubkeyAminoPrefixEd25519 = fromHex("1624de6420"); -const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005"); -const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length; - -/** - * Decodes a pubkey in the Amino binary format to a type/value object. - */ -export function decodeAminoPubkey(data: Uint8Array): PubKey { - const aminoPrefix = data.slice(0, pubkeyAminoPrefixLength); - const rest = data.slice(pubkeyAminoPrefixLength); - if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSecp256k1)) { - if (rest.length !== 33) { - throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); - } - return { - type: pubkeyType.secp256k1, - value: toBase64(rest), - }; - } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixEd25519)) { - if (rest.length !== 32) { - throw new Error("Invalid rest data length. Expected 32 bytes (Ed25519 pubkey)."); - } - return { - type: pubkeyType.ed25519, - value: toBase64(rest), - }; - } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSr25519)) { - if (rest.length !== 32) { - throw new Error("Invalid rest data length. Expected 32 bytes (Sr25519 pubkey)."); - } - return { - type: pubkeyType.sr25519, - value: toBase64(rest), - }; - } else { - throw new Error("Unsupported Pubkey type. Amino prefix: " + toHex(aminoPrefix)); - } -} - -/** - * Decodes a bech32 pubkey to Amino binary, which is then decoded to a type/value object. - * The bech32 prefix is ignored and discareded. - * - * @param bechEncoded the bech32 encoded pubkey - */ -export function decodeBech32Pubkey(bechEncoded: string): PubKey { - const { data } = Bech32.decode(bechEncoded); - return decodeAminoPubkey(data); -} - -/** - * Encodes a public key to binary Amino. - */ -export function encodeAminoPubkey(pubkey: PubKey): Uint8Array { - let aminoPrefix: Uint8Array; - switch (pubkey.type) { - // Note: please don't add cases here without writing additional unit tests - case pubkeyType.secp256k1: - aminoPrefix = pubkeyAminoPrefixSecp256k1; - break; - case pubkeyType.ed25519: - aminoPrefix = pubkeyAminoPrefixEd25519; - break; - default: - throw new Error("Unsupported pubkey type"); - } - return new Uint8Array([...aminoPrefix, ...fromBase64(pubkey.value)]); -} - -/** - * Encodes a public key to binary Amino and then to bech32. - * - * @param pubkey the public key to encode - * @param prefix the bech32 prefix (human readable part) - */ -export function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string { - return Bech32.encode(prefix, encodeAminoPubkey(pubkey)); -} diff --git a/packages/launchpad/src/signature.ts b/packages/launchpad/src/signature.ts index d94ad665..15f37f00 100644 --- a/packages/launchpad/src/signature.ts +++ b/packages/launchpad/src/signature.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { encodeSecp256k1Pubkey, pubkeyType } from "@cosmjs/amino"; import { fromBase64, toBase64 } from "@cosmjs/encoding"; -import { encodeSecp256k1Pubkey } from "./pubkey"; -import { pubkeyType, StdSignature } from "./types"; +import { StdSignature } from "./types"; /** * Takes a binary pubkey and signature to create a signature object diff --git a/packages/launchpad/src/types.ts b/packages/launchpad/src/types.ts index 8f0b5565..7cccf774 100644 --- a/packages/launchpad/src/types.ts +++ b/packages/launchpad/src/types.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { PubKey } from "@cosmjs/amino"; + import { Coin } from "./coins"; export interface StdFee { @@ -10,22 +12,3 @@ export interface StdSignature { readonly pub_key: PubKey; readonly signature: string; } - -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: string; -} - -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 */ - 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/package.json b/packages/proto-signing/package.json index 64355abd..806b3a8f 100644 --- a/packages/proto-signing/package.json +++ b/packages/proto-signing/package.json @@ -43,6 +43,7 @@ "postdefine-proto": "prettier --write \"src/codec/**/*.ts\"" }, "dependencies": { + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/launchpad": "^0.25.0-alpha.0", "long": "^4.0.0", "protobufjs": "~6.10.2" diff --git a/packages/proto-signing/src/pubkey.ts b/packages/proto-signing/src/pubkey.ts index 08bb9b2f..e44d3b80 100644 --- a/packages/proto-signing/src/pubkey.ts +++ b/packages/proto-signing/src/pubkey.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { encodeSecp256k1Pubkey, PubKey as AminoPubKey } from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; -import { encodeSecp256k1Pubkey, PubKey as LaunchpadPubKey } from "@cosmjs/launchpad"; import { PubKey } from "./codec/cosmos/crypto/secp256k1/keys"; import { Any } from "./codec/google/protobuf/any"; -export function encodePubkey(pubkey: LaunchpadPubKey): Any { +export function encodePubkey(pubkey: AminoPubKey): Any { switch (pubkey.type) { case "tendermint/PubKeySecp256k1": { const pubkeyProto = PubKey.fromPartial({ @@ -21,7 +21,7 @@ export function encodePubkey(pubkey: LaunchpadPubKey): Any { } } -export function decodePubkey(pubkey?: Any | null): LaunchpadPubKey | null { +export function decodePubkey(pubkey?: Any | null): AminoPubKey | null { if (!pubkey || !pubkey.value) { return null; } diff --git a/packages/stargate/package.json b/packages/stargate/package.json index 70925d26..0e68bab6 100644 --- a/packages/stargate/package.json +++ b/packages/stargate/package.json @@ -43,6 +43,7 @@ }, "dependencies": { "@confio/ics23": "^0.6.3", + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/encoding": "^0.25.0-alpha.0", "@cosmjs/launchpad": "^0.25.0-alpha.0", "@cosmjs/math": "^0.25.0-alpha.0", diff --git a/packages/stargate/src/accounts.ts b/packages/stargate/src/accounts.ts index 58280523..e6594b35 100644 --- a/packages/stargate/src/accounts.ts +++ b/packages/stargate/src/accounts.ts @@ -1,4 +1,4 @@ -import { PubKey } from "@cosmjs/launchpad"; +import { PubKey } from "@cosmjs/amino"; import { Uint64 } from "@cosmjs/math"; import { decodePubkey } from "@cosmjs/proto-signing"; import { assert } from "@cosmjs/utils"; diff --git a/packages/stargate/src/aminotypes.spec.ts b/packages/stargate/src/aminotypes.spec.ts index 0c768341..bf598e3c 100644 --- a/packages/stargate/src/aminotypes.spec.ts +++ b/packages/stargate/src/aminotypes.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { encodeBech32Pubkey } from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; import { coin, coins, - encodeBech32Pubkey, MsgBeginRedelegate as LaunchpadMsgBeginRedelegate, MsgCreateValidator as LaunchpadMsgCreateValidator, MsgDelegate as LaunchpadMsgDelegate, diff --git a/packages/stargate/src/aminotypes.ts b/packages/stargate/src/aminotypes.ts index b79edf93..7392af22 100644 --- a/packages/stargate/src/aminotypes.ts +++ b/packages/stargate/src/aminotypes.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { decodeBech32Pubkey, encodeBech32Pubkey } from "@cosmjs/amino"; import { fromBase64, toBase64 } from "@cosmjs/encoding"; import { - decodeBech32Pubkey, - encodeBech32Pubkey, Msg, MsgBeginRedelegate as LaunchpadMsgBeginRedelegate, MsgCreateValidator as LaunchpadMsgCreateValidator, diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index b7d5bc75..08d70484 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -1,9 +1,9 @@ +import { encodeSecp256k1Pubkey } from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; import { buildFeeTable, Coin, CosmosFeeTable, - encodeSecp256k1Pubkey, GasLimits, GasPrice, makeSignDoc as makeSignDocAmino, From 810389d784462db639df9c94b9bb8d1cd916cc4a Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 11:09:27 +0100 Subject: [PATCH 02/25] Generalize pubkey types --- CHANGELOG.md | 5 +++ packages/amino/src/encoding.spec.ts | 12 +++--- packages/amino/src/encoding.ts | 12 +++--- packages/amino/src/index.ts | 2 +- packages/amino/src/pubkeys.spec.ts | 44 +++++++++++++++++++++ packages/amino/src/pubkeys.ts | 32 +++++++++++++-- packages/launchpad/src/address.ts | 4 +- packages/launchpad/src/cosmosclient.ts | 4 +- packages/launchpad/src/index.ts | 2 +- packages/launchpad/src/lcdapi/auth.ts | 4 +- packages/launchpad/src/lcdapi/utils.spec.ts | 4 +- packages/launchpad/src/lcdapi/utils.ts | 4 +- packages/launchpad/src/types.ts | 4 +- packages/proto-signing/src/pubkey.ts | 2 +- packages/stargate/src/accounts.ts | 4 +- 15 files changed, 107 insertions(+), 32 deletions(-) create mode 100644 packages/amino/src/pubkeys.spec.ts 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; } From d50575036f585dc0333f4dd0fc2f1bf32acd0772 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 11:36:55 +0100 Subject: [PATCH 03/25] Add arrayContentStartsWith --- CHANGELOG.md | 2 ++ packages/utils/src/array.spec.ts | 30 +++++++++++++++++++++++++++++- packages/utils/src/arrays.ts | 19 +++++++++++++++++++ packages/utils/src/index.ts | 2 +- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e3e53c7..660720ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ and this project adheres to 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. +- @cosmjs/utils: The new `arrayContentStartsWith` works similar to + `arrayContentEquals` but only checks the start of an array. ### Changed diff --git a/packages/utils/src/array.spec.ts b/packages/utils/src/array.spec.ts index d42241bc..0dae826e 100644 --- a/packages/utils/src/array.spec.ts +++ b/packages/utils/src/array.spec.ts @@ -1,4 +1,4 @@ -import { arrayContentEquals } from "./arrays"; +import { arrayContentEquals, arrayContentStartsWith } from "./arrays"; describe("array", () => { describe("arrayContentEquals", () => { @@ -30,4 +30,32 @@ describe("array", () => { expect(arrayContentEquals([], new Uint8Array([]))).toEqual(true); }); }); + + describe("arrayContentStartsWith", () => { + it("can compare number arrays", () => { + // same length + expect(arrayContentStartsWith([], [])).toEqual(true); // Same behaviour as "".startsWith("") + expect(arrayContentStartsWith([1, 2, 3], [1, 2, 3])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3], [1, 2, 8])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3], [0, 0, 0])).toEqual(false); + + // a shorter than b + expect(arrayContentStartsWith([], [1, 2, 3])).toEqual(false); + expect(arrayContentStartsWith([1], [1, 2, 3])).toEqual(false); + expect(arrayContentStartsWith([1, 2], [1, 2, 3])).toEqual(false); + + // a longer than b + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3, 4])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3, 4, 0])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3, 0])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 0])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 0])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [0])).toEqual(false); + }); + }); }); diff --git a/packages/utils/src/arrays.ts b/packages/utils/src/arrays.ts index 87f5a442..4f15b89e 100644 --- a/packages/utils/src/arrays.ts +++ b/packages/utils/src/arrays.ts @@ -16,3 +16,22 @@ export function arrayContentEquals( } return true; } + +/** + * Checks if `a` starts with the contents of `b`. + * + * This requires equality of the element values, where element equality means `===` returning `true`. + * + * This allows you to compare the content of a Buffer, Uint8Array or number[], ignoring the specific type. + * As a consequence, this returns different results than Jasmine's `toEqual`, which ensures elements have the same type. + */ +export function arrayContentStartsWith( + a: ArrayLike, + b: ArrayLike, +): boolean { + if (a.length < b.length) return false; + for (let i = 0; i < b.length; ++i) { + if (a[i] !== b[i]) return false; + } + return true; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 372b3615..790c7d27 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,4 @@ -export { arrayContentEquals } from "./arrays"; +export { arrayContentEquals, arrayContentStartsWith } from "./arrays"; export { assert, assertDefined, assertDefinedAndNotNull } from "./assert"; export { sleep } from "./sleep"; export { isNonNullObject, isUint8Array } from "./typechecks"; From 6190ca5c644c19137c34c66af972c708cef29c8f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 11:53:56 +0100 Subject: [PATCH 04/25] Cleanup prefix handling for decodeAminoPubkey --- packages/amino/src/encoding.spec.ts | 4 ++++ packages/amino/src/encoding.ts | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index e3f60f56..5fd43919 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -49,6 +49,10 @@ describe("encoding", () => { value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", }); }); + + it("works for sr25519", () => { + pending("No test data available"); + }); }); describe("decodeBech32Pubkey", () => { diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index 58ba9202..5acbc8f9 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -1,5 +1,5 @@ import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; -import { arrayContentEquals } from "@cosmjs/utils"; +import { arrayContentStartsWith } from "@cosmjs/utils"; import { Pubkey, pubkeyType, Secp256k1Pubkey } from "./pubkeys"; @@ -16,18 +16,16 @@ export function encodeSecp256k1Pubkey(pubkey: Uint8Array): Secp256k1Pubkey { // 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 -const pubkeyAminoPrefixSecp256k1 = fromHex("eb5ae98721"); -const pubkeyAminoPrefixEd25519 = fromHex("1624de6420"); -const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005"); -const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length; +const pubkeyAminoPrefixSecp256k1 = fromHex("eb5ae987" + "21" /* fixed length */); +const pubkeyAminoPrefixEd25519 = fromHex("1624de64" + "20" /* fixed length */); +const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005" + "20" /* fixed length */); /** * Decodes a pubkey in the Amino binary format to a type/value object. */ export function decodeAminoPubkey(data: Uint8Array): Pubkey { - const aminoPrefix = data.slice(0, pubkeyAminoPrefixLength); - const rest = data.slice(pubkeyAminoPrefixLength); - if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSecp256k1)) { + if (arrayContentStartsWith(data, pubkeyAminoPrefixSecp256k1)) { + const rest = data.slice(pubkeyAminoPrefixSecp256k1.length); if (rest.length !== 33) { throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); } @@ -35,7 +33,8 @@ export function decodeAminoPubkey(data: Uint8Array): Pubkey { type: pubkeyType.secp256k1, value: toBase64(rest), }; - } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixEd25519)) { + } else if (arrayContentStartsWith(data, pubkeyAminoPrefixEd25519)) { + const rest = data.slice(pubkeyAminoPrefixEd25519.length); if (rest.length !== 32) { throw new Error("Invalid rest data length. Expected 32 bytes (Ed25519 pubkey)."); } @@ -43,7 +42,8 @@ export function decodeAminoPubkey(data: Uint8Array): Pubkey { type: pubkeyType.ed25519, value: toBase64(rest), }; - } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSr25519)) { + } else if (arrayContentStartsWith(data, pubkeyAminoPrefixSr25519)) { + const rest = data.slice(pubkeyAminoPrefixSr25519.length); if (rest.length !== 32) { throw new Error("Invalid rest data length. Expected 32 bytes (Sr25519 pubkey)."); } @@ -52,7 +52,7 @@ export function decodeAminoPubkey(data: Uint8Array): Pubkey { value: toBase64(rest), }; } else { - throw new Error("Unsupported Pubkey type. Amino prefix: " + toHex(aminoPrefix)); + throw new Error("Unsupported public key type. Amino data starts with: " + toHex(data.slice(0, 5))); } } From 61aa989fb3714d63545531176230984b7c0cf32d Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 12:13:44 +0100 Subject: [PATCH 05/25] Add MultisigThresholdPubkey/isMultisigThresholdPubkey --- packages/amino/src/index.ts | 10 +++++++++- packages/amino/src/pubkeys.spec.ts | 10 +++++++++- packages/amino/src/pubkeys.ts | 13 +++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index 5469771a..11c4b303 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -5,4 +5,12 @@ export { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; -export { Pubkey, Secp256k1Pubkey, SinglePubkey, isSinglePubkey, pubkeyType } from "./pubkeys"; +export { + MultisigThresholdPubkey, + Pubkey, + Secp256k1Pubkey, + SinglePubkey, + isMultisigThresholdPubkey, + isSinglePubkey, + pubkeyType, +} from "./pubkeys"; diff --git a/packages/amino/src/pubkeys.spec.ts b/packages/amino/src/pubkeys.spec.ts index 858c0815..ec47bdcb 100644 --- a/packages/amino/src/pubkeys.spec.ts +++ b/packages/amino/src/pubkeys.spec.ts @@ -1,4 +1,4 @@ -import { isSinglePubkey } from "./pubkeys"; +import { isMultisigThresholdPubkey, isSinglePubkey } from "./pubkeys"; describe("pubkeys", () => { const pubkeyEd25519 = { @@ -41,4 +41,12 @@ describe("pubkeys", () => { expect(isSinglePubkey(pubkeyMultisigThreshold)).toEqual(false); }); }); + + describe("isMultisigThresholdPubkey", () => { + it("works", () => { + expect(isMultisigThresholdPubkey(pubkeyEd25519)).toEqual(false); + expect(isMultisigThresholdPubkey(pubkeySecp256k1)).toEqual(false); + expect(isMultisigThresholdPubkey(pubkeyMultisigThreshold)).toEqual(true); + }); + }); }); diff --git a/packages/amino/src/pubkeys.ts b/packages/amino/src/pubkeys.ts index e538e657..90da0a79 100644 --- a/packages/amino/src/pubkeys.ts +++ b/packages/amino/src/pubkeys.ts @@ -42,3 +42,16 @@ export function isSinglePubkey(pubkey: Pubkey): pubkey is SinglePubkey { const singPubkeyTypes: string[] = [pubkeyType.ed25519, pubkeyType.secp256k1, pubkeyType.sr25519]; return singPubkeyTypes.includes(pubkey.type); } + +export interface MultisigThresholdPubkey extends Pubkey { + readonly type: "tendermint/PubKeyMultisigThreshold"; + readonly value: { + /** A string-encoded integer */ + readonly threshold: string; + readonly pubkeys: readonly SinglePubkey[]; + }; +} + +export function isMultisigThresholdPubkey(pubkey: Pubkey): pubkey is MultisigThresholdPubkey { + return (pubkey as MultisigThresholdPubkey).type === "tendermint/PubKeyMultisigThreshold"; +} From 99e1ac36b85385d7c7fbd88ab39bc7698dd723f5 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 13:02:29 +0100 Subject: [PATCH 06/25] Add support for multisig in encodeAminoPubkey --- packages/amino/src/encoding.spec.ts | 47 ++++++++++++++++++++++++++++- packages/amino/src/encoding.ts | 23 +++++++++++++- packages/amino/src/pubkeys.ts | 1 + 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 5fd43919..7fa4a203 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -7,7 +7,7 @@ import { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; -import { Pubkey } from "./pubkeys"; +import { MultisigThresholdPubkey, Pubkey } from "./pubkeys"; describe("encoding", () => { describe("encodeSecp256k1Pubkey", () => { @@ -135,5 +135,50 @@ describe("encoding", () => { "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", ); }); + + it("works for multisig", () => { + // ./build/wasmd keys add test1 + // ./build/wasmd keys add test2 + // ./build/wasmd keys add test3 + // ./build/wasmd keys add testgroup1 --multisig=test1,test2,test3 --multisig-threshold 2 + // ./build/wasmd keys add testgroup2 --multisig=test1,test2,test3 --multisig-threshold 1 + // ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 + + const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + ); + const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + ); + const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + ); + + // 2/3 multisig + const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, + }; + const expected1 = Bech32.decode( + "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7v7aysdd", + ).data; + expect(encodeAminoPubkey(testgroup1)).toEqual(expected1); + + // 1/3 multisig + const testgroup2: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "1", + pubkeys: [test1, test2, test3], + }, + }; + const expected2 = Bech32.decode( + "wasmpub1ytql0csgqyfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vc4ejke", + ).data; + expect(encodeAminoPubkey(testgroup2)).toEqual(expected2); + }); }); }); diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index 5acbc8f9..689a67cd 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -1,7 +1,8 @@ import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; +import { Uint53 } from "@cosmjs/math"; import { arrayContentStartsWith } from "@cosmjs/utils"; -import { Pubkey, pubkeyType, Secp256k1Pubkey } from "./pubkeys"; +import { isMultisigThresholdPubkey, Pubkey, pubkeyType, Secp256k1Pubkey } from "./pubkeys"; export function encodeSecp256k1Pubkey(pubkey: Uint8Array): Secp256k1Pubkey { if (pubkey.length !== 33 || (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)) { @@ -19,6 +20,8 @@ export function encodeSecp256k1Pubkey(pubkey: Uint8Array): Secp256k1Pubkey { const pubkeyAminoPrefixSecp256k1 = 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 */ +const pubkeyAminoPrefixMultisigThreshold = fromHex("22c1f7e2" /* variable length not included */); /** * Decodes a pubkey in the Amino binary format to a type/value object. @@ -67,10 +70,28 @@ export function decodeBech32Pubkey(bechEncoded: string): Pubkey { return decodeAminoPubkey(data); } +function encodeSmallUint(value: number | string): number[] { + const checked = Uint53.fromString(value.toString()).toNumber(); + if (checked > 127) throw new Error("Encoding numbers > 127 is not supported here."); + return [checked]; +} + /** * Encodes a public key to binary Amino. */ export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { + if (isMultisigThresholdPubkey(pubkey)) { + const out = Array.from(pubkeyAminoPrefixMultisigThreshold); + out.push(8); // TODO: What is this? + out.push(...encodeSmallUint(pubkey.value.threshold)); + for (const pubkeyData of pubkey.value.pubkeys.map((p) => encodeAminoPubkey(p))) { + out.push(18); // TODO: What is this? + out.push(...encodeSmallUint(pubkeyData.length)); + out.push(...pubkeyData); + } + return new Uint8Array(out); + } + let aminoPrefix: Uint8Array; switch (pubkey.type) { // Note: please don't add cases here without writing additional unit tests diff --git a/packages/amino/src/pubkeys.ts b/packages/amino/src/pubkeys.ts index 90da0a79..215d0016 100644 --- a/packages/amino/src/pubkeys.ts +++ b/packages/amino/src/pubkeys.ts @@ -18,6 +18,7 @@ export const pubkeyType = { 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, + multisigThreshold: "tendermint/PubKeyMultisigThreshold" as const, }; /** From feef58843105cef431c7f1b4939172d1ee8dd615 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 14:53:48 +0100 Subject: [PATCH 07/25] Add 2/2 multisig --- packages/amino/src/encoding.spec.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 7fa4a203..aeebaf06 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -142,16 +142,16 @@ describe("encoding", () => { // ./build/wasmd keys add test3 // ./build/wasmd keys add testgroup1 --multisig=test1,test2,test3 --multisig-threshold 2 // ./build/wasmd keys add testgroup2 --multisig=test1,test2,test3 --multisig-threshold 1 - // ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 + // ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 # creates test1,test3 for some reason const test1 = decodeBech32Pubkey( - "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", // data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 ); const test2 = decodeBech32Pubkey( - "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", // data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 ); const test3 = decodeBech32Pubkey( - "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", // data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 ); // 2/3 multisig @@ -179,6 +179,19 @@ describe("encoding", () => { "wasmpub1ytql0csgqyfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vc4ejke", ).data; expect(encodeAminoPubkey(testgroup2)).toEqual(expected2); + + // 2/2 multisig + const testgroup3: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test3], + }, + }; + const expected3 = Bech32.decode( + "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vzjhugu", + ).data; + expect(encodeAminoPubkey(testgroup3)).toEqual(expected3); }); }); }); From ae97ce76a477b078f8b393d7c99ddfaf39c72226 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 15:11:31 +0100 Subject: [PATCH 08/25] Add more clarity on encodeUvarint --- packages/amino/src/encoding.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index 689a67cd..3a9969a3 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -70,9 +70,19 @@ export function decodeBech32Pubkey(bechEncoded: string): Pubkey { return decodeAminoPubkey(data); } -function encodeSmallUint(value: number | string): number[] { +/** + * Uvarint encoder for Amino. This is the same encoding as `binary.PutUvarint` from the Go + * standard library. + * + * @see https://github.com/tendermint/go-amino/blob/8e779b71f40d175/encoder.go#L77-L85 + */ +function encodeUvarint(value: number | string): number[] { const checked = Uint53.fromString(value.toString()).toNumber(); - if (checked > 127) throw new Error("Encoding numbers > 127 is not supported here."); + if (checked > 127) { + throw new Error( + "Encoding numbers > 127 is not supported here. Please tell those lazy CosmJS maintainers to port the binary.PutUvarint implementation from the Go standard library and write some tests.", + ); + } return [checked]; } @@ -83,10 +93,10 @@ export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { if (isMultisigThresholdPubkey(pubkey)) { const out = Array.from(pubkeyAminoPrefixMultisigThreshold); out.push(8); // TODO: What is this? - out.push(...encodeSmallUint(pubkey.value.threshold)); + out.push(...encodeUvarint(pubkey.value.threshold)); for (const pubkeyData of pubkey.value.pubkeys.map((p) => encodeAminoPubkey(p))) { out.push(18); // TODO: What is this? - out.push(...encodeSmallUint(pubkeyData.length)); + out.push(...encodeUvarint(pubkeyData.length)); out.push(...pubkeyData); } return new Uint8Array(out); From 78198363872432c972dac2d00ae1318e1bb432c5 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 15:43:49 +0100 Subject: [PATCH 09/25] Create test with custom sorting --- packages/amino/src/encoding.spec.ts | 32 +++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index aeebaf06..838ba881 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -142,16 +142,27 @@ describe("encoding", () => { // ./build/wasmd keys add test3 // ./build/wasmd keys add testgroup1 --multisig=test1,test2,test3 --multisig-threshold 2 // ./build/wasmd keys add testgroup2 --multisig=test1,test2,test3 --multisig-threshold 1 - // ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 # creates test1,test3 for some reason + // # By default pubkeys are sorted by its address data (https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/client/keys/add.go#L172-L174) + // ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 + // ./build/wasmd keys add testgroup4 --multisig=test3,test1 --nosort --multisig-threshold 2 const test1 = decodeBech32Pubkey( - "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", // data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + // pubkey data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 + // address: wasm1jq59w7y34msq69g4w3zvq6d5h3stcajd8g62xm + // address data: 9028577891aee00d15157444c069b4bc60bc764d ); const test2 = decodeBech32Pubkey( - "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", // data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + // pubkey data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 + // address: wasm146e52j6zphxw8m67cz8860ad5uju892cqmawsg + // address data: aeb3454b420dcce3ef5ec08e7d3fada725c39558 ); const test3 = decodeBech32Pubkey( - "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", // data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + // pubkey data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 + // address: wasm1a6uxr25mw8qg8zz3l2avsdjsveh4yg9sw7h5np + // address data: eeb861aa9b71c0838851fabac83650666f5220b0 ); // 2/3 multisig @@ -192,6 +203,19 @@ describe("encoding", () => { "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vzjhugu", ).data; expect(encodeAminoPubkey(testgroup3)).toEqual(expected3); + + // 2/2 multisig with custom sorting + const testgroup4: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test3, test1], + }, + }; + const expected4 = Bech32.decode( + "wasmpub1ytql0csgqgfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vujvg56k", + ).data; + expect(encodeAminoPubkey(testgroup4)).toEqual(expected4); }); }); }); From 09ce13333102d7c0efb6edb76c77841e6cf3e955 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 15:50:54 +0100 Subject: [PATCH 10/25] Replace switch with type checks --- packages/amino/src/encoding.ts | 29 ++++++++++++++--------------- packages/amino/src/index.ts | 3 +++ packages/amino/src/pubkeys.ts | 13 +++++++++++++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index 3a9969a3..a658f53e 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -2,7 +2,14 @@ import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; import { arrayContentStartsWith } from "@cosmjs/utils"; -import { isMultisigThresholdPubkey, Pubkey, pubkeyType, Secp256k1Pubkey } from "./pubkeys"; +import { + isEd25519Pubkey, + isMultisigThresholdPubkey, + isSecp256k1Pubkey, + Pubkey, + pubkeyType, + Secp256k1Pubkey, +} from "./pubkeys"; export function encodeSecp256k1Pubkey(pubkey: Uint8Array): Secp256k1Pubkey { if (pubkey.length !== 33 || (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)) { @@ -100,21 +107,13 @@ export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { out.push(...pubkeyData); } return new Uint8Array(out); + } else if (isEd25519Pubkey(pubkey)) { + return new Uint8Array([...pubkeyAminoPrefixEd25519, ...fromBase64(pubkey.value)]); + } else if (isSecp256k1Pubkey(pubkey)) { + return new Uint8Array([...pubkeyAminoPrefixSecp256k1, ...fromBase64(pubkey.value)]); + } else { + throw new Error("Unsupported pubkey type"); } - - let aminoPrefix: Uint8Array; - switch (pubkey.type) { - // Note: please don't add cases here without writing additional unit tests - case pubkeyType.secp256k1: - aminoPrefix = pubkeyAminoPrefixSecp256k1; - break; - case pubkeyType.ed25519: - aminoPrefix = pubkeyAminoPrefixEd25519; - break; - default: - throw new Error("Unsupported pubkey type"); - } - return new Uint8Array([...aminoPrefix, ...fromBase64(pubkey.value)]); } /** diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index 11c4b303..a5c0aa4e 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -8,9 +8,12 @@ export { export { MultisigThresholdPubkey, Pubkey, + Ed25519Pubkey, Secp256k1Pubkey, SinglePubkey, isMultisigThresholdPubkey, + isEd25519Pubkey, + isSecp256k1Pubkey, isSinglePubkey, pubkeyType, } from "./pubkeys"; diff --git a/packages/amino/src/pubkeys.ts b/packages/amino/src/pubkeys.ts index 215d0016..d61bfa3c 100644 --- a/packages/amino/src/pubkeys.ts +++ b/packages/amino/src/pubkeys.ts @@ -6,11 +6,24 @@ export interface Pubkey { readonly value: any; } +export interface Ed25519Pubkey extends SinglePubkey { + readonly type: "tendermint/PubKeyEd25519"; + readonly value: string; +} + +export function isEd25519Pubkey(pubkey: Pubkey): pubkey is Ed25519Pubkey { + return (pubkey as Ed25519Pubkey).type === "tendermint/PubKeyEd25519"; +} + export interface Secp256k1Pubkey extends SinglePubkey { readonly type: "tendermint/PubKeySecp256k1"; readonly value: string; } +export function isSecp256k1Pubkey(pubkey: Pubkey): pubkey is Secp256k1Pubkey { + return (pubkey as Secp256k1Pubkey).type === "tendermint/PubKeySecp256k1"; +} + export const pubkeyType = { /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */ secp256k1: "tendermint/PubKeySecp256k1" as const, From 4f5d919c2c2fc4f1fb935d3f0edad3b750adbc34 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 16:19:22 +0100 Subject: [PATCH 11/25] Create pubkeyToRawAddress and make it work for multisig accounts --- packages/amino/package.json | 1 + packages/amino/src/encoding.spec.ts | 50 ++++++++++++++++++++++++++++- packages/amino/src/encoding.ts | 25 +++++++++++++++ packages/amino/src/index.ts | 1 + packages/launchpad/src/address.ts | 28 +++------------- 5 files changed, 80 insertions(+), 25 deletions(-) diff --git a/packages/amino/package.json b/packages/amino/package.json index bc9cb37d..0df89f2b 100644 --- a/packages/amino/package.json +++ b/packages/amino/package.json @@ -40,6 +40,7 @@ "pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js" }, "dependencies": { + "@cosmjs/crypto": "^0.25.0-alpha.0", "@cosmjs/encoding": "^0.25.0-alpha.0", "@cosmjs/utils": "^0.25.0-alpha.0" }, diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 838ba881..5d915f67 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -1,4 +1,4 @@ -import { Bech32, fromBase64 } from "@cosmjs/encoding"; +import { Bech32, fromBase64, fromHex, toBase64 } from "@cosmjs/encoding"; import { decodeAminoPubkey, @@ -6,6 +6,7 @@ import { encodeAminoPubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey, + pubkeyToRawAddress, } from "./encoding"; import { MultisigThresholdPubkey, Pubkey } from "./pubkeys"; @@ -218,4 +219,51 @@ describe("encoding", () => { expect(encodeAminoPubkey(testgroup4)).toEqual(expected4); }); }); + + describe("pubkeyToRawAddress", () => { + it("works for Secp256k1", () => { + const pubkey = { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }; + expect(pubkeyToRawAddress(pubkey)).toEqual( + Bech32.decode("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r").data, + ); + }); + + it("works for Ed25519", () => { + const pubkey = { + type: "tendermint/PubKeyEd25519", + value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), + }; + expect(pubkeyToRawAddress(pubkey)).toEqual( + Bech32.decode("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz").data, + ); + }); + + it("works for multisig", () => { + const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + // pubkey data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 + // address: wasm1jq59w7y34msq69g4w3zvq6d5h3stcajd8g62xm + // address data: 9028577891aee00d15157444c069b4bc60bc764d + ); + const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + // pubkey data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 + // address: wasm146e52j6zphxw8m67cz8860ad5uju892cqmawsg + // address data: aeb3454b420dcce3ef5ec08e7d3fada725c39558 + ); + const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + // pubkey data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 + // address: wasm1a6uxr25mw8qg8zz3l2avsdjsveh4yg9sw7h5np + // address data: eeb861aa9b71c0838851fabac83650666f5220b0 + ); + + expect(pubkeyToRawAddress(test1)).toEqual(fromHex("9028577891aee00d15157444c069b4bc60bc764d")); + expect(pubkeyToRawAddress(test2)).toEqual(fromHex("aeb3454b420dcce3ef5ec08e7d3fada725c39558")); + expect(pubkeyToRawAddress(test3)).toEqual(fromHex("eeb861aa9b71c0838851fabac83650666f5220b0")); + }); + }); }); diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index a658f53e..571d516b 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -1,3 +1,4 @@ +import { ripemd160, sha256 } from "@cosmjs/crypto"; import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; import { arrayContentStartsWith } from "@cosmjs/utils"; @@ -116,6 +117,30 @@ export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { } } +// See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography +// For secp256k1 this assumes we already have a compressed pubkey. +export function pubkeyToRawAddress(pubkey: Pubkey): Uint8Array { + if (isSecp256k1Pubkey(pubkey)) { + const pubkeyData = fromBase64(pubkey.value); + if (pubkeyData.length !== 33) { + throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyData.length}`); + } + return ripemd160(sha256(pubkeyData)).slice(0, 20); + } 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); + } else if (isMultisigThresholdPubkey(pubkey)) { + // https://github.com/tendermint/tendermint/blob/38b401657e4ad7a7eeb3c30a3cbf512037df3740/crypto/multisig/threshold_pubkey.go#L71-L74 + const pubkeyData = encodeAminoPubkey(pubkey); + return sha256(pubkeyData).slice(0, 20); + } else { + throw new Error("Unsupported public key type"); + } +} + /** * Encodes a public key to binary Amino and then to bech32. * diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index a5c0aa4e..803dbe19 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -4,6 +4,7 @@ export { encodeAminoPubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey, + pubkeyToRawAddress, } from "./encoding"; export { MultisigThresholdPubkey, diff --git a/packages/launchpad/src/address.ts b/packages/launchpad/src/address.ts index adfc43a0..d31e6d52 100644 --- a/packages/launchpad/src/address.ts +++ b/packages/launchpad/src/address.ts @@ -1,6 +1,7 @@ -import { pubkeyType, SinglePubkey } from "@cosmjs/amino"; +import { SinglePubkey } from "@cosmjs/amino"; +import { pubkeyToRawAddress } from "@cosmjs/amino/build/encoding"; import { ripemd160, sha256 } from "@cosmjs/crypto"; -import { Bech32, fromBase64 } from "@cosmjs/encoding"; +import { Bech32 } from "@cosmjs/encoding"; export function rawSecp256k1PubkeyToAddress(pubkeyRaw: Uint8Array, prefix: string): string { if (pubkeyRaw.length !== 33) { @@ -14,26 +15,5 @@ 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: SinglePubkey, prefix: string): string { - const pubkeyBytes = fromBase64(pubkey.value); - switch (pubkey.type) { - case pubkeyType.secp256k1: { - return rawSecp256k1PubkeyToAddress(pubkeyBytes, prefix); - } - case pubkeyType.ed25519: { - if (pubkeyBytes.length !== 32) { - throw new Error(`Invalid Ed25519 pubkey length: ${pubkeyBytes.length}`); - } - const hash = sha256(pubkeyBytes); - return Bech32.encode(prefix, hash.slice(0, 20)); - } - case pubkeyType.sr25519: { - if (pubkeyBytes.length !== 32) { - throw new Error(`Invalid Sr25519 pubkey length: ${pubkeyBytes.length}`); - } - const hash = sha256(pubkeyBytes); - return Bech32.encode(prefix, hash.slice(0, 20)); - } - default: - throw new Error("Unrecognized public key algorithm"); - } + return Bech32.encode(prefix, pubkeyToRawAddress(pubkey)); } From 3fb79b3dd7c027e87a8ab39ad91b05b1ce07e9ff Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 16:29:53 +0100 Subject: [PATCH 12/25] Add rawSecp256k1PubkeyToRawAddress to @cosmjs/amino --- packages/amino/src/encoding.ts | 12 ++++++++---- packages/amino/src/index.ts | 1 + packages/launchpad/src/address.ts | 10 ++-------- packages/launchpad/src/secp256k1hdwallet.ts | 6 +++--- packages/launchpad/src/secp256k1wallet.ts | 5 +++-- .../proto-signing/src/directsecp256k1hdwallet.ts | 6 ++++-- packages/proto-signing/src/directsecp256k1wallet.ts | 6 ++++-- 7 files changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index 571d516b..78ccb34d 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -117,15 +117,19 @@ export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { } } +export function rawSecp256k1PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Array { + if (pubkeyData.length !== 33) { + throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyData.length}`); + } + return ripemd160(sha256(pubkeyData)); +} + // See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography // For secp256k1 this assumes we already have a compressed pubkey. export function pubkeyToRawAddress(pubkey: Pubkey): Uint8Array { if (isSecp256k1Pubkey(pubkey)) { const pubkeyData = fromBase64(pubkey.value); - if (pubkeyData.length !== 33) { - throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyData.length}`); - } - return ripemd160(sha256(pubkeyData)).slice(0, 20); + return rawSecp256k1PubkeyToRawAddress(pubkeyData); } else if (isEd25519Pubkey(pubkey)) { const pubkeyData = fromBase64(pubkey.value); if (pubkeyData.length !== 32) { diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index 803dbe19..3cfc656d 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -5,6 +5,7 @@ export { encodeBech32Pubkey, encodeSecp256k1Pubkey, pubkeyToRawAddress, + rawSecp256k1PubkeyToRawAddress, } from "./encoding"; export { MultisigThresholdPubkey, diff --git a/packages/launchpad/src/address.ts b/packages/launchpad/src/address.ts index d31e6d52..8c8e3d17 100644 --- a/packages/launchpad/src/address.ts +++ b/packages/launchpad/src/address.ts @@ -1,15 +1,9 @@ import { SinglePubkey } from "@cosmjs/amino"; -import { pubkeyToRawAddress } from "@cosmjs/amino/build/encoding"; -import { ripemd160, sha256 } from "@cosmjs/crypto"; +import { pubkeyToRawAddress, rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino/build/encoding"; import { Bech32 } from "@cosmjs/encoding"; export function rawSecp256k1PubkeyToAddress(pubkeyRaw: Uint8Array, prefix: string): string { - if (pubkeyRaw.length !== 33) { - throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyRaw.length}`); - } - const hash1 = sha256(pubkeyRaw); - const hash2 = ripemd160(hash1); - return Bech32.encode(prefix, hash2); + return Bech32.encode(prefix, rawSecp256k1PubkeyToRawAddress(pubkeyRaw)); } // See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography diff --git a/packages/launchpad/src/secp256k1hdwallet.ts b/packages/launchpad/src/secp256k1hdwallet.ts index cea4e63b..fa7d8b9c 100644 --- a/packages/launchpad/src/secp256k1hdwallet.ts +++ b/packages/launchpad/src/secp256k1hdwallet.ts @@ -1,3 +1,4 @@ +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Bip39, EnglishMnemonic, @@ -10,10 +11,9 @@ import { Slip10Curve, stringToPath, } from "@cosmjs/crypto"; -import { fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding"; +import { Bech32, fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding"; import { assert, isNonNullObject } from "@cosmjs/utils"; -import { rawSecp256k1PubkeyToAddress } from "./address"; import { serializeSignDoc, StdSignDoc } from "./encoding"; import { makeCosmoshubPath } from "./paths"; import { encodeSecp256k1Signature } from "./signature"; @@ -246,7 +246,7 @@ export class Secp256k1HdWallet implements OfflineSigner { } private get address(): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, this.accounts[0].prefix); + return Bech32.encode(this.accounts[0].prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); } public async getAccounts(): Promise { diff --git a/packages/launchpad/src/secp256k1wallet.ts b/packages/launchpad/src/secp256k1wallet.ts index b8e84960..94552d84 100644 --- a/packages/launchpad/src/secp256k1wallet.ts +++ b/packages/launchpad/src/secp256k1wallet.ts @@ -1,6 +1,7 @@ +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Secp256k1, Sha256 } from "@cosmjs/crypto"; +import { Bech32 } from "@cosmjs/encoding"; -import { rawSecp256k1PubkeyToAddress } from "./address"; import { serializeSignDoc, StdSignDoc } from "./encoding"; import { encodeSecp256k1Signature } from "./signature"; import { AccountData, AminoSignResponse, OfflineSigner } from "./signer"; @@ -33,7 +34,7 @@ export class Secp256k1Wallet implements OfflineSigner { } private get address(): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, this.prefix); + return Bech32.encode(this.prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); } public async getAccounts(): Promise { diff --git a/packages/proto-signing/src/directsecp256k1hdwallet.ts b/packages/proto-signing/src/directsecp256k1hdwallet.ts index bb6058d7..cfdbc8c4 100644 --- a/packages/proto-signing/src/directsecp256k1hdwallet.ts +++ b/packages/proto-signing/src/directsecp256k1hdwallet.ts @@ -1,3 +1,4 @@ +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Bip39, EnglishMnemonic, @@ -8,7 +9,8 @@ import { Slip10, Slip10Curve, } from "@cosmjs/crypto"; -import { encodeSecp256k1Signature, rawSecp256k1PubkeyToAddress } from "@cosmjs/launchpad"; +import { Bech32 } from "@cosmjs/encoding"; +import { encodeSecp256k1Signature } from "@cosmjs/launchpad"; import { SignDoc } from "./codec/cosmos/tx/v1beta1/tx"; import { makeCosmoshubPath } from "./paths"; @@ -99,7 +101,7 @@ export class DirectSecp256k1HdWallet implements OfflineDirectSigner { } private get address(): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, this.accounts[0].prefix); + return Bech32.encode(this.accounts[0].prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); } public async getAccounts(): Promise { diff --git a/packages/proto-signing/src/directsecp256k1wallet.ts b/packages/proto-signing/src/directsecp256k1wallet.ts index 28133f10..9207d9c6 100644 --- a/packages/proto-signing/src/directsecp256k1wallet.ts +++ b/packages/proto-signing/src/directsecp256k1wallet.ts @@ -1,5 +1,7 @@ +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Secp256k1, sha256 } from "@cosmjs/crypto"; -import { encodeSecp256k1Signature, rawSecp256k1PubkeyToAddress } from "@cosmjs/launchpad"; +import { Bech32 } from "@cosmjs/encoding"; +import { encodeSecp256k1Signature } from "@cosmjs/launchpad"; import { SignDoc } from "./codec/cosmos/tx/v1beta1/tx"; import { AccountData, DirectSignResponse, OfflineDirectSigner } from "./signer"; @@ -33,7 +35,7 @@ export class DirectSecp256k1Wallet implements OfflineDirectSigner { } private get address(): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, this.prefix); + return Bech32.encode(this.prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); } public async getAccounts(): Promise { From 18fcd841a4eb1c8af6db96ae4b5362e875dcb68f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 16:37:44 +0100 Subject: [PATCH 13/25] Let multisig test test multisig --- packages/amino/src/encoding.spec.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 5d915f67..171c7913 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -244,26 +244,22 @@ describe("encoding", () => { it("works for multisig", () => { const test1 = decodeBech32Pubkey( "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", - // pubkey data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 - // address: wasm1jq59w7y34msq69g4w3zvq6d5h3stcajd8g62xm - // address data: 9028577891aee00d15157444c069b4bc60bc764d ); const test2 = decodeBech32Pubkey( "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", - // pubkey data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 - // address: wasm146e52j6zphxw8m67cz8860ad5uju892cqmawsg - // address data: aeb3454b420dcce3ef5ec08e7d3fada725c39558 ); const test3 = decodeBech32Pubkey( "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", - // pubkey data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 - // address: wasm1a6uxr25mw8qg8zz3l2avsdjsveh4yg9sw7h5np - // address data: eeb861aa9b71c0838851fabac83650666f5220b0 ); - expect(pubkeyToRawAddress(test1)).toEqual(fromHex("9028577891aee00d15157444c069b4bc60bc764d")); - expect(pubkeyToRawAddress(test2)).toEqual(fromHex("aeb3454b420dcce3ef5ec08e7d3fada725c39558")); - expect(pubkeyToRawAddress(test3)).toEqual(fromHex("eeb861aa9b71c0838851fabac83650666f5220b0")); + const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, + }; + expect(pubkeyToRawAddress(testgroup1)).toEqual(fromHex("0892a77fab2fa7e192c3b7b2741e6682f3abb72f")); }); }); }); From cb37451f8f5fd7bc81758eb9695e4f68b833a83e Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 16:46:10 +0100 Subject: [PATCH 14/25] Add multisig address derivation example --- packages/cli/examples/multisig_address.ts | 31 +++++++++++++++++++++++ packages/cli/package.json | 1 + packages/cli/run_examples.sh | 1 + 3 files changed, 33 insertions(+) create mode 100644 packages/cli/examples/multisig_address.ts diff --git a/packages/cli/examples/multisig_address.ts b/packages/cli/examples/multisig_address.ts new file mode 100644 index 00000000..8e754d2e --- /dev/null +++ b/packages/cli/examples/multisig_address.ts @@ -0,0 +1,31 @@ +import { MultisigThresholdPubkey, pubkeyToRawAddress } from "@cosmjs/amino"; +import { Bech32 } from "@cosmjs/encoding"; + +// https://github.com/cosmos/cosmjs/issues/673#issuecomment-779847238 +const multisigPubkey: MultisigThresholdPubkey = { + "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" + } + ] + } +}; + +const address = Bech32.encode("cosmos", pubkeyToRawAddress(multisigPubkey)); +console.log(address); diff --git a/packages/cli/package.json b/packages/cli/package.json index 5949e477..7405f013 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -39,6 +39,7 @@ "!**/testdata/" ], "dependencies": { + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/cosmwasm-launchpad": "^0.25.0-alpha.0", "@cosmjs/cosmwasm-stargate": "^0.25.0-alpha.0", "@cosmjs/crypto": "^0.25.0-alpha.0", diff --git a/packages/cli/run_examples.sh b/packages/cli/run_examples.sh index fde36611..8ec0e10e 100755 --- a/packages/cli/run_examples.sh +++ b/packages/cli/run_examples.sh @@ -13,6 +13,7 @@ cosmwasm-cli --init examples/generate_address.ts --code "process.exit(0)" cosmwasm-cli --init examples/helpers.ts --code "process.exit(0)" cosmwasm-cli --init examples/local_faucet.ts --code "process.exit(0)" cosmwasm-cli --init examples/mask.ts --code "process.exit(0)" +cosmwasm-cli --init examples/multisig_address.ts --code "process.exit(0)" if [ -n "${SIMAPP_ENABLED:-}" ]; then cosmwasm-cli --init examples/stargate.ts --code "process.exit(0)" fi From 843b82badd184a7bbd3b3f33096277bb58335d49 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 16:55:06 +0100 Subject: [PATCH 15/25] Create pubkeyToAddress in @cosmjs/amino --- packages/amino/src/encoding.spec.ts | 42 +++++++++++++++++++++++ packages/amino/src/encoding.ts | 4 +++ packages/amino/src/index.ts | 1 + packages/cli/examples/multisig_address.ts | 5 ++- packages/launchpad/src/address.spec.ts | 25 -------------- packages/launchpad/src/address.ts | 9 +---- packages/launchpad/src/index.ts | 3 +- 7 files changed, 52 insertions(+), 37 deletions(-) delete mode 100644 packages/launchpad/src/address.spec.ts diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 171c7913..1389aea1 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -6,6 +6,7 @@ import { encodeAminoPubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey, + pubkeyToAddress, pubkeyToRawAddress, } from "./encoding"; import { MultisigThresholdPubkey, Pubkey } from "./pubkeys"; @@ -262,4 +263,45 @@ describe("encoding", () => { expect(pubkeyToRawAddress(testgroup1)).toEqual(fromHex("0892a77fab2fa7e192c3b7b2741e6682f3abb72f")); }); }); + + describe("pubkeyToAddress", () => { + it("works for Secp256k1", () => { + const prefix = "cosmos"; + const pubkey = { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }; + expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r"); + }); + + it("works for Ed25519", () => { + const prefix = "cosmos"; + const pubkey = { + type: "tendermint/PubKeyEd25519", + value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), + }; + expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz"); + }); + + it("works for multisig", () => { + const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + ); + const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + ); + const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + ); + + const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, + }; + expect(pubkeyToAddress(testgroup1, "wasm")).toEqual("wasm1pzf2wlat97n7rykrk7e8g8nxste6hde0r8jqsy"); + }); + }); }); diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index 78ccb34d..be04dfec 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -145,6 +145,10 @@ export function pubkeyToRawAddress(pubkey: Pubkey): Uint8Array { } } +export function pubkeyToAddress(pubkey: Pubkey, prefix: string): string { + return Bech32.encode(prefix, pubkeyToRawAddress(pubkey)); +} + /** * Encodes a public key to binary Amino and then to bech32. * diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index 3cfc656d..1508bc1e 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -4,6 +4,7 @@ export { encodeAminoPubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey, + pubkeyToAddress, pubkeyToRawAddress, rawSecp256k1PubkeyToRawAddress, } from "./encoding"; diff --git a/packages/cli/examples/multisig_address.ts b/packages/cli/examples/multisig_address.ts index 8e754d2e..94c37c1a 100644 --- a/packages/cli/examples/multisig_address.ts +++ b/packages/cli/examples/multisig_address.ts @@ -1,5 +1,4 @@ -import { MultisigThresholdPubkey, pubkeyToRawAddress } from "@cosmjs/amino"; -import { Bech32 } from "@cosmjs/encoding"; +import { MultisigThresholdPubkey, pubkeyToAddress } from "@cosmjs/amino"; // https://github.com/cosmos/cosmjs/issues/673#issuecomment-779847238 const multisigPubkey: MultisigThresholdPubkey = { @@ -27,5 +26,5 @@ const multisigPubkey: MultisigThresholdPubkey = { } }; -const address = Bech32.encode("cosmos", pubkeyToRawAddress(multisigPubkey)); +const address = pubkeyToAddress(multisigPubkey, "cosmos"); console.log(address); diff --git a/packages/launchpad/src/address.spec.ts b/packages/launchpad/src/address.spec.ts deleted file mode 100644 index f5e3b627..00000000 --- a/packages/launchpad/src/address.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { fromHex, toBase64 } from "@cosmjs/encoding"; - -import { pubkeyToAddress } from "./address"; - -describe("address", () => { - describe("pubkeyToAddress", () => { - it("works for Secp256k1 compressed", () => { - const prefix = "cosmos"; - const pubkey = { - type: "tendermint/PubKeySecp256k1", - value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", - }; - expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r"); - }); - - it("works for Ed25519", () => { - const prefix = "cosmos"; - const pubkey = { - type: "tendermint/PubKeyEd25519", - value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), - }; - expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz"); - }); - }); -}); diff --git a/packages/launchpad/src/address.ts b/packages/launchpad/src/address.ts index 8c8e3d17..97af9b74 100644 --- a/packages/launchpad/src/address.ts +++ b/packages/launchpad/src/address.ts @@ -1,13 +1,6 @@ -import { SinglePubkey } from "@cosmjs/amino"; -import { pubkeyToRawAddress, rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino/build/encoding"; +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Bech32 } from "@cosmjs/encoding"; export function rawSecp256k1PubkeyToAddress(pubkeyRaw: Uint8Array, prefix: string): string { return Bech32.encode(prefix, rawSecp256k1PubkeyToRawAddress(pubkeyRaw)); } - -// 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: SinglePubkey, prefix: string): string { - return Bech32.encode(prefix, pubkeyToRawAddress(pubkey)); -} diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index ea1120b5..e5db48ee 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -5,6 +5,7 @@ export { encodeAminoPubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey, + pubkeyToAddress, pubkeyType, SinglePubkey as PubKey, } from "@cosmjs/amino"; @@ -12,7 +13,7 @@ export { import * as logs from "./logs"; export { logs }; -export { pubkeyToAddress, rawSecp256k1PubkeyToAddress } from "./address"; +export { rawSecp256k1PubkeyToAddress } from "./address"; export { Coin, coin, coins, parseCoins } from "./coins"; export { From 42e2fdb32c3e32eb190d3109159e986fbbb610ba Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 16:57:50 +0100 Subject: [PATCH 16/25] Remove rawSecp256k1PubkeyToAddress --- CHANGELOG.md | 3 +++ packages/launchpad/src/address.ts | 6 ------ packages/launchpad/src/index.ts | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) delete mode 100644 packages/launchpad/src/address.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 660720ec..da09a6b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,9 @@ and this project adheres to `blockIdFlag` is `BlockIdFlag.Absent`. The decoding into `CommitSignature` is only updated for the class `Tendermint34Client`, not for `Client`. Please migrate to the former. +- @cosmjs/launchpad: `rawSecp256k1PubkeyToAddress` was removed. Instead use + `Bech32.encode(prefix, rawSecp256k1PubkeyToRawAddress(pubkeyRaw))` with + `rawSecp256k1PubkeyToRawAddress` from @cosmjs/amino. ### Deprecated diff --git a/packages/launchpad/src/address.ts b/packages/launchpad/src/address.ts deleted file mode 100644 index 97af9b74..00000000 --- a/packages/launchpad/src/address.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; -import { Bech32 } from "@cosmjs/encoding"; - -export function rawSecp256k1PubkeyToAddress(pubkeyRaw: Uint8Array, prefix: string): string { - return Bech32.encode(prefix, rawSecp256k1PubkeyToRawAddress(pubkeyRaw)); -} diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index e5db48ee..11da4d29 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -13,7 +13,6 @@ export { import * as logs from "./logs"; export { logs }; -export { rawSecp256k1PubkeyToAddress } from "./address"; export { Coin, coin, coins, parseCoins } from "./coins"; export { From 1a533fc24d55ed98c64acca450e6aed071b4e287 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 18:02:54 +0100 Subject: [PATCH 17/25] Create addresses module --- packages/amino/src/addresses.spec.ts | 91 ++++++++++++++++++++++++++++ packages/amino/src/addresses.ts | 38 ++++++++++++ packages/amino/src/encoding.spec.ts | 88 +-------------------------- packages/amino/src/encoding.ts | 33 ---------- packages/amino/src/index.ts | 4 +- 5 files changed, 131 insertions(+), 123 deletions(-) create mode 100644 packages/amino/src/addresses.spec.ts create mode 100644 packages/amino/src/addresses.ts diff --git a/packages/amino/src/addresses.spec.ts b/packages/amino/src/addresses.spec.ts new file mode 100644 index 00000000..de90b7b9 --- /dev/null +++ b/packages/amino/src/addresses.spec.ts @@ -0,0 +1,91 @@ +import { Bech32, fromHex, toBase64 } from "@cosmjs/encoding"; + +import { pubkeyToAddress, pubkeyToRawAddress } from "./addresses"; +import { decodeBech32Pubkey } from "./encoding"; +import { MultisigThresholdPubkey } from "./pubkeys"; + +describe("addresses", () => { + describe("pubkeyToRawAddress", () => { + it("works for Secp256k1", () => { + const pubkey = { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }; + expect(pubkeyToRawAddress(pubkey)).toEqual( + Bech32.decode("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r").data, + ); + }); + + it("works for Ed25519", () => { + const pubkey = { + type: "tendermint/PubKeyEd25519", + value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), + }; + expect(pubkeyToRawAddress(pubkey)).toEqual( + Bech32.decode("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz").data, + ); + }); + + it("works for multisig", () => { + const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + ); + const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + ); + const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + ); + + const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, + }; + expect(pubkeyToRawAddress(testgroup1)).toEqual(fromHex("0892a77fab2fa7e192c3b7b2741e6682f3abb72f")); + }); + }); + + describe("pubkeyToAddress", () => { + it("works for Secp256k1", () => { + const prefix = "cosmos"; + const pubkey = { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }; + expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r"); + }); + + it("works for Ed25519", () => { + const prefix = "cosmos"; + const pubkey = { + type: "tendermint/PubKeyEd25519", + value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), + }; + expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz"); + }); + + it("works for multisig", () => { + const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + ); + const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + ); + const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + ); + + const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, + }; + expect(pubkeyToAddress(testgroup1, "wasm")).toEqual("wasm1pzf2wlat97n7rykrk7e8g8nxste6hde0r8jqsy"); + }); + }); +}); diff --git a/packages/amino/src/addresses.ts b/packages/amino/src/addresses.ts new file mode 100644 index 00000000..133f8f8b --- /dev/null +++ b/packages/amino/src/addresses.ts @@ -0,0 +1,38 @@ +// See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography + +import { ripemd160, sha256 } from "@cosmjs/crypto"; +import { Bech32, fromBase64 } from "@cosmjs/encoding"; + +import { encodeAminoPubkey } from "./encoding"; +import { isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, Pubkey } from "./pubkeys"; + +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(pubkey: Pubkey): Uint8Array { + if (isSecp256k1Pubkey(pubkey)) { + const pubkeyData = fromBase64(pubkey.value); + 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); + } else if (isMultisigThresholdPubkey(pubkey)) { + // https://github.com/tendermint/tendermint/blob/38b401657e4ad7a7eeb3c30a3cbf512037df3740/crypto/multisig/threshold_pubkey.go#L71-L74 + const pubkeyData = encodeAminoPubkey(pubkey); + return sha256(pubkeyData).slice(0, 20); + } else { + throw new Error("Unsupported public key type"); + } +} + +export function pubkeyToAddress(pubkey: Pubkey, prefix: string): string { + return Bech32.encode(prefix, pubkeyToRawAddress(pubkey)); +} diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 1389aea1..838ba881 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -1,4 +1,4 @@ -import { Bech32, fromBase64, fromHex, toBase64 } from "@cosmjs/encoding"; +import { Bech32, fromBase64 } from "@cosmjs/encoding"; import { decodeAminoPubkey, @@ -6,8 +6,6 @@ import { encodeAminoPubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey, - pubkeyToAddress, - pubkeyToRawAddress, } from "./encoding"; import { MultisigThresholdPubkey, Pubkey } from "./pubkeys"; @@ -220,88 +218,4 @@ describe("encoding", () => { expect(encodeAminoPubkey(testgroup4)).toEqual(expected4); }); }); - - describe("pubkeyToRawAddress", () => { - it("works for Secp256k1", () => { - const pubkey = { - type: "tendermint/PubKeySecp256k1", - value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", - }; - expect(pubkeyToRawAddress(pubkey)).toEqual( - Bech32.decode("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r").data, - ); - }); - - it("works for Ed25519", () => { - const pubkey = { - type: "tendermint/PubKeyEd25519", - value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), - }; - expect(pubkeyToRawAddress(pubkey)).toEqual( - Bech32.decode("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz").data, - ); - }); - - it("works for multisig", () => { - const test1 = decodeBech32Pubkey( - "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", - ); - const test2 = decodeBech32Pubkey( - "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", - ); - const test3 = decodeBech32Pubkey( - "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", - ); - - const testgroup1: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "2", - pubkeys: [test1, test2, test3], - }, - }; - expect(pubkeyToRawAddress(testgroup1)).toEqual(fromHex("0892a77fab2fa7e192c3b7b2741e6682f3abb72f")); - }); - }); - - describe("pubkeyToAddress", () => { - it("works for Secp256k1", () => { - const prefix = "cosmos"; - const pubkey = { - type: "tendermint/PubKeySecp256k1", - value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", - }; - expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r"); - }); - - it("works for Ed25519", () => { - const prefix = "cosmos"; - const pubkey = { - type: "tendermint/PubKeyEd25519", - value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), - }; - expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz"); - }); - - it("works for multisig", () => { - const test1 = decodeBech32Pubkey( - "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", - ); - const test2 = decodeBech32Pubkey( - "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", - ); - const test3 = decodeBech32Pubkey( - "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", - ); - - const testgroup1: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "2", - pubkeys: [test1, test2, test3], - }, - }; - expect(pubkeyToAddress(testgroup1, "wasm")).toEqual("wasm1pzf2wlat97n7rykrk7e8g8nxste6hde0r8jqsy"); - }); - }); }); diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index be04dfec..a658f53e 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -1,4 +1,3 @@ -import { ripemd160, sha256 } from "@cosmjs/crypto"; import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; import { arrayContentStartsWith } from "@cosmjs/utils"; @@ -117,38 +116,6 @@ export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { } } -export function rawSecp256k1PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Array { - if (pubkeyData.length !== 33) { - throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyData.length}`); - } - return ripemd160(sha256(pubkeyData)); -} - -// See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography -// 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 (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); - } else if (isMultisigThresholdPubkey(pubkey)) { - // https://github.com/tendermint/tendermint/blob/38b401657e4ad7a7eeb3c30a3cbf512037df3740/crypto/multisig/threshold_pubkey.go#L71-L74 - const pubkeyData = encodeAminoPubkey(pubkey); - return sha256(pubkeyData).slice(0, 20); - } else { - throw new Error("Unsupported public key type"); - } -} - -export function pubkeyToAddress(pubkey: Pubkey, prefix: string): string { - return Bech32.encode(prefix, pubkeyToRawAddress(pubkey)); -} - /** * Encodes a public key to binary Amino and then to bech32. * diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index 1508bc1e..cb2ae3bf 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -1,12 +1,10 @@ +export { pubkeyToAddress, pubkeyToRawAddress, rawSecp256k1PubkeyToRawAddress } from "./addresses"; export { decodeAminoPubkey, decodeBech32Pubkey, encodeAminoPubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey, - pubkeyToAddress, - pubkeyToRawAddress, - rawSecp256k1PubkeyToRawAddress, } from "./encoding"; export { MultisigThresholdPubkey, From 7255af418621e6304661a47c5c8cfbd3ad612442 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 18:40:19 +0100 Subject: [PATCH 18/25] Create createMultisigThresholdPubkey --- packages/amino/src/encoding.spec.ts | 88 +++++----------------------- packages/amino/src/index.ts | 1 + packages/amino/src/multisig.spec.ts | 63 ++++++++++++++++++++ packages/amino/src/multisig.ts | 38 ++++++++++++ packages/amino/src/testutils.spec.ts | 73 +++++++++++++++++++++++ 5 files changed, 190 insertions(+), 73 deletions(-) create mode 100644 packages/amino/src/multisig.spec.ts create mode 100644 packages/amino/src/multisig.ts create mode 100644 packages/amino/src/testutils.spec.ts diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 838ba881..ad98e97d 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -7,7 +7,17 @@ import { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; -import { MultisigThresholdPubkey, Pubkey } from "./pubkeys"; +import { Pubkey } from "./pubkeys"; +import { + testgroup1, + testgroup1Address, + testgroup2, + testgroup2Address, + testgroup3, + testgroup3Address, + testgroup4, + testgroup4Address, +} from "./testutils.spec"; describe("encoding", () => { describe("encodeSecp256k1Pubkey", () => { @@ -137,84 +147,16 @@ describe("encoding", () => { }); it("works for multisig", () => { - // ./build/wasmd keys add test1 - // ./build/wasmd keys add test2 - // ./build/wasmd keys add test3 - // ./build/wasmd keys add testgroup1 --multisig=test1,test2,test3 --multisig-threshold 2 - // ./build/wasmd keys add testgroup2 --multisig=test1,test2,test3 --multisig-threshold 1 - // # By default pubkeys are sorted by its address data (https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/client/keys/add.go#L172-L174) - // ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 - // ./build/wasmd keys add testgroup4 --multisig=test3,test1 --nosort --multisig-threshold 2 - - const test1 = decodeBech32Pubkey( - "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", - // pubkey data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 - // address: wasm1jq59w7y34msq69g4w3zvq6d5h3stcajd8g62xm - // address data: 9028577891aee00d15157444c069b4bc60bc764d - ); - const test2 = decodeBech32Pubkey( - "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", - // pubkey data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 - // address: wasm146e52j6zphxw8m67cz8860ad5uju892cqmawsg - // address data: aeb3454b420dcce3ef5ec08e7d3fada725c39558 - ); - const test3 = decodeBech32Pubkey( - "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", - // pubkey data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 - // address: wasm1a6uxr25mw8qg8zz3l2avsdjsveh4yg9sw7h5np - // address data: eeb861aa9b71c0838851fabac83650666f5220b0 - ); - - // 2/3 multisig - const testgroup1: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "2", - pubkeys: [test1, test2, test3], - }, - }; - const expected1 = Bech32.decode( - "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7v7aysdd", - ).data; + const expected1 = Bech32.decode(testgroup1Address).data; expect(encodeAminoPubkey(testgroup1)).toEqual(expected1); - // 1/3 multisig - const testgroup2: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "1", - pubkeys: [test1, test2, test3], - }, - }; - const expected2 = Bech32.decode( - "wasmpub1ytql0csgqyfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vc4ejke", - ).data; + const expected2 = Bech32.decode(testgroup2Address).data; expect(encodeAminoPubkey(testgroup2)).toEqual(expected2); - // 2/2 multisig - const testgroup3: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "2", - pubkeys: [test1, test3], - }, - }; - const expected3 = Bech32.decode( - "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vzjhugu", - ).data; + const expected3 = Bech32.decode(testgroup3Address).data; expect(encodeAminoPubkey(testgroup3)).toEqual(expected3); - // 2/2 multisig with custom sorting - const testgroup4: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "2", - pubkeys: [test3, test1], - }, - }; - const expected4 = Bech32.decode( - "wasmpub1ytql0csgqgfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vujvg56k", - ).data; + const expected4 = Bech32.decode(testgroup4Address).data; expect(encodeAminoPubkey(testgroup4)).toEqual(expected4); }); }); diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index cb2ae3bf..28b4d9e5 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -18,3 +18,4 @@ export { isSinglePubkey, pubkeyType, } from "./pubkeys"; +export { createMultisigThresholdPubkey } from "./multisig"; diff --git a/packages/amino/src/multisig.spec.ts b/packages/amino/src/multisig.spec.ts new file mode 100644 index 00000000..ce239345 --- /dev/null +++ b/packages/amino/src/multisig.spec.ts @@ -0,0 +1,63 @@ +import { compareArrays, createMultisigThresholdPubkey } from "./multisig"; +import { test1, test2, test3, testgroup1, testgroup2, testgroup3, testgroup4 } from "./testutils.spec"; + +describe("multisig", () => { + describe("compareArrays", () => { + it("return 0 for equal arrays", () => { + expect(compareArrays(new Uint8Array([]), new Uint8Array([]))).toEqual(0); + expect(compareArrays(new Uint8Array([1]), new Uint8Array([1]))).toEqual(0); + expect(compareArrays(new Uint8Array([3, 2, 1]), new Uint8Array([3, 2, 1]))).toEqual(0); + }); + + it("return > 0 for left > right", () => { + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 5, 4]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 4, 5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([4, 5, 5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([]))).toBeGreaterThan(0); + }); + + it("return < 0 for left < right", () => { + expect(compareArrays(new Uint8Array([5, 5, 4]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([5, 4, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([4, 5, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([5, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + }); + + it("can be used with sort", () => { + const values = [ + new Uint8Array([2]), + new Uint8Array([1]), + new Uint8Array([2, 5]), + new Uint8Array([3]), + new Uint8Array([]), + ].sort(compareArrays); + expect(values).toEqual([ + new Uint8Array([]), + new Uint8Array([1]), + new Uint8Array([2]), + new Uint8Array([2, 5]), + new Uint8Array([3]), + ]); + }); + }); + + describe("MultisigThresholdPubkey", () => { + it("works with sorting", () => { + expect(createMultisigThresholdPubkey([test1, test2, test3], 2)).toEqual(testgroup1); + expect(createMultisigThresholdPubkey([test1, test2, test3], 1)).toEqual(testgroup2); + expect(createMultisigThresholdPubkey([test3, test1], 2)).toEqual(testgroup3); + + expect(createMultisigThresholdPubkey([test1, test2, test3], 2, false)).toEqual(testgroup1); + expect(createMultisigThresholdPubkey([test1, test2, test3], 1, false)).toEqual(testgroup2); + expect(createMultisigThresholdPubkey([test3, test1], 2, false)).toEqual(testgroup3); + }); + + it("works with nosort", () => { + expect(createMultisigThresholdPubkey([test3, test1], 2, true)).toEqual(testgroup4); + }); + }); +}); diff --git a/packages/amino/src/multisig.ts b/packages/amino/src/multisig.ts new file mode 100644 index 00000000..51203f87 --- /dev/null +++ b/packages/amino/src/multisig.ts @@ -0,0 +1,38 @@ +import { toHex } from "@cosmjs/encoding"; +import { Uint53 } from "@cosmjs/math"; + +import { pubkeyToRawAddress } from "./addresses"; +import { MultisigThresholdPubkey, SinglePubkey } from "./pubkeys"; + +/** + * Compare arrays lexicographically. + * + * Returns value < 0 if `a < b`. + * Returns value > 0 if `a > b`. + * Returns 0 if `a === b`. + */ +export function compareArrays(a: Uint8Array, b: Uint8Array): number { + return toHex(a).localeCompare(toHex(b)); +} + +export function createMultisigThresholdPubkey( + pubkeys: readonly SinglePubkey[], + threshold: number, + nosort = false, +): MultisigThresholdPubkey { + const outPubkeys = nosort + ? pubkeys + : Array.from(pubkeys).sort((lhs, rhs) => { + // https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/client/keys/add.go#L172-L174 + const addressLhs = pubkeyToRawAddress(lhs); + const addressRhs = pubkeyToRawAddress(rhs); + return compareArrays(addressLhs, addressRhs); + }); + return { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: new Uint53(threshold).toString(), + pubkeys: outPubkeys, + }, + }; +} diff --git a/packages/amino/src/testutils.spec.ts b/packages/amino/src/testutils.spec.ts new file mode 100644 index 00000000..36cbc26d --- /dev/null +++ b/packages/amino/src/testutils.spec.ts @@ -0,0 +1,73 @@ +import { decodeBech32Pubkey } from "./encoding"; +import { MultisigThresholdPubkey } from "./pubkeys"; + +// ./build/wasmd keys add test1 +// ./build/wasmd keys add test2 +// ./build/wasmd keys add test3 +// ./build/wasmd keys add testgroup1 --multisig=test1,test2,test3 --multisig-threshold 2 +// ./build/wasmd keys add testgroup2 --multisig=test1,test2,test3 --multisig-threshold 1 +// # By default pubkeys are sorted by its address data (https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/client/keys/add.go#L172-L174) +// ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 +// ./build/wasmd keys add testgroup4 --multisig=test3,test1 --nosort --multisig-threshold 2 + +export const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + // pubkey data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 + // address: wasm1jq59w7y34msq69g4w3zvq6d5h3stcajd8g62xm + // address data: 9028577891aee00d15157444c069b4bc60bc764d +); +export const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + // pubkey data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 + // address: wasm146e52j6zphxw8m67cz8860ad5uju892cqmawsg + // address data: aeb3454b420dcce3ef5ec08e7d3fada725c39558 +); +export const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + // pubkey data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 + // address: wasm1a6uxr25mw8qg8zz3l2avsdjsveh4yg9sw7h5np + // address data: eeb861aa9b71c0838851fabac83650666f5220b0 +); + +// 2/3 multisig +export const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, +}; +export const testgroup1Address = + "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7v7aysdd"; + +export const testgroup2: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "1", + pubkeys: [test1, test2, test3], + }, +}; +export const testgroup2Address = + "wasmpub1ytql0csgqyfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vc4ejke"; + +// 2/2 multisig +export const testgroup3: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test3], + }, +}; +export const testgroup3Address = + "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vzjhugu"; + +// 2/2 multisig with custom sorting +export const testgroup4: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test3, test1], + }, +}; +export const testgroup4Address = + "wasmpub1ytql0csgqgfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vujvg56k"; From 5fe0f5582e69f48fa37f1afbe2605c3beb87d723 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 22 Mar 2021 18:45:31 +0100 Subject: [PATCH 19/25] Check threashold against number of pubkeys --- packages/amino/src/multisig.spec.ts | 6 ++++++ packages/amino/src/multisig.ts | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/amino/src/multisig.spec.ts b/packages/amino/src/multisig.spec.ts index ce239345..758e1f64 100644 --- a/packages/amino/src/multisig.spec.ts +++ b/packages/amino/src/multisig.spec.ts @@ -59,5 +59,11 @@ describe("multisig", () => { it("works with nosort", () => { expect(createMultisigThresholdPubkey([test3, test1], 2, true)).toEqual(testgroup4); }); + + it("throws for threshold lager than number of keys", () => { + expect(() => createMultisigThresholdPubkey([test1, test2, test3], 5)).toThrowError( + /threshold k = 5 exceeds number of keys n = 3/i, + ); + }); }); }); diff --git a/packages/amino/src/multisig.ts b/packages/amino/src/multisig.ts index 51203f87..795b5caf 100644 --- a/packages/amino/src/multisig.ts +++ b/packages/amino/src/multisig.ts @@ -20,6 +20,11 @@ export function createMultisigThresholdPubkey( threshold: number, nosort = false, ): MultisigThresholdPubkey { + const uintThreshold = new Uint53(threshold); + if (uintThreshold.toNumber() > pubkeys.length) { + throw new Error(`Threshold k = ${uintThreshold.toNumber()} exceeds number of keys n = ${pubkeys.length}`); + } + const outPubkeys = nosort ? pubkeys : Array.from(pubkeys).sort((lhs, rhs) => { @@ -31,7 +36,7 @@ export function createMultisigThresholdPubkey( return { type: "tendermint/PubKeyMultisigThreshold", value: { - threshold: new Uint53(threshold).toString(), + threshold: uintThreshold.toString(), pubkeys: outPubkeys, }, }; From e0716fac217a0663550bfd8cab45c51ab36bfeb8 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 13:25:01 +0100 Subject: [PATCH 20/25] Use locale independent sorting --- packages/amino/src/multisig.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/amino/src/multisig.ts b/packages/amino/src/multisig.ts index 795b5caf..7e8ecc5a 100644 --- a/packages/amino/src/multisig.ts +++ b/packages/amino/src/multisig.ts @@ -12,7 +12,9 @@ import { MultisigThresholdPubkey, SinglePubkey } from "./pubkeys"; * Returns 0 if `a === b`. */ export function compareArrays(a: Uint8Array, b: Uint8Array): number { - return toHex(a).localeCompare(toHex(b)); + const aHex = toHex(a); + const bHex = toHex(b); + return aHex === bHex ? 0 : aHex < bHex ? -1 : 1; } export function createMultisigThresholdPubkey( From 2655531891804723189b810c6b33f8edf6d6de21 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 13:25:44 +0100 Subject: [PATCH 21/25] Fix typos --- packages/amino/src/encoding.spec.ts | 2 +- packages/amino/src/multisig.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index ad98e97d..e497d84c 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -21,7 +21,7 @@ import { describe("encoding", () => { describe("encodeSecp256k1Pubkey", () => { - it("encodes a compresed pubkey", () => { + it("encodes a compressed pubkey", () => { const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); expect(encodeSecp256k1Pubkey(pubkey)).toEqual({ type: "tendermint/PubKeySecp256k1", diff --git a/packages/amino/src/multisig.spec.ts b/packages/amino/src/multisig.spec.ts index 758e1f64..ae3e5a5a 100644 --- a/packages/amino/src/multisig.spec.ts +++ b/packages/amino/src/multisig.spec.ts @@ -60,7 +60,7 @@ describe("multisig", () => { expect(createMultisigThresholdPubkey([test3, test1], 2, true)).toEqual(testgroup4); }); - it("throws for threshold lager than number of keys", () => { + it("throws for threshold larger than number of keys", () => { expect(() => createMultisigThresholdPubkey([test1, test2, test3], 5)).toThrowError( /threshold k = 5 exceeds number of keys n = 3/i, ); From e81cb3b01787d4df9ac177f46d14920b43212f61 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 13:26:06 +0100 Subject: [PATCH 22/25] Inprove createMultisigThresholdPubkey threshold testing --- packages/amino/src/multisig.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/amino/src/multisig.spec.ts b/packages/amino/src/multisig.spec.ts index ae3e5a5a..05134891 100644 --- a/packages/amino/src/multisig.spec.ts +++ b/packages/amino/src/multisig.spec.ts @@ -61,8 +61,11 @@ describe("multisig", () => { }); it("throws for threshold larger than number of keys", () => { - expect(() => createMultisigThresholdPubkey([test1, test2, test3], 5)).toThrowError( - /threshold k = 5 exceeds number of keys n = 3/i, + expect(() => createMultisigThresholdPubkey([test1, test2, test3], 4)).toThrowError( + /threshold k = 4 exceeds number of keys n = 3/i, + ); + expect(() => createMultisigThresholdPubkey([test1, test2, test3], 75)).toThrowError( + /threshold k = 75 exceeds number of keys n = 3/i, ); }); }); From 599113da83b61d1eab9e65f9d38d9b0fee5acc3b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 13:30:10 +0100 Subject: [PATCH 23/25] Add more ordering tests --- packages/amino/src/multisig.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/amino/src/multisig.spec.ts b/packages/amino/src/multisig.spec.ts index 05134891..42bd1869 100644 --- a/packages/amino/src/multisig.spec.ts +++ b/packages/amino/src/multisig.spec.ts @@ -16,6 +16,12 @@ describe("multisig", () => { expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 5]))).toBeGreaterThan(0); expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5]))).toBeGreaterThan(0); expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([]))).toBeGreaterThan(0); + + // left or right precedence + expect(compareArrays(new Uint8Array([5, 5, 4]), new Uint8Array([4, 5, 5]))).toBeGreaterThan(0); + + // magnitude is more important than length + expect(compareArrays(new Uint8Array([6]), new Uint8Array([5, 5]))).toBeGreaterThan(0); }); it("return < 0 for left < right", () => { @@ -25,6 +31,12 @@ describe("multisig", () => { expect(compareArrays(new Uint8Array([5, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); expect(compareArrays(new Uint8Array([5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); expect(compareArrays(new Uint8Array([]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + + // left or right precedence + expect(compareArrays(new Uint8Array([4, 5, 5]), new Uint8Array([5, 5, 4]))).toBeLessThan(0); + + // magnitude is more important than length + expect(compareArrays(new Uint8Array([5, 5]), new Uint8Array([6]))).toBeLessThan(0); }); it("can be used with sort", () => { From 8e9cd145cd495cc48ebf534c882a821802a467fe Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 13:33:28 +0100 Subject: [PATCH 24/25] Use hex representations for bytes --- packages/amino/src/encoding.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index a658f53e..79b0e4ab 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -99,10 +99,10 @@ function encodeUvarint(value: number | string): number[] { export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { if (isMultisigThresholdPubkey(pubkey)) { const out = Array.from(pubkeyAminoPrefixMultisigThreshold); - out.push(8); // TODO: What is this? + out.push(0x08); // TODO: What is this? out.push(...encodeUvarint(pubkey.value.threshold)); for (const pubkeyData of pubkey.value.pubkeys.map((p) => encodeAminoPubkey(p))) { - out.push(18); // TODO: What is this? + out.push(0x12); // TODO: What is this? out.push(...encodeUvarint(pubkeyData.length)); out.push(...pubkeyData); } From 9c4eb57f554f23db3dc51eeec1248df10dbc515a Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 14:27:36 +0100 Subject: [PATCH 25/25] Add deprecation notice to PubKey --- packages/launchpad/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index 11da4d29..14db51fa 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -7,8 +7,10 @@ export { encodeSecp256k1Pubkey, pubkeyToAddress, pubkeyType, - SinglePubkey as PubKey, } from "@cosmjs/amino"; +import { SinglePubkey } from "@cosmjs/amino"; +/** @deprecated PubKey is deprecated. Use `SinglePubkey` or the more general `Pubkey` from `@cosmjs/amino`. */ +export type PubKey = SinglePubkey; import * as logs from "./logs"; export { logs };