Fix signing, pubkey parsing. all tests pass on first run against chain

This commit is contained in:
Ethan Frey 2020-01-23 15:43:01 +01:00
parent b3c7a6a478
commit 3eacb108bd
6 changed files with 41 additions and 27 deletions

View File

@ -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));

View File

@ -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)),

View File

@ -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<readonly Nonce[]> {
@ -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<PostTxResponse> {
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<ConfirmedAndSignedTransaction<UnsignedTransaction> | 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);
}
}

View File

@ -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();

View File

@ -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

4
types/types.d.ts vendored
View File

@ -11,9 +11,9 @@ export declare type TokenInfos = ReadonlyArray<TokenInfo>;
export declare function amountToCoin(lookup: ReadonlyArray<TokenInfo>, 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;