@node-dlc/messaging
Version:
DLC Messaging Protocol
285 lines • 12.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DlcAcceptContainer = exports.DlcAcceptWithoutSigs = exports.DlcAcceptV0 = exports.DlcAccept = void 0;
const bitcoin_1 = require("@node-dlc/bitcoin");
const bufio_1 = require("@node-dlc/bufio");
const crypto_1 = require("@node-dlc/crypto");
const bitcoinjs_lib_1 = require("bitcoinjs-lib");
const secp256k1_1 = __importDefault(require("secp256k1"));
const MessageType_1 = require("../MessageType");
const deserializeTlv_1 = require("../serialize/deserializeTlv");
const getTlv_1 = require("../serialize/getTlv");
const BatchFundingGroup_1 = require("./BatchFundingGroup");
const CetAdaptorSignaturesV0_1 = require("./CetAdaptorSignaturesV0");
const FundingInput_1 = require("./FundingInput");
const NegotiationFields_1 = require("./NegotiationFields");
class DlcAccept {
static deserialize(buf, parseCets = true) {
const reader = new bufio_1.BufferReader(buf);
const type = Number(reader.readUInt16BE());
switch (type) {
case MessageType_1.MessageType.DlcAcceptV0:
return DlcAcceptV0.deserialize(buf, parseCets);
default:
throw new Error(`Dlc Accept message type must be DlcAcceptV0`);
}
}
}
exports.DlcAccept = DlcAccept;
/**
* 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.
*/
class DlcAcceptV0 extends DlcAccept {
constructor() {
super(...arguments);
/**
* The type for accept_channel message. accept_channel = 33
*/
this.type = DlcAcceptV0.type;
this.fundingInputs = [];
}
/**
* Deserializes an oracle_info message
* @param buf
*/
static deserialize(buf, parseCets = true) {
const instance = new DlcAcceptV0();
const reader = new bufio_1.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(FundingInput_1.FundingInputV0.deserialize((0, getTlv_1.getTlv)(reader)));
}
const changeSPKLen = reader.readUInt16BE();
instance.changeSPK = reader.readBytes(changeSPKLen);
instance.changeSerialId = reader.readUInt64BE();
if (parseCets) {
instance.cetSignatures = CetAdaptorSignaturesV0_1.CetAdaptorSignaturesV0.deserialize((0, getTlv_1.getTlv)(reader));
}
else {
(0, getTlv_1.skipTlv)(reader);
instance.cetSignatures = new CetAdaptorSignaturesV0_1.CetAdaptorSignaturesV0();
}
instance.refundSignature = reader.readBytes(64);
instance.negotiationFields = NegotiationFields_1.NegotiationFields.deserialize((0, getTlv_1.getTlv)(reader));
while (!reader.eof) {
const buf = (0, getTlv_1.getTlv)(reader);
const tlvReader = new bufio_1.BufferReader(buf);
const { type } = (0, deserializeTlv_1.deserializeTlv)(tlvReader);
switch (Number(type)) {
case MessageType_1.MessageType.BatchFundingGroup:
if (!instance.batchFundingGroups) {
instance.batchFundingGroups = [];
}
instance.batchFundingGroups.push(BatchFundingGroup_1.BatchFundingGroup.deserialize(buf));
break;
default:
break;
}
}
return instance;
}
/**
* Get funding, change and payout address from DlcOffer
* @param network Bitcoin Network
* @returns {IDlcOfferV0Addresses}
*/
getAddresses(network) {
const fundingSPK = bitcoin_1.Script.p2wpkhLock((0, crypto_1.hash160)(this.fundingPubKey))
.serialize()
.slice(1);
const fundingAddress = bitcoinjs_lib_1.address.fromOutputScript(fundingSPK, network);
const changeAddress = bitcoinjs_lib_1.address.fromOutputScript(this.changeSPK, network);
const payoutAddress = bitcoinjs_lib_1.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
*/
validate() {
// 1. Type is set automatically in class
// 2. payout_spk and change_spk must be standard script pubkeys
try {
bitcoinjs_lib_1.address.fromOutputScript(this.payoutSPK);
}
catch (e) {
throw new Error('payoutSPK is invalid');
}
try {
bitcoinjs_lib_1.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_1.default.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) => 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) => input.validate());
// validate funding amount
const fundingAmount = this.fundingInputs.reduce((acc, fundingInput) => {
const input = fundingInput;
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
*/
toJSON() {
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
*/
serialize() {
const writer = new bufio_1.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();
}
withoutSigs() {
return new DlcAcceptWithoutSigs(this.tempContractId, this.acceptCollateralSatoshis, this.fundingPubKey, this.payoutSPK, this.payoutSerialId, this.fundingInputs, this.changeSPK, this.changeSerialId, this.negotiationFields, this.batchFundingGroups);
}
}
exports.DlcAcceptV0 = DlcAcceptV0;
DlcAcceptV0.type = MessageType_1.MessageType.DlcAcceptV0;
class DlcAcceptWithoutSigs {
constructor(tempContractId, acceptCollateralSatoshis, fundingPubKey, payoutSPK, payoutSerialId, fundingInputs, changeSPK, changeSerialId, negotiationFields, batchFundingGroups) {
this.tempContractId = tempContractId;
this.acceptCollateralSatoshis = acceptCollateralSatoshis;
this.fundingPubKey = fundingPubKey;
this.payoutSPK = payoutSPK;
this.payoutSerialId = payoutSerialId;
this.fundingInputs = fundingInputs;
this.changeSPK = changeSPK;
this.changeSerialId = changeSerialId;
this.negotiationFields = negotiationFields;
this.batchFundingGroups = batchFundingGroups;
}
}
exports.DlcAcceptWithoutSigs = DlcAcceptWithoutSigs;
class DlcAcceptContainer {
constructor() {
this.accepts = [];
}
/**
* Adds a DlcAccept to the container.
* @param accept The DlcAccept to add.
*/
addAccept(accept) {
this.accepts.push(accept);
}
/**
* Returns all DlcAccepts in the container.
* @returns An array of DlcAccept instances.
*/
getAccepts() {
return this.accepts;
}
/**
* Serializes all DlcAccepts in the container to a Buffer.
* @returns A Buffer containing the serialized DlcAccepts.
*/
serialize() {
const writer = new bufio_1.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.
*/
static deserialize(buf, parseCets = true) {
const reader = new bufio_1.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;
}
}
exports.DlcAcceptContainer = DlcAcceptContainer;
//# sourceMappingURL=DlcAccept.js.map