Skip to content

Commit

Permalink
Integrate bitcoinjs-lib changes around deposit refund
Browse files Browse the repository at this point in the history
Here we pull changes from #706
  • Loading branch information
lukasz-zimnoch committed Oct 10, 2023
1 parent 3dfad7a commit 162ce25
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 124 deletions.
231 changes: 110 additions & 121 deletions typescript/src/services/deposits/refund.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import bcoin from "bcoin"
import { BigNumber } from "ethers"
import {
BitcoinRawTx,
BitcoinAddressConverter,
BitcoinClient,
BitcoinTxHash,
BitcoinUtxo,
BitcoinHashUtils,
BitcoinNetwork,
BitcoinPrivateKeyUtils,
BitcoinPublicKeyUtils,
BitcoinRawTx,
BitcoinScriptUtils,
BitcoinTxHash,
BitcoinUtxo,
} from "../../lib/bitcoin"
import { validateDepositReceipt } from "../../lib/contracts"
import { DepositScript } from "./"
import wif from "wif"
import {
Signer,
Transaction,
script as btcjsscript,
Stack,
} from "bitcoinjs-lib"

/**
* Component allowing to craft and submit the Bitcoin refund transaction using
Expand Down Expand Up @@ -67,7 +75,10 @@ export class DepositRefund {
transactionHex: utxoRawTransaction.transactionHex,
}

const bitcoinNetwork = await bitcoinClient.getNetwork()

const { transactionHash, rawTransaction } = await this.assembleTransaction(
bitcoinNetwork,
fee,
utxoWithRaw,
refunderAddress,
Expand All @@ -84,6 +95,7 @@ export class DepositRefund {

/**
* Assembles a Bitcoin P2(W)PKH deposit refund transaction.
* @param bitcoinNetwork - The target Bitcoin network.
* @param fee - the value that will be subtracted from the deposit UTXO being
* refunded and used as the transaction fee.
* @param utxo - UTXO that was created during depositing that needs be refunded.
Expand All @@ -96,6 +108,7 @@ export class DepositRefund {
* - the refund transaction in the raw format.
*/
async assembleTransaction(
bitcoinNetwork: BitcoinNetwork,
fee: BigNumber,
utxo: BitcoinUtxo & BitcoinRawTx,
refunderAddress: string,
Expand All @@ -106,36 +119,23 @@ export class DepositRefund {
}> {
validateDepositReceipt(this.script.receipt)

const decodedPrivateKey = wif.decode(refunderPrivateKey)

const refunderKeyRing = new bcoin.KeyRing({
witness: true,
privateKey: decodedPrivateKey.privateKey,
compressed: decodedPrivateKey.compressed,
})
const refunderKeyPair = BitcoinPrivateKeyUtils.createKeyPair(
refunderPrivateKey,
bitcoinNetwork
)

const transaction = new bcoin.MTX()
const outputValue = utxo.value.sub(fee)

transaction.addOutput({
script: bcoin.Script.fromAddress(refunderAddress),
value: utxo.value.toNumber(),
})
const transaction = new Transaction()

const inputCoin = bcoin.Coin.fromTX(
bcoin.MTX.fromRaw(utxo.transactionHex, "hex"),
utxo.outputIndex,
-1
transaction.addInput(
utxo.transactionHash.reverse().toBuffer(),
utxo.outputIndex
)

await transaction.fund([inputCoin], {
changeAddress: refunderAddress,
hardFee: fee.toNumber(),
subtractFee: true,
})

if (transaction.outputs.length != 1) {
throw new Error("Deposit refund transaction must have only one output")
}
const outputScript =
BitcoinAddressConverter.addressToOutputScript(refunderAddress)
transaction.addOutput(outputScript.toBuffer(), outputValue.toNumber())

// In order to be able to spend the UTXO being refunded the transaction's
// locktime must be set to a value equal to or higher than the refund locktime.
Expand All @@ -144,150 +144,139 @@ export class DepositRefund {
transaction.locktime = locktimeToUnixTimestamp(
this.script.receipt.refundLocktime
)
transaction.inputs[0].sequence = 0xfffffffe
transaction.ins[0].sequence = 0xfffffffe

// Sign the input
const previousOutpoint = transaction.inputs[0].prevout
const previousOutput = transaction.view.getOutput(previousOutpoint)
const previousScript = previousOutput.script

if (previousScript.isScripthash()) {
// P2SH UTXO deposit input
await this.signP2SHDepositInput(transaction, 0, refunderKeyRing)
} else if (previousScript.isWitnessScripthash()) {
// P2WSH UTXO deposit input
await this.signP2WSHDepositInput(transaction, 0, refunderKeyRing)
const previousOutput = Transaction.fromHex(utxo.transactionHex).outs[
utxo.outputIndex
]
const previousOutputValue = previousOutput.value
const previousOutputScript = previousOutput.script

if (BitcoinScriptUtils.isP2SHScript(previousOutputScript)) {
// P2SH deposit UTXO
await this.signP2SHDepositInput(transaction, 0, refunderKeyPair)
} else if (BitcoinScriptUtils.isP2WSHScript(previousOutputScript)) {
// P2WSH deposit UTXO
await this.signP2WSHDepositInput(
transaction,
0,
previousOutputValue,
refunderKeyPair
)
} else {
throw new Error("Unsupported UTXO script type")
}

// Verify the transaction by executing its input scripts.
const tx = transaction.toTX()
if (!tx.verify(transaction.view)) {
throw new Error("Transaction verification failure")
}

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

return {
transactionHash,
rawTransaction: {
transactionHex: transaction.toRaw().toString("hex"),
transactionHex: transaction.toHex(),
},
}
}

/**
* Creates data needed to sign a deposit input to be refunded.
* @param transaction - Mutable transaction containing the input to be refunded.
* @param inputIndex - Index that points to the input.
* @param refunderKeyRing - Key ring created using the refunder's private key.
* @returns Data needed to sign the input.
* Assembles the deposit script based on the given deposit details. Performs
* validations on values and key formats.
* @param refunderKeyPair - Signer object containing the refunder's key pair.
* @returns A Promise resolving to the assembled deposit script as a Buffer.
* @throws Error if there are discrepancies in values or key formats.
*/
private async prepareInputSignData(
transaction: any,
inputIndex: number,
refunderKeyRing: any
): Promise<{
refunderPublicKey: string
depositScript: any
previousOutputValue: number
}> {
const previousOutpoint = transaction.inputs[inputIndex].prevout
const previousOutput = transaction.view.getOutput(previousOutpoint)
private async prepareDepositScript(refunderKeyPair: Signer): Promise<Buffer> {
const refunderPublicKey = refunderKeyPair.publicKey.toString("hex")

const refunderPublicKey = refunderKeyRing.getPublicKey("hex")
if (
BitcoinHashUtils.computeHash160(refunderKeyRing.getPublicKey("hex")) !=
BitcoinHashUtils.computeHash160(refunderPublicKey) !=
this.script.receipt.refundPublicKeyHash
) {
throw new Error(
"Refund public key does not correspond to the refunder private key"
"Refund public key does not correspond to wallet private key"
)
}

if (!BitcoinPublicKeyUtils.isCompressedPublicKey(refunderPublicKey)) {
throw new Error("Refunder public key must be compressed")
}

const depositScript = bcoin.Script.fromRaw(
Buffer.from(await this.script.getPlainText(), "hex")
)

return {
refunderPublicKey: refunderPublicKey,
depositScript: depositScript,
previousOutputValue: previousOutput.value,
}
return Buffer.from(await this.script.getPlainText(), "hex")
}

/**
* Creates and sets `scriptSig` for the transaction input at the given index by
* combining signature, refunder's public key and deposit script.
* @param transaction - Mutable transaction containing the input to be signed.
* @param inputIndex - Index that points to the input to be signed.
* @param refunderKeyRing - Key ring created using the refunder's private key.
* @returns Empty return.
* Signs a P2SH deposit transaction input and sets the `scriptSig`.
* @param transaction - The transaction containing the input to be signed.
* @param inputIndex - Index pointing to the input within the transaction.
* @param refunderKeyPair - A Signer object with the refunder's public and private
* key pair.
* @returns An empty promise upon successful signing.
*/
private async signP2SHDepositInput(
transaction: any,
transaction: Transaction,
inputIndex: number,
refunderKeyRing: any
refunderKeyPair: Signer
) {
const { refunderPublicKey, depositScript, previousOutputValue } =
await this.prepareInputSignData(transaction, inputIndex, refunderKeyRing)
const depositScript = await this.prepareDepositScript(refunderKeyPair)

const signature: Buffer = transaction.signature(
const sigHashType = Transaction.SIGHASH_ALL

const sigHash = transaction.hashForSignature(
inputIndex,
depositScript,
previousOutputValue,
refunderKeyRing.privateKey,
bcoin.Script.hashType.ALL,
0 // legacy sighash version
sigHashType
)
const scriptSig = new bcoin.Script()
scriptSig.clear()
scriptSig.pushData(signature)
scriptSig.pushData(Buffer.from(refunderPublicKey, "hex"))
scriptSig.pushData(depositScript.toRaw())
scriptSig.compile()

transaction.inputs[inputIndex].script = scriptSig

const signature = btcjsscript.signature.encode(
refunderKeyPair.sign(sigHash),
sigHashType
)

const scriptSig: Stack = []
scriptSig.push(signature)
scriptSig.push(refunderKeyPair.publicKey)
scriptSig.push(depositScript)

transaction.ins[inputIndex].script = btcjsscript.compile(scriptSig)
}

/**
* Creates and sets witness script for the transaction input at the given index
* by combining signature, refunder public key and deposit script.
* @param transaction - Mutable transaction containing the input to be signed.
* @param inputIndex - Index that points to the input to be signed.
* @param refunderKeyRing - Key ring created using the refunder's private key.
* @returns Empty return.
* Signs a P2WSH deposit transaction input and sets the witness script.
* @param transaction - The transaction containing the input to be signed.
* @param inputIndex - Index pointing to the input within the transaction.
* @param previousOutputValue - The value from the previous transaction output.
* @param refunderKeyPair - A Signer object with the refunder's public and private
* key pair.
* @returns An empty promise upon successful signing.
*/
private async signP2WSHDepositInput(
transaction: any,
transaction: Transaction,
inputIndex: number,
refunderKeyRing: any
previousOutputValue: number,
refunderKeyPair: Signer
) {
const { refunderPublicKey, depositScript, previousOutputValue } =
await this.prepareInputSignData(transaction, inputIndex, refunderKeyRing)
const depositScript = await this.prepareDepositScript(refunderKeyPair)

const signature: Buffer = transaction.signature(
const sigHashType = Transaction.SIGHASH_ALL

const sigHash = transaction.hashForWitnessV0(
inputIndex,
depositScript,
previousOutputValue,
refunderKeyRing.privateKey,
bcoin.Script.hashType.ALL,
1 // segwit sighash version
sigHashType
)

const signature = btcjsscript.signature.encode(
refunderKeyPair.sign(sigHash),
sigHashType
)

const witness = new bcoin.Witness()
witness.clear()
witness.pushData(signature)
witness.pushData(Buffer.from(refunderPublicKey, "hex"))
witness.pushData(depositScript.toRaw())
witness.compile()
const witness: Buffer[] = []
witness.push(signature)
witness.push(refunderKeyPair.publicKey)
witness.push(depositScript)

transaction.inputs[inputIndex].witness = witness
transaction.ins[inputIndex].witness = witness
}
}

Expand Down
3 changes: 0 additions & 3 deletions typescript/test/deposit-refund.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BigNumber } from "ethers"
import { MockBitcoinClient } from "./utils/mock-bitcoin-client"
import bcoin from "bcoin"
import * as chai from "chai"
import chaiAsPromised from "chai-as-promised"
chai.use(chaiAsPromised)
Expand All @@ -23,8 +22,6 @@ describe("Refund", () => {
let bitcoinClient: MockBitcoinClient

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

bitcoinClient = new MockBitcoinClient()
})

Expand Down

0 comments on commit 162ce25

Please sign in to comment.