From bbfa3c6382412db545f86b8bc63055a11ba1831f Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 15 Sep 2023 16:05:19 +0200 Subject: [PATCH 01/32] Added bitcoinjs-lib in version 6.0.2 --- typescript/package.json | 1 + typescript/yarn.lock | 39 +++++++++++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/typescript/package.json b/typescript/package.json index b1be0c883..f4b6de321 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -29,6 +29,7 @@ "@keep-network/tbtc-v2": "development", "bcoin": "git+https://github.com/keep-network/bcoin.git#5accd32c63e6025a0d35d67739c4a6e84095a1f8", "bcrypto": "git+https://github.com/bcoin-org/bcrypto.git#semver:~5.5.0", + "bitcoinjs-lib": "6.0.2", "bufio": "^1.0.6", "electrum-client-js": "git+https://github.com/keep-network/electrum-client-js.git#v0.1.1", "ethers": "^5.5.2", diff --git a/typescript/yarn.lock b/typescript/yarn.lock index 561d0069a..f2778ae49 100644 --- a/typescript/yarn.lock +++ b/typescript/yarn.lock @@ -1522,7 +1522,7 @@ "@openzeppelin/upgrades" "^2.7.2" openzeppelin-solidity "2.4.0" -"@keep-network/keep-ecdsa@1.9.0-dev.1", "@keep-network/keep-ecdsa@>1.9.0-dev <1.9.0-ropsten": +"@keep-network/keep-ecdsa@>1.9.0-dev <1.9.0-ropsten": version "1.9.0-dev.1" resolved "https://registry.yarnpkg.com/@keep-network/keep-ecdsa/-/keep-ecdsa-1.9.0-dev.1.tgz#7522b47dd639ddd7479a0e71dc328a9e0bba7cae" integrity sha512-FRIDejTUiQO7c9gBXgjtTp2sXkEQKFBBqVjYoZE20OCGRxbgum9FbgD/B5RWIctBy4GGr5wJHnA1789iaK3X6A== @@ -2575,6 +2575,11 @@ bech32@1.1.4: resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== +bech32@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + "bevent@git+https://github.com/bcoin-org/bevent.git#semver:~0.1.5": version "0.1.5" resolved "git+https://github.com/bcoin-org/bevent.git#60fb503de3ea1292d29ce438bfba80f0bc5ccb60" @@ -2639,6 +2644,11 @@ bindings@^1.3.0: bs32 "~0.1.5" bsert "~0.0.10" +bip174@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.1.tgz#ef3e968cf76de234a546962bcf572cc150982f9f" + integrity sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ== + bip32@2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/bip32/-/bip32-2.0.5.tgz#e3808a9e97a880dbafd0f5f09ca4a1e14ee275d2" @@ -2681,6 +2691,20 @@ bip39@3.0.4: pbkdf2 "^3.0.9" randombytes "^2.0.1" +bitcoinjs-lib@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.0.2.tgz#0fdf6c41978d93641b936d66f4afce44bb9b7f35" + integrity sha512-I994pGt9cL5s5OA6mkv1e8IuYcsKN2ORXnWbkqAXLNGvEnOHBhKBSvCjFl7YC2uVoJnfr/iwq7JMrq575SYO5w== + dependencies: + bech32 "^2.0.0" + bip174 "^2.0.1" + bs58check "^2.1.2" + create-hash "^1.1.0" + ripemd160 "^2.0.2" + typeforce "^1.11.3" + varuint-bitcoin "^1.1.2" + wif "^2.0.1" + bl@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" @@ -6902,7 +6926,7 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: +ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== @@ -7746,7 +7770,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typeforce@^1.11.5: +typeforce@^1.11.3, typeforce@^1.11.5: version "1.18.0" resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== @@ -7932,6 +7956,13 @@ varint@^5.0.0: resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4" integrity sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow== +varuint-bitcoin@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz#e76c138249d06138b480d4c5b40ef53693e24e92" + integrity sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw== + dependencies: + safe-buffer "^5.1.1" + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -8770,7 +8801,7 @@ wide-align@1.1.3: dependencies: string-width "^1.0.2 || 2" -wif@2.0.6, wif@^2.0.6: +wif@2.0.6, wif@^2.0.1, wif@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" integrity sha1-CNP1IFbGZnkplyb63g1DKudLRwQ= From b9f8ce418892a72ffb536c7035631b9cdf31670f Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 18 Sep 2023 11:01:16 +0200 Subject: [PATCH 02/32] Build deposit script using bitcoinjs-lib --- typescript/src/deposit.ts | 55 ++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/typescript/src/deposit.ts b/typescript/src/deposit.ts index a441346ab..59d8606ec 100644 --- a/typescript/src/deposit.ts +++ b/typescript/src/deposit.ts @@ -1,5 +1,6 @@ import bcoin from "bcoin" import { BigNumber } from "ethers" +import { Stack, script, opcodes } from "bitcoinjs-lib" import { Client as BitcoinClient, decomposeRawTransaction, @@ -13,8 +14,6 @@ import { BitcoinNetwork, toBcoinNetwork } from "./bitcoin-network" import { Bridge, Event, Identifier } from "./chain" import { Hex } from "./hex" -const { opcodes } = bcoin.script.common - // TODO: Replace all properties that are expected to be un-prefixed hexadecimal // strings with a Hex type. @@ -244,33 +243,31 @@ export async function assembleDepositScript( ): Promise { validateDepositScriptParameters(deposit) - // All HEXes pushed to the script must be un-prefixed. - const script = new bcoin.Script() - script.clear() - script.pushData(Buffer.from(deposit.depositor.identifierHex, "hex")) - script.pushOp(opcodes.OP_DROP) - script.pushData(Buffer.from(deposit.blindingFactor, "hex")) - script.pushOp(opcodes.OP_DROP) - script.pushOp(opcodes.OP_DUP) - script.pushOp(opcodes.OP_HASH160) - script.pushData(Buffer.from(deposit.walletPublicKeyHash, "hex")) - script.pushOp(opcodes.OP_EQUAL) - script.pushOp(opcodes.OP_IF) - script.pushOp(opcodes.OP_CHECKSIG) - script.pushOp(opcodes.OP_ELSE) - script.pushOp(opcodes.OP_DUP) - script.pushOp(opcodes.OP_HASH160) - script.pushData(Buffer.from(deposit.refundPublicKeyHash, "hex")) - script.pushOp(opcodes.OP_EQUALVERIFY) - script.pushData(Buffer.from(deposit.refundLocktime, "hex")) - script.pushOp(opcodes.OP_CHECKLOCKTIMEVERIFY) - script.pushOp(opcodes.OP_DROP) - script.pushOp(opcodes.OP_CHECKSIG) - script.pushOp(opcodes.OP_ENDIF) - script.compile() - - // Return script as HEX string. - return script.toRaw().toString("hex") + const chunks: Stack = [] + + // All HEXes pushed to the script must be un-prefixed + chunks.push(Buffer.from(deposit.depositor.identifierHex, "hex")) + chunks.push(opcodes.OP_DROP) + chunks.push(Buffer.from(deposit.blindingFactor, "hex")) + chunks.push(opcodes.OP_DROP) + chunks.push(opcodes.OP_DUP) + chunks.push(opcodes.OP_HASH160) + chunks.push(Buffer.from(deposit.walletPublicKeyHash, "hex")) + chunks.push(opcodes.OP_EQUAL) + chunks.push(opcodes.OP_IF) + chunks.push(opcodes.OP_CHECKSIG) + chunks.push(opcodes.OP_ELSE) + chunks.push(opcodes.OP_DUP) + chunks.push(opcodes.OP_HASH160) + chunks.push(Buffer.from(deposit.refundPublicKeyHash, "hex")) + chunks.push(opcodes.OP_EQUALVERIFY) + chunks.push(Buffer.from(deposit.refundLocktime, "hex")) + chunks.push(opcodes.OP_CHECKLOCKTIMEVERIFY) + chunks.push(opcodes.OP_DROP) + chunks.push(opcodes.OP_CHECKSIG) + chunks.push(opcodes.OP_ENDIF) + + return script.compile(chunks).toString("hex") } // eslint-disable-next-line valid-jsdoc From 85567ff506cbb5c94c7efba8d518cb22c2460930 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 18 Sep 2023 11:43:28 +0200 Subject: [PATCH 03/32] Added functionalities for checking input type --- typescript/src/bitcoin.ts | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index a46a4792b..7c6bd59c4 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -6,6 +6,7 @@ import sha256 from "bcrypto/lib/sha256-browser.js" import { BigNumber } from "ethers" import { Hex } from "./hex" import { BitcoinNetwork, toBcoinNetwork } from "./bitcoin-network" +import { payments } from "bitcoinjs-lib" /** * Represents a transaction hash (or transaction ID) as an un-prefixed hex @@ -679,3 +680,59 @@ export function readCompactSizeUint(varLenData: Hex): { } } } + +/** + * Checks if the provided script comes from a P2PKH input. + * @param script The script to be checked. + * @returns True if the script is P2PKH, false otherwise. + */ +export function isP2PKH(script: Buffer): boolean { + try { + payments.p2pkh({ output: script }); + return true; + } catch (err) { + return false; + } +} + +/** + * Checks if the provided script comes from a P2WPKH input. + * @param script The script to be checked. + * @returns True if the script is P2WPKH, false otherwise. + */ +export function isP2WPKH(script: Buffer): boolean { + try { + payments.p2wpkh({ output: script }); + return true; + } catch (err) { + return false; + } +} + +/** + * Checks if the provided script comes from a P2SH input. + * @param script The script to be checked. + * @returns True if the script is P2SH, false otherwise. + */ +export function isP2SH(script: Buffer): boolean { + try { + payments.p2sh({ output: script }); + return true; + } catch (err) { + return false; + } +} + +/** + * Checks if the provided script comes from a P2PKH input. + * @param script The script to be checked. + * @returns True if the script is P2WSH, false otherwise. + */ +export function isP2WSH(script: Buffer): boolean { + try { + payments.p2wsh({ output: script }); + return true; + } catch (err) { + return false; + } +} From f0c5dc2161836391e640c5f66a9e7b8c8fe39e3b Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 18 Sep 2023 12:21:39 +0200 Subject: [PATCH 04/32] Added basic structure for assembling deposit sweep transaction --- typescript/src/bitcoin.ts | 24 ++--- typescript/src/deposit-sweep.ts | 176 ++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 12 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 7c6bd59c4..473dc61ad 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -688,10 +688,10 @@ export function readCompactSizeUint(varLenData: Hex): { */ export function isP2PKH(script: Buffer): boolean { try { - payments.p2pkh({ output: script }); - return true; + payments.p2pkh({ output: script }) + return true } catch (err) { - return false; + return false } } @@ -702,10 +702,10 @@ export function isP2PKH(script: Buffer): boolean { */ export function isP2WPKH(script: Buffer): boolean { try { - payments.p2wpkh({ output: script }); - return true; + payments.p2wpkh({ output: script }) + return true } catch (err) { - return false; + return false } } @@ -716,10 +716,10 @@ export function isP2WPKH(script: Buffer): boolean { */ export function isP2SH(script: Buffer): boolean { try { - payments.p2sh({ output: script }); - return true; + payments.p2sh({ output: script }) + return true } catch (err) { - return false; + return false } } @@ -730,9 +730,9 @@ export function isP2SH(script: Buffer): boolean { */ export function isP2WSH(script: Buffer): boolean { try { - payments.p2wsh({ output: script }); - return true; + payments.p2wsh({ output: script }) + return true } catch (err) { - return false; + return false } } diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 36f3eb4e4..fbc38f54c 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -1,4 +1,5 @@ import bcoin from "bcoin" +import { Transaction, address } from "bitcoinjs-lib" import { BigNumber } from "ethers" import { RawTransaction, @@ -9,6 +10,10 @@ import { createKeyRing, TransactionHash, computeHash160, + isP2PKH, + isP2WPKH, + isP2SH, + isP2WSH, } from "./bitcoin" import { assembleDepositScript, Deposit } from "./deposit" import { Bridge, Identifier } from "./chain" @@ -235,6 +240,149 @@ export async function assembleDepositSweepTransaction( } } +/** + * Assembles a Bitcoin P2WPKH deposit sweep transaction. + * @dev The caller is responsible for ensuring the provided UTXOs are correctly + * formed, can be spent by the wallet and their combined value is greater + * then the fee. + * @param fee - the value that should be subtracted from the sum of the UTXOs + * values and used as the transaction fee. + * @param walletPrivateKey - Bitcoin private key of the wallet in WIF format. + * @param witness - The parameter used to decide about the type of the new main + * UTXO output. P2WPKH if `true`, P2PKH if `false`. + * @param utxos - UTXOs from new deposit transactions. Must be P2(W)SH. + * @param deposits - Array of deposits. Each element corresponds to UTXO. + * The number of UTXOs and deposit elements must equal. + * @param mainUtxo - main UTXO of the wallet, which is a P2WKH UTXO resulting + * from the previous wallet transaction (optional). + * @returns The outcome consisting of: + * - the sweep transaction hash, + * - the new wallet's main UTXO produced by this transaction. + * - the sweep transaction in the raw format + */ +// TODO: Rename once it's finished. +export async function assembleDepositSweepTransactionBitcoinJsLib( + fee: BigNumber, + walletPrivateKey: string, + witness: boolean, + utxos: (UnspentTransactionOutput & RawTransaction)[], + deposits: Deposit[], + mainUtxo?: UnspentTransactionOutput & RawTransaction +): Promise<{ + transactionHash: TransactionHash + newMainUtxo: UnspentTransactionOutput + rawTransaction: RawTransaction +}> { + if (utxos.length < 1) { + throw new Error("There must be at least one deposit UTXO to sweep") + } + + if (utxos.length != deposits.length) { + throw new Error("Number of UTXOs must equal the number of deposit elements") + } + + // TODO: Replace keyring with bitcoinjs-lib functionalities for managing + // keys (ecpair). + const walletKeyRing = createKeyRing(walletPrivateKey, witness) + const walletAddress = walletKeyRing.getAddress("string") + + const transaction = new Transaction() + let totalInputValue = BigNumber.from(0) + + if (mainUtxo) { + const prevTx = Transaction.fromHex(mainUtxo.transactionHex) + const scriptSig = prevTx.outs[mainUtxo.outputIndex].script + transaction.addInput( + mainUtxo.transactionHash.toBuffer(), + mainUtxo.outputIndex, + undefined, + scriptSig + ) + totalInputValue = totalInputValue.add(mainUtxo.value) + } + + for (const utxo of utxos) { + const prevTx = Transaction.fromHex(utxo.transactionHex) + const scriptSig = prevTx.outs[utxo.outputIndex].script + transaction.addInput( + utxo.transactionHash.toBuffer(), + utxo.outputIndex, + undefined, + scriptSig + ) + totalInputValue = totalInputValue.add(utxo.value) + } + + // TODO: Verify that output script is properly created from both testnet + // and mainnet addresses. + const scriptPubKey = address.toOutputScript(walletAddress) + transaction.addOutput(scriptPubKey, totalInputValue.toNumber()) + + // UTXOs must be mapped to deposits, as `fund` may arrange inputs in any + // order + const utxosWithDeposits: (UnspentTransactionOutput & + RawTransaction & + Deposit)[] = utxos.map((utxo, index) => ({ + ...utxo, + ...deposits[index], + })) + + for (let i = 0; i < transaction.ins.length; i++) { + // P2(W)PKH (main UTXO) + if ( + isP2PKH(transaction.ins[i].script) || + isP2WPKH(transaction.ins[i].script) + ) { + signMainUtxoInputBitcoinJsLib(transaction, i, walletKeyRing) + continue + } + + const utxoWithDeposit = utxosWithDeposits.find( + (u) => + u.transactionHash.toString() === + transaction.ins[i].hash.toString("hex") && + u.outputIndex == transaction.ins[i].index + ) + if (!utxoWithDeposit) { + throw new Error("Unknown input") + } + + if (isP2SH(transaction.ins[i].script)) { + // P2SH (deposit UTXO) + signP2SHDepositInputBitcoinJsLib( + transaction, + i, + utxoWithDeposit, + walletKeyRing + ) + } else if (isP2WSH(transaction.ins[i].script)) { + // P2WSH (deposit UTXO) + signP2WSHDepositInputBitcoinJsLib( + transaction, + i, + utxoWithDeposit, + walletKeyRing + ) + } else { + throw new Error("Unsupported UTXO script type") + } + } + + const transactionHash = TransactionHash.from(transaction.getId()) + + return { + transactionHash, + newMainUtxo: { + transactionHash, + outputIndex: 0, // There is only one output. + value: BigNumber.from(transaction.outs[0].value), + }, + rawTransaction: { + transactionHex: transaction.toHex(), + }, + } +} + /** * Creates script for the transaction input at the given index and signs the * input. @@ -332,6 +480,34 @@ async function signP2WSHDepositInput( transaction.inputs[inputIndex].witness = witness } +async function signMainUtxoInputBitcoinJsLib( + transaction: any, + inputIndex: number, + walletKeyRing: any +) { + // TODO: Implement +} + +// TODO: Rename once the function is implemented. +async function signP2SHDepositInputBitcoinJsLib( + transaction: Transaction, + inputIndex: number, + deposit: Deposit, + walletKeyRing: any +) { + // TODO: Implement +} + +// TODO: Rename once the function is implemented. +async function signP2WSHDepositInputBitcoinJsLib( + transaction: Transaction, + inputIndex: number, + deposit: Deposit, + walletKeyRing: any +) { + // TODO: Implement +} + /** * Creates data needed to sign a deposit input. * @param transaction - Mutable transaction containing the input. From 450579a00d25b1fe8b318dcf4dff25c5063e45fd Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 18 Sep 2023 14:43:39 +0200 Subject: [PATCH 05/32] Added ecpair for handling keys --- typescript/package.json | 2 ++ typescript/src/deposit-sweep.ts | 58 +++++++++++++++++++++++++++++---- typescript/yarn.lock | 23 ++++++++++++- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/typescript/package.json b/typescript/package.json index f4b6de321..0812c3261 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -31,9 +31,11 @@ "bcrypto": "git+https://github.com/bcoin-org/bcrypto.git#semver:~5.5.0", "bitcoinjs-lib": "6.0.2", "bufio": "^1.0.6", + "ecpair": "^2.1.0", "electrum-client-js": "git+https://github.com/keep-network/electrum-client-js.git#v0.1.1", "ethers": "^5.5.2", "p-timeout": "^4.1.0", + "tiny-secp256k1": "^2.2.3", "wif": "2.0.6" }, "devDependencies": { diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index fbc38f54c..8b7297a6a 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -1,5 +1,5 @@ import bcoin from "bcoin" -import { Transaction, address } from "bitcoinjs-lib" +import { Transaction, address, networks } from "bitcoinjs-lib" import { BigNumber } from "ethers" import { RawTransaction, @@ -18,6 +18,8 @@ import { import { assembleDepositScript, Deposit } from "./deposit" import { Bridge, Identifier } from "./chain" import { assembleTransactionProof } from "./proof" +import { ECPairFactory, ECPairInterface } from "ecpair" +import * as tinysecp from "tiny-secp256k1" /** * Submits a deposit sweep by combining all the provided P2(W)SH UTXOs and @@ -286,6 +288,10 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( const walletKeyRing = createKeyRing(walletPrivateKey, witness) const walletAddress = walletKeyRing.getAddress("string") + const ecPairApi = ECPairFactory(tinysecp); + // TODO: Pass appropriate network type (testnet vs mainnet). + const ecPair = ecPairApi.fromWIF(walletPrivateKey, networks.testnet) + const transaction = new Transaction() let totalInputValue = BigNumber.from(0) @@ -302,6 +308,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( } for (const utxo of utxos) { + // TODO: Validate that the utxo's value is the same as the value in deposit const prevTx = Transaction.fromHex(utxo.transactionHex) const scriptSig = prevTx.outs[utxo.outputIndex].script transaction.addInput( @@ -313,6 +320,9 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( totalInputValue = totalInputValue.add(utxo.value) } + // Subtract fee from the output + totalInputValue = totalInputValue.sub(fee) + // TODO: Verify that output script is properly created from both testnet // and mainnet addresses. const scriptPubKey = address.toOutputScript(walletAddress) @@ -333,7 +343,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( isP2PKH(transaction.ins[i].script) || isP2WPKH(transaction.ins[i].script) ) { - signMainUtxoInputBitcoinJsLib(transaction, i, walletKeyRing) + signMainUtxoInputBitcoinJsLib(transaction, i, ecPair) continue } @@ -353,7 +363,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( transaction, i, utxoWithDeposit, - walletKeyRing + ecPair ) } else if (isP2WSH(transaction.ins[i].script)) { // P2WSH (deposit UTXO) @@ -361,7 +371,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( transaction, i, utxoWithDeposit, - walletKeyRing + ecPair ) } else { throw new Error("Unsupported UTXO script type") @@ -483,7 +493,7 @@ async function signP2WSHDepositInput( async function signMainUtxoInputBitcoinJsLib( transaction: any, inputIndex: number, - walletKeyRing: any + ecPair: ECPairInterface ) { // TODO: Implement } @@ -493,7 +503,7 @@ async function signP2SHDepositInputBitcoinJsLib( transaction: Transaction, inputIndex: number, deposit: Deposit, - walletKeyRing: any + ecPair: ECPairInterface ) { // TODO: Implement } @@ -508,6 +518,42 @@ async function signP2WSHDepositInputBitcoinJsLib( // TODO: Implement } +async function prepareInputSignDataBitcoinIsLib( + deposit: Deposit, + ecPair: ECPairInterface +): Promise<{ + walletPublicKey: string + depositScript: any + previousOutputValue: number +}> { + const walletPublicKey = ecPair.publicKey.toString("hex") + + if ( + computeHash160(walletPublicKey) != deposit.walletPublicKeyHash + ) { + throw new Error( + "Wallet public key does not correspond to wallet private key" + ) + } + + if (!isCompressedPublicKey(walletPublicKey)) { + throw new Error("Wallet public key must be compressed") + } + + // eslint-disable-next-line no-unused-vars + const { amount, vault, ...depositScriptParameters } = deposit + + const depositScript = Buffer.from( + await assembleDepositScript(depositScriptParameters) + ) + + return { + walletPublicKey, + depositScript: depositScript, + previousOutputValue: deposit.amount.toNumber(), + } +} + /** * Creates data needed to sign a deposit input. * @param transaction - Mutable transaction containing the input. diff --git a/typescript/yarn.lock b/typescript/yarn.lock index f2778ae49..05897068f 100644 --- a/typescript/yarn.lock +++ b/typescript/yarn.lock @@ -3762,6 +3762,15 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecpair@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ecpair/-/ecpair-2.1.0.tgz#673f826b1d80d5eb091b8e2010c6b588e8d2cb45" + integrity sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw== + dependencies: + randombytes "^2.1.0" + typeforce "^1.18.0" + wif "^2.0.6" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -7570,6 +7579,13 @@ tiny-secp256k1@^1.1.3: elliptic "^6.4.0" nan "^2.13.2" +tiny-secp256k1@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-2.2.3.tgz#fe1dde11a64fcee2091157d4b78bcb300feb9b65" + integrity sha512-SGcL07SxcPN2nGKHTCvRMkQLYPSoeFcvArUSCYtjVARiFAWU44cCIqYS0mYAU6nY7XfvwURuTIGo2Omt3ZQr0Q== + dependencies: + uint8array-tools "0.0.7" + tmp@0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -7770,7 +7786,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typeforce@^1.11.3, typeforce@^1.11.5: +typeforce@^1.11.3, typeforce@^1.11.5, typeforce@^1.18.0: version "1.18.0" resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== @@ -7795,6 +7811,11 @@ typical@^5.2.0: resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== +uint8array-tools@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/uint8array-tools/-/uint8array-tools-0.0.7.tgz#a7a2bb5d8836eae2fade68c771454e6a438b390d" + integrity sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ== + ultron@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" From 53a4bbb84a384078c158ade5187f0bedf86d86dc Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 19 Sep 2023 19:52:56 +0200 Subject: [PATCH 06/32] Added signing of P2SH and P2WSH inputs --- typescript/src/deposit-sweep.ts | 127 ++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 8b7297a6a..fec293b32 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -1,5 +1,5 @@ import bcoin from "bcoin" -import { Transaction, address, networks } from "bitcoinjs-lib" +import { Transaction, Stack, address, script, networks } from "bitcoinjs-lib" import { BigNumber } from "ethers" import { RawTransaction, @@ -292,39 +292,38 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( // TODO: Pass appropriate network type (testnet vs mainnet). const ecPair = ecPairApi.fromWIF(walletPrivateKey, networks.testnet) - const transaction = new Transaction() + // Calculate the value of transaction's output. Note that the value of fee + // needs to be subtracted from the sum. let totalInputValue = BigNumber.from(0) + if (mainUtxo) { + totalInputValue = totalInputValue.add(mainUtxo.value) + } + for (const utxo of utxos) { + totalInputValue = totalInputValue.add(utxo.value) + } + totalInputValue = totalInputValue.sub(fee) + + // Create the transaction. + const transaction = new Transaction() + // Add the transaction's inputs. if (mainUtxo) { - const prevTx = Transaction.fromHex(mainUtxo.transactionHex) - const scriptSig = prevTx.outs[mainUtxo.outputIndex].script transaction.addInput( - mainUtxo.transactionHash.toBuffer(), + mainUtxo.transactionHash.reverse().toBuffer(), mainUtxo.outputIndex, - undefined, - scriptSig ) - totalInputValue = totalInputValue.add(mainUtxo.value) } - for (const utxo of utxos) { // TODO: Validate that the utxo's value is the same as the value in deposit - const prevTx = Transaction.fromHex(utxo.transactionHex) - const scriptSig = prevTx.outs[utxo.outputIndex].script transaction.addInput( - utxo.transactionHash.toBuffer(), - utxo.outputIndex, - undefined, - scriptSig + utxo.transactionHash.reverse().toBuffer(), + utxo.outputIndex ) - totalInputValue = totalInputValue.add(utxo.value) } - // Subtract fee from the output - totalInputValue = totalInputValue.sub(fee) - // TODO: Verify that output script is properly created from both testnet // and mainnet addresses. + // Add transaction output. const scriptPubKey = address.toOutputScript(walletAddress) transaction.addOutput(scriptPubKey, totalInputValue.toNumber()) @@ -338,18 +337,23 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( })) for (let i = 0; i < transaction.ins.length; i++) { + const previousOutput = findPreviousOutput( + TransactionHash.from(transaction.ins[i].hash).reverse(), + transaction.ins[i].index, + utxos, + mainUtxo + ) + const previousOutputScript = previousOutput.script + // P2(W)PKH (main UTXO) - if ( - isP2PKH(transaction.ins[i].script) || - isP2WPKH(transaction.ins[i].script) - ) { + if (isP2PKH(previousOutputScript) || isP2WPKH(previousOutputScript)) { signMainUtxoInputBitcoinJsLib(transaction, i, ecPair) continue } const utxoWithDeposit = utxosWithDeposits.find( (u) => - u.transactionHash.toString() === + u.transactionHash.reverse().toString() === transaction.ins[i].hash.toString("hex") && u.outputIndex == transaction.ins[i].index ) @@ -357,17 +361,17 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( throw new Error("Unknown input") } - if (isP2SH(transaction.ins[i].script)) { + if (isP2SH(previousOutputScript)) { // P2SH (deposit UTXO) - signP2SHDepositInputBitcoinJsLib( + await signP2SHDepositInputBitcoinJsLib( transaction, i, utxoWithDeposit, ecPair ) - } else if (isP2WSH(transaction.ins[i].script)) { + } else if (isP2WSH(previousOutputScript)) { // P2WSH (deposit UTXO) - signP2WSHDepositInputBitcoinJsLib( + await signP2WSHDepositInputBitcoinJsLib( transaction, i, utxoWithDeposit, @@ -393,6 +397,27 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( } } +function findPreviousOutput( + inputHash: TransactionHash, + inputIndex: number, + utxos: (UnspentTransactionOutput & RawTransaction)[], + mainUtxo?: UnspentTransactionOutput & RawTransaction +) { + if (mainUtxo && + mainUtxo.transactionHash.equals(inputHash) && + mainUtxo.outputIndex === inputIndex) { + return Transaction.fromHex(mainUtxo.transactionHex).outs[mainUtxo.outputIndex] + } + + for (const utxo of utxos) { + if (utxo.transactionHash.equals(inputHash) && utxo.outputIndex === inputIndex) { + return Transaction.fromHex(utxo.transactionHex).outs[utxo.outputIndex] + } + } + + throw new Error("Unknown input") +} + /** * Creates script for the transaction input at the given index and signs the * input. @@ -505,7 +530,25 @@ async function signP2SHDepositInputBitcoinJsLib( deposit: Deposit, ecPair: ECPairInterface ) { - // TODO: Implement + const { walletPublicKey, depositScript } = + await prepareInputSignDataBitcoinIsLib(deposit, ecPair) + + const sigHashType = Transaction.SIGHASH_ALL + + const sigHash = transaction.hashForSignature( + inputIndex, + depositScript, + sigHashType + ) + + const signature = script.signature.encode(ecPair.sign(sigHash), sigHashType) + + const scriptSig: Stack = [] + scriptSig.push(signature) + scriptSig.push(Buffer.from(walletPublicKey, "hex")) + scriptSig.push(depositScript) + + transaction.ins[inputIndex].script = script.compile(scriptSig) } // TODO: Rename once the function is implemented. @@ -513,9 +556,28 @@ async function signP2WSHDepositInputBitcoinJsLib( transaction: Transaction, inputIndex: number, deposit: Deposit, - walletKeyRing: any + ecPair: ECPairInterface ) { - // TODO: Implement + const { walletPublicKey, depositScript, previousOutputValue } = + await prepareInputSignDataBitcoinIsLib(deposit, ecPair) + + const sigHashType = Transaction.SIGHASH_ALL + + const sigHash = transaction.hashForWitnessV0( + inputIndex, + depositScript, + previousOutputValue, + sigHashType + ) + + const signature = script.signature.encode(ecPair.sign(sigHash), sigHashType) + + const witness: Buffer[] = [] + witness.push(signature) + witness.push(Buffer.from(walletPublicKey, "hex")) + witness.push(depositScript) + + transaction.ins[inputIndex].witness = witness } async function prepareInputSignDataBitcoinIsLib( @@ -544,7 +606,8 @@ async function prepareInputSignDataBitcoinIsLib( const { amount, vault, ...depositScriptParameters } = deposit const depositScript = Buffer.from( - await assembleDepositScript(depositScriptParameters) + await assembleDepositScript(depositScriptParameters), + "hex" ) return { From e609cee0db388546f6f92fb3d9c6f816ec574690 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 20 Sep 2023 10:21:35 +0200 Subject: [PATCH 07/32] Lint fixes --- typescript/src/deposit-sweep.ts | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index fec293b32..659f1d69c 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -18,7 +18,7 @@ import { import { assembleDepositScript, Deposit } from "./deposit" import { Bridge, Identifier } from "./chain" import { assembleTransactionProof } from "./proof" -import { ECPairFactory, ECPairInterface } from "ecpair" +import { ECPairFactory as ecFactory, ECPairInterface } from "ecpair" import * as tinysecp from "tiny-secp256k1" /** @@ -288,7 +288,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( const walletKeyRing = createKeyRing(walletPrivateKey, witness) const walletAddress = walletKeyRing.getAddress("string") - const ecPairApi = ECPairFactory(tinysecp); + const ecPairApi = ecFactory(tinysecp) // TODO: Pass appropriate network type (testnet vs mainnet). const ecPair = ecPairApi.fromWIF(walletPrivateKey, networks.testnet) @@ -310,7 +310,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( if (mainUtxo) { transaction.addInput( mainUtxo.transactionHash.reverse().toBuffer(), - mainUtxo.outputIndex, + mainUtxo.outputIndex ) } for (const utxo of utxos) { @@ -403,14 +403,21 @@ function findPreviousOutput( utxos: (UnspentTransactionOutput & RawTransaction)[], mainUtxo?: UnspentTransactionOutput & RawTransaction ) { - if (mainUtxo && + if ( + mainUtxo && mainUtxo.transactionHash.equals(inputHash) && - mainUtxo.outputIndex === inputIndex) { - return Transaction.fromHex(mainUtxo.transactionHex).outs[mainUtxo.outputIndex] + mainUtxo.outputIndex === inputIndex + ) { + return Transaction.fromHex(mainUtxo.transactionHex).outs[ + mainUtxo.outputIndex + ] } for (const utxo of utxos) { - if (utxo.transactionHash.equals(inputHash) && utxo.outputIndex === inputIndex) { + if ( + utxo.transactionHash.equals(inputHash) && + utxo.outputIndex === inputIndex + ) { return Transaction.fromHex(utxo.transactionHex).outs[utxo.outputIndex] } } @@ -559,7 +566,7 @@ async function signP2WSHDepositInputBitcoinJsLib( ecPair: ECPairInterface ) { const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignDataBitcoinIsLib(deposit, ecPair) + await prepareInputSignDataBitcoinIsLib(deposit, ecPair) const sigHashType = Transaction.SIGHASH_ALL @@ -590,9 +597,7 @@ async function prepareInputSignDataBitcoinIsLib( }> { const walletPublicKey = ecPair.publicKey.toString("hex") - if ( - computeHash160(walletPublicKey) != deposit.walletPublicKeyHash - ) { + if (computeHash160(walletPublicKey) != deposit.walletPublicKeyHash) { throw new Error( "Wallet public key does not correspond to wallet private key" ) From acc089c04608d3208584b14ec489b816d5e971da Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 21 Sep 2023 16:22:15 +0200 Subject: [PATCH 08/32] Added signing of main UTXO --- typescript/src/deposit-sweep.ts | 112 +++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 22 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 659f1d69c..536857955 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -1,5 +1,13 @@ import bcoin from "bcoin" -import { Transaction, Stack, address, script, networks } from "bitcoinjs-lib" +import { + Transaction, + Stack, + Signer, + payments, + address, + script, + networks, +} from "bitcoinjs-lib" import { BigNumber } from "ethers" import { RawTransaction, @@ -18,7 +26,7 @@ import { import { assembleDepositScript, Deposit } from "./deposit" import { Bridge, Identifier } from "./chain" import { assembleTransactionProof } from "./proof" -import { ECPairFactory as ecFactory, ECPairInterface } from "ecpair" +import { ECPairFactory as ecFactory } from "ecpair" import * as tinysecp from "tiny-secp256k1" /** @@ -288,9 +296,10 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( const walletKeyRing = createKeyRing(walletPrivateKey, witness) const walletAddress = walletKeyRing.getAddress("string") - const ecPairApi = ecFactory(tinysecp) - // TODO: Pass appropriate network type (testnet vs mainnet). - const ecPair = ecPairApi.fromWIF(walletPrivateKey, networks.testnet) + const keyPair = ecFactory(tinysecp).fromWIF( + walletPrivateKey, + networks.testnet + ) // Calculate the value of transaction's output. Note that the value of fee // needs to be subtracted from the sum. @@ -338,16 +347,23 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( for (let i = 0; i < transaction.ins.length; i++) { const previousOutput = findPreviousOutput( - TransactionHash.from(transaction.ins[i].hash).reverse(), + TransactionHash.from(transaction.ins[i].hash), transaction.ins[i].index, utxos, mainUtxo ) const previousOutputScript = previousOutput.script + const previousOutputValue = previousOutput.value // P2(W)PKH (main UTXO) if (isP2PKH(previousOutputScript) || isP2WPKH(previousOutputScript)) { - signMainUtxoInputBitcoinJsLib(transaction, i, ecPair) + signMainUtxoInputBitcoinJsLib( + transaction, + i, + previousOutputScript, + previousOutputValue, + keyPair + ) continue } @@ -367,7 +383,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( transaction, i, utxoWithDeposit, - ecPair + keyPair ) } else if (isP2WSH(previousOutputScript)) { // P2WSH (deposit UTXO) @@ -375,7 +391,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( transaction, i, utxoWithDeposit, - ecPair + keyPair ) } else { throw new Error("Unsupported UTXO script type") @@ -405,7 +421,7 @@ function findPreviousOutput( ) { if ( mainUtxo && - mainUtxo.transactionHash.equals(inputHash) && + mainUtxo.transactionHash.reverse().equals(inputHash) && mainUtxo.outputIndex === inputIndex ) { return Transaction.fromHex(mainUtxo.transactionHex).outs[ @@ -415,14 +431,14 @@ function findPreviousOutput( for (const utxo of utxos) { if ( - utxo.transactionHash.equals(inputHash) && + utxo.transactionHash.reverse().equals(inputHash) && utxo.outputIndex === inputIndex ) { return Transaction.fromHex(utxo.transactionHex).outs[utxo.outputIndex] } } - throw new Error("Unknown input") + throw new Error("Could not find previous output") } /** @@ -523,11 +539,63 @@ async function signP2WSHDepositInput( } async function signMainUtxoInputBitcoinJsLib( - transaction: any, + transaction: Transaction, inputIndex: number, - ecPair: ECPairInterface + prevOutScript: Buffer, + prevOutValue: number, + keyPair: Signer ) { - // TODO: Implement + const sigHashType = Transaction.SIGHASH_ALL + + if (isP2PKH(prevOutScript)) { + // P2PKH + const sigHash = transaction.hashForSignature( + inputIndex, + prevOutScript, + sigHashType + ) + + const signature = script.signature.encode( + keyPair.sign(sigHash), + sigHashType + ) + + const scriptSig = payments.p2pkh({ + signature: signature, + pubkey: keyPair.publicKey, + }).input! + + transaction.ins[inputIndex].script = scriptSig + } else { + // P2WPKH + const decompiledScript = script.decompile(prevOutScript) + if ( + !decompiledScript || + decompiledScript.length !== 2 || + decompiledScript[0] !== 0x00 || + !Buffer.isBuffer(decompiledScript[1]) || + decompiledScript[1].length !== 20 + ) { + throw new Error("Invalid script format") + } + + const publicKeyHash = decompiledScript[1] + const p2pkhScript = payments.p2pkh({ hash: publicKeyHash }).output! + + const sigHash = transaction.hashForWitnessV0( + inputIndex, + p2pkhScript, + prevOutValue, + sigHashType + ) + + const signature = script.signature.encode( + keyPair.sign(sigHash), + sigHashType + ) + + transaction.ins[inputIndex].witness = [signature, keyPair.publicKey] + } } // TODO: Rename once the function is implemented. @@ -535,10 +603,10 @@ async function signP2SHDepositInputBitcoinJsLib( transaction: Transaction, inputIndex: number, deposit: Deposit, - ecPair: ECPairInterface + keyPair: Signer ) { const { walletPublicKey, depositScript } = - await prepareInputSignDataBitcoinIsLib(deposit, ecPair) + await prepareInputSignDataBitcoinIsLib(deposit, keyPair) const sigHashType = Transaction.SIGHASH_ALL @@ -548,7 +616,7 @@ async function signP2SHDepositInputBitcoinJsLib( sigHashType ) - const signature = script.signature.encode(ecPair.sign(sigHash), sigHashType) + const signature = script.signature.encode(keyPair.sign(sigHash), sigHashType) const scriptSig: Stack = [] scriptSig.push(signature) @@ -563,10 +631,10 @@ async function signP2WSHDepositInputBitcoinJsLib( transaction: Transaction, inputIndex: number, deposit: Deposit, - ecPair: ECPairInterface + keyPair: Signer ) { const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignDataBitcoinIsLib(deposit, ecPair) + await prepareInputSignDataBitcoinIsLib(deposit, keyPair) const sigHashType = Transaction.SIGHASH_ALL @@ -577,7 +645,7 @@ async function signP2WSHDepositInputBitcoinJsLib( sigHashType ) - const signature = script.signature.encode(ecPair.sign(sigHash), sigHashType) + const signature = script.signature.encode(keyPair.sign(sigHash), sigHashType) const witness: Buffer[] = [] witness.push(signature) @@ -589,7 +657,7 @@ async function signP2WSHDepositInputBitcoinJsLib( async function prepareInputSignDataBitcoinIsLib( deposit: Deposit, - ecPair: ECPairInterface + ecPair: Signer ): Promise<{ walletPublicKey: string depositScript: any From 841622949c0ee00bf767c5defa92f43d8fa6bafd Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 22 Sep 2023 15:33:14 +0200 Subject: [PATCH 09/32] Added check for mismatch between utxo and deposit values --- typescript/src/deposit-sweep.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 536857955..0340e646b 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -323,7 +323,6 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( ) } for (const utxo of utxos) { - // TODO: Validate that the utxo's value is the same as the value in deposit transaction.addInput( utxo.transactionHash.reverse().toBuffer(), utxo.outputIndex @@ -383,6 +382,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( transaction, i, utxoWithDeposit, + previousOutputValue, keyPair ) } else if (isP2WSH(previousOutputScript)) { @@ -391,6 +391,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( transaction, i, utxoWithDeposit, + previousOutputValue, keyPair ) } else { @@ -545,6 +546,7 @@ async function signMainUtxoInputBitcoinJsLib( prevOutValue: number, keyPair: Signer ) { + // TODO: Check that input the belongs to the wallet. const sigHashType = Transaction.SIGHASH_ALL if (isP2PKH(prevOutScript)) { @@ -603,10 +605,11 @@ async function signP2SHDepositInputBitcoinJsLib( transaction: Transaction, inputIndex: number, deposit: Deposit, + prevOutValue: number, keyPair: Signer ) { const { walletPublicKey, depositScript } = - await prepareInputSignDataBitcoinIsLib(deposit, keyPair) + await prepareInputSignDataBitcoinIsLib(deposit, prevOutValue, keyPair) const sigHashType = Transaction.SIGHASH_ALL @@ -631,10 +634,11 @@ async function signP2WSHDepositInputBitcoinJsLib( transaction: Transaction, inputIndex: number, deposit: Deposit, + prevOutValue: number, keyPair: Signer ) { const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignDataBitcoinIsLib(deposit, keyPair) + await prepareInputSignDataBitcoinIsLib(deposit, prevOutValue, keyPair) const sigHashType = Transaction.SIGHASH_ALL @@ -657,12 +661,17 @@ async function signP2WSHDepositInputBitcoinJsLib( async function prepareInputSignDataBitcoinIsLib( deposit: Deposit, + prevOutValue: number, ecPair: Signer ): Promise<{ walletPublicKey: string depositScript: any previousOutputValue: number }> { + if (prevOutValue != deposit.amount.toNumber()) { + throw new Error("Mismatch between amount in deposit and deposit tx") + } + const walletPublicKey = ecPair.publicKey.toString("hex") if (computeHash160(walletPublicKey) != deposit.walletPublicKeyHash) { From 6a06c63ccbd23a7f98a9c7e8b706b57feca9a81c Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 25 Sep 2023 14:17:46 +0200 Subject: [PATCH 10/32] Added Bitcoin network argument --- typescript/src/bitcoin-network.ts | 17 +++++++++++++++++ typescript/src/deposit-sweep.ts | 12 ++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/typescript/src/bitcoin-network.ts b/typescript/src/bitcoin-network.ts index f14dd5ed4..1f6bf50a9 100644 --- a/typescript/src/bitcoin-network.ts +++ b/typescript/src/bitcoin-network.ts @@ -1,4 +1,5 @@ import { Hex } from "./hex" +import { networks } from "bitcoinjs-lib" /** * Bitcoin networks. @@ -64,3 +65,19 @@ export function toBcoinNetwork(bitcoinNetwork: BitcoinNetwork): string { } } } + +export function toBitcoinJsLibNetwork( + bitcoinNetwork: BitcoinNetwork +): networks.Network { + switch (bitcoinNetwork) { + case BitcoinNetwork.Mainnet: { + return networks.bitcoin + } + case BitcoinNetwork.Testnet: { + return networks.testnet + } + default: { + throw new Error(`network not supported`) + } + } +} diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 0340e646b..7a646ab82 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -6,7 +6,6 @@ import { payments, address, script, - networks, } from "bitcoinjs-lib" import { BigNumber } from "ethers" import { @@ -28,6 +27,7 @@ import { Bridge, Identifier } from "./chain" import { assembleTransactionProof } from "./proof" import { ECPairFactory as ecFactory } from "ecpair" import * as tinysecp from "tiny-secp256k1" +import { BitcoinNetwork, toBitcoinJsLibNetwork } from "./bitcoin-network" /** * Submits a deposit sweep by combining all the provided P2(W)SH UTXOs and @@ -272,6 +272,7 @@ export async function assembleDepositSweepTransaction( */ // TODO: Rename once it's finished. export async function assembleDepositSweepTransactionBitcoinJsLib( + bitcoinNetwork: BitcoinNetwork, fee: BigNumber, walletPrivateKey: string, witness: boolean, @@ -291,15 +292,14 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( throw new Error("Number of UTXOs must equal the number of deposit elements") } + const network = toBitcoinJsLibNetwork(bitcoinNetwork) + // TODO: Replace keyring with bitcoinjs-lib functionalities for managing // keys (ecpair). const walletKeyRing = createKeyRing(walletPrivateKey, witness) const walletAddress = walletKeyRing.getAddress("string") - const keyPair = ecFactory(tinysecp).fromWIF( - walletPrivateKey, - networks.testnet - ) + const keyPair = ecFactory(tinysecp).fromWIF(walletPrivateKey, network) // Calculate the value of transaction's output. Note that the value of fee // needs to be subtracted from the sum. @@ -332,7 +332,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( // TODO: Verify that output script is properly created from both testnet // and mainnet addresses. // Add transaction output. - const scriptPubKey = address.toOutputScript(walletAddress) + const scriptPubKey = address.toOutputScript(walletAddress, network) transaction.addOutput(scriptPubKey, totalInputValue.toNumber()) // UTXOs must be mapped to deposits, as `fund` may arrange inputs in any From dcf7314f4ad1a1b2fb4ce8e01806a0344c7a7ae9 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 25 Sep 2023 15:42:01 +0200 Subject: [PATCH 11/32] Added address extraction from key pair --- typescript/src/bitcoin-network.ts | 1 + typescript/src/bitcoin.ts | 18 +++++++++++++++++- typescript/src/deposit-sweep.ts | 8 ++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/typescript/src/bitcoin-network.ts b/typescript/src/bitcoin-network.ts index 1f6bf50a9..c2805c9f7 100644 --- a/typescript/src/bitcoin-network.ts +++ b/typescript/src/bitcoin-network.ts @@ -66,6 +66,7 @@ export function toBcoinNetwork(bitcoinNetwork: BitcoinNetwork): string { } } +// TODO: Description export function toBitcoinJsLibNetwork( bitcoinNetwork: BitcoinNetwork ): networks.Network { diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 2e4e0fb08..2f7df7a12 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -4,7 +4,8 @@ import bufio from "bufio" import { BigNumber, utils } from "ethers" import { Hex } from "./hex" import { BitcoinNetwork, toBcoinNetwork } from "./bitcoin-network" -import { payments } from "bitcoinjs-lib" +import { payments, networks } from "bitcoinjs-lib" +import { ECPairInterface } from "ecpair" /** * Represents a transaction hash (or transaction ID) as an un-prefixed hex @@ -740,3 +741,18 @@ export function isP2WSH(script: Buffer): boolean { return false } } + +// TODO: Description and unit tests. +export function addressFromKeyPair( + keyPair: ECPairInterface, + network: networks.Network, + witness: boolean +): string { + if (witness) { + // P2WPKH (SegWit) + return payments.p2wpkh({ pubkey: keyPair.publicKey, network }).address! + } else { + // P2PKH (Legacy) + return payments.p2pkh({ pubkey: keyPair.publicKey, network }).address! + } +} diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 7a646ab82..85ad6e02b 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -15,6 +15,7 @@ import { decomposeRawTransaction, isCompressedPublicKey, createKeyRing, + addressFromKeyPair, TransactionHash, computeHash160, isP2PKH, @@ -293,13 +294,8 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( } const network = toBitcoinJsLibNetwork(bitcoinNetwork) - - // TODO: Replace keyring with bitcoinjs-lib functionalities for managing - // keys (ecpair). - const walletKeyRing = createKeyRing(walletPrivateKey, witness) - const walletAddress = walletKeyRing.getAddress("string") - const keyPair = ecFactory(tinysecp).fromWIF(walletPrivateKey, network) + const walletAddress = addressFromKeyPair(keyPair, network, witness) // Calculate the value of transaction's output. Note that the value of fee // needs to be subtracted from the sum. From e63197b5a2f0968fa804301afba7b31394e262ae Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 25 Sep 2023 17:38:28 +0200 Subject: [PATCH 12/32] Added check for own UTXO --- typescript/src/bitcoin.ts | 4 +-- typescript/src/deposit-sweep.ts | 49 ++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 2f7df7a12..55f668c00 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -5,7 +5,7 @@ import { BigNumber, utils } from "ethers" import { Hex } from "./hex" import { BitcoinNetwork, toBcoinNetwork } from "./bitcoin-network" import { payments, networks } from "bitcoinjs-lib" -import { ECPairInterface } from "ecpair" +import { Signer } from "ecpair" /** * Represents a transaction hash (or transaction ID) as an un-prefixed hex @@ -744,7 +744,7 @@ export function isP2WSH(script: Buffer): boolean { // TODO: Description and unit tests. export function addressFromKeyPair( - keyPair: ECPairInterface, + keyPair: Signer, network: networks.Network, witness: boolean ): string { diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 85ad6e02b..4d2c6f994 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -6,6 +6,7 @@ import { payments, address, script, + networks, } from "bitcoinjs-lib" import { BigNumber } from "ethers" import { @@ -352,12 +353,13 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( // P2(W)PKH (main UTXO) if (isP2PKH(previousOutputScript) || isP2WPKH(previousOutputScript)) { - signMainUtxoInputBitcoinJsLib( + await signMainUtxoInputBitcoinJsLib( transaction, i, previousOutputScript, previousOutputValue, - keyPair + keyPair, + network ) continue } @@ -540,9 +542,13 @@ async function signMainUtxoInputBitcoinJsLib( inputIndex: number, prevOutScript: Buffer, prevOutValue: number, - keyPair: Signer + keyPair: Signer, + network: networks.Network ) { - // TODO: Check that input the belongs to the wallet. + if (!ownsUtxo(keyPair, prevOutScript, network)) { + throw new Error("UTXO does not belong to the wallet") + } + const sigHashType = Transaction.SIGHASH_ALL if (isP2PKH(prevOutScript)) { @@ -783,3 +789,38 @@ export async function submitDepositSweepProof( vault ) } + +// TODO: Description and unit test. +export function ownsUtxo( + keyPair: Signer, + prevOutScript: Buffer, + network: networks.Network +): boolean { + // Derive P2PKH and P2WPKH addresses from the public key. + const p2pkhAddress = + payments.p2pkh({ pubkey: keyPair.publicKey, network }).address || "" + const p2wpkhAddress = + payments.p2wpkh({ pubkey: keyPair.publicKey, network }).address || "" + + // Try to extract an address from the provided prevOutScript. + let addressFromOutput = "" + try { + addressFromOutput = + payments.p2pkh({ output: prevOutScript, network }).address || "" + } catch (e) { + // If not P2PKH, try P2WPKH. + try { + addressFromOutput = + payments.p2wpkh({ output: prevOutScript, network }).address || "" + } catch (err) { + // If neither p2pkh nor p2wpkh address can be derived, assume the previous + // output script comes from a different UTXO type or is corrupted. + return false + } + } + + // Check if the UTXO's address matches either of the derived addresses. + return ( + addressFromOutput === p2pkhAddress || addressFromOutput === p2wpkhAddress + ) +} From da9856a0702bea8b1fc8fced832f9faa1d05079e Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 25 Sep 2023 18:07:13 +0200 Subject: [PATCH 13/32] Updated unit tests for assembleDepositSweepTransaction --- typescript/src/deposit-sweep.ts | 318 +------------------------- typescript/test/data/deposit-sweep.ts | 29 ++- typescript/test/deposit-sweep.test.ts | 51 +++-- 3 files changed, 55 insertions(+), 343 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 4d2c6f994..196361025 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -1,4 +1,3 @@ -import bcoin from "bcoin" import { Transaction, Stack, @@ -15,7 +14,6 @@ import { Client as BitcoinClient, decomposeRawTransaction, isCompressedPublicKey, - createKeyRing, addressFromKeyPair, TransactionHash, computeHash160, @@ -89,8 +87,11 @@ export async function submitDepositSweepTransaction( } } + const bitcoinNetwork = await bitcoinClient.getNetwork() + const { transactionHash, newMainUtxo, rawTransaction } = await assembleDepositSweepTransaction( + bitcoinNetwork, fee, walletPrivateKey, witness, @@ -128,152 +129,6 @@ export async function submitDepositSweepTransaction( * - the sweep transaction in the raw format */ export async function assembleDepositSweepTransaction( - fee: BigNumber, - walletPrivateKey: string, - witness: boolean, - utxos: (UnspentTransactionOutput & RawTransaction)[], - deposits: Deposit[], - mainUtxo?: UnspentTransactionOutput & RawTransaction -): Promise<{ - transactionHash: TransactionHash - newMainUtxo: UnspentTransactionOutput - rawTransaction: RawTransaction -}> { - if (utxos.length < 1) { - throw new Error("There must be at least one deposit UTXO to sweep") - } - - if (utxos.length != deposits.length) { - throw new Error("Number of UTXOs must equal the number of deposit elements") - } - - const walletKeyRing = createKeyRing(walletPrivateKey, witness) - const walletAddress = walletKeyRing.getAddress("string") - - const inputCoins = [] - let totalInputValue = BigNumber.from(0) - - if (mainUtxo) { - inputCoins.push( - bcoin.Coin.fromTX( - bcoin.MTX.fromRaw(mainUtxo.transactionHex, "hex"), - mainUtxo.outputIndex, - -1 - ) - ) - totalInputValue = totalInputValue.add(mainUtxo.value) - } - - for (const utxo of utxos) { - inputCoins.push( - bcoin.Coin.fromTX( - bcoin.MTX.fromRaw(utxo.transactionHex, "hex"), - utxo.outputIndex, - -1 - ) - ) - totalInputValue = totalInputValue.add(utxo.value) - } - - const transaction = new bcoin.MTX() - - transaction.addOutput({ - script: bcoin.Script.fromAddress(walletAddress), - value: totalInputValue.toNumber(), - }) - - await transaction.fund(inputCoins, { - changeAddress: walletAddress, - hardFee: fee.toNumber(), - subtractFee: true, - }) - - if (transaction.outputs.length != 1) { - throw new Error("Deposit sweep transaction must have only one output") - } - - // UTXOs must be mapped to deposits, as `fund` may arrange inputs in any - // order - const utxosWithDeposits: (UnspentTransactionOutput & - RawTransaction & - Deposit)[] = utxos.map((utxo, index) => ({ - ...utxo, - ...deposits[index], - })) - - for (let i = 0; i < transaction.inputs.length; i++) { - const previousOutpoint = transaction.inputs[i].prevout - const previousOutput = transaction.view.getOutput(previousOutpoint) - const previousScript = previousOutput.script - - // P2(W)PKH (main UTXO) - if (previousScript.isPubkeyhash() || previousScript.isWitnessPubkeyhash()) { - await signMainUtxoInput(transaction, i, walletKeyRing) - continue - } - - const utxoWithDeposit = utxosWithDeposits.find( - (u) => - u.transactionHash.toString() === previousOutpoint.txid() && - u.outputIndex == previousOutpoint.index - ) - if (!utxoWithDeposit) { - throw new Error("Unknown input") - } - - if (previousScript.isScripthash()) { - // P2SH (deposit UTXO) - await signP2SHDepositInput(transaction, i, utxoWithDeposit, walletKeyRing) - } else if (previousScript.isWitnessScripthash()) { - // P2WSH (deposit UTXO) - await signP2WSHDepositInput( - transaction, - i, - utxoWithDeposit, - walletKeyRing - ) - } else { - throw new Error("Unsupported UTXO script type") - } - } - - const transactionHash = TransactionHash.from(transaction.txid()) - - return { - transactionHash, - newMainUtxo: { - transactionHash, - outputIndex: 0, // There is only one output. - value: BigNumber.from(transaction.outputs[0].value), - }, - rawTransaction: { - transactionHex: transaction.toRaw().toString("hex"), - }, - } -} - -/** - * Assembles a Bitcoin P2WPKH deposit sweep transaction. - * @dev The caller is responsible for ensuring the provided UTXOs are correctly - * formed, can be spent by the wallet and their combined value is greater - * then the fee. - * @param fee - the value that should be subtracted from the sum of the UTXOs - * values and used as the transaction fee. - * @param walletPrivateKey - Bitcoin private key of the wallet in WIF format. - * @param witness - The parameter used to decide about the type of the new main - * UTXO output. P2WPKH if `true`, P2PKH if `false`. - * @param utxos - UTXOs from new deposit transactions. Must be P2(W)SH. - * @param deposits - Array of deposits. Each element corresponds to UTXO. - * The number of UTXOs and deposit elements must equal. - * @param mainUtxo - main UTXO of the wallet, which is a P2WKH UTXO resulting - * from the previous wallet transaction (optional). - * @returns The outcome consisting of: - * - the sweep transaction hash, - * - the new wallet's main UTXO produced by this transaction. - * - the sweep transaction in the raw format - */ -// TODO: Rename once it's finished. -export async function assembleDepositSweepTransactionBitcoinJsLib( bitcoinNetwork: BitcoinNetwork, fee: BigNumber, walletPrivateKey: string, @@ -326,14 +181,13 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( ) } - // TODO: Verify that output script is properly created from both testnet - // and mainnet addresses. // Add transaction output. const scriptPubKey = address.toOutputScript(walletAddress, network) transaction.addOutput(scriptPubKey, totalInputValue.toNumber()) // UTXOs must be mapped to deposits, as `fund` may arrange inputs in any // order + // TODO: Most likely remove. const utxosWithDeposits: (UnspentTransactionOutput & RawTransaction & Deposit)[] = utxos.map((utxo, index) => ({ @@ -353,7 +207,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( // P2(W)PKH (main UTXO) if (isP2PKH(previousOutputScript) || isP2WPKH(previousOutputScript)) { - await signMainUtxoInputBitcoinJsLib( + await signMainUtxoInput( transaction, i, previousOutputScript, @@ -376,7 +230,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( if (isP2SH(previousOutputScript)) { // P2SH (deposit UTXO) - await signP2SHDepositInputBitcoinJsLib( + await signP2SHDepositInput( transaction, i, utxoWithDeposit, @@ -385,7 +239,7 @@ export async function assembleDepositSweepTransactionBitcoinJsLib( ) } else if (isP2WSH(previousOutputScript)) { // P2WSH (deposit UTXO) - await signP2WSHDepositInputBitcoinJsLib( + await signP2WSHDepositInput( transaction, i, utxoWithDeposit, @@ -440,104 +294,7 @@ function findPreviousOutput( throw new Error("Could not find previous output") } -/** - * Creates script for the transaction input at the given index and signs the - * input. - * @param transaction - Mutable transaction containing the input to be signed. - * @param inputIndex - Index that points to the input to be signed. - * @param walletKeyRing - Key ring created using the wallet's private key. - * @returns Empty promise. - */ async function signMainUtxoInput( - transaction: any, - inputIndex: number, - walletKeyRing: any -) { - const previousOutpoint = transaction.inputs[inputIndex].prevout - const previousOutput = transaction.view.getOutput(previousOutpoint) - if (!walletKeyRing.ownOutput(previousOutput)) { - throw new Error("UTXO does not belong to the wallet") - } - // Build script and set it as input's witness - transaction.scriptInput(inputIndex, previousOutput, walletKeyRing) - // Build signature and add it in front of script in input's witness - transaction.signInput(inputIndex, previousOutput, walletKeyRing) -} - -/** - * Creates and sets `scriptSig` for the transaction input at the given index by - * combining signature, wallet 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 deposit - Data of the deposit. - * @param walletKeyRing - Key ring created using the wallet's private key. - * @returns Empty promise. - */ -async function signP2SHDepositInput( - transaction: any, - inputIndex: number, - deposit: Deposit, - walletKeyRing: any -): Promise { - const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignData(transaction, inputIndex, deposit, walletKeyRing) - - const signature: Buffer = transaction.signature( - inputIndex, - depositScript, - previousOutputValue, - walletKeyRing.privateKey, - bcoin.Script.hashType.ALL, - 0 // legacy sighash version - ) - const scriptSig = new bcoin.Script() - scriptSig.clear() - scriptSig.pushData(signature) - scriptSig.pushData(Buffer.from(walletPublicKey, "hex")) - scriptSig.pushData(depositScript.toRaw()) - scriptSig.compile() - - transaction.inputs[inputIndex].script = scriptSig -} - -/** - * Creates and sets witness script for the transaction input at the given index - * by combining signature, wallet 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 deposit - Data of the deposit. - * @param walletKeyRing - Key ring created using the wallet's private key. - * @returns Empty promise. - */ -async function signP2WSHDepositInput( - transaction: any, - inputIndex: number, - deposit: Deposit, - walletKeyRing: any -): Promise { - const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignData(transaction, inputIndex, deposit, walletKeyRing) - - const signature: Buffer = transaction.signature( - inputIndex, - depositScript, - previousOutputValue, - walletKeyRing.privateKey, - bcoin.Script.hashType.ALL, - 1 // segwit sighash version - ) - - const witness = new bcoin.Witness() - witness.clear() - witness.pushData(signature) - witness.pushData(Buffer.from(walletPublicKey, "hex")) - witness.pushData(depositScript.toRaw()) - witness.compile() - - transaction.inputs[inputIndex].witness = witness -} - -async function signMainUtxoInputBitcoinJsLib( transaction: Transaction, inputIndex: number, prevOutScript: Buffer, @@ -602,8 +359,8 @@ async function signMainUtxoInputBitcoinJsLib( } } -// TODO: Rename once the function is implemented. -async function signP2SHDepositInputBitcoinJsLib( +// TODO: Description. +async function signP2SHDepositInput( transaction: Transaction, inputIndex: number, deposit: Deposit, @@ -631,8 +388,8 @@ async function signP2SHDepositInputBitcoinJsLib( transaction.ins[inputIndex].script = script.compile(scriptSig) } -// TODO: Rename once the function is implemented. -async function signP2WSHDepositInputBitcoinJsLib( +// TODO: Description. +async function signP2WSHDepositInput( transaction: Transaction, inputIndex: number, deposit: Deposit, @@ -701,59 +458,6 @@ async function prepareInputSignDataBitcoinIsLib( } } -/** - * Creates data needed to sign a deposit input. - * @param transaction - Mutable transaction containing the input. - * @param inputIndex - Index that points to the input. - * @param deposit - Data of the deposit. - * @param walletKeyRing - Key ring created using the wallet's private key. - * @returns Data needed to sign the input. - */ -async function prepareInputSignData( - transaction: any, - inputIndex: number, - deposit: Deposit, - walletKeyRing: any -): Promise<{ - walletPublicKey: string - depositScript: any - previousOutputValue: number -}> { - const previousOutpoint = transaction.inputs[inputIndex].prevout - const previousOutput = transaction.view.getOutput(previousOutpoint) - - if (previousOutput.value != deposit.amount.toNumber()) { - throw new Error("Mismatch between amount in deposit and deposit tx") - } - - const walletPublicKey = walletKeyRing.getPublicKey("hex") - if ( - computeHash160(walletKeyRing.getPublicKey("hex")) != - deposit.walletPublicKeyHash - ) { - throw new Error( - "Wallet public key does not correspond to wallet private key" - ) - } - - if (!isCompressedPublicKey(walletPublicKey)) { - throw new Error("Wallet public key must be compressed") - } - - // eslint-disable-next-line no-unused-vars - const { amount, vault, ...depositScriptParameters } = deposit - - const depositScript = bcoin.Script.fromRaw( - Buffer.from(await assembleDepositScript(depositScriptParameters), "hex") - ) - - return { - walletPublicKey, - depositScript: depositScript, - previousOutputValue: previousOutput.value, - } -} - /** * Prepares the proof of a deposit sweep transaction and submits it to the * Bridge on-chain contract. diff --git a/typescript/test/data/deposit-sweep.ts b/typescript/test/data/deposit-sweep.ts index 59e3f5770..74e001b6b 100644 --- a/typescript/test/data/deposit-sweep.ts +++ b/typescript/test/data/deposit-sweep.ts @@ -350,24 +350,23 @@ export const depositSweepWithNonWitnessMainUtxoAndWitnessOutput: DepositSweepTes witness: true, expectedSweep: { transactionHash: TransactionHash.from( - "7831d0dfde7e160f3b9bb66c433710f0d3110d73ea78b9db65e81c091a6718a0" + "1933781f01f27f086c3a31c4a53035ebc7c4688e1f4b316babefa8f6dab77dc2" ), transaction: { transactionHex: - "01000000000102173a201f597a2c8ccd7842303a6653bb87437fb08dae671731a0" + - "75403b32a2fd0000000000ffffffffe19612be756bf7e740b47bec0e24845089ac" + - "e48c78d473cb34949b3007c4a2c8000000006a47304402204382deb051f9f3e2b5" + - "39e4bac2d1a50faf8d66bc7a3a3f3d286dabd96d92b58b02207c74c6aaf48e25d0" + - "7e02bb4039606d77ecfd80c492c050ab2486af6027fc2d5a012103989d253b17a6" + - "a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9ffffffff010884" + - "0000000000001600148db50eb52063ea9d98b3eac91489a90f738986f603483045" + - "022100c52bc876cdee80a3061ace3ffbce5e860942d444cd38e00e5f63fd8e818d" + - "7e7c022040a7017bb8213991697705e7092c481526c788a4731d06e582dc1c57be" + - "d7243b012103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc2" + - "9dcf8581d95c14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d" + - "000395237576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776" + - "a914e257eccafbc07c381642ce6e7e55120fb077fbed880448f2b262b175ac6800" + - "00000000", + "01000000000102e19612be756bf7e740b47bec0e24845089ace48c78d473cb34949" + + "b3007c4a2c8000000006a473044022013787be70eda0620002fa55c92abfcd32257" + + "d64fa652dd32bac65d705162a95902203407fea1abc99a9273ead3179ce60f60a34" + + "33fb2e93f58569e4bee9f63c0d679012103989d253b17a6a0f41838b84ff0d20e88" + + "98f9d7b1a98f2564da4cc29dcf8581d9ffffffff173a201f597a2c8ccd7842303a6" + + "653bb87437fb08dae671731a075403b32a2fd0000000000ffffffff010884000000" + + "0000001600148db50eb52063ea9d98b3eac91489a90f738986f6000348304502210" + + "0804f0fa989d632cda99a24159e28b8d31d4033c2d5de47d8207ea2767273d10a02" + + "20278e82d0714867b31eb013762306e2b97c2c1cc74b8135bee78d565e72ee630e0" + + "12103989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581" + + "d95c14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d000395237" + + "576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776a914e257ec" + + "cafbc07c381642ce6e7e55120fb077fbed880448f2b262b175ac6800000000", }, }, } diff --git a/typescript/test/deposit-sweep.test.ts b/typescript/test/deposit-sweep.test.ts index 494aa24c5..0d429efd3 100644 --- a/typescript/test/deposit-sweep.test.ts +++ b/typescript/test/deposit-sweep.test.ts @@ -31,6 +31,7 @@ import { submitDepositSweepProof, submitDepositSweepTransaction, } from "../src/deposit-sweep" +import { BitcoinNetwork } from "../src/bitcoin-network" describe("Sweep", () => { const fee = BigNumber.from(1600) @@ -39,8 +40,6 @@ describe("Sweep", () => { let bitcoinClient: MockBitcoinClient beforeEach(async () => { - bcoin.set("testnet") - bitcoinClient = new MockBitcoinClient() }) @@ -379,6 +378,7 @@ describe("Sweep", () => { newMainUtxo, rawTransaction: transaction, } = await assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -508,6 +508,7 @@ describe("Sweep", () => { newMainUtxo, rawTransaction: transaction, } = await assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -658,6 +659,7 @@ describe("Sweep", () => { newMainUtxo, rawTransaction: transaction, } = await assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -686,25 +688,7 @@ describe("Sweep", () => { // Validate inputs. expect(txJSON.inputs.length).to.be.equal(2) - const p2wshInput = txJSON.inputs[0] - expect(p2wshInput.prevout.hash).to.be.equal( - depositSweepWithNonWitnessMainUtxoAndWitnessOutput.deposits[0].utxo.transactionHash.toString() - ) - expect(p2wshInput.prevout.index).to.be.equal( - depositSweepWithNonWitnessMainUtxoAndWitnessOutput.deposits[0] - .utxo.outputIndex - ) - // Transaction should be signed. As it's a SegWit input, the `witness` - // field should be filled, while the `script` field should be empty. - expect(p2wshInput.witness.length).to.be.greaterThan(0) - expect(p2wshInput.script.length).to.be.equal(0) - // Input's address should be set to the address generated from deposit - // script hash - expect(p2wshInput.address).to.be.equal( - "tb1qk8urugnf08wfle6wslmdxq7mkz9z0gw8e6gkvspn7dx87tfpfntshdm7qr" - ) - - const p2pkhInput = txJSON.inputs[1] // main UTXO + const p2pkhInput = txJSON.inputs[0] // main UTXO expect(p2pkhInput.prevout.hash).to.be.equal( depositSweepWithNonWitnessMainUtxoAndWitnessOutput.mainUtxo.transactionHash.toString() ) @@ -722,6 +706,24 @@ describe("Sweep", () => { "mtSEUCE7G8om9zJttG9twtjoiSsUz7QnY9" ) + const p2wshInput = txJSON.inputs[1] + expect(p2wshInput.prevout.hash).to.be.equal( + depositSweepWithNonWitnessMainUtxoAndWitnessOutput.deposits[0].utxo.transactionHash.toString() + ) + expect(p2wshInput.prevout.index).to.be.equal( + depositSweepWithNonWitnessMainUtxoAndWitnessOutput.deposits[0] + .utxo.outputIndex + ) + // Transaction should be signed. As it's a SegWit input, the `witness` + // field should be filled, while the `script` field should be empty. + expect(p2wshInput.witness.length).to.be.greaterThan(0) + expect(p2wshInput.script.length).to.be.equal(0) + // Input's address should be set to the address generated from deposit + // script hash + expect(p2wshInput.address).to.be.equal( + "tb1qk8urugnf08wfle6wslmdxq7mkz9z0gw8e6gkvspn7dx87tfpfntshdm7qr" + ) + // Validate outputs. expect(txJSON.outputs.length).to.be.equal(1) @@ -791,6 +793,7 @@ describe("Sweep", () => { newMainUtxo, rawTransaction: transaction, } = await assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -878,6 +881,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, true, @@ -906,6 +910,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, witness, @@ -931,6 +936,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, true, @@ -970,6 +976,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, true, @@ -994,6 +1001,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, anotherPrivateKey, true, @@ -1027,6 +1035,7 @@ describe("Sweep", () => { it("should revert", async () => { await expect( assembleDepositSweepTransaction( + BitcoinNetwork.Testnet, fee, testnetWalletPrivateKey, true, From 4bc749f31f705b6288a4712dfddf450222543dee Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 26 Sep 2023 12:14:24 +0200 Subject: [PATCH 14/32] Simplified signing inputs --- typescript/src/deposit-sweep.ts | 109 +++++++++++--------------------- 1 file changed, 36 insertions(+), 73 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 196361025..a37de71b9 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -185,64 +185,53 @@ export async function assembleDepositSweepTransaction( const scriptPubKey = address.toOutputScript(walletAddress, network) transaction.addOutput(scriptPubKey, totalInputValue.toNumber()) - // UTXOs must be mapped to deposits, as `fund` may arrange inputs in any - // order - // TODO: Most likely remove. - const utxosWithDeposits: (UnspentTransactionOutput & - RawTransaction & - Deposit)[] = utxos.map((utxo, index) => ({ - ...utxo, - ...deposits[index], - })) - - for (let i = 0; i < transaction.ins.length; i++) { - const previousOutput = findPreviousOutput( - TransactionHash.from(transaction.ins[i].hash), - transaction.ins[i].index, - utxos, - mainUtxo + // Sign the main UTXO input if there is main UTXO. + if (mainUtxo) { + const inputIndex = 0 // Main UTXO is the first input. + const previousOutput = Transaction.fromHex(mainUtxo.transactionHex).outs[ + mainUtxo.outputIndex + ] + + await signMainUtxoInput( + transaction, + inputIndex, + previousOutput.script, + previousOutput.value, + keyPair, + network ) - const previousOutputScript = previousOutput.script - const previousOutputValue = previousOutput.value + } - // P2(W)PKH (main UTXO) - if (isP2PKH(previousOutputScript) || isP2WPKH(previousOutputScript)) { - await signMainUtxoInput( - transaction, - i, - previousOutputScript, - previousOutputValue, - keyPair, - network - ) - continue - } + // Sign the deposit inputs. + for (let depositIndex = 0; depositIndex < deposits.length; depositIndex++) { + // If there is a main UTXO index, we must adjust input index as the first + // input is the main UTXO input. + const inputIndex = mainUtxo ? depositIndex + 1 : depositIndex - const utxoWithDeposit = utxosWithDeposits.find( - (u) => - u.transactionHash.reverse().toString() === - transaction.ins[i].hash.toString("hex") && - u.outputIndex == transaction.ins[i].index - ) - if (!utxoWithDeposit) { - throw new Error("Unknown input") - } + const utxo = utxos[depositIndex] + const previousOutput = Transaction.fromHex(utxo.transactionHex).outs[ + utxo.outputIndex + ] + const previousOutputValue = previousOutput.value + const previousOutputScript = previousOutput.script + + const deposit = deposits[depositIndex] if (isP2SH(previousOutputScript)) { // P2SH (deposit UTXO) await signP2SHDepositInput( transaction, - i, - utxoWithDeposit, - previousOutputValue, + inputIndex, + deposit, + previousOutput.value, keyPair ) } else if (isP2WSH(previousOutputScript)) { // P2WSH (deposit UTXO) await signP2WSHDepositInput( transaction, - i, - utxoWithDeposit, + inputIndex, + deposit, previousOutputValue, keyPair ) @@ -266,34 +255,6 @@ export async function assembleDepositSweepTransaction( } } -function findPreviousOutput( - inputHash: TransactionHash, - inputIndex: number, - utxos: (UnspentTransactionOutput & RawTransaction)[], - mainUtxo?: UnspentTransactionOutput & RawTransaction -) { - if ( - mainUtxo && - mainUtxo.transactionHash.reverse().equals(inputHash) && - mainUtxo.outputIndex === inputIndex - ) { - return Transaction.fromHex(mainUtxo.transactionHex).outs[ - mainUtxo.outputIndex - ] - } - - for (const utxo of utxos) { - if ( - utxo.transactionHash.reverse().equals(inputHash) && - utxo.outputIndex === inputIndex - ) { - return Transaction.fromHex(utxo.transactionHex).outs[utxo.outputIndex] - } - } - - throw new Error("Could not find previous output") -} - async function signMainUtxoInput( transaction: Transaction, inputIndex: number, @@ -327,7 +288,7 @@ async function signMainUtxoInput( }).input! transaction.ins[inputIndex].script = scriptSig - } else { + } else if (isP2WPKH(prevOutScript)) { // P2WPKH const decompiledScript = script.decompile(prevOutScript) if ( @@ -356,6 +317,8 @@ async function signMainUtxoInput( ) transaction.ins[inputIndex].witness = [signature, keyPair.publicKey] + } else { + throw new Error("Unknown type of main UTXO") } } From d1a8c9ab23d4c0ec3895a4791be55cc314cf0197 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 26 Sep 2023 13:04:09 +0200 Subject: [PATCH 15/32] Added function descriptions --- typescript/src/bitcoin-network.ts | 8 ++- typescript/src/bitcoin.ts | 11 ++- typescript/src/deposit-sweep.ts | 99 +++++++++++++++++++++------ typescript/test/deposit-sweep.test.ts | 2 - 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/typescript/src/bitcoin-network.ts b/typescript/src/bitcoin-network.ts index c2805c9f7..c59a134f2 100644 --- a/typescript/src/bitcoin-network.ts +++ b/typescript/src/bitcoin-network.ts @@ -66,7 +66,13 @@ export function toBcoinNetwork(bitcoinNetwork: BitcoinNetwork): string { } } -// TODO: Description +/** + * Converts the provided {@link BitcoinNetwork} enumeration to a format expected + * by the `bitcoinjs-lib` library. + * @param bitcoinNetwork - Specified Bitcoin network. + * @returns Network representation compatible with the `bitcoinjs-lib` library. + * @throws An error if the network is not supported by `bitcoinjs-lib`. + */ export function toBitcoinJsLibNetwork( bitcoinNetwork: BitcoinNetwork ): networks.Network { diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 55f668c00..63936a464 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -742,7 +742,16 @@ export function isP2WSH(script: Buffer): boolean { } } -// TODO: Description and unit tests. +/** + * Generates a Bitcoin address based on the provided key pair and network. + * Can produce either SegWit (P2WPKH) or Legacy (P2PKH) addresses. + * @param keyPair - The key pair used to derive the Bitcoin address. + * @param network - Specified Bitcoin network. + * @param witness - Boolean flag indicating if the address should be SegWit + * (P2WPKH) or not (P2PKH). + * @returns The generated Bitcoin address as a string. + */ +// TODO: Unit tests. export function addressFromKeyPair( keyPair: Signer, network: networks.Network, diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index a37de71b9..06f769115 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -109,24 +109,25 @@ export async function submitDepositSweepTransaction( } /** - * Assembles a Bitcoin P2WPKH deposit sweep transaction. + * Constructs a Bitcoin deposit sweep transaction using provided UTXOs. * @dev The caller is responsible for ensuring the provided UTXOs are correctly * formed, can be spent by the wallet and their combined value is greater * then the fee. - * @param fee - the value that should be subtracted from the sum of the UTXOs - * values and used as the transaction fee. + * @param bitcoinNetwork - The target Bitcoin network (mainnet or testnet). + * @param fee - Transaction fee to be subtracted from the sum of the UTXOs' + * values. * @param walletPrivateKey - Bitcoin private key of the wallet in WIF format. - * @param witness - The parameter used to decide about the type of the new main - * UTXO output. P2WPKH if `true`, P2PKH if `false`. + * @param witness - Determines the type of the new main UTXO output: P2WPKH if + * `true`, P2PKH if `false`. * @param utxos - UTXOs from new deposit transactions. Must be P2(W)SH. - * @param deposits - Array of deposits. Each element corresponds to UTXO. - * The number of UTXOs and deposit elements must equal. - * @param mainUtxo - main UTXO of the wallet, which is a P2WKH UTXO resulting - * from the previous wallet transaction (optional). - * @returns The outcome consisting of: - * - the sweep transaction hash, - * - the new wallet's main UTXO produced by this transaction. - * - the sweep transaction in the raw format + * @param deposits - Deposit data corresponding to each UTXO. The number of + * UTXOs and deposits must match. + * @param mainUtxo - The wallet's main UTXO (optional), which is a P2(W)PKH UTXO + * from a previous transaction. + * @returns An object containing the sweep transaction hash, new wallet's main + * UTXO, and the raw deposit sweep transaction representation. + * @throws Error if the provided UTXOs and deposits mismatch or if an unsupported + * UTXO script type is encountered. */ export async function assembleDepositSweepTransaction( bitcoinNetwork: BitcoinNetwork, @@ -223,7 +224,7 @@ export async function assembleDepositSweepTransaction( transaction, inputIndex, deposit, - previousOutput.value, + previousOutputValue, keyPair ) } else if (isP2WSH(previousOutputScript)) { @@ -255,6 +256,19 @@ export async function assembleDepositSweepTransaction( } } +/** + * Signs the main UTXO transaction input and sets the appropriate script or + * witness data. + * @param transaction - The transaction containing the input to be signed. + * @param inputIndex - Index pointing to the input within the transaction. + * @param prevOutScript - The previous output script for the input. + * @param prevOutValue - The value from the previous transaction output. + * @param keyPair - A Signer object with the public and private key pair. + * @param network - The Bitcoin network type (mainnet or testnet). + * @returns An empty promise upon successful signing. + * @throws Error if the UTXO doesn't belong to the wallet, or if the script + * format is invalid or unknown. + */ async function signMainUtxoInput( transaction: Transaction, inputIndex: number, @@ -322,7 +336,15 @@ async function signMainUtxoInput( } } -// TODO: Description. +/** + * 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 deposit - Details of the deposit transaction. + * @param prevOutValue - The value from the previous transaction output. + * @param keyPair - A Signer object with the public and private key pair. + * @returns An empty promise upon successful signing. + */ async function signP2SHDepositInput( transaction: Transaction, inputIndex: number, @@ -330,8 +352,11 @@ async function signP2SHDepositInput( prevOutValue: number, keyPair: Signer ) { - const { walletPublicKey, depositScript } = - await prepareInputSignDataBitcoinIsLib(deposit, prevOutValue, keyPair) + const { walletPublicKey, depositScript } = await prepareInputSignData( + deposit, + prevOutValue, + keyPair + ) const sigHashType = Transaction.SIGHASH_ALL @@ -351,7 +376,15 @@ async function signP2SHDepositInput( transaction.ins[inputIndex].script = script.compile(scriptSig) } -// TODO: Description. +/** + * 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 deposit - Details of the deposit transaction. + * @param prevOutValue - The value from the previous transaction output. + * @param keyPair - A Signer object with the public and private key pair. + * @returns An empty promise upon successful signing. + */ async function signP2WSHDepositInput( transaction: Transaction, inputIndex: number, @@ -360,7 +393,7 @@ async function signP2WSHDepositInput( keyPair: Signer ) { const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignDataBitcoinIsLib(deposit, prevOutValue, keyPair) + await prepareInputSignData(deposit, prevOutValue, keyPair) const sigHashType = Transaction.SIGHASH_ALL @@ -381,7 +414,18 @@ async function signP2WSHDepositInput( transaction.ins[inputIndex].witness = witness } -async function prepareInputSignDataBitcoinIsLib( +/** + * Prepares data for signing a deposit transaction input. + * @param deposit - The deposit details. + * @param prevOutValue - The value from the previous transaction output. + * @param ecPair - A Signer object with the public and private key pair. + * @returns A Promise resolving to: + * - walletPublicKey: Hexstring representation of the wallet's public key. + * - depositScript: Buffer containing the assembled deposit script. + * - previousOutputValue: Numeric value of the prior transaction output. + * @throws Error if there are discrepancies in values or key formats. + */ +async function prepareInputSignData( deposit: Deposit, prevOutValue: number, ecPair: Signer @@ -457,7 +501,20 @@ export async function submitDepositSweepProof( ) } -// TODO: Description and unit test. +/** + * Checks if a UTXO is owned by a provided key pair based on its previous output + * script. + * @dev The function assumes previous output script comes form the P2PKH or + * P2WPKH UTXO. + * @param keyPair - A Signer object containing the public key and private key + * pair. + * @param prevOutScript - A Buffer containing the previous output script of the + * UTXO. + * @param network - The Bitcoin network configuration, i.e. mainnet or testnet. + * @returns A boolean indicating whether the derived address from the UTXO's + * previous output script matches either of the P2PKH or P2WPKH + * addresses derived from the provided key pair. + */ export function ownsUtxo( keyPair: Signer, prevOutScript: Buffer, diff --git a/typescript/test/deposit-sweep.test.ts b/typescript/test/deposit-sweep.test.ts index 0d429efd3..be41c9a81 100644 --- a/typescript/test/deposit-sweep.test.ts +++ b/typescript/test/deposit-sweep.test.ts @@ -1052,8 +1052,6 @@ describe("Sweep", () => { let bridge: MockBridge beforeEach(async () => { - bcoin.set("testnet") - bitcoinClient = new MockBitcoinClient() bridge = new MockBridge() From bc969a945b64bb6f37d2586bd96a0a3e1000e223 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 10:32:49 +0200 Subject: [PATCH 16/32] Renamed functions for checking script type --- typescript/src/bitcoin.ts | 8 ++++---- typescript/src/deposit-sweep.ts | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 63936a464..83f868ea5 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -691,7 +691,7 @@ export function readCompactSizeUint(varLenData: Hex): { * @param script The script to be checked. * @returns True if the script is P2PKH, false otherwise. */ -export function isP2PKH(script: Buffer): boolean { +export function isP2PKHScript(script: Buffer): boolean { try { payments.p2pkh({ output: script }) return true @@ -705,7 +705,7 @@ export function isP2PKH(script: Buffer): boolean { * @param script The script to be checked. * @returns True if the script is P2WPKH, false otherwise. */ -export function isP2WPKH(script: Buffer): boolean { +export function isP2WPKHScript(script: Buffer): boolean { try { payments.p2wpkh({ output: script }) return true @@ -719,7 +719,7 @@ export function isP2WPKH(script: Buffer): boolean { * @param script The script to be checked. * @returns True if the script is P2SH, false otherwise. */ -export function isP2SH(script: Buffer): boolean { +export function isP2SHScript(script: Buffer): boolean { try { payments.p2sh({ output: script }) return true @@ -733,7 +733,7 @@ export function isP2SH(script: Buffer): boolean { * @param script The script to be checked. * @returns True if the script is P2WSH, false otherwise. */ -export function isP2WSH(script: Buffer): boolean { +export function isP2WSHScript(script: Buffer): boolean { try { payments.p2wsh({ output: script }) return true diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 06f769115..70c42c42a 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -17,10 +17,10 @@ import { addressFromKeyPair, TransactionHash, computeHash160, - isP2PKH, - isP2WPKH, - isP2SH, - isP2WSH, + isP2PKHScript, + isP2WPKHScript, + isP2SHScript, + isP2WSHScript, } from "./bitcoin" import { assembleDepositScript, Deposit } from "./deposit" import { Bridge, Identifier } from "./chain" @@ -218,7 +218,7 @@ export async function assembleDepositSweepTransaction( const deposit = deposits[depositIndex] - if (isP2SH(previousOutputScript)) { + if (isP2SHScript(previousOutputScript)) { // P2SH (deposit UTXO) await signP2SHDepositInput( transaction, @@ -227,7 +227,7 @@ export async function assembleDepositSweepTransaction( previousOutputValue, keyPair ) - } else if (isP2WSH(previousOutputScript)) { + } else if (isP2WSHScript(previousOutputScript)) { // P2WSH (deposit UTXO) await signP2WSHDepositInput( transaction, @@ -283,7 +283,7 @@ async function signMainUtxoInput( const sigHashType = Transaction.SIGHASH_ALL - if (isP2PKH(prevOutScript)) { + if (isP2PKHScript(prevOutScript)) { // P2PKH const sigHash = transaction.hashForSignature( inputIndex, @@ -302,7 +302,7 @@ async function signMainUtxoInput( }).input! transaction.ins[inputIndex].script = scriptSig - } else if (isP2WPKH(prevOutScript)) { + } else if (isP2WPKHScript(prevOutScript)) { // P2WPKH const decompiledScript = script.decompile(prevOutScript) if ( From 3fee28303edea7886229914ae8fa6c909c3daa84 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 10:40:57 +0200 Subject: [PATCH 17/32] Disabled linitng error --- typescript/src/deposit-sweep.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 70c42c42a..d2e6b452e 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -25,7 +25,7 @@ import { import { assembleDepositScript, Deposit } from "./deposit" import { Bridge, Identifier } from "./chain" import { assembleTransactionProof } from "./proof" -import { ECPairFactory as ecFactory } from "ecpair" +import { ECPairFactory } from "ecpair" import * as tinysecp from "tiny-secp256k1" import { BitcoinNetwork, toBitcoinJsLibNetwork } from "./bitcoin-network" @@ -151,7 +151,8 @@ export async function assembleDepositSweepTransaction( } const network = toBitcoinJsLibNetwork(bitcoinNetwork) - const keyPair = ecFactory(tinysecp).fromWIF(walletPrivateKey, network) + // eslint-disable-next-line new-cap + const keyPair = ECPairFactory(tinysecp).fromWIF(walletPrivateKey, network) const walletAddress = addressFromKeyPair(keyPair, network, witness) // Calculate the value of transaction's output. Note that the value of fee From d021ddc33d40a28768021528180d8d0c088b8c02 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 11:01:32 +0200 Subject: [PATCH 18/32] Refactored address generating function --- typescript/src/bitcoin.ts | 37 +++++++++++++++++++-------------- typescript/src/deposit-sweep.ts | 9 ++++++-- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 83f868ea5..4679c9bde 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -3,9 +3,12 @@ import wif from "wif" import bufio from "bufio" import { BigNumber, utils } from "ethers" import { Hex } from "./hex" -import { BitcoinNetwork, toBcoinNetwork } from "./bitcoin-network" -import { payments, networks } from "bitcoinjs-lib" -import { Signer } from "ecpair" +import { + BitcoinNetwork, + toBcoinNetwork, + toBitcoinJsLibNetwork, +} from "./bitcoin-network" +import { payments } from "bitcoinjs-lib" /** * Represents a transaction hash (or transaction ID) as an un-prefixed hex @@ -743,25 +746,27 @@ export function isP2WSHScript(script: Buffer): boolean { } /** - * Generates a Bitcoin address based on the provided key pair and network. - * Can produce either SegWit (P2WPKH) or Legacy (P2PKH) addresses. - * @param keyPair - The key pair used to derive the Bitcoin address. - * @param network - Specified Bitcoin network. - * @param witness - Boolean flag indicating if the address should be SegWit - * (P2WPKH) or not (P2PKH). - * @returns The generated Bitcoin address as a string. + * Generates a Bitcoin address from a public key. Supports SegWit (P2WPKH) and + * Legacy (P2PKH) formats. + * @param publicKey - Public key used to derive the Bitcoin address. + * @param bitcoinNetwork - Target Bitcoin network. + * @param witness - Flag to determine address format: true for SegWit (P2WPKH) + * and false for Legacy (P2PKH). Default is true. + * @returns The derived Bitcoin address. */ // TODO: Unit tests. -export function addressFromKeyPair( - keyPair: Signer, - network: networks.Network, - witness: boolean +export function publicKeyToAddress( + publicKey: Hex, + bitcoinNetwork: BitcoinNetwork, + witness: boolean = true ): string { + const network = toBitcoinJsLibNetwork(bitcoinNetwork) + if (witness) { // P2WPKH (SegWit) - return payments.p2wpkh({ pubkey: keyPair.publicKey, network }).address! + return payments.p2wpkh({ pubkey: publicKey.toBuffer(), network }).address! } else { // P2PKH (Legacy) - return payments.p2pkh({ pubkey: keyPair.publicKey, network }).address! + return payments.p2pkh({ pubkey: publicKey.toBuffer(), network }).address! } } diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index d2e6b452e..0e744c370 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -8,13 +8,14 @@ import { networks, } from "bitcoinjs-lib" import { BigNumber } from "ethers" +import { Hex } from "./hex" import { RawTransaction, UnspentTransactionOutput, Client as BitcoinClient, decomposeRawTransaction, isCompressedPublicKey, - addressFromKeyPair, + publicKeyToAddress, TransactionHash, computeHash160, isP2PKHScript, @@ -153,7 +154,11 @@ export async function assembleDepositSweepTransaction( const network = toBitcoinJsLibNetwork(bitcoinNetwork) // eslint-disable-next-line new-cap const keyPair = ECPairFactory(tinysecp).fromWIF(walletPrivateKey, network) - const walletAddress = addressFromKeyPair(keyPair, network, witness) + const walletAddress = publicKeyToAddress( + Hex.from(keyPair.publicKey), + bitcoinNetwork, + witness + ) // Calculate the value of transaction's output. Note that the value of fee // needs to be subtracted from the sum. From 770032c86d128432817958704bb94decda957f84 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 11:07:41 +0200 Subject: [PATCH 19/32] Renamed UTXO value sum variable --- typescript/src/deposit-sweep.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 0e744c370..8021be431 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -160,16 +160,14 @@ export async function assembleDepositSweepTransaction( witness ) - // Calculate the value of transaction's output. Note that the value of fee - // needs to be subtracted from the sum. - let totalInputValue = BigNumber.from(0) + let outputValue = BigNumber.from(0) if (mainUtxo) { - totalInputValue = totalInputValue.add(mainUtxo.value) + outputValue = outputValue.add(mainUtxo.value) } for (const utxo of utxos) { - totalInputValue = totalInputValue.add(utxo.value) + outputValue = outputValue.add(utxo.value) } - totalInputValue = totalInputValue.sub(fee) + outputValue = outputValue.sub(fee) // Create the transaction. const transaction = new Transaction() @@ -190,7 +188,7 @@ export async function assembleDepositSweepTransaction( // Add transaction output. const scriptPubKey = address.toOutputScript(walletAddress, network) - transaction.addOutput(scriptPubKey, totalInputValue.toNumber()) + transaction.addOutput(scriptPubKey, outputValue.toNumber()) // Sign the main UTXO input if there is main UTXO. if (mainUtxo) { From 039b2a9ed4f01ff7f0d9e92ec38685f1f29e5433 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 11:12:50 +0200 Subject: [PATCH 20/32] Moved input adding and output value calculation under the same loop --- typescript/src/deposit-sweep.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 8021be431..bf001cbd7 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -160,33 +160,25 @@ export async function assembleDepositSweepTransaction( witness ) - let outputValue = BigNumber.from(0) - if (mainUtxo) { - outputValue = outputValue.add(mainUtxo.value) - } - for (const utxo of utxos) { - outputValue = outputValue.add(utxo.value) - } - outputValue = outputValue.sub(fee) - - // Create the transaction. const transaction = new Transaction() - // Add the transaction's inputs. + let outputValue = BigNumber.from(0) if (mainUtxo) { transaction.addInput( mainUtxo.transactionHash.reverse().toBuffer(), mainUtxo.outputIndex ) + outputValue = outputValue.add(mainUtxo.value) } for (const utxo of utxos) { transaction.addInput( utxo.transactionHash.reverse().toBuffer(), utxo.outputIndex ) + outputValue = outputValue.add(utxo.value) } + outputValue = outputValue.sub(fee) - // Add transaction output. const scriptPubKey = address.toOutputScript(walletAddress, network) transaction.addOutput(scriptPubKey, outputValue.toNumber()) From 536c989a83f164165d5ab435ea2f307654c0cac5 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 11:24:12 +0200 Subject: [PATCH 21/32] Used createOutputScriptFromAddress --- typescript/src/deposit-sweep.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index bf001cbd7..4ef9be88a 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -3,7 +3,6 @@ import { Stack, Signer, payments, - address, script, networks, } from "bitcoinjs-lib" @@ -22,6 +21,7 @@ import { isP2WPKHScript, isP2SHScript, isP2WSHScript, + createOutputScriptFromAddress, } from "./bitcoin" import { assembleDepositScript, Deposit } from "./deposit" import { Bridge, Identifier } from "./chain" @@ -179,8 +179,8 @@ export async function assembleDepositSweepTransaction( } outputValue = outputValue.sub(fee) - const scriptPubKey = address.toOutputScript(walletAddress, network) - transaction.addOutput(scriptPubKey, outputValue.toNumber()) + const outputScript = createOutputScriptFromAddress(walletAddress) + transaction.addOutput(outputScript.toBuffer(), outputValue.toNumber()) // Sign the main UTXO input if there is main UTXO. if (mainUtxo) { From 59fb549db1ebf7f92310cd5c1ac05dbdf9f34630 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 11:34:30 +0200 Subject: [PATCH 22/32] Key pair variable rename --- typescript/src/deposit-sweep.ts | 49 +++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 4ef9be88a..a49a47f29 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -153,9 +153,12 @@ export async function assembleDepositSweepTransaction( const network = toBitcoinJsLibNetwork(bitcoinNetwork) // eslint-disable-next-line new-cap - const keyPair = ECPairFactory(tinysecp).fromWIF(walletPrivateKey, network) + const walletKeyPair = ECPairFactory(tinysecp).fromWIF( + walletPrivateKey, + network + ) const walletAddress = publicKeyToAddress( - Hex.from(keyPair.publicKey), + Hex.from(walletKeyPair.publicKey), bitcoinNetwork, witness ) @@ -194,7 +197,7 @@ export async function assembleDepositSweepTransaction( inputIndex, previousOutput.script, previousOutput.value, - keyPair, + walletKeyPair, network ) } @@ -221,7 +224,7 @@ export async function assembleDepositSweepTransaction( inputIndex, deposit, previousOutputValue, - keyPair + walletKeyPair ) } else if (isP2WSHScript(previousOutputScript)) { // P2WSH (deposit UTXO) @@ -230,7 +233,7 @@ export async function assembleDepositSweepTransaction( inputIndex, deposit, previousOutputValue, - keyPair + walletKeyPair ) } else { throw new Error("Unsupported UTXO script type") @@ -259,7 +262,7 @@ export async function assembleDepositSweepTransaction( * @param inputIndex - Index pointing to the input within the transaction. * @param prevOutScript - The previous output script for the input. * @param prevOutValue - The value from the previous transaction output. - * @param keyPair - A Signer object with the public and private key pair. + * @param walletKeyPair - A Signer object with the public and private key pair. * @param network - The Bitcoin network type (mainnet or testnet). * @returns An empty promise upon successful signing. * @throws Error if the UTXO doesn't belong to the wallet, or if the script @@ -270,10 +273,10 @@ async function signMainUtxoInput( inputIndex: number, prevOutScript: Buffer, prevOutValue: number, - keyPair: Signer, + walletKeyPair: Signer, network: networks.Network ) { - if (!ownsUtxo(keyPair, prevOutScript, network)) { + if (!ownsUtxo(walletKeyPair, prevOutScript, network)) { throw new Error("UTXO does not belong to the wallet") } @@ -288,13 +291,13 @@ async function signMainUtxoInput( ) const signature = script.signature.encode( - keyPair.sign(sigHash), + walletKeyPair.sign(sigHash), sigHashType ) const scriptSig = payments.p2pkh({ signature: signature, - pubkey: keyPair.publicKey, + pubkey: walletKeyPair.publicKey, }).input! transaction.ins[inputIndex].script = scriptSig @@ -322,11 +325,11 @@ async function signMainUtxoInput( ) const signature = script.signature.encode( - keyPair.sign(sigHash), + walletKeyPair.sign(sigHash), sigHashType ) - transaction.ins[inputIndex].witness = [signature, keyPair.publicKey] + transaction.ins[inputIndex].witness = [signature, walletKeyPair.publicKey] } else { throw new Error("Unknown type of main UTXO") } @@ -338,7 +341,7 @@ async function signMainUtxoInput( * @param inputIndex - Index pointing to the input within the transaction. * @param deposit - Details of the deposit transaction. * @param prevOutValue - The value from the previous transaction output. - * @param keyPair - A Signer object with the public and private key pair. + * @param walletKeyPair - A Signer object with the public and private key pair. * @returns An empty promise upon successful signing. */ async function signP2SHDepositInput( @@ -346,12 +349,12 @@ async function signP2SHDepositInput( inputIndex: number, deposit: Deposit, prevOutValue: number, - keyPair: Signer + walletKeyPair: Signer ) { const { walletPublicKey, depositScript } = await prepareInputSignData( deposit, prevOutValue, - keyPair + walletKeyPair ) const sigHashType = Transaction.SIGHASH_ALL @@ -362,7 +365,10 @@ async function signP2SHDepositInput( sigHashType ) - const signature = script.signature.encode(keyPair.sign(sigHash), sigHashType) + const signature = script.signature.encode( + walletKeyPair.sign(sigHash), + sigHashType + ) const scriptSig: Stack = [] scriptSig.push(signature) @@ -378,7 +384,7 @@ async function signP2SHDepositInput( * @param inputIndex - Index pointing to the input within the transaction. * @param deposit - Details of the deposit transaction. * @param prevOutValue - The value from the previous transaction output. - * @param keyPair - A Signer object with the public and private key pair. + * @param walletKeyPair - A Signer object with the public and private key pair. * @returns An empty promise upon successful signing. */ async function signP2WSHDepositInput( @@ -386,10 +392,10 @@ async function signP2WSHDepositInput( inputIndex: number, deposit: Deposit, prevOutValue: number, - keyPair: Signer + walletKeyPair: Signer ) { const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignData(deposit, prevOutValue, keyPair) + await prepareInputSignData(deposit, prevOutValue, walletKeyPair) const sigHashType = Transaction.SIGHASH_ALL @@ -400,7 +406,10 @@ async function signP2WSHDepositInput( sigHashType ) - const signature = script.signature.encode(keyPair.sign(sigHash), sigHashType) + const signature = script.signature.encode( + walletKeyPair.sign(sigHash), + sigHashType + ) const witness: Buffer[] = [] witness.push(signature) From be1b1fe44aa1784fb28c630a62dcc1b10a85e0a6 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 11:40:06 +0200 Subject: [PATCH 23/32] Replaced network type with Bitcoin Network --- typescript/src/deposit-sweep.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index a49a47f29..2b4ec06cd 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -1,11 +1,4 @@ -import { - Transaction, - Stack, - Signer, - payments, - script, - networks, -} from "bitcoinjs-lib" +import { Transaction, Stack, Signer, payments, script } from "bitcoinjs-lib" import { BigNumber } from "ethers" import { Hex } from "./hex" import { @@ -198,7 +191,7 @@ export async function assembleDepositSweepTransaction( previousOutput.script, previousOutput.value, walletKeyPair, - network + bitcoinNetwork ) } @@ -263,7 +256,7 @@ export async function assembleDepositSweepTransaction( * @param prevOutScript - The previous output script for the input. * @param prevOutValue - The value from the previous transaction output. * @param walletKeyPair - A Signer object with the public and private key pair. - * @param network - The Bitcoin network type (mainnet or testnet). + * @param bitcoinNetwork - The Bitcoin network type. * @returns An empty promise upon successful signing. * @throws Error if the UTXO doesn't belong to the wallet, or if the script * format is invalid or unknown. @@ -274,9 +267,9 @@ async function signMainUtxoInput( prevOutScript: Buffer, prevOutValue: number, walletKeyPair: Signer, - network: networks.Network + bitcoinNetwork: BitcoinNetwork ) { - if (!ownsUtxo(walletKeyPair, prevOutScript, network)) { + if (!ownsUtxo(walletKeyPair, prevOutScript, bitcoinNetwork)) { throw new Error("UTXO does not belong to the wallet") } @@ -515,7 +508,7 @@ export async function submitDepositSweepProof( * pair. * @param prevOutScript - A Buffer containing the previous output script of the * UTXO. - * @param network - The Bitcoin network configuration, i.e. mainnet or testnet. + * @param bitcoinNetwork - The Bitcoin network type. * @returns A boolean indicating whether the derived address from the UTXO's * previous output script matches either of the P2PKH or P2WPKH * addresses derived from the provided key pair. @@ -523,8 +516,10 @@ export async function submitDepositSweepProof( export function ownsUtxo( keyPair: Signer, prevOutScript: Buffer, - network: networks.Network + bitcoinNetwork: BitcoinNetwork ): boolean { + const network = toBitcoinJsLibNetwork(bitcoinNetwork) + // Derive P2PKH and P2WPKH addresses from the public key. const p2pkhAddress = payments.p2pkh({ pubkey: keyPair.publicKey, network }).address || "" From 783a3bb9d9328e550f738ed4be9b1ba588859cb9 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 11:46:46 +0200 Subject: [PATCH 24/32] Passed whole previous output instead of splitting --- typescript/src/deposit-sweep.ts | 39 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 2b4ec06cd..4355293c5 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -1,4 +1,11 @@ -import { Transaction, Stack, Signer, payments, script } from "bitcoinjs-lib" +import { + Transaction, + TxOutput, + Stack, + Signer, + payments, + script, +} from "bitcoinjs-lib" import { BigNumber } from "ethers" import { Hex } from "./hex" import { @@ -188,8 +195,7 @@ export async function assembleDepositSweepTransaction( await signMainUtxoInput( transaction, inputIndex, - previousOutput.script, - previousOutput.value, + previousOutput, walletKeyPair, bitcoinNetwork ) @@ -253,9 +259,9 @@ export async function assembleDepositSweepTransaction( * witness data. * @param transaction - The transaction containing the input to be signed. * @param inputIndex - Index pointing to the input within the transaction. - * @param prevOutScript - The previous output script for the input. - * @param prevOutValue - The value from the previous transaction output. - * @param walletKeyPair - A Signer object with the public and private key pair. + * @param previousOutput - The previous output for the main UTXO input. + * @param walletKeyPair - A Signer object with the wallet's public and private + * key pair. * @param bitcoinNetwork - The Bitcoin network type. * @returns An empty promise upon successful signing. * @throws Error if the UTXO doesn't belong to the wallet, or if the script @@ -264,22 +270,21 @@ export async function assembleDepositSweepTransaction( async function signMainUtxoInput( transaction: Transaction, inputIndex: number, - prevOutScript: Buffer, - prevOutValue: number, + previousOutput: TxOutput, walletKeyPair: Signer, bitcoinNetwork: BitcoinNetwork ) { - if (!ownsUtxo(walletKeyPair, prevOutScript, bitcoinNetwork)) { + if (!ownsUtxo(walletKeyPair, previousOutput.script, bitcoinNetwork)) { throw new Error("UTXO does not belong to the wallet") } const sigHashType = Transaction.SIGHASH_ALL - if (isP2PKHScript(prevOutScript)) { + if (isP2PKHScript(previousOutput.script)) { // P2PKH const sigHash = transaction.hashForSignature( inputIndex, - prevOutScript, + previousOutput.script, sigHashType ) @@ -294,9 +299,9 @@ async function signMainUtxoInput( }).input! transaction.ins[inputIndex].script = scriptSig - } else if (isP2WPKHScript(prevOutScript)) { + } else if (isP2WPKHScript(previousOutput.script)) { // P2WPKH - const decompiledScript = script.decompile(prevOutScript) + const decompiledScript = script.decompile(previousOutput.script) if ( !decompiledScript || decompiledScript.length !== 2 || @@ -313,7 +318,7 @@ async function signMainUtxoInput( const sigHash = transaction.hashForWitnessV0( inputIndex, p2pkhScript, - prevOutValue, + previousOutput.value, sigHashType ) @@ -334,7 +339,8 @@ async function signMainUtxoInput( * @param inputIndex - Index pointing to the input within the transaction. * @param deposit - Details of the deposit transaction. * @param prevOutValue - The value from the previous transaction output. - * @param walletKeyPair - A Signer object with the public and private key pair. + * @param walletKeyPair - A Signer object with the wallet's public and private + * key pair. * @returns An empty promise upon successful signing. */ async function signP2SHDepositInput( @@ -377,7 +383,8 @@ async function signP2SHDepositInput( * @param inputIndex - Index pointing to the input within the transaction. * @param deposit - Details of the deposit transaction. * @param prevOutValue - The value from the previous transaction output. - * @param walletKeyPair - A Signer object with the public and private key pair. + * @param walletKeyPair - A Signer object with the wallet's public and private + * key pair. * @returns An empty promise upon successful signing. */ async function signP2WSHDepositInput( From 98b622a5628e24d69b073ce22d713a8995ce0b2a Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 12:03:32 +0200 Subject: [PATCH 25/32] Refactored deposit assembling --- typescript/src/deposit-sweep.ts | 57 +++++++++++++++------------------ 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 4355293c5..2ea592072 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -338,7 +338,7 @@ async function signMainUtxoInput( * @param transaction - The transaction containing the input to be signed. * @param inputIndex - Index pointing to the input within the transaction. * @param deposit - Details of the deposit transaction. - * @param prevOutValue - The value from the previous transaction output. + * @param previousOutputValue - The value from the previous transaction output. * @param walletKeyPair - A Signer object with the wallet's public and private * key pair. * @returns An empty promise upon successful signing. @@ -347,12 +347,12 @@ async function signP2SHDepositInput( transaction: Transaction, inputIndex: number, deposit: Deposit, - prevOutValue: number, + previousOutputValue: number, walletKeyPair: Signer ) { - const { walletPublicKey, depositScript } = await prepareInputSignData( + const depositScript = await prepareDepositScript( deposit, - prevOutValue, + previousOutputValue, walletKeyPair ) @@ -371,7 +371,7 @@ async function signP2SHDepositInput( const scriptSig: Stack = [] scriptSig.push(signature) - scriptSig.push(Buffer.from(walletPublicKey, "hex")) + scriptSig.push(walletKeyPair.publicKey) scriptSig.push(depositScript) transaction.ins[inputIndex].script = script.compile(scriptSig) @@ -382,7 +382,7 @@ async function signP2SHDepositInput( * @param transaction - The transaction containing the input to be signed. * @param inputIndex - Index pointing to the input within the transaction. * @param deposit - Details of the deposit transaction. - * @param prevOutValue - The value from the previous transaction output. + * @param previousOutputValue - The value from the previous transaction output. * @param walletKeyPair - A Signer object with the wallet's public and private * key pair. * @returns An empty promise upon successful signing. @@ -391,11 +391,14 @@ async function signP2WSHDepositInput( transaction: Transaction, inputIndex: number, deposit: Deposit, - prevOutValue: number, + previousOutputValue: number, walletKeyPair: Signer ) { - const { walletPublicKey, depositScript, previousOutputValue } = - await prepareInputSignData(deposit, prevOutValue, walletKeyPair) + const depositScript = await prepareDepositScript( + deposit, + previousOutputValue, + walletKeyPair + ) const sigHashType = Transaction.SIGHASH_ALL @@ -413,37 +416,31 @@ async function signP2WSHDepositInput( const witness: Buffer[] = [] witness.push(signature) - witness.push(Buffer.from(walletPublicKey, "hex")) + witness.push(walletKeyPair.publicKey) witness.push(depositScript) transaction.ins[inputIndex].witness = witness } /** - * Prepares data for signing a deposit transaction input. + * Assembles the deposit script based on the given deposit details. Performs + * validations on values and key formats. * @param deposit - The deposit details. - * @param prevOutValue - The value from the previous transaction output. - * @param ecPair - A Signer object with the public and private key pair. - * @returns A Promise resolving to: - * - walletPublicKey: Hexstring representation of the wallet's public key. - * - depositScript: Buffer containing the assembled deposit script. - * - previousOutputValue: Numeric value of the prior transaction output. + * @param previousOutputValue - Value from the previous transaction output. + * @param walletKeyPair - Signer object containing the wallet'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. */ -async function prepareInputSignData( +async function prepareDepositScript( deposit: Deposit, - prevOutValue: number, - ecPair: Signer -): Promise<{ - walletPublicKey: string - depositScript: any - previousOutputValue: number -}> { - if (prevOutValue != deposit.amount.toNumber()) { + previousOutputValue: number, + walletKeyPair: Signer +): Promise { + if (previousOutputValue != deposit.amount.toNumber()) { throw new Error("Mismatch between amount in deposit and deposit tx") } - const walletPublicKey = ecPair.publicKey.toString("hex") + const walletPublicKey = walletKeyPair.publicKey.toString("hex") if (computeHash160(walletPublicKey) != deposit.walletPublicKeyHash) { throw new Error( @@ -463,11 +460,7 @@ async function prepareInputSignData( "hex" ) - return { - walletPublicKey, - depositScript: depositScript, - previousOutputValue: deposit.amount.toNumber(), - } + return depositScript } /** From e5c6ea2969c82701458e928bf3ff34c8f9585585 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 12:09:01 +0200 Subject: [PATCH 26/32] Renamed output script variable --- typescript/src/deposit-sweep.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 2ea592072..bdd66a8c8 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -506,7 +506,7 @@ export async function submitDepositSweepProof( * P2WPKH UTXO. * @param keyPair - A Signer object containing the public key and private key * pair. - * @param prevOutScript - A Buffer containing the previous output script of the + * @param outputScript - A Buffer containing the previous output script of the * UTXO. * @param bitcoinNetwork - The Bitcoin network type. * @returns A boolean indicating whether the derived address from the UTXO's @@ -515,7 +515,7 @@ export async function submitDepositSweepProof( */ export function ownsUtxo( keyPair: Signer, - prevOutScript: Buffer, + outputScript: Buffer, bitcoinNetwork: BitcoinNetwork ): boolean { const network = toBitcoinJsLibNetwork(bitcoinNetwork) @@ -526,19 +526,19 @@ export function ownsUtxo( const p2wpkhAddress = payments.p2wpkh({ pubkey: keyPair.publicKey, network }).address || "" - // Try to extract an address from the provided prevOutScript. + // Try to extract an address from the provided output script. let addressFromOutput = "" try { addressFromOutput = - payments.p2pkh({ output: prevOutScript, network }).address || "" + payments.p2pkh({ output: outputScript, network }).address || "" } catch (e) { // If not P2PKH, try P2WPKH. try { addressFromOutput = - payments.p2wpkh({ output: prevOutScript, network }).address || "" + payments.p2wpkh({ output: outputScript, network }).address || "" } catch (err) { - // If neither p2pkh nor p2wpkh address can be derived, assume the previous - // output script comes from a different UTXO type or is corrupted. + // If neither p2pkh nor p2wpkh address can be derived, assume the output + // script comes from a different UTXO type or is corrupted. return false } } From 6099e46a5c0bac81e7d1c9776f5d41712d1581d0 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 12:15:49 +0200 Subject: [PATCH 27/32] Refactored UTXO ownership validation function --- typescript/src/deposit-sweep.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index bdd66a8c8..323b67706 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -274,7 +274,13 @@ async function signMainUtxoInput( walletKeyPair: Signer, bitcoinNetwork: BitcoinNetwork ) { - if (!ownsUtxo(walletKeyPair, previousOutput.script, bitcoinNetwork)) { + if ( + !ownsUtxo( + Hex.from(walletKeyPair.publicKey), + previousOutput.script, + bitcoinNetwork + ) + ) { throw new Error("UTXO does not belong to the wallet") } @@ -500,21 +506,17 @@ export async function submitDepositSweepProof( } /** - * Checks if a UTXO is owned by a provided key pair based on its previous output - * script. - * @dev The function assumes previous output script comes form the P2PKH or - * P2WPKH UTXO. - * @param keyPair - A Signer object containing the public key and private key - * pair. - * @param outputScript - A Buffer containing the previous output script of the - * UTXO. + * Checks if the UTXO is owned by the provided public key based on its previous + * output script. + * @dev The function assumes output script comes form the P2PKH or P2WPKH UTXO. + * @param publicKey - Public key for UTXO ownership validation. + * @param outputScript - Buffer of the UTXO's previous output script. * @param bitcoinNetwork - The Bitcoin network type. - * @returns A boolean indicating whether the derived address from the UTXO's - * previous output script matches either of the P2PKH or P2WPKH - * addresses derived from the provided key pair. + * @returns True if the UTXO's address from the output script matches P2PKH + * or P2WPKH addresses derived from the key pair. False otherwise. */ export function ownsUtxo( - keyPair: Signer, + publicKey: Hex, outputScript: Buffer, bitcoinNetwork: BitcoinNetwork ): boolean { @@ -522,9 +524,9 @@ export function ownsUtxo( // Derive P2PKH and P2WPKH addresses from the public key. const p2pkhAddress = - payments.p2pkh({ pubkey: keyPair.publicKey, network }).address || "" + payments.p2pkh({ pubkey: publicKey.toBuffer(), network }).address || "" const p2wpkhAddress = - payments.p2wpkh({ pubkey: keyPair.publicKey, network }).address || "" + payments.p2wpkh({ pubkey: publicKey.toBuffer(), network }).address || "" // Try to extract an address from the provided output script. let addressFromOutput = "" From b0eac234470947361c8235fbff563bf48f96eb3d Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 14:46:14 +0200 Subject: [PATCH 28/32] Refactor function checking if UTXO can be spent --- typescript/src/deposit-sweep.ts | 65 ++++++++------------------------- 1 file changed, 15 insertions(+), 50 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 323b67706..8fb7e4687 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -196,8 +196,7 @@ export async function assembleDepositSweepTransaction( transaction, inputIndex, previousOutput, - walletKeyPair, - bitcoinNetwork + walletKeyPair ) } @@ -271,15 +270,10 @@ async function signMainUtxoInput( transaction: Transaction, inputIndex: number, previousOutput: TxOutput, - walletKeyPair: Signer, - bitcoinNetwork: BitcoinNetwork + walletKeyPair: Signer ) { if ( - !ownsUtxo( - Hex.from(walletKeyPair.publicKey), - previousOutput.script, - bitcoinNetwork - ) + !canSpendOutput(Hex.from(walletKeyPair.publicKey), previousOutput.script) ) { throw new Error("UTXO does not belong to the wallet") } @@ -506,47 +500,18 @@ export async function submitDepositSweepProof( } /** - * Checks if the UTXO is owned by the provided public key based on its previous - * output script. - * @dev The function assumes output script comes form the P2PKH or P2WPKH UTXO. - * @param publicKey - Public key for UTXO ownership validation. - * @param outputScript - Buffer of the UTXO's previous output script. - * @param bitcoinNetwork - The Bitcoin network type. - * @returns True if the UTXO's address from the output script matches P2PKH - * or P2WPKH addresses derived from the key pair. False otherwise. + * Determines if a UTXO's output script can be spent using the provided public + * key. + * @param publicKey - Public key used to derive the corresponding P2PKH and + * P2WPKH output scripts. + * @param outputScript - The output script of the UTXO in question. + * @returns True if the provided output script matches the P2PKH or P2WPKH + * output scripts derived from the given public key. False otherwise. */ -export function ownsUtxo( - publicKey: Hex, - outputScript: Buffer, - bitcoinNetwork: BitcoinNetwork -): boolean { - const network = toBitcoinJsLibNetwork(bitcoinNetwork) +function canSpendOutput(publicKey: Hex, outputScript: Buffer): boolean { + const pubkeyBuffer = publicKey.toBuffer() + const p2pkhOutput = payments.p2pkh({ pubkey: pubkeyBuffer }).output! + const p2wpkhOutput = payments.p2wpkh({ pubkey: pubkeyBuffer }).output! - // Derive P2PKH and P2WPKH addresses from the public key. - const p2pkhAddress = - payments.p2pkh({ pubkey: publicKey.toBuffer(), network }).address || "" - const p2wpkhAddress = - payments.p2wpkh({ pubkey: publicKey.toBuffer(), network }).address || "" - - // Try to extract an address from the provided output script. - let addressFromOutput = "" - try { - addressFromOutput = - payments.p2pkh({ output: outputScript, network }).address || "" - } catch (e) { - // If not P2PKH, try P2WPKH. - try { - addressFromOutput = - payments.p2wpkh({ output: outputScript, network }).address || "" - } catch (err) { - // If neither p2pkh nor p2wpkh address can be derived, assume the output - // script comes from a different UTXO type or is corrupted. - return false - } - } - - // Check if the UTXO's address matches either of the derived addresses. - return ( - addressFromOutput === p2pkhAddress || addressFromOutput === p2wpkhAddress - ) + return outputScript.equals(p2pkhOutput) || outputScript.equals(p2wpkhOutput) } From 3fa34d91cf1ded905b7f5abf9691f91144df09cb Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 15:58:51 +0200 Subject: [PATCH 29/32] Simplified script calculation for p2wpkh main UTXO --- typescript/src/deposit-sweep.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 8fb7e4687..386d1e3ff 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -301,18 +301,8 @@ async function signMainUtxoInput( transaction.ins[inputIndex].script = scriptSig } else if (isP2WPKHScript(previousOutput.script)) { // P2WPKH - const decompiledScript = script.decompile(previousOutput.script) - if ( - !decompiledScript || - decompiledScript.length !== 2 || - decompiledScript[0] !== 0x00 || - !Buffer.isBuffer(decompiledScript[1]) || - decompiledScript[1].length !== 20 - ) { - throw new Error("Invalid script format") - } - - const publicKeyHash = decompiledScript[1] + const publicKeyHash = payments.p2wpkh({ output: previousOutput.script }) + .hash! const p2pkhScript = payments.p2pkh({ hash: publicKeyHash }).output! const sigHash = transaction.hashForWitnessV0( From bfb593a632b1c42341bf63609010094878ef41d0 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 17:41:06 +0200 Subject: [PATCH 30/32] Added unit tests for createAddressFromPublicKey --- typescript/src/bitcoin.ts | 51 ++++++++++++++++----------------- typescript/src/deposit-sweep.ts | 4 +-- typescript/test/bitcoin.test.ts | 27 ++++++++++++++++- typescript/test/data/bitcoin.ts | 34 ++++++++++++++++++++++ 4 files changed, 87 insertions(+), 29 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 4679c9bde..8bf6b6a2e 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -649,6 +649,31 @@ export function createAddressFromOutputScript( ?.toString(toBcoinNetwork(network)) } +/** + * Creates the Bitcoin address from the public key. Supports SegWit (P2WPKH) and + * Legacy (P2PKH) formats. + * @param publicKey - Public key used to derive the Bitcoin address. + * @param bitcoinNetwork - Target Bitcoin network. + * @param witness - Flag to determine address format: true for SegWit (P2WPKH) + * and false for Legacy (P2PKH). Default is true. + * @returns The derived Bitcoin address. + */ +export function createAddressFromPublicKey( + publicKey: Hex, + bitcoinNetwork: BitcoinNetwork, + witness: boolean = true +): string { + const network = toBitcoinJsLibNetwork(bitcoinNetwork) + + if (witness) { + // P2WPKH (SegWit) + return payments.p2wpkh({ pubkey: publicKey.toBuffer(), network }).address! + } else { + // P2PKH (Legacy) + return payments.p2pkh({ pubkey: publicKey.toBuffer(), network }).address! + } +} + /** * Reads the leading compact size uint from the provided variable length data. * @@ -744,29 +769,3 @@ export function isP2WSHScript(script: Buffer): boolean { return false } } - -/** - * Generates a Bitcoin address from a public key. Supports SegWit (P2WPKH) and - * Legacy (P2PKH) formats. - * @param publicKey - Public key used to derive the Bitcoin address. - * @param bitcoinNetwork - Target Bitcoin network. - * @param witness - Flag to determine address format: true for SegWit (P2WPKH) - * and false for Legacy (P2PKH). Default is true. - * @returns The derived Bitcoin address. - */ -// TODO: Unit tests. -export function publicKeyToAddress( - publicKey: Hex, - bitcoinNetwork: BitcoinNetwork, - witness: boolean = true -): string { - const network = toBitcoinJsLibNetwork(bitcoinNetwork) - - if (witness) { - // P2WPKH (SegWit) - return payments.p2wpkh({ pubkey: publicKey.toBuffer(), network }).address! - } else { - // P2PKH (Legacy) - return payments.p2pkh({ pubkey: publicKey.toBuffer(), network }).address! - } -} diff --git a/typescript/src/deposit-sweep.ts b/typescript/src/deposit-sweep.ts index 386d1e3ff..0627382d7 100644 --- a/typescript/src/deposit-sweep.ts +++ b/typescript/src/deposit-sweep.ts @@ -14,7 +14,7 @@ import { Client as BitcoinClient, decomposeRawTransaction, isCompressedPublicKey, - publicKeyToAddress, + createAddressFromPublicKey, TransactionHash, computeHash160, isP2PKHScript, @@ -157,7 +157,7 @@ export async function assembleDepositSweepTransaction( walletPrivateKey, network ) - const walletAddress = publicKeyToAddress( + const walletAddress = createAddressFromPublicKey( Hex.from(walletKeyPair.publicKey), bitcoinNetwork, witness diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index 80eebfda5..c8a243f87 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -13,6 +13,7 @@ import { targetToDifficulty, createOutputScriptFromAddress, createAddressFromOutputScript, + createAddressFromPublicKey, readCompactSizeUint, computeHash160, computeHash256, @@ -21,7 +22,7 @@ import { calculateDepositRefundLocktime } from "../src/deposit" import { BitcoinNetwork } from "../src/bitcoin-network" import { Hex } from "../src/hex" import { BigNumber } from "ethers" -import { btcAddresses } from "./data/bitcoin" +import { btcAddresses, btcAddressFromPublicKey } from "./data/bitcoin" describe("Bitcoin", () => { describe("compressPublicKey", () => { @@ -535,6 +536,30 @@ describe("Bitcoin", () => { }) }) + describe("createAddressFromPublicKey", () => { + Object.entries(btcAddressFromPublicKey).forEach( + ([bitcoinNetwork, addressData]) => { + context(`with ${bitcoinNetwork} addresses`, () => { + Object.entries(addressData).forEach( + ([addressType, { publicKey, address }]) => { + it(`should return correct ${addressType} address for ${bitcoinNetwork}`, () => { + const witness = addressType === "P2WPKH" + const result = createAddressFromPublicKey( + publicKey, + bitcoinNetwork === "mainnet" + ? BitcoinNetwork.Mainnet + : BitcoinNetwork.Testnet, + witness + ) + expect(result).to.eq(address) + }) + } + ) + }) + } + ) + }) + describe("readCompactSizeUint", () => { context("when the compact size uint is 1-byte", () => { it("should return the the uint value and byte length", () => { diff --git a/typescript/test/data/bitcoin.ts b/typescript/test/data/bitcoin.ts index d44b4b737..b04baad81 100644 --- a/typescript/test/data/bitcoin.ts +++ b/typescript/test/data/bitcoin.ts @@ -70,3 +70,37 @@ export const btcAddresses: Record< }, }, } + +export const btcAddressFromPublicKey: Record< + Exclude, + Record +> = { + testnet: { + P2PKH: { + publicKey: Hex.from( + "0304cc460f320822d17d567a9a1b1039f765ff72512758605b5962226b3d8e5329" + ), + address: "msVQ3CCdqffxc5BtxUrtHFPq6CoZSTaJTq", + }, + P2WPKH: { + publicKey: Hex.from( + "0304cc460f320822d17d567a9a1b1039f765ff72512758605b5962226b3d8e5329" + ), + address: "tb1qsdtz442y5fmay39rj39vancf7jm0jrf40qkulw", + }, + }, + mainnet: { + P2PKH: { + publicKey: Hex.from( + "0304cc460f320822d17d567a9a1b1039f765ff72512758605b5962226b3d8e5329" + ), + address: "1CySk97f2eEhpxiHEutWTLBWEDCrZDbSCr", + }, + P2WPKH: { + publicKey: Hex.from( + "0304cc460f320822d17d567a9a1b1039f765ff72512758605b5962226b3d8e5329" + ), + address: "bc1qsdtz442y5fmay39rj39vancf7jm0jrf49xd0ya", + }, + }, +} From 96c204a239af9b51cbb57c9a4f334c04974bc64f Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 17:55:56 +0200 Subject: [PATCH 31/32] Added unit tests for toBitcoinJsLibNetwork --- typescript/test/bitcoin-network.test.ts | 34 +++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/typescript/test/bitcoin-network.test.ts b/typescript/test/bitcoin-network.test.ts index d2ff7b370..e610c2ee9 100644 --- a/typescript/test/bitcoin-network.test.ts +++ b/typescript/test/bitcoin-network.test.ts @@ -1,6 +1,11 @@ import { expect } from "chai" -import { BitcoinNetwork, toBcoinNetwork } from "../src/bitcoin-network" +import { + BitcoinNetwork, + toBcoinNetwork, + toBitcoinJsLibNetwork, +} from "../src/bitcoin-network" import { TransactionHash } from "../src/bitcoin" +import { networks } from "bitcoinjs-lib" describe("BitcoinNetwork", () => { const testData = [ @@ -10,6 +15,7 @@ describe("BitcoinNetwork", () => { // any value that doesn't match other supported networks genesisHash: TransactionHash.from("0x00010203"), expectedToBcoinResult: new Error("network not supported"), + expectedToBitcoinJsLibResult: new Error("network not supported"), }, { enumKey: BitcoinNetwork.Testnet, @@ -18,6 +24,7 @@ describe("BitcoinNetwork", () => { "0x000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943" ), expectedToBcoinResult: "testnet", + expectedToBitcoinJsLibResult: networks.testnet, }, { enumKey: BitcoinNetwork.Mainnet, @@ -26,11 +33,18 @@ describe("BitcoinNetwork", () => { "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" ), expectedToBcoinResult: "main", + expectedToBitcoinJsLibResult: networks.bitcoin, }, ] testData.forEach( - ({ enumKey, enumValue, genesisHash, expectedToBcoinResult }) => { + ({ + enumKey, + enumValue, + genesisHash, + expectedToBcoinResult, + expectedToBitcoinJsLibResult, + }) => { context(enumKey, async () => { describe(`toString`, async () => { it(`should return correct value`, async () => { @@ -59,6 +73,22 @@ describe("BitcoinNetwork", () => { }) } }) + + describe(`toBitcoinJsLibNetwork`, async () => { + if (expectedToBitcoinJsLibResult instanceof Error) { + it(`should throw an error`, async () => { + expect(() => toBitcoinJsLibNetwork(enumKey)).to.throw( + expectedToBitcoinJsLibResult.message + ) + }) + } else { + it(`should return ${expectedToBitcoinJsLibResult}`, async () => { + expect(toBitcoinJsLibNetwork(enumKey)).to.be.equal( + expectedToBitcoinJsLibResult + ) + }) + } + }) }) } ) From a5ef9e89e617931fcda77bcf214e3ca3d7ae4208 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 27 Sep 2023 18:25:03 +0200 Subject: [PATCH 32/32] Added unit tests for script type functions --- typescript/test/bitcoin.test.ts | 57 +++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index c8a243f87..0bcc335ba 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -17,6 +17,10 @@ import { readCompactSizeUint, computeHash160, computeHash256, + isP2PKHScript, + isP2WPKHScript, + isP2SHScript, + isP2WSHScript, } from "../src/bitcoin" import { calculateDepositRefundLocktime } from "../src/deposit" import { BitcoinNetwork } from "../src/bitcoin-network" @@ -596,4 +600,57 @@ describe("Bitcoin", () => { }) }) }) + + describe("Bitcoin Script Type", () => { + const testData = [ + { + testFunction: isP2PKHScript, + validScript: Buffer.from( + "76a9148db50eb52063ea9d98b3eac91489a90f738986f688ac", + "hex" + ), + name: "P2PKH", + }, + { + testFunction: isP2WPKHScript, + validScript: Buffer.from( + "00148db50eb52063ea9d98b3eac91489a90f738986f6", + "hex" + ), + name: "P2WPKH", + }, + { + testFunction: isP2SHScript, + validScript: Buffer.from( + "a914a9a5f97d5d3c4687a52e90718168270005b369c487", + "hex" + ), + name: "P2SH", + }, + { + testFunction: isP2WSHScript, + validScript: Buffer.from( + "0020b1f83e226979dc9fe74e87f6d303dbb08a27a1c7ce91664033f34c7f2d214cd7", + "hex" + ), + name: "P2WSH", + }, + ] + + testData.forEach(({ testFunction, validScript, name }) => { + describe(`is${name}Script`, () => { + it(`should return true for a valid ${name} script`, () => { + expect(testFunction(validScript)).to.be.true + }) + + it("should return false for other scripts", () => { + testData.forEach((data) => { + if (data.name !== name) { + expect(testFunction(data.validScript)).to.be.false + } + }) + }) + }) + }) + }) })