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