@node-dlc/messaging
Version:
DLC Messaging Protocol
528 lines (477 loc) • 21.7 kB
text/typescript
import { BitcoinNetworks } from 'bitcoin-networks';
import { expect } from 'chai';
import {
BatchFundingGroup,
OrderIrcInfoV0,
OrderMetadataV0,
OrderPositionInfoV0,
} from '../../lib';
import { ContractInfo, ContractInfoV0 } from '../../lib/messages/ContractInfo';
import {
DlcOffer,
DlcOfferContainer,
DlcOfferV0,
LOCKTIME_THRESHOLD,
} from '../../lib/messages/DlcOffer';
import { FundingInputV0 } from '../../lib/messages/FundingInput';
import { MessageType } from '../../lib/MessageType';
describe('DlcOffer', () => {
const bitcoinNetwork = BitcoinNetworks.bitcoin_regtest;
let instance: DlcOfferV0;
const type = Buffer.from('a71a', 'hex');
const contractFlags = Buffer.from('00', 'hex');
const chainHash = Buffer.from(
'06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f',
'hex',
);
const contractInfo = Buffer.from(
'fdd82e' + // type contract_info
'fd0131' + // length
'000000000bebc200' + // total_collateral
'fda710' + // type contract_descriptor
'79' + // length
'03' + // num_outcomes
'c5a7affd51901bc7a51829b320d588dc7af0ad1f3d56f20a1d3c60c9ba7c6722' + // outcome_1
'0000000000000000' + // payout_1
'adf1c23fbeed6611efa5caa0e9ed4c440c450a18bc010a6c867e05873ac08ead' + // outcome_2
'00000000092363a3' + // payout_2
'6922250552ad6bb10ab3ddd6981b530aa9a6fd05725bf85b59e3e51163905288' + // outcome_3
'000000000bebc200' + // payout_3
'fda712' + // type oracle_info
'a8' + // length
'fdd824' + // type oracle_announcement
'a4' + // length
'fab22628f6e2602e1671c286a2f63a9246794008627a1749639217f4214cb4a9' + // announcement_signature_r
'494c93d1a852221080f44f697adb4355df59eb339f6ba0f9b01ba661a8b108d4' + // announcement_signature_s
'da078bbb1d34e7729e38e2ae34236e776da121af442626fa31e31ae55a279a0b' + // oracle_public_key
'fdd822' + // type oracle_event
'40' + // length
'0001' + // nb_nonces
'3cfba011378411b20a5ab773cb95daab93e9bcd1e4cce44986a7dda84e01841b' + // oracle_nonces
'00000000' + // event_maturity_epoch
'fdd806' + // type enum_event_descriptor
'10' + // length
'0002' + // num_outcomes
'06' + // outcome_1_len
'64756d6d7931' + // outcome_1
'06' + // outcome_2_len
'64756d6d7932' + // outcome_2
'05' + // event_id_length
'64756d6d79', // event_id
'hex',
);
const fundingPubKey = Buffer.from(
'0327efea09ff4dfb13230e887cbab8821d5cc249c7ff28668c6633ff9f4b4c08e3',
'hex',
);
const payoutSPKLen = Buffer.from('0016', 'hex');
const payoutSPK = Buffer.from(
'00142bbdec425007dc360523b0294d2c64d2213af498',
'hex',
);
const payoutSerialID = Buffer.from('0000000000b051dc', 'hex');
const offerCollateralSatoshis = Buffer.from('0000000005f5e0FF', 'hex'); // 99999999
const fundingInputsLen = Buffer.from('0001', 'hex');
const fundingInputV0 = Buffer.from(
'fda714' +
'3f' + // length
'000000000000fa51' + // input_serial_id
'0029' + // prevtx_len
'02000000000100c2eb0b00000000160014369d63a82ed846f4d47ad55045e594ab95539d6000000000' + // prevtx
'00000000' + // prevtx_vout
'ffffffff' + // sequence
'006b' + // max_witness_len
'0000', // redeemscript_len
'hex',
);
const changeSPKLen = Buffer.from('0016', 'hex');
const changeSPK = Buffer.from(
'0014afa16f949f3055f38bd3a73312bed00b61558884',
'hex',
);
const changeSerialID = Buffer.from('00000000001ea3ed', 'hex');
const fundOutputSerialID = Buffer.from('000000000052947a', 'hex');
const feeRatePerVb = Buffer.from('0000000000000001', 'hex');
const cetLocktime = Buffer.from('00000064', 'hex');
const refundLocktime = Buffer.from('000000c8', 'hex');
const dlcOfferHex = Buffer.concat([
type,
contractFlags,
chainHash,
contractInfo,
fundingPubKey,
payoutSPKLen,
payoutSPK,
payoutSerialID,
offerCollateralSatoshis,
fundingInputsLen,
fundingInputV0,
changeSPKLen,
changeSPK,
changeSerialID,
fundOutputSerialID,
feeRatePerVb,
cetLocktime,
refundLocktime,
]);
beforeEach(() => {
instance = new DlcOfferV0();
instance.contractFlags = contractFlags;
instance.chainHash = chainHash;
instance.contractInfo = ContractInfo.deserialize(contractInfo);
instance.fundingPubKey = fundingPubKey;
instance.payoutSPK = payoutSPK;
instance.payoutSerialId = BigInt(11555292);
instance.offerCollateralSatoshis = BigInt(99999999);
instance.fundingInputs = [FundingInputV0.deserialize(fundingInputV0)];
instance.changeSPK = changeSPK;
instance.changeSerialId = BigInt(2008045);
instance.fundOutputSerialId = BigInt(5411962);
instance.feeRatePerVb = BigInt(1);
instance.cetLocktime = 100;
instance.refundLocktime = 200;
});
describe('deserialize', () => {
it('should throw if incorrect type', () => {
instance.type = 0x123 as MessageType;
expect(function () {
DlcOffer.deserialize(instance.serialize());
}).to.throw(Error);
});
it('has correct type', () => {
expect(DlcOffer.deserialize(instance.serialize()).type).to.equal(
instance.type,
);
});
});
describe('DlcOfferV0', () => {
describe('serialize', () => {
it('serializes', () => {
expect(instance.serialize().toString('hex')).to.equal(
dlcOfferHex.toString('hex'),
);
});
it('serializes with positioninfo', () => {
const positionInfo = new OrderPositionInfoV0();
positionInfo.shiftForFees = 'acceptor';
positionInfo.fees = BigInt(1000);
instance.positionInfo = positionInfo;
expect(instance.serialize().toString('hex')).to.equal(
Buffer.concat([dlcOfferHex, positionInfo.serialize()]).toString(
'hex',
),
);
});
});
describe('deserialize', () => {
it('deserializes', () => {
const instance = DlcOfferV0.deserialize(dlcOfferHex);
expect(instance.contractFlags).to.deep.equal(contractFlags);
expect(instance.chainHash).to.deep.equal(chainHash);
expect(instance.contractInfo.serialize().toString('hex')).to.equal(
contractInfo.toString('hex'),
);
expect(instance.fundingPubKey).to.deep.equal(fundingPubKey);
expect(instance.payoutSPK).to.deep.equal(payoutSPK);
expect(Number(instance.payoutSerialId)).to.equal(11555292);
expect(Number(instance.offerCollateralSatoshis)).to.equal(99999999);
expect(instance.fundingInputs[0].serialize().toString('hex')).to.equal(
fundingInputV0.toString('hex'),
);
expect(instance.changeSPK).to.deep.equal(changeSPK);
expect(Number(instance.changeSerialId)).to.equal(2008045);
expect(Number(instance.fundOutputSerialId)).to.equal(5411962);
expect(Number(instance.feeRatePerVb)).to.equal(1);
expect(instance.cetLocktime).to.equal(100);
expect(instance.refundLocktime).to.equal(200);
});
it('has correct type', () => {
expect(DlcOfferV0.deserialize(dlcOfferHex).type).to.equal(
MessageType.DlcOfferV0,
);
});
it('deserializes with positioninfo', () => {
const positionInfo = new OrderPositionInfoV0();
positionInfo.shiftForFees = 'acceptor';
positionInfo.fees = BigInt(1000);
instance.positionInfo = positionInfo;
expect(
DlcOfferV0.deserialize(instance.serialize()).positionInfo.serialize(),
).to.deep.equal(positionInfo.serialize());
});
});
describe('toJSON', () => {
it('converts to JSON', async () => {
const json = instance.toJSON();
expect(json.type).to.equal(instance.type);
expect(json.contractFlags).to.equal(
instance.contractFlags.toString('hex'),
);
expect(json.chainHash).to.equal(instance.chainHash.toString('hex'));
expect(json.contractInfo.type).to.equal(instance.contractInfo.type);
expect(json.contractInfo.totalCollateral).to.equal(
Number(instance.contractInfo.totalCollateral),
);
expect(json.fundingPubKey).to.equal(
instance.fundingPubKey.toString('hex'),
);
expect(json.payoutSPK).to.equal(instance.payoutSPK.toString('hex'));
expect(json.payoutSerialId).to.equal(Number(instance.payoutSerialId));
expect(json.offerCollateralSatoshis).to.equal(
Number(instance.offerCollateralSatoshis),
);
expect(json.fundingInputs[0].type).to.equal(
instance.fundingInputs[0].type,
);
expect(json.fundingInputs[0].inputSerialId).to.equal(
Number(instance.fundingInputs[0].toJSON().inputSerialId),
);
expect(json.changeSPK).to.equal(instance.changeSPK.toString('hex'));
expect(json.changeSerialId).to.equal(Number(instance.changeSerialId));
expect(json.fundOutputSerialId).to.equal(
Number(instance.fundOutputSerialId),
);
expect(json.feeRatePerVb).to.equal(Number(instance.feeRatePerVb));
expect(json.cetLocktime).to.equal(instance.cetLocktime);
expect(json.refundLocktime).to.equal(instance.refundLocktime);
});
});
describe('getAddresses', () => {
it('should get addresses', () => {
const expectedFundingAddress =
'bcrt1qayylp95g2tzq2a60x2l7f8mclnx5y2jxm0yt09';
const expectedChangeAddress =
'bcrt1q47skl9ylxp2l8z7n5ue390kspds4tzyy5jdxs8';
const expectedPayoutAddress =
'bcrt1q9w77csjsqlwrvpfrkq556try6gsn4ayc2kn0kl';
const instance = DlcOfferV0.deserialize(dlcOfferHex);
const {
fundingAddress,
changeAddress,
payoutAddress,
} = instance.getAddresses(bitcoinNetwork);
expect(fundingAddress).to.equal(expectedFundingAddress);
expect(changeAddress).to.equal(expectedChangeAddress);
expect(payoutAddress).to.equal(expectedPayoutAddress);
});
});
describe('validate', () => {
it('should throw if payout_spk is invalid', () => {
instance.payoutSPK = Buffer.from('fff', 'hex');
expect(function () {
instance.validate();
}).to.throw(Error);
});
it('should throw if change_spk is invalid', () => {
instance.changeSPK = Buffer.from('fff', 'hex');
expect(function () {
instance.validate();
}).to.throw(Error);
});
it('should throw if fundingpubkey is not a valid pubkey', () => {
instance.fundingPubKey = Buffer.from(
'00f003aa11f2a97b6be755a86b9fd798a7451c670196a5245b7bae971306b7c87e',
'hex',
);
expect(function () {
instance.validate();
}).to.throw(Error);
});
it('should throw if fundingpubkey is not in compressed secp256k1 format', () => {
instance.fundingPubKey = Buffer.from(
'045162991c7299223973cabc99ef5087d7bab2dafe61f78e5388b2f9492f7978123f51fd05ef0693790c0b2d4f30848363a3f3fbcf2bd53a05ba0fd5bb708c3184',
'hex',
);
expect(function () {
instance.validate();
}).to.throw(Error);
});
it('should throw if offerCollateralSatoshis is less than 1000', () => {
instance.offerCollateralSatoshis = BigInt(999);
expect(function () {
instance.validate();
}).to.throw(Error);
// boundary check
instance.offerCollateralSatoshis = BigInt(1000);
expect(function () {
instance.validate();
}).to.not.throw(Error);
});
it('should throw if cet_locktime is less than 0', () => {
instance.cetLocktime = -1;
expect(() => {
instance.validate();
}).to.throw('cet_locktime must be greater than or equal to 0');
});
it('should throw if refund_locktime is less than 0', () => {
instance.refundLocktime = -1;
expect(() => {
instance.validate();
}).to.throw('refund_locktime must be greater than or equal to 0');
});
it('should throw if cet_locktime and refund_locktime are not in same units', () => {
instance.cetLocktime = 100;
instance.refundLocktime = LOCKTIME_THRESHOLD + 200;
expect(function () {
instance.validate();
}).to.throw(Error);
});
it('should not throw if cet_locktime and refund_locktime are in same units', () => {
instance.cetLocktime = 100;
instance.refundLocktime = 200;
expect(function () {
instance.validate();
}).to.not.throw(Error);
instance.cetLocktime = LOCKTIME_THRESHOLD + 100;
instance.refundLocktime = LOCKTIME_THRESHOLD + 200;
expect(function () {
instance.validate();
}).to.not.throw(Error);
});
it('should throw if cet_locktime >= refund_locktime', () => {
instance.cetLocktime = 200;
instance.refundLocktime = 100;
expect(function () {
instance.validate();
}).to.throw(Error);
instance.cetLocktime = 100;
instance.refundLocktime = 100;
expect(function () {
instance.validate();
}).to.throw(Error);
});
it('should throw if inputSerialIds arent unique', () => {
instance.fundingInputs = [
FundingInputV0.deserialize(fundingInputV0),
FundingInputV0.deserialize(fundingInputV0),
];
expect(function () {
instance.validate();
}).to.throw(Error);
});
it('should throw if changeSerialId == fundOutputSerialId', () => {
instance.changeSerialId = instance.fundOutputSerialId;
expect(function () {
instance.validate();
}).to.throw(Error);
});
it('should throw if totalCollateral <= offerCollateral', () => {
instance.contractInfo.totalCollateral = BigInt(200000000);
instance.offerCollateralSatoshis = BigInt(200000000);
expect(function () {
instance.validate();
}).to.throw(Error);
instance.contractInfo.totalCollateral = BigInt(200000000);
instance.offerCollateralSatoshis = BigInt(200000001);
expect(function () {
instance.validate();
}).to.throw(Error);
});
it('should throw if funding amount less than offer collateral satoshis', () => {
instance.offerCollateralSatoshis = BigInt(3e8);
expect(function () {
instance.validate();
}).to.throw(Error);
});
});
});
describe('DlcOfferContainer', () => {
it('should serialize and deserialize', () => {
const dlcOffer = DlcOfferV0.deserialize(dlcOfferHex);
// swap payout and change spk to differentiate between dlcoffers
const dlcOffer2 = DlcOfferV0.deserialize(dlcOfferHex);
dlcOffer2.payoutSPK = dlcOffer.changeSPK;
dlcOffer2.changeSPK = dlcOffer.payoutSPK;
const container = new DlcOfferContainer();
container.addOffer(dlcOffer);
container.addOffer(dlcOffer2);
const instance = DlcOfferContainer.deserialize(container.serialize());
expect(container.serialize()).to.deep.equal(instance.serialize());
});
});
describe('TLVs', () => {
it('should deserialize with all TLV types present', () => {
const contractFlags = Buffer.from('00', 'hex');
const chainHash = Buffer.from(
'06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f',
'hex',
);
const contractInfo = ContractInfoV0.deserialize(
Buffer.from(
'fdd82efd03e80000000000fe502bfda7209c0015fda72684000300fe00f3e2a30000fda72816000200fe00f3e2a30000fe000ea79ffe00f3e2a30000fe000ea79ffe00f3e2a30000fda7281a0002fe000ea79ffe00f3e2a30000fe000f47c8fe00fe502b0000fe000f47c8fe00fe502b0000fda7281a0002fe000f47c8fe00fe502b0000fe001ffffffe00fe502b0000fe001ffffffe00fe502b0000fda7240e000200fd8235fe000b71b0fd411afda712fd033afdd824fd03340c9050848d0ae5294a88020f45ee3c920dcf5c050effa1894480f1c7d6c8fb24ffb379ba9b87f6b4279b104761c776126f85400ea9bb6c6c91c8953af92ae686c3d07289c2ade25405c1c421b38c9322cd73fb2c89f42ce0730a35fae1f8875dfdd822fd02ce001536393424064fae281c59dfce1842dbdb1a696f34620611d25035ed53b8f79f07a855f3c3b2d300d4bfdce1e325b6a59a60272276d89f283db41ca90e3aedbb522fa3c1f8bc08de68c4c5b6ce177f0c62c683fca22b61eccb41fb06bb6e8881d3c23b4d88536e68d65de66e8a7d1c2ec0e6316fdc0d25c4cf42ebaf1775b168fdc847c824efd6f6ae43d9ba0e973de205979971020ebec4919c97cae238513e41e2bc759cd11d213ca2a118e948e0c380eb8bdcf4bcb5eebce6e57c2b22c10fd7083e53643d29206ff66ca5e02485c710e57a1a2de36612323cb7d6ddd1521b4128ce0fb18ff87dda1b796cefa1ec25e89e26f964a0bb6b78423cdf2dbf60df41f6187bdb7e01aeb6c53d7ccb608cc5afbd469e8b45017e1c34bf94dbfb58f3b9a363d4c89bb3ca7f060540b6b03ea6e296381c781fe70d9343aefa095dfdd39dc65f6f748a7a23d4b293ad36e2df52fd988d7a6327088afb483c45b4d21a8e13ef3bec9324cd8b8945dc3c7c534c2c742bd3ecd45bd2c50b78707ea43752ced2620e3cf429ab93c07e137ccf1662376e1d3290818a2020033911deeedc0fe8322091d5bb7b959723c15cb20b7fd2d71abded50cfd4b4db89076d87ad2a0171de9bac64d34e18b7c741618a4e68ca2431631c6fd88b5b90a1d007cc7ac22597df943df27b23d53a1e3a6fe01f5f230a8eb6ae8c7f4c1d40e63d2765250b66f468692bf1b0afa7d7e2ccc8cdc7ca6d691f3327aa3b5b27440ecf3cdcc0da0a9be605f3b4518685624688aa3da90e6a74041cf9330dda94f44d8aabf904e2e31fa54c0503289729cf92bbd63aaf56f97df7baa5a3b677cce06edf54602242671f18695728b465e95fa997a8a4852727e5416433970b902f918485554577129f130b177b7f9d958936c8adf69711e0369f07e8ce92cba06f6e0d91c3f32a8634e52b67046313fdd80a0e00020004626974730000000000151561746f6d69632d656e67696e652d74726164652d31',
'hex',
),
);
const fundingPubkey = Buffer.from(
'03636a2812026c6ea83a6eb27f579ae588aa48dd221fafa049e6613e48ff03953a',
'hex',
);
const payoutSPK = Buffer.from(
'0014ecf78cf9c4e3cf16b5dccdbd013a7b84cd530d0c',
'hex',
);
const payoutSerialId = BigInt(29829);
const offerCollateralSatoshis = BigInt(16649967);
const fundingInput = FundingInputV0.deserialize(
Buffer.from(
'fda714fd01fd0000000000a34e3b01e70200000000010369a13b156aff1cf5027140f45d41840c5c552b6d1c512cbb3b1610b850e47ae40000000000feffffff55767d33c145aea3efe723d9803916340fd013ee4f5ad8491e88d38bf8cd180e0000000000feffffff6bab2f95d6b0ca6abf8e8adbec1e70a79bc553c433e1d942f49084f1a1b73bb70100000000feffffff0100e1f50500000000160014bc32a8067fe02ea3a0d1b0daf42e17dd4039992d02473044022055662f02effb509e37bc074c6915eddb09658e6d388e545270ad08ce0bf339a702204b54713323a9bf37e575d4f3349080e4fae1a50762ab2d572967d185a0d16e8901210293914f759527e8e47770750242e7198738705b3a4b2de4f672456497aebf00820247304402202f9d4f5dc184868f866f978f7b4433167d4f50d13329a91eef5f6feec1974019022059b38836960fc3d45fc60021fd2215ad54d2a50224f523e110d48ab63dec4e61012102248d7ea318582d01f9f4c5267a18e28b9a4c03f69877075451f37f4c38af529e0247304402202b592244c641b678d13b059b822754b8f532c0d8ba7ece9a1c3b199b7acea27e02202c506668a40278075d4c6ad68bab0fa8db26fcd1b458aa5becc8a3c845cd86d701210230dbeb555b88731390f0431d3b329002c057cec662d80a8b47f5560433f2efd63104000000000000ffffffff006c0000',
'hex',
),
);
const changeSPK = Buffer.from(
'0014a734d87e6d29d79422f7e5ea7a7709f65dac60e9',
'hex',
);
const changeSerialId = BigInt(94880);
const fundOutputSerialId = BigInt(44394);
const feeRatePerVb = BigInt(45);
const cetLocktime = 1712689645;
const refundLocktime = 1719255498;
const metadata = OrderMetadataV0.deserialize(
Buffer.from('fdf5360f06656e67696e650000000000000000', 'hex'),
);
const ircInfo = OrderIrcInfoV0.deserialize(
Buffer.from(
'fdf53832104130346a7a504b4b726a6751456f506802dff1fe9bd33ce81881120be26c27d443247bfd3398866a7dc071867e94ff69c9',
'hex',
),
);
const positionInfo = OrderPositionInfoV0.deserialize(
Buffer.from(
'fdf53a32010000000000001b121561746f6d69632d656e67696e652d74726164652d310000000000fe502b0000000000000000000000',
'hex',
),
);
const batchFundingGroup = BatchFundingGroup.deserialize(
BatchFundingGroup.deserialize(
Buffer.from(
'fdff967900000000000005f5e100051561746f6d69632d656e67696e652d74726164652d311561746f6d69632d656e67696e652d74726164652d321561746f6d69632d656e67696e652d74726164652d331561746f6d69632d656e67696e652d74726164652d341561746f6d69632d656e67696e652d74726164652d35',
'hex',
),
).serialize(),
);
const dlcOffer = new DlcOfferV0();
dlcOffer.contractFlags = contractFlags;
dlcOffer.chainHash = chainHash;
dlcOffer.contractInfo = contractInfo;
dlcOffer.fundingPubKey = fundingPubkey;
dlcOffer.payoutSPK = payoutSPK;
dlcOffer.payoutSerialId = payoutSerialId;
dlcOffer.offerCollateralSatoshis = offerCollateralSatoshis;
dlcOffer.fundingInputs = [fundingInput];
dlcOffer.changeSPK = changeSPK;
dlcOffer.changeSerialId = changeSerialId;
dlcOffer.fundOutputSerialId = fundOutputSerialId;
dlcOffer.feeRatePerVb = feeRatePerVb;
dlcOffer.cetLocktime = cetLocktime;
dlcOffer.refundLocktime = refundLocktime;
dlcOffer.metadata = metadata;
dlcOffer.ircInfo = ircInfo;
dlcOffer.positionInfo = positionInfo;
dlcOffer.batchFundingGroups = [batchFundingGroup];
expect(dlcOffer.toJSON()).to.deep.equal(
DlcOfferV0.deserialize(dlcOffer.serialize()).toJSON(),
);
});
});
});