From 6fe7ceaba981edb9176f1f578873ab70bbda6d77 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 26 Sep 2023 17:49:54 +0200 Subject: [PATCH 01/15] Updated calculating of deposit address --- typescript/src/deposit.ts | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index 59d8606ec..c95726283 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -1,6 +1,6 @@ import bcoin from "bcoin" import { BigNumber } from "ethers" -import { Stack, script, opcodes } from "bitcoinjs-lib" +import { Stack, payments, script, opcodes } from "bitcoinjs-lib" import { Client as BitcoinClient, decomposeRawTransaction, @@ -10,7 +10,10 @@ import { TransactionHash, isPublicKeyHashLength, } from "./bitcoin" -import { BitcoinNetwork, toBcoinNetwork } from "./bitcoin-network" +import { + BitcoinNetwork, + toBitcoinJsLibNetwork, +} from "./bitcoin-network" import { Bridge, Event, Identifier } from "./chain" import { Hex } from "./hex" @@ -361,10 +364,27 @@ export async function calculateDepositAddress( witness: boolean ): Promise { const scriptHash = await calculateDepositScriptHash(deposit, witness) - const address = witness - ? bcoin.Address.fromWitnessScripthash(scriptHash) - : bcoin.Address.fromScripthash(scriptHash) - return address.toString(toBcoinNetwork(network)) + const bitcoinNetwork = toBitcoinJsLibNetwork(network) + + if (witness) { + // OP_0 + const p2wshOutput = Buffer.concat([ + Buffer.from([opcodes.OP_0, 0x20]), + scriptHash, + ]) + + return payments.p2wsh({ output: p2wshOutput, network: bitcoinNetwork }) + .address! + } + + // OP_HASH160 OP_EQUAL + const p2shOutput = Buffer.concat([ + Buffer.from([opcodes.OP_HASH160, 0x14]), + scriptHash, + Buffer.from([opcodes.OP_EQUAL]), + ]) + + return payments.p2sh({ output: p2shOutput, network: bitcoinNetwork }).address! } /** From ec8a4996ce1cee5e98381c6fde0ea17fd9def5d7 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 26 Sep 2023 18:25:50 +0200 Subject: [PATCH 02/15] Updated calculating of deposit script hash --- typescript/src/bitcoin.ts | 12 ++++++++++++ typescript/src/deposit.ts | 15 ++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 63936a464..75b0f4163 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -524,6 +524,7 @@ export function createKeyRing( * @param text - Text the HASH160 is computed for. * @returns Hash as a 20-byte un-prefixed hex string. */ +// TODO: Make it use Hex for input and return values. export function computeHash160(text: string): string { const sha256Hash = utils.sha256( Hex.from(Buffer.from(text, "hex")).toPrefixedString() @@ -533,6 +534,17 @@ export function computeHash160(text: string): string { return Hex.from(hash160).toString() } +/** + * Computes the single SHA256 for the given text. + * @param text - Text the single SHA256 is computed for. + * @returns Hash as a 32-byte un-prefixed hex string. + */ +// TODO: Consider adding unit tests. +export function computeSha256(text: Hex): Hex { + const hash = utils.sha256(text.toPrefixedString()) + return Hex.from(hash) +} + /** * Computes the double SHA256 for the given text. * @param text - Text the double SHA256 is computed for. diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index c95726283..a30c13bf7 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -9,11 +9,10 @@ import { UnspentTransactionOutput, TransactionHash, isPublicKeyHashLength, + computeSha256, + computeHash160, } from "./bitcoin" -import { - BitcoinNetwork, - toBitcoinJsLibNetwork, -} from "./bitcoin-network" +import { BitcoinNetwork, toBitcoinJsLibNetwork } from "./bitcoin-network" import { Bridge, Event, Identifier } from "./chain" import { Hex } from "./hex" @@ -343,11 +342,13 @@ export async function calculateDepositScriptHash( witness: boolean ): Promise { const script = await assembleDepositScript(deposit) - // Parse the script from HEX string. - const parsedScript = bcoin.Script.fromRaw(Buffer.from(script, "hex")) // If witness script hash should be produced, SHA256 should be used. // Legacy script hash needs HASH160. - return witness ? parsedScript.sha256() : parsedScript.hash160() + if (witness) { + return computeSha256(Hex.from(script)).toBuffer() + } + + return Buffer.from(computeHash160(script), "hex") } /** From c61a158f247532954dd7f3faa483bb86f8291d15 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 28 Sep 2023 17:03:16 +0200 Subject: [PATCH 03/15] Added coinselect library --- typescript/package.json | 1 + typescript/typings.d.ts | 1 + typescript/yarn.lock | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/typescript/package.json b/typescript/package.json index 806339a53..077d75b03 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -30,6 +30,7 @@ "bcoin": "git+https://github.com/keep-network/bcoin.git#5accd32c63e6025a0d35d67739c4a6e84095a1f8", "bitcoinjs-lib": "6.0.2", "bufio": "^1.0.6", + "coinselect": "^3.1.13", "ecpair": "^2.1.0", "electrum-client-js": "git+https://github.com/keep-network/electrum-client-js.git#v0.1.1", "ethers": "^5.5.2", diff --git a/typescript/typings.d.ts b/typescript/typings.d.ts index 2558e6a79..82aae9dd0 100644 --- a/typescript/typings.d.ts +++ b/typescript/typings.d.ts @@ -4,5 +4,6 @@ */ declare module "bcoin" declare module "bufio" +declare module "coinselect" declare module "electrum-client-js" declare module "wif" diff --git a/typescript/yarn.lock b/typescript/yarn.lock index 05897068f..3eb3e707f 100644 --- a/typescript/yarn.lock +++ b/typescript/yarn.lock @@ -3261,6 +3261,11 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +coinselect@^3.1.13: + version "3.1.13" + resolved "https://registry.yarnpkg.com/coinselect/-/coinselect-3.1.13.tgz#b88c7f9659ed4891d1f1d0c894105b1c10ef89a1" + integrity sha512-iJOrKH/7N9gX0jRkxgOHuGjvzvoxUMSeylDhH1sHn+CjLjdin5R0Hz2WEBu/jrZV5OrHcm+6DMzxwu9zb5mSZg== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" From f44861073501423e2c81c9ae430d435ef4857993 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 28 Sep 2023 17:12:15 +0200 Subject: [PATCH 04/15] Created inputs and outputs for the deposit transaction --- typescript/src/deposit.ts | 78 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index a30c13bf7..7fa3b61a3 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -1,8 +1,10 @@ import bcoin from "bcoin" +import coinSelect from "coinselect" import { BigNumber } from "ethers" import { Stack, payments, script, opcodes } from "bitcoinjs-lib" import { Client as BitcoinClient, + createAddressFromPublicKey, decomposeRawTransaction, createKeyRing, RawTransaction, @@ -15,6 +17,8 @@ import { import { BitcoinNetwork, toBitcoinJsLibNetwork } from "./bitcoin-network" import { Bridge, Event, Identifier } from "./chain" import { Hex } from "./hex" +import { ECPairFactory } from "ecpair" +import * as tinysecp from "tiny-secp256k1" // TODO: Replace all properties that are expected to be un-prefixed hexadecimal // strings with a Hex type. @@ -235,6 +239,80 @@ export async function assembleDepositTransaction( } } +// TODO: Description and name change. +export async function assembleDepositTransactionBitcoinJsLib( + bitcoinNetwork: BitcoinNetwork, + deposit: Deposit, + utxos: (UnspentTransactionOutput & RawTransaction)[], + depositorPrivateKey: string, + witness: boolean +): Promise<{ + transactionHash: TransactionHash + depositUtxo: UnspentTransactionOutput + rawTransaction: RawTransaction +}> { + const network = toBitcoinJsLibNetwork(bitcoinNetwork) + // eslint-disable-next-line new-cap + const depositorKeyPair = ECPairFactory(tinysecp).fromWIF( + depositorPrivateKey, + network + ) + const depositorAddress = createAddressFromPublicKey( + Hex.from(depositorKeyPair.publicKey), + BitcoinNetwork.Mainnet + ) + console.log("depositor address", depositorAddress) + + // TODO: Think what this value should be. It seems to be the default + // value used by `bcoin`. Think if it should be fixed or based on the + // network current conditions. + const feeRate = 10000 + + const inputUtxos = [] + for (const utxo of utxos) { + inputUtxos.push({ + txId: utxo.transactionHash.reverse().toBuffer(), + vout: utxo.outputIndex, + value: utxo.value.toNumber(), + }) + } + + const depositAddress = await calculateDepositAddress( + deposit, + bitcoinNetwork, + witness + ) + const depositOutput = { + address: depositAddress, + value: deposit.amount.toNumber(), + } + const { inputs, outputs /* fee*/ } = coinSelect( + inputUtxos, + [depositOutput], + feeRate + ) + + if (!inputs || !outputs) { + throw new Error("Transaction could not be built from the provided UTXOs") + } + + // TODO: Fill with correct values. + const transactionHash = Hex.from("") + const transactionHex = "" + + return { + transactionHash, + depositUtxo: { + transactionHash, + outputIndex: 0, // The deposit is always the first output. + value: deposit.amount, + }, + rawTransaction: { + transactionHex: transactionHex, + }, + } +} + /** * Assembles a Bitcoin locking script for P2(W)SH deposit transaction. * @param deposit - Details of the deposit. From c2cb19fd7f59680e1a08bbb55d1d5e3761f062ac Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 29 Sep 2023 18:32:55 +0200 Subject: [PATCH 05/15] Replaced bcoin with bitcoinjs-lib for deposits --- typescript/src/deposit.ts | 196 +++++++++++++------------------- typescript/test/deposit.test.ts | 57 ++++++---- 2 files changed, 112 insertions(+), 141 deletions(-) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index 7fa3b61a3..8a24fe590 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -1,18 +1,23 @@ -import bcoin from "bcoin" -import coinSelect from "coinselect" import { BigNumber } from "ethers" -import { Stack, payments, script, opcodes } from "bitcoinjs-lib" +import { + Psbt, + Stack, + Transaction, + payments, + script, + opcodes, +} from "bitcoinjs-lib" import { Client as BitcoinClient, createAddressFromPublicKey, decomposeRawTransaction, - createKeyRing, RawTransaction, UnspentTransactionOutput, TransactionHash, isPublicKeyHashLength, computeSha256, computeHash160, + isP2WPKHScript, } from "./bitcoin" import { BitcoinNetwork, toBitcoinJsLibNetwork } from "./bitcoin-network" import { Bridge, Event, Identifier } from "./chain" @@ -124,6 +129,9 @@ export type DepositRevealedEvent = Deposit & { * @param bitcoinClient - Bitcoin client used to interact with the network. * @param witness - If true, a witness (P2WSH) transaction will be created. * Otherwise, a legacy P2SH transaction will be made. + * @param utxos - UTXOs to be used for funding the deposit transaction. + * @param fee - the value that should be subtracted from the sum of the UTXOs + * values and used as the transaction fee. * @returns The outcome consisting of: * - the deposit transaction hash, * - the deposit UTXO produced by this transaction. @@ -132,18 +140,13 @@ export async function submitDepositTransaction( deposit: Deposit, depositorPrivateKey: string, bitcoinClient: BitcoinClient, - witness: boolean + witness: boolean, + utxos: UnspentTransactionOutput[], + fee: BigNumber ): Promise<{ transactionHash: TransactionHash depositUtxo: UnspentTransactionOutput }> { - const depositorKeyRing = createKeyRing(depositorPrivateKey) - const depositorAddress = depositorKeyRing.getAddress("string") - - const utxos = await bitcoinClient.findAllUnspentTransactionOutputs( - depositorAddress - ) - const utxosWithRaw: (UnspentTransactionOutput & RawTransaction)[] = [] for (const utxo of utxos) { const utxoRawTransaction = await bitcoinClient.getRawTransaction( @@ -156,12 +159,16 @@ export async function submitDepositTransaction( }) } + const bitcoinNetwork = await bitcoinClient.getNetwork() + const { transactionHash, depositUtxo, rawTransaction } = - await assembleDepositTransaction( + await assembleDepositTransactionBitcoinJsLib( + bitcoinNetwork, deposit, - utxosWithRaw, depositorPrivateKey, - witness + witness, + utxosWithRaw, + fee ) await bitcoinClient.broadcast(rawTransaction) @@ -172,80 +179,14 @@ export async function submitDepositTransaction( } } -/** - * Assembles a Bitcoin P2(W)SH deposit transaction. - * @param deposit - Details of the deposit. - * @param utxos - UTXOs that should be used as transaction inputs. - * @param depositorPrivateKey - Bitcoin private key of the depositor. - * @param witness - If true, a witness (P2WSH) transaction will be created. - * Otherwise, a legacy P2SH transaction will be made. - * @returns The outcome consisting of: - * - the deposit transaction hash, - * - the deposit UTXO produced by this transaction. - * - the deposit transaction in the raw format - */ -export async function assembleDepositTransaction( - deposit: Deposit, - utxos: (UnspentTransactionOutput & RawTransaction)[], - depositorPrivateKey: string, - witness: boolean -): Promise<{ - transactionHash: TransactionHash - depositUtxo: UnspentTransactionOutput - rawTransaction: RawTransaction -}> { - const depositorKeyRing = createKeyRing(depositorPrivateKey) - const depositorAddress = depositorKeyRing.getAddress("string") - - const inputCoins = utxos.map((utxo) => - bcoin.Coin.fromTX( - bcoin.MTX.fromRaw(utxo.transactionHex, "hex"), - utxo.outputIndex, - -1 - ) - ) - - const transaction = new bcoin.MTX() - - const scriptHash = await calculateDepositScriptHash(deposit, witness) - - transaction.addOutput({ - script: witness - ? bcoin.Script.fromProgram(0, scriptHash) - : bcoin.Script.fromScripthash(scriptHash), - value: deposit.amount.toNumber(), - }) - - await transaction.fund(inputCoins, { - rate: null, // set null explicitly to always use the default value - changeAddress: depositorAddress, - subtractFee: false, // do not subtract the fee from outputs - }) - - transaction.sign(depositorKeyRing) - - const transactionHash = TransactionHash.from(transaction.txid()) - - return { - transactionHash, - depositUtxo: { - transactionHash, - outputIndex: 0, // The deposit is always the first output. - value: deposit.amount, - }, - rawTransaction: { - transactionHex: transaction.toRaw().toString("hex"), - }, - } -} - // TODO: Description and name change. export async function assembleDepositTransactionBitcoinJsLib( bitcoinNetwork: BitcoinNetwork, deposit: Deposit, - utxos: (UnspentTransactionOutput & RawTransaction)[], depositorPrivateKey: string, - witness: boolean + witness: boolean, + utxos: (UnspentTransactionOutput & RawTransaction)[], + fee: BigNumber ): Promise<{ transactionHash: TransactionHash depositUtxo: UnspentTransactionOutput @@ -257,48 +198,67 @@ export async function assembleDepositTransactionBitcoinJsLib( depositorPrivateKey, network ) - const depositorAddress = createAddressFromPublicKey( - Hex.from(depositorKeyPair.publicKey), - BitcoinNetwork.Mainnet - ) - console.log("depositor address", depositorAddress) - // TODO: Think what this value should be. It seems to be the default - // value used by `bcoin`. Think if it should be fixed or based on the - // network current conditions. - const feeRate = 10000 + const psbt = new Psbt({ network }) + psbt.setVersion(1) + + let totalInputValue = BigNumber.from(0) - const inputUtxos = [] for (const utxo of utxos) { - inputUtxos.push({ - txId: utxo.transactionHash.reverse().toBuffer(), - vout: utxo.outputIndex, - value: utxo.value.toNumber(), - }) + const previousOutput = Transaction.fromHex(utxo.transactionHex).outs[ + utxo.outputIndex + ] + const previousOutputValue = previousOutput.value + const previousOutputScript = previousOutput.script + + // TODO: add support for other utxo types along with unit tests for the + // given type. + if (isP2WPKHScript(previousOutputScript)) { + psbt.addInput({ + hash: utxo.transactionHash.reverse().toBuffer(), + index: utxo.outputIndex, + witnessUtxo: { + script: previousOutputScript, + value: previousOutputValue, + }, + }) + + totalInputValue = totalInputValue.add(utxo.value) + } + // Skip UTXO if the type is unsupported. } - const depositAddress = await calculateDepositAddress( - deposit, - bitcoinNetwork, - witness - ) - const depositOutput = { - address: depositAddress, - value: deposit.amount.toNumber(), + // Sum of the selected UTXOs must be equal to or grater than the deposit + // amount plus fee. + const totalExpenses = deposit.amount.add(fee) + if (totalInputValue.lt(totalExpenses)) { + throw new Error("Not enough funds in selected UTXOs to fund transaction") } - const { inputs, outputs /* fee*/ } = coinSelect( - inputUtxos, - [depositOutput], - feeRate - ) - if (!inputs || !outputs) { - throw new Error("Transaction could not be built from the provided UTXOs") + // Add deposit output. + psbt.addOutput({ + address: await calculateDepositAddress(deposit, bitcoinNetwork, witness), + value: deposit.amount.toNumber(), + }) + + // Add change output if needed. + const changeValue = totalInputValue.sub(totalExpenses) + if (changeValue.gt(0)) { + const depositorAddress = createAddressFromPublicKey( + Hex.from(depositorKeyPair.publicKey), + bitcoinNetwork + ) + psbt.addOutput({ + address: depositorAddress, + value: changeValue.toNumber(), + }) } - // TODO: Fill with correct values. - const transactionHash = Hex.from("") - const transactionHex = "" + psbt.signAllInputs(depositorKeyPair) + psbt.finalizeAllInputs() + + const transaction = psbt.extractTransaction() + const transactionHash = TransactionHash.from(transaction.getId()) return { transactionHash, @@ -308,7 +268,7 @@ export async function assembleDepositTransactionBitcoinJsLib( value: deposit.amount, }, rawTransaction: { - transactionHex: transactionHex, + transactionHex: transaction.toHex(), }, } } diff --git a/typescript/test/deposit.test.ts b/typescript/test/deposit.test.ts index 981b6d8f5..f495d46c7 100644 --- a/typescript/test/deposit.test.ts +++ b/typescript/test/deposit.test.ts @@ -17,7 +17,7 @@ import { MockBitcoinClient } from "./utils/mock-bitcoin-client" import bcoin from "bcoin" import { assembleDepositScript, - assembleDepositTransaction, + assembleDepositTransactionBitcoinJsLib, calculateDepositAddress, calculateDepositRefundLocktime, calculateDepositScriptHash, @@ -224,16 +224,8 @@ describe("Deposit", () => { let bitcoinClient: MockBitcoinClient beforeEach(async () => { - bcoin.set("testnet") - bitcoinClient = new MockBitcoinClient() - // Tie used testnetAddress with testnetUTXO to use it during deposit - // creation. - const utxos = new Map() - utxos.set(testnetAddress, [testnetUTXO]) - bitcoinClient.unspentTransactionOutputs = utxos - // Tie testnetTransaction to testnetUTXO. This is needed since // submitDepositTransaction attach transaction data to each UTXO. const rawTransactions = new Map() @@ -246,11 +238,14 @@ describe("Deposit", () => { let depositUtxo: UnspentTransactionOutput beforeEach(async () => { + const fee = BigNumber.from(1520) ;({ transactionHash, depositUtxo } = await submitDepositTransaction( deposit, testnetPrivateKey, bitcoinClient, - true + true, + [testnetUTXO], + fee )) }) @@ -283,11 +278,15 @@ describe("Deposit", () => { let depositUtxo: UnspentTransactionOutput beforeEach(async () => { + const fee = BigNumber.from(1410) + ;({ transactionHash, depositUtxo } = await submitDepositTransaction( deposit, testnetPrivateKey, bitcoinClient, - false + false, + [testnetUTXO], + fee )) }) @@ -323,15 +322,18 @@ describe("Deposit", () => { let transaction: RawTransaction beforeEach(async () => { + const fee = BigNumber.from(1520) ;({ transactionHash, depositUtxo, rawTransaction: transaction, - } = await assembleDepositTransaction( + } = await assembleDepositTransactionBitcoinJsLib( + BitcoinNetwork.Testnet, deposit, - [testnetUTXO], testnetPrivateKey, - true + true, + [testnetUTXO], + fee )) }) @@ -420,15 +422,18 @@ describe("Deposit", () => { let transaction: RawTransaction beforeEach(async () => { + const fee = BigNumber.from(1410) ;({ transactionHash, depositUtxo, rawTransaction: transaction, - } = await assembleDepositTransaction( + } = await assembleDepositTransactionBitcoinJsLib( + BitcoinNetwork.Testnet, deposit, - [testnetUTXO], testnetPrivateKey, - false + false, + [testnetUTXO], + fee )) }) @@ -698,11 +703,14 @@ describe("Deposit", () => { beforeEach(async () => { // Create a deposit transaction. - const result = await assembleDepositTransaction( + const fee = BigNumber.from(1520) + const result = await assembleDepositTransactionBitcoinJsLib( + BitcoinNetwork.Testnet, deposit, - [testnetUTXO], testnetPrivateKey, - true + true, + [testnetUTXO], + fee ) transaction = result.rawTransaction @@ -740,11 +748,14 @@ describe("Deposit", () => { beforeEach(async () => { // Create a deposit transaction. - ;({ depositUtxo } = await assembleDepositTransaction( + const fee = BigNumber.from(1520) + ;({ depositUtxo } = await assembleDepositTransactionBitcoinJsLib( + BitcoinNetwork.Testnet, deposit, - [testnetUTXO], testnetPrivateKey, - true + true, + [testnetUTXO], + fee )) revealedDeposit = { From 3baf5368253a78f774176ce96fdca3ac7a60aea7 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 29 Sep 2023 18:45:04 +0200 Subject: [PATCH 06/15] Function rename and description added --- typescript/src/deposit.ts | 20 +++++++++++++++++--- typescript/test/deposit.test.ts | 10 +++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index 8a24fe590..781d23b7a 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -162,7 +162,7 @@ export async function submitDepositTransaction( const bitcoinNetwork = await bitcoinClient.getNetwork() const { transactionHash, depositUtxo, rawTransaction } = - await assembleDepositTransactionBitcoinJsLib( + await assembleDepositTransaction( bitcoinNetwork, deposit, depositorPrivateKey, @@ -179,8 +179,22 @@ export async function submitDepositTransaction( } } -// TODO: Description and name change. -export async function assembleDepositTransactionBitcoinJsLib( +/** + * Assembles a Bitcoin P2(W)SH deposit transaction. + * @param bitcoinNetwork - The target Bitcoin network (mainnet or testnet). + * @param deposit - Details of the deposit. + * @param depositorPrivateKey - Bitcoin private key of the depositor. + * @param witness - If true, a witness (P2WSH) transaction will be created. + * Otherwise, a legacy P2SH transaction will be made. + * @param utxos - UTXOs that should be used as transaction inputs. + * @param fee - Transaction fee to be subtracted from the sum of the UTXOs' + * values. + * @returns The outcome consisting of: + * - the deposit transaction hash, + * - the deposit UTXO produced by this transaction. + * - the deposit transaction in the raw format + */ +export async function assembleDepositTransaction( bitcoinNetwork: BitcoinNetwork, deposit: Deposit, depositorPrivateKey: string, diff --git a/typescript/test/deposit.test.ts b/typescript/test/deposit.test.ts index f495d46c7..27bb5eadd 100644 --- a/typescript/test/deposit.test.ts +++ b/typescript/test/deposit.test.ts @@ -17,7 +17,7 @@ import { MockBitcoinClient } from "./utils/mock-bitcoin-client" import bcoin from "bcoin" import { assembleDepositScript, - assembleDepositTransactionBitcoinJsLib, + assembleDepositTransaction, calculateDepositAddress, calculateDepositRefundLocktime, calculateDepositScriptHash, @@ -327,7 +327,7 @@ describe("Deposit", () => { transactionHash, depositUtxo, rawTransaction: transaction, - } = await assembleDepositTransactionBitcoinJsLib( + } = await assembleDepositTransaction( BitcoinNetwork.Testnet, deposit, testnetPrivateKey, @@ -427,7 +427,7 @@ describe("Deposit", () => { transactionHash, depositUtxo, rawTransaction: transaction, - } = await assembleDepositTransactionBitcoinJsLib( + } = await assembleDepositTransaction( BitcoinNetwork.Testnet, deposit, testnetPrivateKey, @@ -704,7 +704,7 @@ describe("Deposit", () => { beforeEach(async () => { // Create a deposit transaction. const fee = BigNumber.from(1520) - const result = await assembleDepositTransactionBitcoinJsLib( + const result = await assembleDepositTransaction( BitcoinNetwork.Testnet, deposit, testnetPrivateKey, @@ -749,7 +749,7 @@ describe("Deposit", () => { beforeEach(async () => { // Create a deposit transaction. const fee = BigNumber.from(1520) - ;({ depositUtxo } = await assembleDepositTransactionBitcoinJsLib( + ;({ depositUtxo } = await assembleDepositTransaction( BitcoinNetwork.Testnet, deposit, testnetPrivateKey, From f5a998ab7d2f1a5f340ef543a68c6576a35818ba Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 29 Sep 2023 18:52:19 +0200 Subject: [PATCH 07/15] Break when enough funds collected --- typescript/src/deposit.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index 781d23b7a..b8119e05d 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -216,6 +216,7 @@ export async function assembleDepositTransaction( const psbt = new Psbt({ network }) psbt.setVersion(1) + const totalExpenses = deposit.amount.add(fee) let totalInputValue = BigNumber.from(0) for (const utxo of utxos) { @@ -238,13 +239,15 @@ export async function assembleDepositTransaction( }) totalInputValue = totalInputValue.add(utxo.value) + if (totalInputValue.gte(totalExpenses)) { + break + } } // Skip UTXO if the type is unsupported. } // Sum of the selected UTXOs must be equal to or grater than the deposit // amount plus fee. - const totalExpenses = deposit.amount.add(fee) if (totalInputValue.lt(totalExpenses)) { throw new Error("Not enough funds in selected UTXOs to fund transaction") } From d868a2f6d3601fef64ba8f4daca52f17d57b06a1 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 29 Sep 2023 18:58:38 +0200 Subject: [PATCH 08/15] Removed coinselect package --- typescript/package.json | 1 - typescript/typings.d.ts | 1 - typescript/yarn.lock | 5 ----- 3 files changed, 7 deletions(-) diff --git a/typescript/package.json b/typescript/package.json index 077d75b03..806339a53 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -30,7 +30,6 @@ "bcoin": "git+https://github.com/keep-network/bcoin.git#5accd32c63e6025a0d35d67739c4a6e84095a1f8", "bitcoinjs-lib": "6.0.2", "bufio": "^1.0.6", - "coinselect": "^3.1.13", "ecpair": "^2.1.0", "electrum-client-js": "git+https://github.com/keep-network/electrum-client-js.git#v0.1.1", "ethers": "^5.5.2", diff --git a/typescript/typings.d.ts b/typescript/typings.d.ts index 82aae9dd0..2558e6a79 100644 --- a/typescript/typings.d.ts +++ b/typescript/typings.d.ts @@ -4,6 +4,5 @@ */ declare module "bcoin" declare module "bufio" -declare module "coinselect" declare module "electrum-client-js" declare module "wif" diff --git a/typescript/yarn.lock b/typescript/yarn.lock index 3eb3e707f..05897068f 100644 --- a/typescript/yarn.lock +++ b/typescript/yarn.lock @@ -3261,11 +3261,6 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -coinselect@^3.1.13: - version "3.1.13" - resolved "https://registry.yarnpkg.com/coinselect/-/coinselect-3.1.13.tgz#b88c7f9659ed4891d1f1d0c894105b1c10ef89a1" - integrity sha512-iJOrKH/7N9gX0jRkxgOHuGjvzvoxUMSeylDhH1sHn+CjLjdin5R0Hz2WEBu/jrZV5OrHcm+6DMzxwu9zb5mSZg== - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" From 04e8d75630ac21c1dd19a18ac73e2690c4e2c628 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 29 Sep 2023 19:17:01 +0200 Subject: [PATCH 09/15] Added unit tests for sha256 --- typescript/src/bitcoin.ts | 2 -- typescript/test/bitcoin.test.ts | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 1c3e3751f..d7c04a2cf 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -527,7 +527,6 @@ export function createKeyRing( * @param text - Text the HASH160 is computed for. * @returns Hash as a 20-byte un-prefixed hex string. */ -// TODO: Make it use Hex for input and return values. export function computeHash160(text: string): string { const sha256Hash = utils.sha256( Hex.from(Buffer.from(text, "hex")).toPrefixedString() @@ -542,7 +541,6 @@ export function computeHash160(text: string): string { * @param text - Text the single SHA256 is computed for. * @returns Hash as a 32-byte un-prefixed hex string. */ -// TODO: Consider adding unit tests. export function computeSha256(text: Hex): Hex { const hash = utils.sha256(text.toPrefixedString()) return Hex.from(hash) diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index 0bcc335ba..2c57ed006 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -16,6 +16,7 @@ import { createAddressFromPublicKey, readCompactSizeUint, computeHash160, + computeSha256, computeHash256, isP2PKHScript, isP2WPKHScript, @@ -98,6 +99,21 @@ describe("Bitcoin", () => { }) }) + describe("computeSha256", () => { + it("should compute hash256 correctly", () => { + const hexValue = Hex.from( + "03474444cca71c678f5019d16782b6522735717a94602085b4adf707b465c36ca8" + ) + const expectedSha256 = Hex.from( + "c62e5cb26c97cb52fea7f9965e9ea1f8d41c97773688aa88674e64629fc02901" + ) + + expect(computeSha256(hexValue).toString()).to.be.equal( + expectedSha256.toString() + ) + }) + }) + describe("P2PKH <-> public key hash conversion", () => { const publicKeyHash = "3a38d44d6a0c8d0bb84e0232cc632b7e48c72e0e" const P2WPKHAddress = "bc1q8gudgnt2pjxshwzwqgevccet0eyvwtswt03nuy" From 5a6711b3c8d770bec12b1f52ccb18bb15664a7b4 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 29 Sep 2023 19:33:31 +0200 Subject: [PATCH 10/15] Updated function docstrings --- typescript/src/deposit.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index b8119e05d..078fee832 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -124,6 +124,9 @@ export type DepositRevealedEvent = Deposit & { /** * Submits a deposit by creating and broadcasting a Bitcoin P2(W)SH * deposit transaction. + * @dev UTXOs are selected for transaction funding based on their types. UTXOs + * with unsupported types are skipped. The selection process stops once + * the sum of the chosen UTXOs meets the required funding amount. * @param deposit - Details of the deposit. * @param depositorPrivateKey - Bitcoin private key of the depositor. * @param bitcoinClient - Bitcoin client used to interact with the network. @@ -135,6 +138,8 @@ export type DepositRevealedEvent = Deposit & { * @returns The outcome consisting of: * - the deposit transaction hash, * - the deposit UTXO produced by this transaction. + * @throws {Error} When the sum of the selected UTXOs is insufficient to cover + * the deposit amount and transaction fee. */ export async function submitDepositTransaction( deposit: Deposit, @@ -181,6 +186,9 @@ export async function submitDepositTransaction( /** * Assembles a Bitcoin P2(W)SH deposit transaction. + * @dev UTXOs are selected for transaction funding based on their types. UTXOs + * with unsupported types are skipped. The selection process stops once + * the sum of the chosen UTXOs meets the required funding amount. * @param bitcoinNetwork - The target Bitcoin network (mainnet or testnet). * @param deposit - Details of the deposit. * @param depositorPrivateKey - Bitcoin private key of the depositor. @@ -193,6 +201,8 @@ export async function submitDepositTransaction( * - the deposit transaction hash, * - the deposit UTXO produced by this transaction. * - the deposit transaction in the raw format + * @throws {Error} When the sum of the selected UTXOs is insufficient to cover + * the deposit amount and transaction fee. */ export async function assembleDepositTransaction( bitcoinNetwork: BitcoinNetwork, @@ -226,7 +236,7 @@ export async function assembleDepositTransaction( const previousOutputValue = previousOutput.value const previousOutputScript = previousOutput.script - // TODO: add support for other utxo types along with unit tests for the + // TODO: Add support for other utxo types along with unit tests for the // given type. if (isP2WPKHScript(previousOutputScript)) { psbt.addInput({ @@ -246,7 +256,7 @@ export async function assembleDepositTransaction( // Skip UTXO if the type is unsupported. } - // Sum of the selected UTXOs must be equal to or grater than the deposit + // Sum of the selected UTXOs must be equal to or greater than the deposit // amount plus fee. if (totalInputValue.lt(totalExpenses)) { throw new Error("Not enough funds in selected UTXOs to fund transaction") From ba335fbad9b246603d9ae18d799b60c1cfdc7430 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 3 Oct 2023 15:15:11 +0200 Subject: [PATCH 11/15] Variable rename and description update --- typescript/src/deposit.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index 078fee832..17d9ad369 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -132,7 +132,8 @@ export type DepositRevealedEvent = Deposit & { * @param bitcoinClient - Bitcoin client used to interact with the network. * @param witness - If true, a witness (P2WSH) transaction will be created. * Otherwise, a legacy P2SH transaction will be made. - * @param utxos - UTXOs to be used for funding the deposit transaction. + * @param inputUtxos - UTXOs to be used for funding the deposit transaction. So + * far only P2WPKH UTXO inputs are supported. * @param fee - the value that should be subtracted from the sum of the UTXOs * values and used as the transaction fee. * @returns The outcome consisting of: @@ -146,14 +147,14 @@ export async function submitDepositTransaction( depositorPrivateKey: string, bitcoinClient: BitcoinClient, witness: boolean, - utxos: UnspentTransactionOutput[], + inputUtxos: UnspentTransactionOutput[], fee: BigNumber ): Promise<{ transactionHash: TransactionHash depositUtxo: UnspentTransactionOutput }> { const utxosWithRaw: (UnspentTransactionOutput & RawTransaction)[] = [] - for (const utxo of utxos) { + for (const utxo of inputUtxos) { const utxoRawTransaction = await bitcoinClient.getRawTransaction( utxo.transactionHash ) From 36ad06f55e9da48ceb1bd1ec1726dc327b526638 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 3 Oct 2023 15:18:37 +0200 Subject: [PATCH 12/15] Updated docstring --- typescript/src/deposit.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index 17d9ad369..38f50eda1 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -124,9 +124,6 @@ export type DepositRevealedEvent = Deposit & { /** * Submits a deposit by creating and broadcasting a Bitcoin P2(W)SH * deposit transaction. - * @dev UTXOs are selected for transaction funding based on their types. UTXOs - * with unsupported types are skipped. The selection process stops once - * the sum of the chosen UTXOs meets the required funding amount. * @param deposit - Details of the deposit. * @param depositorPrivateKey - Bitcoin private key of the depositor. * @param bitcoinClient - Bitcoin client used to interact with the network. @@ -139,6 +136,11 @@ export type DepositRevealedEvent = Deposit & { * @returns The outcome consisting of: * - the deposit transaction hash, * - the deposit UTXO produced by this transaction. + * @dev UTXOs are selected for transaction funding based on their types. UTXOs + * with unsupported types are skipped. The selection process stops once + * the sum of the chosen UTXOs meets the required funding amount. + * Be aware that the function will attempt to broadcast the transaction, + * although successful broadcast is not guaranteed. * @throws {Error} When the sum of the selected UTXOs is insufficient to cover * the deposit amount and transaction fee. */ @@ -177,6 +179,9 @@ export async function submitDepositTransaction( fee ) + // Note that `broadcast` may fail silently (i.e. no error will be returned, + // even if the transaction is rejected by other nodes and does not enter the + // mempool, for example due to an UTXO being already spent). await bitcoinClient.broadcast(rawTransaction) return { @@ -187,9 +192,6 @@ export async function submitDepositTransaction( /** * Assembles a Bitcoin P2(W)SH deposit transaction. - * @dev UTXOs are selected for transaction funding based on their types. UTXOs - * with unsupported types are skipped. The selection process stops once - * the sum of the chosen UTXOs meets the required funding amount. * @param bitcoinNetwork - The target Bitcoin network (mainnet or testnet). * @param deposit - Details of the deposit. * @param depositorPrivateKey - Bitcoin private key of the depositor. @@ -202,6 +204,9 @@ export async function submitDepositTransaction( * - the deposit transaction hash, * - the deposit UTXO produced by this transaction. * - the deposit transaction in the raw format +* @dev UTXOs are selected for transaction funding based on their types. UTXOs + * with unsupported types are skipped. The selection process stops once + * the sum of the chosen UTXOs meets the required funding amount. * @throws {Error} When the sum of the selected UTXOs is insufficient to cover * the deposit amount and transaction fee. */ From ce7d1ee7a689ccc66605d9781ef81932e6e2a6de Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 3 Oct 2023 15:36:43 +0200 Subject: [PATCH 13/15] Variable rename and docstring update --- typescript/src/deposit-sweep.ts | 2 +- typescript/src/deposit.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 0627382d7..7c68008eb 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -114,7 +114,7 @@ export async function submitDepositSweepTransaction( * @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 bitcoinNetwork - The target Bitcoin network (mainnet or testnet). + * @param bitcoinNetwork - The target Bitcoin network. * @param fee - Transaction fee to be subtracted from the sum of the UTXOs' * values. * @param walletPrivateKey - Bitcoin private key of the wallet in WIF format. diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index 38f50eda1..f21731fb9 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -136,7 +136,7 @@ export type DepositRevealedEvent = Deposit & { * @returns The outcome consisting of: * - the deposit transaction hash, * - the deposit UTXO produced by this transaction. - * @dev UTXOs are selected for transaction funding based on their types. UTXOs + * @dev UTXOs are selected for transaction funding based on their types. UTXOs * with unsupported types are skipped. The selection process stops once * the sum of the chosen UTXOs meets the required funding amount. * Be aware that the function will attempt to broadcast the transaction, @@ -192,19 +192,20 @@ export async function submitDepositTransaction( /** * Assembles a Bitcoin P2(W)SH deposit transaction. - * @param bitcoinNetwork - The target Bitcoin network (mainnet or testnet). + * @param bitcoinNetwork - The target Bitcoin network. * @param deposit - Details of the deposit. * @param depositorPrivateKey - Bitcoin private key of the depositor. * @param witness - If true, a witness (P2WSH) transaction will be created. * Otherwise, a legacy P2SH transaction will be made. - * @param utxos - UTXOs that should be used as transaction inputs. + * @param inputUtxos - UTXOs to be used for funding the deposit transaction. So + * far only P2WPKH UTXO inputs are supported. * @param fee - Transaction fee to be subtracted from the sum of the UTXOs' * values. * @returns The outcome consisting of: * - the deposit transaction hash, * - the deposit UTXO produced by this transaction. * - the deposit transaction in the raw format -* @dev UTXOs are selected for transaction funding based on their types. UTXOs + * @dev UTXOs are selected for transaction funding based on their types. UTXOs * with unsupported types are skipped. The selection process stops once * the sum of the chosen UTXOs meets the required funding amount. * @throws {Error} When the sum of the selected UTXOs is insufficient to cover @@ -215,7 +216,7 @@ export async function assembleDepositTransaction( deposit: Deposit, depositorPrivateKey: string, witness: boolean, - utxos: (UnspentTransactionOutput & RawTransaction)[], + inputUtxos: (UnspentTransactionOutput & RawTransaction)[], fee: BigNumber ): Promise<{ transactionHash: TransactionHash @@ -235,7 +236,7 @@ export async function assembleDepositTransaction( const totalExpenses = deposit.amount.add(fee) let totalInputValue = BigNumber.from(0) - for (const utxo of utxos) { + for (const utxo of inputUtxos) { const previousOutput = Transaction.fromHex(utxo.transactionHex).outs[ utxo.outputIndex ] From 332b0e772cc2eb9f5a61b678cb51a4ad15d16774 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 3 Oct 2023 15:49:34 +0200 Subject: [PATCH 14/15] Minor refactor --- typescript/src/deposit.ts | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index f21731fb9..f897dadbc 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -416,11 +416,9 @@ export async function calculateDepositScriptHash( const script = await assembleDepositScript(deposit) // If witness script hash should be produced, SHA256 should be used. // Legacy script hash needs HASH160. - if (witness) { - return computeSha256(Hex.from(script)).toBuffer() - } - - return Buffer.from(computeHash160(script), "hex") + return witness + ? computeSha256(Hex.from(script)).toBuffer() + : Buffer.from(computeHash160(script), "hex") } /** @@ -448,16 +446,17 @@ export async function calculateDepositAddress( return payments.p2wsh({ output: p2wshOutput, network: bitcoinNetwork }) .address! - } - - // OP_HASH160 OP_EQUAL - const p2shOutput = Buffer.concat([ - Buffer.from([opcodes.OP_HASH160, 0x14]), - scriptHash, - Buffer.from([opcodes.OP_EQUAL]), - ]) + } else { + // OP_HASH160 OP_EQUAL + const p2shOutput = Buffer.concat([ + Buffer.from([opcodes.OP_HASH160, 0x14]), + scriptHash, + Buffer.from([opcodes.OP_EQUAL]), + ]) - return payments.p2sh({ output: p2shOutput, network: bitcoinNetwork }).address! + return payments.p2sh({ output: p2shOutput, network: bitcoinNetwork }) + .address! + } } /** From e202330c56fc053c664d3396d6b2c63514ffa021 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 3 Oct 2023 15:56:14 +0200 Subject: [PATCH 15/15] Minor update of docstring --- typescript/src/redemption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/src/redemption.ts b/typescript/src/redemption.ts index 528c4627a..7e99a85ea 100644 --- a/typescript/src/redemption.ts +++ b/typescript/src/redemption.ts @@ -256,7 +256,7 @@ async function getWalletRedemptionRequests( * - there is at least one redemption * - the `requestedAmount` in each redemption request is greater than * the sum of its `txFee` and `treasuryFee` - * @param bitcoinNetwork - The target Bitcoin network (mainnet or testnet). + * @param bitcoinNetwork - The target Bitcoin network. * @param walletPrivateKey - The private key of the wallet in the WIF format * @param mainUtxo - The main UTXO of the wallet. Must match the main UTXO held * by the on-chain Bridge contract