Skip to content

Commit

Permalink
Integrate bitcoinjs-lib changes around redemptions
Browse files Browse the repository at this point in the history
Here we pull changes from #703
  • Loading branch information
lukasz-zimnoch committed Oct 10, 2023
1 parent 6647d69 commit 3dfad7a
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 73 deletions.
31 changes: 16 additions & 15 deletions typescript/src/lib/bitcoin/ecdsa-key.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import bcoin from "bcoin"
import wif from "wif"
import { BigNumber } from "ethers"
import { Hex } from "../utils"
import { ECPairFactory, ECPairInterface } from "ecpair"
import * as tinysecp from "tiny-secp256k1"
import { BitcoinNetwork, toBitcoinJsLibNetwork } from "./network"

/**
* Checks whether given public key is a compressed Bitcoin public key.
Expand Down Expand Up @@ -59,25 +60,25 @@ export const BitcoinPublicKeyUtils = {
}

/**
* Creates a Bitcoin key ring based on the given private key.
* @param privateKey Private key that should be used to create the key ring
* @param witness Flag indicating whether the key ring will create witness
* or non-witness addresses
* Creates a Bitcoin key pair based on the given private key.
* @param privateKey Private key that should be used to create the key pair.
* @param bitcoinNetwork Bitcoin network the given key pair is relevant for.
* @returns Bitcoin key ring.
*/
function createKeyRing(privateKey: string, witness: boolean = true): any {
const decodedPrivateKey = wif.decode(privateKey)

return new bcoin.KeyRing({
witness: witness,
privateKey: decodedPrivateKey.privateKey,
compressed: decodedPrivateKey.compressed,
})
function createKeyPair(
privateKey: string,
bitcoinNetwork: BitcoinNetwork
): ECPairInterface {
// eslint-disable-next-line new-cap
return ECPairFactory(tinysecp).fromWIF(
privateKey,
toBitcoinJsLibNetwork(bitcoinNetwork)
)
}

/**
* Utility functions allowing to perform Bitcoin ECDSA public keys.
*/
export const BitcoinPrivateKeyUtils = {
createKeyRing,
createKeyPair,
}
9 changes: 4 additions & 5 deletions typescript/src/services/deposits/funding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BitcoinAddressConverter,
BitcoinClient,
BitcoinNetwork,
BitcoinPrivateKeyUtils,
BitcoinRawTx,
BitcoinScriptUtils,
BitcoinTxHash,
Expand All @@ -11,8 +12,6 @@ import {
} from "../../lib/bitcoin"
import { BigNumber } from "ethers"
import { Psbt, Transaction } from "bitcoinjs-lib"
import { ECPairFactory } from "ecpair"
import * as tinysecp from "tiny-secp256k1"
import { Hex } from "../../lib/utils"

/**
Expand Down Expand Up @@ -72,10 +71,10 @@ export class DepositFunding {
rawTransaction: BitcoinRawTx
}> {
const network = toBitcoinJsLibNetwork(bitcoinNetwork)
// eslint-disable-next-line new-cap
const depositorKeyPair = ECPairFactory(tinysecp).fromWIF(

const depositorKeyPair = BitcoinPrivateKeyUtils.createKeyPair(
depositorPrivateKey,
network
bitcoinNetwork
)

const psbt = new Psbt({ network })
Expand Down
11 changes: 8 additions & 3 deletions typescript/src/services/deposits/refund.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import bcoin from "bcoin"
import { BigNumber } from "ethers"
import {
BitcoinPrivateKeyUtils,
BitcoinRawTx,
BitcoinClient,
BitcoinTxHash,
Expand All @@ -11,6 +10,7 @@ import {
} from "../../lib/bitcoin"
import { validateDepositReceipt } from "../../lib/contracts"
import { DepositScript } from "./"
import wif from "wif"

/**
* Component allowing to craft and submit the Bitcoin refund transaction using
Expand Down Expand Up @@ -106,8 +106,13 @@ export class DepositRefund {
}> {
validateDepositReceipt(this.script.receipt)

const refunderKeyRing =
BitcoinPrivateKeyUtils.createKeyRing(refunderPrivateKey)
const decodedPrivateKey = wif.decode(refunderPrivateKey)

const refunderKeyRing = new bcoin.KeyRing({
witness: true,
privateKey: decodedPrivateKey.privateKey,
compressed: decodedPrivateKey.compressed,
})

const transaction = new bcoin.MTX()

Expand Down
103 changes: 59 additions & 44 deletions typescript/src/services/maintenance/wallet-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ import {
RedemptionRequest,
TBTCContracts,
} from "../../lib/contracts"
import bcoin from "bcoin"
import { DepositScript } from "../deposits"
import { ECPairFactory } from "ecpair"
import * as tinysecp from "tiny-secp256k1"
import { Hex } from "../../lib/utils"
import {
payments,
Expand All @@ -29,6 +26,7 @@ import {
Stack,
Transaction,
TxOutput,
Psbt,
} from "bitcoinjs-lib"

/**
Expand Down Expand Up @@ -189,11 +187,9 @@ class DepositSweep {
)
}

const network = toBitcoinJsLibNetwork(bitcoinNetwork)
// eslint-disable-next-line new-cap
const walletKeyPair = ECPairFactory(tinysecp).fromWIF(
const walletKeyPair = BitcoinPrivateKeyUtils.createKeyPair(
walletPrivateKey,
network
bitcoinNetwork
)
const walletAddress = BitcoinAddressConverter.publicKeyToAddress(
Hex.from(walletKeyPair.publicKey),
Expand Down Expand Up @@ -566,11 +562,14 @@ class Redemption {
transactionHex: mainUtxoRawTransaction.transactionHex,
}

const walletPublicKey = BitcoinPrivateKeyUtils.createKeyRing(
walletPrivateKey
const bitcoinNetwork = await this.bitcoinClient.getNetwork()

const walletKeyPair = BitcoinPrivateKeyUtils.createKeyPair(
walletPrivateKey,
bitcoinNetwork
)
.getPublicKey()
.toString("hex")

const walletPublicKey = walletKeyPair.publicKey.toString("hex")

const redemptionRequests: RedemptionRequest[] = []

Expand All @@ -593,6 +592,7 @@ class Redemption {

const { transactionHash, newMainUtxo, rawTransaction } =
await this.assembleTransaction(
bitcoinNetwork,
walletPrivateKey,
mainUtxoWithRaw,
redemptionRequests
Expand All @@ -617,6 +617,7 @@ class Redemption {
* - 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.
* @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
Expand All @@ -627,6 +628,7 @@ class Redemption {
* - the redemption transaction in the raw format
*/
async assembleTransaction(
bitcoinNetwork: BitcoinNetwork,
walletPrivateKey: string,
mainUtxo: BitcoinUtxo & BitcoinRawTx,
redemptionRequests: RedemptionRequest[]
Expand All @@ -639,22 +641,45 @@ class Redemption {
throw new Error("There must be at least one request to redeem")
}

const walletKeyRing = BitcoinPrivateKeyUtils.createKeyRing(
const walletKeyPair = BitcoinPrivateKeyUtils.createKeyPair(
walletPrivateKey,
bitcoinNetwork
)
const walletAddress = BitcoinAddressConverter.publicKeyToAddress(
Hex.from(walletKeyPair.publicKey),
bitcoinNetwork,
this.witness
)
const walletAddress = walletKeyRing.getAddress("string")

// Use the main UTXO as the single transaction input
const inputCoins = [
bcoin.Coin.fromTX(
bcoin.MTX.fromRaw(mainUtxo.transactionHex, "hex"),
mainUtxo.outputIndex,
-1
),
]

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

// Add input (current main UTXO).
const previousOutput = Transaction.fromHex(mainUtxo.transactionHex).outs[
mainUtxo.outputIndex
]
const previousOutputScript = previousOutput.script
const previousOutputValue = previousOutput.value

if (BitcoinScriptUtils.isP2PKHScript(previousOutputScript)) {
psbt.addInput({
hash: mainUtxo.transactionHash.reverse().toBuffer(),
index: mainUtxo.outputIndex,
nonWitnessUtxo: Buffer.from(mainUtxo.transactionHex, "hex"),
})
} else if (BitcoinScriptUtils.isP2WPKHScript(previousOutputScript)) {
psbt.addInput({
hash: mainUtxo.transactionHash.reverse().toBuffer(),
index: mainUtxo.outputIndex,
witnessUtxo: {
script: previousOutputScript,
value: previousOutputValue,
},
})
} else {
throw new Error("Unexpected main UTXO type")
}

let txTotalFee = BigNumber.from(0)
let totalOutputsValue = BigNumber.from(0)
Expand All @@ -673,44 +698,34 @@ class Redemption {
// Add the fee for this particular request to the overall transaction fee
txTotalFee = txTotalFee.add(request.txMaxFee)

transaction.addOutput({
script: bcoin.Script.fromRaw(
Buffer.from(request.redeemerOutputScript, "hex")
),
psbt.addOutput({
script: Buffer.from(request.redeemerOutputScript, "hex"),
value: outputValue.toNumber(),
})
}

// If there is a change output, add it explicitly to the transaction.
// If we did not add this output explicitly, the bcoin library would add it
// anyway during funding, but if the value of the change output was very low,
// the library would consider it "dust" and add it to the fee rather than
// create a new output.
// If there is a change output, add it to the transaction.
const changeOutputValue = mainUtxo.value
.sub(totalOutputsValue)
.sub(txTotalFee)
if (changeOutputValue.gt(0)) {
transaction.addOutput({
script: bcoin.Script.fromAddress(walletAddress),
psbt.addOutput({
address: walletAddress,
value: changeOutputValue.toNumber(),
})
}

await transaction.fund(inputCoins, {
changeAddress: walletAddress,
hardFee: txTotalFee.toNumber(),
subtractFee: false,
})

transaction.sign(walletKeyRing)
psbt.signAllInputs(walletKeyPair)
psbt.finalizeAllInputs()

const transactionHash = BitcoinTxHash.from(transaction.txid())
const transaction = psbt.extractTransaction()
const transactionHash = BitcoinTxHash.from(transaction.getId())
// If there is a change output, it will be the new wallet's main UTXO.
const newMainUtxo = changeOutputValue.gt(0)
? {
transactionHash,
// It was the last output added to the transaction.
outputIndex: transaction.outputs.length - 1,
outputIndex: transaction.outs.length - 1,
value: changeOutputValue,
}
: undefined
Expand All @@ -719,7 +734,7 @@ class Redemption {
transactionHash,
newMainUtxo,
rawTransaction: {
transactionHex: transaction.toRaw().toString("hex"),
transactionHex: transaction.toHex(),
},
}
}
Expand Down
Loading

0 comments on commit 3dfad7a

Please sign in to comment.