diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index e144a09e..4352341b 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -24,3 +24,4 @@ export { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./pubkey"; +export { findSequenceForSignedTx } from "./sequence"; diff --git a/packages/sdk/src/sequence.spec.ts b/packages/sdk/src/sequence.spec.ts new file mode 100644 index 00000000..e5d8f4cd --- /dev/null +++ b/packages/sdk/src/sequence.spec.ts @@ -0,0 +1,30 @@ +import { findSequenceForSignedTx } from "./sequence"; +import response1 from "./testdata/txresponse1.json"; +import response2 from "./testdata/txresponse2.json"; +import response3 from "./testdata/txresponse3.json"; + +// Those values must match ./testdata/txresponse*.json +const chainId = "testing"; +const accountNumber = 4; + +describe("sequence", () => { + describe("findSequenceForSignedTx", () => { + it("works", async () => { + const current = 100; // what we get from GET /auth/accounts/{address} + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, current)).toEqual(10); + // We know response3.height > response1.height, so the sequence must be at least 10+1 + expect(await findSequenceForSignedTx(response3.tx, chainId, accountNumber, current, 11)).toEqual(19); + // We know response3.height > response2.height > response1.height, so the sequence must be at least 10+1 and smaller than 19 + expect(await findSequenceForSignedTx(response2.tx, chainId, accountNumber, 19, 11)).toEqual(13); + }); + + it("returns undefined when sequence is not in range", async () => { + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 5)).toBeUndefined(); + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 20, 11)).toBeUndefined(); + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 20, 50)).toBeUndefined(); + + // upper bound is not included in the possible results + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 10)).toBeUndefined(); + }); + }); +}); diff --git a/packages/sdk/src/sequence.ts b/packages/sdk/src/sequence.ts new file mode 100644 index 00000000..8a53e897 --- /dev/null +++ b/packages/sdk/src/sequence.ts @@ -0,0 +1,46 @@ +import { Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto"; + +import { makeSignBytes } from "./encoding"; +import { decodeSignature } from "./signature"; +import { CosmosSdkTx } from "./types"; + +/** + * Serach for sequence s with `min` <= `s` < `upperBound` to find the sequence that was used to sign the transaction + * + * @param tx The signed transaction + * @param chainId The chain ID for which this transaction was signed + * @param accountNumber The account number for which this transaction was signed + * @param upperBound The upper bound for the testing, i.e. sequence must be lower than this value + * @param min The lowest sequence that is tested + * + * @returns the sequence if a match was found and undefined otherwise + */ +export async function findSequenceForSignedTx( + tx: CosmosSdkTx, + chainId: string, + accountNumber: number, + upperBound: number, + min = 1, +): Promise { + const firstSignature = tx.value.signatures.find(() => true); + if (!firstSignature) throw new Error("Signature missing in tx"); + + const { pubkey, signature } = decodeSignature(firstSignature); + const secp256keSignature = new Secp256k1Signature(signature.slice(0, 32), signature.slice(32, 64)); + + for (let s = min; s < upperBound; s++) { + // console.log(`Trying sequence ${s}`); + const signBytes = makeSignBytes( + tx.value.msg, + tx.value.fee, + chainId, + tx.value.memo || "", + accountNumber, + s, + ); + const prehashed = new Sha256(signBytes).digest(); + const valid = await Secp256k1.verifySignature(secp256keSignature, prehashed, pubkey); + if (valid) return s; + } + return undefined; +} diff --git a/packages/sdk/src/testdata/txresponse1.json b/packages/sdk/src/testdata/txresponse1.json new file mode 100644 index 00000000..703ea696 --- /dev/null +++ b/packages/sdk/src/testdata/txresponse1.json @@ -0,0 +1,57 @@ +{ + "height": "15888", + "txhash": "672DEDE8EF4DE8B5818959F417CCA357079D4D7A19C4B65443C7FBF8176AABF9", + "raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]", + "logs": [ + { + "msg_index": 0, + "log": "", + "events": [ + { + "type": "message", + "attributes": [ + { "key": "action", "value": "send" }, + { "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" }, + { "key": "module", "value": "bank" } + ] + }, + { + "type": "transfer", + "attributes": [ + { "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" }, + { "key": "amount", "value": "75000ucosm" } + ] + } + ] + } + ], + "gas_wanted": "200000", + "gas_used": "65407", + "tx": { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgSend", + "value": { + "from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + "to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2", + "amount": [{ "denom": "ucosm", "amount": "75000" }] + } + } + ], + "fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ" + }, + "signature": "US7oH8S/8TxVrtBQkOhHxAM+oDB2spNAEawgh6H8CCFLRMOJK+uvQZZ6ceUgUsvDbxwCz7re1RU272fymMYRZQ==" + } + ], + "memo": "My first payment" + } + }, + "timestamp": "2020-02-14T11:25:55Z" +} diff --git a/packages/sdk/src/testdata/txresponse2.json b/packages/sdk/src/testdata/txresponse2.json new file mode 100644 index 00000000..0d1a37f9 --- /dev/null +++ b/packages/sdk/src/testdata/txresponse2.json @@ -0,0 +1,57 @@ +{ + "height": "16456", + "txhash": "7BFE4B93AF190F60132C62D08FDF50BE462FBCE374EB13D3FD0C32461E771EC0", + "raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]", + "logs": [ + { + "msg_index": 0, + "log": "", + "events": [ + { + "type": "message", + "attributes": [ + { "key": "action", "value": "send" }, + { "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" }, + { "key": "module", "value": "bank" } + ] + }, + { + "type": "transfer", + "attributes": [ + { "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" }, + { "key": "amount", "value": "75000ucosm" } + ] + } + ] + } + ], + "gas_wanted": "200000", + "gas_used": "65407", + "tx": { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgSend", + "value": { + "from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + "to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2", + "amount": [{ "denom": "ucosm", "amount": "75000" }] + } + } + ], + "fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ" + }, + "signature": "ltvd9Rb3RF4zjbUVrpDpkok34g+py7XR8ZcM0tZUYRxxVdcMEin010x+ZFd/mOuutPj9fDmSENnienc/yi4msw==" + } + ], + "memo": "My first payment" + } + }, + "timestamp": "2020-02-14T11:35:41Z" +} diff --git a/packages/sdk/src/testdata/txresponse3.json b/packages/sdk/src/testdata/txresponse3.json new file mode 100644 index 00000000..8ffd0727 --- /dev/null +++ b/packages/sdk/src/testdata/txresponse3.json @@ -0,0 +1,57 @@ +{ + "height": "20730", + "txhash": "625BC75E697F73DA037387C34002BB2F682E7ACDCC4E015D3E90420516C6D0C8", + "raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]", + "logs": [ + { + "msg_index": 0, + "log": "", + "events": [ + { + "type": "message", + "attributes": [ + { "key": "action", "value": "send" }, + { "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" }, + { "key": "module", "value": "bank" } + ] + }, + { + "type": "transfer", + "attributes": [ + { "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" }, + { "key": "amount", "value": "75000ucosm" } + ] + } + ] + } + ], + "gas_wanted": "200000", + "gas_used": "65407", + "tx": { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgSend", + "value": { + "from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + "to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2", + "amount": [{ "denom": "ucosm", "amount": "75000" }] + } + } + ], + "fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ" + }, + "signature": "eOFGl1tIHDMv3JdCK9fRSikVbYUD8+B0ksb3dJFya8MPYgpEpdSA7zZc+5n/cW6LR/BJdib4nqmJQv1yD9lm3g==" + } + ], + "memo": "My first payment" + } + }, + "timestamp": "2020-02-14T12:48:56Z" +} diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index 0a4c02d7..dcb618cc 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -23,3 +23,4 @@ export { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./pubkey"; +export { findSequenceForSignedTx } from "./sequence"; diff --git a/packages/sdk/types/sequence.d.ts b/packages/sdk/types/sequence.d.ts new file mode 100644 index 00000000..70d38469 --- /dev/null +++ b/packages/sdk/types/sequence.d.ts @@ -0,0 +1,19 @@ +import { CosmosSdkTx } from "./types"; +/** + * Serach for sequence s with `min` <= `s` < `upperBound` to find the sequence that was used to sign the transaction + * + * @param tx The signed transaction + * @param chainId The chain ID for which this transaction was signed + * @param accountNumber The account number for which this transaction was signed + * @param upperBound The upper bound for the testing, i.e. sequence must be lower than this value + * @param min The lowest sequence that is tested + * + * @returns the sequence if a match was found and undefined otherwise + */ +export declare function findSequenceForSignedTx( + tx: CosmosSdkTx, + chainId: string, + accountNumber: number, + upperBound: number, + min?: number, +): Promise;