Merge pull request #366 from CosmWasm/encodeBech32Pubkey-tests

Backport pubkey helpers to 0.22
This commit is contained in:
Simon Warta 2020-08-11 09:49:51 +02:00 committed by GitHub
commit 749f195677
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 239 additions and 23 deletions

View File

@ -1,5 +1,13 @@
# CHANGELOG
## 0.22.1 (unreleased)
- @cosmjs/cli: Import `encodeAminoPubkey`, `encodeBech32Pubkey`,
`decodeAminoPubkey` and `decodeBech32Pubkey` by default.
- @cosmjs/launchpad: Add ed25519 support to `encodeBech32Pubkey`.
- @cosmjs/launchpad: Add `encodeAminoPubkey` and `decodeAminoPubkey`.
- @cosmjs/utils: Add `arrayContentEquals`.
## 0.22.0 (2020-08-03)
- @cosmjs/cli: Now supports HTTPs URLs for `--init` code sources.

View File

@ -89,6 +89,10 @@ export async function main(originalArgs: readonly string[]): Promise<void> {
[
"coin",
"coins",
"decodeAminoPubkey",
"decodeBech32Pubkey",
"encodeAminoPubkey",
"encodeBech32Pubkey",
"encodeSecp256k1Pubkey",
"encodeSecp256k1Signature",
"logs",
@ -112,7 +116,7 @@ export async function main(originalArgs: readonly string[]): Promise<void> {
],
],
["@cosmjs/math", ["Decimal", "Int53", "Uint32", "Uint53", "Uint64"]],
["@cosmjs/utils", ["assert", "sleep"]],
["@cosmjs/utils", ["assert", "arrayContentEquals", "sleep"]],
]);
console.info(colors.green("Initializing session for you. Have fun!"));
@ -156,6 +160,12 @@ export async function main(originalArgs: readonly string[]): Promise<void> {
const data = toAscii("foo bar");
const signature = await wallet.sign(address, data);
const bechPubkey = "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq";
assert(encodeBech32Pubkey(decodeBech32Pubkey(bechPubkey), "coralvalconspub") == bechPubkey);
const aminoPubkey = fromHex("eb5ae98721034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c70290");
assert(arrayContentEquals(encodeAminoPubkey(decodeAminoPubkey(aminoPubkey)), aminoPubkey));
console.info("Done testing, will exit now.");
process.exit(0);
`;

View File

@ -44,7 +44,6 @@
"@cosmjs/math": "^0.22.0",
"@cosmjs/utils": "^0.22.0",
"axios": "^0.19.0",
"fast-deep-equal": "^3.1.1",
"pako": "^1.0.11"
},
"devDependencies": {

View File

@ -45,8 +45,7 @@
"@cosmjs/encoding": "^0.22.0",
"@cosmjs/math": "^0.22.0",
"@cosmjs/utils": "^0.22.0",
"axios": "^0.19.0",
"fast-deep-equal": "^3.1.1"
"axios": "^0.19.0"
},
"devDependencies": {
"readonly-date": "^1.0.0"

View File

@ -84,7 +84,13 @@ export {
uint64ToString,
} from "./lcdapi";
export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs";
export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey";
export {
decodeAminoPubkey,
decodeBech32Pubkey,
encodeAminoPubkey,
encodeBech32Pubkey,
encodeSecp256k1Pubkey,
} from "./pubkey";
export { findSequenceForSignedTx } from "./sequence";
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
export { FeeTable, SigningCosmosClient } from "./signingcosmosclient";

View File

@ -1,6 +1,12 @@
import { fromBase64 } from "@cosmjs/encoding";
import { Bech32, fromBase64 } from "@cosmjs/encoding";
import { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey";
import {
decodeAminoPubkey,
decodeBech32Pubkey,
encodeAminoPubkey,
encodeBech32Pubkey,
encodeSecp256k1Pubkey,
} from "./pubkey";
import { PubKey } from "./types";
describe("pubkey", () => {
@ -21,6 +27,30 @@ describe("pubkey", () => {
});
});
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(
@ -39,6 +69,44 @@ describe("pubkey", () => {
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", () => {
@ -51,5 +119,17 @@ describe("pubkey", () => {
"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",
);
});
});
});

View File

@ -1,5 +1,5 @@
import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding";
import equal from "fast-deep-equal";
import { arrayContentEquals } from "@cosmjs/utils";
import { PubKey, pubkeyType } from "./types";
@ -21,12 +21,13 @@ const pubkeyAminoPrefixEd25519 = fromHex("1624de6420");
const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005");
const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length;
export function decodeBech32Pubkey(bechEncoded: string): PubKey {
const { data } = Bech32.decode(bechEncoded);
/**
* 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 (equal(aminoPrefix, pubkeyAminoPrefixSecp256k1)) {
if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSecp256k1)) {
if (rest.length !== 33) {
throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey).");
}
@ -34,7 +35,7 @@ export function decodeBech32Pubkey(bechEncoded: string): PubKey {
type: pubkeyType.secp256k1,
value: toBase64(rest),
};
} else if (equal(aminoPrefix, pubkeyAminoPrefixEd25519)) {
} else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixEd25519)) {
if (rest.length !== 32) {
throw new Error("Invalid rest data length. Expected 32 bytes (Ed25519 pubkey).");
}
@ -42,7 +43,7 @@ export function decodeBech32Pubkey(bechEncoded: string): PubKey {
type: pubkeyType.ed25519,
value: toBase64(rest),
};
} else if (equal(aminoPrefix, pubkeyAminoPrefixSr25519)) {
} else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSr25519)) {
if (rest.length !== 32) {
throw new Error("Invalid rest data length. Expected 32 bytes (Sr25519 pubkey).");
}
@ -55,17 +56,42 @@ export function decodeBech32Pubkey(bechEncoded: string): PubKey {
}
}
export function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string {
/**
* 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");
}
const data = new Uint8Array([...aminoPrefix, ...fromBase64(pubkey.value)]);
return Bech32.encode(prefix, data);
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));
}

View File

@ -37,7 +37,7 @@ export interface StdSignature {
}
export interface PubKey {
// type is one of the strings defined in pubkeyTypes
// 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/CosmWasm/cosmjs/pull/44#pullrequestreview-353280504
readonly type: string;
@ -54,5 +54,3 @@ export const pubkeyType = {
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */
sr25519: "tendermint/PubKeySr25519" as const,
};
export const pubkeyTypes: readonly string[] = [pubkeyType.secp256k1, pubkeyType.ed25519, pubkeyType.sr25519];

View File

@ -82,7 +82,13 @@ export {
uint64ToString,
} from "./lcdapi";
export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs";
export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey";
export {
decodeAminoPubkey,
decodeBech32Pubkey,
encodeAminoPubkey,
encodeBech32Pubkey,
encodeSecp256k1Pubkey,
} from "./pubkey";
export { findSequenceForSignedTx } from "./sequence";
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
export { FeeTable, SigningCosmosClient } from "./signingcosmosclient";

View File

@ -1,4 +1,24 @@
import { PubKey } from "./types";
export declare function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey;
/**
* Decodes a pubkey in the Amino binary format to a type/value object.
*/
export declare function decodeAminoPubkey(data: Uint8Array): PubKey;
/**
* 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 declare function decodeBech32Pubkey(bechEncoded: string): PubKey;
/**
* Encodes a public key to binary Amino.
*/
export declare function encodeAminoPubkey(pubkey: PubKey): Uint8Array;
/**
* 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 declare function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string;

View File

@ -36,4 +36,3 @@ export declare const pubkeyType: {
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */
sr25519: "tendermint/PubKeySr25519";
};
export declare const pubkeyTypes: readonly string[];

View File

@ -0,0 +1,33 @@
import { arrayContentEquals } from "./arrays";
describe("array", () => {
describe("arrayContentEquals", () => {
it("can compare number arrays", () => {
expect(arrayContentEquals([1, 2, 3], [1, 2, 3])).toEqual(true);
expect(arrayContentEquals([1, 2, 3], [1, 2, 3, 4])).toEqual(false);
expect(arrayContentEquals([1, 2, 3], [3, 2, 1])).toEqual(false);
});
it("can compare string arrays", () => {
expect(arrayContentEquals(["a", "b"], ["a", "b"])).toEqual(true);
expect(arrayContentEquals(["a", "b"], ["a", "b", "c"])).toEqual(false);
expect(arrayContentEquals(["a", "b"], ["b", "a"])).toEqual(false);
});
it("can compare bool arrays", () => {
expect(arrayContentEquals([true, false], [true, false])).toEqual(true);
expect(arrayContentEquals([true, false], [true, false, true])).toEqual(false);
expect(arrayContentEquals([true, false], [false, true])).toEqual(false);
});
it("can compare different array types", () => {
expect(arrayContentEquals([1, 2, 3], new Uint8Array([1, 2, 3]))).toEqual(true);
expect(arrayContentEquals([1, 2, 3], new Uint8Array([3, 2, 1]))).toEqual(false);
});
it("works for empty arrays", () => {
expect(arrayContentEquals([], [])).toEqual(true);
expect(arrayContentEquals([], new Uint8Array([]))).toEqual(true);
});
});
});

View File

@ -0,0 +1,18 @@
/**
* Compares the content of two arrays-like objects for equality.
*
* Equality is defined as having equal length and 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 arrayContentEquals<T extends string | number | boolean>(
a: ArrayLike<T>,
b: ArrayLike<T>,
): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}

View File

@ -1,3 +1,4 @@
export { arrayContentEquals } from "./arrays";
export { assert } from "./assert";
export { sleep } from "./sleep";
export { isNonNullObject, isUint8Array } from "./typechecks";

12
packages/utils/types/arrays.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
/**
* Compares the content of two arrays-like objects for equality.
*
* Equality is defined as having equal length and 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 declare function arrayContentEquals<T extends string | number | boolean>(
a: ArrayLike<T>,
b: ArrayLike<T>,
): boolean;

View File

@ -1,3 +1,4 @@
export { arrayContentEquals } from "./arrays";
export { assert } from "./assert";
export { sleep } from "./sleep";
export { isNonNullObject, isUint8Array } from "./typechecks";