From 48d800cedd1d3b8ee1d8722e51f316e618cc5612 Mon Sep 17 00:00:00 2001 From: Matthew Black Date: Thu, 14 Mar 2024 10:46:27 -0400 Subject: [PATCH] feat: add batch funding group tlv --- .../messages/BatchFundingGroup.spec.ts | 52 +++++++ packages/messaging/lib/MessageType.ts | 2 + packages/messaging/lib/index.ts | 1 + .../lib/messages/BatchFundingGroup.ts | 131 ++++++++++++++++++ packages/messaging/lib/messages/DlcAccept.ts | 38 +++++ packages/messaging/lib/messages/DlcOffer.ts | 24 +++- packages/messaging/lib/messages/OrderOffer.ts | 24 +++- packages/messaging/package.json | 1 + 8 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 packages/messaging/__tests__/messages/BatchFundingGroup.spec.ts create mode 100644 packages/messaging/lib/messages/BatchFundingGroup.ts diff --git a/packages/messaging/__tests__/messages/BatchFundingGroup.spec.ts b/packages/messaging/__tests__/messages/BatchFundingGroup.spec.ts new file mode 100644 index 00000000..07d57059 --- /dev/null +++ b/packages/messaging/__tests__/messages/BatchFundingGroup.spec.ts @@ -0,0 +1,52 @@ +import { Value } from '@node-dlc/bitcoin'; +import { expect } from 'chai'; + +import { BatchFundingGroup } from '../../lib'; + +describe('BatchFundingGroup TLV', () => { + it('should serialize and deserialize without contract ids', () => { + const batchFundingGroup = new BatchFundingGroup(); + + const eventIds = ['event1', 'event2', 'event3']; + + batchFundingGroup.eventIds = eventIds; + batchFundingGroup.allocatedCollateral = Value.fromBitcoin(0.5); + + const deserialized = BatchFundingGroup.deserialize( + batchFundingGroup.serialize(), + ); + + expect(deserialized.eventIds).to.deep.equal(eventIds); + expect(batchFundingGroup.serialize()).to.deep.equal( + deserialized.serialize(), + ); + }); + + it('should serialize and deserialize with contract ids', () => { + const batchFundingGroup = new BatchFundingGroup(); + + const eventIds = ['event1', 'event2', 'event3']; + + batchFundingGroup.eventIds = eventIds; + batchFundingGroup.allocatedCollateral = Value.fromBitcoin(0.5); + batchFundingGroup.tempContractIds = [ + Buffer.from('tempContractId1'), + Buffer.from('tempContractId2'), + Buffer.from('tempContractId3'), + ]; + batchFundingGroup.contractIds = [ + Buffer.from('contractId1'), + Buffer.from('contractId2'), + Buffer.from('contractId3'), + ]; + + const deserialized = BatchFundingGroup.deserialize( + batchFundingGroup.serialize(), + ); + + expect(deserialized.eventIds).to.deep.equal(eventIds); + expect(batchFundingGroup.serialize()).to.deep.equal( + deserialized.serialize(), + ); + }); +}); diff --git a/packages/messaging/lib/MessageType.ts b/packages/messaging/lib/MessageType.ts index d55c939f..cbdffa54 100644 --- a/packages/messaging/lib/MessageType.ts +++ b/packages/messaging/lib/MessageType.ts @@ -75,6 +75,8 @@ export enum MessageType { AddressCache = 65132, + BatchFundingGroup = 65430, + IrcMessageV0 = 59314, NodeAnnouncement = 51394, diff --git a/packages/messaging/lib/index.ts b/packages/messaging/lib/index.ts index 1f4cd6a4..8ee288b0 100644 --- a/packages/messaging/lib/index.ts +++ b/packages/messaging/lib/index.ts @@ -4,6 +4,7 @@ export * from './chain/IChainFilterChainClient'; export * from './domain/Address'; export * from './irc/IrcMessage'; export * from './messages/AddressCache'; +export * from './messages/BatchFundingGroup'; export * from './messages/CetAdaptorSignaturesV0'; export * from './messages/ContractDescriptor'; export * from './messages/ContractInfo'; diff --git a/packages/messaging/lib/messages/BatchFundingGroup.ts b/packages/messaging/lib/messages/BatchFundingGroup.ts new file mode 100644 index 00000000..5ca00c79 --- /dev/null +++ b/packages/messaging/lib/messages/BatchFundingGroup.ts @@ -0,0 +1,131 @@ +import { Value } from '@node-dlc/bitcoin'; +import { BufferReader, BufferWriter } from '@node-lightning/bufio'; + +import { MessageType } from '../MessageType'; +import { IDlcMessage } from './DlcMessage'; + +/** + * The BatchFundingGroup TLV contains information about the intent to + * enter multiple DLCs simulatenously within one batch dlc funding + * transaction in the contract negotiation stage of the peer protocol + * + * This is the first step toward creating a batch dlc funding transaction + * + * A DlcOffer or DlcAccept can contain one or multiple BatchFundingInfo + * TLVs to specify one or more groupings. This allows specification of + * collateral put towards different types of contracts, such as options + * contracts, futures contracts, or other investment types. + * + * Attributes: + * - tempContractIds: Temporary identifiers for contracts proposed in DlcOffers. + * - contractIds: Identifiers for contracts that have been accepted and are + * part of the funding transaction. These are derived from DlcOffers and DlcAccepts. + * - allocatedCollateral: The amount of collateral allocated to the contracts + * within this group. This is specified early in the negotiation process. + * - eventIds: Oracle event identifiers for the contracts in this group. These + * are also specified early in the negotiation process. + * + * Note: During the early stages of the negotiation protocol, only allocatedCollateral + * and eventIds are specified. tempContractIds and contractIds are added to the + * DlcAccept upon creation. + */ +export class BatchFundingGroup implements IDlcMessage { + public static type = MessageType.BatchFundingGroup; + + /** + * Deserializes a batch_contract_info message + * @param buf + */ + public static deserialize(buf: Buffer): BatchFundingGroup { + const instance = new BatchFundingGroup(); + const reader = new BufferReader(buf); + + reader.readBigSize(); // read type + const tempContractIdsCount = reader.readBigSize(); + for (let i = 0; i < Number(tempContractIdsCount); i++) { + const length = reader.readBigSize(); + instance.tempContractIds.push(reader.readBytes(Number(length))); + } + + const contractIdsCount = reader.readBigSize(); + for (let i = 0; i < Number(contractIdsCount); i++) { + const length = reader.readBigSize(); + instance.contractIds.push(reader.readBytes(Number(length))); + } + + instance.allocatedCollateral = Value.fromSats(reader.readUInt64BE()); + + const eventIdsCount = reader.readBigSize(); + for (let i = 0; i < Number(eventIdsCount); i++) { + const length = reader.readBigSize(); + instance.eventIds.push(reader.readBytes(Number(length)).toString()); + } + + return instance; + } + + /** + * The type for batch_contract_info message. + */ + public type = BatchFundingGroup.type; + + public tempContractIds: Buffer[] = []; + + public contractIds: Buffer[] = []; + + public allocatedCollateral: Value; + + public eventIds: string[] = []; + + /** + * Converts batch_funding_info to JSON + */ + public toJSON(): IBatchFundingGroupJSON { + return { + type: this.type, + tempContractIds: this.tempContractIds.map((id) => id.toString('hex')), + contractIds: this.contractIds.map((id) => id.toString('hex')), + totalCollateral: Number(this.allocatedCollateral.sats), + eventIds: this.eventIds, + }; + } + + /** + * Serializes the batch_funding_info message into a Buffer + */ + public serialize(): Buffer { + const writer = new BufferWriter(); + writer.writeBigSize(this.type); + + writer.writeBigSize(this.tempContractIds.length); + this.tempContractIds.forEach((id) => { + writer.writeBigSize(id.length); + writer.writeBytes(id); + }); + + writer.writeBigSize(this.contractIds.length); + this.contractIds.forEach((id) => { + writer.writeBigSize(id.length); + writer.writeBytes(id); + }); + + writer.writeUInt64BE(this.allocatedCollateral.sats); + + writer.writeBigSize(this.eventIds.length); + this.eventIds.forEach((id) => { + const idBuffer = Buffer.from(id); + writer.writeBigSize(idBuffer.length); + writer.writeBytes(idBuffer); + }); + + return writer.toBuffer(); + } +} + +export interface IBatchFundingGroupJSON { + type: number; + tempContractIds: string[]; + contractIds: string[]; + totalCollateral: number; + eventIds: string[]; +} diff --git a/packages/messaging/lib/messages/DlcAccept.ts b/packages/messaging/lib/messages/DlcAccept.ts index 4b876d6b..91f95cd5 100644 --- a/packages/messaging/lib/messages/DlcAccept.ts +++ b/packages/messaging/lib/messages/DlcAccept.ts @@ -6,7 +6,9 @@ import { address } from 'bitcoinjs-lib'; import secp256k1 from 'secp256k1'; import { MessageType } from '../MessageType'; +import { deserializeTlv } from '../serialize/deserializeTlv'; import { getTlv, skipTlv } from '../serialize/getTlv'; +import { BatchFundingGroup, IBatchFundingGroupJSON } from './BatchFundingGroup'; import { CetAdaptorSignaturesV0, ICetAdaptorSignaturesV0JSON, @@ -85,6 +87,23 @@ export class DlcAcceptV0 extends DlcAccept implements IDlcMessage { instance.refundSignature = reader.readBytes(64); instance.negotiationFields = NegotiationFields.deserialize(getTlv(reader)); + while (!reader.eof) { + const buf = getTlv(reader); + const tlvReader = new BufferReader(buf); + const { type } = deserializeTlv(tlvReader); + + switch (Number(type)) { + case MessageType.BatchFundingGroup: + if (!instance.batchFundingGroups) { + instance.batchFundingGroups = []; + } + instance.batchFundingGroups.push(BatchFundingGroup.deserialize(buf)); + break; + default: + break; + } + } + return instance; } @@ -115,6 +134,8 @@ export class DlcAcceptV0 extends DlcAccept implements IDlcMessage { public negotiationFields: NegotiationFields; + public batchFundingGroups?: BatchFundingGroup[]; + /** * Get funding, change and payout address from DlcOffer * @param network Bitcoin Network @@ -196,6 +217,14 @@ export class DlcAcceptV0 extends DlcAccept implements IDlcMessage { * Converts dlc_accept_v0 to JSON */ public toJSON(): IDlcAcceptV0JSON { + const tlvs = []; + + if (this.batchFundingGroups) { + this.batchFundingGroups.forEach((group) => { + tlvs.push(group.serialize()); + }); + } + return { type: this.type, tempContractId: this.tempContractId.toString('hex'), @@ -209,6 +238,7 @@ export class DlcAcceptV0 extends DlcAccept implements IDlcMessage { cetSignatures: this.cetSignatures.toJSON(), refundSignature: this.refundSignature.toString('hex'), negotiationFields: this.negotiationFields.toJSON(), + tlvs, }; } @@ -237,6 +267,11 @@ export class DlcAcceptV0 extends DlcAccept implements IDlcMessage { writer.writeBytes(this.refundSignature); writer.writeBytes(this.negotiationFields.serialize()); + if (this.batchFundingGroups) + this.batchFundingGroups.forEach((fundingInfo) => + writer.writeBytes(fundingInfo.serialize()), + ); + return writer.toBuffer(); } @@ -251,6 +286,7 @@ export class DlcAcceptV0 extends DlcAccept implements IDlcMessage { this.changeSPK, this.changeSerialId, this.negotiationFields, + this.batchFundingGroups, ); } } @@ -266,6 +302,7 @@ export class DlcAcceptWithoutSigs { readonly changeSPK: Buffer, readonly changeSerialId: bigint, readonly negotiationFields: NegotiationFields, + readonly batchFundingGroups?: BatchFundingGroup[], ) {} } @@ -285,6 +322,7 @@ export interface IDlcAcceptV0JSON { | INegotiationFieldsV0JSON | INegotiationFieldsV1JSON | INegotiationFieldsV2JSON; + tlvs: IBatchFundingGroupJSON[]; } export interface IDlcAcceptV0Addresses { diff --git a/packages/messaging/lib/messages/DlcOffer.ts b/packages/messaging/lib/messages/DlcOffer.ts index c11d9f67..9a487faf 100644 --- a/packages/messaging/lib/messages/DlcOffer.ts +++ b/packages/messaging/lib/messages/DlcOffer.ts @@ -8,6 +8,7 @@ import secp256k1 from 'secp256k1'; import { MessageType } from '../MessageType'; import { deserializeTlv } from '../serialize/deserializeTlv'; import { getTlv } from '../serialize/getTlv'; +import { BatchFundingGroup, IBatchFundingGroupJSON } from './BatchFundingGroup'; import { ContractInfo, IContractInfoV0JSON, @@ -111,6 +112,12 @@ export class DlcOfferV0 extends DlcOffer implements IDlcMessage { case MessageType.OrderPositionInfoV0: instance.positionInfo = OrderPositionInfo.deserialize(buf); break; + case MessageType.BatchFundingGroup: + if (!instance.batchFundingGroups) { + instance.batchFundingGroups = []; + } + instance.batchFundingGroups.push(BatchFundingGroup.deserialize(buf)); + break; default: break; } @@ -158,6 +165,8 @@ export class DlcOfferV0 extends DlcOffer implements IDlcMessage { public positionInfo?: OrderPositionInfo; + public batchFundingGroups?: BatchFundingGroup[]; + /** * Get funding, change and payout address from DlcOffer * @param network Bitcoin Network @@ -294,6 +303,10 @@ export class DlcOfferV0 extends DlcOffer implements IDlcMessage { if (this.metadata) tlvs.push(this.metadata.toJSON()); if (this.ircInfo) tlvs.push(this.ircInfo.toJSON()); if (this.positionInfo) tlvs.push(this.positionInfo.toJSON()); + if (this.batchFundingGroups) + this.batchFundingGroups.forEach((fundingInfo) => + tlvs.push(fundingInfo.toJSON()), + ); return { type: this.type, @@ -346,6 +359,10 @@ export class DlcOfferV0 extends DlcOffer implements IDlcMessage { if (this.metadata) writer.writeBytes(this.metadata.serialize()); if (this.ircInfo) writer.writeBytes(this.ircInfo.serialize()); if (this.positionInfo) writer.writeBytes(this.positionInfo.serialize()); + if (this.batchFundingGroups) + this.batchFundingGroups.forEach((fundingInfo) => + writer.writeBytes(fundingInfo.serialize()), + ); return writer.toBuffer(); } @@ -367,7 +384,12 @@ export interface IDlcOfferV0JSON { feeRatePerVb: number; cetLocktime: number; refundLocktime: number; - tlvs: (IOrderMetadataJSON | IOrderIrcInfoJSON | IOrderPositionInfoJSON)[]; + tlvs: ( + | IOrderMetadataJSON + | IOrderIrcInfoJSON + | IOrderPositionInfoJSON + | IBatchFundingGroupJSON + )[]; } export interface IDlcOfferV0Addresses { diff --git a/packages/messaging/lib/messages/OrderOffer.ts b/packages/messaging/lib/messages/OrderOffer.ts index 6628ea3b..52d85e3c 100644 --- a/packages/messaging/lib/messages/OrderOffer.ts +++ b/packages/messaging/lib/messages/OrderOffer.ts @@ -9,6 +9,7 @@ import { validateBuffer, validateNumber, } from '../validation/validate'; +import { BatchFundingGroup, IBatchFundingGroupJSON } from './BatchFundingGroup'; import { ContractInfo, IContractInfoV0JSON, @@ -86,6 +87,12 @@ export class OrderOfferV0 extends OrderOffer implements IDlcMessage { case MessageType.OrderPositionInfoV0: instance.positionInfo = OrderPositionInfo.deserialize(buf); break; + case MessageType.BatchFundingGroup: + if (!instance.batchFundingGroups) { + instance.batchFundingGroups = []; + } + instance.batchFundingGroups.push(BatchFundingGroup.deserialize(buf)); + break; default: break; } @@ -117,6 +124,8 @@ export class OrderOfferV0 extends OrderOffer implements IDlcMessage { public positionInfo?: OrderPositionInfo; + public batchFundingGroups?: BatchFundingGroup[]; + public validate(): void { validateBuffer(this.chainHash, 'chainHash', OrderOfferV0.name, 32); this.contractInfo.validate(); @@ -185,6 +194,10 @@ export class OrderOfferV0 extends OrderOffer implements IDlcMessage { if (this.metadata) tlvs.push(this.metadata.toJSON()); if (this.ircInfo) tlvs.push(this.ircInfo.toJSON()); if (this.positionInfo) tlvs.push(this.positionInfo.toJSON()); + if (this.batchFundingGroups) + this.batchFundingGroups.forEach((fundingInfo) => + tlvs.push(fundingInfo.toJSON()), + ); return { type: this.type, @@ -214,6 +227,10 @@ export class OrderOfferV0 extends OrderOffer implements IDlcMessage { if (this.metadata) writer.writeBytes(this.metadata.serialize()); if (this.ircInfo) writer.writeBytes(this.ircInfo.serialize()); if (this.positionInfo) writer.writeBytes(this.positionInfo.serialize()); + if (this.batchFundingGroups) + this.batchFundingGroups.forEach((fundingInfo) => + writer.writeBytes(fundingInfo.serialize()), + ); return writer.toBuffer(); } @@ -227,5 +244,10 @@ export interface IOrderOfferJSON { feeRatePerVb: number; cetLocktime: number; refundLocktime: number; - tlvs: (IOrderMetadataJSON | IOrderIrcInfoJSON | IOrderPositionInfoJSON)[]; + tlvs: ( + | IOrderMetadataJSON + | IOrderIrcInfoJSON + | IOrderPositionInfoJSON + | IBatchFundingGroupJSON + )[]; } diff --git a/packages/messaging/package.json b/packages/messaging/package.json index 8f77b2af..9132681a 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -22,6 +22,7 @@ "url": "git+https://github.com/atomicfinance/node-dlc.git" }, "dependencies": { + "@node-dlc/bitcoin": "^0.22.6", "@node-lightning/bitcoin": "0.26.1", "@node-lightning/bufio": "0.26.1", "@node-lightning/checksum": "0.26.1",