Skip to content

Commit

Permalink
Integrate bitcoinjs-lib changes around deposits
Browse files Browse the repository at this point in the history
Here we pull changes from #702
  • Loading branch information
lukasz-zimnoch committed Oct 10, 2023
1 parent 1b21713 commit 6647d69
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 61 deletions.
13 changes: 13 additions & 0 deletions typescript/src/lib/bitcoin/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function computeHash160(text: string): string {
* Computes the double SHA256 for the given text.
* @param text - Text the double SHA256 is computed for.
* @returns Hash as a 32-byte un-prefixed hex string.
* @dev Do not confuse it with computeSha256 which computes single SHA256.
*/
function computeHash256(text: Hex): Hex {
const firstHash = utils.sha256(text.toPrefixedString())
Expand All @@ -36,11 +37,23 @@ function hashLEToBigNumber(hash: Hex): BigNumber {
return BigNumber.from(hash.reverse().toPrefixedString())
}

/**
* 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.
* @dev Do not confuse it with computeHash256 which computes double SHA256.
*/
function computeSha256(text: Hex): Hex {
const hash = utils.sha256(text.toPrefixedString())
return Hex.from(hash)
}

/**
* Utility functions allowing to deal with Bitcoin hashes.
*/
export const BitcoinHashUtils = {
computeHash160,
computeHash256,
hashLEToBigNumber,
computeSha256,
}
42 changes: 32 additions & 10 deletions typescript/src/services/deposits/deposit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import {
TBTCContracts,
validateDepositReceipt,
} from "../../lib/contracts"
import bcoin from "bcoin"
import {
BitcoinClient,
BitcoinHashUtils,
BitcoinNetwork,
BitcoinTxOutpoint,
BitcoinUtxo,
extractBitcoinRawTxVectors,
toBcoinNetwork,
toBitcoinJsLibNetwork,
} from "../../lib/bitcoin"
import { Stack, script, opcodes } from "bitcoinjs-lib"
import { payments, Stack, script, opcodes } from "bitcoinjs-lib"
import { Hex } from "../../lib/utils"

/**
* Component representing an instance of the tBTC v2 deposit process.
Expand Down Expand Up @@ -181,11 +182,11 @@ export class DepositScript {
*/
async getHash(): Promise<Buffer> {
const script = await this.getPlainText()
// 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 this.witness ? parsedScript.sha256() : parsedScript.hash160()
return this.witness
? BitcoinHashUtils.computeSha256(Hex.from(script)).toBuffer()
: Buffer.from(BitcoinHashUtils.computeHash160(script), "hex")
}

/**
Expand Down Expand Up @@ -226,9 +227,30 @@ export class DepositScript {
*/
async deriveAddress(bitcoinNetwork: BitcoinNetwork): Promise<string> {
const scriptHash = await this.getHash()
const address = this.witness
? bcoin.Address.fromWitnessScripthash(scriptHash)
: bcoin.Address.fromScripthash(scriptHash)
return address.toString(toBcoinNetwork(bitcoinNetwork))

const bitcoinJsLibNetwork = toBitcoinJsLibNetwork(bitcoinNetwork)

if (this.witness) {
// OP_0 <hash-length> <hash>
const p2wshOutput = Buffer.concat([
Buffer.from([opcodes.OP_0, 0x20]),
scriptHash,
])

return payments.p2wsh({
output: p2wshOutput,
network: bitcoinJsLibNetwork,
}).address!
} else {
// OP_HASH160 <hash-length> <hash> OP_EQUAL
const p2shOutput = Buffer.concat([
Buffer.from([opcodes.OP_HASH160, 0x14]),
scriptHash,
Buffer.from([opcodes.OP_EQUAL]),
])

return payments.p2sh({ output: p2shOutput, network: bitcoinJsLibNetwork })
.address!
}
}
}
144 changes: 107 additions & 37 deletions typescript/src/services/deposits/funding.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { DepositScript } from "./deposit"
import {
BitcoinAddressConverter,
BitcoinClient,
BitcoinPrivateKeyUtils,
BitcoinNetwork,
BitcoinRawTx,
BitcoinScriptUtils,
BitcoinTxHash,
BitcoinUtxo,
toBitcoinJsLibNetwork,
} from "../../lib/bitcoin"
import { BigNumber } from "ethers"
import bcoin from "bcoin"
import { Psbt, Transaction } from "bitcoinjs-lib"
import { ECPairFactory } from "ecpair"
import * as tinysecp from "tiny-secp256k1"
import { Hex } from "../../lib/utils"

/**
* Component allowing to craft and submit the Bitcoin funding transaction using
Expand Down Expand Up @@ -37,56 +43,104 @@ export class DepositFunding {
* can be unlocked using the depositor's private key. It is also
* caller's responsibility to ensure the given deposit is funded exactly
* once.
* @param bitcoinNetwork The target Bitcoin network.
* @param amount Deposit amount in satoshis.
* @param inputUtxos 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.
* @param depositorPrivateKey Bitcoin private key of the depositor. Must
* be able to unlock input UTXOs.
* @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
* 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.
*/
async assembleTransaction(
bitcoinNetwork: BitcoinNetwork,
amount: BigNumber,
inputUtxos: (BitcoinUtxo & BitcoinRawTx)[],
fee: BigNumber,
depositorPrivateKey: string
): Promise<{
transactionHash: BitcoinTxHash
depositUtxo: BitcoinUtxo
rawTransaction: BitcoinRawTx
}> {
const depositorKeyRing =
BitcoinPrivateKeyUtils.createKeyRing(depositorPrivateKey)
const depositorAddress = depositorKeyRing.getAddress("string")

const inputCoins = inputUtxos.map((utxo) =>
bcoin.Coin.fromTX(
bcoin.MTX.fromRaw(utxo.transactionHex, "hex"),
utxo.outputIndex,
-1
)
const network = toBitcoinJsLibNetwork(bitcoinNetwork)
// eslint-disable-next-line new-cap
const depositorKeyPair = ECPairFactory(tinysecp).fromWIF(
depositorPrivateKey,
network
)

const transaction = new bcoin.MTX()
const psbt = new Psbt({ network })
psbt.setVersion(1)

const totalExpenses = amount.add(fee)
let totalInputValue = BigNumber.from(0)

for (const utxo of inputUtxos) {
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 (BitcoinScriptUtils.isP2WPKHScript(previousOutputScript)) {
psbt.addInput({
hash: utxo.transactionHash.reverse().toBuffer(),
index: utxo.outputIndex,
witnessUtxo: {
script: previousOutputScript,
value: previousOutputValue,
},
})

totalInputValue = totalInputValue.add(utxo.value)
if (totalInputValue.gte(totalExpenses)) {
break
}
}
// Skip UTXO if the type is unsupported.
}

const scriptHash = await this.script.getHash()
// 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")
}

transaction.addOutput({
script: this.script.witness
? bcoin.Script.fromProgram(0, scriptHash)
: bcoin.Script.fromScripthash(scriptHash),
// Add deposit output.
psbt.addOutput({
address: await this.script.deriveAddress(bitcoinNetwork),
value: 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
})
// Add change output if needed.
const changeValue = totalInputValue.sub(totalExpenses)
if (changeValue.gt(0)) {
const depositorAddress = BitcoinAddressConverter.publicKeyToAddress(
Hex.from(depositorKeyPair.publicKey),
bitcoinNetwork
)
psbt.addOutput({
address: depositorAddress,
value: changeValue.toNumber(),
})
}

transaction.sign(depositorKeyRing)
psbt.signAllInputs(depositorKeyPair)
psbt.finalizeAllInputs()

const transactionHash = BitcoinTxHash.from(transaction.txid())
const transaction = psbt.extractTransaction()
const transactionHash = BitcoinTxHash.from(transaction.getId())

return {
transactionHash,
Expand All @@ -96,7 +150,7 @@ export class DepositFunding {
value: amount,
},
rawTransaction: {
transactionHex: transaction.toRaw().toString("hex"),
transactionHex: transaction.toHex(),
},
}
}
Expand All @@ -108,30 +162,35 @@ export class DepositFunding {
* some UTXOs that can be used as input. It is also caller's responsibility
* to ensure the given deposit is funded exactly once.
* @param amount Deposit amount in satoshis.
* @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.
* @param depositorPrivateKey Bitcoin private key of the depositor.
* @param bitcoinClient Bitcoin client used to interact with the network.
* @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.
*/
async submitTransaction(
amount: BigNumber,
inputUtxos: BitcoinUtxo[],
fee: BigNumber,
depositorPrivateKey: string,
bitcoinClient: BitcoinClient
): Promise<{
transactionHash: BitcoinTxHash
depositUtxo: BitcoinUtxo
}> {
const depositorKeyRing =
BitcoinPrivateKeyUtils.createKeyRing(depositorPrivateKey)
const depositorAddress = depositorKeyRing.getAddress("string")

const utxos = await bitcoinClient.findAllUnspentTransactionOutputs(
depositorAddress
)

const utxosWithRaw: (BitcoinUtxo & BitcoinRawTx)[] = []
for (const utxo of utxos) {
for (const utxo of inputUtxos) {
const utxoRawTransaction = await bitcoinClient.getRawTransaction(
utxo.transactionHash
)
Expand All @@ -142,9 +201,20 @@ export class DepositFunding {
})
}

const bitcoinNetwork = await bitcoinClient.getNetwork()

const { transactionHash, depositUtxo, rawTransaction } =
await this.assembleTransaction(amount, utxosWithRaw, depositorPrivateKey)
await this.assembleTransaction(
bitcoinNetwork,
amount,
utxosWithRaw,
fee,
depositorPrivateKey
)

// 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 {
Expand Down
17 changes: 16 additions & 1 deletion typescript/test/bitcoin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe("Bitcoin", () => {
})

describe("BitcoinHashUtils", () => {
const { computeHash160, computeHash256, hashLEToBigNumber } =
const { computeHash160, computeHash256, hashLEToBigNumber, computeSha256 } =
BitcoinHashUtils

describe("computeHash160", () => {
Expand Down Expand Up @@ -104,6 +104,21 @@ describe("Bitcoin", () => {
expect(hashLEToBigNumber(hash)).to.equal(expectedBigNumber)
})
})

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("BitcoinAddressConverter", () => {
Expand Down
Loading

0 comments on commit 6647d69

Please sign in to comment.