loopout+sweepbatcher: calculate the per sweep onchain fees correctly

Previously we'd report the fees per sweep as the total sweep cost of a
batch. With this change the reported cost will be the proportional fee
which should be equal for all sweeps except if there's any rounding
difference in which case that is paid by the sweep belonging to the
first input of the batch tx.
pull/694/head
Andras Banki-Horvath 3 months ago
parent b4ebb19a77
commit e1ddb50dfe
No known key found for this signature in database
GPG Key ID: 80E5375C094198D8

@ -514,7 +514,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
} }
// Try to spend htlc and continue (rbf) until a spend has confirmed. // Try to spend htlc and continue (rbf) until a spend has confirmed.
spendTx, err := s.waitForHtlcSpendConfirmedV2( spend, err := s.waitForHtlcSpendConfirmedV2(
globalCtx, *htlcOutpoint, htlcValue, globalCtx, *htlcOutpoint, htlcValue,
) )
if err != nil { if err != nil {
@ -523,7 +523,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
// If spend details are nil, we resolved the swap without waiting for // If spend details are nil, we resolved the swap without waiting for
// its spend, so we can exit. // its spend, so we can exit.
if spendTx == nil { if spend == nil {
return nil return nil
} }
@ -531,7 +531,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
// don't just try to match with the hash of our sweep tx, because it // don't just try to match with the hash of our sweep tx, because it
// may be swept by a different (fee) sweep tx from a previous run. // may be swept by a different (fee) sweep tx from a previous run.
htlcInput, err := swap.GetTxInputByOutpoint( htlcInput, err := swap.GetTxInputByOutpoint(
spendTx, htlcOutpoint, spend.Tx, htlcOutpoint,
) )
if err != nil { if err != nil {
return err return err
@ -540,9 +540,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
sweepSuccessful := s.htlc.IsSuccessWitness(htlcInput.Witness) sweepSuccessful := s.htlc.IsSuccessWitness(htlcInput.Witness)
if sweepSuccessful { if sweepSuccessful {
s.cost.Server -= htlcValue s.cost.Server -= htlcValue
s.cost.Onchain = spend.OnChainFeePortion
s.cost.Onchain = htlcValue -
btcutil.Amount(spendTx.TxOut[0].Value)
s.state = loopdb.StateSuccess s.state = loopdb.StateSuccess
} else { } else {
@ -1005,9 +1003,9 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) (
// sweep or a server revocation tx. // sweep or a server revocation tx.
func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context, func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context,
htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) ( htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) (
*wire.MsgTx, error) { *sweepbatcher.SpendDetail, error) {
spendChan := make(chan *wire.MsgTx) spendChan := make(chan *sweepbatcher.SpendDetail)
spendErrChan := make(chan error, 1) spendErrChan := make(chan error, 1)
quitChan := make(chan bool, 1) quitChan := make(chan bool, 1)
@ -1054,10 +1052,10 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context,
for { for {
select { select {
// Htlc spend, break loop. // Htlc spend, break loop.
case spendTx := <-spendChan: case spend := <-spendChan:
s.log.Infof("Htlc spend by tx: %v", spendTx.TxHash()) s.log.Infof("Htlc spend by tx: %v", spend.Tx.TxHash())
return spendTx, nil return spend, nil
// Spend notification error. // Spend notification error.
case err := <-spendErrChan: case err := <-spendErrChan:

@ -1136,6 +1136,33 @@ func (b *batch) monitorConfirmations(ctx context.Context) error {
return nil return nil
} }
// getFeePortionForSweep calculates the fee portion that each sweep should pay
// for the batch transaction. The fee is split evenly among the sweeps, If the
// fee cannot be split evenly, the remainder is paid by the first sweep.
func getFeePortionForSweep(spendTx *wire.MsgTx, numSweeps int,
totalSweptAmt btcutil.Amount) (btcutil.Amount, btcutil.Amount) {
totalFee := spendTx.TxOut[0].Value - int64(totalSweptAmt)
feePortionPerSweep := (int64(totalSweptAmt) -
spendTx.TxOut[0].Value) / int64(numSweeps)
roundingDiff := totalFee - (int64(numSweeps) * feePortionPerSweep)
return btcutil.Amount(feePortionPerSweep), btcutil.Amount(roundingDiff)
}
// getFeePortionPaidBySweep returns the fee portion that the sweep should pay
// for the batch transaction. If the sweep is the first sweep in the batch, it
// pays the rounding difference.
func getFeePortionPaidBySweep(spendTx *wire.MsgTx, feePortionPerSweep,
roundingDiff btcutil.Amount, sweep *sweep) btcutil.Amount {
if bytes.Equal(spendTx.TxIn[0].SignatureScript, sweep.htlc.SigScript) {
return feePortionPerSweep + roundingDiff
}
return feePortionPerSweep
}
// handleSpend handles a spend notification. // handleSpend handles a spend notification.
func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error { func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error {
var ( var (
@ -1151,12 +1178,14 @@ func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error {
// sweeps that did not make it to the confirmed transaction and feed // sweeps that did not make it to the confirmed transaction and feed
// them back to the batcher. This will ensure that the sweeps will enter // them back to the batcher. This will ensure that the sweeps will enter
// a new batch instead of remaining dangling. // a new batch instead of remaining dangling.
var totalSweptAmt btcutil.Amount
for _, sweep := range b.sweeps { for _, sweep := range b.sweeps {
found := false found := false
for _, txIn := range spendTx.TxIn { for _, txIn := range spendTx.TxIn {
if txIn.PreviousOutPoint == sweep.outpoint { if txIn.PreviousOutPoint == sweep.outpoint {
found = true found = true
totalSweptAmt += sweep.value
notifyList = append(notifyList, sweep) notifyList = append(notifyList, sweep)
} }
} }
@ -1176,7 +1205,13 @@ func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error {
} }
} }
// Calculate the fee portion that each sweep should pay for the batch.
feePortionPaidPerSweep, roundingDifference := getFeePortionForSweep(
spendTx, len(notifyList), totalSweptAmt,
)
for _, sweep := range notifyList { for _, sweep := range notifyList {
sweep := sweep
// Save the sweep as completed. // Save the sweep as completed.
err := b.persistSweep(ctx, sweep, true) err := b.persistSweep(ctx, sweep, true)
if err != nil { if err != nil {
@ -1192,9 +1227,17 @@ func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error {
continue continue
} }
spendDetail := SpendDetail{
Tx: spendTx,
OnChainFeePortion: getFeePortionPaidBySweep(
spendTx, feePortionPaidPerSweep,
roundingDifference, &sweep,
),
}
// Dispatch the sweep notifier, we don't care about the outcome // Dispatch the sweep notifier, we don't care about the outcome
// of this action so we don't wait for it. // of this action so we don't wait for it.
go notifySweepSpend(ctx, sweep, spendTx) go sweep.notifySweepSpend(ctx, &spendDetail)
} }
// Proceed with purging the sweeps. This will feed the sweeps that // Proceed with purging the sweeps. This will feed the sweeps that
@ -1318,10 +1361,12 @@ func (b *batch) insertAndAcquireID(ctx context.Context) (int32, error) {
} }
// notifySweepSpend writes the spendTx to the sweep's notifier channel. // notifySweepSpend writes the spendTx to the sweep's notifier channel.
func notifySweepSpend(ctx context.Context, s sweep, spendTx *wire.MsgTx) { func (s *sweep) notifySweepSpend(ctx context.Context,
spendDetail *SpendDetail) {
select { select {
// Try to write the update to the notification channel. // Try to write the update to the notification channel.
case s.notifier.SpendChan <- spendTx: case s.notifier.SpendChan <- spendDetail:
// If a quit signal was provided by the swap, continue. // If a quit signal was provided by the swap, continue.
case <-s.notifier.QuitChan: case <-s.notifier.QuitChan:

@ -110,11 +110,22 @@ type SweepRequest struct {
Notifier *SpendNotifier Notifier *SpendNotifier
} }
type SpendDetail struct {
// Tx is the transaction that spent the outpoint.
Tx *wire.MsgTx
// OnChainFeePortion is the fee portion that was paid to get this sweep
// confirmed on chain. This is the difference between the value of the
// outpoint and the value of all sweeps that were included in the batch
// divided by the number of sweeps.
OnChainFeePortion btcutil.Amount
}
// SpendNotifier is a notifier that is used to notify the requester of a sweep // SpendNotifier is a notifier that is used to notify the requester of a sweep
// that the sweep was successful. // that the sweep was successful.
type SpendNotifier struct { type SpendNotifier struct {
// SpendChan is a channel where the spend details are received. // SpendChan is a channel where the spend details are received.
SpendChan chan *wire.MsgTx SpendChan chan *SpendDetail
// SpendErrChan is a channel where spend errors are received. // SpendErrChan is a channel where spend errors are received.
SpendErrChan chan error SpendErrChan chan error
@ -521,6 +532,18 @@ func (b *Batcher) monitorSpendAndNotify(ctx context.Context, sweep *sweep,
spendCtx, cancel := context.WithCancel(ctx) spendCtx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
// First get the batch that completed the sweep.
parentBatch, err := b.store.GetParentBatch(ctx, sweep.swapHash)
if err != nil {
return err
}
// Then we get the total amount that was swept by the batch.
totalSwept, err := b.store.TotalSweptAmount(ctx, parentBatch.ID)
if err != nil {
return err
}
spendChan, spendErr, err := b.chainNotifier.RegisterSpendNtfn( spendChan, spendErr, err := b.chainNotifier.RegisterSpendNtfn(
spendCtx, &sweep.outpoint, sweep.htlc.PkScript, spendCtx, &sweep.outpoint, sweep.htlc.PkScript,
sweep.initiationHeight, sweep.initiationHeight,
@ -538,8 +561,28 @@ func (b *Batcher) monitorSpendAndNotify(ctx context.Context, sweep *sweep,
for { for {
select { select {
case spend := <-spendChan: case spend := <-spendChan:
spendTx := spend.SpendingTx
// Calculate the fee portion that each sweep
// should pay for the batch.
feePortionPerSweep, roundingDifference :=
getFeePortionForSweep(
spendTx, len(spendTx.TxIn),
totalSwept,
)
// Notify the requester of the spend
// with the spend details, including the fee
// portion for this particular sweep.
spendDetail := &SpendDetail{
Tx: spendTx,
OnChainFeePortion: getFeePortionPaidBySweep( // nolint:lll
spendTx, feePortionPerSweep,
roundingDifference, sweep,
),
}
select { select {
case notifier.SpendChan <- spend.SpendingTx: case notifier.SpendChan <- spendDetail:
case <-ctx.Done(): case <-ctx.Done():
} }

@ -38,7 +38,7 @@ func testMuSig2SignSweep(ctx context.Context,
} }
var dummyNotifier = SpendNotifier{ var dummyNotifier = SpendNotifier{
SpendChan: make(chan *wire.MsgTx, ntfnBufferSize), SpendChan: make(chan *SpendDetail, ntfnBufferSize),
SpendErrChan: make(chan error, ntfnBufferSize), SpendErrChan: make(chan error, ntfnBufferSize),
QuitChan: make(chan bool, ntfnBufferSize), QuitChan: make(chan bool, ntfnBufferSize),
} }

Loading…
Cancel
Save