Merge pull request #8751 from yyforyongyu/fix-sweeper-18

contractcourt+sweep: fix fee function and deadline issue
This commit is contained in:
Olaoluwa Osuntokun 2024-05-20 18:11:26 -07:00 committed by GitHub
commit e1c5fe2f9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 973 additions and 387 deletions

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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:

View file

@ -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],
)
}

View file

@ -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",

View file

@ -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)

View file

@ -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)

View file

@ -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 +

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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)
}

View file

@ -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

View file

@ -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(

View file

@ -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