From 95722a04953eed4a380239a36a4ca2b795166f86 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 23 Jan 2020 14:45:38 +0100 Subject: [PATCH 1/9] Located bug. Example output: from query: account: number = 4, sequence = 0 for signing: nonce: 0 --- src/cosmoscodec.ts | 2 ++ src/cosmosconnection.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/cosmoscodec.ts b/src/cosmoscodec.ts index 88a74ea8..31d6e3cf 100644 --- a/src/cosmoscodec.ts +++ b/src/cosmoscodec.ts @@ -54,6 +54,8 @@ export class CosmosCodec implements TxCodec { } public bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob { + console.log("nonce:", nonce); + // ohhh... this is a bug!!! const accountNumber = 0; const memo = (unsigned as any).memo; const built = buildUnsignedTx(unsigned, this.tokens); diff --git a/src/cosmosconnection.ts b/src/cosmosconnection.ts index d882d7d7..6e7a3816 100644 --- a/src/cosmosconnection.ts +++ b/src/cosmosconnection.ts @@ -171,6 +171,7 @@ export class CosmosConnection implements BlockchainConnection { const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.prefix) : query.address; const { result } = await this.restClient.authAccounts(address); const account = result.value; + console.log(`account: number = ${account.account_number}, sequence = ${account.sequence}`); return parseInt(account.sequence, 10) as Nonce; } From 5074a8c8d3f58cf8d5aebefec07fc995b8de0150 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 23 Jan 2020 15:08:00 +0100 Subject: [PATCH 2/9] add nonce encode/decode functions and test --- src/cosmoscodec.ts | 2 -- src/types.spec.ts | 15 +++++++++++++++ src/types.ts | 41 ++++++++++++++++++++++++++++++++++++++++- types/types.d.ts | 5 ++++- 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 src/types.spec.ts diff --git a/src/cosmoscodec.ts b/src/cosmoscodec.ts index 31d6e3cf..88a74ea8 100644 --- a/src/cosmoscodec.ts +++ b/src/cosmoscodec.ts @@ -54,8 +54,6 @@ export class CosmosCodec implements TxCodec { } public bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob { - console.log("nonce:", nonce); - // ohhh... this is a bug!!! const accountNumber = 0; const memo = (unsigned as any).memo; const built = buildUnsignedTx(unsigned, this.tokens); diff --git a/src/types.spec.ts b/src/types.spec.ts new file mode 100644 index 00000000..87f07fe7 --- /dev/null +++ b/src/types.spec.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { accountToNonce, nonceToAccountNumber, nonceToSequence } from "./types"; + +describe("nonceEncoding", () => { + it("works for input in range", () => { + const nonce = accountToNonce("1234", "7890"); + expect(nonceToAccountNumber(nonce)).toEqual("1234"); + expect(nonceToSequence(nonce)).toEqual("7890"); + }); + + it("errors on input too large", () => { + expect(() => accountToNonce("1234567890", "7890")).toThrow(); + expect(() => accountToNonce("178", "97320247923")).toThrow(); + }); +}); diff --git a/src/types.ts b/src/types.ts index 5e44a030..2ebcec1c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { Amount, Token } from "@iov/bcp"; +import { Amount, Nonce, Token } from "@iov/bcp"; import amino from "@tendermint/amino-js"; export type AminoTx = amino.Tx & { readonly value: amino.StdTx }; @@ -40,3 +40,42 @@ export function coinToAmount(tokens: TokenInfos, coin: amino.Coin): Amount { quantity: coin.amount, }; } + +// tslint:disable-next-line:no-bitwise +const maxAcct = 1 << 23; +// tslint:disable-next-line:no-bitwise +const maxSeq = 1 << 20; + +// this (lossily) encodes the two pieces of info (uint64) needed to sign into +// one (53-bit) number. Cross your fingers. +export function accountToNonce(accountNumber: string, sequence: string): Nonce { + const acct = parseInt(accountNumber, 10); + const seq = parseInt(sequence, 10); + + // we allow 23 bits (8 million) for accounts, and 20 bits (1 million) for tx/account + // let's fix this soon + if (acct > maxAcct) { + throw new Error("Account number is greater than 2^23, must update Nonce handler"); + } + if (seq > maxSeq) { + throw new Error("Sequence is greater than 2^20, must update Nonce handler"); + } + + const val = acct * maxSeq + seq; + return val as Nonce; +} + +// this extracts info from nonce for signing +export function nonceToAccountNumber(nonce: Nonce): string { + const acct = nonce / maxSeq; + if (acct > maxAcct) { + throw new Error("Invalid Nonce, account number is higher than can safely be encoded in Nonce"); + } + return Math.round(acct).toString(); +} + +// this extracts info from nonce for signing +export function nonceToSequence(nonce: Nonce): string { + const seq = nonce % maxSeq; + return Math.round(seq).toString(); +} diff --git a/types/types.d.ts b/types/types.d.ts index 62d5ab9b..eda75b7b 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1,4 +1,4 @@ -import { Amount, Token } from "@iov/bcp"; +import { Amount, Token, Nonce } from "@iov/bcp"; import amino from "@tendermint/amino-js"; export declare type AminoTx = amino.Tx & { readonly value: amino.StdTx; @@ -10,3 +10,6 @@ export interface TokenInfo extends Token { export declare type TokenInfos = ReadonlyArray; export declare function amountToCoin(lookup: ReadonlyArray, amount: Amount): amino.Coin; export declare function coinToAmount(tokens: TokenInfos, coin: amino.Coin): Amount; +export declare function accountToNonce(accountNumber: string, sequence: string): Nonce; +export declare function nonceToAccountNumber(nonce: Nonce): string; +export declare function nonceToSequence(nonce: Nonce): string; From b3c7a6a4783daa482bb39d387d7c74134995a328 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 23 Jan 2020 15:10:53 +0100 Subject: [PATCH 3/9] Cleanup nonce encoding --- src/types.spec.ts | 19 ++++++++++++++++--- src/types.ts | 9 ++++++++- types/types.d.ts | 8 ++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/types.spec.ts b/src/types.spec.ts index 87f07fe7..42d2bd79 100644 --- a/src/types.spec.ts +++ b/src/types.spec.ts @@ -3,13 +3,26 @@ import { accountToNonce, nonceToAccountNumber, nonceToSequence } from "./types"; describe("nonceEncoding", () => { it("works for input in range", () => { - const nonce = accountToNonce("1234", "7890"); + const nonce = accountToNonce({ + accountNumber: "1234", + sequence: "7890", + }); expect(nonceToAccountNumber(nonce)).toEqual("1234"); expect(nonceToSequence(nonce)).toEqual("7890"); }); it("errors on input too large", () => { - expect(() => accountToNonce("1234567890", "7890")).toThrow(); - expect(() => accountToNonce("178", "97320247923")).toThrow(); + expect(() => + accountToNonce({ + accountNumber: "1234567890", + sequence: "7890", + }), + ).toThrow(); + expect(() => + accountToNonce({ + accountNumber: "178", + sequence: "97320247923", + }), + ).toThrow(); }); }); diff --git a/src/types.ts b/src/types.ts index 2ebcec1c..d2f1ab85 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,9 +46,16 @@ const maxAcct = 1 << 23; // tslint:disable-next-line:no-bitwise const maxSeq = 1 << 20; +// NonceInfo is the data we need from account to create a nonce +// Use this so no confusion about order of arguments +export interface NonceInfo { + readonly accountNumber: string; + readonly sequence: string; +} + // this (lossily) encodes the two pieces of info (uint64) needed to sign into // one (53-bit) number. Cross your fingers. -export function accountToNonce(accountNumber: string, sequence: string): Nonce { +export function accountToNonce({ accountNumber, sequence }: NonceInfo): Nonce { const acct = parseInt(accountNumber, 10); const seq = parseInt(sequence, 10); diff --git a/types/types.d.ts b/types/types.d.ts index eda75b7b..292d0e16 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1,4 +1,4 @@ -import { Amount, Token, Nonce } from "@iov/bcp"; +import { Amount, Nonce, Token } from "@iov/bcp"; import amino from "@tendermint/amino-js"; export declare type AminoTx = amino.Tx & { readonly value: amino.StdTx; @@ -10,6 +10,10 @@ export interface TokenInfo extends Token { export declare type TokenInfos = ReadonlyArray; export declare function amountToCoin(lookup: ReadonlyArray, amount: Amount): amino.Coin; export declare function coinToAmount(tokens: TokenInfos, coin: amino.Coin): Amount; -export declare function accountToNonce(accountNumber: string, sequence: string): Nonce; +export interface NonceInfo { + readonly accountNumber: string; + readonly sequence: string; +} +export declare function accountToNonce({ accountNumber, sequence }: NonceInfo): Nonce; export declare function nonceToAccountNumber(nonce: Nonce): string; export declare function nonceToSequence(nonce: Nonce): string; From 3eacb108bdceb4c0d6315fe0d156253e17566326 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 23 Jan 2020 15:43:01 +0100 Subject: [PATCH 4/9] Fix signing, pubkey parsing. all tests pass on first run against chain --- src/cosmoscodec.ts | 7 +++---- src/cosmosconnection.spec.ts | 20 ++++++++++++++------ src/cosmosconnection.ts | 25 ++++++++++++++++--------- src/types.spec.ts | 6 +++--- src/types.ts | 6 +++--- types/types.d.ts | 4 ++-- 6 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/cosmoscodec.ts b/src/cosmoscodec.ts index 88a74ea8..5bce7c36 100644 --- a/src/cosmoscodec.ts +++ b/src/cosmoscodec.ts @@ -22,7 +22,7 @@ import { CosmosBech32Prefix, isValidAddress, pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; import { parseTx } from "./decode"; import { buildSignedTx, buildUnsignedTx } from "./encode"; -import { TokenInfos } from "./types"; +import { TokenInfos, nonceToAccountNumber, nonceToSequence } from "./types"; const { toHex, toUtf8 } = Encoding; @@ -54,17 +54,16 @@ export class CosmosCodec implements TxCodec { } public bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob { - const accountNumber = 0; const memo = (unsigned as any).memo; const built = buildUnsignedTx(unsigned, this.tokens); const signMsg = sortJson({ - account_number: accountNumber.toString(), + account_number: nonceToAccountNumber(nonce), chain_id: Caip5.decode(unsigned.chainId), fee: (built.value as any).fee, memo: memo, msgs: (built.value as any).msg, - sequence: nonce.toString(), + sequence: nonceToSequence(nonce), }); const signBytes = toUtf8(JSON.stringify(signMsg)); diff --git a/src/cosmosconnection.spec.ts b/src/cosmosconnection.spec.ts index 7d140726..4c7d7460 100644 --- a/src/cosmosconnection.spec.ts +++ b/src/cosmosconnection.spec.ts @@ -17,7 +17,7 @@ import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; import { CosmosBech32Prefix } from "./address"; import { CosmosCodec, cosmosCodec } from "./cosmoscodec"; import { CosmosConnection } from "./cosmosconnection"; -import { TokenInfos } from "./types"; +import { TokenInfos, nonceToSequence } from "./types"; const { fromBase64, toHex } = Encoding; @@ -150,8 +150,10 @@ describe("CosmosConnection", () => { throw new Error("Expected account not to be undefined"); } expect(account.address).toEqual(defaultAddress); - // Undefined until we sign a transaction - expect(account.pubkey).toEqual(undefined); + // Undefined until we sign a transaction (on multiple runs against one server this will be set), allow both + if (account.pubkey !== undefined) { + expect(account.pubkey).toEqual(defaultPubkey); + } // Starts with two tokens expect(account.balance.length).toEqual(2); connection.disconnect(); @@ -165,8 +167,12 @@ describe("CosmosConnection", () => { throw new Error("Expected account not to be undefined"); } expect(account.address).toEqual(defaultAddress); - // Undefined until we sign a transaction - expect(account.pubkey).toEqual(undefined); + // Undefined until we sign a transaction (on multiple runs against one server this will be set), allow both + if (account.pubkey !== undefined) { + console.log(account.pubkey); + console.log(defaultPubkey); + expect(account.pubkey).toEqual(defaultPubkey); + } // Starts with two tokens expect(account.balance.length).toEqual(2); connection.disconnect(); @@ -223,7 +229,9 @@ describe("CosmosConnection", () => { expect(transaction.chainId).toEqual(unsigned.chainId); expect(signatures.length).toEqual(1); - expect(signatures[0].nonce).toEqual(signed.signatures[0].nonce); + // TODO: the nonce we recover in response doesn't have accountNumber, only sequence + const signedSequence = parseInt(nonceToSequence(signed.signatures[0].nonce), 10); + expect(signatures[0].nonce).toEqual(signedSequence); expect(signatures[0].pubkey.algo).toEqual(signed.signatures[0].pubkey.algo); expect(toHex(signatures[0].pubkey.data)).toEqual( toHex(Secp256k1.compressPubkey(signed.signatures[0].pubkey.data)), diff --git a/src/cosmosconnection.ts b/src/cosmosconnection.ts index 6e7a3816..6646924e 100644 --- a/src/cosmosconnection.ts +++ b/src/cosmosconnection.ts @@ -27,18 +27,19 @@ import { TransactionQuery, TransactionState, UnsignedTransaction, + PubkeyBundle, } from "@iov/bcp"; -import { Encoding, Uint53 } from "@iov/encoding"; +import { Encoding, Uint53, Bech32 } from "@iov/encoding"; import { DefaultValueProducer, ValueAndUpdates } from "@iov/stream"; import equal from "fast-deep-equal"; import { ReadonlyDate } from "readonly-date"; import { Stream } from "xstream"; -import { CosmosBech32Prefix, pubkeyToAddress } from "./address"; +import { CosmosBech32Prefix, decodeCosmosAddress, pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; import { decodeAmount, parseTxsResponse } from "./decode"; import { RestClient, TxsResponse } from "./restclient"; -import { TokenInfos } from "./types"; +import { TokenInfos, accountToNonce } from "./types"; const { fromBase64 } = Encoding; @@ -150,11 +151,15 @@ export class CosmosConnection implements BlockchainConnection { const supportedCoins = account.coins.filter(({ denom }) => this.tokenInfo.find(token => token.denom === denom), ); + + console.log(account.public_key); const pubkey = !account.public_key ? undefined : { algo: Algorithm.Secp256k1, - data: fromBase64(account.public_key.value) as PubkeyBytes, + // amino-js has wrong (outdated) types + data: Bech32.decode(account.public_key as any).data as PubkeyBytes, + // data: decodeCosmosAddress(account.public_key).data as PubkeyBytes, }; return { address: address, @@ -171,8 +176,7 @@ export class CosmosConnection implements BlockchainConnection { const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.prefix) : query.address; const { result } = await this.restClient.authAccounts(address); const account = result.value; - console.log(`account: number = ${account.account_number}, sequence = ${account.sequence}`); - return parseInt(account.sequence, 10) as Nonce; + return accountToNonce(account); } public async getNonces(query: AddressQuery | PubkeyQuery, count: number): Promise { @@ -181,6 +185,7 @@ export class CosmosConnection implements BlockchainConnection { return []; } const firstNonce = await this.getNonce(query); + // Note: this still works with the encoded format (see types/accountToNonce) as least-significant digits are sequence return [...new Array(checkedCount)].map((_, i) => (firstNonce + i) as Nonce); } @@ -215,7 +220,7 @@ export class CosmosConnection implements BlockchainConnection { public async postTx(tx: PostableBytes): Promise { const { code, txhash, raw_log } = await this.restClient.postTx(tx); - if (code !== 0) { + if (code) { throw new Error(raw_log); } const transactionId = txhash as TransactionId; @@ -303,7 +308,9 @@ export class CosmosConnection implements BlockchainConnection { ): Promise | FailedTransaction> { const sender = (response.tx.value as any).msg[0].value.from_address; const accountForHeight = await this.restClient.authAccounts(sender, response.height); - const nonce = (parseInt(accountForHeight.result.value.sequence, 10) - 1) as Nonce; - return parseTxsResponse(chainId, parseInt(response.height, 10), nonce, response, this.tokenInfo); + // this is technically not the proper nonce. maybe this causes issues for sig validation? + // leaving for now unless it causes issues + const sequence = (parseInt(accountForHeight.result.value.sequence, 10) - 1) as Nonce; + return parseTxsResponse(chainId, parseInt(response.height, 10), sequence, response, this.tokenInfo); } } diff --git a/src/types.spec.ts b/src/types.spec.ts index 42d2bd79..0023e63f 100644 --- a/src/types.spec.ts +++ b/src/types.spec.ts @@ -4,7 +4,7 @@ import { accountToNonce, nonceToAccountNumber, nonceToSequence } from "./types"; describe("nonceEncoding", () => { it("works for input in range", () => { const nonce = accountToNonce({ - accountNumber: "1234", + account_number: "1234", sequence: "7890", }); expect(nonceToAccountNumber(nonce)).toEqual("1234"); @@ -14,13 +14,13 @@ describe("nonceEncoding", () => { it("errors on input too large", () => { expect(() => accountToNonce({ - accountNumber: "1234567890", + account_number: "1234567890", sequence: "7890", }), ).toThrow(); expect(() => accountToNonce({ - accountNumber: "178", + account_number: "178", sequence: "97320247923", }), ).toThrow(); diff --git a/src/types.ts b/src/types.ts index d2f1ab85..9700c1a6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,14 +49,14 @@ const maxSeq = 1 << 20; // NonceInfo is the data we need from account to create a nonce // Use this so no confusion about order of arguments export interface NonceInfo { - readonly accountNumber: string; + readonly account_number: string; readonly sequence: string; } // this (lossily) encodes the two pieces of info (uint64) needed to sign into // one (53-bit) number. Cross your fingers. -export function accountToNonce({ accountNumber, sequence }: NonceInfo): Nonce { - const acct = parseInt(accountNumber, 10); +export function accountToNonce({ account_number, sequence }: NonceInfo): Nonce { + const acct = parseInt(account_number, 10); const seq = parseInt(sequence, 10); // we allow 23 bits (8 million) for accounts, and 20 bits (1 million) for tx/account diff --git a/types/types.d.ts b/types/types.d.ts index 292d0e16..259be466 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -11,9 +11,9 @@ export declare type TokenInfos = ReadonlyArray; export declare function amountToCoin(lookup: ReadonlyArray, amount: Amount): amino.Coin; export declare function coinToAmount(tokens: TokenInfos, coin: amino.Coin): Amount; export interface NonceInfo { - readonly accountNumber: string; + readonly account_number: string; readonly sequence: string; } -export declare function accountToNonce({ accountNumber, sequence }: NonceInfo): Nonce; +export declare function accountToNonce({ account_number, sequence }: NonceInfo): Nonce; export declare function nonceToAccountNumber(nonce: Nonce): string; export declare function nonceToSequence(nonce: Nonce): string; From be1eb6f72fe6f09894f45fb8b8822a8dcdde2647 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 23 Jan 2020 15:46:56 +0100 Subject: [PATCH 5/9] Try to debug odd test failure --- src/cosmosconnection.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cosmosconnection.spec.ts b/src/cosmosconnection.spec.ts index 4c7d7460..d5a540eb 100644 --- a/src/cosmosconnection.spec.ts +++ b/src/cosmosconnection.spec.ts @@ -169,8 +169,8 @@ describe("CosmosConnection", () => { expect(account.address).toEqual(defaultAddress); // Undefined until we sign a transaction (on multiple runs against one server this will be set), allow both if (account.pubkey !== undefined) { - console.log(account.pubkey); - console.log(defaultPubkey); + console.log(`account: ${account.pubkey.data.length}`, account.pubkey); + console.log(`default: ${defaultPubkey.data.length}`, defaultPubkey); expect(account.pubkey).toEqual(defaultPubkey); } // Starts with two tokens From c428e40e794d72f03d2d2144a832e645ea9703e7 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 23 Jan 2020 17:20:17 +0100 Subject: [PATCH 6/9] Format code --- src/cosmoscodec.ts | 2 +- src/cosmosconnection.spec.ts | 2 +- src/cosmosconnection.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cosmoscodec.ts b/src/cosmoscodec.ts index 5bce7c36..f4b3619d 100644 --- a/src/cosmoscodec.ts +++ b/src/cosmoscodec.ts @@ -22,7 +22,7 @@ import { CosmosBech32Prefix, isValidAddress, pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; import { parseTx } from "./decode"; import { buildSignedTx, buildUnsignedTx } from "./encode"; -import { TokenInfos, nonceToAccountNumber, nonceToSequence } from "./types"; +import { nonceToAccountNumber, nonceToSequence, TokenInfos } from "./types"; const { toHex, toUtf8 } = Encoding; diff --git a/src/cosmosconnection.spec.ts b/src/cosmosconnection.spec.ts index d5a540eb..0a97cd34 100644 --- a/src/cosmosconnection.spec.ts +++ b/src/cosmosconnection.spec.ts @@ -17,7 +17,7 @@ import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; import { CosmosBech32Prefix } from "./address"; import { CosmosCodec, cosmosCodec } from "./cosmoscodec"; import { CosmosConnection } from "./cosmosconnection"; -import { TokenInfos, nonceToSequence } from "./types"; +import { nonceToSequence, TokenInfos } from "./types"; const { fromBase64, toHex } = Encoding; diff --git a/src/cosmosconnection.ts b/src/cosmosconnection.ts index 6646924e..467dea83 100644 --- a/src/cosmosconnection.ts +++ b/src/cosmosconnection.ts @@ -19,6 +19,7 @@ import { Nonce, PostableBytes, PostTxResponse, + PubkeyBundle, PubkeyBytes, PubkeyQuery, Token, @@ -27,9 +28,8 @@ import { TransactionQuery, TransactionState, UnsignedTransaction, - PubkeyBundle, } from "@iov/bcp"; -import { Encoding, Uint53, Bech32 } from "@iov/encoding"; +import { Bech32, Encoding, Uint53 } from "@iov/encoding"; import { DefaultValueProducer, ValueAndUpdates } from "@iov/stream"; import equal from "fast-deep-equal"; import { ReadonlyDate } from "readonly-date"; @@ -39,7 +39,7 @@ import { CosmosBech32Prefix, decodeCosmosAddress, pubkeyToAddress } from "./addr import { Caip5 } from "./caip5"; import { decodeAmount, parseTxsResponse } from "./decode"; import { RestClient, TxsResponse } from "./restclient"; -import { TokenInfos, accountToNonce } from "./types"; +import { accountToNonce, TokenInfos } from "./types"; const { fromBase64 } = Encoding; From cae35522ce1a546d3eb95628a29ded98619160c3 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 23 Jan 2020 17:20:34 +0100 Subject: [PATCH 7/9] Add decodeCosmosPubkey --- src/address.spec.ts | 13 ++++++++++++- src/address.ts | 30 +++++++++++++++++++++++++++++- types/address.d.ts | 6 ++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/address.spec.ts b/src/address.spec.ts index f0a97792..1d138206 100644 --- a/src/address.spec.ts +++ b/src/address.spec.ts @@ -1,7 +1,7 @@ import { Address, Algorithm, PubkeyBytes } from "@iov/bcp"; import { Encoding } from "@iov/encoding"; -import { decodeCosmosAddress, isValidAddress, pubkeyToAddress } from "./address"; +import { decodeCosmosAddress, decodeCosmosPubkey, isValidAddress, pubkeyToAddress } from "./address"; const { fromBase64, fromHex } = Encoding; @@ -28,6 +28,17 @@ describe("address", () => { }); }); + describe("decodeCosmosPubkey", () => { + it("works", () => { + expect( + decodeCosmosPubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"), + ).toEqual({ + prefix: "cosmospub", + data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"), + }); + }); + }); + describe("isValidAddress", () => { it("accepts valid addresses", () => { expect(isValidAddress("cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6")).toEqual(true); diff --git a/src/address.ts b/src/address.ts index 853e24af..d36a50d4 100644 --- a/src/address.ts +++ b/src/address.ts @@ -1,15 +1,23 @@ import { Address, Algorithm, PubkeyBundle } from "@iov/bcp"; import { Ripemd160, Secp256k1, Sha256 } from "@iov/crypto"; -import { Bech32 } from "@iov/encoding"; +import { Bech32, Encoding } from "@iov/encoding"; +import equal from "fast-deep-equal"; export type CosmosAddressBech32Prefix = "cosmos" | "cosmosvalcons" | "cosmosvaloper"; export type CosmosPubkeyBech32Prefix = "cosmospub" | "cosmosvalconspub" | "cosmosvaloperpub"; export type CosmosBech32Prefix = CosmosAddressBech32Prefix | CosmosPubkeyBech32Prefix; +// As discussed in https://github.com/binance-chain/javascript-sdk/issues/163 +const pubkeyAminoPrefix = Encoding.fromHex("eb5ae98721"); + 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); +} + export function decodeCosmosAddress( address: Address, ): { readonly prefix: CosmosAddressBech32Prefix; readonly data: Uint8Array } { @@ -23,6 +31,26 @@ export function decodeCosmosAddress( return { prefix: prefix, data: data }; } +export function decodeCosmosPubkey( + encodedPubkey: string, +): { readonly prefix: CosmosPubkeyBech32Prefix; readonly data: Uint8Array } { + const { prefix, data } = Bech32.decode(encodedPubkey); + if (!isCosmosPubkeyBech32Prefix(prefix)) { + throw new Error(`Invalid bech32 prefix. Must be one of cosmos, cosmosvalcons, or cosmosvaloper.`); + } + + if (!equal(data.slice(0, pubkeyAminoPrefix.length), pubkeyAminoPrefix)) { + throw new Error("Pubkey does not have the expected amino prefix " + Encoding.toHex(pubkeyAminoPrefix)); + } + + const rest = data.slice(pubkeyAminoPrefix.length); + if (rest.length !== 33) { + throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); + } + + return { prefix: prefix, data: rest }; +} + export function isValidAddress(address: string): boolean { try { decodeCosmosAddress(address as Address); diff --git a/types/address.d.ts b/types/address.d.ts index 776cb61d..252add6e 100644 --- a/types/address.d.ts +++ b/types/address.d.ts @@ -8,5 +8,11 @@ export declare function decodeCosmosAddress( readonly prefix: CosmosAddressBech32Prefix; readonly data: Uint8Array; }; +export declare function decodeCosmosPubkey( + encodedPubkey: string, +): { + readonly prefix: CosmosPubkeyBech32Prefix; + readonly data: Uint8Array; +}; export declare function isValidAddress(address: string): boolean; export declare function pubkeyToAddress(pubkey: PubkeyBundle, prefix: CosmosBech32Prefix): Address; From de0f68a90c1b2c9a7e7090d201be5f833023ef94 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 23 Jan 2020 17:23:15 +0100 Subject: [PATCH 8/9] Use decodeCosmosPubkey to decode pubkey --- src/cosmosconnection.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cosmosconnection.ts b/src/cosmosconnection.ts index 467dea83..fb5221fd 100644 --- a/src/cosmosconnection.ts +++ b/src/cosmosconnection.ts @@ -35,7 +35,7 @@ import equal from "fast-deep-equal"; import { ReadonlyDate } from "readonly-date"; import { Stream } from "xstream"; -import { CosmosBech32Prefix, decodeCosmosAddress, pubkeyToAddress } from "./address"; +import { CosmosBech32Prefix, decodeCosmosPubkey, pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; import { decodeAmount, parseTxsResponse } from "./decode"; import { RestClient, TxsResponse } from "./restclient"; @@ -158,8 +158,7 @@ export class CosmosConnection implements BlockchainConnection { : { algo: Algorithm.Secp256k1, // amino-js has wrong (outdated) types - data: Bech32.decode(account.public_key as any).data as PubkeyBytes, - // data: decodeCosmosAddress(account.public_key).data as PubkeyBytes, + data: decodeCosmosPubkey(account.public_key as any).data as PubkeyBytes, }; return { address: address, From 72ba6d138207a5424222618de16d833284052126 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 23 Jan 2020 18:19:32 +0100 Subject: [PATCH 9/9] Remove console.log, fix linter errors --- src/cosmosconnection.spec.ts | 2 -- src/cosmosconnection.ts | 6 +----- src/types.ts | 1 + 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/cosmosconnection.spec.ts b/src/cosmosconnection.spec.ts index 0a97cd34..fc916c56 100644 --- a/src/cosmosconnection.spec.ts +++ b/src/cosmosconnection.spec.ts @@ -169,8 +169,6 @@ describe("CosmosConnection", () => { expect(account.address).toEqual(defaultAddress); // Undefined until we sign a transaction (on multiple runs against one server this will be set), allow both if (account.pubkey !== undefined) { - console.log(`account: ${account.pubkey.data.length}`, account.pubkey); - console.log(`default: ${defaultPubkey.data.length}`, defaultPubkey); expect(account.pubkey).toEqual(defaultPubkey); } // Starts with two tokens diff --git a/src/cosmosconnection.ts b/src/cosmosconnection.ts index fb5221fd..3c10fa6f 100644 --- a/src/cosmosconnection.ts +++ b/src/cosmosconnection.ts @@ -19,7 +19,6 @@ import { Nonce, PostableBytes, PostTxResponse, - PubkeyBundle, PubkeyBytes, PubkeyQuery, Token, @@ -29,7 +28,7 @@ import { TransactionState, UnsignedTransaction, } from "@iov/bcp"; -import { Bech32, Encoding, Uint53 } from "@iov/encoding"; +import { Uint53 } from "@iov/encoding"; import { DefaultValueProducer, ValueAndUpdates } from "@iov/stream"; import equal from "fast-deep-equal"; import { ReadonlyDate } from "readonly-date"; @@ -41,8 +40,6 @@ import { decodeAmount, parseTxsResponse } from "./decode"; import { RestClient, TxsResponse } from "./restclient"; import { accountToNonce, TokenInfos } from "./types"; -const { fromBase64 } = Encoding; - interface ChainData { readonly chainId: ChainId; } @@ -152,7 +149,6 @@ export class CosmosConnection implements BlockchainConnection { this.tokenInfo.find(token => token.denom === denom), ); - console.log(account.public_key); const pubkey = !account.public_key ? undefined : { diff --git a/src/types.ts b/src/types.ts index 9700c1a6..e929d61b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,6 +55,7 @@ export interface NonceInfo { // this (lossily) encodes the two pieces of info (uint64) needed to sign into // one (53-bit) number. Cross your fingers. +/* eslint-disable-next-line @typescript-eslint/camelcase */ export function accountToNonce({ account_number, sequence }: NonceInfo): Nonce { const acct = parseInt(account_number, 10); const seq = parseInt(sequence, 10);