diff --git a/packages/messaging/__tests__/messages/FundingInput.spec.ts b/packages/messaging/__tests__/messages/FundingInput.spec.ts index 702ff3c9..fcd3d452 100644 --- a/packages/messaging/__tests__/messages/FundingInput.spec.ts +++ b/packages/messaging/__tests__/messages/FundingInput.spec.ts @@ -1,8 +1,5 @@ -import { Sequence, Tx } from '@node-dlc/bitcoin'; -import { StreamReader } from '@node-dlc/bufio'; import { expect } from 'chai'; -import { DlcInput } from '../../lib/messages/DlcInput'; import { FundingInput } from '../../lib/messages/FundingInput'; describe('FundingInput', () => { @@ -150,11 +147,46 @@ describe('FundingInput', () => { }); describe('validate', () => { - it('should ensure inputs are segwit', () => { + // Valid segwit transaction with P2WPKH output at index 0 + const validSegwitTx = + '020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03520101ffffffff0200f2052a01000000160014f32b43ed1a9f5a7f09023af065a212d8a159c7f70000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000'; + + it('should throw if output is not segwit (OP_RETURN)', () => { + // Using output index 1 which is an OP_RETURN output (not a segwit output) + const fundingInput = FundingInput.fromJSON({ + inputSerialId: 1, + prevTx: validSegwitTx, + prevTxVout: 1, // OP_RETURN output - not valid for funding + sequence: 4294967295, + maxWitnessLen: 108, + redeemScript: '', + }); + + expect(function () { + fundingInput.validate(); + }).to.throw('fundingInput must spend a segwit output'); + }); + + it('should pass validation for P2WPKH output', () => { + const fundingInput = FundingInput.fromJSON({ + inputSerialId: 1, + prevTx: validSegwitTx, + prevTxVout: 0, // P2WPKH output + sequence: 4294967295, + maxWitnessLen: 108, + redeemScript: '', + }); + + expect(function () { + fundingInput.validate(); + }).to.not.throw(); + }); + + it('should pass validation for P2WSH output', () => { const fundingInput = FundingInput.fromJSON({ inputSerialId: 1, prevTx: - '02000000000100c2eb0b00000000160014e70dcc9ffa7ff84c889c9e79b218708bae3bc95800000000', // has no inputs + '0200000001f58f85b356ad5bb5b6d0ef3eb863be8a6cb95e08e1e9e92885b4b22e7e51eb9d0000000000ffffffff01008c8647000000002200201234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef00000000', // P2WSH output prevTxVout: 0, sequence: 4294967295, maxWitnessLen: 108, @@ -163,7 +195,22 @@ describe('FundingInput', () => { expect(function () { fundingInput.validate(); - }).to.throw(Error); + }).to.not.throw(); + }); + + it('should throw if prevTxVout does not exist', () => { + const fundingInput = FundingInput.fromJSON({ + inputSerialId: 1, + prevTx: validSegwitTx, + prevTxVout: 5, // Output index 5 doesn't exist + sequence: 4294967295, + maxWitnessLen: 108, + redeemScript: '', + }); + + expect(function () { + fundingInput.validate(); + }).to.throw('fundingInput prevTxVout 5 does not exist in prevTx'); }); }); }); diff --git a/packages/messaging/lib/messages/FundingInput.ts b/packages/messaging/lib/messages/FundingInput.ts index 30a89b81..7e51fa9d 100644 --- a/packages/messaging/lib/messages/FundingInput.ts +++ b/packages/messaging/lib/messages/FundingInput.ts @@ -168,8 +168,38 @@ export class FundingInput implements IDlcMessage { */ public validate(): void { // 1. Type is set automatically in class - // 2. Ensure inputs are segwit - if (!this.prevTx.isSegWit) throw new Error('fundingInput must be segwit'); + // 2. Ensure the output being spent is a segwit output (P2WPKH or P2WSH) + // Note: We check the output script type, not whether prevTx was created with segwit inputs. + // This allows spending UTXOs from transactions that used legacy inputs but have segwit outputs. + const output = this.prevTx.outputs[this.prevTxVout]; + if (!output) { + throw new Error( + `fundingInput prevTxVout ${this.prevTxVout} does not exist in prevTx`, + ); + } + + const scriptPubKey = output.scriptPubKey.serializeCmds(); + // P2WPKH: OP_0 <20-byte-key-hash> = 0x0014{20 bytes} = 22 bytes total + // P2WSH: OP_0 <32-byte-script-hash> = 0x0020{32 bytes} = 34 bytes total + // P2TR: OP_1 <32-byte-pubkey> = 0x5120{32 bytes} = 34 bytes total + const isP2WPKH = + scriptPubKey.length === 22 && + scriptPubKey[0] === 0x00 && + scriptPubKey[1] === 0x14; + const isP2WSH = + scriptPubKey.length === 34 && + scriptPubKey[0] === 0x00 && + scriptPubKey[1] === 0x20; + const isP2TR = + scriptPubKey.length === 34 && + scriptPubKey[0] === 0x51 && + scriptPubKey[1] === 0x20; + + if (!isP2WPKH && !isP2WSH && !isP2TR) { + throw new Error( + 'fundingInput must spend a segwit output (P2WPKH, P2WSH, or P2TR)', + ); + } } /**