Skip to content

Commit

Permalink
Clarify commit tx fee anchor cost (#1721)
Browse files Browse the repository at this point in the history
Since anchor outputs, we not only deduce the commit tx fee from the funder's
main output but the cost of the anchors as well.

We rename the function that does that for more clarity.
  • Loading branch information
t-bast authored Mar 24, 2021
1 parent f39718a commit f202587
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,9 @@ case class Commitments(channelVersion: ChannelVersion,
val balanceNoFees = (reduced.toRemote - remoteParams.channelReserve).max(0 msat)
if (localParams.isFunder) {
// The funder always pays the on-chain fees, so we must subtract that from the amount we can send.
val commitFees = commitTxFeeMsat(remoteParams.dustLimit, reduced, commitmentFormat)
val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, commitmentFormat)
// the funder needs to keep a "funder fee buffer" (see explanation above)
val funderFeeBuffer = commitTxFeeMsat(remoteParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val funderFeeBuffer = commitTxTotalCostMsat(remoteParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val amountToReserve = commitFees.max(funderFeeBuffer)
if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, commitmentFormat)) {
// htlc will be trimmed
Expand All @@ -220,9 +220,9 @@ case class Commitments(channelVersion: ChannelVersion,
balanceNoFees
} else {
// The funder always pays the on-chain fees, so we must subtract that from the amount we can receive.
val commitFees = commitTxFeeMsat(localParams.dustLimit, reduced, commitmentFormat)
val commitFees = commitTxTotalCostMsat(localParams.dustLimit, reduced, commitmentFormat)
// we expected the funder to keep a "funder fee buffer" (see explanation above)
val funderFeeBuffer = commitTxFeeMsat(localParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val funderFeeBuffer = commitTxTotalCostMsat(localParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val amountToReserve = commitFees.max(funderFeeBuffer)
if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, commitmentFormat)) {
// htlc will be trimmed
Expand Down Expand Up @@ -306,10 +306,10 @@ object Commitments {
val outgoingHtlcs = reduced.htlcs.collect(incoming)

// note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment
val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat)
val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat)
// the funder needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid
// getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728).
val funderFeeBuffer = commitTxFeeMsat(commitments1.remoteParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitments.commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitments.commitmentFormat)
val funderFeeBuffer = commitTxTotalCostMsat(commitments1.remoteParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitments.commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitments.commitmentFormat)
// NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold)
// which may result in a lower commit tx fee; this is why we take the max of the two.
val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat)
Expand Down Expand Up @@ -365,7 +365,7 @@ object Commitments {
val incomingHtlcs = reduced.htlcs.collect(incoming)

// note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment
val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat)
val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat)
// NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote funder that doesn't have this mitigation in place
// We could enforce it once we're confident a large portion of the network implements it.
val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees)
Expand Down Expand Up @@ -484,7 +484,7 @@ object Commitments {

// a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee
// we look from remote's point of view, so if local is funder remote doesn't pay the fees
val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat)
val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat)
val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees
if (missing < 0.sat) {
Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees))
Expand Down Expand Up @@ -517,7 +517,7 @@ object Commitments {
val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed)

// a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee
val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat)
val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat)
val missing = reduced.toRemote.truncateToSatoshi - commitments1.localParams.channelReserve - fees
if (missing < 0.sat) {
Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ object Helpers {
if (!localParams.isFunder) {
// they are funder, therefore they pay the fee: we need to make sure they can afford it!
val toRemoteMsat = remoteSpec.toLocal
val fees = commitTxFee(remoteParams.dustLimit, remoteSpec, channelVersion.commitmentFormat)
val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelVersion.commitmentFormat)
val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees
if (missing < Satoshi(0)) {
return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,28 +232,33 @@ object Transactions {
/** Fee for an un-trimmed HTLC. */
def htlcOutputFee(feeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat): MilliSatoshi = weight2feeMsat(feeratePerKw, commitmentFormat.htlcOutputWeight)

/**
* While fees are generally computed in Satoshis (since this is the smallest on-chain unit), it may be useful in some
* cases to calculate it in MilliSatoshi to avoid rounding issues.
* If you are adding multiple fees together for example, you should always add them in MilliSatoshi and then round
* down to Satoshi.
*/
/** Fee paid by the commit tx (depends on which HTLCs will be trimmed). */
def commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): MilliSatoshi = {
val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec, commitmentFormat)
val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec, commitmentFormat)
val weight = commitmentFormat.commitWeight + commitmentFormat.htlcOutputWeight * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size)
val fee = weight2feeMsat(spec.feeratePerKw, weight)
weight2feeMsat(spec.feeratePerKw, weight)
}

/**
* While on-chain amounts are generally computed in Satoshis (since this is the smallest on-chain unit), it may be
* useful in some cases to calculate it in MilliSatoshi to avoid rounding issues.
* If you are adding multiple fees together for example, you should always add them in MilliSatoshi and then round
* down to Satoshi.
*/
def commitTxTotalCostMsat(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): MilliSatoshi = {
// The funder pays the on-chain fee by deducing it from its main output.
val txFee = commitTxFeeMsat(dustLimit, spec, commitmentFormat)
// When using anchor outputs, the funder pays for *both* anchors all the time, even if only one anchor is present.
// This is not technically a fee (it doesn't go to miners) but it has to be deduced from the funder's main output,
// so for simplicity we deduce it here.
// This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the funder's main output.
val anchorsCost = commitmentFormat match {
case DefaultCommitmentFormat => Satoshi(0)
case AnchorOutputsCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2
}
fee + anchorsCost
txFee + anchorsCost
}

def commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = commitTxFeeMsat(dustLimit, spec, commitmentFormat).truncateToSatoshi
def commitTxTotalCost(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = commitTxTotalCostMsat(dustLimit, spec, commitmentFormat).truncateToSatoshi

/**
* @param commitTxNumber commit tx number
Expand Down Expand Up @@ -357,9 +362,9 @@ object Transactions {
val hasHtlcs = outputs.nonEmpty

val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) {
(spec.toLocal.truncateToSatoshi - commitTxFee(localDustLimit, spec, commitmentFormat), spec.toRemote.truncateToSatoshi)
(spec.toLocal.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat), spec.toRemote.truncateToSatoshi)
} else {
(spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxFee(localDustLimit, spec, commitmentFormat))
(spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat))
} // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway

if (toLocalAmount >= localDustLimit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging {
Transactions.addSigs(tx, Local.funding_pubkey, Remote.funding_pubkey, local_sig, remote_sig)
}

val baseFee = Transactions.commitTxFee(Local.dustLimit, spec, commitmentFormat)
val baseFee = Transactions.commitTxFeeMsat(Local.dustLimit, spec, commitmentFormat)
logger.info(s"# base commitment transaction fee = ${baseFee.toLong}")
val actualFee = fundingAmount - commitTx.tx.txOut.map(_.amount).sum
logger.info(s"# actual commitment transaction fee = ${actualFee.toLong}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ class TransactionsSpec extends AnyFunSuite with Logging {
IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000 msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket))
)
val spec = CommitmentSpec(htlcs, feeratePerKw = FeeratePerKw(5000 sat), toLocal = 0 msat, toRemote = 0 msat)
val fee = Transactions.commitTxFee(546 sat, spec, DefaultCommitmentFormat)
assert(fee === 5340.sat)
val fee = commitTxFeeMsat(546 sat, spec, DefaultCommitmentFormat)
assert(fee === 5340000.msat)
}

test("check pre-computed transaction weights") {
Expand Down Expand Up @@ -214,7 +214,7 @@ class TransactionsSpec extends AnyFunSuite with Logging {

test("generate valid commitment with some outputs that don't materialize (default commitment format)") {
val spec = CommitmentSpec(htlcs = Set.empty, feeratePerKw = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi)
val commitFee = commitTxFee(localDustLimit, spec, DefaultCommitmentFormat)
val commitFee = commitTxTotalCost(localDustLimit, spec, DefaultCommitmentFormat)
val belowDust = (localDustLimit * 0.9).toMilliSatoshi
val belowDustWithFee = (localDustLimit + commitFee * 0.9).toMilliSatoshi

Expand Down Expand Up @@ -431,24 +431,24 @@ class TransactionsSpec extends AnyFunSuite with Logging {

test("generate valid commitment with some outputs that don't materialize (anchor outputs)") {
val spec = CommitmentSpec(htlcs = Set.empty, feeratePerKw = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi)
val commitFee = commitTxFee(localDustLimit, spec, AnchorOutputsCommitmentFormat)
val commitFeeAndAnchorCost = commitTxTotalCost(localDustLimit, spec, AnchorOutputsCommitmentFormat)
val belowDust = (localDustLimit * 0.9).toMilliSatoshi
val belowDustWithFeeAndAnchors = (localDustLimit + commitFee * 0.9).toMilliSatoshi
val belowDustWithFeeAndAnchors = (localDustLimit + commitFeeAndAnchorCost * 0.9).toMilliSatoshi

{
val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, AnchorOutputsCommitmentFormat)
assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToRemote, CommitmentOutput.ToLocalAnchor, CommitmentOutput.ToRemoteAnchor))
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount)
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount)
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFee)
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFeeAndAnchorCost)
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote)
}
{
val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust)
val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, AnchorOutputsCommitmentFormat)
assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor))
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount)
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFee)
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFeeAndAnchorCost)
}
{
val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFeeAndAnchors)
Expand All @@ -469,7 +469,7 @@ class TransactionsSpec extends AnyFunSuite with Logging {
val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, AnchorOutputsCommitmentFormat)
assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor))
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount)
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote - commitFee)
assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote - commitFeeAndAnchorCost)
}
{
val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust)
Expand Down Expand Up @@ -861,7 +861,7 @@ class TransactionsSpec extends AnyFunSuite with Logging {
assert(tests.size === 30, "there were 15 tests at b201efe0546120c14bf154ce5f4e18da7243fe7a") // simple non-reg to make sure we are not missing tests
tests.foreach(test => {
logger.info(s"running BOLT 3 test: '${test.name}'")
val fee = commitTxFee(dustLimit, test.spec, DefaultCommitmentFormat)
val fee = commitTxTotalCost(dustLimit, test.spec, DefaultCommitmentFormat)
assert(fee === test.expectedFee)
})
}
Expand Down

0 comments on commit f202587

Please sign in to comment.