mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-22 14:22:37 +01:00
Merge pull request #8751 from yyforyongyu/fix-sweeper-18
contractcourt+sweep: fix fee function and deadline issue
This commit is contained in:
commit
e1c5fe2f9e
16 changed files with 973 additions and 387 deletions
|
@ -772,8 +772,8 @@ func (n *TxNotifier) CancelConf(confRequest ConfRequest, confID uint64) {
|
|||
return
|
||||
}
|
||||
|
||||
Log.Infof("Canceling confirmation notification: conf_id=%d, %v", confID,
|
||||
confRequest)
|
||||
Log.Debugf("Canceling confirmation notification: conf_id=%d, %v",
|
||||
confID, confRequest)
|
||||
|
||||
// We'll close all the notification channels to let the client know
|
||||
// their cancel request has been fulfilled.
|
||||
|
@ -925,7 +925,7 @@ func (n *TxNotifier) dispatchConfDetails(
|
|||
// we'll dispatch a confirmation notification to the caller.
|
||||
confHeight := details.BlockHeight + ntfn.NumConfirmations - 1
|
||||
if confHeight <= n.currentHeight {
|
||||
Log.Infof("Dispatching %v confirmation notification for %v",
|
||||
Log.Debugf("Dispatching %v confirmation notification for %v",
|
||||
ntfn.NumConfirmations, ntfn.ConfRequest)
|
||||
|
||||
// We'll send a 0 value to the Updates channel,
|
||||
|
@ -1058,7 +1058,7 @@ func (n *TxNotifier) RegisterSpend(outpoint *wire.OutPoint, pkScript []byte,
|
|||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
Log.Infof("New spend subscription: spend_id=%d, %v, height_hint=%d",
|
||||
Log.Debugf("New spend subscription: spend_id=%d, %v, height_hint=%d",
|
||||
ntfn.SpendID, ntfn.SpendRequest, startHeight)
|
||||
|
||||
// Keep track of the notification request so that we can properly
|
||||
|
@ -1136,7 +1136,7 @@ func (n *TxNotifier) RegisterSpend(outpoint *wire.OutPoint, pkScript []byte,
|
|||
// notifications don't also attempt a historical dispatch.
|
||||
spendSet.rescanStatus = rescanPending
|
||||
|
||||
Log.Infof("Dispatching historical spend rescan for %v, start=%d, "+
|
||||
Log.Debugf("Dispatching historical spend rescan for %v, start=%d, "+
|
||||
"end=%d", ntfn.SpendRequest, startHeight, n.currentHeight)
|
||||
|
||||
return &SpendRegistration{
|
||||
|
@ -1171,7 +1171,7 @@ func (n *TxNotifier) CancelSpend(spendRequest SpendRequest, spendID uint64) {
|
|||
return
|
||||
}
|
||||
|
||||
Log.Infof("Canceling spend notification: spend_id=%d, %v", spendID,
|
||||
Log.Debugf("Canceling spend notification: spend_id=%d, %v", spendID,
|
||||
spendRequest)
|
||||
|
||||
// We'll close all the notification channels to let the client know
|
||||
|
@ -1364,7 +1364,7 @@ func (n *TxNotifier) dispatchSpendDetails(ntfn *SpendNtfn, details *SpendDetail)
|
|||
return nil
|
||||
}
|
||||
|
||||
Log.Infof("Dispatching confirmed spend notification for %v at "+
|
||||
Log.Debugf("Dispatching confirmed spend notification for %v at "+
|
||||
"current height=%d: %v", ntfn.SpendRequest, n.currentHeight,
|
||||
details)
|
||||
|
||||
|
@ -1743,7 +1743,7 @@ func (n *TxNotifier) NotifyHeight(height uint32) error {
|
|||
for ntfn := range n.ntfnsByConfirmHeight[height] {
|
||||
confSet := n.confNotifications[ntfn.ConfRequest]
|
||||
|
||||
Log.Infof("Dispatching %v confirmation notification for %v",
|
||||
Log.Debugf("Dispatching %v confirmation notification for %v",
|
||||
ntfn.NumConfirmations, ntfn.ConfRequest)
|
||||
|
||||
// The default notification we assigned above includes the
|
||||
|
|
|
@ -287,10 +287,11 @@ func bumpFee(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
resp, err := client.BumpFee(ctxc, &walletrpc.BumpFeeRequest{
|
||||
Outpoint: protoOutPoint,
|
||||
TargetConf: uint32(ctx.Uint64("conf_target")),
|
||||
Immediate: immediate,
|
||||
Budget: ctx.Uint64("budget"),
|
||||
Outpoint: protoOutPoint,
|
||||
TargetConf: uint32(ctx.Uint64("conf_target")),
|
||||
Immediate: immediate,
|
||||
Budget: ctx.Uint64("budget"),
|
||||
SatPerVbyte: ctx.Uint64("sat_per_vbyte"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -487,10 +488,11 @@ func bumpForceCloseFee(ctx *cli.Context) error {
|
|||
|
||||
resp, err := walletClient.BumpFee(
|
||||
ctxc, &walletrpc.BumpFeeRequest{
|
||||
Outpoint: sweep.Outpoint,
|
||||
TargetConf: uint32(ctx.Uint64("conf_target")),
|
||||
Budget: ctx.Uint64("budget"),
|
||||
Immediate: ctx.Bool("immediate"),
|
||||
Outpoint: sweep.Outpoint,
|
||||
TargetConf: uint32(ctx.Uint64("conf_target")),
|
||||
Budget: ctx.Uint64("budget"),
|
||||
Immediate: ctx.Bool("immediate"),
|
||||
SatPerVbyte: ctx.Uint64("sat_per_vbyte"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -1287,6 +1287,10 @@ func (c *ChainArbitrator) FindOutgoingHTLCDeadline(scid lnwire.ShortChannelID,
|
|||
continue
|
||||
}
|
||||
|
||||
// Make sure the channel arbitrator has the latest view of its
|
||||
// active HTLCs.
|
||||
channelArb.updateActiveHTLCs()
|
||||
|
||||
// Iterate all the known HTLCs to find the targeted incoming
|
||||
// HTLC.
|
||||
for _, htlcs := range channelArb.activeHTLCs {
|
||||
|
|
|
@ -1436,9 +1436,13 @@ func (c *ChannelArbitrator) sweepAnchors(anchors *lnwallet.AnchorResolutions,
|
|||
|
||||
// findCommitmentDeadlineAndValue finds the deadline (relative block height)
|
||||
// for a commitment transaction by extracting the minimum CLTV from its HTLCs.
|
||||
// From our PoV, the deadline is defined to be the smaller of,
|
||||
// - the least CLTV from outgoing HTLCs, or,
|
||||
// - the least CLTV from incoming HTLCs if the preimage is available.
|
||||
// From our PoV, the deadline delta is defined to be the smaller of,
|
||||
// - half of the least CLTV from outgoing HTLCs' corresponding incoming
|
||||
// HTLCs, or,
|
||||
// - half of the least CLTV from incoming HTLCs if the preimage is available.
|
||||
//
|
||||
// We use half of the CTLV value to ensure that we have enough time to sweep
|
||||
// the second-level HTLCs.
|
||||
//
|
||||
// It also finds the total value that are time-sensitive, which is the sum of
|
||||
// all the outgoing HTLCs plus incoming HTLCs whose preimages are known. It
|
||||
|
@ -1468,10 +1472,24 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32,
|
|||
}
|
||||
|
||||
value := htlc.Amt.ToSatoshis()
|
||||
totalValue += value
|
||||
|
||||
if htlc.RefundTimeout < deadlineMinHeight {
|
||||
deadlineMinHeight = htlc.RefundTimeout
|
||||
// Find the expiry height for this outgoing HTLC's incoming
|
||||
// HTLC.
|
||||
deadlineOpt := c.cfg.FindOutgoingHTLCDeadline(htlc)
|
||||
|
||||
// The deadline is default to the current deadlineMinHeight,
|
||||
// and it's overwritten when it's not none.
|
||||
deadline := deadlineMinHeight
|
||||
deadlineOpt.WhenSome(func(d int32) {
|
||||
deadline = uint32(d)
|
||||
|
||||
// We only consider the value is under protection when
|
||||
// it's time-sensitive.
|
||||
totalValue += value
|
||||
})
|
||||
|
||||
if deadline < deadlineMinHeight {
|
||||
deadlineMinHeight = deadline
|
||||
|
||||
log.Tracef("ChannelArbitrator(%v): outgoing HTLC has "+
|
||||
"deadline=%v, value=%v", c.cfg.ChanPoint,
|
||||
|
@ -1521,7 +1539,7 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32,
|
|||
// * none of the HTLCs are preimageAvailable.
|
||||
// - when our deadlineMinHeight is no greater than the heightHint,
|
||||
// which means we are behind our schedule.
|
||||
deadline := deadlineMinHeight - heightHint
|
||||
var deadline uint32
|
||||
switch {
|
||||
// When we couldn't find a deadline height from our HTLCs, we will fall
|
||||
// back to the default value as there's no time pressure here.
|
||||
|
@ -1535,6 +1553,11 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32,
|
|||
"deadlineMinHeight=%d, heightHint=%d",
|
||||
c.cfg.ChanPoint, deadlineMinHeight, heightHint)
|
||||
deadline = 1
|
||||
|
||||
// Use half of the deadline delta, and leave the other half to be used
|
||||
// to sweep the HTLCs.
|
||||
default:
|
||||
deadline = (deadlineMinHeight - heightHint) / 2
|
||||
}
|
||||
|
||||
// Calculate the value left after subtracting the budget used for
|
||||
|
@ -2800,7 +2823,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) {
|
|||
// state, so we'll get the most up to date signals to we can
|
||||
// properly do our job.
|
||||
case signalUpdate := <-c.signalUpdates:
|
||||
log.Tracef("ChannelArbitrator(%v) got new signal "+
|
||||
log.Tracef("ChannelArbitrator(%v): got new signal "+
|
||||
"update!", c.cfg.ChanPoint)
|
||||
|
||||
// We'll update the ShortChannelID.
|
||||
|
@ -3066,6 +3089,9 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) {
|
|||
// We've just received a request to forcibly close out the
|
||||
// channel. We'll
|
||||
case closeReq := <-c.forceCloseReqs:
|
||||
log.Infof("ChannelArbitrator(%v): received force "+
|
||||
"close request", c.cfg.ChanPoint)
|
||||
|
||||
if c.state != StateDefault {
|
||||
select {
|
||||
case closeReq.closeTx <- nil:
|
||||
|
|
|
@ -2305,20 +2305,22 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) {
|
|||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
htlcs htlcSet
|
||||
err error
|
||||
deadline fn.Option[int32]
|
||||
expectedBudget btcutil.Amount
|
||||
name string
|
||||
htlcs htlcSet
|
||||
err error
|
||||
deadline fn.Option[int32]
|
||||
mockFindOutgoingHTLCDeadline func()
|
||||
expectedBudget btcutil.Amount
|
||||
}{
|
||||
{
|
||||
// When we have no HTLCs, the default value should be
|
||||
// used.
|
||||
name: "use default conf target",
|
||||
htlcs: htlcSet{},
|
||||
err: nil,
|
||||
deadline: fn.None[int32](),
|
||||
expectedBudget: 0,
|
||||
name: "use default conf target",
|
||||
htlcs: htlcSet{},
|
||||
err: nil,
|
||||
deadline: fn.None[int32](),
|
||||
mockFindOutgoingHTLCDeadline: func() {},
|
||||
expectedBudget: 0,
|
||||
},
|
||||
{
|
||||
// When we have a preimage available in the local HTLC
|
||||
|
@ -2329,8 +2331,17 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) {
|
|||
htlcs: makeHTLCSet(htlcPreimage, htlcLargeExpiry),
|
||||
err: nil,
|
||||
deadline: fn.Some(int32(
|
||||
htlcPreimage.RefundTimeout - heightHint,
|
||||
(htlcPreimage.RefundTimeout - heightHint) / 2,
|
||||
)),
|
||||
mockFindOutgoingHTLCDeadline: func() {
|
||||
chanArb.cfg.FindOutgoingHTLCDeadline = func(
|
||||
htlc channeldb.HTLC) fn.Option[int32] {
|
||||
|
||||
return fn.Some(int32(
|
||||
htlcLargeExpiry.RefundTimeout,
|
||||
))
|
||||
}
|
||||
},
|
||||
expectedBudget: htlcAmt.ToSatoshis(),
|
||||
},
|
||||
{
|
||||
|
@ -2342,8 +2353,18 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) {
|
|||
htlcs: makeHTLCSet(htlcSmallExipry, htlcLargeExpiry),
|
||||
err: nil,
|
||||
deadline: fn.Some(int32(
|
||||
htlcLargeExpiry.RefundTimeout - heightHint,
|
||||
(htlcLargeExpiry.RefundTimeout -
|
||||
heightHint) / 2,
|
||||
)),
|
||||
mockFindOutgoingHTLCDeadline: func() {
|
||||
chanArb.cfg.FindOutgoingHTLCDeadline = func(
|
||||
htlc channeldb.HTLC) fn.Option[int32] {
|
||||
|
||||
return fn.Some(int32(
|
||||
htlcLargeExpiry.RefundTimeout,
|
||||
))
|
||||
}
|
||||
},
|
||||
expectedBudget: htlcAmt.ToSatoshis() / 2,
|
||||
},
|
||||
{
|
||||
|
@ -2354,18 +2375,36 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) {
|
|||
htlcs: makeHTLCSet(htlcPreimage, htlcDust),
|
||||
err: nil,
|
||||
deadline: fn.Some(int32(
|
||||
htlcPreimage.RefundTimeout - heightHint,
|
||||
(htlcPreimage.RefundTimeout - heightHint) / 2,
|
||||
)),
|
||||
mockFindOutgoingHTLCDeadline: func() {
|
||||
chanArb.cfg.FindOutgoingHTLCDeadline = func(
|
||||
htlc channeldb.HTLC) fn.Option[int32] {
|
||||
|
||||
return fn.Some(int32(
|
||||
htlcDust.RefundTimeout,
|
||||
))
|
||||
}
|
||||
},
|
||||
expectedBudget: htlcAmt.ToSatoshis() / 2,
|
||||
},
|
||||
{
|
||||
// When we've reached our deadline, use conf target of
|
||||
// 1 as our deadline. And the value left should be
|
||||
// htlcAmt.
|
||||
name: "use conf target 1",
|
||||
htlcs: makeHTLCSet(htlcPreimage, htlcExpired),
|
||||
err: nil,
|
||||
deadline: fn.Some(int32(1)),
|
||||
name: "use conf target 1",
|
||||
htlcs: makeHTLCSet(htlcPreimage, htlcExpired),
|
||||
err: nil,
|
||||
deadline: fn.Some(int32(1)),
|
||||
mockFindOutgoingHTLCDeadline: func() {
|
||||
chanArb.cfg.FindOutgoingHTLCDeadline = func(
|
||||
htlc channeldb.HTLC) fn.Option[int32] {
|
||||
|
||||
return fn.Some(int32(
|
||||
htlcExpired.RefundTimeout,
|
||||
))
|
||||
}
|
||||
},
|
||||
expectedBudget: htlcAmt.ToSatoshis(),
|
||||
},
|
||||
}
|
||||
|
@ -2373,7 +2412,9 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) {
|
|||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Mock the method `FindOutgoingHTLCDeadline`.
|
||||
tc.mockFindOutgoingHTLCDeadline()
|
||||
|
||||
deadline, budget, err := chanArb.
|
||||
findCommitmentDeadlineAndValue(
|
||||
heightHint, tc.htlcs,
|
||||
|
@ -2412,31 +2453,35 @@ func TestSweepAnchors(t *testing.T) {
|
|||
chanArbCtx.chanArb.blocks <- int32(heightHint)
|
||||
|
||||
htlcIndexBase := uint64(99)
|
||||
htlcExpiryBase := heightHint + uint32(10)
|
||||
deadlineDelta := uint32(10)
|
||||
|
||||
htlcAmt := lnwire.MilliSatoshi(1_000_000)
|
||||
|
||||
// Create three testing HTLCs.
|
||||
htlcDust := channeldb.HTLC{
|
||||
HtlcIndex: htlcIndexBase + 1,
|
||||
RefundTimeout: htlcExpiryBase + 1,
|
||||
RefundTimeout: heightHint + 1,
|
||||
OutputIndex: -1,
|
||||
}
|
||||
|
||||
deadlinePreimageDelta := deadlineDelta + 2
|
||||
htlcWithPreimage := channeldb.HTLC{
|
||||
HtlcIndex: htlcIndexBase + 2,
|
||||
RefundTimeout: htlcExpiryBase + 2,
|
||||
RefundTimeout: heightHint + deadlinePreimageDelta,
|
||||
RHash: rHash,
|
||||
Amt: htlcAmt,
|
||||
}
|
||||
|
||||
deadlineSmallDelta := deadlineDelta + 4
|
||||
htlcSmallExipry := channeldb.HTLC{
|
||||
HtlcIndex: htlcIndexBase + 3,
|
||||
RefundTimeout: htlcExpiryBase + 3,
|
||||
RefundTimeout: heightHint + deadlineSmallDelta,
|
||||
Amt: htlcAmt,
|
||||
}
|
||||
|
||||
// Setup our local HTLC set such that we will use the HTLC's CLTV from
|
||||
// the incoming HTLC set.
|
||||
expectedLocalDeadline := htlcWithPreimage.RefundTimeout
|
||||
expectedLocalDeadline := heightHint + deadlinePreimageDelta/2
|
||||
chanArb.activeHTLCs[LocalHtlcSet] = htlcSet{
|
||||
incomingHTLCs: map[uint64]channeldb.HTLC{
|
||||
htlcWithPreimage.HtlcIndex: htlcWithPreimage,
|
||||
|
@ -2475,7 +2520,7 @@ func TestSweepAnchors(t *testing.T) {
|
|||
|
||||
// Setup out pending remote HTLC set such that we will use the HTLC's
|
||||
// CLTV from the outgoing HTLC set.
|
||||
expectedPendingDeadline := htlcSmallExipry.RefundTimeout
|
||||
expectedPendingDeadline := heightHint + deadlineSmallDelta/2
|
||||
chanArb.activeHTLCs[RemotePendingHtlcSet] = htlcSet{
|
||||
incomingHTLCs: map[uint64]channeldb.HTLC{
|
||||
htlcDust.HtlcIndex: htlcDust,
|
||||
|
@ -2493,6 +2538,18 @@ func TestSweepAnchors(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
// Mock FindOutgoingHTLCDeadline so the pending remote's outgoing HTLC
|
||||
// returns the small expiry value.
|
||||
chanArb.cfg.FindOutgoingHTLCDeadline = func(
|
||||
htlc channeldb.HTLC) fn.Option[int32] {
|
||||
|
||||
if htlc.RHash != htlcSmallExipry.RHash {
|
||||
return fn.None[int32]()
|
||||
}
|
||||
|
||||
return fn.Some(int32(htlcSmallExipry.RefundTimeout))
|
||||
}
|
||||
|
||||
// Create AnchorResolutions.
|
||||
anchors := &lnwallet.AnchorResolutions{
|
||||
Local: &lnwallet.AnchorResolution{
|
||||
|
@ -2599,17 +2656,20 @@ func TestChannelArbitratorAnchors(t *testing.T) {
|
|||
htlcAmt := lnwire.MilliSatoshi(1_000_000)
|
||||
|
||||
// Create testing HTLCs.
|
||||
htlcExpiryBase := heightHint + uint32(10)
|
||||
deadlineDelta := uint32(10)
|
||||
deadlinePreimageDelta := deadlineDelta + 2
|
||||
htlcWithPreimage := channeldb.HTLC{
|
||||
HtlcIndex: 99,
|
||||
RefundTimeout: htlcExpiryBase + 2,
|
||||
RefundTimeout: heightHint + deadlinePreimageDelta,
|
||||
RHash: rHash,
|
||||
Incoming: true,
|
||||
Amt: htlcAmt,
|
||||
}
|
||||
|
||||
deadlineHTLCdelta := deadlineDelta + 3
|
||||
htlc := channeldb.HTLC{
|
||||
HtlcIndex: 100,
|
||||
RefundTimeout: htlcExpiryBase + 3,
|
||||
RefundTimeout: heightHint + deadlineHTLCdelta,
|
||||
Amt: htlcAmt,
|
||||
}
|
||||
|
||||
|
@ -2755,11 +2815,11 @@ func TestChannelArbitratorAnchors(t *testing.T) {
|
|||
// to htlcWithPreimage's CLTV.
|
||||
require.Equal(t, 2, len(chanArbCtx.sweeper.deadlines))
|
||||
require.EqualValues(t,
|
||||
htlcWithPreimage.RefundTimeout,
|
||||
heightHint+deadlinePreimageDelta/2,
|
||||
chanArbCtx.sweeper.deadlines[0],
|
||||
)
|
||||
require.EqualValues(t,
|
||||
htlcWithPreimage.RefundTimeout,
|
||||
heightHint+deadlinePreimageDelta/2,
|
||||
chanArbCtx.sweeper.deadlines[1],
|
||||
)
|
||||
}
|
||||
|
|
|
@ -611,8 +611,12 @@ var allTestCases = []*lntest.TestCase{
|
|||
TestFunc: testNativeSQLNoMigration,
|
||||
},
|
||||
{
|
||||
Name: "sweep anchor cpfp local force close",
|
||||
TestFunc: testSweepAnchorCPFPLocalForceClose,
|
||||
Name: "sweep cpfp anchor outgoing timeout",
|
||||
TestFunc: testSweepCPFPAnchorOutgoingTimeout,
|
||||
},
|
||||
{
|
||||
Name: "sweep cpfp anchor incoming timeout",
|
||||
TestFunc: testSweepCPFPAnchorIncomingTimeout,
|
||||
},
|
||||
{
|
||||
Name: "sweep htlcs",
|
||||
|
|
|
@ -216,34 +216,17 @@ func channelForceClosureTest(ht *lntest.HarnessTest,
|
|||
// We expect to see Alice's force close tx in the mempool.
|
||||
ht.Miner.GetNumTxsFromMempool(1)
|
||||
|
||||
// Assert Alice's has the pending anchor outputs - one for local and
|
||||
// the other for remote (invalid).
|
||||
sweeps := ht.AssertNumPendingSweeps(alice, 2)
|
||||
aliceAnchor := sweeps[0]
|
||||
if aliceAnchor.Outpoint.TxidStr != waitingClose.Commitments.LocalTxid {
|
||||
aliceAnchor = sweeps[1]
|
||||
}
|
||||
require.Equal(ht, aliceAnchor.Outpoint.TxidStr,
|
||||
waitingClose.Commitments.LocalTxid)
|
||||
|
||||
// Mine a block which should confirm the commitment transaction
|
||||
// broadcast as a result of the force closure. Once mined, we also
|
||||
// expect Alice's anchor sweeping tx being published.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
|
||||
// Assert Alice's anchor sweeping tx is found in the mempool.
|
||||
aliceSweepTxid := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
|
||||
// Add alice's anchor to our expected set of reports.
|
||||
op := fmt.Sprintf("%v:%v", aliceAnchor.Outpoint.TxidStr,
|
||||
aliceAnchor.Outpoint.OutputIndex)
|
||||
aliceReports[op] = &lnrpc.Resolution{
|
||||
ResolutionType: lnrpc.ResolutionType_ANCHOR,
|
||||
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
||||
SweepTxid: aliceSweepTxid.String(),
|
||||
Outpoint: aliceAnchor.Outpoint,
|
||||
AmountSat: uint64(anchorSize),
|
||||
}
|
||||
// Assert Alice's has one pending anchor output - because she doesn't
|
||||
// have incoming HTLCs, her outgoing HTLC won't have a deadline, thus
|
||||
// she won't use the anchor to perform CPFP.
|
||||
aliceAnchor := ht.AssertNumPendingSweeps(alice, 1)[0]
|
||||
require.Equal(ht, aliceAnchor.Outpoint.TxidStr,
|
||||
waitingClose.Commitments.LocalTxid)
|
||||
|
||||
// Now that the commitment has been confirmed, the channel should be
|
||||
// marked as force closed.
|
||||
|
@ -290,10 +273,8 @@ func channelForceClosureTest(ht *lntest.HarnessTest,
|
|||
}
|
||||
|
||||
// Mine a block to trigger Carol's sweeper to make decisions on the
|
||||
// anchor sweeping. This block will also confirm Alice's anchor
|
||||
// sweeping tx as her anchor is used for CPFP due to there are
|
||||
// time-sensitive HTLCs.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
// anchor sweeping.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
// Carol's sweep tx should be in the mempool already, as her output is
|
||||
// not timelocked.
|
||||
|
@ -307,7 +288,7 @@ func channelForceClosureTest(ht *lntest.HarnessTest,
|
|||
totalFeeCarol := ht.CalculateTxFee(carolTx)
|
||||
|
||||
// If we have anchors, add an anchor resolution for carol.
|
||||
op = fmt.Sprintf("%v:%v", carolAnchor.Outpoint.TxidStr,
|
||||
op := fmt.Sprintf("%v:%v", carolAnchor.Outpoint.TxidStr,
|
||||
carolAnchor.Outpoint.OutputIndex)
|
||||
carolReports[op] = &lnrpc.Resolution{
|
||||
ResolutionType: lnrpc.ResolutionType_ANCHOR,
|
||||
|
@ -336,27 +317,8 @@ func channelForceClosureTest(ht *lntest.HarnessTest,
|
|||
// commit and anchor outputs.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
|
||||
// Once Alice's anchor sweeping is mined, she should have no pending
|
||||
// sweep requests atm.
|
||||
ht.AssertNumPendingSweeps(alice, 0)
|
||||
|
||||
// TODO(yy): fix the case in 0.18.1 - the CPFP anchor sweeping may be
|
||||
// replaced with a following request after the above restart - the
|
||||
// anchor will be offered to the sweeper again with updated params,
|
||||
// which cannot be swept due to it being uneconomical.
|
||||
var anchorRecovered bool
|
||||
err = wait.NoError(func() error {
|
||||
sweepResp := alice.RPC.ListSweeps(false, 0)
|
||||
txns := sweepResp.GetTransactionIds().TransactionIds
|
||||
|
||||
if len(txns) >= 1 {
|
||||
anchorRecovered = true
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("expected 1 sweep tx, got %d", len(txns))
|
||||
}, wait.DefaultTimeout)
|
||||
ht.Logf("waiting for Alice's anchor sweep to be broadcast: %v", err)
|
||||
// Alice should still have the anchor sweeping request.
|
||||
ht.AssertNumPendingSweeps(alice, 1)
|
||||
|
||||
// The following restart checks to ensure that outputs in the
|
||||
// kindergarten bucket are persisted while waiting for the required
|
||||
|
@ -399,12 +361,8 @@ func channelForceClosureTest(ht *lntest.HarnessTest,
|
|||
return errors.New("all funds should still be in " +
|
||||
"limbo")
|
||||
}
|
||||
if !anchorRecovered {
|
||||
return nil
|
||||
}
|
||||
if forceClose.RecoveredBalance != anchorSize {
|
||||
return fmt.Errorf("expected %v to be recovered",
|
||||
anchorSize)
|
||||
if forceClose.RecoveredBalance != 0 {
|
||||
return errors.New("no funds should be recovered")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -417,7 +375,11 @@ func channelForceClosureTest(ht *lntest.HarnessTest,
|
|||
|
||||
// At this point, the CSV will expire in the next block, meaning that
|
||||
// the output should be offered to the sweeper.
|
||||
aliceCommit := ht.AssertNumPendingSweeps(alice, 1)[0]
|
||||
sweeps := ht.AssertNumPendingSweeps(alice, 2)
|
||||
commitSweep, anchorSweep := sweeps[0], sweeps[1]
|
||||
if commitSweep.AmountSat < anchorSweep.AmountSat {
|
||||
commitSweep, anchorSweep = anchorSweep, commitSweep
|
||||
}
|
||||
|
||||
// Restart Alice to ensure that she resumes watching the finalized
|
||||
// commitment sweep txid.
|
||||
|
@ -438,16 +400,27 @@ func channelForceClosureTest(ht *lntest.HarnessTest,
|
|||
}
|
||||
|
||||
// We expect a resolution which spends our commit output.
|
||||
op = fmt.Sprintf("%v:%v", aliceCommit.Outpoint.TxidStr,
|
||||
aliceCommit.Outpoint.OutputIndex)
|
||||
op = fmt.Sprintf("%v:%v", commitSweep.Outpoint.TxidStr,
|
||||
commitSweep.Outpoint.OutputIndex)
|
||||
aliceReports[op] = &lnrpc.Resolution{
|
||||
ResolutionType: lnrpc.ResolutionType_COMMIT,
|
||||
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
||||
SweepTxid: sweepingTXID.String(),
|
||||
Outpoint: aliceCommit.Outpoint,
|
||||
Outpoint: commitSweep.Outpoint,
|
||||
AmountSat: uint64(aliceBalance),
|
||||
}
|
||||
|
||||
// Add alice's anchor to our expected set of reports.
|
||||
op = fmt.Sprintf("%v:%v", aliceAnchor.Outpoint.TxidStr,
|
||||
aliceAnchor.Outpoint.OutputIndex)
|
||||
aliceReports[op] = &lnrpc.Resolution{
|
||||
ResolutionType: lnrpc.ResolutionType_ANCHOR,
|
||||
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
||||
SweepTxid: sweepingTXID.String(),
|
||||
Outpoint: aliceAnchor.Outpoint,
|
||||
AmountSat: uint64(anchorSize),
|
||||
}
|
||||
|
||||
// Check that we can find the commitment sweep in our set of known
|
||||
// sweeps, using the simple transaction id ListSweeps output.
|
||||
ht.AssertSweepFound(alice, sweepingTXID.String(), false, 0)
|
||||
|
|
|
@ -713,10 +713,6 @@ func runMultiHopLocalForceCloseOnChainHtlcTimeout(ht *lntest.HarnessTest,
|
|||
// to be mined to trigger a force close later on.
|
||||
var blocksMined uint32
|
||||
|
||||
// Increase the fee estimate so that the following force close tx will
|
||||
// be cpfp'ed.
|
||||
ht.SetFeeEstimate(30000)
|
||||
|
||||
// Now that all parties have the HTLC locked in, we'll immediately
|
||||
// force close the Bob -> Carol channel. This should trigger contract
|
||||
// resolution mode for both of them.
|
||||
|
@ -755,7 +751,7 @@ func runMultiHopLocalForceCloseOnChainHtlcTimeout(ht *lntest.HarnessTest,
|
|||
ht.MineEmptyBlocks(int(defaultCSV - blocksMined))
|
||||
blocksMined = defaultCSV
|
||||
|
||||
// Assert Bob has the sweep and trigger it..
|
||||
// Assert Bob has the sweep and trigger it.
|
||||
ht.AssertNumPendingSweeps(bob, 1)
|
||||
ht.MineEmptyBlocks(1)
|
||||
blocksMined++
|
||||
|
@ -1523,10 +1519,6 @@ func runMultiHopHtlcRemoteChainClaim(ht *lntest.HarnessTest,
|
|||
ht.AssertNumPendingSweeps(bob, 1)
|
||||
ht.AssertNumPendingSweeps(alice, 1)
|
||||
|
||||
// Mine a block to confirm Alice's CPFP anchor sweeping.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
blocksMined++
|
||||
|
||||
// Mine enough blocks for Alice to sweep her funds from the force
|
||||
// closed channel. AssertStreamChannelForceClosed() already mined a
|
||||
// block containing the commitment tx and the commit sweep tx will be
|
||||
|
@ -1537,7 +1529,7 @@ func runMultiHopHtlcRemoteChainClaim(ht *lntest.HarnessTest,
|
|||
blocksMined = defaultCSV
|
||||
|
||||
// Alice should now sweep her funds.
|
||||
ht.AssertNumPendingSweeps(alice, 1)
|
||||
ht.AssertNumPendingSweeps(alice, 2)
|
||||
|
||||
// Mine a block to trigger the sweep.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
@ -1690,7 +1682,7 @@ func runMultiHopHtlcRemoteChainClaim(ht *lntest.HarnessTest,
|
|||
ht.MineEmptyBlocks(numBlocks)
|
||||
|
||||
// Both Alice and Bob should offer their commit sweeps.
|
||||
ht.AssertNumPendingSweeps(alice, 1)
|
||||
ht.AssertNumPendingSweeps(alice, 2)
|
||||
ht.AssertNumPendingSweeps(bob, 1)
|
||||
|
||||
// Mine a block to trigger the sweeps.
|
||||
|
@ -2472,7 +2464,6 @@ func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest,
|
|||
if ht.IsNeutrinoBackend() {
|
||||
// Mine a block to confirm Carol's 2nd level success tx.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
numTxesMempool--
|
||||
numBlocks--
|
||||
}
|
||||
|
||||
|
@ -2503,6 +2494,15 @@ func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest,
|
|||
case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE:
|
||||
}
|
||||
|
||||
// For neutrino backend, Carol's second-stage sweep should be offered
|
||||
// to her sweeper.
|
||||
if ht.IsNeutrinoBackend() {
|
||||
ht.AssertNumPendingSweeps(carol, 1)
|
||||
|
||||
// Mine a block to trigger the sweep.
|
||||
ht.MineEmptyBlocks(1)
|
||||
}
|
||||
|
||||
// Mine a block to clean the mempool.
|
||||
ht.MineBlocksAndAssertNumTxes(1, numTxesMempool)
|
||||
|
||||
|
|
|
@ -24,126 +24,159 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testSweepAnchorCPFPLocalForceClose checks when a channel is force closed by
|
||||
// a local node with a time-sensitive HTLC, the anchor output is used for
|
||||
// CPFPing the force close tx.
|
||||
// testSweepCPFPAnchorOutgoingTimeout checks when a channel is force closed by
|
||||
// a local node due to the outgoing HTLC times out, the anchor output is used
|
||||
// for CPFPing the force close tx.
|
||||
//
|
||||
// Setup:
|
||||
// 1. Fund Alice with 2 UTXOs - she will need two to sweep her anchors from
|
||||
// the local and remote commitments, with one of them being invalid.
|
||||
// 2. Fund Bob with no UTXOs - his sweeping txns don't need wallet utxos as he
|
||||
// doesn't need to sweep any time-sensitive outputs.
|
||||
// 3. Alice opens a channel with Bob, and sends him an HTLC without being
|
||||
// settled - we achieve this by letting Bob hold the preimage, which means
|
||||
// he will consider his incoming HTLC has no preimage.
|
||||
// 4. Alice force closes the channel.
|
||||
// 1. Fund Alice with 1 UTXO - she only needs one for the funding process,
|
||||
// 2. Fund Bob with 1 UTXO - he only needs one for the funding process, and
|
||||
// the change output will be used for sweeping his anchor on local commit.
|
||||
// 3. Create a linear network from Alice -> Bob -> Carol.
|
||||
// 4. Alice pays an invoice to Carol through Bob, with Carol holding the
|
||||
// settlement.
|
||||
// 5. Carol goes offline.
|
||||
//
|
||||
// Test:
|
||||
// 1. Alice's force close tx should be CPFPed using the anchor output.
|
||||
// 2. Bob attempts to sweep his anchor output and fails due to it's
|
||||
// uneconomical.
|
||||
// 3. Alice's RBF attempt is using the fee rates calculated from the deadline
|
||||
// and budget.
|
||||
// 4. Wallet UTXOs requirements are met - for Alice she needs at least 2, and
|
||||
// Bob he needs none.
|
||||
func testSweepAnchorCPFPLocalForceClose(ht *lntest.HarnessTest) {
|
||||
// Setup testing params for Alice.
|
||||
// 1. Bob force closes the channel with Carol, using the anchor output for
|
||||
// CPFPing the force close tx.
|
||||
// 2. Bob's anchor output is swept and fee bumped based on its deadline and
|
||||
// budget.
|
||||
func testSweepCPFPAnchorOutgoingTimeout(ht *lntest.HarnessTest) {
|
||||
// Setup testing params.
|
||||
//
|
||||
// startFeeRate is returned by the fee estimator in sat/kw. This
|
||||
// will be used as the starting fee rate for the linear fee func used
|
||||
// by Alice.
|
||||
startFeeRate := chainfee.SatPerKWeight(2000)
|
||||
// Invoice is 100k sats.
|
||||
invoiceAmt := btcutil.Amount(100_000)
|
||||
|
||||
// deadline is the expected deadline for the CPFP transaction.
|
||||
deadline := uint32(10)
|
||||
// Use the smallest CLTV so we can mine fewer blocks.
|
||||
cltvDelta := routing.MinCLTVDelta
|
||||
|
||||
// deadlineDeltaAnchor is the expected deadline delta for the CPFP
|
||||
// anchor sweeping tx.
|
||||
deadlineDeltaAnchor := uint32(cltvDelta / 2)
|
||||
|
||||
// startFeeRateAnchor is the starting fee rate for the CPFP anchor
|
||||
// sweeping tx.
|
||||
startFeeRateAnchor := chainfee.SatPerKWeight(2500)
|
||||
|
||||
// Set up the fee estimator to return the testing fee rate when the
|
||||
// conf target is the deadline.
|
||||
ht.SetFeeEstimateWithConf(startFeeRate, deadline)
|
||||
|
||||
// Calculate the final ctlv delta based on the expected deadline.
|
||||
finalCltvDelta := int32(deadline - uint32(routing.BlockPadding) + 1)
|
||||
|
||||
// toLocalCSV is the CSV delay for Alice's to_local output. This value
|
||||
// is chosen so the commit sweep happens after the anchor sweep,
|
||||
// enabling us to focus on checking the fees in CPFP here.
|
||||
toLocalCSV := deadline * 2
|
||||
|
||||
// htlcAmt is the amount of the HTLC in sats. With default settings,
|
||||
// this will give us 25000 sats as the budget to sweep the CPFP anchor
|
||||
// output.
|
||||
htlcAmt := btcutil.Amount(100_000)
|
||||
|
||||
// Calculate the budget. Since it's a time-sensitive HTLC, we will use
|
||||
// its value after subtracting its own budget as the CPFP budget.
|
||||
valueLeft := htlcAmt.MulF64(1 - contractcourt.DefaultBudgetRatio)
|
||||
budget := valueLeft.MulF64(1 - contractcourt.DefaultBudgetRatio)
|
||||
|
||||
// We now set up testing params for Bob.
|
||||
//
|
||||
// bobBalance is the push amount when Alice opens the channel with Bob.
|
||||
// We will use zero here so we can focus on testing the CPFP logic from
|
||||
// Alice's side here.
|
||||
bobBalance := btcutil.Amount(0)
|
||||
// TODO(yy): switch to conf when `blockbeat` is in place.
|
||||
// ht.SetFeeEstimateWithConf(startFeeRateAnchor, deadlineDeltaAnchor)
|
||||
ht.SetFeeEstimate(startFeeRateAnchor)
|
||||
|
||||
// Make sure our assumptions and calculations are correct.
|
||||
require.EqualValues(ht, 25000, budget)
|
||||
// htlcValue is the outgoing HTLC's value.
|
||||
htlcValue := invoiceAmt
|
||||
|
||||
// We now set up the force close scenario. Alice will open a channel
|
||||
// with Bob, send an HTLC, and then force close it with a
|
||||
// time-sensitive outgoing HTLC.
|
||||
// htlcBudget is the budget used to sweep the outgoing HTLC.
|
||||
htlcBudget := htlcValue.MulF64(contractcourt.DefaultBudgetRatio)
|
||||
|
||||
// cpfpBudget is the budget used to sweep the CPFP anchor.
|
||||
cpfpBudget := (htlcValue - htlcBudget).MulF64(
|
||||
contractcourt.DefaultBudgetRatio,
|
||||
)
|
||||
|
||||
// Create a preimage, that will be held by Carol.
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], ht.Random32Bytes())
|
||||
payHash := preimage.Hash()
|
||||
|
||||
// We now set up the force close scenario. We will create a network
|
||||
// from Alice -> Bob -> Carol, where Alice will send a payment to Carol
|
||||
// via Bob, Carol goes offline. We expect Bob to sweep his anchor and
|
||||
// outgoing HTLC.
|
||||
//
|
||||
// Prepare node params.
|
||||
// Prepare params.
|
||||
cfg := []string{
|
||||
"--hodl.exit-settle",
|
||||
"--protocol.anchors",
|
||||
// Use a small CLTV to mine less blocks.
|
||||
fmt.Sprintf("--bitcoin.timelockdelta=%d", cltvDelta),
|
||||
// Use a very large CSV, this way to_local outputs are never
|
||||
// swept so we can focus on testing HTLCs.
|
||||
fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV),
|
||||
fmt.Sprintf("--bitcoin.defaultremotedelay=%v", cltvDelta*10),
|
||||
}
|
||||
openChannelParams := lntest.OpenChannelParams{
|
||||
Amt: htlcAmt * 10,
|
||||
PushAmt: bobBalance,
|
||||
Amt: invoiceAmt * 10,
|
||||
}
|
||||
|
||||
// Create a two hop network: Alice -> Bob.
|
||||
chanPoints, nodes := createSimpleNetwork(ht, cfg, 2, openChannelParams)
|
||||
// Create a three hop network: Alice -> Bob -> Carol.
|
||||
chanPoints, nodes := createSimpleNetwork(ht, cfg, 3, openChannelParams)
|
||||
|
||||
// Unwrap the results.
|
||||
chanPoint := chanPoints[0]
|
||||
alice, bob := nodes[0], nodes[1]
|
||||
abChanPoint, bcChanPoint := chanPoints[0], chanPoints[1]
|
||||
alice, bob, carol := nodes[0], nodes[1], nodes[2]
|
||||
|
||||
// Send one more utxo to Alice - she will need two utxos to sweep the
|
||||
// anchor output living on the local and remote commits.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
|
||||
// For neutrino backend, we need one more UTXO for Bob to create his
|
||||
// sweeping txns.
|
||||
if ht.IsNeutrinoBackend() {
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, bob)
|
||||
}
|
||||
|
||||
// Send a payment with a specified finalCTLVDelta, which will be used
|
||||
// as our deadline later on when Alice force closes the channel.
|
||||
// Subscribe the invoice.
|
||||
streamCarol := carol.RPC.SubscribeSingleInvoice(payHash[:])
|
||||
|
||||
// With the network active, we'll now add a hodl invoice at Carol's
|
||||
// end.
|
||||
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
|
||||
Value: int64(invoiceAmt),
|
||||
CltvExpiry: finalCltvDelta,
|
||||
Hash: payHash[:],
|
||||
}
|
||||
invoice := carol.RPC.AddHoldInvoice(invoiceReq)
|
||||
|
||||
// Let Alice pay the invoices.
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
Dest: bob.PubKey[:],
|
||||
Amt: int64(htlcAmt),
|
||||
PaymentHash: ht.Random32Bytes(),
|
||||
FinalCltvDelta: finalCltvDelta,
|
||||
PaymentRequest: invoice.PaymentRequest,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
alice.RPC.SendPayment(req)
|
||||
|
||||
// Once the HTLC has cleared, all the nodes in our mini network should
|
||||
// show that the HTLC has been locked in.
|
||||
ht.AssertNumActiveHtlcs(alice, 1)
|
||||
ht.AssertNumActiveHtlcs(bob, 1)
|
||||
// Assert the payments are inflight.
|
||||
ht.SendPaymentAndAssertStatus(alice, req, lnrpc.Payment_IN_FLIGHT)
|
||||
|
||||
// Alice force closes the channel.
|
||||
_, closeTxid := ht.CloseChannelAssertPending(alice, chanPoint, true)
|
||||
// Wait for Carol to mark invoice as accepted. There is a small gap to
|
||||
// bridge between adding the htlc to the channel and executing the exit
|
||||
// hop logic.
|
||||
ht.AssertInvoiceState(streamCarol, lnrpc.Invoice_ACCEPTED)
|
||||
|
||||
// Now that the channel has been force closed, it should show up in the
|
||||
// PendingChannels RPC under the waiting close section.
|
||||
ht.AssertChannelWaitingClose(alice, chanPoint)
|
||||
// At this point, all 3 nodes should now have an active channel with
|
||||
// the created HTLCs pending on all of them.
|
||||
//
|
||||
// Alice should have one outgoing HTLCs on channel Alice -> Bob.
|
||||
ht.AssertOutgoingHTLCActive(alice, abChanPoint, payHash[:])
|
||||
|
||||
// Alice should have two pending sweeps,
|
||||
// - anchor sweeping from her local commitment.
|
||||
// - anchor sweeping from her remote commitment (invalid).
|
||||
// Bob should have one incoming HTLC on channel Alice -> Bob, and one
|
||||
// outgoing HTLC on channel Bob -> Carol.
|
||||
ht.AssertIncomingHTLCActive(bob, abChanPoint, payHash[:])
|
||||
ht.AssertOutgoingHTLCActive(bob, bcChanPoint, payHash[:])
|
||||
|
||||
// Carol should have one incoming HTLC on channel Bob -> Carol.
|
||||
ht.AssertIncomingHTLCActive(carol, bcChanPoint, payHash[:])
|
||||
|
||||
// Let Carol go offline so we can focus on testing Bob's sweeping
|
||||
// behavior.
|
||||
ht.Shutdown(carol)
|
||||
|
||||
// We'll now mine enough blocks to trigger Bob to force close channel
|
||||
// Bob->Carol due to his outgoing HTLC is about to timeout. With the
|
||||
// default outgoing broadcast delta of zero, this will be the same
|
||||
// height as the outgoing htlc's expiry height.
|
||||
numBlocks := padCLTV(uint32(
|
||||
invoiceReq.CltvExpiry - lncfg.DefaultOutgoingBroadcastDelta,
|
||||
))
|
||||
ht.MineEmptyBlocks(int(numBlocks))
|
||||
|
||||
// Assert Bob's force closing tx has been broadcast.
|
||||
closeTxid := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
|
||||
// Remember the force close height so we can calculate the deadline
|
||||
// height.
|
||||
_, forceCloseHeight := ht.Miner.GetBestBlock()
|
||||
|
||||
// Bob should have two pending sweeps,
|
||||
// - anchor sweeping from his local commitment.
|
||||
// - anchor sweeping from his remote commitment (invalid).
|
||||
//
|
||||
// TODO(yy): consider only sweeping the anchor from the local
|
||||
// commitment. Previously we would sweep up to three versions of
|
||||
|
@ -152,108 +185,114 @@ func testSweepAnchorCPFPLocalForceClose(ht *lntest.HarnessTest) {
|
|||
// their commitment tx and replaces ours. With the new fee bumping, we
|
||||
// should be safe to only sweep our local anchor since we RBF it on
|
||||
// every new block, which destroys the remote's ability to pin us.
|
||||
ht.AssertNumPendingSweeps(alice, 2)
|
||||
sweeps := ht.AssertNumPendingSweeps(bob, 2)
|
||||
|
||||
// Bob should have no pending sweeps here. Although he learned about
|
||||
// the force close tx, because he doesn't have any outgoing HTLCs, he
|
||||
// doesn't need to sweep anything.
|
||||
ht.AssertNumPendingSweeps(bob, 0)
|
||||
// The two anchor sweeping should have the same deadline height.
|
||||
deadlineHeight := uint32(forceCloseHeight) + deadlineDeltaAnchor
|
||||
require.Equal(ht, deadlineHeight, sweeps[0].DeadlineHeight)
|
||||
require.Equal(ht, deadlineHeight, sweeps[1].DeadlineHeight)
|
||||
|
||||
// Mine a block so Alice's force closing tx stays in the mempool, which
|
||||
// also triggers the sweep.
|
||||
// Remember the deadline height for the CPFP anchor.
|
||||
anchorDeadline := sweeps[0].DeadlineHeight
|
||||
|
||||
// Mine a block so Bob's force closing tx stays in the mempool, which
|
||||
// also triggers the CPFP anchor sweep.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
// TODO(yy): we should also handle the edge case where the force close
|
||||
// tx confirms here - we should cancel the fee bumping attempt for this
|
||||
// anchor sweep and let it stay in mempool? Or should we unlease the
|
||||
// wallet input and ask the sweeper to re-sweep the anchor?
|
||||
// ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
// Bob should still have two pending sweeps,
|
||||
// - anchor sweeping from his local commitment.
|
||||
// - anchor sweeping from his remote commitment (invalid).
|
||||
ht.AssertNumPendingSweeps(bob, 2)
|
||||
|
||||
// We now check the expected fee and fee rate are used for Alice.
|
||||
// We now check the expected fee and fee rate are used for Bob's anchor
|
||||
// sweeping tx.
|
||||
//
|
||||
// We should see Alice's anchor sweeping tx triggered by the above
|
||||
// block, along with Alice's force close tx.
|
||||
// We should see Bob's anchor sweeping tx triggered by the above
|
||||
// block, along with his force close tx.
|
||||
txns := ht.Miner.GetNumTxsFromMempool(2)
|
||||
|
||||
// Find the sweeping tx.
|
||||
sweepTx := ht.FindSweepingTxns(txns, 1, *closeTxid)[0]
|
||||
|
||||
// Get the weight for Alice's sweep tx.
|
||||
// Get the weight for Bob's anchor sweeping tx.
|
||||
txWeight := ht.CalculateTxWeight(sweepTx)
|
||||
|
||||
// Calculate the fee and fee rate of Alice's sweeping tx.
|
||||
// Bob should start with the initial fee rate of 2500 sat/kw.
|
||||
startFeeAnchor := startFeeRateAnchor.FeeForWeight(txWeight)
|
||||
|
||||
// Calculate the fee and fee rate of Bob's sweeping tx.
|
||||
fee := uint64(ht.CalculateTxFee(sweepTx))
|
||||
feeRate := uint64(ht.CalculateTxFeeRate(sweepTx))
|
||||
|
||||
// Alice should start with the initial fee rate of 2000 sat/kw.
|
||||
startFee := startFeeRate.FeeForWeight(txWeight)
|
||||
// feeFuncWidth is the width of the fee function. By the time we got
|
||||
// here, we've already mined one block, and the fee function maxes
|
||||
// out one block before the deadline, so the width is the original
|
||||
// deadline minus 2.
|
||||
feeFuncWidth := deadlineDeltaAnchor - 2
|
||||
|
||||
// Calculate the expected delta increased per block.
|
||||
//
|
||||
// NOTE: Assume a wallet tr output is used for fee bumping, with the tx
|
||||
// weight of 725, we expect this value to be 2355.
|
||||
feeDeltaAlice := (budget - startFee).MulF64(1 / float64(10))
|
||||
feeDelta := (cpfpBudget - startFeeAnchor).MulF64(
|
||||
1 / float64(feeFuncWidth),
|
||||
)
|
||||
|
||||
// We expect the startingFee and startingFeeRate being used. Allow some
|
||||
// deviation because weight estimates during tx generation are
|
||||
// estimates.
|
||||
//
|
||||
// TODO(yy): unify all the units and types re int vs uint!
|
||||
require.InEpsilonf(ht, uint64(startFee), fee, 0.01,
|
||||
"want %d, got %d", startFee, fee)
|
||||
require.InEpsilonf(ht, uint64(startFeeRate), feeRate,
|
||||
0.01, "want %d, got %d", startFeeRate, fee)
|
||||
require.InEpsilonf(ht, uint64(startFeeAnchor), fee, 0.01,
|
||||
"want %d, got %d", startFeeAnchor, fee)
|
||||
require.InEpsilonf(ht, uint64(startFeeRateAnchor), feeRate,
|
||||
0.01, "want %d, got %d", startFeeRateAnchor, fee)
|
||||
|
||||
// Bob has no time-sensitive outputs, so he should sweep nothing.
|
||||
ht.AssertNumPendingSweeps(bob, 0)
|
||||
|
||||
// We now mine deadline-1 empty blocks. For each block mined, Alice
|
||||
// should perform an RBF on her CPFP anchor sweeping tx. By the end of
|
||||
// this iteration, we expect Alice to use start sweeping her htlc
|
||||
// output after one more block.
|
||||
for i := uint32(1); i <= deadline; i++ {
|
||||
// We now mine deadline-2 empty blocks. For each block mined, Bob
|
||||
// should perform an RBF on his CPFP anchor sweeping tx. By the end of
|
||||
// this iteration, we expect Bob to use up his CPFP budget after one
|
||||
// more block.
|
||||
for i := uint32(1); i <= feeFuncWidth-1; i++ {
|
||||
// Mine an empty block. Since the sweeping tx is not confirmed,
|
||||
// Alice's fee bumper should increase its fees.
|
||||
// Bob's fee bumper should increase its fees.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
// Alice should still have two pending sweeps,
|
||||
// - anchor sweeping from her local commitment.
|
||||
// - anchor sweeping from her remote commitment (invalid).
|
||||
ht.AssertNumPendingSweeps(alice, 2)
|
||||
// Bob should still have two pending sweeps,
|
||||
// - anchor sweeping from his local commitment.
|
||||
// - anchor sweeping from his remote commitment (invalid).
|
||||
ht.AssertNumPendingSweeps(bob, 2)
|
||||
|
||||
// We expect to see two txns in the mempool,
|
||||
// - Alice's force close tx.
|
||||
// - Alice's anchor sweep tx.
|
||||
ht.Miner.AssertNumTxsInMempool(2)
|
||||
|
||||
// Make sure Alice's old sweeping tx has been removed from the
|
||||
// Make sure Bob's old sweeping tx has been removed from the
|
||||
// mempool.
|
||||
ht.Miner.AssertTxNotInMempool(sweepTx.TxHash())
|
||||
|
||||
// We expect to see two txns in the mempool,
|
||||
// - Bob's force close tx.
|
||||
// - Bob's anchor sweep tx.
|
||||
ht.Miner.AssertNumTxsInMempool(2)
|
||||
|
||||
// We expect the fees to increase by i*delta.
|
||||
expectedFee := startFee + feeDeltaAlice.MulF64(float64(i))
|
||||
expectedFee := startFeeAnchor + feeDelta.MulF64(float64(i))
|
||||
expectedFeeRate := chainfee.NewSatPerKWeight(
|
||||
expectedFee, uint64(txWeight),
|
||||
)
|
||||
|
||||
// We should see Alice's anchor sweeping tx being fee bumped
|
||||
// since it's not confirmed, along with her force close tx.
|
||||
// We should see Bob's anchor sweeping tx being fee bumped
|
||||
// since it's not confirmed, along with his force close tx.
|
||||
txns = ht.Miner.GetNumTxsFromMempool(2)
|
||||
|
||||
// Find the sweeping tx.
|
||||
sweepTx = ht.FindSweepingTxns(txns, 1, *closeTxid)[0]
|
||||
|
||||
// Calculate the fee rate of Alice's new sweeping tx.
|
||||
// Calculate the fee rate of Bob's new sweeping tx.
|
||||
feeRate = uint64(ht.CalculateTxFeeRate(sweepTx))
|
||||
|
||||
// Calculate the fee of Alice's new sweeping tx.
|
||||
// Calculate the fee of Bob's new sweeping tx.
|
||||
fee = uint64(ht.CalculateTxFee(sweepTx))
|
||||
|
||||
ht.Logf("Alice(deadline=%v): txWeight=%v, expected: [fee=%d, "+
|
||||
"feerate=%v], got: [fee=%v, feerate=%v]", deadline-i,
|
||||
txWeight, expectedFee, expectedFeeRate, fee, feeRate)
|
||||
ht.Logf("Bob(position=%v): txWeight=%v, expected: [fee=%d, "+
|
||||
"feerate=%v], got: [fee=%v, feerate=%v]",
|
||||
feeFuncWidth-i, txWeight, expectedFee,
|
||||
expectedFeeRate, fee, feeRate)
|
||||
|
||||
// Assert Alice's tx has the expected fee and fee rate.
|
||||
// Assert Bob's tx has the expected fee and fee rate.
|
||||
require.InEpsilonf(ht, uint64(expectedFee), fee, 0.01,
|
||||
"deadline=%v, want %d, got %d", i, expectedFee, fee)
|
||||
require.InEpsilonf(ht, uint64(expectedFeeRate), feeRate, 0.01,
|
||||
|
@ -261,27 +300,34 @@ func testSweepAnchorCPFPLocalForceClose(ht *lntest.HarnessTest) {
|
|||
feeRate)
|
||||
}
|
||||
|
||||
// Once out of the above loop, we should've mined deadline-1 blocks. If
|
||||
// we mine one more block, we'd use up all the CPFP budget.
|
||||
// We now check the budget has been used up at the deadline-1 block.
|
||||
//
|
||||
// Once out of the above loop, we expect to be 2 blocks before the CPFP
|
||||
// deadline.
|
||||
_, currentHeight := ht.Miner.GetBestBlock()
|
||||
require.Equal(ht, int(anchorDeadline-2), int(currentHeight))
|
||||
|
||||
// Mine one more block, we'd use up all the CPFP budget.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
// Get the last sweeping tx - we should see two txns here, Alice's
|
||||
// anchor sweeping tx and her force close tx.
|
||||
// Make sure Bob's old sweeping tx has been removed from the mempool.
|
||||
ht.Miner.AssertTxNotInMempool(sweepTx.TxHash())
|
||||
|
||||
// Get the last sweeping tx - we should see two txns here, Bob's anchor
|
||||
// sweeping tx and his force close tx.
|
||||
txns = ht.Miner.GetNumTxsFromMempool(2)
|
||||
|
||||
// Find the sweeping tx.
|
||||
sweepTx = ht.FindSweepingTxns(txns, 1, *closeTxid)[0]
|
||||
|
||||
// Calculate the fee and fee rate of Alice's new sweeping tx.
|
||||
// Calculate the fee of Bob's new sweeping tx.
|
||||
fee = uint64(ht.CalculateTxFee(sweepTx))
|
||||
feeRate = uint64(ht.CalculateTxFeeRate(sweepTx))
|
||||
|
||||
// Alice should still have two pending sweeps,
|
||||
// - anchor sweeping from her local commitment.
|
||||
// - anchor sweeping from her remote commitment (invalid).
|
||||
ht.AssertNumPendingSweeps(alice, 2)
|
||||
// Assert the budget is now used up.
|
||||
require.InEpsilonf(ht, uint64(cpfpBudget), fee, 0.01, "want %d, got %d",
|
||||
cpfpBudget, fee)
|
||||
|
||||
// Mine one more block. Since Alice's budget has been used up, there
|
||||
// Mine one more block. Since Bob's budget has been used up, there
|
||||
// won't be any more sweeping attempts. We now assert this by checking
|
||||
// that the sweeping tx stayed unchanged.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
@ -289,30 +335,361 @@ func testSweepAnchorCPFPLocalForceClose(ht *lntest.HarnessTest) {
|
|||
// Get the current sweeping tx and assert it stays unchanged.
|
||||
//
|
||||
// We expect two txns here, one for the anchor sweeping, the other for
|
||||
// the HTLC sweeping.
|
||||
// the force close tx.
|
||||
txns = ht.Miner.GetNumTxsFromMempool(2)
|
||||
|
||||
// Find the sweeping tx.
|
||||
currentSweepTx := ht.FindSweepingTxns(txns, 1, *closeTxid)[0]
|
||||
|
||||
// Calculate the fee and fee rate of Alice's current sweeping tx.
|
||||
currentFee := uint64(ht.CalculateTxFee(sweepTx))
|
||||
currentFeeRate := uint64(ht.CalculateTxFeeRate(sweepTx))
|
||||
// Assert the anchor sweep tx stays unchanged.
|
||||
require.Equal(ht, sweepTx.TxHash(), currentSweepTx.TxHash())
|
||||
|
||||
// Mine a block to confirm Bob's sweeping and force close txns, this is
|
||||
// needed to clean up the mempool.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 2)
|
||||
|
||||
// The above mined block should confirm Bob's force close tx, and his
|
||||
// contractcourt will offer the HTLC to his sweeper. We are not testing
|
||||
// the HTLC sweeping behaviors so we just perform a simple check and
|
||||
// exit the test.
|
||||
ht.AssertNumPendingSweeps(bob, 1)
|
||||
|
||||
// Finally, clean the mempool for the next test.
|
||||
ht.CleanShutDown()
|
||||
}
|
||||
|
||||
// testSweepCPFPAnchorIncomingTimeout checks when a channel is force closed by
|
||||
// a local node due to the incoming HTLC is about to time out, the anchor
|
||||
// output is used for CPFPing the force close tx.
|
||||
//
|
||||
// Setup:
|
||||
// 1. Fund Alice with 1 UTXOs - she only needs one for the funding process,
|
||||
// 2. Fund Bob with 1 UTXO - he only needs one for the funding process, and
|
||||
// the change output will be used for sweeping his anchor on local commit.
|
||||
// 3. Create a linear network from Alice -> Bob -> Carol.
|
||||
// 4. Alice pays an invoice to Carol through Bob.
|
||||
// 5. Alice goes offline.
|
||||
// 6. Carol settles the invoice.
|
||||
//
|
||||
// Test:
|
||||
// 1. Bob force closes the channel with Alice, using the anchor output for
|
||||
// CPFPing the force close tx.
|
||||
// 2. Bob's anchor output is swept and fee bumped based on its deadline and
|
||||
// budget.
|
||||
func testSweepCPFPAnchorIncomingTimeout(ht *lntest.HarnessTest) {
|
||||
// Setup testing params.
|
||||
//
|
||||
// Invoice is 100k sats.
|
||||
invoiceAmt := btcutil.Amount(100_000)
|
||||
|
||||
// Use the smallest CLTV so we can mine fewer blocks.
|
||||
cltvDelta := routing.MinCLTVDelta
|
||||
|
||||
// goToChainDelta is the broadcast delta of Bob's incoming HTLC. When
|
||||
// the block height is at CLTV-goToChainDelta, Bob will force close the
|
||||
// channel Alice=>Bob.
|
||||
goToChainDelta := uint32(lncfg.DefaultIncomingBroadcastDelta)
|
||||
|
||||
// deadlineDeltaAnchor is the expected deadline delta for the CPFP
|
||||
// anchor sweeping tx.
|
||||
deadlineDeltaAnchor := goToChainDelta / 2
|
||||
|
||||
// startFeeRateAnchor is the starting fee rate for the CPFP anchor
|
||||
// sweeping tx.
|
||||
startFeeRateAnchor := chainfee.SatPerKWeight(2500)
|
||||
|
||||
// Set up the fee estimator to return the testing fee rate when the
|
||||
// conf target is the deadline.
|
||||
//
|
||||
// TODO(yy): switch to conf when `blockbeat` is in place.
|
||||
// ht.SetFeeEstimateWithConf(startFeeRateAnchor, deadlineDeltaAnchor)
|
||||
ht.SetFeeEstimate(startFeeRateAnchor)
|
||||
|
||||
// Create a preimage, that will be held by Carol.
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], ht.Random32Bytes())
|
||||
payHash := preimage.Hash()
|
||||
|
||||
// We now set up the force close scenario. We will create a network
|
||||
// from Alice -> Bob -> Carol, where Alice will send a payment to Carol
|
||||
// via Bob, Alice goes offline, Carol settles the payment. We expect
|
||||
// Bob to sweep his anchor and incoming HTLC.
|
||||
//
|
||||
// Prepare params.
|
||||
cfg := []string{
|
||||
"--protocol.anchors",
|
||||
// Use a small CLTV to mine less blocks.
|
||||
fmt.Sprintf("--bitcoin.timelockdelta=%d", cltvDelta),
|
||||
// Use a very large CSV, this way to_local outputs are never
|
||||
// swept so we can focus on testing HTLCs.
|
||||
fmt.Sprintf("--bitcoin.defaultremotedelay=%v", cltvDelta*10),
|
||||
}
|
||||
openChannelParams := lntest.OpenChannelParams{
|
||||
Amt: invoiceAmt * 10,
|
||||
}
|
||||
|
||||
// Create a three hop network: Alice -> Bob -> Carol.
|
||||
chanPoints, nodes := createSimpleNetwork(ht, cfg, 3, openChannelParams)
|
||||
|
||||
// Unwrap the results.
|
||||
abChanPoint, bcChanPoint := chanPoints[0], chanPoints[1]
|
||||
alice, bob, carol := nodes[0], nodes[1], nodes[2]
|
||||
|
||||
// For neutrino backend, we need one more UTXO for Bob to create his
|
||||
// sweeping txns.
|
||||
if ht.IsNeutrinoBackend() {
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, bob)
|
||||
}
|
||||
|
||||
// Subscribe the invoice.
|
||||
streamCarol := carol.RPC.SubscribeSingleInvoice(payHash[:])
|
||||
|
||||
// With the network active, we'll now add a hodl invoice at Carol's
|
||||
// end.
|
||||
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
|
||||
Value: int64(invoiceAmt),
|
||||
CltvExpiry: finalCltvDelta,
|
||||
Hash: payHash[:],
|
||||
}
|
||||
invoice := carol.RPC.AddHoldInvoice(invoiceReq)
|
||||
|
||||
// Let Alice pay the invoices.
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: invoice.PaymentRequest,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
|
||||
// Assert the payments are inflight.
|
||||
ht.SendPaymentAndAssertStatus(alice, req, lnrpc.Payment_IN_FLIGHT)
|
||||
|
||||
// Wait for Carol to mark invoice as accepted. There is a small gap to
|
||||
// bridge between adding the htlc to the channel and executing the exit
|
||||
// hop logic.
|
||||
ht.AssertInvoiceState(streamCarol, lnrpc.Invoice_ACCEPTED)
|
||||
|
||||
// At this point, all 3 nodes should now have an active channel with
|
||||
// the created HTLCs pending on all of them.
|
||||
//
|
||||
// Alice should have one outgoing HTLCs on channel Alice -> Bob.
|
||||
ht.AssertOutgoingHTLCActive(alice, abChanPoint, payHash[:])
|
||||
|
||||
// Bob should have one incoming HTLC on channel Alice -> Bob, and one
|
||||
// outgoing HTLC on channel Bob -> Carol.
|
||||
htlc := ht.AssertIncomingHTLCActive(bob, abChanPoint, payHash[:])
|
||||
ht.AssertOutgoingHTLCActive(bob, bcChanPoint, payHash[:])
|
||||
|
||||
// Calculate the budget used for Bob's anchor sweeping.
|
||||
//
|
||||
// htlcValue is the incoming HTLC's value.
|
||||
htlcValue := btcutil.Amount(htlc.Amount)
|
||||
|
||||
// htlcBudget is the budget used to sweep the incoming HTLC.
|
||||
htlcBudget := htlcValue.MulF64(contractcourt.DefaultBudgetRatio)
|
||||
|
||||
// cpfpBudget is the budget used to sweep the CPFP anchor.
|
||||
cpfpBudget := (htlcValue - htlcBudget).MulF64(
|
||||
contractcourt.DefaultBudgetRatio,
|
||||
)
|
||||
|
||||
// Carol should have one incoming HTLC on channel Bob -> Carol.
|
||||
ht.AssertIncomingHTLCActive(carol, bcChanPoint, payHash[:])
|
||||
|
||||
// Let Alice go offline. Once Bob later learns the preimage, he
|
||||
// couldn't settle it with Alice so he has to go onchain to collect it.
|
||||
ht.Shutdown(alice)
|
||||
|
||||
// Carol settles invoice.
|
||||
carol.RPC.SettleInvoice(preimage[:])
|
||||
|
||||
// Bob should have settled his outgoing HTLC with Carol.
|
||||
ht.AssertHTLCNotActive(bob, bcChanPoint, payHash[:])
|
||||
|
||||
// We'll now mine enough blocks to trigger Bob to force close channel
|
||||
// Alice->Bob due to his incoming HTLC is about to timeout. With the
|
||||
// default incoming broadcast delta of 10, this will be the same
|
||||
// height as the incoming htlc's expiry height minus 10.
|
||||
forceCloseHeight := htlc.ExpirationHeight - goToChainDelta
|
||||
|
||||
// Mine till the goToChainHeight is reached.
|
||||
_, currentHeight := ht.Miner.GetBestBlock()
|
||||
numBlocks := forceCloseHeight - uint32(currentHeight)
|
||||
ht.MineEmptyBlocks(int(numBlocks))
|
||||
|
||||
// Assert Bob's force closing tx has been broadcast.
|
||||
closeTxid := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
|
||||
// Bob should have two pending sweeps,
|
||||
// - anchor sweeping from his local commitment.
|
||||
// - anchor sweeping from his remote commitment (invalid).
|
||||
sweeps := ht.AssertNumPendingSweeps(bob, 2)
|
||||
|
||||
// The two anchor sweeping should have the same deadline height.
|
||||
deadlineHeight := forceCloseHeight + deadlineDeltaAnchor
|
||||
require.Equal(ht, deadlineHeight, sweeps[0].DeadlineHeight)
|
||||
require.Equal(ht, deadlineHeight, sweeps[1].DeadlineHeight)
|
||||
|
||||
// Remember the deadline height for the CPFP anchor.
|
||||
anchorDeadline := sweeps[0].DeadlineHeight
|
||||
|
||||
// Mine a block so Bob's force closing tx stays in the mempool, which
|
||||
// also triggers the CPFP anchor sweep.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
// Bob should still have two pending sweeps,
|
||||
// - anchor sweeping from his local commitment.
|
||||
// - anchor sweeping from his remote commitment (invalid).
|
||||
ht.AssertNumPendingSweeps(bob, 2)
|
||||
|
||||
// We now check the expected fee and fee rate are used for Bob's anchor
|
||||
// sweeping tx.
|
||||
//
|
||||
// We should see Bob's anchor sweeping tx triggered by the above
|
||||
// block, along with his force close tx.
|
||||
txns := ht.Miner.GetNumTxsFromMempool(2)
|
||||
|
||||
// Find the sweeping tx.
|
||||
sweepTx := ht.FindSweepingTxns(txns, 1, *closeTxid)[0]
|
||||
|
||||
// Get the weight for Bob's anchor sweeping tx.
|
||||
txWeight := ht.CalculateTxWeight(sweepTx)
|
||||
|
||||
// Bob should start with the initial fee rate of 2500 sat/kw.
|
||||
startFeeAnchor := startFeeRateAnchor.FeeForWeight(txWeight)
|
||||
|
||||
// Calculate the fee and fee rate of Bob's sweeping tx.
|
||||
fee := uint64(ht.CalculateTxFee(sweepTx))
|
||||
feeRate := uint64(ht.CalculateTxFeeRate(sweepTx))
|
||||
|
||||
// feeFuncWidth is the width of the fee function. By the time we got
|
||||
// here, we've already mined one block, and the fee function maxes
|
||||
// out one block before the deadline, so the width is the original
|
||||
// deadline minus 2.
|
||||
feeFuncWidth := deadlineDeltaAnchor - 2
|
||||
|
||||
// Calculate the expected delta increased per block.
|
||||
feeDelta := (cpfpBudget - startFeeAnchor).MulF64(
|
||||
1 / float64(feeFuncWidth),
|
||||
)
|
||||
|
||||
// We expect the startingFee and startingFeeRate being used. Allow some
|
||||
// deviation because weight estimates during tx generation are
|
||||
// estimates.
|
||||
//
|
||||
// TODO(yy): unify all the units and types re int vs uint!
|
||||
require.InEpsilonf(ht, uint64(startFeeAnchor), fee, 0.01,
|
||||
"want %d, got %d", startFeeAnchor, fee)
|
||||
require.InEpsilonf(ht, uint64(startFeeRateAnchor), feeRate,
|
||||
0.01, "want %d, got %d", startFeeRateAnchor, fee)
|
||||
|
||||
// We now mine deadline-2 empty blocks. For each block mined, Bob
|
||||
// should perform an RBF on his CPFP anchor sweeping tx. By the end of
|
||||
// this iteration, we expect Bob to use up his CPFP budget after one
|
||||
// more block.
|
||||
for i := uint32(1); i <= feeFuncWidth-1; i++ {
|
||||
// Mine an empty block. Since the sweeping tx is not confirmed,
|
||||
// Bob's fee bumper should increase its fees.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
// Bob should still have two pending sweeps,
|
||||
// - anchor sweeping from his local commitment.
|
||||
// - anchor sweeping from his remote commitment (invalid).
|
||||
ht.AssertNumPendingSweeps(bob, 2)
|
||||
|
||||
// Make sure Bob's old sweeping tx has been removed from the
|
||||
// mempool.
|
||||
ht.Miner.AssertTxNotInMempool(sweepTx.TxHash())
|
||||
|
||||
// We expect to see two txns in the mempool,
|
||||
// - Bob's force close tx.
|
||||
// - Bob's anchor sweep tx.
|
||||
ht.Miner.AssertNumTxsInMempool(2)
|
||||
|
||||
// We expect the fees to increase by i*delta.
|
||||
expectedFee := startFeeAnchor + feeDelta.MulF64(float64(i))
|
||||
expectedFeeRate := chainfee.NewSatPerKWeight(
|
||||
expectedFee, uint64(txWeight),
|
||||
)
|
||||
|
||||
// We should see Bob's anchor sweeping tx being fee bumped
|
||||
// since it's not confirmed, along with his force close tx.
|
||||
txns = ht.Miner.GetNumTxsFromMempool(2)
|
||||
|
||||
// Find the sweeping tx.
|
||||
sweepTx = ht.FindSweepingTxns(txns, 1, *closeTxid)[0]
|
||||
|
||||
// Calculate the fee rate of Bob's new sweeping tx.
|
||||
feeRate = uint64(ht.CalculateTxFeeRate(sweepTx))
|
||||
|
||||
// Calculate the fee of Bob's new sweeping tx.
|
||||
fee = uint64(ht.CalculateTxFee(sweepTx))
|
||||
|
||||
ht.Logf("Bob(position=%v): txWeight=%v, expected: [fee=%d, "+
|
||||
"feerate=%v], got: [fee=%v, feerate=%v]",
|
||||
feeFuncWidth-i, txWeight, expectedFee,
|
||||
expectedFeeRate, fee, feeRate)
|
||||
|
||||
// Assert Bob's tx has the expected fee and fee rate.
|
||||
require.InEpsilonf(ht, uint64(expectedFee), fee, 0.01,
|
||||
"deadline=%v, want %d, got %d", i, expectedFee, fee)
|
||||
require.InEpsilonf(ht, uint64(expectedFeeRate), feeRate, 0.01,
|
||||
"deadline=%v, want %d, got %d", i, expectedFeeRate,
|
||||
feeRate)
|
||||
}
|
||||
|
||||
// We now check the budget has been used up at the deadline-1 block.
|
||||
//
|
||||
// Once out of the above loop, we expect to be 2 blocks before the CPFP
|
||||
// deadline.
|
||||
_, currentHeight = ht.Miner.GetBestBlock()
|
||||
require.Equal(ht, int(anchorDeadline-2), int(currentHeight))
|
||||
|
||||
// Mine one more block, we'd use up all the CPFP budget.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
// Make sure Bob's old sweeping tx has been removed from the mempool.
|
||||
ht.Miner.AssertTxNotInMempool(sweepTx.TxHash())
|
||||
|
||||
// Get the last sweeping tx - we should see two txns here, Bob's anchor
|
||||
// sweeping tx and his force close tx.
|
||||
txns = ht.Miner.GetNumTxsFromMempool(2)
|
||||
|
||||
// Find the sweeping tx.
|
||||
sweepTx = ht.FindSweepingTxns(txns, 1, *closeTxid)[0]
|
||||
|
||||
// Calculate the fee of Bob's new sweeping tx.
|
||||
fee = uint64(ht.CalculateTxFee(sweepTx))
|
||||
|
||||
// Assert the budget is now used up.
|
||||
require.InEpsilonf(ht, uint64(cpfpBudget), fee, 0.01, "want %d, got %d",
|
||||
cpfpBudget, fee)
|
||||
|
||||
// Mine one more block. Since Bob's budget has been used up, there
|
||||
// won't be any more sweeping attempts. We now assert this by checking
|
||||
// that the sweeping tx stayed unchanged.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
// Get the current sweeping tx and assert it stays unchanged.
|
||||
//
|
||||
// We expect two txns here, one for the anchor sweeping, the other for
|
||||
// the force close tx.
|
||||
txns = ht.Miner.GetNumTxsFromMempool(2)
|
||||
|
||||
// Find the sweeping tx.
|
||||
currentSweepTx := ht.FindSweepingTxns(txns, 1, *closeTxid)[0]
|
||||
|
||||
// Assert the anchor sweep tx stays unchanged.
|
||||
require.Equal(ht, sweepTx.TxHash(), currentSweepTx.TxHash())
|
||||
require.Equal(ht, fee, currentFee)
|
||||
require.Equal(ht, feeRate, currentFeeRate)
|
||||
|
||||
// Mine a block to confirm Alice's sweeping and force close txns, this
|
||||
// is needed to clean up the mempool.
|
||||
// Mine a block to confirm Bob's sweeping and force close txns, this is
|
||||
// needed to clean up the mempool.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 2)
|
||||
|
||||
// The above mined block should confirm Alice's force close tx, and her
|
||||
// contractcourt will offer the HTLC to her sweeper. We are not testing
|
||||
// The above mined block should confirm Bob's force close tx, and his
|
||||
// contractcourt will offer the HTLC to his sweeper. We are not testing
|
||||
// the HTLC sweeping behaviors so we just perform a simple check and
|
||||
// exit the test.
|
||||
ht.AssertNumPendingSweeps(alice, 1)
|
||||
ht.AssertNumPendingSweeps(bob, 1)
|
||||
|
||||
// Finally, clean the mempool for the next test.
|
||||
ht.CleanShutDown()
|
||||
|
@ -497,9 +874,6 @@ func testSweepHTLCs(ht *lntest.HarnessTest) {
|
|||
))
|
||||
ht.MineBlocks(numBlocks)
|
||||
|
||||
// Bob force closes the channel.
|
||||
// ht.CloseChannelAssertPending(bob, bcChanPoint, true)
|
||||
|
||||
// Before we mine empty blocks to check the RBF behavior, we need to be
|
||||
// aware that Bob's incoming HTLC will expire before his outgoing HTLC
|
||||
// deadline is reached. This happens because the incoming HTLC is sent
|
||||
|
@ -567,7 +941,7 @@ func testSweepHTLCs(ht *lntest.HarnessTest) {
|
|||
// Now the start fee rate is checked, we can calculate the fee rate
|
||||
// delta.
|
||||
outgoingFeeRateDelta := (outgoingEndFeeRate - outgoingStartFeeRate) /
|
||||
chainfee.SatPerKWeight(outgoingHTLCDeadline)
|
||||
chainfee.SatPerKWeight(outgoingHTLCDeadline-1)
|
||||
|
||||
// outgoingFuncPosition records the position of Bob's fee function used
|
||||
// for his outgoing HTLC sweeping tx.
|
||||
|
@ -706,7 +1080,7 @@ func testSweepHTLCs(ht *lntest.HarnessTest) {
|
|||
// Now the start fee rate is checked, we can calculate the fee rate
|
||||
// delta.
|
||||
incomingFeeRateDelta := (incomingEndFeeRate - incomingStartFeeRate) /
|
||||
chainfee.SatPerKWeight(incomingHTLCDeadline)
|
||||
chainfee.SatPerKWeight(incomingHTLCDeadline-1)
|
||||
|
||||
// incomingFuncPosition records the position of Bob's fee function used
|
||||
// for his incoming HTLC sweeping tx.
|
||||
|
@ -766,7 +1140,10 @@ func testSweepHTLCs(ht *lntest.HarnessTest) {
|
|||
// We now mine enough blocks till we reach the end of the outgoing
|
||||
// HTLC's deadline. Along the way, we check the expected fee rates are
|
||||
// used for both incoming and outgoing HTLC sweeping txns.
|
||||
blocksLeft := outgoingHTLCDeadline - outgoingFuncPosition
|
||||
//
|
||||
// NOTE: We need to subtract 1 from the deadline as the budget must be
|
||||
// used up before the deadline.
|
||||
blocksLeft := outgoingHTLCDeadline - outgoingFuncPosition - 1
|
||||
for i := int32(0); i < blocksLeft; i++ {
|
||||
// Mine an empty block.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
@ -1041,7 +1418,7 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
|
|||
bobTxWeight := uint64(ht.CalculateTxWeight(bobSweepTx))
|
||||
bobEndingFeeRate := chainfee.NewSatPerKWeight(bobBudget, bobTxWeight)
|
||||
bobFeeRateDelta := (bobEndingFeeRate - bobStartFeeRate) /
|
||||
chainfee.SatPerKWeight(deadlineB)
|
||||
chainfee.SatPerKWeight(deadlineB-1)
|
||||
|
||||
// Mine an empty block, which should trigger Alice's contractcourt to
|
||||
// offer her commit output to the sweeper.
|
||||
|
@ -1173,7 +1550,7 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
|
|||
aliceTxWeight := uint64(ht.CalculateTxWeight(aliceSweepTx))
|
||||
aliceEndingFeeRate := sweep.DefaultMaxFeeRate.FeePerKWeight()
|
||||
aliceFeeRateDelta := (aliceEndingFeeRate - aliceStartingFeeRate) /
|
||||
chainfee.SatPerKWeight(deadlineA)
|
||||
chainfee.SatPerKWeight(deadlineA-1)
|
||||
|
||||
aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx)
|
||||
expectedFeeRateAlice := aliceStartingFeeRate +
|
||||
|
|
|
@ -82,7 +82,8 @@ flowchart LR
|
|||
deadline together. Inputs with the same deadline express the same time
|
||||
sensitivity so it makes sense to sweep them in the same transaction. Once
|
||||
grouped, inputs in each batch are sorted based on their budgets. The only
|
||||
exception is inputs with `ExclusiveGroup` flag set, which will be swept alone.
|
||||
exception is inputs with the `ExclusiveGroup` flag set, which will be swept
|
||||
alone.
|
||||
|
||||
Once the batching is finished, an `InputSet` is returned, which is an interface
|
||||
used to decide whether a wallet UTXO is needed or not when creating the
|
||||
|
@ -91,11 +92,10 @@ the sum of the output values from these inputs against the sum of their
|
|||
budgets - if the total budget cannot be covered, one or more wallet UTXOs are
|
||||
needed.
|
||||
|
||||
For instance, when anchor output is swept to perform a CPFP, one or more wallet
|
||||
UTXOs are likely to be used to meet the specified budget, which is also the
|
||||
case when sweeping second-level HTLC transactions. However, if the sweeping
|
||||
transaction also contains other to-be-swept inputs, a wallet UTXO is no longer
|
||||
needed if their values can cover the total budget.
|
||||
For instance, commitment and HTLC transactions usually have some proportion of
|
||||
their outputs timelocked, preventing them from being used to pay fees
|
||||
immediately. For these transactions, wallet UTXOs are often needed to get them
|
||||
confirmed in a timely manner.
|
||||
|
||||
#### `Bumper`
|
||||
|
||||
|
@ -144,7 +144,7 @@ initialized with:
|
|||
- a starting fee rate of 10 sat/vB, which is the result from calling
|
||||
`estimatesmartfee 1000`.
|
||||
- an ending fee rate of 400 sat/vB, which is the result of `200,000/500`.
|
||||
- a fee rate delta of 390 sat/kvB, which is the result of `(400 - 10) / 500 *
|
||||
- a fee rate delta of 390 sat/kvB, which is the result of `(400 - 10) / 1000 *
|
||||
1000`.
|
||||
|
||||
## Sweeping Outputs from a Force Close Transaction
|
||||
|
|
|
@ -1216,8 +1216,8 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input, changePkScript []byte,
|
|||
}
|
||||
}
|
||||
|
||||
log.Debugf("Created sweep tx %v for %v inputs", sweepTx.TxHash(),
|
||||
len(inputs))
|
||||
log.Debugf("Created sweep tx %v for inputs:\n%v", sweepTx.TxHash(),
|
||||
inputTypeSummary(inputs))
|
||||
|
||||
return sweepTx, txFee, nil
|
||||
}
|
||||
|
|
|
@ -86,11 +86,14 @@ type LinearFeeFunction struct {
|
|||
currentFeeRate chainfee.SatPerKWeight
|
||||
|
||||
// width is the number of blocks between the starting block height
|
||||
// and the deadline block height.
|
||||
// and the deadline block height minus one.
|
||||
//
|
||||
// NOTE: We do minus one from the conf target here because we want to
|
||||
// max out the budget before the deadline height is reached.
|
||||
width uint32
|
||||
|
||||
// position is the number of blocks between the starting block height
|
||||
// and the current block height.
|
||||
// position is the fee function's current position, given a width of w,
|
||||
// a valid position should lie in range [0, w].
|
||||
position uint32
|
||||
|
||||
// deltaFeeRate is the fee rate (msat/kw) increase per block.
|
||||
|
@ -116,10 +119,10 @@ func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight,
|
|||
startingFeeRate fn.Option[chainfee.SatPerKWeight]) (
|
||||
*LinearFeeFunction, error) {
|
||||
|
||||
// If the deadline has already been reached, there's nothing the fee
|
||||
// function can do. In this case, we'll use the max fee rate
|
||||
// immediately.
|
||||
if confTarget == 0 {
|
||||
// If the deadline is one block away or has already been reached,
|
||||
// there's nothing the fee function can do. In this case, we'll use the
|
||||
// max fee rate immediately.
|
||||
if confTarget <= 1 {
|
||||
return &LinearFeeFunction{
|
||||
startingFeeRate: maxFeeRate,
|
||||
endingFeeRate: maxFeeRate,
|
||||
|
@ -129,7 +132,7 @@ func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight,
|
|||
|
||||
l := &LinearFeeFunction{
|
||||
endingFeeRate: maxFeeRate,
|
||||
width: confTarget,
|
||||
width: confTarget - 1,
|
||||
estimator: estimator,
|
||||
}
|
||||
|
||||
|
@ -153,18 +156,18 @@ func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight,
|
|||
|
||||
// The starting and ending fee rates are in sat/kw, so we need to
|
||||
// convert them to msat/kw by multiplying by 1000.
|
||||
delta := btcutil.Amount(end - start).MulF64(1000 / float64(confTarget))
|
||||
delta := btcutil.Amount(end - start).MulF64(1000 / float64(l.width))
|
||||
l.deltaFeeRate = mSatPerKWeight(delta)
|
||||
|
||||
// We only allow the delta to be zero if the width is one - when the
|
||||
// delta is zero, it means the starting and ending fee rates are the
|
||||
// same, which means there's nothing to increase, so any width greater
|
||||
// than 1 doesn't provide any utility. This could happen when the
|
||||
// sweeper is offered to sweep an input that has passed its deadline.
|
||||
// budget is too small.
|
||||
if l.deltaFeeRate == 0 && l.width != 1 {
|
||||
log.Errorf("Failed to init fee function: startingFeeRate=%v, "+
|
||||
"endingFeeRate=%v, width=%v, delta=%v", start, end,
|
||||
confTarget, l.deltaFeeRate)
|
||||
l.width, l.deltaFeeRate)
|
||||
|
||||
return nil, fmt.Errorf("fee rate delta is zero")
|
||||
}
|
||||
|
@ -175,7 +178,7 @@ func NewLinearFeeFunction(maxFeeRate chainfee.SatPerKWeight,
|
|||
|
||||
log.Debugf("Linear fee function initialized with startingFeeRate=%v, "+
|
||||
"endingFeeRate=%v, width=%v, delta=%v", start, end,
|
||||
confTarget, l.deltaFeeRate)
|
||||
l.width, l.deltaFeeRate)
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
@ -211,12 +214,12 @@ func (l *LinearFeeFunction) IncreaseFeeRate(confTarget uint32) (bool, error) {
|
|||
newPosition := uint32(0)
|
||||
|
||||
// Only calculate the new position when the conf target is less than
|
||||
// the function's width - the width is the initial conf target, and we
|
||||
// expect the current conf target to decrease over time. However, we
|
||||
// the function's width - the width is the initial conf target-1, and
|
||||
// we expect the current conf target to decrease over time. However, we
|
||||
// still allow the supplied conf target to be greater than the width,
|
||||
// and we won't increase the fee rate in that case.
|
||||
if confTarget < l.width {
|
||||
newPosition = l.width - confTarget
|
||||
if confTarget < l.width+1 {
|
||||
newPosition = l.width + 1 - confTarget
|
||||
log.Tracef("Increasing position from %v to %v", l.position,
|
||||
newPosition)
|
||||
}
|
||||
|
@ -290,7 +293,7 @@ func (l *LinearFeeFunction) estimateFeeRate(
|
|||
// (1008), we will use the min relay fee instead.
|
||||
if confTarget >= chainfee.MaxBlockTarget {
|
||||
minFeeRate := l.estimator.RelayFeePerKW()
|
||||
log.Debugf("Conf target %v is greater than max block target, "+
|
||||
log.Infof("Conf target %v is greater than max block target, "+
|
||||
"using min relay fee rate %v", confTarget, minFeeRate)
|
||||
|
||||
return minFeeRate, nil
|
||||
|
|
|
@ -8,22 +8,20 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestLinearFeeFunctionNew tests the NewLinearFeeFunction function.
|
||||
func TestLinearFeeFunctionNew(t *testing.T) {
|
||||
// TestLinearFeeFunctionNewMaxFeeRateUsed tests when the conf target is <= 1,
|
||||
// the max fee rate is used.
|
||||
func TestLinearFeeFunctionNewMaxFeeRateUsed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := require.New(t)
|
||||
|
||||
// Create a mock fee estimator.
|
||||
estimator := &chainfee.MockEstimator{}
|
||||
defer estimator.AssertExpectations(t)
|
||||
|
||||
// Create testing params.
|
||||
maxFeeRate := chainfee.SatPerKWeight(10000)
|
||||
estimatedFeeRate := chainfee.SatPerKWeight(500)
|
||||
minRelayFeeRate := chainfee.SatPerKWeight(100)
|
||||
confTarget := uint32(6)
|
||||
noStartFeeRate := fn.None[chainfee.SatPerKWeight]()
|
||||
startFeeRate := chainfee.SatPerKWeight(1000)
|
||||
|
||||
// Assert init fee function with zero conf value will end up using the
|
||||
// max fee rate.
|
||||
|
@ -36,23 +34,56 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
rt.Equal(maxFeeRate, f.endingFeeRate)
|
||||
rt.Equal(maxFeeRate, f.currentFeeRate)
|
||||
|
||||
// When the fee estimator returns an error, it's returned.
|
||||
//
|
||||
// Mock the fee estimator to return an error.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
chainfee.SatPerKWeight(0), errDummy).Once()
|
||||
// Assert init fee function with conf of one will end up using the max
|
||||
// fee rate.
|
||||
f, err = NewLinearFeeFunction(maxFeeRate, 1, estimator, noStartFeeRate)
|
||||
rt.NoError(err)
|
||||
rt.NotNil(f)
|
||||
|
||||
f, err = NewLinearFeeFunction(
|
||||
maxFeeRate, confTarget, estimator, noStartFeeRate,
|
||||
)
|
||||
rt.ErrorIs(err, errDummy)
|
||||
rt.Nil(f)
|
||||
// Assert the internal state.
|
||||
rt.Equal(maxFeeRate, f.startingFeeRate)
|
||||
rt.Equal(maxFeeRate, f.endingFeeRate)
|
||||
rt.Equal(maxFeeRate, f.currentFeeRate)
|
||||
}
|
||||
|
||||
// When the starting feerate is greater than the ending feerate, the
|
||||
// starting feerate is capped.
|
||||
// TestLinearFeeFunctionNewZeroFeeRateDelta tests when the fee rate delta is
|
||||
// zero, it will return an error except when the width is one.
|
||||
func TestLinearFeeFunctionNewZeroFeeRateDelta(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := require.New(t)
|
||||
|
||||
// Create a mock fee estimator.
|
||||
estimator := &chainfee.MockEstimator{}
|
||||
defer estimator.AssertExpectations(t)
|
||||
|
||||
// Create testing params.
|
||||
maxFeeRate := chainfee.SatPerKWeight(10000)
|
||||
estimatedFeeRate := chainfee.SatPerKWeight(500)
|
||||
confTarget := uint32(6)
|
||||
noStartFeeRate := fn.None[chainfee.SatPerKWeight]()
|
||||
|
||||
// When the calculated fee rate delta is 0, an error should be returned
|
||||
// when the width is not one.
|
||||
//
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
smallConf := uint32(1)
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
// The starting fee rate is the max fee rate.
|
||||
maxFeeRate, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
|
||||
f, err := NewLinearFeeFunction(
|
||||
maxFeeRate, confTarget, estimator, noStartFeeRate,
|
||||
)
|
||||
rt.ErrorContains(err, "fee rate delta is zero")
|
||||
rt.Nil(f)
|
||||
|
||||
// When the calculated fee rate delta is 0, an error should NOT be
|
||||
// returned when the width is one, and the starting feerate is capped
|
||||
// at the max fee rate.
|
||||
//
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
smallConf := uint32(2)
|
||||
estimator.On("EstimateFeePerKW", smallConf).Return(
|
||||
// The fee rate is greater than the max fee rate.
|
||||
maxFeeRate+1, nil).Once()
|
||||
|
@ -64,18 +95,41 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
rt.NoError(err)
|
||||
rt.NotNil(f)
|
||||
|
||||
// When the calculated fee rate delta is 0, an error should be returned.
|
||||
//
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
// The starting fee rate is the max fee rate.
|
||||
maxFeeRate, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
// Assert the internal state.
|
||||
rt.Equal(maxFeeRate, f.startingFeeRate)
|
||||
rt.Equal(maxFeeRate, f.endingFeeRate)
|
||||
rt.Equal(maxFeeRate, f.currentFeeRate)
|
||||
rt.Zero(f.deltaFeeRate)
|
||||
rt.Equal(smallConf-1, f.width)
|
||||
}
|
||||
|
||||
f, err = NewLinearFeeFunction(
|
||||
// TestLinearFeeFunctionNewEsimator tests the NewLinearFeeFunction function
|
||||
// properly reacts to the response or error returned from the fee estimator.
|
||||
func TestLinearFeeFunctionNewEsimator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := require.New(t)
|
||||
|
||||
// Create a mock fee estimator.
|
||||
estimator := &chainfee.MockEstimator{}
|
||||
defer estimator.AssertExpectations(t)
|
||||
|
||||
// Create testing params.
|
||||
maxFeeRate := chainfee.SatPerKWeight(10000)
|
||||
minRelayFeeRate := chainfee.SatPerKWeight(100)
|
||||
confTarget := uint32(6)
|
||||
noStartFeeRate := fn.None[chainfee.SatPerKWeight]()
|
||||
|
||||
// When the fee estimator returns an error, it's returned.
|
||||
//
|
||||
// Mock the fee estimator to return an error.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
chainfee.SatPerKWeight(0), errDummy).Once()
|
||||
|
||||
f, err := NewLinearFeeFunction(
|
||||
maxFeeRate, confTarget, estimator, noStartFeeRate,
|
||||
)
|
||||
rt.ErrorContains(err, "fee rate delta is zero")
|
||||
rt.ErrorIs(err, errDummy)
|
||||
rt.Nil(f)
|
||||
|
||||
// When the conf target is >= 1008, the min relay fee should be used.
|
||||
|
@ -95,16 +149,36 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
rt.Equal(maxFeeRate, f.endingFeeRate)
|
||||
rt.Equal(minRelayFeeRate, f.currentFeeRate)
|
||||
rt.NotZero(f.deltaFeeRate)
|
||||
rt.Equal(largeConf, f.width)
|
||||
rt.Equal(largeConf-1, f.width)
|
||||
}
|
||||
|
||||
// TestLinearFeeFunctionNewSuccess tests we can create the fee function
|
||||
// successfully.
|
||||
func TestLinearFeeFunctionNewSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := require.New(t)
|
||||
|
||||
// Create a mock fee estimator.
|
||||
estimator := &chainfee.MockEstimator{}
|
||||
defer estimator.AssertExpectations(t)
|
||||
|
||||
// Create testing params.
|
||||
maxFeeRate := chainfee.SatPerKWeight(10000)
|
||||
estimatedFeeRate := chainfee.SatPerKWeight(500)
|
||||
minRelayFeeRate := chainfee.SatPerKWeight(100)
|
||||
confTarget := uint32(6)
|
||||
noStartFeeRate := fn.None[chainfee.SatPerKWeight]()
|
||||
startFeeRate := chainfee.SatPerKWeight(1000)
|
||||
|
||||
// Check a successfully created fee function.
|
||||
//
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
estimatedFeeRate, nil).Once()
|
||||
estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()
|
||||
estimator.On("RelayFeePerKW").Return(minRelayFeeRate).Once()
|
||||
|
||||
f, err = NewLinearFeeFunction(
|
||||
f, err := NewLinearFeeFunction(
|
||||
maxFeeRate, confTarget, estimator, noStartFeeRate,
|
||||
)
|
||||
rt.NoError(err)
|
||||
|
@ -115,7 +189,7 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
rt.Equal(maxFeeRate, f.endingFeeRate)
|
||||
rt.Equal(estimatedFeeRate, f.currentFeeRate)
|
||||
rt.NotZero(f.deltaFeeRate)
|
||||
rt.Equal(confTarget, f.width)
|
||||
rt.Equal(confTarget-1, f.width)
|
||||
|
||||
// Check a successfully created fee function using the specified
|
||||
// starting fee rate.
|
||||
|
@ -131,7 +205,10 @@ func TestLinearFeeFunctionNew(t *testing.T) {
|
|||
|
||||
// Assert the customized starting fee rate is used.
|
||||
rt.Equal(startFeeRate, f.startingFeeRate)
|
||||
rt.Equal(maxFeeRate, f.endingFeeRate)
|
||||
rt.Equal(startFeeRate, f.currentFeeRate)
|
||||
rt.NotZero(f.deltaFeeRate)
|
||||
rt.Equal(confTarget-1, f.width)
|
||||
}
|
||||
|
||||
// TestLinearFeeFunctionFeeRateAtPosition checks the expected feerate is
|
||||
|
@ -201,12 +278,13 @@ func TestLinearFeeFunctionIncrement(t *testing.T) {
|
|||
|
||||
// Create a mock fee estimator.
|
||||
estimator := &chainfee.MockEstimator{}
|
||||
defer estimator.AssertExpectations(t)
|
||||
|
||||
// Create testing params. These params are chosen so the delta value is
|
||||
// 100.
|
||||
maxFeeRate := chainfee.SatPerKWeight(1000)
|
||||
maxFeeRate := chainfee.SatPerKWeight(900)
|
||||
estimatedFeeRate := chainfee.SatPerKWeight(100)
|
||||
confTarget := uint32(9)
|
||||
confTarget := uint32(9) // This means the width is 8.
|
||||
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
|
@ -219,8 +297,8 @@ func TestLinearFeeFunctionIncrement(t *testing.T) {
|
|||
)
|
||||
rt.NoError(err)
|
||||
|
||||
// We now increase the position from 1 to 9.
|
||||
for i := uint32(1); i <= confTarget; i++ {
|
||||
// We now increase the position from 1 to 8.
|
||||
for i := uint32(1); i <= confTarget-1; i++ {
|
||||
// Increase the fee rate.
|
||||
increased, err := f.Increment()
|
||||
rt.NoError(err)
|
||||
|
@ -236,7 +314,7 @@ func TestLinearFeeFunctionIncrement(t *testing.T) {
|
|||
rt.Equal(estimatedFeeRate+delta, f.FeeRate())
|
||||
}
|
||||
|
||||
// Now the position is at 9th, increase it again should give us an
|
||||
// Now the position is at 8th, increase it again should give us an
|
||||
// error.
|
||||
increased, err := f.Increment()
|
||||
rt.ErrorIs(err, ErrMaxPosition)
|
||||
|
@ -252,12 +330,13 @@ func TestLinearFeeFunctionIncreaseFeeRate(t *testing.T) {
|
|||
|
||||
// Create a mock fee estimator.
|
||||
estimator := &chainfee.MockEstimator{}
|
||||
defer estimator.AssertExpectations(t)
|
||||
|
||||
// Create testing params. These params are chosen so the delta value is
|
||||
// 100.
|
||||
maxFeeRate := chainfee.SatPerKWeight(1000)
|
||||
maxFeeRate := chainfee.SatPerKWeight(900)
|
||||
estimatedFeeRate := chainfee.SatPerKWeight(100)
|
||||
confTarget := uint32(9)
|
||||
confTarget := uint32(9) // This means the width is 8.
|
||||
|
||||
// Mock the fee estimator to return the fee rate.
|
||||
estimator.On("EstimateFeePerKW", confTarget).Return(
|
||||
|
@ -281,9 +360,9 @@ func TestLinearFeeFunctionIncreaseFeeRate(t *testing.T) {
|
|||
rt.NoError(err)
|
||||
rt.False(increased)
|
||||
|
||||
// We now increase the fee rate from conf target 8 to 1 and assert we
|
||||
// We now increase the fee rate from conf target 8 to 2 and assert we
|
||||
// get no error and true.
|
||||
for i := uint32(1); i < confTarget; i++ {
|
||||
for i := uint32(1); i < confTarget-1; i++ {
|
||||
// Increase the fee rate.
|
||||
increased, err := f.IncreaseFeeRate(confTarget - i)
|
||||
rt.NoError(err)
|
||||
|
@ -299,11 +378,17 @@ func TestLinearFeeFunctionIncreaseFeeRate(t *testing.T) {
|
|||
rt.Equal(estimatedFeeRate+delta, f.FeeRate())
|
||||
}
|
||||
|
||||
// Test that when we use a conf target of 0, we get the ending fee
|
||||
// Test that when we use a conf target of 1, we get the ending fee
|
||||
// rate.
|
||||
increased, err = f.IncreaseFeeRate(0)
|
||||
increased, err = f.IncreaseFeeRate(1)
|
||||
rt.NoError(err)
|
||||
rt.True(increased)
|
||||
rt.Equal(confTarget, f.position)
|
||||
rt.Equal(confTarget-1, f.position)
|
||||
rt.Equal(maxFeeRate, f.currentFeeRate)
|
||||
|
||||
// Test that when we use a conf target of 0, ErrMaxPosition is
|
||||
// returned.
|
||||
increased, err = f.IncreaseFeeRate(0)
|
||||
rt.ErrorIs(err, ErrMaxPosition)
|
||||
rt.False(increased)
|
||||
}
|
||||
|
|
|
@ -767,11 +767,11 @@ func (s *UtxoSweeper) signalResult(pi *SweeperInput, result Result) {
|
|||
listeners := pi.listeners
|
||||
|
||||
if result.Err == nil {
|
||||
log.Debugf("Dispatching sweep success for %v to %v listeners",
|
||||
log.Tracef("Dispatching sweep success for %v to %v listeners",
|
||||
op, len(listeners),
|
||||
)
|
||||
} else {
|
||||
log.Debugf("Dispatching sweep error for %v to %v listeners: %v",
|
||||
log.Tracef("Dispatching sweep error for %v to %v listeners: %v",
|
||||
op, len(listeners), result.Err,
|
||||
)
|
||||
}
|
||||
|
@ -830,6 +830,9 @@ func (s *UtxoSweeper) sweep(set InputSet) error {
|
|||
outpoints[i] = inp.OutPoint()
|
||||
}
|
||||
|
||||
log.Errorf("Initial broadcast failed: %v, inputs=\n%v", err,
|
||||
inputTypeSummary(set.Inputs()))
|
||||
|
||||
// TODO(yy): find out which input is causing the failure.
|
||||
s.markInputsPublishFailed(outpoints)
|
||||
|
||||
|
@ -855,7 +858,7 @@ func (s *UtxoSweeper) markInputsPendingPublish(set InputSet) {
|
|||
// It could be that this input is an additional wallet
|
||||
// input that was attached. In that case there also
|
||||
// isn't a pending input to update.
|
||||
log.Debugf("Skipped marking input as pending "+
|
||||
log.Tracef("Skipped marking input as pending "+
|
||||
"published: %v not found in pending inputs",
|
||||
input.OutPoint())
|
||||
|
||||
|
@ -904,7 +907,7 @@ func (s *UtxoSweeper) markInputsPublished(tr *TxRecord,
|
|||
// It could be that this input is an additional wallet
|
||||
// input that was attached. In that case there also
|
||||
// isn't a pending input to update.
|
||||
log.Debugf("Skipped marking input as published: %v "+
|
||||
log.Tracef("Skipped marking input as published: %v "+
|
||||
"not found in pending inputs",
|
||||
input.PreviousOutPoint)
|
||||
|
||||
|
@ -940,7 +943,7 @@ func (s *UtxoSweeper) markInputsPublishFailed(outpoints []wire.OutPoint) {
|
|||
// It could be that this input is an additional wallet
|
||||
// input that was attached. In that case there also
|
||||
// isn't a pending input to update.
|
||||
log.Debugf("Skipped marking input as publish failed: "+
|
||||
log.Tracef("Skipped marking input as publish failed: "+
|
||||
"%v not found in pending inputs", op)
|
||||
|
||||
continue
|
||||
|
@ -948,7 +951,7 @@ func (s *UtxoSweeper) markInputsPublishFailed(outpoints []wire.OutPoint) {
|
|||
|
||||
// Valdiate that the input is in an expected state.
|
||||
if pi.state != PendingPublish && pi.state != Published {
|
||||
log.Errorf("Expect input %v to have %v, instead it "+
|
||||
log.Debugf("Expect input %v to have %v, instead it "+
|
||||
"has %v", op, PendingPublish, pi.state)
|
||||
|
||||
continue
|
||||
|
@ -1397,7 +1400,7 @@ func (s *UtxoSweeper) markInputsSwept(tx *wire.MsgTx, isOurTx bool) {
|
|||
if !ok {
|
||||
// It's very likely that a spending tx contains inputs
|
||||
// that we don't know.
|
||||
log.Debugf("Skipped marking input as swept: %v not "+
|
||||
log.Tracef("Skipped marking input as swept: %v not "+
|
||||
"found in pending inputs", outpoint)
|
||||
|
||||
continue
|
||||
|
|
|
@ -206,7 +206,7 @@ func TestMarkInputsPublishFailed(t *testing.T) {
|
|||
Store: mockStore,
|
||||
})
|
||||
|
||||
// Create three testing inputs.
|
||||
// Create testing inputs for each state.
|
||||
//
|
||||
// inputNotExist specifies an input that's not found in the sweeper's
|
||||
// `inputs` map.
|
||||
|
@ -240,18 +240,52 @@ func TestMarkInputsPublishFailed(t *testing.T) {
|
|||
state: Published,
|
||||
}
|
||||
|
||||
// inputPublishFailed specifies an input that's failed to be published.
|
||||
inputPublishFailed := &wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{Index: 5},
|
||||
}
|
||||
s.inputs[inputPublishFailed.PreviousOutPoint] = &SweeperInput{
|
||||
state: PublishFailed,
|
||||
}
|
||||
|
||||
// inputSwept specifies an input that's swept.
|
||||
inputSwept := &wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{Index: 6},
|
||||
}
|
||||
s.inputs[inputSwept.PreviousOutPoint] = &SweeperInput{
|
||||
state: Swept,
|
||||
}
|
||||
|
||||
// inputExcluded specifies an input that's excluded.
|
||||
inputExcluded := &wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{Index: 7},
|
||||
}
|
||||
s.inputs[inputExcluded.PreviousOutPoint] = &SweeperInput{
|
||||
state: Excluded,
|
||||
}
|
||||
|
||||
// inputFailed specifies an input that's failed.
|
||||
inputFailed := &wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{Index: 8},
|
||||
}
|
||||
s.inputs[inputFailed.PreviousOutPoint] = &SweeperInput{
|
||||
state: Failed,
|
||||
}
|
||||
|
||||
// Gather all inputs' outpoints.
|
||||
pendingOps := make([]wire.OutPoint, 0, len(s.inputs)+1)
|
||||
for op := range s.inputs {
|
||||
pendingOps = append(pendingOps, op)
|
||||
}
|
||||
pendingOps = append(pendingOps, inputNotExist.PreviousOutPoint)
|
||||
|
||||
// Mark the test inputs. We expect the non-exist input and the
|
||||
// inputInit to be skipped, and the final input to be marked as
|
||||
// published.
|
||||
s.markInputsPublishFailed([]wire.OutPoint{
|
||||
inputNotExist.PreviousOutPoint,
|
||||
inputInit.PreviousOutPoint,
|
||||
inputPendingPublish.PreviousOutPoint,
|
||||
inputPublished.PreviousOutPoint,
|
||||
})
|
||||
s.markInputsPublishFailed(pendingOps)
|
||||
|
||||
// We expect unchanged number of pending inputs.
|
||||
require.Len(s.inputs, 3)
|
||||
require.Len(s.inputs, 7)
|
||||
|
||||
// We expect the init input's state to stay unchanged.
|
||||
require.Equal(Init,
|
||||
|
@ -266,6 +300,19 @@ func TestMarkInputsPublishFailed(t *testing.T) {
|
|||
require.Equal(PublishFailed,
|
||||
s.inputs[inputPublished.PreviousOutPoint].state)
|
||||
|
||||
// We expect the publish failed input to stay unchanged.
|
||||
require.Equal(PublishFailed,
|
||||
s.inputs[inputPublishFailed.PreviousOutPoint].state)
|
||||
|
||||
// We expect the swept input to stay unchanged.
|
||||
require.Equal(Swept, s.inputs[inputSwept.PreviousOutPoint].state)
|
||||
|
||||
// We expect the excluded input to stay unchanged.
|
||||
require.Equal(Excluded, s.inputs[inputExcluded.PreviousOutPoint].state)
|
||||
|
||||
// We expect the failed input to stay unchanged.
|
||||
require.Equal(Failed, s.inputs[inputFailed.PreviousOutPoint].state)
|
||||
|
||||
// Assert mocked statements are executed as expected.
|
||||
mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
@ -648,12 +695,12 @@ func TestSweepPendingInputs(t *testing.T) {
|
|||
|
||||
// Mock the methods used in `sweep`. This is not important for this
|
||||
// unit test.
|
||||
setNeedWallet.On("Inputs").Return(nil).Times(4)
|
||||
setNeedWallet.On("Inputs").Return(nil).Maybe()
|
||||
setNeedWallet.On("DeadlineHeight").Return(testHeight).Once()
|
||||
setNeedWallet.On("Budget").Return(btcutil.Amount(1)).Once()
|
||||
setNeedWallet.On("StartingFeeRate").Return(
|
||||
fn.None[chainfee.SatPerKWeight]()).Once()
|
||||
normalSet.On("Inputs").Return(nil).Times(4)
|
||||
normalSet.On("Inputs").Return(nil).Maybe()
|
||||
normalSet.On("DeadlineHeight").Return(testHeight).Once()
|
||||
normalSet.On("Budget").Return(btcutil.Amount(1)).Once()
|
||||
normalSet.On("StartingFeeRate").Return(
|
||||
|
|
|
@ -241,13 +241,13 @@ func (b *BudgetInputSet) NeedWalletInput() bool {
|
|||
// If the input's budget is not even covered by itself, we need
|
||||
// to borrow outputs from other inputs.
|
||||
if budgetBorrowable < 0 {
|
||||
log.Debugf("Input %v specified a budget that exceeds "+
|
||||
log.Tracef("Input %v specified a budget that exceeds "+
|
||||
"its output value: %v > %v", inp, budget,
|
||||
output)
|
||||
}
|
||||
}
|
||||
|
||||
log.Tracef("NeedWalletInput: budgetNeeded=%v, budgetBorrowable=%v",
|
||||
log.Debugf("NeedWalletInput: budgetNeeded=%v, budgetBorrowable=%v",
|
||||
budgetNeeded, budgetBorrowable)
|
||||
|
||||
// If we don't have enough extra budget to borrow, we need wallet
|
||||
|
@ -314,6 +314,8 @@ func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error {
|
|||
}
|
||||
b.addInput(pi)
|
||||
|
||||
log.Debugf("Added wallet input %v to input set", pi)
|
||||
|
||||
// Return if we've reached the minimum output amount.
|
||||
if !b.NeedWalletInput() {
|
||||
return nil
|
||||
|
|
Loading…
Add table
Reference in a new issue