Skip to content

Commit

Permalink
Replaced bcoin with bitcoinjs-lib for redemptions (#703)
Browse files Browse the repository at this point in the history
Refs: #695.
This PR replaces the bcoin library with bitcoinjs-lib for redemptions.
  • Loading branch information
lukasz-zimnoch authored Oct 3, 2023
2 parents c5edcdf + 14c9fc6 commit 241a551
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 41 deletions.
105 changes: 69 additions & 36 deletions typescript/src/redemption.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import bcoin from "bcoin"
import { BigNumber } from "ethers"
import {
createKeyRing,
createAddressFromPublicKey,
decomposeRawTransaction,
RawTransaction,
UnspentTransactionOutput,
Client as BitcoinClient,
TransactionHash,
isP2PKHScript,
isP2WPKHScript,
} from "./bitcoin"
import { Bridge, Event, Identifier, TBTCToken } from "./chain"
import { assembleTransactionProof } from "./proof"
import { determineWalletMainUtxo, WalletState } from "./wallet"
import { BitcoinNetwork } from "./bitcoin-network"
import { BitcoinNetwork, toBitcoinJsLibNetwork } from "./bitcoin-network"
import { Psbt, Transaction } from "bitcoinjs-lib"
import { ECPairFactory } from "ecpair"
import * as tinysecp from "tiny-secp256k1"
import { Hex } from "./hex"

/**
Expand Down Expand Up @@ -140,15 +144,25 @@ export async function submitRedemptionTransaction(
transactionHex: mainUtxoRawTransaction.transactionHex,
}

const bitcoinNetwork = await bitcoinClient.getNetwork()

// eslint-disable-next-line new-cap
const walletKeyPair = ECPairFactory(tinysecp).fromWIF(
walletPrivateKey,
toBitcoinJsLibNetwork(bitcoinNetwork)
)
const walletPublicKey = walletKeyPair.publicKey.toString("hex")

const redemptionRequests = await getWalletRedemptionRequests(
bridge,
createKeyRing(walletPrivateKey).getPublicKey().toString("hex"),
walletPublicKey,
redeemerOutputScripts,
"pending"
)

const { transactionHash, newMainUtxo, rawTransaction } =
await assembleRedemptionTransaction(
bitcoinNetwork,
walletPrivateKey,
mainUtxoWithRaw,
redemptionRequests,
Expand Down Expand Up @@ -242,6 +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 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 @@ -254,6 +269,7 @@ async function getWalletRedemptionRequests(
* - the redemption transaction in the raw format
*/
export async function assembleRedemptionTransaction(
bitcoinNetwork: BitcoinNetwork,
walletPrivateKey: string,
mainUtxo: UnspentTransactionOutput & RawTransaction,
redemptionRequests: RedemptionRequest[],
Expand All @@ -267,19 +283,46 @@ export async function assembleRedemptionTransaction(
throw new Error("There must be at least one request to redeem")
}

const walletKeyRing = createKeyRing(walletPrivateKey, witness)
const walletAddress = walletKeyRing.getAddress("string")
const network = toBitcoinJsLibNetwork(bitcoinNetwork)
// eslint-disable-next-line new-cap
const walletKeyPair = ECPairFactory(tinysecp).fromWIF(
walletPrivateKey,
network
)
const walletAddress = createAddressFromPublicKey(
Hex.from(walletKeyPair.publicKey),
bitcoinNetwork,
witness
)

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

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

if (isP2PKHScript(previousOutputScript)) {
psbt.addInput({
hash: mainUtxo.transactionHash.reverse().toBuffer(),
index: mainUtxo.outputIndex,
nonWitnessUtxo: Buffer.from(mainUtxo.transactionHex, "hex"),
})
} else if (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 @@ -303,44 +346,34 @@ export async function assembleRedemptionTransaction(
// use the proposed fee and add the difference to outputs proportionally.
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 = TransactionHash.from(transaction.txid())
const transaction = psbt.extractTransaction()
const transactionHash = TransactionHash.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 @@ -349,7 +382,7 @@ export async function assembleRedemptionTransaction(
transactionHash,
newMainUtxo,
rawTransaction: {
transactionHex: transaction.toRaw().toString("hex"),
transactionHex: transaction.toHex(),
},
}
}
Expand Down
13 changes: 8 additions & 5 deletions typescript/test/redemption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ describe("Redemption", () => {
const token: MockTBTCToken = new MockTBTCToken()

beforeEach(async () => {
bcoin.set("testnet")

await requestRedemption(
walletPublicKey,
mainUtxo,
Expand Down Expand Up @@ -82,7 +80,6 @@ describe("Redemption", () => {
let bridge: MockBridge

beforeEach(async () => {
bcoin.set("testnet")
bitcoinClient = new MockBitcoinClient()
bridge = new MockBridge()
})
Expand Down Expand Up @@ -498,6 +495,7 @@ describe("Redemption", () => {
newMainUtxo,
rawTransaction: transaction,
} = await assembleRedemptionTransaction(
BitcoinNetwork.Testnet,
walletPrivateKey,
data.mainUtxo,
redemptionRequests,
Expand Down Expand Up @@ -611,6 +609,7 @@ describe("Redemption", () => {
newMainUtxo,
rawTransaction: transaction,
} = await assembleRedemptionTransaction(
BitcoinNetwork.Testnet,
walletPrivateKey,
data.mainUtxo,
redemptionRequests,
Expand Down Expand Up @@ -723,6 +722,7 @@ describe("Redemption", () => {
newMainUtxo,
rawTransaction: transaction,
} = await assembleRedemptionTransaction(
BitcoinNetwork.Testnet,
walletPrivateKey,
data.mainUtxo,
redemptionRequests,
Expand Down Expand Up @@ -835,6 +835,7 @@ describe("Redemption", () => {
newMainUtxo,
rawTransaction: transaction,
} = await assembleRedemptionTransaction(
BitcoinNetwork.Testnet,
walletPrivateKey,
data.mainUtxo,
redemptionRequests,
Expand Down Expand Up @@ -946,6 +947,7 @@ describe("Redemption", () => {
newMainUtxo,
rawTransaction: transaction,
} = await assembleRedemptionTransaction(
BitcoinNetwork.Testnet,
walletPrivateKey,
data.mainUtxo,
redemptionRequests,
Expand Down Expand Up @@ -1103,6 +1105,7 @@ describe("Redemption", () => {
newMainUtxo,
rawTransaction: transaction,
} = await assembleRedemptionTransaction(
BitcoinNetwork.Testnet,
walletPrivateKey,
data.mainUtxo,
redemptionRequests,
Expand Down Expand Up @@ -1209,6 +1212,7 @@ describe("Redemption", () => {
newMainUtxo,
rawTransaction: transaction,
} = await assembleRedemptionTransaction(
BitcoinNetwork.Testnet,
walletPrivateKey,
data.mainUtxo,
redemptionRequests,
Expand Down Expand Up @@ -1298,6 +1302,7 @@ describe("Redemption", () => {
it("should revert", async () => {
await expect(
assembleRedemptionTransaction(
BitcoinNetwork.Testnet,
walletPrivateKey,
data.mainUtxo,
[], // empty list of redemption requests
Expand All @@ -1321,8 +1326,6 @@ describe("Redemption", () => {
let bridge: MockBridge

beforeEach(async () => {
bcoin.set("testnet")

bitcoinClient = new MockBitcoinClient()
bridge = new MockBridge()

Expand Down

0 comments on commit 241a551

Please sign in to comment.