Skip to content

Commit

Permalink
Merge pull request #838 from starius/loopout-retarget-feerate-ever-block
Browse files Browse the repository at this point in the history
loopout: re-target sweep's feerate every block
  • Loading branch information
starius authored Nov 11, 2024
2 parents 9c347b4 + 2f22f96 commit 0fe952a
Show file tree
Hide file tree
Showing 10 changed files with 600 additions and 40 deletions.
50 changes: 49 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,14 @@ var (
// probeTimeout is the maximum time until a probe is allowed to take.
probeTimeout = 3 * time.Minute

// repushDelay is the delay of (re)adding a sweep to sweepbatcher after
// a block is mined.
repushDelay = 1 * time.Second

// additionalDelay is the delay added on top of repushDelay inside the
// sweepbatcher to publish a sweep transaction.
additionalDelay = 1 * time.Second

// MinerFeeEstimationFailed is a magic number that is returned in a
// quote call as the miner fee if the fee estimation in lnd's wallet
// failed because of insufficient funds.
Expand Down Expand Up @@ -185,13 +191,52 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
"NewSweepFetcherFromSwapStore failed: %w", err)
}

// There is circular dependency between executor and sweepbatcher, as
// executor stores sweepbatcher and sweepbatcher depends on
// executor.height() though loopOutSweepFeerateProvider.
var executor *executor

// getHeight returns current height, according to executor.
getHeight := func() int32 {
if executor == nil {
// This must not happen, because executor is set in this
// function, before sweepbatcher.Run is called.
log.Errorf("getHeight called when executor is nil")

return 0
}

return executor.height()
}

loopOutSweepFeerateProvider := newLoopOutSweepFeerateProvider(
sweeper, loopDB, cfg.Lnd.ChainParams, getHeight,
)

batcher := sweepbatcher.NewBatcher(
cfg.Lnd.WalletKit, cfg.Lnd.ChainNotifier, cfg.Lnd.Signer,
swapServerClient.MultiMuSig2SignSweep, verifySchnorrSig,
cfg.Lnd.ChainParams, sweeperDb, sweepStore,

// Disable 100 sats/kw fee bump every block and retarget feerate
// every block according to the current mempool condition.
sweepbatcher.WithCustomFeeRate(
loopOutSweepFeerateProvider.GetMinFeeRate,
),

// Upon new block arrival, republishing is triggered in both
// loopout.go code (waitForHtlcSpendConfirmedV2/ <-timerChan)
// and in sweepbatcher code (batch.Run/case <-timerChan). The
// former updates the fee rate which is used by the later by
// calling AddSweep. Make sure they are ordered, add additional
// delay time to sweepbatcher's handling. The delay used in
// loopout.go is repushDelay.
sweepbatcher.WithPublishDelay(
repushDelay+additionalDelay,
),
)

executor := newExecutor(&executorConfig{
executor = newExecutor(&executorConfig{
lnd: cfg.Lnd,
store: loopDB,
sweeper: sweeper,
Expand Down Expand Up @@ -570,8 +615,11 @@ func (s *Client) getLoopOutSweepFee(ctx context.Context, confTarget int32) (
htlc = swap.QuoteHtlcP2WSH
}

label := "loopout-quote"

return s.sweeper.GetSweepFee(
ctx, htlc.AddSuccessToEstimator, p2wshAddress, confTarget,
label,
)
}

Expand Down
4 changes: 2 additions & 2 deletions liquidity/liquidity.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ const (
// time we reach timeout. We set this to a high estimate so that we can
// account for worst-case fees, (1250 * 4 / 1000) = 50 sat/byte.
defaultLoopInSweepFee = chainfee.SatPerKWeight(1250)
)

var (
// defaultHtlcConfTarget is the default confirmation target we use for
// loop in swap htlcs, set to the same default at the client.
defaultHtlcConfTarget = loop.DefaultHtlcConfTarget
)

var (
// defaultBudget is the default autoloop budget we set. This budget will
// only be used for automatically dispatched swaps if autoloop is
// explicitly enabled, so we are happy to set a non-zero value here. The
Expand Down
4 changes: 4 additions & 0 deletions loopd/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/notifications"
"github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/build"
Expand Down Expand Up @@ -52,6 +53,9 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) {
lnd.AddSubLogger(
root, notifications.Subsystem, intercept, notifications.UseLogger,
)
lnd.AddSubLogger(
root, sweep.Subsystem, intercept, sweep.UseLogger,
)
}

// genSubLogger creates a logger for a subsystem. We provide an instance of
Expand Down
4 changes: 3 additions & 1 deletion loopin.go
Original file line number Diff line number Diff line change
Expand Up @@ -1077,10 +1077,12 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
}
}

label := fmt.Sprintf("loopin-timeout-%x", s.hash[:6])

// Calculate sweep tx fee.
fee, err := s.sweeper.GetSweepFee(
ctx, s.htlc.AddTimeoutToEstimator, s.timeoutAddr,
TimeoutTxConfTarget,
TimeoutTxConfTarget, label,
)
if err != nil {
return 0, err
Expand Down
4 changes: 3 additions & 1 deletion loopin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,13 @@ func handleHtlcExpiry(t *testing.T, ctx *loopInTestContext, inSwap *loopInSwap,
// Expect timeout tx to be published.
timeoutTx := <-ctx.lnd.TxPublishChannel

label := fmt.Sprintf("loopin-timeout-%x", inSwap.hash[:6])

// We can just get our sweep fee as we would in the swap code because
// our estimate is static.
fee, err := inSwap.sweeper.GetSweepFee(
context.Background(), inSwap.htlc.AddTimeoutToEstimator,
inSwap.timeoutAddr, TimeoutTxConfTarget,
inSwap.timeoutAddr, TimeoutTxConfTarget, label,
)
require.NoError(t, err)
cost.Onchain += fee
Expand Down
52 changes: 23 additions & 29 deletions loopout.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,33 @@ const (
// We'll try to sweep with MuSig2 at most 10 times. If that fails we'll
// fail back to using standard scriptspend sweep.
maxMusigSweepRetries = 10
)

var (
// MinLoopOutPreimageRevealDelta configures the minimum number of
// remaining blocks before htlc expiry required to reveal preimage.
MinLoopOutPreimageRevealDelta int32 = 20
MinLoopOutPreimageRevealDelta = 20

// DefaultSweepConfTarget is the default confirmation target we'll use
// when sweeping on-chain HTLCs.
DefaultSweepConfTarget int32 = 9
DefaultSweepConfTarget = 9

// DefaultHtlcConfTarget is the default confirmation target we'll use
// for on-chain htlcs published by the swap client for Loop In.
DefaultHtlcConfTarget int32 = 6
DefaultHtlcConfTarget = 6

// DefaultSweepConfTargetDelta is the delta of blocks from a Loop Out
// swap's expiration height at which we begin to use the default sweep
// confirmation target.
//
// TODO(wilmer): tune?
DefaultSweepConfTargetDelta = DefaultSweepConfTarget * 2
// swap's expiration height at which we begin to cap the sweep
// confirmation target with urgentSweepConfTarget and multiply feerate
// by factor urgentSweepConfTargetFactor.
DefaultSweepConfTargetDelta = 10

// urgentSweepConfTarget is the confirmation target we'll use when the
// loop-out swap is about to expire (<= DefaultSweepConfTargetDelta
// blocks to expire).
urgentSweepConfTarget = 3

// urgentSweepConfTargetFactor is the factor we apply to feerate of
// loop-out sweep if it is about to expire.
urgentSweepConfTargetFactor = 1.1
)

// loopOutSwap contains all the in-memory state related to a pending loop out
Expand Down Expand Up @@ -1169,12 +1175,12 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context,
timerChan = s.timerFactory(repushDelay)

case <-timerChan:
// sweepConfTarget will return false if the preimage is
// canSweep will return false if the preimage is
// not revealed yet but the conf target is closer than
// 20 blocks. In this case to be sure we won't attempt
// to sweep at all and we won't reveal the preimage
// either.
_, canSweep := s.sweepConfTarget()
canSweep := s.canSweep()
if !canSweep {
s.log.Infof("Aborting swap, timed " +
"out on-chain")
Expand Down Expand Up @@ -1375,9 +1381,9 @@ func validateLoopOutContract(lnd *lndclient.LndServices, request *OutRequest,
return nil
}

// sweepConfTarget returns the confirmation target for the htlc sweep or false
// if we're too late.
func (s *loopOutSwap) sweepConfTarget() (int32, bool) {
// canSweep will return false if the preimage is not revealed yet but the conf
// target is closer than 20 blocks (i.e. it is too late to reveal the preimage).
func (s *loopOutSwap) canSweep() bool {
remainingBlocks := s.CltvExpiry - s.height
blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta
preimageRevealed := s.state == loopdb.StatePreimageRevealed
Expand All @@ -1393,20 +1399,8 @@ func (s *loopOutSwap) sweepConfTarget() (int32, bool) {
s.height)

s.state = loopdb.StateFailTimeout
return 0, false
}

// Calculate the transaction fee based on the confirmation target
// required to sweep the HTLC before the timeout. We'll use the
// confirmation target provided by the client unless we've come too
// close to the expiration height, in which case we'll use the default
// if it is better than what the client provided.
confTarget := s.SweepConfTarget
if remainingBlocks <= DefaultSweepConfTargetDelta &&
confTarget > DefaultSweepConfTarget {

confTarget = DefaultSweepConfTarget
return false
}

return confTarget, true
return true
}
Loading

0 comments on commit 0fe952a

Please sign in to comment.