mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-04-03 09:28:34 +02:00
contractcourt: specify deadline and budget for htlc timeout
This commit is contained in:
parent
d1ad07fa21
commit
cab180a52e
@ -3,6 +3,7 @@ package contractcourt
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -14,6 +15,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/labels"
|
||||
@ -384,6 +386,11 @@ func newActiveChannelArbitrator(channel *channeldb.OpenChannel,
|
||||
chanStateDB := c.chanSource.ChannelStateDB()
|
||||
return chanStateDB.FetchHistoricalChannel(&chanPoint)
|
||||
},
|
||||
FindOutgoingHTLCDeadline: func(
|
||||
rHash chainhash.Hash) fn.Option[int32] {
|
||||
|
||||
return c.FindOutgoingHTLCDeadline(chanPoint, rHash)
|
||||
},
|
||||
}
|
||||
|
||||
// The final component needed is an arbitrator log that the arbitrator
|
||||
@ -578,6 +585,7 @@ func (c *ChainArbitrator) Start() error {
|
||||
// corresponding more restricted resolver, as we don't have to watch
|
||||
// the chain any longer, only resolve the contracts on the confirmed
|
||||
// commitment.
|
||||
//nolint:lll
|
||||
for _, closeChanInfo := range closingChannels {
|
||||
// We can leave off the CloseContract and ForceCloseChan
|
||||
// methods as the channel is already closed at this point.
|
||||
@ -601,6 +609,11 @@ func (c *ChainArbitrator) Start() error {
|
||||
chanStateDB := c.chanSource.ChannelStateDB()
|
||||
return chanStateDB.FetchHistoricalChannel(&chanPoint)
|
||||
},
|
||||
FindOutgoingHTLCDeadline: func(
|
||||
rHash chainhash.Hash) fn.Option[int32] {
|
||||
|
||||
return c.FindOutgoingHTLCDeadline(chanPoint, rHash)
|
||||
},
|
||||
}
|
||||
chanLog, err := newBoltArbitratorLog(
|
||||
c.chanSource.Backend, arbCfg, c.cfg.ChainHash, chanPoint,
|
||||
@ -1204,5 +1217,63 @@ func (c *ChainArbitrator) SubscribeChannelEvents(
|
||||
return watcher.SubscribeChannelEvents(), nil
|
||||
}
|
||||
|
||||
// FindOutgoingHTLCDeadline returns the deadline in absolute block height for
|
||||
// the specified outgoing HTLC. For an outgoing HTLC, its deadline is defined
|
||||
// by the timeout height of its corresponding incoming HTLC - this is the
|
||||
// expiry height the that remote peer can spend his/her outgoing HTLC via the
|
||||
// timeout path.
|
||||
func (c *ChainArbitrator) FindOutgoingHTLCDeadline(chanPoint wire.OutPoint,
|
||||
rHash chainhash.Hash) fn.Option[int32] {
|
||||
|
||||
// minRefundTimeout tracks the minimal refund timeout found using the
|
||||
// rHash. It's possible that we find multiple HTLCs living in different
|
||||
// channels sharing the same rHash if an MPP is routed by us. In this
|
||||
// case, we'll use the smallest refund timeout as the deadline.
|
||||
//
|
||||
// TODO(yy): can instead query the circuit map to find the exact HTLC.
|
||||
minRefundTimeout := uint32(math.MaxInt32)
|
||||
|
||||
// Iterate over all active channels to find the HTLC with the matching
|
||||
// rHash.
|
||||
for cp, channelArb := range c.activeChannels {
|
||||
// Skip the targeted channel as the incoming HTLC is not here.
|
||||
if cp == chanPoint {
|
||||
continue
|
||||
}
|
||||
|
||||
// Iterate all the known HTLCs to find the targeted incoming
|
||||
// HTLC.
|
||||
for _, htlcs := range channelArb.activeHTLCs {
|
||||
for _, htlc := range htlcs.incomingHTLCs {
|
||||
if htlc.RHash != rHash {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("ChannelArbitrator(%v): found "+
|
||||
"incoming HTLC in channel=%v using "+
|
||||
"rHash=%v, refundTimeout=%v", chanPoint,
|
||||
cp, rHash, htlc.RefundTimeout)
|
||||
|
||||
// Update the value if it's smaller.
|
||||
if minRefundTimeout > htlc.RefundTimeout {
|
||||
minRefundTimeout = htlc.RefundTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the refund timeout value if found.
|
||||
if minRefundTimeout != math.MaxInt32 {
|
||||
return fn.Some(int32(minRefundTimeout))
|
||||
}
|
||||
|
||||
// If there's no incoming HTLC found, it means we are the first hop. In
|
||||
// this case, we can relax the deadline.
|
||||
log.Infof("ChannelArbitrator(%v): incoming HTLC not found for "+
|
||||
"rHash=%v, using default deadline instead", chanPoint, rHash)
|
||||
|
||||
return fn.None[int32]()
|
||||
}
|
||||
|
||||
// TODO(roasbeef): arbitration reports
|
||||
// * types: contested, waiting for success conf, etc
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
@ -170,6 +171,13 @@ type ChannelArbitratorConfig struct {
|
||||
// additional information required for proper contract resolution.
|
||||
FetchHistoricalChannel func() (*channeldb.OpenChannel, error)
|
||||
|
||||
// FindOutgoingHTLCDeadline returns the deadline in absolute block
|
||||
// height for the specified outgoing HTLC. For an outgoing HTLC, its
|
||||
// deadline is defined by the timeout height of its corresponding
|
||||
// incoming HTLC - this is the expiry height the that remote peer can
|
||||
// spend his/her outgoing HTLC via the timeout path.
|
||||
FindOutgoingHTLCDeadline func(rHash chainhash.Hash) fn.Option[int32]
|
||||
|
||||
ChainArbitratorConfig
|
||||
}
|
||||
|
||||
@ -757,6 +765,14 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet,
|
||||
}
|
||||
|
||||
htlcResolver.Supplement(*htlc)
|
||||
|
||||
// If this is an outgoing HTLC, we will also need to supplement
|
||||
// the resolver with the expiry block height of its
|
||||
// corresponding incoming HTLC.
|
||||
if !htlc.Incoming {
|
||||
deadline := c.cfg.FindOutgoingHTLCDeadline(htlc.RHash)
|
||||
htlcResolver.SupplementDeadline(deadline)
|
||||
}
|
||||
}
|
||||
|
||||
// The anchor resolver is stateless and can always be re-instantiated.
|
||||
@ -1733,8 +1749,15 @@ func (c *ChannelArbitrator) checkCommitChainActions(height uint32,
|
||||
for _, htlc := range htlcs.outgoingHTLCs {
|
||||
// We'll need to go on-chain for an outgoing HTLC if it was
|
||||
// never resolved downstream, and it's "close" to timing out.
|
||||
toChain := c.shouldGoOnChain(htlc, c.cfg.OutgoingBroadcastDelta,
|
||||
height,
|
||||
//
|
||||
// TODO(yy): If there's no corresponding incoming HTLC, it
|
||||
// means we are the first hop, hence the payer. This is a
|
||||
// tricky case - unlike a forwarding hop, we don't have an
|
||||
// incoming HTLC that will time out, which means as long as we
|
||||
// can learn the preimage, we can settle the invoice (before it
|
||||
// expires?).
|
||||
toChain := c.shouldGoOnChain(
|
||||
htlc, c.cfg.OutgoingBroadcastDelta, height,
|
||||
)
|
||||
|
||||
if toChain {
|
||||
@ -2349,6 +2372,16 @@ func (c *ChannelArbitrator) prepContractResolutions(
|
||||
if chanState != nil {
|
||||
resolver.SupplementState(chanState)
|
||||
}
|
||||
|
||||
// For outgoing HTLCs, we will also need to
|
||||
// supplement the resolver with the expiry
|
||||
// block height of its corresponding incoming
|
||||
// HTLC.
|
||||
deadline := c.cfg.FindOutgoingHTLCDeadline(
|
||||
htlc.RHash,
|
||||
)
|
||||
resolver.SupplementDeadline(deadline)
|
||||
|
||||
htlcResolvers = append(htlcResolvers, resolver)
|
||||
}
|
||||
|
||||
@ -2441,6 +2474,16 @@ func (c *ChannelArbitrator) prepContractResolutions(
|
||||
if chanState != nil {
|
||||
resolver.SupplementState(chanState)
|
||||
}
|
||||
|
||||
// For outgoing HTLCs, we will also need to
|
||||
// supplement the resolver with the expiry
|
||||
// block height of its corresponding incoming
|
||||
// HTLC.
|
||||
deadline := c.cfg.FindOutgoingHTLCDeadline(
|
||||
htlc.RHash,
|
||||
)
|
||||
resolver.SupplementDeadline(deadline)
|
||||
|
||||
htlcResolvers = append(htlcResolvers, resolver)
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lntest/mock"
|
||||
@ -425,6 +426,11 @@ func createTestChannelArbitrator(t *testing.T, log ArbitratorLog,
|
||||
FetchHistoricalChannel: func() (*channeldb.OpenChannel, error) {
|
||||
return &channeldb.OpenChannel{}, nil
|
||||
},
|
||||
FindOutgoingHTLCDeadline: func(
|
||||
rHash chainhash.Hash) fn.Option[int32] {
|
||||
|
||||
return fn.None[int32]()
|
||||
},
|
||||
}
|
||||
|
||||
testOpts := &testChanArbOpts{
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -20,10 +21,6 @@ const (
|
||||
// sweepConfTarget is the default number of blocks that we'll use as a
|
||||
// confirmation target when sweeping.
|
||||
sweepConfTarget = 6
|
||||
|
||||
// secondLevelConfTarget is the confirmation target we'll use when
|
||||
// adding fees to our second-level HTLC transactions.
|
||||
secondLevelConfTarget = 6
|
||||
)
|
||||
|
||||
// ContractResolver is an interface which packages a state machine which is
|
||||
@ -75,6 +72,10 @@ type htlcContractResolver interface {
|
||||
// Supplement adds additional information to the resolver that is
|
||||
// required before Resolve() is called.
|
||||
Supplement(htlc channeldb.HTLC)
|
||||
|
||||
// SupplementDeadline gives the deadline height for the HTLC output.
|
||||
// This is only useful for outgoing HTLCs.
|
||||
SupplementDeadline(deadlineHeight fn.Option[int32])
|
||||
}
|
||||
|
||||
// reportingContractResolver is a ContractResolver that also exposes a report on
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
@ -516,6 +517,12 @@ func (h *htlcIncomingContestResolver) Supplement(htlc channeldb.HTLC) {
|
||||
h.htlc = htlc
|
||||
}
|
||||
|
||||
// SupplementDeadline does nothing for an incoming htlc resolver.
|
||||
//
|
||||
// NOTE: Part of the htlcContractResolver interface.
|
||||
func (h *htlcIncomingContestResolver) SupplementDeadline(_ fn.Option[int32]) {
|
||||
}
|
||||
|
||||
// decodePayload (re)decodes the hop payload of a received htlc.
|
||||
func (h *htlcIncomingContestResolver) decodePayload() (*hop.Payload,
|
||||
[]byte, error) {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
)
|
||||
|
||||
@ -196,6 +197,12 @@ func (h *htlcOutgoingContestResolver) Encode(w io.Writer) error {
|
||||
return h.htlcTimeoutResolver.Encode(w)
|
||||
}
|
||||
|
||||
// SupplementDeadline does nothing for an incoming htlc resolver.
|
||||
//
|
||||
// NOTE: Part of the htlcContractResolver interface.
|
||||
func (h *htlcOutgoingContestResolver) SupplementDeadline(_ fn.Option[int32]) {
|
||||
}
|
||||
|
||||
// newOutgoingContestResolverFromReader attempts to decode an encoded ContractResolver
|
||||
// from the passed Reader instance, returning an active ContractResolver
|
||||
// instance.
|
||||
|
@ -737,6 +737,12 @@ func (h *htlcSuccessResolver) HtlcPoint() wire.OutPoint {
|
||||
return h.htlcResolution.HtlcPoint()
|
||||
}
|
||||
|
||||
// SupplementDeadline does nothing for an incoming htlc resolver.
|
||||
//
|
||||
// NOTE: Part of the htlcContractResolver interface.
|
||||
func (h *htlcSuccessResolver) SupplementDeadline(_ fn.Option[int32]) {
|
||||
}
|
||||
|
||||
// A compile time assertion to ensure htlcSuccessResolver meets the
|
||||
// ContractResolver interface.
|
||||
var _ htlcContractResolver = (*htlcSuccessResolver)(nil)
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnutils"
|
||||
@ -61,6 +62,11 @@ type htlcTimeoutResolver struct {
|
||||
contractResolverKit
|
||||
|
||||
htlcLeaseResolver
|
||||
|
||||
// incomingHTLCExpiryHeight is the absolute block height at which the
|
||||
// incoming HTLC will expire. This is used as the deadline height as
|
||||
// the outgoing HTLC must be swept before its incoming HTLC expires.
|
||||
incomingHTLCExpiryHeight fn.Option[int32]
|
||||
}
|
||||
|
||||
// newTimeoutResolver instantiates a new timeout htlc resolver.
|
||||
@ -483,13 +489,45 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx() error {
|
||||
h.broadcastHeight,
|
||||
))
|
||||
}
|
||||
|
||||
// Calculate the budget.
|
||||
//
|
||||
// TODO(yy): the budget is twice the output's value, which is needed as
|
||||
// we don't force sweep the output now. To prevent cascading force
|
||||
// closes, we use all its output value plus a wallet input as the
|
||||
// budget. This is a temporary solution until we can optionally cancel
|
||||
// the incoming HTLC, more details in,
|
||||
// - https://github.com/lightningnetwork/lnd/issues/7969
|
||||
budget := calculateBudget(
|
||||
btcutil.Amount(inp.SignDesc().Output.Value), 2, 0,
|
||||
)
|
||||
|
||||
// For an outgoing HTLC, it must be swept before the RefundTimeout of
|
||||
// its incoming HTLC is reached.
|
||||
//
|
||||
// TODO(yy): we may end up mixing inputs with different time locks.
|
||||
// Suppose we have two outgoing HTLCs,
|
||||
// - HTLC1: nLocktime is 800000, CLTV delta is 80.
|
||||
// - HTLC2: nLocktime is 800001, CLTV delta is 79.
|
||||
// This means they would both have an incoming HTLC that expires at
|
||||
// 800080, hence they share the same deadline but different locktimes.
|
||||
// However, with current design, when we are at block 800000, HTLC1 is
|
||||
// offered to the sweeper. When block 800001 is reached, HTLC1's
|
||||
// sweeping process is already started, while HTLC2 is being offered to
|
||||
// the sweeper, so they won't be mixed. This can become an issue tho,
|
||||
// if we decide to sweep per X blocks. Or the contractcourt sees the
|
||||
// block first while the sweeper is only aware of the last block. To
|
||||
// properly fix it, we need `blockbeat` to make sure subsystems are in
|
||||
// sync.
|
||||
log.Infof("%T(%x): offering second-level HTLC timeout tx to sweeper "+
|
||||
"with deadline=%v, budget=%v", h, h.htlc.RHash[:],
|
||||
h.incomingHTLCExpiryHeight, budget)
|
||||
|
||||
_, err := h.Sweeper.SweepInput(
|
||||
inp,
|
||||
sweep.Params{
|
||||
Fee: sweep.FeeEstimateInfo{
|
||||
ConfTarget: secondLevelConfTarget,
|
||||
},
|
||||
Force: true,
|
||||
Budget: budget,
|
||||
DeadlineHeight: h.incomingHTLCExpiryHeight,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@ -699,12 +737,26 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
||||
h.htlcResolution.CsvDelay, h.broadcastHeight,
|
||||
h.htlc.RHash,
|
||||
)
|
||||
// Calculate the budget for this sweep.
|
||||
budget := calculateBudget(
|
||||
btcutil.Amount(inp.SignDesc().Output.Value),
|
||||
h.Budget.NoDeadlineHTLCRatio,
|
||||
h.Budget.NoDeadlineHTLC,
|
||||
)
|
||||
|
||||
log.Infof("%T(%x): offering second-level timeout tx output to "+
|
||||
"sweeper with no deadline and budget=%v", h,
|
||||
h.htlc.RHash[:], budget)
|
||||
|
||||
_, err = h.Sweeper.SweepInput(
|
||||
inp,
|
||||
sweep.Params{
|
||||
Fee: sweep.FeeEstimateInfo{
|
||||
ConfTarget: sweepConfTarget,
|
||||
},
|
||||
Budget: budget,
|
||||
|
||||
// For second level success tx, there's no rush
|
||||
// to get it confirmed, so we use a nil
|
||||
// deadline.
|
||||
DeadlineHeight: fn.None[int32](),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@ -918,6 +970,14 @@ func (h *htlcTimeoutResolver) HtlcPoint() wire.OutPoint {
|
||||
return h.htlcResolution.HtlcPoint()
|
||||
}
|
||||
|
||||
// SupplementDeadline sets the incomingHTLCExpiryHeight for this outgoing htlc
|
||||
// resolver.
|
||||
//
|
||||
// NOTE: Part of the htlcContractResolver interface.
|
||||
func (h *htlcTimeoutResolver) SupplementDeadline(d fn.Option[int32]) {
|
||||
h.incomingHTLCExpiryHeight = d
|
||||
}
|
||||
|
||||
// A compile time assertion to ensure htlcTimeoutResolver meets the
|
||||
// ContractResolver interface.
|
||||
var _ htlcContractResolver = (*htlcTimeoutResolver)(nil)
|
||||
|
Loading…
x
Reference in New Issue
Block a user