Skip to content

Commit

Permalink
Problem: Missing amino JSON PoC
Browse files Browse the repository at this point in the history
Solution: (Fix crypto-org-chain#110) Add MsgBank with amino JSON signing support
  • Loading branch information
calvinlauyh committed Dec 11, 2020
1 parent 8fbf0c8 commit c2bfe09
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 41 deletions.
16 changes: 5 additions & 11 deletions lib/e2e/transaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Secp256k1KeyPair } from '../src/keypair/secp256k1';
import { CroSDK } from '../src/core/cro';
import { Units } from '../src/coin/coin';
import { Network } from '../src/network/network';
import { SIGN_MODE } from '../src/transaction/signable';
import { SIGN_MODE } from '../src/transaction/types';

const customNetwork: Network = {
chainId: 'testnet',
Expand Down Expand Up @@ -98,25 +98,19 @@ describe('e2e test suite', function () {
publicKey: keyPair.getPubKey(),
accountNumber: new Big(account1!.accountNumber),
accountSequence: new Big(account1!.sequence),
signMode: SIGN_MODE.LEGACY_AMINO_JSON,
})
.addSigner({
publicKey: keyPair2.getPubKey(),
accountNumber: new Big(account2!.accountNumber),
accountSequence: new Big(account2!.sequence),
signMode: SIGN_MODE.LEGACY_AMINO_JSON,
})
.toSignable();

const signedTx = signableTx
.setSignature(
0,
keyPair.sign(signableTx.toSignDoc(0, SIGN_MODE.LEGACY_AMINO_JSON)),
SIGN_MODE.LEGACY_AMINO_JSON,
)
.setSignature(
1,
keyPair2.sign(signableTx.toSignDoc(1, SIGN_MODE.LEGACY_AMINO_JSON)),
SIGN_MODE.LEGACY_AMINO_JSON,
)
.setSignature(0, keyPair.sign(signableTx.toSignDoc(0)))
.setSignature(1, keyPair2.sign(signableTx.toSignDoc(1)))
.toSigned();

expect(msgSend1.fromAddress).to.eq(account1!.address);
Expand Down
2 changes: 1 addition & 1 deletion lib/src/cosmos/amino/signdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface StdSignDoc {
readonly fee: StdFee;
readonly msgs: readonly Msg[];
readonly memo: string;
readonly timeout_height: string;
readonly timeout_height?: string;
}

/** Returns a JSON string with objects sorted by key */
Expand Down
17 changes: 10 additions & 7 deletions lib/src/transaction/amino.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { Secp256k1KeyPair } from '../keypair/secp256k1';
import { SIGN_MODE } from './types';

describe('Amino JSON sign mode', function () {
it.only('should work', function () {
const menmonic =
it('should work', function () {
const mnemonic =
'source knee choice chef exact recall craft satoshi coffee intact fun eternal sudden client quote recall sausage injury return duck bottom security like title';
const hdKey = HDKey.fromMnemonic(menmonic);
const hdKey = HDKey.fromMnemonic(mnemonic);
const privKey = hdKey.derivePrivKey("m/44'/1'/0'/0/0");
const keyPair = Secp256k1KeyPair.fromPrivKey(privKey);

Expand All @@ -28,24 +28,27 @@ describe('Amino JSON sign mode', function () {
.addSigner({
publicKey: keyPair.getPubKey(),
accountNumber: new Big('179'),
accountSequence: new Big('0'),
accountSequence: new Big('1'),
signMode: SIGN_MODE.LEGACY_AMINO_JSON,
})
.setFee(cro.Coin.fromBaseUnit('10000'))
.setGasLimit('100000')
.setMemo('amino test')
.setTimeOutHeight(12345)
.setTimeOutHeight(800000)
.toSignable();

const signature = keyPair.sign(signableTx.toSignDoc(0));
const expectedSignature =
'VKTiDDdbkf4HHeSF+Y30DGy/DBeWzMU1nbk+V2qtj9oqVkhy1zleWx0WqUlo/hjv9bvAEbq0JZQA7RQuFwKwSg==';
'RwttPpxKNy7t1ZIUubckiUKexQ/mDZQ4nSpICdUWtrggtyMK22E31+FVWsdrYWT/r0hBaA4FBAWTqMs/TU1utw==';
expect(signature.toBase64String()).to.eq(expectedSignature);

const signedTx = signableTx.setSignature(0, signature).toSigned();

const expectedTxHex =
'0a9e010a8c010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126c0a2b7463726f31667a63727a61336a3466323637376a667578756c6b6733337a36383532717371733868783530122b7463726f31667a63727a61336a3466323637376a667578756c6b6733337a363835327173717338687835301a100a08626173657463726f120431303030120a616d696e6f207465737418b96012690a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a210223c9395d41013e6470c8d27da8b75850554faada3fe3e812660cbdf4534a85d712040a02087f12170a110a08626173657463726f1205313030303010a08d061a4054a4e20c375b91fe071de485f98df40c6cbf0c1796ccc5359db93e576aad8fda2a564872d7395e5b1d16a94968fe18eff5bbc011bab4259400ed142e1702b04a';
'0a9f010a8c010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126c0a2b7463726f31667a63727a61336a3466323637376a667578756c6b6733337a36383532717371733868783530122b7463726f31667a63727a61336a3466323637376a667578756c6b6733337a363835327173717338687835301a100a08626173657463726f120431303030120a616d696e6f20746573741880ea30126b0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a210223c9395d41013e6470c8d27da8b75850554faada3fe3e812660cbdf4534a85d712040a02087f180112170a110a08626173657463726f1205313030303010a08d061a40470b6d3e9c4a372eedd59214b9b72489429ec50fe60d94389d2a4809d516b6b820b7230adb6137d7e1555ac76b6164ffaf4841680e05040593a8cb3f4d4d6eb7';
expect(signedTx.getHexEncoded()).to.eq(expectedTxHex);

const expectedTxHash = 'E9B8FCAB8ED3ECD9F8E2746D8740FCC45E2B49B61A6D5999540DB2C66ECF85CF';
expect(signedTx.getTxHash()).to.eq(expectedTxHash);
});
});
47 changes: 46 additions & 1 deletion lib/src/transaction/edgecasetx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import Big from 'big.js';
import { expect } from 'chai';
import { Network } from '../network/network';
import { HDKey } from '../hdkey/hdkey';
import { CroSDK } from '../core/cro';
import { CroNetwork, CroSDK } from '../core/cro';
import { Secp256k1KeyPair } from '../keypair/secp256k1';
import { Units } from '../coin/coin';
import { SIGN_MODE } from './types';

const PystaportTestNet: Network = {
chainId: 'chainmaind',
Expand Down Expand Up @@ -90,4 +91,48 @@ describe('Testing edge case Txs with 0 account numbers or 0 sequence', function
'0aa0010a8c010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126c0a2b7463726f316c346778656a793871786c3376786378763776706b34637175387168727a326e667878723270122b7463726f31327a33617774336b6b6830753538686d7738356c6874646736376434347077753632783873611a100a08626173657463726f120432323130120f48656c6c6f2054657374204d656d6f12580a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21030bf28c5f92c336db4703791691fa650fee408690b0a22c5ee4afb7e2508d32a712040a0208011800120410c09a0c1a40f7a03f0510596020b41b3ba7729a248b357a7ab857cf9c7956b2a20d750a859e36f2254829151cbb986f42b0cce1eb0ac17805ffb34b0de860b36062a4338441',
);
});

it('test tx when sequence is 0 in legacy amino json sign mode', function () {
const mnemonic =
'reflect bean panda cost actor arrest speed brave attend finish picnic hundred essay hen bulb cash relax wrist tank claim glide combine notable roof';
const hdKey = HDKey.fromMnemonic(mnemonic);
const privKey = hdKey.derivePrivKey("m/44'/1'/0'/0/0");
const keyPair = Secp256k1KeyPair.fromPrivKey(privKey);

const cro = CroSDK({ network: CroNetwork.Testnet });
const msg = new cro.bank.MsgSend({
fromAddress: new cro.Address(keyPair).account(),
toAddress: 'tcro1ca066afeuj52k3r29je25q0auyr32k4plkh33r',
amount: cro.Coin.fromBaseUnit('1000'),
});

const rawTx = new cro.RawTransaction();
const signableTx = rawTx
.appendMessage(msg)
.addSigner({
publicKey: keyPair.getPubKey(),
accountNumber: new Big('182'),
accountSequence: new Big('0'),
signMode: SIGN_MODE.LEGACY_AMINO_JSON,
})
.setFee(cro.Coin.fromBaseUnit('10000'))
.setGasLimit('100000')
.setMemo('amino test')
.setTimeOutHeight(800000)
.toSignable();

const signature = keyPair.sign(signableTx.toSignDoc(0));
const expectedSignature =
'SvhVHQf6OgHLQtzIGgDX9+u1Y5CiV9UZWVqkw+T+QNZn724s4mRs/+MvNoxKpxi5AqLzoxmh62AwezJIrIQmwA==';
expect(signature.toBase64String()).to.eq(expectedSignature);

const signedTx = signableTx.setSignature(0, signature).toSigned();

const expectedTxHex =
'0a9f010a8c010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126c0a2b7463726f316361303636616665756a35326b337232396a65323571306175797233326b34706c6b68333372122b7463726f316361303636616665756a35326b337232396a65323571306175797233326b34706c6b683333721a100a08626173657463726f120431303030120a616d696e6f20746573741880ea30126b0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2102a412df6c9f0709b5c73b3f35927f6924ca24fb11f8a9c3bf78aacabd81db091612040a02087f180012170a110a08626173657463726f1205313030303010a08d061a404af8551d07fa3a01cb42dcc81a00d7f7ebb56390a257d519595aa4c3e4fe40d667ef6e2ce2646cffe32f368c4aa718b902a2f3a319a1eb60307b3248ac8426c0';
expect(signedTx.getHexEncoded()).to.eq(expectedTxHex);

const expectedTxHash = 'D101675818C82B4C083F9B60EC7532A9CF80B8535D98D2A7C9F215EF1D04DF06';
expect(signedTx.getTxHash()).to.eq(expectedTxHash);
});
});
14 changes: 8 additions & 6 deletions lib/src/transaction/ow.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ export const owRawTransactionOptions = owStrictObject().exactShape({
network: owNetwork(),
});

export const owSignMode = () =>
ow.number.validate((value) => ({
validator: Object.values(SIGN_MODE).includes(value as any),
message: (label) => `Expected ${label} to be one of the sign mode, got \`${value}\``,
}));
const validateSignMode = (value: number) => ({
validator: Object.values(SIGN_MODE).includes(value as any),
message: (label: string) => `Expected ${label} to be one of the sign mode, got \`${value}\``,
});

export const owSignMode = () => ow.number.validate(validateSignMode);
export const owOptionalSignMode = () => ow.optional.number.validate(validateSignMode);

export const owRawTransactionSigner = owStrictObject().exactShape({
publicKey: owBytes(),
accountNumber: owBig(),
accountSequence: owBig(),
signMode: owSignMode(),
signMode: owOptionalSignMode(),
});

export const owTimeoutHeight = ow.string.validate((value) => {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/transaction/raw.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ describe('Transaction', function () {
expect(actualSignerAccountNumbers[0]).to.deep.eq({
publicKey: anySigner.publicKey,
accountNumber: anySigner.accountNumber,
signMode: anySigner.signMode,
});

const anotherSigner = TransactionSignerFactory.build();
Expand All @@ -95,6 +96,7 @@ describe('Transaction', function () {
expect(actualSignerAccountNumbers[1]).to.deep.eq({
publicKey: anotherSigner.publicKey,
accountNumber: anotherSigner.accountNumber,
signMode: anotherSigner.signMode,
});
});
});
Expand Down
15 changes: 11 additions & 4 deletions lib/src/transaction/raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,17 @@ export const rawTransaction = function (config: InitConfigurations) {
signMode = SIGN_MODE.DIRECT;
}

const cosmosSignMode =
signMode === SIGN_MODE.DIRECT
? cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_DIRECT
: cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_LEGACY_AMINO_JSON;
let cosmosSignMode: cosmos.tx.signing.v1beta1.SignMode;
switch (signMode) {
case SIGN_MODE.DIRECT:
cosmosSignMode = cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_DIRECT;
break;
case SIGN_MODE.LEGACY_AMINO_JSON:
cosmosSignMode = cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_LEGACY_AMINO_JSON;
break;
default:
throw new Error(`Unsupported sign mode: ${signMode}`);
}
this.authInfo.signerInfos.push({
publicKey: signer.publicKey,
// TODO: support multisig
Expand Down
41 changes: 32 additions & 9 deletions lib/src/transaction/signable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class SignableTransaction {
const signMode = this.getSignerSignMode(index);
if (signMode === SIGN_MODE.DIRECT) {
return sha256(
makeDirectSignDoc(
makeSignDoc(
this.txRaw.bodyBytes,
this.txRaw.authInfoBytes,
this.network.chainId,
Expand All @@ -95,14 +95,14 @@ export class SignableTransaction {
}
if (signMode === SIGN_MODE.LEGACY_AMINO_JSON) {
return sha256(
makeAminoJSONSignDoc(
makeLegacyAminoSignDoc(
legacyEncodeMsgs(this.txBody.value.messages),
legacyEncodeStdFee(this.authInfo.fee.amount, this.authInfo.fee.gasLimit),
this.network.chainId,
this.txBody.value.memo || '',
this.signerAccounts[index].accountNumber.toString(),
this.authInfo.signerInfos[index].sequence.toString(),
this.txBody.value.timeoutHeight!.toString(),
legacyEncodeTimeoutHeight(this.txBody.value.timeoutHeight?.toString()),
),
);
}
Expand Down Expand Up @@ -286,7 +286,7 @@ const protoEncodePubKey = (pubKey: Bytes): google.protobuf.IAny => {
/**
* Generate SignDoc binary bytes ready to be signed in direct mode
*/
const makeDirectSignDoc = (txBodyBytes: Bytes, authInfoBytes: Bytes, chainId: string, accountNumber: Big): Bytes => {
const makeSignDoc = (txBodyBytes: Bytes, authInfoBytes: Bytes, chainId: string, accountNumber: Big): Bytes => {
const signDoc = omitDefaults({
bodyBytes: txBodyBytes.toUint8Array(),
authInfoBytes: authInfoBytes.toUint8Array(),
Expand All @@ -296,7 +296,7 @@ const makeDirectSignDoc = (txBodyBytes: Bytes, authInfoBytes: Bytes, chainId: st
// Omit encoding the Long value when it's either 0, null or undefined to keep it consistent with backend encoding
// https://github.com/protobufjs/protobuf.js/issues/1138
if (accountNumber.toNumber()) {
signDoc.accountNumber = Long.fromNumber(accountNumber.toNumber(), true);
signDoc.accountNumber = Long.fromNumber(accountNumber.toNumber());
}
const signDocProto = cosmos.tx.v1beta1.SignDoc.create(signDoc);
return Bytes.fromUint8Array(cosmos.tx.v1beta1.SignDoc.encode(signDocProto).finish());
Expand All @@ -313,23 +313,46 @@ const legacyEncodeStdFee = (fee: ICoin | undefined, gas: Big | undefined): legac
};
};

const makeAminoJSONSignDoc = (
const legacyEncodeTimeoutHeight = (timeoutHeight?: string): string | undefined => {
if (typeof timeoutHeight === 'undefined' || timeoutHeight === '0') {
return undefined;
}

return timeoutHeight;
};

const makeLegacyAminoSignDoc = (
msgs: readonly legacyAmino.Msg[],
fee: legacyAmino.StdFee,
chainId: string,
memo: string,
accountNumber: number | string,
sequence: number | string,
timeoutHeight: number | string,
timeoutHeight?: string,
): Bytes => {
const stdSignDoc: legacyAmino.StdSignDoc = {
let encodedTimeoutHeight: string | undefined;
if (typeof timeoutHeight !== 'undefined') {
encodedTimeoutHeight = legacyAmino.Uint53.fromString(timeoutHeight.toString()).toString();
}

const stdSignDocBase: legacyAmino.StdSignDoc = {
chain_id: chainId,
account_number: legacyAmino.Uint53.fromString(accountNumber.toString()).toString(),
sequence: legacyAmino.Uint53.fromString(sequence.toString()).toString(),
fee,
msgs,
memo,
timeout_height: legacyAmino.Uint53.fromString(timeoutHeight.toString()).toString(),
};
let stdSignDoc: legacyAmino.StdSignDoc;
if (typeof timeoutHeight === 'undefined') {
stdSignDoc = {
...stdSignDocBase,
};
} else {
stdSignDoc = {
...stdSignDocBase,
timeout_height: encodedTimeoutHeight,
};
}
return Bytes.fromUint8Array(legacyAmino.toUtf8(legacyAmino.sortedJsonStringify(stdSignDoc)));
};
1 change: 1 addition & 0 deletions lib/src/transaction/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const TransactionSignerFactory = new Factory<TransactionSigner & { keyPai
.attrs({
accountNumber: new Big(chance.integer({ min: 0 })),
accountSequence: new Big(chance.integer({ min: 0 })),
signMode: SIGN_MODE.DIRECT,
});

export type SignableTransactionParamsSuite = {
Expand Down
Loading

0 comments on commit c2bfe09

Please sign in to comment.