Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/bitcoin/__tests__/bitcoin/script.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect } from 'chai';

import { Script } from '../../lib/Script';

describe('Script', () => {
it('outputs Taproot scriptPubKey', () => {
const pubkey =
'f1637044d08d5891cdf4c6b066667e75eebaae79a9de91c5f5eb5bcc682a502e';
const buffer = Buffer.from(pubkey, 'hex');

const expected = Buffer.concat([Buffer.from([34, 81, 32]), buffer]);

expect(Script.p2trLock(buffer).serialize()).to.be.eql(expected);
});

it('parses raw Taproot scriptPubKey bytes', () => {
const pubkey =
'f1637044d08d5891cdf4c6b066667e75eebaae79a9de91c5f5eb5bcc682a502e';
const script = Script.p2trLock(Buffer.from(pubkey, 'hex'));

expect(Script.fromRaw(script.serializeCmds()).serializeCmds()).to.be.eql(
script.serializeCmds(),
);
});

it('throws when parsing empty raw script bytes', () => {
expect(() => Script.fromRaw(Buffer.alloc(0))).to.throw(
'Cannot parse empty script',
);
});

it('throws on invalid length pubkey', () => {
const buffer = Buffer.from([0x00, 0x01, 0x02]);
expect(() => Script.p2trLock(buffer)).to.throw();
});
});
65 changes: 46 additions & 19 deletions packages/bitcoin/lib/Script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ export class Script implements ICloneable<Script> {
}

return new Script(
Script.number(m),
...pubkeys,
Script.number(pubkeys.length),
OpCode.OP_CHECKMULTISIG,
); // prettier-ignore
Script.number(m),
...pubkeys,
Script.number(pubkeys.length),
OpCode.OP_CHECKMULTISIG,
); // prettier-ignore
}

/**
Expand All @@ -170,9 +170,9 @@ export class Script implements ICloneable<Script> {
asssertValidSig(sig);
}
return new Script(
OpCode.OP_0,
...sigs
); // prettier-ignore
OpCode.OP_0,
...sigs
); // prettier-ignore
}

/**
Expand All @@ -198,10 +198,10 @@ export class Script implements ICloneable<Script> {
}

return new Script(
OpCode.OP_HASH160,
scriptHash160,
OpCode.OP_EQUAL,
); // prettier-ignore
OpCode.OP_HASH160,
scriptHash160,
OpCode.OP_EQUAL,
); // prettier-ignore
}

/**
Expand Down Expand Up @@ -255,9 +255,9 @@ export class Script implements ICloneable<Script> {
const hash160PubKey = value.length === 20 ? value : hash160(value);

return new Script(
OpCode.OP_0,
hash160PubKey,
); // prettier-ignore
OpCode.OP_0,
hash160PubKey,
); // prettier-ignore
}

/**
Expand All @@ -274,9 +274,25 @@ export class Script implements ICloneable<Script> {
}

return new Script(
OpCode.OP_0,
sha256Script,
); // prettier-ignore
OpCode.OP_0,
sha256Script,
); // prettier-ignore
}

/**
* Create a standard Pay-to-Taproot scriptPubKey by accepting the
* sha256 of the Taproot as input. It is of the format:
* OP_1 <x-only-tweaked-pubkey>
*/
public static p2trLock(taproot: Buffer): Script {
if (taproot.length !== 32) {
throw new BitcoinError(BitcoinErrorCode.Hash256Invalid);
}

return new Script(
OpCode.OP_1,
taproot,
); // prettier-ignore
}

/**
Expand All @@ -298,6 +314,17 @@ export class Script implements ICloneable<Script> {
return new Script(...cmds);
}

/**
* Parses raw script command bytes without a leading length prefix.
*/
public static fromRaw(buf: Buffer): Script {
if (buf.length === 0) {
throw new Error('Cannot parse empty script');
}

return new Script(...Script.parseCmds(buf));
}

Comment thread
matthewjablack marked this conversation as resolved.
/**
* When supplied with a Buffer of cmds this method will parse the commands
* into data blocks or op_codes depending on the meaning of the bytes
Expand Down Expand Up @@ -362,7 +389,7 @@ export class Script implements ICloneable<Script> {

/**
* Returns true if other script is an exact match of the current script.
* This requires all data element sto be exact matches and all operations
* This requires all data elements to be exact matches and all operations
* to be exact matches.
* @param other
*/
Expand Down
46 changes: 43 additions & 3 deletions packages/core/__tests__/dlc/TxBuilder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Value } from '@node-dlc/bitcoin';
import { Script, Value } from '@node-dlc/bitcoin';
import {
DlcAcceptWithoutSigs,
DlcOffer,
Expand Down Expand Up @@ -77,7 +77,13 @@ describe('TxBuilder', () => {
const createTestDlcAccept = (
acceptCollateral: bigint,
fundingInputs: FundingInput[] = [],
scripts: { payoutSpk?: Buffer; changeSpk?: Buffer } = {},
): DlcAcceptWithoutSigs => {
const defaultSpk = Buffer.from(
'0014' + Buffer.alloc(20).toString('hex'),
'hex',
);

return new DlcAcceptWithoutSigs(
1, // protocolVersion
Buffer.alloc(32), // temporaryContractId
Expand All @@ -87,10 +93,10 @@ describe('TxBuilder', () => {
'02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9',
'hex',
),
Buffer.from('0014' + Buffer.alloc(20).toString('hex'), 'hex'), // payoutSpk
scripts.payoutSpk ?? defaultSpk,
BigInt(4), // payoutSerialId
fundingInputs,
Buffer.from('0014' + Buffer.alloc(20).toString('hex'), 'hex'), // changeSpk
scripts.changeSpk ?? defaultSpk,
BigInt(5), // changeSerialId
);
};
Expand Down Expand Up @@ -294,6 +300,40 @@ describe('TxBuilder', () => {
Number(DUST_LIMIT),
);
});

it('should preserve offer Taproot change scriptPubKey', () => {
const offerInput = createTestFundingInput(BigInt(1050000));
const offer = createTestDlcOffer(BigInt(1000000), [offerInput]);
const accept = createTestDlcAccept(BigInt(0), []);
const taprootSpk = Script.p2trLock(Buffer.alloc(32, 1)).serializeCmds();
offer.changeSpk = taprootSpk;

const builder = new BatchDlcTxBuilder([offer], [accept]);
const tx = builder.buildFundingTransaction();

const hasTaprootChange = tx.outputs.some((output) =>
output.scriptPubKey.serializeCmds().equals(taprootSpk),
);
expect(hasTaprootChange).to.equal(true);
});

it('should preserve accept Taproot change scriptPubKey', () => {
const offerInput = createTestFundingInput(BigInt(1000000));
const acceptInput = createTestFundingInput(BigInt(1050000), 2);
const offer = createTestDlcOffer(BigInt(500000), [offerInput]);
const taprootSpk = Script.p2trLock(Buffer.alloc(32, 2)).serializeCmds();
const accept = createTestDlcAccept(BigInt(500000), [acceptInput], {
changeSpk: taprootSpk,
});

const builder = new BatchDlcTxBuilder([offer], [accept]);
const tx = builder.buildFundingTransaction();

const hasTaprootChange = tx.outputs.some((output) =>
output.scriptPubKey.serializeCmds().equals(taprootSpk),
);
expect(hasTaprootChange).to.equal(true);
});
});

describe('BatchDlcTxBuilder.buildFundingTransaction - Error Handling', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/lib/dlc/TxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,15 @@ export class BatchDlcTxBuilder {
if (offerChangeValue >= DUST_LIMIT) {
outputs.push({
value: Value.fromSats(Number(offerChangeValue)),
script: Script.p2wpkhLock(this.dlcOffers[0].changeSpk.slice(2)),
script: Script.fromRaw(this.dlcOffers[0].changeSpk),
serialId: this.dlcOffers[0].changeSerialId,
});
}

if (acceptChangeValue >= DUST_LIMIT) {
outputs.push({
value: Value.fromSats(Number(acceptChangeValue)),
script: Script.p2wpkhLock(this.dlcAccepts[0].changeSpk.slice(2)),
script: Script.fromRaw(this.dlcAccepts[0].changeSpk),
serialId: this.dlcAccepts[0].changeSerialId,
});
}
Expand Down
Loading