From da9856a0702bea8b1fc8fced832f9faa1d05079e Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 25 Sep 2023 18:07:13 +0200 Subject: [PATCH] Updated unit tests for assembleDepositSweepTransaction --- typescript/src/deposit-sweep.ts | 318 +------------------------- typescript/test/data/deposit-sweep.ts | 29 ++- typescript/test/deposit-sweep.test.ts | 51 +++-- 3 files changed, 55 insertions(+), 343 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 4d2c6f994..196361025 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -1,4 +1,3 @@ -import bcoin from "bcoin" import { Transaction, Stack, @@ -15,7 +14,6 @@ import { Client as BitcoinClient, decomposeRawTransaction, isCompressedPublicKey, - createKeyRing, addressFromKeyPair, TransactionHash, computeHash160, @@ -89,8 +87,11 @@ export async function submitDepositSweepTransaction( } } + const bitcoinNetwork = await bitcoinClient.getNetwork() + const { transactionHash, newMainUtxo, rawTransaction } = await assembleDepositSweepTransaction( + bitcoinNetwork, fee, walletPrivateKey, witness, @@ -128,152 +129,6 @@ export async function submitDepositSweepTransaction( * - the sweep transaction in the raw format */ export async function assembleDepositSweepTransaction( - fee: BigNumber, - walletPrivateKey: string, - witness: boolean, - utxos: (UnspentTransactionOutput & RawTransaction)[], - deposits: Deposit[], - mainUtxo?: UnspentTransactionOutput & RawTransaction -): Promise<{ - transactionHash: TransactionHash - newMainUtxo: UnspentTransactionOutput - rawTransaction: RawTransaction -}> { - if (utxos.length < 1) { - throw new Error("There must be at least one deposit UTXO to sweep") - } - - if (utxos.length != deposits.length) { - throw new Error("Number of UTXOs must equal the number of deposit elements") - } - - const walletKeyRing = createKeyRing(walletPrivateKey, witness) - const walletAddress = walletKeyRing.getAddress("string") - - const inputCoins = [] - let totalInputValue = BigNumber.from(0) - - if (mainUtxo) { - inputCoins.push( - bcoin.Coin.fromTX( - bcoin.MTX.fromRaw(mainUtxo.transactionHex, "hex"), - mainUtxo.outputIndex, - -1 - ) - ) - totalInputValue = totalInputValue.add(mainUtxo.value) - } - - for (const utxo of utxos) { - inputCoins.push( - bcoin.Coin.fromTX( - bcoin.MTX.fromRaw(utxo.transactionHex, "hex"), - utxo.outputIndex, - -1 - ) - ) - totalInputValue = totalInputValue.add(utxo.value) - } - - const transaction = new bcoin.MTX() - - transaction.addOutput({ - script: bcoin.Script.fromAddress(walletAddress), - value: totalInputValue.toNumber(), - }) - - await transaction.fund(inputCoins, { - changeAddress: walletAddress, - hardFee: fee.toNumber(), - subtractFee: true, - }) - - if (transaction.outputs.length != 1) { - throw new Error("Deposit sweep transaction must have only one output") - } - - // UTXOs must be mapped to deposits, as `fund` may arrange inputs in any - // order - const utxosWithDeposits: (UnspentTransactionOutput & - RawTransaction & - Deposit)[] = utxos.map((utxo, index) => ({ - ...utxo, - ...deposits[index], - })) - - for (let i = 0; i < transaction.inputs.length; i++) { - const previousOutpoint = transaction.inputs[i].prevout - const previousOutput = transaction.view.getOutput(previousOutpoint) - const previousScript = previousOutput.script - - // P2(W)PKH (main UTXO) - if (previousScript.isPubkeyhash() || previousScript.isWitnessPubkeyhash()) { - await signMainUtxoInput(transaction, i, walletKeyRing) - continue - } - - const utxoWithDeposit = utxosWithDeposits.find( - (u) => - u.transactionHash.toString() === previousOutpoint.txid() && - u.outputIndex == previousOutpoint.index - ) - if (!utxoWithDeposit) { - throw new Error("Unknown input") - } - - if (previousScript.isScripthash()) { - // P2SH (deposit UTXO) - await signP2SHDepositInput(transaction, i, utxoWithDeposit, walletKeyRing) - } else if (previousScript.isWitnessScripthash()) { - // P2WSH (deposit UTXO) - await signP2WSHDepositInput( - transaction, - i, - utxoWithDeposit, - walletKeyRing - ) - } else { - throw new Error("Unsupported UTXO script type") - } - } - - const transactionHash = TransactionHash.from(transaction.txid()) - - return { - transactionHash, - newMainUtxo: { - transactionHash, - outputIndex: 0, // There is only one output. - value: BigNumber.from(transaction.outputs[0].value), - }, - rawTransaction: { - transactionHex: transaction.toRaw().toString("hex"), - }, - } -} - -/** - * Assembles a Bitcoin P2WPKH deposit sweep transaction. - * @dev The caller is responsible for ensuring the provided UTXOs are correctly - * formed, can be spent by the wallet and their combined value is greater - * then the fee. - * @param fee - the value that should be subtracted from the sum of the UTXOs - * values and used as the transaction fee. - * @param walletPrivateKey - Bitcoin private key of the wallet in WIF format. - * @param witness - The parameter used to decide about the type of the new main - * UTXO output. P2WPKH if `true`, P2PKH if `false`. - * @param utxos - UTXOs from new deposit transactions. Must be P2(W)SH. - * @param deposits - Array of deposits. Each element corresponds to UTXO. - * The number of UTXOs and deposit elements must equal. - * @param mainUtxo - main UTXO of the wallet, which is a P2WKH UTXO resulting - * from the previous wallet transaction (optional). - * @returns The outcome consisting of: - * - the sweep transaction hash, - * - the new wallet's main UTXO produced by this transaction. - * - the sweep transaction in the raw format - */ -// TODO: Rename once it's finished. -export async function assembleDepositSweepTransactionBitcoinJsLib( bitcoinNetwork: BitcoinNetwork, fee: BigNumber, walletPrivateKey: string, @@ -326,14 +181,13 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( ) } - // TODO: Verify that output script is properly created from both testnet - // and mainnet addresses. // Add transaction output. const scriptPubKey = address.toOutputScript(walletAddress, network) transaction.addOutput(scriptPubKey, totalInputValue.toNumber()) // UTXOs must be mapped to deposits, as `fund` may arrange inputs in any // order + // TODO: Most likely remove. const utxosWithDeposits: (UnspentTransactionOutput & RawTransaction & Deposit)[] = utxos.map((utxo, index) => ({ @@ -353,7 +207,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( // P2(W)PKH (main UTXO) if (isP2PKH(previousOutputScript) || isP2WPKH(previousOutputScript)) { - await signMainUtxoInputBitcoinJsLib( + await signMainUtxoInput( transaction, i, previousOutputScript, @@ -376,7 +230,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( if (isP2SH(previousOutputScript)) { // P2SH (deposit UTXO) - await signP2SHDepositInputBitcoinJsLib( + await signP2SHDepositInput( transaction, i, utxoWithDeposit, @@ -385,7 +239,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( ) } else if (isP2WSH(previousOutputScript)) { // P2WSH (deposit UTXO) - await signP2WSHDepositInputBitcoinJsLib( + await signP2WSHDepositInput( transaction, i, utxoWithDeposit, @@ -440,104 +294,7 @@ function findPreviousOutput( throw new Error("Could not find previous output") } -/** - * Creates script for the transaction input at the given index and signs the - * input. - * @param transaction - Mutable transaction containing the input to be signed. - * @param inputIndex - Index that points to the input to be signed. - * @param walletKeyRing - Key ring created using the wallet's private key. - * @returns Empty promise. - */ async function signMainUtxoInput( - transaction: any, - inputIndex: number, - walletKeyRing: any -) { - const previousOutpoint = transaction.inputs[inputIndex].prevout - const previousOutput = transaction.view.getOutput(previousOutpoint) - if (!walletKeyRing.ownOutput(previousOutput)) { - throw new Error("UTXO does not belong to the wallet") - } - // Build script and set it as input's witness - transaction.scriptInput(inputIndex, previousOutput, walletKeyRing) - // Build signature and add it in front of script in input's witness - transaction.signInput(inputIndex, previousOutput, walletKeyRing) -} - -/** - * Creates and sets `scriptSig` for the transaction input at the given index by - * combining signature, wallet public key and deposit script. - * @param transaction - Mutable transaction containing the input to be signed. - * @param inputIndex - Index that points to the input to be signed. - * @param deposit - Data of the deposit. - * @param walletKeyRing - Key ring created using the wallet's private key. - * @returns Empty promise. - */ -async function signP2SHDepositInput( - transaction: any, - inputIndex: number, - deposit: Deposit, - walletKeyRing: any -): Promise { - const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignData(transaction, inputIndex, deposit, walletKeyRing) - - const signature: Buffer = transaction.signature( - inputIndex, - depositScript, - previousOutputValue, - walletKeyRing.privateKey, - bcoin.Script.hashType.ALL, - 0 // legacy sighash version - ) - const scriptSig = new bcoin.Script() - scriptSig.clear() - scriptSig.pushData(signature) - scriptSig.pushData(Buffer.from(walletPublicKey, "hex")) - scriptSig.pushData(depositScript.toRaw()) - scriptSig.compile() - - transaction.inputs[inputIndex].script = scriptSig -} - -/** - * Creates and sets witness script for the transaction input at the given index - * by combining signature, wallet public key and deposit script. - * @param transaction - Mutable transaction containing the input to be signed. - * @param inputIndex - Index that points to the input to be signed. - * @param deposit - Data of the deposit. - * @param walletKeyRing - Key ring created using the wallet's private key. - * @returns Empty promise. - */ -async function signP2WSHDepositInput( - transaction: any, - inputIndex: number, - deposit: Deposit, - walletKeyRing: any -): Promise { - const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignData(transaction, inputIndex, deposit, walletKeyRing) - - const signature: Buffer = transaction.signature( - inputIndex, - depositScript, - previousOutputValue, - walletKeyRing.privateKey, - bcoin.Script.hashType.ALL, - 1 // segwit sighash version - ) - - const witness = new bcoin.Witness() - witness.clear() - witness.pushData(signature) - witness.pushData(Buffer.from(walletPublicKey, "hex")) - witness.pushData(depositScript.toRaw()) - witness.compile() - - transaction.inputs[inputIndex].witness = witness -} - -async function signMainUtxoInputBitcoinJsLib( transaction: Transaction, inputIndex: number, prevOutScript: Buffer, @@ -602,8 +359,8 @@ async function signMainUtxoInputBitcoinJsLib( } } -// TODO: Rename once the function is implemented. -async function signP2SHDepositInputBitcoinJsLib( +// TODO: Description. +async function signP2SHDepositInput( transaction: Transaction, inputIndex: number, deposit: Deposit, @@ -631,8 +388,8 @@ async function signP2SHDepositInputBitcoinJsLib( transaction.ins[inputIndex].script = script.compile(scriptSig) } -// TODO: Rename once the function is implemented. -async function signP2WSHDepositInputBitcoinJsLib( +// TODO: Description. +async function signP2WSHDepositInput( transaction: Transaction, inputIndex: number, deposit: Deposit, @@ -701,59 +458,6 @@ async function prepareInputSignDataBitcoinIsLib( } } -/** - * Creates data needed to sign a deposit input. - * @param transaction - Mutable transaction containing the input. - * @param inputIndex - Index that points to the input. - * @param deposit - Data of the deposit. - * @param walletKeyRing - Key ring created using the wallet's private key. - * @returns Data needed to sign the input. - */ -async function prepareInputSignData( - transaction: any, - inputIndex: number, - deposit: Deposit, - walletKeyRing: any -): Promise<{ - walletPublicKey: string - depositScript: any - previousOutputValue: number -}> { - const previousOutpoint = transaction.inputs[inputIndex].prevout - const previousOutput = transaction.view.getOutput(previousOutpoint) - - if (previousOutput.value != deposit.amount.toNumber()) { - throw new Error("Mismatch between amount in deposit and deposit tx") - } - - const walletPublicKey = walletKeyRing.getPublicKey("hex") - if ( - computeHash160(walletKeyRing.getPublicKey("hex")) != - deposit.walletPublicKeyHash - ) { - throw new Error( - "Wallet public key does not correspond to wallet private key" - ) - } - - if (!isCompressedPublicKey(walletPublicKey)) { - throw new Error("Wallet public key must be compressed") - } - - // eslint-disable-next-line no-unused-vars - const { amount, vault, ...depositScriptParameters } = deposit - - const depositScript = bcoin.Script.fromRaw( - Buffer.from(await assembleDepositScript(depositScriptParameters), "hex") - ) - - return { - walletPublicKey, - depositScript: depositScript, - previousOutputValue: previousOutput.value, - } -} - /** * Prepares the proof of a deposit sweep transaction and submits it to the * Bridge on-chain contract. diff --git a/typescript/test/data/deposit-sweep.ts b/typescript/test/data/deposit-sweep.ts index 59e3f5770..74e001b6b 100644 --- a/typescript/test/data/deposit-sweep.ts +++ b/typescript/test/data/deposit-sweep.ts @@ -350,24 +350,23 @@ export const depositSweepWithNonWitnessMainUtxoAndWitnessOutput: DepositSweepTes witness: true, expectedSweep: { transactionHash: TransactionHash.from( - "7831d0dfde7e160f3b9bb66c433710f0d3110d73ea78b9db65e81c091a6718a0" + "1933781f01f27f086c3a31c4a53035ebc7c4688e1f4b316babefa8f6dab77dc2" ), transaction: { transactionHex: - "01000000000102173a201f597a2c8ccd7842303a6653bb87437fb08dae671731a0" + - "75403b32a2fd0000000000ffffffffe19612be756bf7e740b47bec0e24845089ac" + - "e48c78d473cb34949b3007c4a2c8000000006a47304402204382deb051f9f3e2b5" + - "39e4bac2d1a50faf8d66bc7a3a3f3d286dabd96d92b58b02207c74c6aaf48e25d0" + - "7e02bb4039606d77ecfd80c492c050ab2486af6027fc2d5a012103989d253b17a6" + - "a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9ffffffff010884" + - "0000000000001600148db50eb52063ea9d98b3eac91489a90f738986f603483045" + - "022100c52bc876cdee80a3061ace3ffbce5e860942d444cd38e00e5f63fd8e818d" + - "7e7c022040a7017bb8213991697705e7092c481526c788a4731d06e582dc1c57be" + - "d7243b012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc2" + - "9dcf8581d95c14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d" + - "000395237576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776" + - "a914e257eccafbc07c381642ce6e7e55120fb077fbed880448f2b262b175ac6800" + - "00000000", + "01000000000102e19612be756bf7e740b47bec0e24845089ace48c78d473cb34949" + + "b3007c4a2c8000000006a473044022013787be70eda0620002fa55c92abfcd32257" + + "d64fa652dd32bac65d705162a95902203407fea1abc99a9273ead3179ce60f60a34" + + "33fb2e93f58569e4bee9f63c0d679012103989d253b17a6a0f41838b84ff0d20e88" + + "98f9d7b1a98f2564da4cc29dcf8581d9ffffffff173a201f597a2c8ccd7842303a6" + + "653bb87437fb08dae671731a075403b32a2fd0000000000ffffffff010884000000" + + "0000001600148db50eb52063ea9d98b3eac91489a90f738986f6000348304502210" + + "0804f0fa989d632cda99a24159e28b8d31d4033c2d5de47d8207ea2767273d10a02" + + "20278e82d0714867b31eb013762306e2b97c2c1cc74b8135bee78d565e72ee630e0" + + "12103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581" + + "d95c14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d000395237" + + "576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776a914e257ec" + + "cafbc07c381642ce6e7e55120fb077fbed880448f2b262b175ac6800000000", }, }, } diff --git a/typescript/test/deposit-sweep.test.ts b/typescript/test/deposit-sweep.test.ts index 494aa24c5..0d429efd3 100644 --- a/typescript/test/deposit-sweep.test.ts +++ b/typescript/test/deposit-sweep.test.ts @@ -31,6 +31,7 @@ import { submitDepositSweepProof, submitDepositSweepTransaction, } from "../src/deposit-sweep" +import { BitcoinNetwork } from "../src/bitcoin-network" describe("Sweep", () => { const fee = BigNumber.from(1600) @@ -39,8 +40,6 @@ describe("Sweep", () => { let bitcoinClient: MockBitcoinClient beforeEach(async () => { - bcoin.set("testnet") - bitcoinClient = new MockBitcoinClient() }) @@ -379,6 +378,7 @@ describe("Sweep", () => { newMainUtxo, rawTransaction: transaction, } = await assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -508,6 +508,7 @@ describe("Sweep", () => { newMainUtxo, rawTransaction: transaction, } = await assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -658,6 +659,7 @@ describe("Sweep", () => { newMainUtxo, rawTransaction: transaction, } = await assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -686,25 +688,7 @@ describe("Sweep", () => { // Validate inputs. expect(txJSON.inputs.length).to.be.equal(2) - const p2wshInput = txJSON.inputs[0] - expect(p2wshInput.prevout.hash).to.be.equal( - depositSweepWithNonWitnessMainUtxoAndWitnessOutput.deposits[0].utxo.transactionHash.toString() - ) - expect(p2wshInput.prevout.index).to.be.equal( - depositSweepWithNonWitnessMainUtxoAndWitnessOutput.deposits[0] - .utxo.outputIndex - ) - // Transaction should be signed. As it's a SegWit input, the `witness` - // field should be filled, while the `script` field should be empty. - expect(p2wshInput.witness.length).to.be.greaterThan(0) - expect(p2wshInput.script.length).to.be.equal(0) - // Input's address should be set to the address generated from deposit - // script hash - expect(p2wshInput.address).to.be.equal( - "tb1qk8urugnf08wfle6wslmdxq7mkz9z0gw8e6gkvspn7dx87tfpfntshdm7qr" - ) - - const p2pkhInput = txJSON.inputs[1] // main UTXO + const p2pkhInput = txJSON.inputs[0] // main UTXO expect(p2pkhInput.prevout.hash).to.be.equal( depositSweepWithNonWitnessMainUtxoAndWitnessOutput.mainUtxo.transactionHash.toString() ) @@ -722,6 +706,24 @@ describe("Sweep", () => { "mtSEUCE7G8om9zJttG9twtjoiSsUz7QnY9" ) + const p2wshInput = txJSON.inputs[1] + expect(p2wshInput.prevout.hash).to.be.equal( + depositSweepWithNonWitnessMainUtxoAndWitnessOutput.deposits[0].utxo.transactionHash.toString() + ) + expect(p2wshInput.prevout.index).to.be.equal( + depositSweepWithNonWitnessMainUtxoAndWitnessOutput.deposits[0] + .utxo.outputIndex + ) + // Transaction should be signed. As it's a SegWit input, the `witness` + // field should be filled, while the `script` field should be empty. + expect(p2wshInput.witness.length).to.be.greaterThan(0) + expect(p2wshInput.script.length).to.be.equal(0) + // Input's address should be set to the address generated from deposit + // script hash + expect(p2wshInput.address).to.be.equal( + "tb1qk8urugnf08wfle6wslmdxq7mkz9z0gw8e6gkvspn7dx87tfpfntshdm7qr" + ) + // Validate outputs. expect(txJSON.outputs.length).to.be.equal(1) @@ -791,6 +793,7 @@ describe("Sweep", () => { newMainUtxo, rawTransaction: transaction, } = await assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -878,6 +881,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, true, @@ -906,6 +910,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -931,6 +936,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, true, @@ -970,6 +976,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, true, @@ -994,6 +1001,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, anotherPrivateKey, true, @@ -1027,6 +1035,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, true,