UNPKG

@node-dlc/messaging

Version:
389 lines (332 loc) 12.2 kB
import { Script } from '@node-dlc/bitcoin'; import { BufferReader, BufferWriter } from '@node-dlc/bufio'; import { hash160 } from '@node-dlc/crypto'; import { BitcoinNetwork } from 'bitcoin-networks'; 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, } from './CetAdaptorSignaturesV0'; import { IDlcMessage } from './DlcMessage'; import { FundingInputV0, IFundingInputV0JSON } from './FundingInput'; import { INegotiationFieldsV0JSON, INegotiationFieldsV1JSON, INegotiationFieldsV2JSON, NegotiationFields, } from './NegotiationFields'; export abstract class DlcAccept implements IDlcMessage { public static deserialize(buf: Buffer, parseCets = true): DlcAcceptV0 { const reader = new BufferReader(buf); const type = Number(reader.readUInt16BE()); switch (type) { case MessageType.DlcAcceptV0: return DlcAcceptV0.deserialize(buf, parseCets); default: throw new Error(`Dlc Accept message type must be DlcAcceptV0`); } } public abstract type: number; public abstract getAddresses(network: BitcoinNetwork): IDlcAcceptV0Addresses; public abstract toJSON(): IDlcAcceptV0JSON; public abstract serialize(): Buffer; } /** * DlcAccept contains information about a node and indicates its * acceptance of the new DLC, as well as its CET and refund * transaction signatures. This is the second step toward creating * the funding transaction and closing transactions. */ export class DlcAcceptV0 extends DlcAccept implements IDlcMessage { public static type = MessageType.DlcAcceptV0; /** * Deserializes an oracle_info message * @param buf */ public static deserialize(buf: Buffer, parseCets = true): DlcAcceptV0 { const instance = new DlcAcceptV0(); const reader = new BufferReader(buf); reader.readUInt16BE(); // read type instance.tempContractId = reader.readBytes(32); instance.acceptCollateralSatoshis = reader.readUInt64BE(); instance.fundingPubKey = reader.readBytes(33); const payoutSPKLen = reader.readUInt16BE(); instance.payoutSPK = reader.readBytes(payoutSPKLen); instance.payoutSerialId = reader.readUInt64BE(); const fundingInputsLen = reader.readUInt16BE(); for (let i = 0; i < fundingInputsLen; i++) { instance.fundingInputs.push(FundingInputV0.deserialize(getTlv(reader))); } const changeSPKLen = reader.readUInt16BE(); instance.changeSPK = reader.readBytes(changeSPKLen); instance.changeSerialId = reader.readUInt64BE(); if (parseCets) { instance.cetSignatures = CetAdaptorSignaturesV0.deserialize( getTlv(reader), ); } else { skipTlv(reader); instance.cetSignatures = new CetAdaptorSignaturesV0(); } 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; } /** * The type for accept_channel message. accept_channel = 33 */ public type = DlcAcceptV0.type; public tempContractId: Buffer; public acceptCollateralSatoshis: bigint; public fundingPubKey: Buffer; public payoutSPK: Buffer; public payoutSerialId: bigint; public fundingInputs: FundingInputV0[] = []; public changeSPK: Buffer; public changeSerialId: bigint; public cetSignatures: CetAdaptorSignaturesV0; public refundSignature: Buffer; public negotiationFields: NegotiationFields; public batchFundingGroups?: BatchFundingGroup[]; /** * Get funding, change and payout address from DlcOffer * @param network Bitcoin Network * @returns {IDlcOfferV0Addresses} */ public getAddresses(network: BitcoinNetwork): IDlcAcceptV0Addresses { const fundingSPK = Script.p2wpkhLock(hash160(this.fundingPubKey)) .serialize() .slice(1); const fundingAddress = address.fromOutputScript(fundingSPK, network); const changeAddress = address.fromOutputScript(this.changeSPK, network); const payoutAddress = address.fromOutputScript(this.payoutSPK, network); return { fundingAddress, changeAddress, payoutAddress, }; } /** * Validates correctness of all fields * https://github.com/discreetlogcontracts/dlcspecs/blob/master/Protocol.md#the-accept_dlc-message * @throws Will throw an error if validation fails */ public validate(): void { // 1. Type is set automatically in class // 2. payout_spk and change_spk must be standard script pubkeys try { address.fromOutputScript(this.payoutSPK); } catch (e) { throw new Error('payoutSPK is invalid'); } try { address.fromOutputScript(this.changeSPK); } catch (e) { throw new Error('changeSPK is invalid'); } // 3. funding_pubkey must be a valid secp256k1 pubkey in compressed format // https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki#background-on-ecdsa-signatures if (secp256k1.publicKeyVerify(Buffer.from(this.fundingPubKey))) { if (this.fundingPubKey[0] != 0x02 && this.fundingPubKey[0] != 0x03) { throw new Error('fundingPubKey must be in compressed format'); } } else { throw new Error('fundingPubKey is not a valid secp256k1 key'); } // 4. inputSerialId must be unique for each input const inputSerialIds = this.fundingInputs.map( (input: FundingInputV0) => input.inputSerialId, ); if (new Set(inputSerialIds).size !== inputSerialIds.length) { throw new Error('inputSerialIds must be unique'); } // 5. Ensure funding inputs are segwit this.fundingInputs.forEach((input: FundingInputV0) => input.validate()); // validate funding amount const fundingAmount = this.fundingInputs.reduce((acc, fundingInput) => { const input = fundingInput as FundingInputV0; return acc + input.prevTx.outputs[input.prevTxVout].value.sats; }, BigInt(0)); if (this.acceptCollateralSatoshis >= fundingAmount) { throw new Error( 'fundingAmount must be greater than acceptCollateralSatoshis', ); } } /** * 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'), acceptCollateralSatoshis: Number(this.acceptCollateralSatoshis), fundingPubKey: this.fundingPubKey.toString('hex'), payoutSPK: this.payoutSPK.toString('hex'), payoutSerialId: Number(this.payoutSerialId), fundingInputs: this.fundingInputs.map((input) => input.toJSON()), changeSPK: this.changeSPK.toString('hex'), changeSerialId: Number(this.changeSerialId), cetSignatures: this.cetSignatures.toJSON(), refundSignature: this.refundSignature.toString('hex'), negotiationFields: this.negotiationFields.toJSON(), tlvs, }; } /** * Serializes the accept_channel message into a Buffer */ public serialize(): Buffer { const writer = new BufferWriter(); writer.writeUInt16BE(this.type); writer.writeBytes(this.tempContractId); writer.writeUInt64BE(this.acceptCollateralSatoshis); writer.writeBytes(this.fundingPubKey); writer.writeUInt16BE(this.payoutSPK.length); writer.writeBytes(this.payoutSPK); writer.writeUInt64BE(this.payoutSerialId); writer.writeUInt16BE(this.fundingInputs.length); for (const fundingInput of this.fundingInputs) { writer.writeBytes(fundingInput.serialize()); } writer.writeUInt16BE(this.changeSPK.length); writer.writeBytes(this.changeSPK); writer.writeUInt64BE(this.changeSerialId); writer.writeBytes(this.cetSignatures.serialize()); writer.writeBytes(this.refundSignature); writer.writeBytes(this.negotiationFields.serialize()); if (this.batchFundingGroups) this.batchFundingGroups.forEach((fundingInfo) => writer.writeBytes(fundingInfo.serialize()), ); return writer.toBuffer(); } public withoutSigs(): DlcAcceptWithoutSigs { return new DlcAcceptWithoutSigs( this.tempContractId, this.acceptCollateralSatoshis, this.fundingPubKey, this.payoutSPK, this.payoutSerialId, this.fundingInputs, this.changeSPK, this.changeSerialId, this.negotiationFields, this.batchFundingGroups, ); } } export class DlcAcceptWithoutSigs { constructor( readonly tempContractId: Buffer, readonly acceptCollateralSatoshis: bigint, readonly fundingPubKey: Buffer, readonly payoutSPK: Buffer, readonly payoutSerialId: bigint, readonly fundingInputs: FundingInputV0[], readonly changeSPK: Buffer, readonly changeSerialId: bigint, readonly negotiationFields: NegotiationFields, readonly batchFundingGroups?: BatchFundingGroup[], ) {} } export interface IDlcAcceptV0JSON { type: number; tempContractId: string; acceptCollateralSatoshis: number; fundingPubKey: string; payoutSPK: string; payoutSerialId: number; fundingInputs: IFundingInputV0JSON[]; changeSPK: string; changeSerialId: number; cetSignatures: ICetAdaptorSignaturesV0JSON; refundSignature: string; negotiationFields: | INegotiationFieldsV0JSON | INegotiationFieldsV1JSON | INegotiationFieldsV2JSON; tlvs: IBatchFundingGroupJSON[]; } export interface IDlcAcceptV0Addresses { fundingAddress: string; changeAddress: string; payoutAddress: string; } export class DlcAcceptContainer { private accepts: DlcAccept[] = []; /** * Adds a DlcAccept to the container. * @param accept The DlcAccept to add. */ public addAccept(accept: DlcAccept): void { this.accepts.push(accept); } /** * Returns all DlcAccepts in the container. * @returns An array of DlcAccept instances. */ public getAccepts(): DlcAccept[] { return this.accepts; } /** * Serializes all DlcAccepts in the container to a Buffer. * @returns A Buffer containing the serialized DlcAccepts. */ public serialize(): Buffer { const writer = new BufferWriter(); // Write the number of accepts in the container first. writer.writeBigSize(this.accepts.length); // Serialize each accept and write it. this.accepts.forEach((accept) => { const serializedAccept = accept.serialize(); // Optionally, write the length of the serialized accept for easier deserialization. writer.writeBigSize(serializedAccept.length); writer.writeBytes(serializedAccept); }); return writer.toBuffer(); } /** * Deserializes a Buffer into a DlcAcceptContainer with DlcAccepts. * @param buf The Buffer to deserialize. * @returns A DlcAcceptContainer instance. */ public static deserialize(buf: Buffer, parseCets = true): DlcAcceptContainer { const reader = new BufferReader(buf); const container = new DlcAcceptContainer(); const acceptsCount = reader.readBigSize(); for (let i = 0; i < acceptsCount; i++) { const acceptLength = reader.readBigSize(); const acceptBuf = reader.readBytes(Number(acceptLength)); const accept = DlcAccept.deserialize(acceptBuf, parseCets); // Adjust based on actual implementation. container.addAccept(accept); } return container; } }