diff --git a/loopout.go b/loopout.go index 8035b9d..9a0ab0f 100644 --- a/loopout.go +++ b/loopout.go @@ -514,7 +514,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error { } // Try to spend htlc and continue (rbf) until a spend has confirmed. - spendTx, err := s.waitForHtlcSpendConfirmedV2( + spend, err := s.waitForHtlcSpendConfirmedV2( globalCtx, *htlcOutpoint, htlcValue, ) 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 // its spend, so we can exit. - if spendTx == nil { + if spend == 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 // may be swept by a different (fee) sweep tx from a previous run. htlcInput, err := swap.GetTxInputByOutpoint( - spendTx, htlcOutpoint, + spend.Tx, htlcOutpoint, ) if err != nil { return err @@ -540,9 +540,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error { sweepSuccessful := s.htlc.IsSuccessWitness(htlcInput.Witness) if sweepSuccessful { s.cost.Server -= htlcValue - - s.cost.Onchain = htlcValue - - btcutil.Amount(spendTx.TxOut[0].Value) + s.cost.Onchain = spend.OnChainFeePortion s.state = loopdb.StateSuccess } else { @@ -1005,9 +1003,9 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) ( // sweep or a server revocation tx. func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context, 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) quitChan := make(chan bool, 1) @@ -1054,10 +1052,10 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context, for { select { // Htlc spend, break loop. - case spendTx := <-spendChan: - s.log.Infof("Htlc spend by tx: %v", spendTx.TxHash()) + case spend := <-spendChan: + s.log.Infof("Htlc spend by tx: %v", spend.Tx.TxHash()) - return spendTx, nil + return spend, nil // Spend notification error. case err := <-spendErrChan: diff --git a/sweepbatcher/sweep_batch.go b/sweepbatcher/sweep_batch.go index 971b83d..0960f31 100644 --- a/sweepbatcher/sweep_batch.go +++ b/sweepbatcher/sweep_batch.go @@ -1136,6 +1136,33 @@ func (b *batch) monitorConfirmations(ctx context.Context) error { 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. func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error { 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 // them back to the batcher. This will ensure that the sweeps will enter // a new batch instead of remaining dangling. + var totalSweptAmt btcutil.Amount for _, sweep := range b.sweeps { found := false for _, txIn := range spendTx.TxIn { if txIn.PreviousOutPoint == sweep.outpoint { found = true + totalSweptAmt += sweep.value 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 { + sweep := sweep // Save the sweep as completed. err := b.persistSweep(ctx, sweep, true) if err != nil { @@ -1192,9 +1227,17 @@ func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error { continue } + spendDetail := SpendDetail{ + Tx: spendTx, + OnChainFeePortion: getFeePortionPaidBySweep( + spendTx, feePortionPaidPerSweep, + roundingDifference, &sweep, + ), + } + // Dispatch the sweep notifier, we don't care about the outcome // 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 @@ -1318,10 +1361,12 @@ func (b *batch) insertAndAcquireID(ctx context.Context) (int32, error) { } // 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 { // 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. case <-s.notifier.QuitChan: diff --git a/sweepbatcher/sweep_batcher.go b/sweepbatcher/sweep_batcher.go index d9062fd..7065a47 100644 --- a/sweepbatcher/sweep_batcher.go +++ b/sweepbatcher/sweep_batcher.go @@ -110,11 +110,22 @@ type SweepRequest struct { 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 // that the sweep was successful. type SpendNotifier struct { // 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 chan error @@ -521,6 +532,18 @@ func (b *Batcher) monitorSpendAndNotify(ctx context.Context, sweep *sweep, spendCtx, cancel := context.WithCancel(ctx) 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( spendCtx, &sweep.outpoint, sweep.htlc.PkScript, sweep.initiationHeight, @@ -538,8 +561,28 @@ func (b *Batcher) monitorSpendAndNotify(ctx context.Context, sweep *sweep, for { select { 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 { - case notifier.SpendChan <- spend.SpendingTx: + case notifier.SpendChan <- spendDetail: case <-ctx.Done(): } diff --git a/sweepbatcher/sweep_batcher_test.go b/sweepbatcher/sweep_batcher_test.go index 2d233b9..1671007 100644 --- a/sweepbatcher/sweep_batcher_test.go +++ b/sweepbatcher/sweep_batcher_test.go @@ -38,7 +38,7 @@ func testMuSig2SignSweep(ctx context.Context, } var dummyNotifier = SpendNotifier{ - SpendChan: make(chan *wire.MsgTx, ntfnBufferSize), + SpendChan: make(chan *SpendDetail, ntfnBufferSize), SpendErrChan: make(chan error, ntfnBufferSize), QuitChan: make(chan bool, ntfnBufferSize), }