Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replaced bcoin with bitcoinjs-lib for redemptions #703

Merged
merged 4 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as in #702 (comment) but non-blocking and we can address it there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e202330.

* @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