Move bcp-free address en/de-coding into sdk

This commit is contained in:
Ethan Frey 2020-02-04 19:39:49 +01:00
parent 82447e8afe
commit 8807e7532d
6 changed files with 206 additions and 10 deletions

View File

@ -0,0 +1,56 @@
import { Encoding } from "@iov/encoding";
import { decodeBech32Pubkey, isValidAddress, pubkeyToAddress } from "./address";
import { PubKeyEd25519, PubKeySecp256k1 } from "./types";
const { toBase64, fromHex } = Encoding;
describe("address", () => {
describe("decodeBech32Pubkey", () => {
it("works", () => {
expect(
decodeBech32Pubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"),
).toEqual({
type: "tendermint/PubKeySecp256k1",
value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ",
});
});
});
describe("isValidAddress", () => {
it("accepts valid addresses", () => {
expect(isValidAddress("cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6")).toEqual(true);
expect(isValidAddress("cosmosvalcons10q82zkzzmaku5lazhsvxv7hsg4ntpuhdwadmss")).toEqual(true);
expect(isValidAddress("cosmosvaloper17mggn4znyeyg25wd7498qxl7r2jhgue8u4qjcq")).toEqual(true);
});
it("rejects invalid addresses", () => {
// Bad size
expect(isValidAddress("cosmos10q82zkzzmaku5lazhsvxv7hsg4ntpuhh8289f")).toEqual(false);
// Bad checksum
expect(isValidAddress("cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs7")).toEqual(false);
// Bad prefix
expect(isValidAddress("cosmot10q82zkzzmaku5lazhsvxv7hsg4ntpuhd8j5266")).toEqual(false);
});
});
describe("pubkeyToAddress", () => {
it("works for Secp256k1 compressed", () => {
const prefix = "cosmos";
const pubkey: PubKeySecp256k1 = {
type: "tendermint/PubKeySecp256k1",
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
};
expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r");
});
it("works for Ed25519", () => {
const prefix = "cosmos";
const pubkey: PubKeyEd25519 = {
type: "tendermint/PubKeyEd25519",
value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")),
};
expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz");
});
});
});

109
packages/sdk/src/address.ts Normal file
View File

@ -0,0 +1,109 @@
import { Ripemd160, Sha256 } from "@iov/crypto";
import { Bech32, Encoding } from "@iov/encoding";
import equal from "fast-deep-equal";
import { Bech32PubKey, PubKey } from "./types";
const { fromBase64, toBase64 } = Encoding;
// TODO: make this much more configurable
export type CosmosAddressBech32Prefix = "cosmos" | "cosmosvalcons" | "cosmosvaloper";
export type CosmosPubkeyBech32Prefix = "cosmospub" | "cosmosvalconspub" | "cosmosvaloperpub";
export type CosmosBech32Prefix = CosmosAddressBech32Prefix | CosmosPubkeyBech32Prefix;
function isCosmosAddressBech32Prefix(prefix: string): prefix is CosmosAddressBech32Prefix {
return ["cosmos", "cosmosvalcons", "cosmosvaloper"].includes(prefix);
}
function isCosmosPubkeyBech32Prefix(prefix: string): prefix is CosmosPubkeyBech32Prefix {
return ["cosmospub", "cosmosvalconspub", "cosmosvaloperpub"].includes(prefix);
}
// 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 = Encoding.fromHex("eb5ae98721");
const pubkeyAminoPrefixEd25519 = Encoding.fromHex("1624de6420");
const pubkeyAminoPrefixSr25519 = Encoding.fromHex("0dfb1005");
const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length;
export function decodeBech32Pubkey(bech: Bech32PubKey): PubKey {
const { prefix, data } = Bech32.decode(bech);
if (!isCosmosPubkeyBech32Prefix(prefix)) {
throw new Error(`Invalid bech32 prefix. Must be one of cosmos, cosmosvalcons, or cosmosvaloper.`);
}
const aminoPrefix = data.slice(0, pubkeyAminoPrefixLength);
const rest = data.slice(pubkeyAminoPrefixLength);
if (equal(aminoPrefix, pubkeyAminoPrefixSecp256k1)) {
if (rest.length !== 33) {
throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey).");
}
return {
type: "tendermint/PubKeySecp256k1",
value: toBase64(rest),
};
} else if (equal(aminoPrefix, pubkeyAminoPrefixEd25519)) {
if (rest.length !== 32) {
throw new Error("Invalid rest data length. Expected 32 bytes (Ed25519 pubkey).");
}
return {
type: "tendermint/PubKeyEd25519",
value: toBase64(rest),
};
} else if (equal(aminoPrefix, pubkeyAminoPrefixSr25519)) {
if (rest.length !== 32) {
throw new Error("Invalid rest data length. Expected 32 bytes (Sr25519 pubkey).");
}
return {
type: "tendermint/PubKeySr25519",
value: toBase64(rest),
};
} else {
throw new Error("Unsupported Pubkey type. Amino prefix: " + Encoding.toHex(aminoPrefix));
}
}
export function isValidAddress(address: string): boolean {
try {
const { prefix, data } = Bech32.decode(address);
if (!isCosmosAddressBech32Prefix(prefix)) {
return false;
}
return data.length === 20;
} catch {
return false;
}
}
// 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 {
const pubkeyBytes = fromBase64(pubkey.value);
switch (pubkey.type) {
case "tendermint/PubKeySecp256k1": {
if (pubkeyBytes.length !== 33) {
throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyBytes.length}`);
}
const hash1 = new Sha256(pubkeyBytes).digest();
const hash2 = new Ripemd160(hash1).digest();
return Bech32.encode(prefix, hash2);
}
case "tendermint/PubKeyEd25519": {
if (pubkeyBytes.length !== 32) {
throw new Error(`Invalid Ed25519 pubkey length: ${pubkeyBytes.length}`);
}
const hash = new Sha256(pubkeyBytes).digest();
return Bech32.encode(prefix, hash.slice(0, 20));
}
case "tendermint/PubKeySr25519": {
if (pubkeyBytes.length !== 32) {
throw new Error(`Invalid Sr25519 pubkey length: ${pubkeyBytes.length}`);
}
const hash = new Sha256(pubkeyBytes).digest();
return Bech32.encode(prefix, hash.slice(0, 20));
}
default:
throw new Error("Unrecognized public key algorithm");
}
}

View File

@ -78,7 +78,8 @@ describe("RestClient", () => {
describe("encodeTx", () => {
it("works for cosmoshub example", async () => {
pendingWithoutCosmos();
const tx: StdTx = data.tx.value;
// need to convince the compiler we have a valid string for pubkey type
const tx: StdTx = data.tx.value as StdTx;
const client = new RestClient(httpUrl);
expect(await client.encodeTx(tx)).toEqual(fromBase64(data.tx_data));
});

View File

@ -100,19 +100,33 @@ export interface StdSignature {
readonly signature: string;
}
export interface PubKey {
readonly type: string;
// value field is base64-encoded in all cases
export type PubKey = PubKeyEd25519 | PubKeySecp256k1 | PubKeySr25519;
export interface PubKeySecp256k1 {
readonly type: "tendermint/PubKeySecp256k1";
// Note: this contains a Secp256k1 COMPRESSED pubkey - to encode from bcp/keycontrol land, you must compress it first
readonly value: string;
}
// AccountPubKey is bech32-encoded amino-binary encoded PubKey interface. oof.
export type AccountPubKey = string;
export interface PubKeyEd25519 {
readonly type: "tendermint/PubKeyEd25519";
readonly value: string;
}
export interface PubKeySr25519 {
readonly type: "tendermint/PubKeySr25519";
readonly value: string;
}
// Bech32PubKey is bech32-encoded amino-binary encoded PubKey interface. oof.
export type Bech32PubKey = string;
export interface BaseAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: ReadonlyArray<Coin>;
readonly public_key: AccountPubKey;
readonly public_key: Bech32PubKey;
readonly account_number: number;
readonly sequence: number;
}

7
packages/sdk/types/address.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import { Bech32PubKey, PubKey } from "./types";
export declare type CosmosAddressBech32Prefix = "cosmos" | "cosmosvalcons" | "cosmosvaloper";
export declare type CosmosPubkeyBech32Prefix = "cosmospub" | "cosmosvalconspub" | "cosmosvaloperpub";
export declare type CosmosBech32Prefix = CosmosAddressBech32Prefix | CosmosPubkeyBech32Prefix;
export declare function decodeBech32Pubkey(bech: Bech32PubKey): PubKey;
export declare function isValidAddress(address: string): boolean;
export declare function pubkeyToAddress(pubkey: PubKey, prefix: string): string;

View File

@ -70,16 +70,25 @@ export interface StdSignature {
readonly pub_key: PubKey;
readonly signature: string;
}
export interface PubKey {
readonly type: string;
export declare type PubKey = PubKeyEd25519 | PubKeySecp256k1 | PubKeySr25519;
export interface PubKeySecp256k1 {
readonly type: "tendermint/PubKeySecp256k1";
readonly value: string;
}
export declare type AccountPubKey = string;
export interface PubKeyEd25519 {
readonly type: "tendermint/PubKeyEd25519";
readonly value: string;
}
export interface PubKeySr25519 {
readonly type: "tendermint/PubKeySr25519";
readonly value: string;
}
export declare type Bech32PubKey = string;
export interface BaseAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: ReadonlyArray<Coin>;
readonly public_key: AccountPubKey;
readonly public_key: Bech32PubKey;
readonly account_number: number;
readonly sequence: number;
}