@node-dlc/core
Version:
339 lines (307 loc) • 11.1 kB
text/typescript
import {
HashValue,
LockTime,
OutPoint,
Script,
Sequence,
TxBuilder,
TxOut,
Value,
} from '@node-dlc/bitcoin';
import { CommitmentNumber } from './CommitmentNumber';
import { Htlc } from './Htlc';
import { HtlcDirection } from './HtlcDirection';
import { ScriptFactory } from './ScriptFactory';
export class TxFactory {
/**
* Creates a TxOut to attach to a funding transaction. This includes
* the P2WSH-P2MS script that uses 2-2MS. The open and accept funding
* pubkeys are sorted lexicographcially to create the script.
* @param builder
*/
public static createFundingOutput(
value: Value,
openPubKey: Buffer,
acceptPubKey: Buffer,
): TxOut {
const script = Script.p2wshLock(
ScriptFactory.fundingScript(openPubKey, acceptPubKey),
);
return new TxOut(value, script);
}
/**
* Constructs an unsigned commitment transaction according to BOLT3.
* This method is a low level commitment transaction builder, meaning
* it accepts primatives and constructs a commitment transaction
* accordingly. The proper inputs are determiend
*
* @param isFunderLocal True when the funding node is local. This
* is used to determine which output pays fees (to_local/to_remote).
* @param commitmentNumber The commitment number of the transaction
* which is used to generate the obscurred commitment number.
* @param openPaymentBasePoint The basepoint sent in open_channel
* which is used to generate the obscurred commitment number.
* @param acceptPaymentBasePoint The basepoitn sent in accept_channel
* which is used to generate the obscurred commitment number.
* @param fundingOutPoint The outpoint of the funding transaction
* which was established in funding_created.
* @param dustLimitSatoshi The dust limit in sats after which outputs
* will be prune
* @param feePerKw The fee rate per kiloweight which will be deducted
* from the funding node's output
* @param localDelay The delay applied to the to_local output
* @param localValue Value paid to the to_local RSMC output
* @param remoteValue Value paid to the to_emote P2WPKH output
* @param revocationPubKey The revocation public key used to in the
* to_local and HTLC outputs
* @param delayedPubKey The delayed public key used to spend the
* to_local output
* @param remotePubKey The public key used to spend the to_remote
* output
* @param reverseHtlcs True when the HTLC direction needs to be
* inverted because the holder of this commitment transaction is
* our counterparty.
* @param localHtlcPubKey The public key used to spend HTLC outputs
* by the commitment holder.
* @param remoteHtlcPubKey The public key used to spend HTLC outputs
* by the commitment counterparty.
* @param htlcs A full list of HTLCs that will be selectively
* included in the commitment transaction based on the feePerKw.
*/
public static createCommitment(
isFunderLocal: boolean,
commitmentNumber: number,
openPaymentBasePoint: Buffer,
acceptPaymentBasePoint: Buffer,
fundingOutPoint: OutPoint,
dustLimitSatoshi: Value,
feePerKw: bigint,
localDelay: number,
localValue: Value,
remoteValue: Value,
revocationPubKey: Buffer,
delayedPubKey: Buffer,
remotePubKey: Buffer,
reverseHtlcs: boolean,
localHtlcPubKey?: Buffer,
remoteHtlcPubKey?: Buffer,
htlcs: Htlc[] = [],
): [TxBuilder, Htlc[]] {
const obscuredCommitmentNumber = CommitmentNumber.obscure(
commitmentNumber,
openPaymentBasePoint,
acceptPaymentBasePoint,
);
// 1. add the input as the funding outpoint and set the nSequence
const tx = new TxBuilder();
tx.version = 2;
tx.addInput(
fundingOutPoint,
CommitmentNumber.getSequence(obscuredCommitmentNumber),
);
// 2. set the locktime to the obscurred commitment number
tx.locktime = CommitmentNumber.getLockTime(obscuredCommitmentNumber);
// 3. find unpruned outputs
const unprunedHtlcs: Htlc[] = [];
for (const htlc of htlcs) {
const valueInSats = htlc.value.sats;
let feeWeight: bigint;
// HtlcDirection refers to the local nodes perception of the HTLC.
// When isLocal, offered uses the HTLC-Timeout weight of 663. When
// remote, the commitment is for the remote counterparty and an
// offered HTLC is received and will be spent by the remote
// counterparty using the HTLC-Success transaction with a weight of 703
if (reverseHtlcs) {
feeWeight =
htlc.direction === HtlcDirection.Offered ? BigInt(703) : BigInt(663);
} else {
feeWeight =
htlc.direction === HtlcDirection.Offered ? BigInt(663) : BigInt(703);
}
// Calculate the HTLC less fees
const feeInSats = (feeWeight * feePerKw) / BigInt(1000);
const satsLessFee = valueInSats - feeInSats;
// Only keep HTLCs greater than the dustLimitSatoshi for the tx
if (satsLessFee >= dustLimitSatoshi.sats) {
unprunedHtlcs.push(htlc);
}
}
// 4. calculate base fee
const weight = 724 + unprunedHtlcs.length * 172;
const baseFee = (BigInt(weight) * feePerKw) / BigInt(1000);
// 5. substract base fee from funding node
if (isFunderLocal) {
const newValue = localValue.sats - baseFee;
if (newValue > BigInt(0)) {
localValue = Value.fromSats(newValue);
} else {
localValue = Value.zero();
}
} else {
const newValue = remoteValue.sats - baseFee;
if (newValue > BigInt(0)) {
remoteValue = Value.fromSats(newValue);
} else {
remoteValue = Value.zero();
}
}
// 6/7. add unpruned offered/received HTLCs
const txouts: Array<[TxOut, Htlc?]> = [];
for (const htlc of unprunedHtlcs) {
const witnessScript: Script =
(!reverseHtlcs && htlc.direction === HtlcDirection.Offered) ||
(reverseHtlcs && htlc.direction === HtlcDirection.Accepted)
? ScriptFactory.offeredHtlcScript(
htlc.paymentHash,
revocationPubKey,
localHtlcPubKey,
remoteHtlcPubKey,
)
: ScriptFactory.receivedHtlcScript(
htlc.paymentHash,
htlc.cltvExpiry,
revocationPubKey,
localHtlcPubKey,
remoteHtlcPubKey,
);
const txout = new TxOut(htlc.value, Script.p2wshLock(witnessScript));
txouts.push([txout, htlc]);
}
// 8. add local if unpruned
if (localValue.sats >= dustLimitSatoshi.sats) {
txouts.push([
new TxOut(
localValue,
Script.p2wshLock(
ScriptFactory.toLocalScript(
revocationPubKey,
delayedPubKey,
localDelay,
),
),
),
]);
}
// 9. add remote if unpruned
if (remoteValue.sats >= dustLimitSatoshi.sats) {
txouts.push([new TxOut(remoteValue, Script.p2wpkhLock(remotePubKey))]);
}
// 10. sort outputs using bip69 and using cltv for htlc tiebreaks
txouts.sort((a, b) => {
// compare on value
const value = Number(a[0].value.sats - b[0].value.sats);
if (value !== 0) return value;
// compare on script
const scriptCompare = a[0].scriptPubKey
.serializeCmds()
.compare(b[0].scriptPubKey.serializeCmds());
if (scriptCompare !== 0) return scriptCompare;
// tie-break on htlcs
return b[1].cltvExpiry - a[1].cltvExpiry;
});
// add hte outputs in sorted order
const sortedHtlcs: Htlc[] = [];
for (const [txout, htlc] of txouts) {
tx.addOutput(txout);
sortedHtlcs.push(htlc);
}
// return the tuple with the sorted htlcs
return [tx, sortedHtlcs];
}
/**
* Constructs an HTLC-Timeout transaction as defined in BOLT3. This
* transaction spends an offered HTLC from the commitment transaction
* and outputs the HTLC value less the fee. The output is spendable
* via an RSMC that is sequence locked for the received by the
* transaction owner. Finally this transaction has an absolute
* locktime of the HTLC's cltv expiry.
* @param commitmentTx
* @param outputIndex
* @param localDelay
* @param revocationPubKey
* @param delayedPubKey
* @param feePerKw
* @param htlc
*/
public static createHtlcTimeout(
commitmentTx: HashValue,
outputIndex: number,
localDelay: number,
revocationPubKey: Buffer,
delayedPubKey: Buffer,
feePerKw: bigint,
htlc: Htlc,
): TxBuilder {
const tx = new TxBuilder();
// Input points to the commmitment transaction and the BIP69
// sorted index of the HTLC. nSequence is set to zero.
tx.addInput(new OutPoint(commitmentTx, outputIndex), Sequence.zero());
// calc value less fees for this transaction
const weight = BigInt(663);
const fees = (weight * feePerKw) / BigInt(1000);
const sats = fees > htlc.value.sats ? 0 : htlc.value.sats - fees;
// Spends a P2WSH RSMC
tx.addOutput(
Value.fromSats(sats),
Script.p2wshLock(
ScriptFactory.toLocalScript(
revocationPubKey,
delayedPubKey,
localDelay,
),
),
);
// nLocktime is set to the cltvExpiry of the HTLC. This prevents
// the HTLC-Timeout from being broadcast until after the expiry
// has been reached.
tx.locktime = new LockTime(htlc.cltvExpiry);
return tx;
}
/**
* Constructs an HTLC-Success transaction as defined in BOLT3. This
* transaction spends a received HTLC form the commitment transaction
* and outputs the HTLC value less the fee. The output is spendable
* via an RSMC that is sequence locked for the received by the
* transaction owner.
* @param commitmentTx
* @param outputIndex
* @param localDelay
* @param revocationPubKey
* @param delayedPubKey
* @param feePerKw
* @param htlc
*/
public static createHtlcSuccess(
commitmentTx: HashValue,
outputIndex: number,
localDelay: number,
revocationPubKey: Buffer,
delayedPubKey: Buffer,
feePerKw: bigint,
htlc: Htlc,
): TxBuilder {
const tx = new TxBuilder();
// Input points to the commmitment transaction and the BIP69
// sorted index of the HTLC. nSequence is set to zero.
tx.addInput(new OutPoint(commitmentTx, outputIndex), Sequence.zero());
// calc value less fees for this transaction
const weight = BigInt(703);
const fees = (weight * feePerKw) / BigInt(1000);
const sats = fees > htlc.value.sats ? 0 : htlc.value.sats - fees;
// Spends a P2WSH RSMC
tx.addOutput(
Value.fromSats(sats),
Script.p2wshLock(
ScriptFactory.toLocalScript(
revocationPubKey,
delayedPubKey,
localDelay,
),
),
);
// nLockTime is zero since the tx owner can immediately spend
// this transaction if they have the preimage
tx.locktime = LockTime.zero();
return tx;
}
}