mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-13 12:32:48 +02:00
Revert "Merge pull request #10510 from lightningnetwork/backport-10331-to-v0.20.x-branch"
This reverts commit40d8b475b5, reversing changes made to0b9e8c33e1.
This commit is contained in:
parent
706ec5e136
commit
1495920bb9
30 changed files with 119 additions and 2406 deletions
|
|
@ -230,12 +230,6 @@ type ChainArbitratorConfig struct {
|
|||
// AuxResolver is an optional interface that can be used to modify the
|
||||
// way contracts are resolved.
|
||||
AuxResolver fn.Option[lnwallet.AuxContractResolver]
|
||||
|
||||
// ChannelCloseConfs is an optional override for the number of
|
||||
// confirmations required for channel closes. When set, this overrides
|
||||
// the normal capacity-based scaling. This is only available in
|
||||
// dev/integration builds for testing purposes.
|
||||
ChannelCloseConfs fn.Option[uint32]
|
||||
}
|
||||
|
||||
// ChainArbitrator is a sub-system that oversees the on-chain resolution of all
|
||||
|
|
@ -1144,7 +1138,6 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error
|
|||
extractStateNumHint: lnwallet.GetStateNumHint,
|
||||
auxLeafStore: c.cfg.AuxLeafStore,
|
||||
auxResolver: c.cfg.AuxResolver,
|
||||
chanCloseConfs: c.cfg.ChannelCloseConfs,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -1322,7 +1315,6 @@ func (c *ChainArbitrator) loadOpenChannels() error {
|
|||
extractStateNumHint: lnwallet.GetStateNumHint,
|
||||
auxLeafStore: c.cfg.AuxLeafStore,
|
||||
auxResolver: c.cfg.AuxResolver,
|
||||
chanCloseConfs: c.cfg.ChannelCloseConfs,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -88,38 +88,6 @@ type BreachCloseInfo struct {
|
|||
CloseSummary channeldb.ChannelCloseSummary
|
||||
}
|
||||
|
||||
// spendConfirmationState represents the state of spend confirmation tracking
|
||||
// in the closeObserver state machine. We wait for N confirmations before
|
||||
// processing any spend to protect against shallow reorgs.
|
||||
type spendConfirmationState uint8
|
||||
|
||||
const (
|
||||
// spendStateNone indicates no spend has been detected yet.
|
||||
spendStateNone spendConfirmationState = iota
|
||||
|
||||
// spendStatePending indicates a spend has been detected and we're
|
||||
// waiting for the required number of confirmations.
|
||||
spendStatePending
|
||||
|
||||
// spendStateConfirmed indicates the spend has reached the required
|
||||
// confirmations and has been processed.
|
||||
spendStateConfirmed
|
||||
)
|
||||
|
||||
// String returns a human-readable representation of the state.
|
||||
func (s spendConfirmationState) String() string {
|
||||
switch s {
|
||||
case spendStateNone:
|
||||
return "None"
|
||||
case spendStatePending:
|
||||
return "Pending"
|
||||
case spendStateConfirmed:
|
||||
return "Confirmed"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// CommitSet is a collection of the set of known valid commitments at a given
|
||||
// instant. If ConfCommitKey is set, then the commitment identified by the
|
||||
// HtlcSetKey has hit the chain. This struct will be used to examine all live
|
||||
|
|
@ -261,12 +229,6 @@ type chainWatcherConfig struct {
|
|||
|
||||
// auxResolver is used to supplement contract resolution.
|
||||
auxResolver fn.Option[lnwallet.AuxContractResolver]
|
||||
|
||||
// chanCloseConfs is an optional override for the number of
|
||||
// confirmations required for channel closes. When set, this overrides
|
||||
// the normal capacity-based scaling. This is only available in
|
||||
// dev/integration builds for testing purposes.
|
||||
chanCloseConfs fn.Option[uint32]
|
||||
}
|
||||
|
||||
// chainWatcher is a system that's assigned to every active channel. The duty
|
||||
|
|
@ -683,263 +645,52 @@ func newChainSet(chanState *channeldb.OpenChannel) (*chainSet, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// spendProcessResult holds the results of processing a detected spend.
|
||||
type spendProcessResult struct {
|
||||
// pendingSpend is the spend to track (nil if fast-path was used).
|
||||
pendingSpend *chainntnfs.SpendDetail
|
||||
|
||||
// confNtfn is the confirmation notification (nil if fast-path or
|
||||
// error).
|
||||
confNtfn *chainntnfs.ConfirmationEvent
|
||||
}
|
||||
|
||||
// processDetectedSpend handles a newly detected spend from either blockbeat or
|
||||
// spend notification. It determines whether to use the fast-path (single conf)
|
||||
// or async-path (multiple confs), and returns the updated state.
|
||||
//
|
||||
// For single-confirmation mode (numConfs == 1), it immediately dispatches the
|
||||
// close event and returns empty result. For multi-confirmation mode, it
|
||||
// registers for confirmations and returns the new pending state.
|
||||
func (c *chainWatcher) processDetectedSpend(
|
||||
spend *chainntnfs.SpendDetail, source string,
|
||||
currentPendingSpend *chainntnfs.SpendDetail,
|
||||
currentConfNtfn *chainntnfs.ConfirmationEvent) spendProcessResult {
|
||||
|
||||
// FAST PATH: Single confirmation mode dispatches immediately.
|
||||
if c.handleSpendDispatch(spend, source) {
|
||||
if currentConfNtfn != nil {
|
||||
currentConfNtfn.Cancel()
|
||||
}
|
||||
|
||||
return spendProcessResult{}
|
||||
}
|
||||
|
||||
// ASYNC PATH: Multiple confirmations (production).
|
||||
// STATE TRANSITION: None -> Pending.
|
||||
log.Infof("ChannelPoint(%v): detected spend from %s, "+
|
||||
"transitioning to %v", c.cfg.chanState.FundingOutpoint,
|
||||
source, spendStatePending)
|
||||
|
||||
// Check for duplicate spend detection.
|
||||
if currentPendingSpend != nil {
|
||||
if *currentPendingSpend.SpenderTxHash == *spend.SpenderTxHash {
|
||||
log.Debugf("ChannelPoint(%v): ignoring duplicate "+
|
||||
"spend detection for tx %v",
|
||||
c.cfg.chanState.FundingOutpoint,
|
||||
spend.SpenderTxHash)
|
||||
|
||||
return spendProcessResult{
|
||||
pendingSpend: currentPendingSpend,
|
||||
confNtfn: currentConfNtfn,
|
||||
}
|
||||
}
|
||||
|
||||
// Different spend detected. Cancel existing confNtfn.
|
||||
log.Warnf("ChannelPoint(%v): detected different spend tx %v, "+
|
||||
"replacing pending tx %v",
|
||||
c.cfg.chanState.FundingOutpoint,
|
||||
spend.SpenderTxHash, currentPendingSpend.SpenderTxHash)
|
||||
|
||||
if currentConfNtfn != nil {
|
||||
currentConfNtfn.Cancel()
|
||||
}
|
||||
}
|
||||
|
||||
numConfs := c.requiredConfsForSpend()
|
||||
txid := spend.SpenderTxHash
|
||||
|
||||
newConfNtfn, err := c.cfg.notifier.RegisterConfirmationsNtfn(
|
||||
txid, spend.SpendingTx.TxOut[0].PkScript, numConfs,
|
||||
uint32(spend.SpendingHeight),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to register confirmations: %v", err)
|
||||
|
||||
return spendProcessResult{
|
||||
pendingSpend: currentPendingSpend,
|
||||
confNtfn: currentConfNtfn,
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("ChannelPoint(%v): waiting for %d confirmations of "+
|
||||
"spend tx %v", c.cfg.chanState.FundingOutpoint, numConfs, txid)
|
||||
|
||||
return spendProcessResult{
|
||||
pendingSpend: spend,
|
||||
confNtfn: newConfNtfn,
|
||||
}
|
||||
}
|
||||
|
||||
// closeObserver is a dedicated goroutine that will watch for any closes of the
|
||||
// channel that it's watching on chain. It implements a state machine to handle
|
||||
// spend detection and confirmation with reorg protection. The states are:
|
||||
//
|
||||
// - None (confNtfn == nil): No spend detected yet, waiting for spend
|
||||
// notification
|
||||
//
|
||||
// - Pending (confNtfn != nil): Spend detected, waiting for N confirmations
|
||||
//
|
||||
// - Confirmed: Spend confirmed with N blocks, close has been processed
|
||||
//
|
||||
// For single-confirmation scenarios (numConfs == 1), we bypass the async state
|
||||
// machine and immediately dispatch close events upon spend detection. This
|
||||
// provides synchronous behavior for integration tests which expect immediate
|
||||
// notifications. For multi-confirmation scenarios (production with numConfs
|
||||
// >= 3), we use the full async state machine with reorg protection.
|
||||
// channel that it's watching on chain. In the event of an on-chain event, the
|
||||
// close observer will assembled the proper materials required to claim the
|
||||
// funds of the channel on-chain (if required), then dispatch these as
|
||||
// notifications to all subscribers.
|
||||
func (c *chainWatcher) closeObserver() {
|
||||
defer c.wg.Done()
|
||||
|
||||
registerForSpend := func() (*chainntnfs.SpendEvent, error) {
|
||||
fundingPkScript, err := deriveFundingPkScript(c.cfg.chanState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
heightHint := c.cfg.chanState.DeriveHeightHint()
|
||||
|
||||
return c.cfg.notifier.RegisterSpendNtfn(
|
||||
&c.cfg.chanState.FundingOutpoint,
|
||||
fundingPkScript,
|
||||
heightHint,
|
||||
)
|
||||
}
|
||||
|
||||
spendNtfn := c.fundingSpendNtfn
|
||||
defer func() { spendNtfn.Cancel() }()
|
||||
|
||||
// We use these variables to implement a state machine to track the
|
||||
// state of the spend confirmation process:
|
||||
// * When confNtfn is nil, we're in state "None" waiting for a spend.
|
||||
// * When confNtfn is set, we're in state "Pending" waiting for
|
||||
// confirmations.
|
||||
//
|
||||
// After confirmations, we transition to state "Confirmed" and clean up.
|
||||
var (
|
||||
pendingSpend *chainntnfs.SpendDetail
|
||||
confNtfn *chainntnfs.ConfirmationEvent
|
||||
)
|
||||
defer c.fundingSpendNtfn.Cancel()
|
||||
|
||||
log.Infof("Close observer for ChannelPoint(%v) active",
|
||||
c.cfg.chanState.FundingOutpoint)
|
||||
|
||||
for {
|
||||
// We only listen to confirmation channels when we have a
|
||||
// pending spend. By setting these to nil when not needed, Go's
|
||||
// select ignores those cases, effectively implementing our
|
||||
// state machine.
|
||||
var (
|
||||
confChan <-chan *chainntnfs.TxConfirmation
|
||||
negativeConfChan <-chan int32
|
||||
)
|
||||
if confNtfn != nil {
|
||||
confChan = confNtfn.Confirmed
|
||||
negativeConfChan = confNtfn.NegativeConf
|
||||
}
|
||||
|
||||
select {
|
||||
// A new block beat has just arrived, we'll handle the block
|
||||
// beat, and see if it contains the spend of our funding
|
||||
// transaction or not.
|
||||
// A new block is received, we will check whether this block
|
||||
// contains a spending tx that we are interested in.
|
||||
case beat := <-c.BlockbeatChan:
|
||||
log.Debugf("ChainWatcher(%v) received blockbeat %v",
|
||||
c.cfg.chanState.FundingOutpoint, beat.Height())
|
||||
|
||||
spend := c.handleBlockbeat(beat)
|
||||
if spend == nil {
|
||||
continue
|
||||
}
|
||||
// Process the block.
|
||||
c.handleBlockbeat(beat)
|
||||
|
||||
result := c.processDetectedSpend(
|
||||
spend, "blockbeat", pendingSpend, confNtfn,
|
||||
)
|
||||
|
||||
pendingSpend = result.pendingSpend
|
||||
confNtfn = result.confNtfn
|
||||
|
||||
// A direct spend was just detected, we'll process the new spend
|
||||
// then see if we need to dispatch instantly, or wait around for
|
||||
// additional confirmations.
|
||||
case spend, ok := <-spendNtfn.Spend:
|
||||
// If the funding outpoint is spent, we now go ahead and handle
|
||||
// it. Note that we cannot rely solely on the `block` event
|
||||
// above to trigger a close event, as deep down, the receiving
|
||||
// of block notifications and the receiving of spending
|
||||
// notifications are done in two different goroutines, so the
|
||||
// expected order: [receive block -> receive spend] is not
|
||||
// guaranteed .
|
||||
case spend, ok := <-c.fundingSpendNtfn.Spend:
|
||||
// If the channel was closed, then this means that the
|
||||
// notifier exited, so we will as well.
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
result := c.processDetectedSpend(
|
||||
spend, "spend notification", pendingSpend,
|
||||
confNtfn,
|
||||
)
|
||||
|
||||
pendingSpend = result.pendingSpend
|
||||
confNtfn = result.confNtfn
|
||||
|
||||
// The spend has reached required confirmations. It's now safe
|
||||
// to process since we've protected against shallow reorgs.
|
||||
//
|
||||
// * STATE TRANSITION: Pending -> Confirmed
|
||||
case conf, ok := <-confChan:
|
||||
if !ok {
|
||||
log.Errorf("Confirmation channel closed " +
|
||||
"unexpectedly")
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("ChannelPoint(%v): spend confirmed at "+
|
||||
"height %d, transitioning to %v",
|
||||
c.cfg.chanState.FundingOutpoint,
|
||||
conf.BlockHeight, spendStateConfirmed)
|
||||
|
||||
err := c.handleCommitSpend(pendingSpend)
|
||||
err := c.handleCommitSpend(spend)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to handle confirmed "+
|
||||
"spend: %v", err)
|
||||
log.Errorf("Failed to handle commit spend: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
confNtfn.Cancel()
|
||||
confNtfn = nil
|
||||
pendingSpend = nil
|
||||
|
||||
// A reorg removed the spend tx. We reset to initial state and
|
||||
// wait for ANY new spend (could be the same tx re-mined, or a
|
||||
// different tx like an RBF replacement).
|
||||
//
|
||||
// * STATE TRANSITION: Pending -> None
|
||||
case reorgDepth, ok := <-negativeConfChan:
|
||||
if !ok {
|
||||
log.Errorf("Negative conf channel closed " +
|
||||
"unexpectedly")
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("ChannelPoint(%v): spend reorged out at "+
|
||||
"depth %d, transitioning back to %v",
|
||||
c.cfg.chanState.FundingOutpoint, reorgDepth,
|
||||
spendStateNone)
|
||||
|
||||
confNtfn.Cancel()
|
||||
confNtfn = nil
|
||||
pendingSpend = nil
|
||||
|
||||
spendNtfn.Cancel()
|
||||
var err error
|
||||
spendNtfn, err = registerForSpend()
|
||||
if err != nil {
|
||||
log.Errorf("Unable to re-register for "+
|
||||
"spend: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.fundingSpendNtfn = spendNtfn
|
||||
|
||||
log.Infof("ChannelPoint(%v): re-registered for spend "+
|
||||
"detection", c.cfg.chanState.FundingOutpoint)
|
||||
|
||||
// The chainWatcher has been signalled to exit, so we'll do so
|
||||
// now.
|
||||
case <-c.quit:
|
||||
if confNtfn != nil {
|
||||
confNtfn.Cancel()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -1235,18 +986,6 @@ func (c *chainWatcher) toSelfAmount(tx *wire.MsgTx) btcutil.Amount {
|
|||
return btcutil.Amount(fn.Sum(vals))
|
||||
}
|
||||
|
||||
// requiredConfsForSpend determines the number of confirmations required before
|
||||
// processing a spend of the funding output. Uses config override if set
|
||||
// (typically for testing), otherwise scales with channel capacity to balance
|
||||
// security vs user experience for channels of different sizes.
|
||||
func (c *chainWatcher) requiredConfsForSpend() uint32 {
|
||||
return c.cfg.chanCloseConfs.UnwrapOrFunc(func() uint32 {
|
||||
return lnwallet.CloseConfsForCapacity(
|
||||
c.cfg.chanState.Capacity,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// dispatchCooperativeClose processed a detect cooperative channel closure.
|
||||
// We'll use the spending transaction to locate our output within the
|
||||
// transaction, then clean up the database state. We'll also dispatch a
|
||||
|
|
@ -1264,8 +1003,8 @@ func (c *chainWatcher) dispatchCooperativeClose(commitSpend *chainntnfs.SpendDet
|
|||
localAmt := c.toSelfAmount(broadcastTx)
|
||||
|
||||
// Once this is known, we'll mark the state as fully closed in the
|
||||
// database. For cooperative closes, we wait for a confirmation depth
|
||||
// determined by channel capacity before dispatching this event.
|
||||
// database. We can do this as a cooperatively closed channel has all
|
||||
// its outputs resolved after only one confirmation.
|
||||
closeSummary := &channeldb.ChannelCloseSummary{
|
||||
ChanPoint: c.cfg.chanState.FundingOutpoint,
|
||||
ChainHash: c.cfg.chanState.ChainHash,
|
||||
|
|
@ -1620,30 +1359,6 @@ func deriveFundingPkScript(chanState *channeldb.OpenChannel) ([]byte, error) {
|
|||
return fundingPkScript, nil
|
||||
}
|
||||
|
||||
// handleSpendDispatch processes a detected spend. For single-confirmation
|
||||
// scenarios (numConfs == 1), it immediately dispatches the close event and
|
||||
// returns true. For multi-confirmation scenarios, it returns false, indicating
|
||||
// the caller should proceed with the async state machine.
|
||||
func (c *chainWatcher) handleSpendDispatch(spend *chainntnfs.SpendDetail,
|
||||
source string) bool {
|
||||
|
||||
numConfs := c.requiredConfsForSpend()
|
||||
if numConfs == 1 {
|
||||
log.Infof("ChannelPoint(%v): single confirmation mode, "+
|
||||
"dispatching immediately from %s",
|
||||
c.cfg.chanState.FundingOutpoint, source)
|
||||
|
||||
err := c.handleCommitSpend(spend)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to handle commit spend: %v", err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// handleCommitSpend takes a spending tx of the funding output and handles the
|
||||
// channel close based on the closure type.
|
||||
func (c *chainWatcher) handleCommitSpend(
|
||||
|
|
@ -1699,10 +1414,9 @@ func (c *chainWatcher) handleCommitSpend(
|
|||
case wire.MaxTxInSequenceNum:
|
||||
fallthrough
|
||||
case mempool.MaxRBFSequence:
|
||||
// This is a cooperative close. Dispatch it directly - the
|
||||
// confirmation waiting and reorg handling is done in the
|
||||
// closeObserver state machine before we reach this point.
|
||||
if err := c.dispatchCooperativeClose(commitSpend); err != nil {
|
||||
// TODO(roasbeef): rare but possible, need itest case for
|
||||
err := c.dispatchCooperativeClose(commitSpend)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handle coop close: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -1807,10 +1521,9 @@ func (c *chainWatcher) chanPointConfirmed() bool {
|
|||
}
|
||||
|
||||
// handleBlockbeat takes a blockbeat and queries for a spending tx for the
|
||||
// funding output. If found, it returns the spend details so closeObserver can
|
||||
// process it. Returns nil if no spend was detected.
|
||||
func (c *chainWatcher) handleBlockbeat(
|
||||
beat chainio.Blockbeat) *chainntnfs.SpendDetail {
|
||||
// funding output. If the spending tx is found, it will be handled based on the
|
||||
// closure type.
|
||||
func (c *chainWatcher) handleBlockbeat(beat chainio.Blockbeat) {
|
||||
// Notify the chain watcher has processed the block.
|
||||
defer c.NotifyBlockProcessed(beat, nil)
|
||||
|
||||
|
|
@ -1822,23 +1535,24 @@ func (c *chainWatcher) handleBlockbeat(
|
|||
// If the funding output hasn't confirmed in this block, we
|
||||
// will check it again in the next block.
|
||||
if !c.chanPointConfirmed() {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Perform a non-blocking read to check whether the funding output was
|
||||
// spent. The actual spend handling is done in closeObserver's state
|
||||
// machine to avoid blocking the block processing pipeline.
|
||||
// spent.
|
||||
spend := c.checkFundingSpend()
|
||||
if spend == nil {
|
||||
log.Tracef("No spend found for ChannelPoint(%v) in block %v",
|
||||
c.cfg.chanState.FundingOutpoint, beat.Height())
|
||||
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Detected spend of ChannelPoint(%v) in block %v",
|
||||
c.cfg.chanState.FundingOutpoint, beat.Height())
|
||||
|
||||
return spend
|
||||
// The funding output was spent, we now handle it by sending a close
|
||||
// event to the channel arbitrator.
|
||||
err := c.handleCommitSpend(spend)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to handle commit spend: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
package contractcourt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
)
|
||||
|
||||
// TestChainWatcherCoopCloseReorg tests that the chain watcher properly handles
|
||||
// a reorganization during cooperative close confirmation waiting. When a
|
||||
// cooperative close transaction is reorganized out, the chain watcher should
|
||||
// re-register for spend notifications and detect an alternative transaction.
|
||||
func TestChainWatcherCoopCloseReorg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create test harness.
|
||||
harness := newChainWatcherTestHarness(t)
|
||||
|
||||
// Create two cooperative close transactions with different fees.
|
||||
tx1 := harness.createCoopCloseTx(5000)
|
||||
tx2 := harness.createCoopCloseTx(4900)
|
||||
|
||||
// Run cooperative close flow with reorg.
|
||||
closeInfo := harness.runCoopCloseFlow(tx1, true, 2, tx2)
|
||||
|
||||
// Assert that the second transaction was confirmed.
|
||||
harness.assertCoopCloseTx(closeInfo, tx2)
|
||||
}
|
||||
|
||||
// TestChainWatcherCoopCloseSameTransactionAfterReorg tests that if the same
|
||||
// transaction re-confirms after a reorganization, it is properly handled.
|
||||
func TestChainWatcherCoopCloseSameTransactionAfterReorg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
harness := newChainWatcherTestHarness(t)
|
||||
|
||||
// Create a single cooperative close transaction.
|
||||
tx := harness.createCoopCloseTx(5000)
|
||||
|
||||
// Run flow with the same tx confirming after the reorg.
|
||||
closeInfo := harness.runCoopCloseFlow(tx, true, 2, tx)
|
||||
|
||||
harness.assertCoopCloseTx(closeInfo, tx)
|
||||
}
|
||||
|
||||
// TestChainWatcherCoopCloseMultipleReorgs tests handling of multiple
|
||||
// consecutive reorganizations during cooperative close confirmation.
|
||||
func TestChainWatcherCoopCloseMultipleReorgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create test harness.
|
||||
harness := newChainWatcherTestHarness(t)
|
||||
|
||||
// Create multiple cooperative close transactions with different fees.
|
||||
txs := []*wire.MsgTx{
|
||||
harness.createCoopCloseTx(5000),
|
||||
harness.createCoopCloseTx(4950),
|
||||
harness.createCoopCloseTx(4900),
|
||||
harness.createCoopCloseTx(4850),
|
||||
}
|
||||
|
||||
// Define reorg depths for each transition.
|
||||
reorgDepths := []int32{1, 2, 3}
|
||||
|
||||
// Run multiple reorg flow.
|
||||
closeInfo := harness.runMultipleReorgFlow(txs, reorgDepths)
|
||||
|
||||
// Assert that the final transaction was confirmed.
|
||||
harness.assertCoopCloseTx(closeInfo, txs[3])
|
||||
}
|
||||
|
||||
// TestChainWatcherCoopCloseReorgNoAlternative tests that if a cooperative
|
||||
// close is reorganized out and no alternative transaction appears, the
|
||||
// chain watcher continues waiting.
|
||||
func TestChainWatcherCoopCloseReorgNoAlternative(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create test harness.
|
||||
harness := newChainWatcherTestHarness(t)
|
||||
|
||||
// Create a cooperative close transaction.
|
||||
tx := harness.createCoopCloseTx(5000)
|
||||
|
||||
// Send spend and wait for confirmation registration.
|
||||
harness.sendSpend(tx)
|
||||
harness.waitForConfRegistration()
|
||||
|
||||
// Trigger reorg after some confirmations.
|
||||
harness.mineBlocks(2)
|
||||
harness.triggerReorg(tx, 2)
|
||||
|
||||
// Assert no cooperative close event is received.
|
||||
harness.assertNoCoopClose(2 * time.Second)
|
||||
|
||||
// Now send a new transaction after the timeout.
|
||||
harness.waitForSpendRegistration()
|
||||
newTx := harness.createCoopCloseTx(4900)
|
||||
harness.sendSpend(newTx)
|
||||
harness.waitForConfRegistration()
|
||||
harness.mineBlocks(1)
|
||||
harness.confirmTx(newTx, harness.currentHeight)
|
||||
|
||||
// Should receive cooperative close for the new transaction.
|
||||
closeInfo := harness.waitForCoopClose(5 * time.Second)
|
||||
harness.assertCoopCloseTx(closeInfo, newTx)
|
||||
}
|
||||
|
||||
// TestChainWatcherCoopCloseScaledConfirmationsWithReorg tests that scaled
|
||||
// confirmations (based on channel capacity) work correctly with reorgs.
|
||||
func TestChainWatcherCoopCloseScaledConfirmationsWithReorg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test with different confirmation requirements and reorg depths.
|
||||
// Note: We start at 3 confirmations because 1-conf uses the fast path
|
||||
// which bypasses reorg protection (it dispatches immediately).
|
||||
testCases := []struct {
|
||||
name string
|
||||
requiredConfs uint32
|
||||
reorgDepth int32
|
||||
}{
|
||||
{
|
||||
name: "triple_conf",
|
||||
requiredConfs: 3,
|
||||
reorgDepth: 2,
|
||||
},
|
||||
{
|
||||
name: "six_conf",
|
||||
requiredConfs: 6,
|
||||
reorgDepth: 4,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create harness with specific confirmation
|
||||
// requirements.
|
||||
harness := newChainWatcherTestHarness(
|
||||
t, withRequiredConfs(tc.requiredConfs),
|
||||
)
|
||||
|
||||
// Create transactions.
|
||||
tx1 := harness.createCoopCloseTx(5000)
|
||||
tx2 := harness.createCoopCloseTx(4900)
|
||||
|
||||
// Run with reorg at different depths based on capacity.
|
||||
closeInfo := harness.runCoopCloseFlow(
|
||||
tx1, true, tc.reorgDepth, tx2,
|
||||
)
|
||||
|
||||
// Verify correct transaction confirmed.
|
||||
harness.assertCoopCloseTx(closeInfo, tx2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainWatcherCoopCloseRapidReorgs tests that the chain watcher handles
|
||||
// multiple rapid reorgs in succession without getting into a broken state.
|
||||
func TestChainWatcherCoopCloseRapidReorgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create test harness.
|
||||
harness := newChainWatcherTestHarness(t)
|
||||
|
||||
// Create a cooperative close transaction.
|
||||
tx := harness.createCoopCloseTx(5000)
|
||||
|
||||
// Send spend notification.
|
||||
harness.sendSpend(tx)
|
||||
|
||||
// Trigger multiple rapid reorgs to stress the state machine.
|
||||
for i := 0; i < 5; i++ {
|
||||
harness.waitForConfRegistration()
|
||||
harness.mineBlocks(1)
|
||||
harness.triggerReorg(tx, int32(i+1))
|
||||
if i < 4 {
|
||||
// Re-register for spend after each reorg except the
|
||||
// last.
|
||||
harness.waitForSpendRegistration()
|
||||
harness.sendSpend(tx)
|
||||
}
|
||||
}
|
||||
|
||||
// After stress, send a clean transaction.
|
||||
harness.waitForSpendRegistration()
|
||||
cleanTx := harness.createCoopCloseTx(4800)
|
||||
harness.sendSpend(cleanTx)
|
||||
harness.waitForConfRegistration()
|
||||
harness.mineBlocks(1)
|
||||
harness.confirmTx(cleanTx, harness.currentHeight)
|
||||
|
||||
// Should still receive the cooperative close.
|
||||
closeInfo := harness.waitForCoopClose(10 * time.Second)
|
||||
harness.assertCoopCloseTx(closeInfo, cleanTx)
|
||||
}
|
||||
|
|
@ -1,404 +0,0 @@
|
|||
package contractcourt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"pgregory.net/rapid"
|
||||
)
|
||||
|
||||
// closeType represents the type of channel close for testing purposes.
|
||||
type closeType int
|
||||
|
||||
const (
|
||||
// closeTypeCoop represents a cooperative channel close.
|
||||
closeTypeCoop closeType = iota
|
||||
|
||||
// closeTypeRemoteUnilateral represents a remote unilateral close
|
||||
// (remote party broadcasting their commitment).
|
||||
closeTypeRemoteUnilateral
|
||||
|
||||
// closeTypeLocalForce represents a local force close (us broadcasting
|
||||
// our commitment).
|
||||
closeTypeLocalForce
|
||||
|
||||
// closeTypeBreach represents a breach (remote party broadcasting a
|
||||
// revoked commitment).
|
||||
closeTypeBreach
|
||||
)
|
||||
|
||||
// String returns a string representation of the close type.
|
||||
func (c closeType) String() string {
|
||||
switch c {
|
||||
case closeTypeCoop:
|
||||
return "cooperative"
|
||||
case closeTypeRemoteUnilateral:
|
||||
return "remote_unilateral"
|
||||
case closeTypeLocalForce:
|
||||
return "local_force"
|
||||
case closeTypeBreach:
|
||||
return "breach"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// createCloseTx creates a close transaction of the specified type using the
|
||||
// harness.
|
||||
func createCloseTx(h *chainWatcherTestHarness, ct closeType,
|
||||
outputValue int64) *wire.MsgTx {
|
||||
|
||||
switch ct {
|
||||
case closeTypeCoop:
|
||||
return h.createCoopCloseTx(outputValue)
|
||||
case closeTypeRemoteUnilateral:
|
||||
return h.createRemoteForceCloseTx()
|
||||
case closeTypeLocalForce:
|
||||
return h.createLocalForceCloseTx()
|
||||
case closeTypeBreach:
|
||||
return h.createBreachCloseTx()
|
||||
default:
|
||||
h.t.Fatalf("unknown close type: %v", ct)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// waitForCloseEvent waits for the appropriate close event based on close type.
|
||||
func waitForCloseEvent(h *chainWatcherTestHarness, ct closeType,
|
||||
timeout time.Duration) any {
|
||||
|
||||
switch ct {
|
||||
case closeTypeCoop:
|
||||
return h.waitForCoopClose(timeout)
|
||||
case closeTypeRemoteUnilateral:
|
||||
return h.waitForRemoteUnilateralClose(timeout)
|
||||
case closeTypeLocalForce:
|
||||
return h.waitForLocalUnilateralClose(timeout)
|
||||
case closeTypeBreach:
|
||||
return h.waitForBreach(timeout)
|
||||
default:
|
||||
h.t.Fatalf("unknown close type: %v", ct)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// assertCloseEventTx asserts that the close event matches the expected
|
||||
// transaction based on close type.
|
||||
func assertCloseEventTx(h *chainWatcherTestHarness, ct closeType,
|
||||
event any, expectedTx *wire.MsgTx) {
|
||||
|
||||
switch ct {
|
||||
case closeTypeCoop:
|
||||
coopInfo, ok := event.(*CooperativeCloseInfo)
|
||||
if !ok {
|
||||
h.t.Fatalf("expected CooperativeCloseInfo, got %T",
|
||||
event)
|
||||
}
|
||||
h.assertCoopCloseTx(coopInfo, expectedTx)
|
||||
|
||||
case closeTypeRemoteUnilateral:
|
||||
remoteInfo, ok := event.(*RemoteUnilateralCloseInfo)
|
||||
if !ok {
|
||||
h.t.Fatalf("expected RemoteUnilateralCloseInfo, got %T",
|
||||
event)
|
||||
}
|
||||
h.assertRemoteUnilateralCloseTx(remoteInfo, expectedTx)
|
||||
|
||||
case closeTypeLocalForce:
|
||||
localInfo, ok := event.(*LocalUnilateralCloseInfo)
|
||||
if !ok {
|
||||
h.t.Fatalf("expected LocalUnilateralCloseInfo, got %T",
|
||||
event)
|
||||
}
|
||||
h.assertLocalUnilateralCloseTx(localInfo, expectedTx)
|
||||
|
||||
case closeTypeBreach:
|
||||
breachInfo, ok := event.(*BreachCloseInfo)
|
||||
if !ok {
|
||||
h.t.Fatalf("expected BreachCloseInfo, got %T", event)
|
||||
}
|
||||
h.assertBreachTx(breachInfo, expectedTx)
|
||||
|
||||
default:
|
||||
h.t.Fatalf("unknown close type: %v", ct)
|
||||
}
|
||||
}
|
||||
|
||||
// generateAltTxsForReorgs generates alternative transactions for reorg
|
||||
// scenarios. For commitment-based closes (breach, remote/local force), the same
|
||||
// tx is reused since we can only have one commitment tx per channel state. For
|
||||
// coop closes, new transactions with different output values are created.
|
||||
func generateAltTxsForReorgs(h *chainWatcherTestHarness, ct closeType,
|
||||
originalTx *wire.MsgTx, numReorgs int, sameTxAtEnd bool) []*wire.MsgTx {
|
||||
|
||||
altTxs := make([]*wire.MsgTx, numReorgs)
|
||||
|
||||
for i := 0; i < numReorgs; i++ {
|
||||
switch ct {
|
||||
case closeTypeBreach, closeTypeRemoteUnilateral,
|
||||
closeTypeLocalForce:
|
||||
|
||||
// Non-coop closes can only have one commitment tx, so
|
||||
// all reorgs use the same transaction.
|
||||
altTxs[i] = originalTx
|
||||
|
||||
case closeTypeCoop:
|
||||
if i == numReorgs-1 && sameTxAtEnd {
|
||||
// Last reorg goes back to original transaction.
|
||||
altTxs[i] = originalTx
|
||||
} else {
|
||||
// Create different coop close tx with different
|
||||
// output value to make it unique.
|
||||
outputValue := int64(5000 - (i+1)*100)
|
||||
altTxs[i] = createCloseTx(h, ct, outputValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return altTxs
|
||||
}
|
||||
|
||||
// testReorgProperties is the main property-based test for reorg handling
|
||||
// across all close types.
|
||||
//
|
||||
// The testingT parameter is captured from the outer test function and used
|
||||
// for operations that require *testing.T (like channel creation), while the
|
||||
// rapid.T is used for all test reporting and property generation.
|
||||
func testReorgProperties(testingT *testing.T) func(*rapid.T) {
|
||||
return func(t *rapid.T) {
|
||||
// Generate random close type.
|
||||
allCloseTypes := []closeType{
|
||||
closeTypeCoop,
|
||||
closeTypeRemoteUnilateral,
|
||||
closeTypeLocalForce,
|
||||
closeTypeBreach,
|
||||
}
|
||||
ct := rapid.SampledFrom(allCloseTypes).Draw(t, "closeType")
|
||||
|
||||
// Generate random number of required confirmations (2-6). We
|
||||
// use at least 2 so we have room for reorgs during
|
||||
// confirmation.
|
||||
requiredConfs := rapid.IntRange(2, 6).Draw(t, "requiredConfs")
|
||||
|
||||
// Generate number of reorgs (1-3 to keep test runtime
|
||||
// reasonable).
|
||||
numReorgs := rapid.IntRange(1, 3).Draw(t, "numReorgs")
|
||||
|
||||
// Generate whether the final transaction is the same as the
|
||||
// original.
|
||||
sameTxAtEnd := rapid.Bool().Draw(t, "sameTxAtEnd")
|
||||
|
||||
// Log test parameters for debugging.
|
||||
t.Logf("Testing %s close with %d confs, %d reorgs, "+
|
||||
"sameTxAtEnd=%v",
|
||||
ct, requiredConfs, numReorgs, sameTxAtEnd)
|
||||
|
||||
// Create test harness using both the concrete *testing.T for
|
||||
// channel creation and the rapid.T for test reporting.
|
||||
harness := newChainWatcherTestHarnessFromReporter(
|
||||
testingT, t, withRequiredConfs(uint32(requiredConfs)),
|
||||
)
|
||||
|
||||
// Create initial transaction.
|
||||
tx1 := createCloseTx(harness, ct, 5000)
|
||||
|
||||
// Generate alternative transactions for each reorg.
|
||||
altTxs := generateAltTxsForReorgs(
|
||||
harness, ct, tx1, numReorgs, sameTxAtEnd,
|
||||
)
|
||||
|
||||
// Send the initial spend.
|
||||
harness.sendSpend(tx1)
|
||||
harness.waitForConfRegistration()
|
||||
|
||||
// Execute the set of re-orgs, based on our random sample, we'll
|
||||
// mine N blocks, do a re-org of size N, then wait for
|
||||
// detection, and repeat.
|
||||
for i := 0; i < numReorgs; i++ {
|
||||
// Generate random reorg depth (1 to requiredConfs-1).
|
||||
// We cap it to avoid reorging too far back.
|
||||
reorgDepth := rapid.IntRange(
|
||||
1, requiredConfs-1,
|
||||
).Draw(t, "reorgDepth")
|
||||
|
||||
// Mine some blocks (but less than required confs).
|
||||
blocksToMine := rapid.IntRange(
|
||||
1, requiredConfs-1,
|
||||
).Draw(t, "blocksToMine")
|
||||
harness.mineBlocks(int32(blocksToMine))
|
||||
|
||||
// Trigger reorg.
|
||||
if i == 0 {
|
||||
harness.triggerReorg(
|
||||
tx1, int32(reorgDepth),
|
||||
)
|
||||
} else {
|
||||
harness.triggerReorg(
|
||||
altTxs[i-1], int32(reorgDepth),
|
||||
)
|
||||
}
|
||||
|
||||
harness.waitForSpendRegistration()
|
||||
|
||||
harness.sendSpend(altTxs[i])
|
||||
harness.waitForConfRegistration()
|
||||
}
|
||||
|
||||
// Mine enough blocks to confirm final transaction.
|
||||
harness.mineBlocks(1)
|
||||
finalTx := altTxs[numReorgs-1]
|
||||
harness.confirmTx(finalTx, harness.currentHeight)
|
||||
|
||||
// Wait for and verify close event.
|
||||
event := waitForCloseEvent(harness, ct, 10*time.Second)
|
||||
assertCloseEventTx(harness, ct, event, finalTx)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainWatcherReorgAllCloseTypes runs property-based tests for reorg
|
||||
// handling across all channel close types. It generates random combinations of
|
||||
// the following:
|
||||
// - Close type (coop, remote unilateral, local force, breach)
|
||||
// - Number of confirmations required (2-6)
|
||||
// - Number of reorgs (1-3)
|
||||
// - Whether the final tx is same as original or different.
|
||||
func TestChainWatcherReorgAllCloseTypes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rapid.Check(t, testReorgProperties(t))
|
||||
}
|
||||
|
||||
// TestRemoteUnilateralCloseWithSingleReorg tests that a remote unilateral
|
||||
// close is properly handled when a single reorg occurs during confirmation.
|
||||
func TestRemoteUnilateralCloseWithSingleReorg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
harness := newChainWatcherTestHarness(t)
|
||||
|
||||
// Create two remote unilateral close transactions.
|
||||
// Since these are commitment transactions, we can only have one per
|
||||
// state, so we'll use the current one as tx1.
|
||||
tx1 := harness.createRemoteForceCloseTx()
|
||||
|
||||
// Advance channel state to get a different commitment.
|
||||
_ = harness.createBreachCloseTx()
|
||||
tx2 := harness.createRemoteForceCloseTx()
|
||||
|
||||
// Send initial spend.
|
||||
harness.sendSpend(tx1)
|
||||
harness.waitForConfRegistration()
|
||||
|
||||
// Mine a block and trigger reorg.
|
||||
harness.mineBlocks(1)
|
||||
harness.triggerReorg(tx1, 1)
|
||||
|
||||
// Send alternative transaction after reorg.
|
||||
harness.waitForSpendRegistration()
|
||||
harness.sendSpend(tx2)
|
||||
harness.waitForConfRegistration()
|
||||
harness.mineBlocks(1)
|
||||
harness.confirmTx(tx2, harness.currentHeight)
|
||||
|
||||
// Verify correct event.
|
||||
closeInfo := harness.waitForRemoteUnilateralClose(5 * time.Second)
|
||||
harness.assertRemoteUnilateralCloseTx(closeInfo, tx2)
|
||||
}
|
||||
|
||||
// TestLocalForceCloseWithMultipleReorgs tests that a local force close is
|
||||
// properly handled through multiple consecutive reorgs.
|
||||
func TestLocalForceCloseWithMultipleReorgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
harness := newChainWatcherTestHarness(t)
|
||||
|
||||
// For local force close, we can only broadcast our current commitment.
|
||||
// We'll simulate multiple reorgs where the same tx keeps getting
|
||||
// reorganized out and re-broadcast.
|
||||
tx := harness.createLocalForceCloseTx()
|
||||
|
||||
// First spend and reorg.
|
||||
harness.sendSpend(tx)
|
||||
harness.waitForConfRegistration()
|
||||
harness.mineBlocks(1)
|
||||
harness.triggerReorg(tx, 1)
|
||||
|
||||
// Second spend and reorg.
|
||||
harness.waitForSpendRegistration()
|
||||
harness.sendSpend(tx)
|
||||
harness.waitForConfRegistration()
|
||||
harness.mineBlocks(1)
|
||||
harness.triggerReorg(tx, 1)
|
||||
|
||||
// Third spend - this one confirms.
|
||||
harness.waitForSpendRegistration()
|
||||
harness.sendSpend(tx)
|
||||
harness.waitForConfRegistration()
|
||||
harness.mineBlocks(1)
|
||||
harness.confirmTx(tx, harness.currentHeight)
|
||||
|
||||
// Verify correct event.
|
||||
closeInfo := harness.waitForLocalUnilateralClose(5 * time.Second)
|
||||
harness.assertLocalUnilateralCloseTx(closeInfo, tx)
|
||||
}
|
||||
|
||||
// TestBreachCloseWithDeepReorg tests that a breach (revoked commitment) is
|
||||
// properly detected after a deep reorganization.
|
||||
func TestBreachCloseWithDeepReorg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
harness := newChainWatcherTestHarness(t)
|
||||
|
||||
// Create a revoked commitment transaction.
|
||||
revokedTx := harness.createBreachCloseTx()
|
||||
|
||||
// Send spend and wait for confirmation registration.
|
||||
harness.sendSpend(revokedTx)
|
||||
harness.waitForConfRegistration()
|
||||
|
||||
// Mine several blocks and then trigger a deep reorg.
|
||||
harness.mineBlocks(5)
|
||||
harness.triggerReorg(revokedTx, 5)
|
||||
|
||||
// Re-broadcast same transaction after reorg.
|
||||
harness.waitForSpendRegistration()
|
||||
harness.sendSpend(revokedTx)
|
||||
harness.waitForConfRegistration()
|
||||
harness.mineBlocks(1)
|
||||
harness.confirmTx(revokedTx, harness.currentHeight)
|
||||
|
||||
// Verify breach detection.
|
||||
breachInfo := harness.waitForBreach(5 * time.Second)
|
||||
harness.assertBreachTx(breachInfo, revokedTx)
|
||||
}
|
||||
|
||||
// TestCoopCloseReorgToForceClose tests the edge case where a cooperative
|
||||
// close gets reorged out and is replaced by a force close.
|
||||
func TestCoopCloseReorgToForceClose(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
harness := newChainWatcherTestHarness(t)
|
||||
|
||||
// Create a cooperative close and a force close transaction.
|
||||
coopTx := harness.createCoopCloseTx(5000)
|
||||
forceTx := harness.createRemoteForceCloseTx()
|
||||
|
||||
// Send cooperative close.
|
||||
harness.sendSpend(coopTx)
|
||||
harness.waitForConfRegistration()
|
||||
|
||||
// Trigger reorg that removes coop close.
|
||||
harness.mineBlocks(1)
|
||||
harness.triggerReorg(coopTx, 1)
|
||||
|
||||
// Send force close as alternative.
|
||||
harness.waitForSpendRegistration()
|
||||
harness.sendSpend(forceTx)
|
||||
harness.waitForConfRegistration()
|
||||
harness.mineBlocks(1)
|
||||
harness.confirmTx(forceTx, harness.currentHeight)
|
||||
|
||||
// Should receive remote unilateral close event, not coop close.
|
||||
closeInfo := harness.waitForRemoteUnilateralClose(5 * time.Second)
|
||||
harness.assertRemoteUnilateralCloseTx(closeInfo, forceTx)
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import (
|
|||
"github.com/lightningnetwork/lnd/chainio"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
lnmock "github.com/lightningnetwork/lnd/lntest/mock"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
|
|
@ -35,19 +34,16 @@ func TestChainWatcherRemoteUnilateralClose(t *testing.T) {
|
|||
|
||||
// With the channels created, we'll now create a chain watcher instance
|
||||
// which will be watching for any closes of Alice's channel.
|
||||
confRegistered := make(chan struct{}, 1)
|
||||
aliceNotifier := &lnmock.ChainNotifier{
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail, 1),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation, 1),
|
||||
ConfRegistered: confRegistered,
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail, 1),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||
}
|
||||
aliceChainWatcher, err := newChainWatcher(chainWatcherConfig{
|
||||
chanState: aliceChannel.State(),
|
||||
notifier: aliceNotifier,
|
||||
signer: aliceChannel.Signer,
|
||||
extractStateNumHint: lnwallet.GetStateNumHint,
|
||||
chanCloseConfs: fn.Some(uint32(1)),
|
||||
})
|
||||
require.NoError(t, err, "unable to create chain watcher")
|
||||
err = aliceChainWatcher.Start()
|
||||
|
|
@ -94,10 +90,6 @@ func TestChainWatcherRemoteUnilateralClose(t *testing.T) {
|
|||
t.Fatalf("unable to send blockbeat")
|
||||
}
|
||||
|
||||
// With chanCloseConfs set to 1, the fast-path dispatches immediately
|
||||
// without confirmation registration. The close event should arrive
|
||||
// directly after processing the blockbeat.
|
||||
|
||||
// We should get a new spend event over the remote unilateral close
|
||||
// event channel.
|
||||
var uniClose *RemoteUnilateralCloseInfo
|
||||
|
|
@ -152,19 +144,16 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) {
|
|||
|
||||
// With the channels created, we'll now create a chain watcher instance
|
||||
// which will be watching for any closes of Alice's channel.
|
||||
confRegistered := make(chan struct{}, 1)
|
||||
aliceNotifier := &lnmock.ChainNotifier{
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||
ConfRegistered: confRegistered,
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||
}
|
||||
aliceChainWatcher, err := newChainWatcher(chainWatcherConfig{
|
||||
chanState: aliceChannel.State(),
|
||||
notifier: aliceNotifier,
|
||||
signer: aliceChannel.Signer,
|
||||
extractStateNumHint: lnwallet.GetStateNumHint,
|
||||
chanCloseConfs: fn.Some(uint32(1)),
|
||||
})
|
||||
require.NoError(t, err, "unable to create chain watcher")
|
||||
if err := aliceChainWatcher.Start(); err != nil {
|
||||
|
|
@ -230,10 +219,6 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) {
|
|||
t.Fatalf("unable to send blockbeat")
|
||||
}
|
||||
|
||||
// With chanCloseConfs set to 1, the fast-path dispatches immediately
|
||||
// without confirmation registration. The close event should arrive
|
||||
// directly after processing the blockbeat.
|
||||
|
||||
// We should get a new spend event over the remote unilateral close
|
||||
// event channel.
|
||||
var uniClose *RemoteUnilateralCloseInfo
|
||||
|
|
@ -346,12 +331,10 @@ func TestChainWatcherDataLossProtect(t *testing.T) {
|
|||
// With the channels created, we'll now create a chain watcher
|
||||
// instance which will be watching for any closes of Alice's
|
||||
// channel.
|
||||
confRegistered := make(chan struct{}, 1)
|
||||
aliceNotifier := &lnmock.ChainNotifier{
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||
ConfRegistered: confRegistered,
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||
}
|
||||
aliceChainWatcher, err := newChainWatcher(chainWatcherConfig{
|
||||
chanState: aliceChanState,
|
||||
|
|
@ -424,8 +407,6 @@ func TestChainWatcherDataLossProtect(t *testing.T) {
|
|||
t.Fatalf("unable to send blockbeat")
|
||||
}
|
||||
|
||||
aliceNotifier.WaitForConfRegistrationAndSend(t)
|
||||
|
||||
// We should get a new uni close resolution that indicates we
|
||||
// processed the DLP scenario.
|
||||
var uniClose *RemoteUnilateralCloseInfo
|
||||
|
|
@ -551,12 +532,10 @@ func TestChainWatcherLocalForceCloseDetect(t *testing.T) {
|
|||
// With the channels created, we'll now create a chain watcher
|
||||
// instance which will be watching for any closes of Alice's
|
||||
// channel.
|
||||
confRegistered := make(chan struct{}, 1)
|
||||
aliceNotifier := &lnmock.ChainNotifier{
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||
ConfRegistered: confRegistered,
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||
}
|
||||
aliceChainWatcher, err := newChainWatcher(chainWatcherConfig{
|
||||
chanState: aliceChanState,
|
||||
|
|
@ -625,8 +604,6 @@ func TestChainWatcherLocalForceCloseDetect(t *testing.T) {
|
|||
t.Fatalf("unable to send blockbeat")
|
||||
}
|
||||
|
||||
aliceNotifier.WaitForConfRegistrationAndSend(t)
|
||||
|
||||
// We should get a local force close event from Alice as she
|
||||
// should be able to detect the close based on the commitment
|
||||
// outputs.
|
||||
|
|
|
|||
|
|
@ -1,656 +0,0 @@
|
|||
package contractcourt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
lnmock "github.com/lightningnetwork/lnd/lntest/mock"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
// testReporter is a minimal interface for test reporting that is satisfied
|
||||
// by both *testing.T and *rapid.T, allowing the harness to work with
|
||||
// property-based tests.
|
||||
type testReporter interface {
|
||||
Helper()
|
||||
Fatalf(format string, args ...any)
|
||||
}
|
||||
|
||||
// chainWatcherTestHarness provides a test harness for chain watcher tests
|
||||
// with utilities for simulating spends, confirmations, and reorganizations.
|
||||
type chainWatcherTestHarness struct {
|
||||
t testReporter
|
||||
|
||||
// aliceChannel and bobChannel are the test channels.
|
||||
aliceChannel *lnwallet.LightningChannel
|
||||
bobChannel *lnwallet.LightningChannel
|
||||
|
||||
// chainWatcher is the chain watcher under test.
|
||||
chainWatcher *chainWatcher
|
||||
|
||||
// notifier is the mock chain notifier.
|
||||
notifier *mockChainNotifier
|
||||
|
||||
// chanEvents is the channel event subscription.
|
||||
chanEvents *ChainEventSubscription
|
||||
|
||||
// currentHeight tracks the current block height.
|
||||
currentHeight int32
|
||||
|
||||
// blockbeatProcessed is a channel that signals when a blockbeat has
|
||||
// been processed.
|
||||
blockbeatProcessed chan struct{}
|
||||
}
|
||||
|
||||
// mockChainNotifier extends the standard mock with additional channels for
|
||||
// testing cooperative close reorgs.
|
||||
type mockChainNotifier struct {
|
||||
*lnmock.ChainNotifier
|
||||
|
||||
// confEvents tracks active confirmation event subscriptions.
|
||||
confEvents []*mockConfirmationEvent
|
||||
|
||||
// confRegistered is a channel that signals when a new confirmation
|
||||
// event has been registered.
|
||||
confRegistered chan struct{}
|
||||
|
||||
// spendEvents tracks active spend event subscriptions.
|
||||
spendEvents []*chainntnfs.SpendEvent
|
||||
|
||||
// spendRegistered is a channel that signals when a new spend
|
||||
// event has been registered.
|
||||
spendRegistered chan struct{}
|
||||
}
|
||||
|
||||
// mockConfirmationEvent represents a mock confirmation event subscription.
|
||||
type mockConfirmationEvent struct {
|
||||
txid chainhash.Hash
|
||||
numConfs uint32
|
||||
confirmedChan chan *chainntnfs.TxConfirmation
|
||||
negConfChan chan int32
|
||||
cancelled bool
|
||||
}
|
||||
|
||||
// RegisterSpendNtfn creates a new mock spend event.
|
||||
func (m *mockChainNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
|
||||
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
|
||||
|
||||
// The base mock already has SpendChan, use that.
|
||||
spendEvent := &chainntnfs.SpendEvent{
|
||||
Spend: m.SpendChan,
|
||||
Cancel: func() {
|
||||
// No-op for now.
|
||||
},
|
||||
}
|
||||
|
||||
m.spendEvents = append(m.spendEvents, spendEvent)
|
||||
|
||||
// Signal that a new spend event has been registered.
|
||||
select {
|
||||
case m.spendRegistered <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
return spendEvent, nil
|
||||
}
|
||||
|
||||
// RegisterConfirmationsNtfn creates a new mock confirmation event.
|
||||
func (m *mockChainNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
|
||||
pkScript []byte, numConfs, heightHint uint32,
|
||||
opts ...chainntnfs.NotifierOption,
|
||||
) (*chainntnfs.ConfirmationEvent, error) {
|
||||
|
||||
mockEvent := &mockConfirmationEvent{
|
||||
txid: *txid,
|
||||
numConfs: numConfs,
|
||||
confirmedChan: make(chan *chainntnfs.TxConfirmation, 1),
|
||||
negConfChan: make(chan int32, 1),
|
||||
}
|
||||
|
||||
m.confEvents = append(m.confEvents, mockEvent)
|
||||
|
||||
// Signal that a new confirmation event has been registered.
|
||||
select {
|
||||
case m.confRegistered <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
return &chainntnfs.ConfirmationEvent{
|
||||
Confirmed: mockEvent.confirmedChan,
|
||||
NegativeConf: mockEvent.negConfChan,
|
||||
Cancel: func() {
|
||||
mockEvent.cancelled = true
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// harnessOpt is a functional option for configuring the test harness.
|
||||
type harnessOpt func(*harnessConfig)
|
||||
|
||||
// harnessConfig holds configuration for the test harness.
|
||||
type harnessConfig struct {
|
||||
requiredConfs fn.Option[uint32]
|
||||
}
|
||||
|
||||
// withRequiredConfs sets the number of confirmations required for channel
|
||||
// closes.
|
||||
func withRequiredConfs(confs uint32) harnessOpt {
|
||||
return func(cfg *harnessConfig) {
|
||||
cfg.requiredConfs = fn.Some(confs)
|
||||
}
|
||||
}
|
||||
|
||||
// newChainWatcherTestHarness creates a new test harness for chain watcher
|
||||
// tests.
|
||||
func newChainWatcherTestHarness(t *testing.T,
|
||||
opts ...harnessOpt) *chainWatcherTestHarness {
|
||||
|
||||
return newChainWatcherTestHarnessFromReporter(t, t, opts...)
|
||||
}
|
||||
|
||||
// newChainWatcherTestHarnessFromReporter creates a test harness that works
|
||||
// with both *testing.T and *rapid.T. The t parameter is used for
|
||||
// operations that specifically require *testing.T (like CreateTestChannels),
|
||||
// while reporter is used for all test reporting (Helper, Fatalf).
|
||||
func newChainWatcherTestHarnessFromReporter(t *testing.T,
|
||||
reporter testReporter, opts ...harnessOpt) *chainWatcherTestHarness {
|
||||
|
||||
reporter.Helper()
|
||||
|
||||
// Apply options.
|
||||
cfg := &harnessConfig{
|
||||
requiredConfs: fn.None[uint32](),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
// Create test channels.
|
||||
aliceChannel, bobChannel, err := lnwallet.CreateTestChannels(
|
||||
t, channeldb.SingleFunderTweaklessBit,
|
||||
)
|
||||
if err != nil {
|
||||
reporter.Fatalf("unable to create test channels: %v", err)
|
||||
}
|
||||
|
||||
// Create mock notifier.
|
||||
baseNotifier := &lnmock.ChainNotifier{
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail, 1),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation, 1),
|
||||
}
|
||||
|
||||
notifier := &mockChainNotifier{
|
||||
ChainNotifier: baseNotifier,
|
||||
confEvents: make([]*mockConfirmationEvent, 0),
|
||||
confRegistered: make(chan struct{}, 10),
|
||||
spendEvents: make([]*chainntnfs.SpendEvent, 0),
|
||||
spendRegistered: make(chan struct{}, 10),
|
||||
}
|
||||
|
||||
// Create chain watcher.
|
||||
chainWatcher, err := newChainWatcher(chainWatcherConfig{
|
||||
chanState: aliceChannel.State(),
|
||||
notifier: notifier,
|
||||
signer: aliceChannel.Signer,
|
||||
extractStateNumHint: lnwallet.GetStateNumHint,
|
||||
chanCloseConfs: cfg.requiredConfs,
|
||||
contractBreach: func(
|
||||
retInfo *lnwallet.BreachRetribution,
|
||||
) error {
|
||||
// In tests, we just need to accept the breach
|
||||
// notification.
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
reporter.Fatalf("unable to create chain watcher: %v", err)
|
||||
}
|
||||
|
||||
// Start chain watcher (this will register for spend notification).
|
||||
err = chainWatcher.Start()
|
||||
if err != nil {
|
||||
reporter.Fatalf("unable to start chain watcher: %v", err)
|
||||
}
|
||||
|
||||
// Subscribe to channel events.
|
||||
chanEvents := chainWatcher.SubscribeChannelEvents()
|
||||
|
||||
harness := &chainWatcherTestHarness{
|
||||
t: reporter,
|
||||
aliceChannel: aliceChannel,
|
||||
bobChannel: bobChannel,
|
||||
chainWatcher: chainWatcher,
|
||||
notifier: notifier,
|
||||
chanEvents: chanEvents,
|
||||
currentHeight: 100,
|
||||
blockbeatProcessed: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Wait for the initial spend registration that happens in Start().
|
||||
harness.waitForSpendRegistration()
|
||||
|
||||
// Verify BlockbeatChan is initialized.
|
||||
if chainWatcher.BlockbeatChan == nil {
|
||||
reporter.Fatalf("BlockbeatChan is nil after initialization")
|
||||
}
|
||||
|
||||
// Register cleanup. We use the t for Cleanup since rapid.T
|
||||
// may not have this method in the same way.
|
||||
t.Cleanup(func() {
|
||||
_ = chainWatcher.Stop()
|
||||
})
|
||||
|
||||
return harness
|
||||
}
|
||||
|
||||
// createCoopCloseTx creates a cooperative close transaction with the given
|
||||
// output value. The transaction will have the proper sequence number to
|
||||
// indicate it's a cooperative close.
|
||||
func (h *chainWatcherTestHarness) createCoopCloseTx(
|
||||
outputValue int64) *wire.MsgTx {
|
||||
|
||||
fundingOutpoint := h.aliceChannel.State().FundingOutpoint
|
||||
|
||||
return &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{{
|
||||
PreviousOutPoint: fundingOutpoint,
|
||||
Sequence: wire.MaxTxInSequenceNum,
|
||||
}},
|
||||
TxOut: []*wire.TxOut{{
|
||||
Value: outputValue,
|
||||
// Unique script.
|
||||
PkScript: []byte{byte(outputValue % 255)},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// createRemoteForceCloseTx creates a remote force close transaction.
|
||||
// From Alice's perspective, this is Bob's local commitment transaction.
|
||||
func (h *chainWatcherTestHarness) createRemoteForceCloseTx() *wire.MsgTx {
|
||||
return h.bobChannel.State().LocalCommitment.CommitTx
|
||||
}
|
||||
|
||||
// createLocalForceCloseTx creates a local force close transaction.
|
||||
// This is Alice's local commitment transaction.
|
||||
func (h *chainWatcherTestHarness) createLocalForceCloseTx() *wire.MsgTx {
|
||||
return h.aliceChannel.State().LocalCommitment.CommitTx
|
||||
}
|
||||
|
||||
// createBreachCloseTx creates a breach (revoked commitment) transaction.
|
||||
// We advance the channel state, save the commitment, then advance again
|
||||
// to revoke it. Returns the revoked commitment tx.
|
||||
func (h *chainWatcherTestHarness) createBreachCloseTx() *wire.MsgTx {
|
||||
h.t.Helper()
|
||||
|
||||
// To create a revoked commitment, we need to advance the channel state
|
||||
// at least once. We'll use the test utils helper to add an HTLC and
|
||||
// force a state transition.
|
||||
|
||||
// Get the current commitment before we advance (this will be revoked).
|
||||
revokedCommit := h.bobChannel.State().LocalCommitment.CommitTx
|
||||
|
||||
// Add a fake HTLC to advance state.
|
||||
htlcAmount := lnwire.NewMSatFromSatoshis(10000)
|
||||
paymentHash := [32]byte{4, 5, 6}
|
||||
htlc := &lnwire.UpdateAddHTLC{
|
||||
ID: 0,
|
||||
Amount: htlcAmount,
|
||||
Expiry: uint32(h.currentHeight + 100),
|
||||
PaymentHash: paymentHash,
|
||||
}
|
||||
|
||||
// Add HTLC to both channels.
|
||||
if _, err := h.aliceChannel.AddHTLC(htlc, nil); err != nil {
|
||||
h.t.Fatalf("unable to add HTLC to alice: %v", err)
|
||||
}
|
||||
if _, err := h.bobChannel.ReceiveHTLC(htlc); err != nil {
|
||||
h.t.Fatalf("unable to add HTLC to bob: %v", err)
|
||||
}
|
||||
|
||||
// Force state transition using the helper.
|
||||
err := lnwallet.ForceStateTransition(h.aliceChannel, h.bobChannel)
|
||||
if err != nil {
|
||||
h.t.Fatalf("unable to force state transition: %v", err)
|
||||
}
|
||||
|
||||
// Return the revoked commitment (Bob's previous local commitment).
|
||||
return revokedCommit
|
||||
}
|
||||
|
||||
// sendSpend sends a spend notification for the given transaction.
|
||||
func (h *chainWatcherTestHarness) sendSpend(tx *wire.MsgTx) {
|
||||
h.t.Helper()
|
||||
|
||||
txHash := tx.TxHash()
|
||||
spend := &chainntnfs.SpendDetail{
|
||||
SpenderTxHash: &txHash,
|
||||
SpendingTx: tx,
|
||||
SpendingHeight: h.currentHeight,
|
||||
}
|
||||
|
||||
select {
|
||||
case h.notifier.SpendChan <- spend:
|
||||
case <-time.After(time.Second):
|
||||
h.t.Fatalf("unable to send spend notification")
|
||||
}
|
||||
}
|
||||
|
||||
// confirmTx sends a confirmation notification for the given transaction.
|
||||
func (h *chainWatcherTestHarness) confirmTx(tx *wire.MsgTx, height int32) {
|
||||
h.t.Helper()
|
||||
|
||||
// Find the confirmation event for this transaction.
|
||||
txHash := tx.TxHash()
|
||||
var confEvent *mockConfirmationEvent
|
||||
for _, event := range h.notifier.confEvents {
|
||||
if event.txid == txHash && !event.cancelled {
|
||||
confEvent = event
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if confEvent == nil {
|
||||
h.t.Fatalf("no confirmation event registered for tx %v", txHash)
|
||||
}
|
||||
|
||||
// Send confirmation.
|
||||
select {
|
||||
case confEvent.confirmedChan <- &chainntnfs.TxConfirmation{
|
||||
Tx: tx,
|
||||
BlockHeight: uint32(height),
|
||||
}:
|
||||
case <-time.After(time.Second):
|
||||
h.t.Fatalf("unable to send confirmation")
|
||||
}
|
||||
}
|
||||
|
||||
// triggerReorg sends a negative confirmation (reorg) notification for the
|
||||
// given transaction with the specified reorg depth.
|
||||
func (h *chainWatcherTestHarness) triggerReorg(tx *wire.MsgTx,
|
||||
reorgDepth int32) {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
// Find the confirmation event for this transaction.
|
||||
txHash := tx.TxHash()
|
||||
var confEvent *mockConfirmationEvent
|
||||
for _, event := range h.notifier.confEvents {
|
||||
if event.txid == txHash && !event.cancelled {
|
||||
confEvent = event
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if confEvent == nil {
|
||||
// The chain watcher might not have registered for
|
||||
// confirmations yet.
|
||||
return
|
||||
}
|
||||
|
||||
// Send negative confirmation.
|
||||
select {
|
||||
case confEvent.negConfChan <- reorgDepth:
|
||||
case <-time.After(time.Second):
|
||||
h.t.Fatalf("unable to send negative confirmation")
|
||||
}
|
||||
}
|
||||
|
||||
// mineBlocks advances the current block height.
|
||||
func (h *chainWatcherTestHarness) mineBlocks(n int32) {
|
||||
h.currentHeight += n
|
||||
}
|
||||
|
||||
// waitForCoopClose waits for a cooperative close event and returns it.
|
||||
func (h *chainWatcherTestHarness) waitForCoopClose(
|
||||
timeout time.Duration) *CooperativeCloseInfo {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
select {
|
||||
case coopClose := <-h.chanEvents.CooperativeClosure:
|
||||
return coopClose
|
||||
case <-time.After(timeout):
|
||||
h.t.Fatalf("didn't receive cooperative close event")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// waitForConfRegistration waits for the chain watcher to register for
|
||||
// confirmation notifications.
|
||||
func (h *chainWatcherTestHarness) waitForConfRegistration() {
|
||||
h.t.Helper()
|
||||
|
||||
select {
|
||||
case <-h.notifier.confRegistered:
|
||||
// Registration complete.
|
||||
case <-time.After(2 * time.Second):
|
||||
// Not necessarily a failure - some tests don't register.
|
||||
}
|
||||
}
|
||||
|
||||
// waitForSpendRegistration waits for the chain watcher to register for
|
||||
// spend notifications.
|
||||
func (h *chainWatcherTestHarness) waitForSpendRegistration() {
|
||||
h.t.Helper()
|
||||
|
||||
select {
|
||||
case <-h.notifier.spendRegistered:
|
||||
// Registration complete.
|
||||
case <-time.After(2 * time.Second):
|
||||
// Not necessarily a failure - some tests don't register.
|
||||
}
|
||||
}
|
||||
|
||||
// assertCoopCloseTx asserts that the given cooperative close info matches
|
||||
// the expected transaction.
|
||||
func (h *chainWatcherTestHarness) assertCoopCloseTx(
|
||||
closeInfo *CooperativeCloseInfo, expectedTx *wire.MsgTx) {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
expectedHash := expectedTx.TxHash()
|
||||
if closeInfo.ClosingTXID != expectedHash {
|
||||
h.t.Fatalf("wrong tx confirmed: expected %v, got %v",
|
||||
expectedHash, closeInfo.ClosingTXID)
|
||||
}
|
||||
}
|
||||
|
||||
// assertNoCoopClose asserts that no cooperative close event is received
|
||||
// within the given timeout.
|
||||
func (h *chainWatcherTestHarness) assertNoCoopClose(timeout time.Duration) {
|
||||
h.t.Helper()
|
||||
|
||||
select {
|
||||
case <-h.chanEvents.CooperativeClosure:
|
||||
h.t.Fatalf("unexpected cooperative close event")
|
||||
case <-time.After(timeout):
|
||||
// Expected timeout.
|
||||
}
|
||||
}
|
||||
|
||||
// runCoopCloseFlow runs a complete cooperative close flow including spend,
|
||||
// optional reorg, and confirmation. This helper coordinates the timing
|
||||
// between the different events.
|
||||
func (h *chainWatcherTestHarness) runCoopCloseFlow(
|
||||
tx *wire.MsgTx, shouldReorg bool, reorgDepth int32,
|
||||
altTx *wire.MsgTx) *CooperativeCloseInfo {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
// Send initial spend notification. The closeObserver's state machine
|
||||
// will detect this and register for confirmations.
|
||||
h.sendSpend(tx)
|
||||
|
||||
// Wait for the chain watcher to register for confirmations.
|
||||
h.waitForConfRegistration()
|
||||
|
||||
if shouldReorg {
|
||||
// Trigger reorg which resets the state machine.
|
||||
h.triggerReorg(tx, reorgDepth)
|
||||
|
||||
// If we have an alternative transaction, send it.
|
||||
if altTx != nil {
|
||||
// After reorg, the chain watcher should re-register for
|
||||
// ANY spend of the funding output.
|
||||
h.waitForSpendRegistration()
|
||||
|
||||
// Send alternative spend.
|
||||
h.sendSpend(altTx)
|
||||
|
||||
// Wait for it to register for confirmations.
|
||||
h.waitForConfRegistration()
|
||||
|
||||
// Confirm alternative transaction to unblock.
|
||||
h.mineBlocks(1)
|
||||
h.confirmTx(altTx, h.currentHeight)
|
||||
}
|
||||
} else {
|
||||
// Normal confirmation flow - confirm to unblock
|
||||
// waitForCoopCloseConfirmation.
|
||||
h.mineBlocks(1)
|
||||
h.confirmTx(tx, h.currentHeight)
|
||||
}
|
||||
|
||||
// Wait for cooperative close event.
|
||||
return h.waitForCoopClose(5 * time.Second)
|
||||
}
|
||||
|
||||
// runMultipleReorgFlow simulates multiple consecutive reorganizations with
|
||||
// different transactions confirming after each reorg.
|
||||
func (h *chainWatcherTestHarness) runMultipleReorgFlow(txs []*wire.MsgTx,
|
||||
reorgDepths []int32) *CooperativeCloseInfo {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
if len(txs) < 2 {
|
||||
h.t.Fatalf("need at least 2 transactions for reorg flow")
|
||||
}
|
||||
if len(reorgDepths) != len(txs)-1 {
|
||||
h.t.Fatalf("reorg depths must be one less than transactions")
|
||||
}
|
||||
|
||||
// Send initial spend.
|
||||
h.sendSpend(txs[0])
|
||||
|
||||
// Process each reorg.
|
||||
for i, depth := range reorgDepths {
|
||||
// Wait for confirmation registration.
|
||||
h.waitForConfRegistration()
|
||||
|
||||
// Trigger reorg for current transaction.
|
||||
h.triggerReorg(txs[i], depth)
|
||||
|
||||
// Wait for re-registration for spend.
|
||||
h.waitForSpendRegistration()
|
||||
|
||||
// Send next transaction.
|
||||
h.sendSpend(txs[i+1])
|
||||
}
|
||||
|
||||
// Wait for final confirmation registration.
|
||||
h.waitForConfRegistration()
|
||||
|
||||
// Confirm the final transaction.
|
||||
finalTx := txs[len(txs)-1]
|
||||
h.mineBlocks(1)
|
||||
h.confirmTx(finalTx, h.currentHeight)
|
||||
|
||||
// Wait for cooperative close event.
|
||||
return h.waitForCoopClose(10 * time.Second)
|
||||
}
|
||||
|
||||
// waitForRemoteUnilateralClose waits for a remote unilateral close event.
|
||||
func (h *chainWatcherTestHarness) waitForRemoteUnilateralClose(
|
||||
timeout time.Duration) *RemoteUnilateralCloseInfo {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
select {
|
||||
case remoteClose := <-h.chanEvents.RemoteUnilateralClosure:
|
||||
return remoteClose
|
||||
case <-time.After(timeout):
|
||||
h.t.Fatalf("didn't receive remote unilateral close event")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// waitForLocalUnilateralClose waits for a local unilateral close event.
|
||||
func (h *chainWatcherTestHarness) waitForLocalUnilateralClose(
|
||||
timeout time.Duration) *LocalUnilateralCloseInfo {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
select {
|
||||
case localClose := <-h.chanEvents.LocalUnilateralClosure:
|
||||
return localClose
|
||||
case <-time.After(timeout):
|
||||
h.t.Fatalf("didn't receive local unilateral close event")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// waitForBreach waits for a breach (contract breach) event.
|
||||
func (h *chainWatcherTestHarness) waitForBreach(
|
||||
timeout time.Duration) *BreachCloseInfo {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
select {
|
||||
case breach := <-h.chanEvents.ContractBreach:
|
||||
return breach
|
||||
case <-time.After(timeout):
|
||||
h.t.Fatalf("didn't receive contract breach event")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// assertRemoteUnilateralCloseTx asserts that the given remote unilateral close
|
||||
// info matches the expected transaction.
|
||||
func (h *chainWatcherTestHarness) assertRemoteUnilateralCloseTx(
|
||||
closeInfo *RemoteUnilateralCloseInfo, expectedTx *wire.MsgTx) {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
expectedHash := expectedTx.TxHash()
|
||||
actualHash := closeInfo.UnilateralCloseSummary.SpendDetail.SpenderTxHash
|
||||
if *actualHash != expectedHash {
|
||||
h.t.Fatalf("wrong tx confirmed: expected %v, got %v",
|
||||
expectedHash, *actualHash)
|
||||
}
|
||||
}
|
||||
|
||||
// assertLocalUnilateralCloseTx asserts that the given local unilateral close
|
||||
// info matches the expected transaction.
|
||||
func (h *chainWatcherTestHarness) assertLocalUnilateralCloseTx(
|
||||
closeInfo *LocalUnilateralCloseInfo, expectedTx *wire.MsgTx) {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
expectedHash := expectedTx.TxHash()
|
||||
actualHash := closeInfo.LocalForceCloseSummary.CloseTx.TxHash()
|
||||
if actualHash != expectedHash {
|
||||
h.t.Fatalf("wrong tx confirmed: expected %v, got %v",
|
||||
expectedHash, actualHash)
|
||||
}
|
||||
}
|
||||
|
||||
// assertBreachTx asserts that the given breach info matches the expected
|
||||
// transaction.
|
||||
func (h *chainWatcherTestHarness) assertBreachTx(
|
||||
breachInfo *BreachCloseInfo, expectedTx *wire.MsgTx) {
|
||||
|
||||
h.t.Helper()
|
||||
|
||||
expectedHash := expectedTx.TxHash()
|
||||
if breachInfo.CommitHash != expectedHash {
|
||||
h.t.Fatalf("wrong tx confirmed: expected %v, got %v",
|
||||
expectedHash, breachInfo.CommitHash)
|
||||
}
|
||||
}
|
||||
|
|
@ -93,13 +93,6 @@
|
|||
ensures dependencies are properly freed and logs the panic trace for
|
||||
debugging.
|
||||
|
||||
* [Improved confirmation scaling for cooperative
|
||||
closes](https://github.com/lightningnetwork/lnd/pull/10331) to provide better
|
||||
reorg protection. Previously, cooperative closes required a minimum of 3
|
||||
confirmations. Now, small channels only require 1 confirmation, while larger
|
||||
channels scale proportionally using the standard 0.16 BTC threshold (matching
|
||||
funding confirmation scaling).
|
||||
|
||||
## RPC Updates
|
||||
|
||||
* The `EstimateRouteFee` RPC now implements an [LSP detection
|
||||
|
|
@ -114,15 +107,6 @@
|
|||
|
||||
## Breaking Changes
|
||||
|
||||
* [Increased MinCLTVDelta from 18 to
|
||||
24](https://github.com/lightningnetwork/lnd/pull/TODO) to provide a larger
|
||||
safety margin above the `DefaultFinalCltvRejectDelta` (19 blocks). This
|
||||
affects users who create invoices with custom `cltv_expiry_delta` values
|
||||
between 18-23, which will now require a minimum of 24. The default value of
|
||||
80 blocks for invoice creation remains unchanged, so most users will not be
|
||||
affected. Existing invoices created before the upgrade will continue to work
|
||||
normally.
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
* [Added new Postgres configuration
|
||||
|
|
@ -161,5 +145,4 @@
|
|||
|
||||
* Abdulkbk
|
||||
* bitromortac
|
||||
* Olaoluwa Osuntokun
|
||||
* Ziggie
|
||||
|
|
|
|||
|
|
@ -727,10 +727,6 @@ var allTestCases = []*lntest.TestCase{
|
|||
Name: "rbf coop close disconnect",
|
||||
TestFunc: testRBFCoopCloseDisconnect,
|
||||
},
|
||||
{
|
||||
Name: "coop close rbf with reorg",
|
||||
TestFunc: testCoopCloseRBFWithReorg,
|
||||
},
|
||||
{
|
||||
Name: "bump fee low budget",
|
||||
TestFunc: testBumpFeeLowBudget,
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ func testUpdateChannelPolicy(ht *lntest.HarnessTest) {
|
|||
// propagated.
|
||||
baseFee = int64(800)
|
||||
feeRate = int64(123)
|
||||
timeLockDelta = uint32(24)
|
||||
timeLockDelta = uint32(22)
|
||||
maxHtlc *= 2
|
||||
inboundBaseFee := int32(-400)
|
||||
inboundFeeRatePpm := int32(-60)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -158,172 +153,3 @@ func testRBFCoopCloseDisconnect(ht *lntest.HarnessTest) {
|
|||
// Disconnect Bob from Alice.
|
||||
ht.DisconnectNodes(alice, bob)
|
||||
}
|
||||
|
||||
// testCoopCloseRBFWithReorg tests that when a cooperative close transaction
|
||||
// is reorganized out during confirmation waiting, the system properly handles
|
||||
// RBF replacements and re-registration for any spend of the funding output.
|
||||
func testCoopCloseRBFWithReorg(ht *lntest.HarnessTest) {
|
||||
// Skip this test for neutrino backend as we can't trigger reorgs.
|
||||
if ht.IsNeutrinoBackend() {
|
||||
ht.Skipf("skipping reorg test for neutrino backend")
|
||||
}
|
||||
|
||||
// Force cooperative close to require 3 confirmations for predictable
|
||||
// testing.
|
||||
const requiredConfs = 3
|
||||
rbfCoopFlags := []string{
|
||||
"--protocol.rbf-coop-close",
|
||||
"--dev.force-channel-close-confs=3",
|
||||
}
|
||||
|
||||
// Set the fee estimate to 1sat/vbyte to ensure our RBF attempts work.
|
||||
ht.SetFeeEstimate(250)
|
||||
ht.SetFeeEstimateWithConf(250, 6)
|
||||
|
||||
// Create two nodes with enough coins for a 50/50 channel.
|
||||
cfgs := [][]string{rbfCoopFlags, rbfCoopFlags}
|
||||
params := lntest.OpenChannelParams{
|
||||
Amt: btcutil.Amount(10_000_000),
|
||||
PushAmt: btcutil.Amount(5_000_000),
|
||||
}
|
||||
chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params)
|
||||
alice, bob := nodes[0], nodes[1]
|
||||
chanPoint := chanPoints[0]
|
||||
|
||||
// Initiate cooperative close with initial fee rate of 5 sat/vb.
|
||||
initialFeeRate := chainfee.SatPerVByte(5)
|
||||
_, aliceCloseUpdate := ht.CloseChannelAssertPending(
|
||||
alice, chanPoint, false,
|
||||
lntest.WithCoopCloseFeeRate(initialFeeRate),
|
||||
lntest.WithLocalTxNotify(),
|
||||
)
|
||||
|
||||
// Verify the initial close transaction is at the expected fee rate.
|
||||
alicePendingUpdate := aliceCloseUpdate.GetClosePending()
|
||||
require.NotNil(ht, aliceCloseUpdate)
|
||||
require.Equal(
|
||||
ht, int64(initialFeeRate), alicePendingUpdate.FeePerVbyte,
|
||||
)
|
||||
|
||||
// Capture the initial close transaction from the mempool.
|
||||
initialCloseTxid, err := chainhash.NewHash(alicePendingUpdate.Txid)
|
||||
require.NoError(ht, err)
|
||||
initialCloseTx := ht.AssertTxInMempool(*initialCloseTxid)
|
||||
|
||||
// Create first RBF replacement before any mining.
|
||||
firstRbfFeeRate := chainfee.SatPerVByte(10)
|
||||
_, firstRbfUpdate := ht.CloseChannelAssertPending(
|
||||
bob, chanPoint, false,
|
||||
lntest.WithCoopCloseFeeRate(firstRbfFeeRate),
|
||||
lntest.WithLocalTxNotify(),
|
||||
)
|
||||
|
||||
// Capture the first RBF transaction.
|
||||
closePending := firstRbfUpdate.GetClosePending()
|
||||
firstRbfTxid, err := chainhash.NewHash(closePending.Txid)
|
||||
require.NoError(ht, err)
|
||||
firstRbfTx := ht.AssertTxInMempool(*firstRbfTxid)
|
||||
|
||||
_, bestHeight := ht.GetBestBlock()
|
||||
ht.Logf("Current block height: %d", bestHeight)
|
||||
|
||||
// Mine n-1 blocks (2 blocks when requiring 3 confirmations) with the
|
||||
// first RBF transaction. This is just shy of full confirmation.
|
||||
block1 := ht.Miner().MineBlockWithTxes(
|
||||
[]*btcutil.Tx{btcutil.NewTx(firstRbfTx)},
|
||||
)
|
||||
|
||||
ht.Logf("Mined block %d with first RBF tx", bestHeight+1)
|
||||
|
||||
block2 := ht.MineEmptyBlocks(1)[0]
|
||||
|
||||
ht.Logf("Mined block %d", bestHeight+2)
|
||||
|
||||
ht.Logf("Re-orging two blocks to remove first RBF tx")
|
||||
|
||||
// Trigger a reorganization that removes the last 2 blocks. This is safe
|
||||
// because we haven't reached full confirmation yet.
|
||||
bestBlockHash := block2.Header.BlockHash()
|
||||
require.NoError(
|
||||
ht, ht.Miner().Client.InvalidateBlock(&bestBlockHash),
|
||||
)
|
||||
bestBlockHash = block1.Header.BlockHash()
|
||||
require.NoError(
|
||||
ht, ht.Miner().Client.InvalidateBlock(&bestBlockHash),
|
||||
)
|
||||
|
||||
_, bestHeight = ht.GetBestBlock()
|
||||
ht.Logf("Re-orged to block height: %d", bestHeight)
|
||||
|
||||
ht.Log("Mining blocks to surpass previous chain")
|
||||
|
||||
// Mine 2 empty blocks to trigger the reorg on the nodes.
|
||||
ht.MineEmptyBlocks(2)
|
||||
|
||||
_, bestHeight = ht.GetBestBlock()
|
||||
ht.Logf("Mined blocks to reach height: %d", bestHeight)
|
||||
|
||||
// Now, instead of mining the second RBF, mine the INITIAL transaction
|
||||
// to test that the system can handle any valid spend of the funding
|
||||
// output.
|
||||
block := ht.Miner().MineBlockWithTxes(
|
||||
[]*btcutil.Tx{btcutil.NewTx(initialCloseTx)},
|
||||
)
|
||||
ht.AssertTxInBlock(block, *initialCloseTxid)
|
||||
|
||||
// Mine additional blocks to reach the required confirmations (3 total).
|
||||
ht.MineEmptyBlocks(requiredConfs - 1)
|
||||
|
||||
// Both parties should see that the channel is now fully closed on chain
|
||||
// with the expected closing txid.
|
||||
expectedClosingTxid := initialCloseTxid.String()
|
||||
err = wait.NoError(func() error {
|
||||
req := &lnrpc.ClosedChannelsRequest{}
|
||||
aliceClosedChans := alice.RPC.ClosedChannels(req)
|
||||
bobClosedChans := bob.RPC.ClosedChannels(req)
|
||||
if len(aliceClosedChans.Channels) != 1 {
|
||||
return fmt.Errorf("alice: expected 1 closed "+
|
||||
"chan, got %d", len(aliceClosedChans.Channels))
|
||||
}
|
||||
if len(bobClosedChans.Channels) != 1 {
|
||||
return fmt.Errorf("bob: expected 1 closed chan, got %d",
|
||||
len(bobClosedChans.Channels))
|
||||
}
|
||||
|
||||
// Verify both Alice and Bob have the expected closing txid.
|
||||
aliceClosedChan := aliceClosedChans.Channels[0]
|
||||
if aliceClosedChan.ClosingTxHash != expectedClosingTxid {
|
||||
return fmt.Errorf("alice: expected closing txid %s, "+
|
||||
"got %s",
|
||||
expectedClosingTxid,
|
||||
aliceClosedChan.ClosingTxHash)
|
||||
}
|
||||
if aliceClosedChan.CloseType !=
|
||||
lnrpc.ChannelCloseSummary_COOPERATIVE_CLOSE {
|
||||
|
||||
return fmt.Errorf("alice: expected cooperative "+
|
||||
"close, got %v",
|
||||
aliceClosedChan.CloseType)
|
||||
}
|
||||
|
||||
bobClosedChan := bobClosedChans.Channels[0]
|
||||
if bobClosedChan.ClosingTxHash != expectedClosingTxid {
|
||||
return fmt.Errorf("bob: expected closing txid %s, "+
|
||||
"got %s",
|
||||
expectedClosingTxid,
|
||||
bobClosedChan.ClosingTxHash)
|
||||
}
|
||||
if bobClosedChan.CloseType !=
|
||||
lnrpc.ChannelCloseSummary_COOPERATIVE_CLOSE {
|
||||
|
||||
return fmt.Errorf("bob: expected cooperative "+
|
||||
"close, got %v",
|
||||
bobClosedChan.CloseType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err)
|
||||
|
||||
ht.Logf("Successfully verified closing txid: %s", expectedClosingTxid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1272,17 +1272,8 @@ func testChannelFundingWithUnstableUtxos(ht *lntest.HarnessTest) {
|
|||
// Make sure Carol sees her to_remote output from the force close tx.
|
||||
ht.AssertNumPendingSweeps(carol, 1)
|
||||
|
||||
// Wait for Carol's sweep transaction to appear in the mempool. Due to
|
||||
// async confirmation notifications, there's a race between when the
|
||||
// sweep is registered and when the sweeper processes the next block.
|
||||
// The sweeper uses immediate=false, so it broadcasts on the next block
|
||||
// after registration. Mine an empty block to trigger the broadcast.
|
||||
ht.MineEmptyBlocks(1)
|
||||
|
||||
// Now the sweep should be in the mempool.
|
||||
ht.AssertNumTxsInMempool(1)
|
||||
|
||||
// Now we should see the unconfirmed UTXO from the sweep.
|
||||
// We need to wait for carol initiating the sweep of the to_remote
|
||||
// output of chanPoint2.
|
||||
utxo := ht.AssertNumUTXOsUnconfirmed(carol, 1)[0]
|
||||
|
||||
// We now try to open channel using the unconfirmed utxo.
|
||||
|
|
@ -1338,11 +1329,6 @@ func testChannelFundingWithUnstableUtxos(ht *lntest.HarnessTest) {
|
|||
// Make sure Carol sees her to_remote output from the force close tx.
|
||||
ht.AssertNumPendingSweeps(carol, 1)
|
||||
|
||||
// Mine an empty block to trigger the sweep broadcast (same fix as
|
||||
// above).
|
||||
ht.MineEmptyBlocks(1)
|
||||
ht.AssertNumTxsInMempool(1)
|
||||
|
||||
// Wait for the to_remote sweep tx to show up in carol's wallet.
|
||||
ht.AssertNumUTXOsUnconfirmed(carol, 1)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
finalCltvDelta = routing.MinCLTVDelta // 24.
|
||||
thawHeightDelta = finalCltvDelta * 2 // 48.
|
||||
finalCltvDelta = routing.MinCLTVDelta // 18.
|
||||
thawHeightDelta = finalCltvDelta * 2 // 36.
|
||||
)
|
||||
|
||||
// makeRouteHints creates a route hints that will allow Carol to be reached
|
||||
|
|
|
|||
|
|
@ -352,7 +352,7 @@ func (b *blindedForwardTest) setupNetwork(ctx context.Context,
|
|||
withInterceptor bool) {
|
||||
|
||||
carolArgs := []string{
|
||||
"--bitcoin.timelockdelta=24",
|
||||
"--bitcoin.timelockdelta=18",
|
||||
fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV),
|
||||
}
|
||||
if withInterceptor {
|
||||
|
|
@ -360,7 +360,7 @@ func (b *blindedForwardTest) setupNetwork(ctx context.Context,
|
|||
}
|
||||
|
||||
daveArgs := []string{
|
||||
"--bitcoin.timelockdelta=24",
|
||||
"--bitcoin.timelockdelta=18",
|
||||
fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV),
|
||||
}
|
||||
cfgs := [][]string{nil, nil, carolArgs, daveArgs}
|
||||
|
|
|
|||
|
|
@ -879,7 +879,7 @@ func testSweepHTLCs(ht *lntest.HarnessTest) {
|
|||
// Before we mine empty blocks to check the RBF behavior, we need to be
|
||||
// aware that Bob's incoming HTLC will expire before his outgoing HTLC
|
||||
// deadline is reached. This happens because the incoming HTLC is sent
|
||||
// onchain at CLTVDelta-BroadcastDelta=24-16=8, which means after 8
|
||||
// onchain at CLTVDelta-BroadcastDelta=18-10=8, which means after 8
|
||||
// blocks are mined, we expect Bob force closes the channel Alice->Bob.
|
||||
blocksTillIncomingSweep := cltvDelta -
|
||||
lncfg.DefaultIncomingBroadcastDelta
|
||||
|
|
|
|||
|
|
@ -20,16 +20,11 @@ const (
|
|||
// DefaultIncomingBroadcastDelta defines the number of blocks before the
|
||||
// expiry of an incoming htlc at which we force close the channel. We
|
||||
// only go to chain if we also have the preimage to actually pull in the
|
||||
// htlc. BOLT #2 suggests 7 blocks. We use more for extra safety.
|
||||
//
|
||||
// The value accounts for:
|
||||
// - Up to 6 blocks waiting for close tx confirmation (reorg safety)
|
||||
// - Time to broadcast and confirm our sweep/2nd level success tx
|
||||
//
|
||||
// Within this window we need to get our sweep confirmed, because after
|
||||
// that the remote party is also able to claim the htlc using the
|
||||
// timeout path.
|
||||
DefaultIncomingBroadcastDelta = 16
|
||||
// htlc. BOLT #2 suggests 7 blocks. We use a few more for extra safety.
|
||||
// Within this window we need to get our sweep or 2nd level success tx
|
||||
// confirmed, because after that the remote party is also able to claim
|
||||
// the htlc using the timeout path.
|
||||
DefaultIncomingBroadcastDelta = 10
|
||||
|
||||
// DefaultFinalCltvRejectDelta defines the number of blocks before the
|
||||
// expiry of an incoming exit hop htlc at which we cancel it back
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ package lncfg
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||
)
|
||||
|
||||
|
|
@ -59,9 +58,3 @@ func (d *DevConfig) GetMaxWaitNumBlocksFundingConf() uint32 {
|
|||
func (d *DevConfig) GetUnsafeConnect() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ChannelCloseConfs returns the config value for channel close confirmations
|
||||
// override, which is always None for production build.
|
||||
func (d *DevConfig) ChannelCloseConfs() fn.Option[uint32] {
|
||||
return fn.None[uint32]()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ package lncfg
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||
)
|
||||
|
||||
|
|
@ -28,7 +27,6 @@ type DevConfig struct {
|
|||
UnsafeDisconnect bool `long:"unsafedisconnect" description:"Allows the rpcserver to intentionally disconnect from peers with open channels."`
|
||||
MaxWaitNumBlocksFundingConf uint32 `long:"maxwaitnumblocksfundingconf" description:"Maximum blocks to wait for funding confirmation before discarding non-initiated channels."`
|
||||
UnsafeConnect bool `long:"unsafeconnect" description:"Allow the rpcserver to connect to a peer even if there's already a connection."`
|
||||
ForceChannelCloseConfs uint32 `long:"force-channel-close-confs" description:"Force a specific number of confirmations for channel closes (dev/test only)"`
|
||||
}
|
||||
|
||||
// ChannelReadyWait returns the config value `ProcessChannelReadyWait`.
|
||||
|
|
@ -73,13 +71,3 @@ func (d *DevConfig) GetMaxWaitNumBlocksFundingConf() uint32 {
|
|||
func (d *DevConfig) GetUnsafeConnect() bool {
|
||||
return d.UnsafeConnect
|
||||
}
|
||||
|
||||
// ChannelCloseConfs returns the forced confirmation count if set, or None if
|
||||
// the default behavior should be used.
|
||||
func (d *DevConfig) ChannelCloseConfs() fn.Option[uint32] {
|
||||
if d.ForceChannelCloseConfs == 0 {
|
||||
return fn.None[uint32]()
|
||||
}
|
||||
|
||||
return fn.Some(d.ForceChannelCloseConfs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,15 +54,15 @@ const (
|
|||
// mining blocks.
|
||||
maxBlocksAllowed = 100
|
||||
|
||||
finalCltvDelta = routing.MinCLTVDelta // 24.
|
||||
thawHeightDelta = finalCltvDelta * 2 // 48.
|
||||
finalCltvDelta = routing.MinCLTVDelta // 18.
|
||||
thawHeightDelta = finalCltvDelta * 2 // 36.
|
||||
)
|
||||
|
||||
var (
|
||||
// MaxBlocksMinedPerTest is the maximum number of blocks that we allow
|
||||
// a test to mine. This is an exported global variable so it can be
|
||||
// overwritten by other projects that don't have the same constraints.
|
||||
MaxBlocksMinedPerTest = 70
|
||||
MaxBlocksMinedPerTest = 50
|
||||
)
|
||||
|
||||
// TestCase defines a test case that's been used in the integration test.
|
||||
|
|
@ -409,13 +409,13 @@ func (h *HarnessTest) checkAndLimitBlocksMined(startHeight int32) {
|
|||
desc += "1. break test into smaller individual tests, especially if " +
|
||||
"this is a table-drive test.\n" +
|
||||
"2. use smaller CSV via `--bitcoin.defaultremotedelay=1.`\n" +
|
||||
"3. use smaller CLTV via `--bitcoin.timelockdelta=24.`\n" +
|
||||
"3. use smaller CLTV via `--bitcoin.timelockdelta=18.`\n" +
|
||||
"4. remove unnecessary CloseChannel when test ends.\n" +
|
||||
"5. use `CreateSimpleNetwork` for efficient channel creation.\n"
|
||||
h.Log(desc)
|
||||
|
||||
// We enforce that the test should not mine more than
|
||||
// MaxBlocksMinedPerTest (70 by default) blocks, which is more than
|
||||
// MaxBlocksMinedPerTest (50 by default) blocks, which is more than
|
||||
// enough to test a multi hop force close scenario.
|
||||
require.LessOrEqualf(
|
||||
h, int(blocksMined), MaxBlocksMinedPerTest,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import (
|
|||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
|
|
@ -553,10 +552,8 @@ func (h HarnessTest) WaitForChannelCloseEvent(
|
|||
require.NoError(h, err)
|
||||
|
||||
resp, ok := event.Update.(*lnrpc.CloseStatusUpdate_ChanClose)
|
||||
require.Truef(
|
||||
h, ok, "expected channel close update, instead got %T: %v",
|
||||
event.Update, spew.Sdump(event.Update),
|
||||
)
|
||||
require.Truef(h, ok, "expected channel close update, instead got %v",
|
||||
event.Update)
|
||||
|
||||
txid, err := chainhash.NewHash(resp.ChanClose.ClosingTxid)
|
||||
require.NoErrorf(h, err, "wrong format found in closing txid: %v",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package mock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
|
|
@ -11,10 +8,9 @@ import (
|
|||
|
||||
// ChainNotifier is a mock implementation of the ChainNotifier interface.
|
||||
type ChainNotifier struct {
|
||||
SpendChan chan *chainntnfs.SpendDetail
|
||||
EpochChan chan *chainntnfs.BlockEpoch
|
||||
ConfChan chan *chainntnfs.TxConfirmation
|
||||
ConfRegistered chan struct{}
|
||||
SpendChan chan *chainntnfs.SpendDetail
|
||||
EpochChan chan *chainntnfs.BlockEpoch
|
||||
ConfChan chan *chainntnfs.TxConfirmation
|
||||
}
|
||||
|
||||
// RegisterConfirmationsNtfn returns a ConfirmationEvent that contains a channel
|
||||
|
|
@ -23,14 +19,6 @@ func (c *ChainNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
|
|||
pkScript []byte, numConfs, heightHint uint32,
|
||||
opts ...chainntnfs.NotifierOption) (*chainntnfs.ConfirmationEvent, error) {
|
||||
|
||||
// Signal that a confirmation registration occurred.
|
||||
if c.ConfRegistered != nil {
|
||||
select {
|
||||
case c.ConfRegistered <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return &chainntnfs.ConfirmationEvent{
|
||||
Confirmed: c.ConfChan,
|
||||
Cancel: func() {},
|
||||
|
|
@ -73,25 +61,3 @@ func (c *ChainNotifier) Started() bool {
|
|||
func (c *ChainNotifier) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitForConfRegistrationAndSend waits for a confirmation registration to
|
||||
// occur and then sends a confirmation notification. This is a helper function
|
||||
// for tests that need to ensure the chain watcher has registered for
|
||||
// confirmations before sending the confirmation.
|
||||
func (c *ChainNotifier) WaitForConfRegistrationAndSend(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
// Wait for the chain watcher to register for confirmations.
|
||||
select {
|
||||
case <-c.ConfRegistered:
|
||||
case <-time.After(time.Second * 2):
|
||||
t.Fatalf("timeout waiting for conf registration")
|
||||
}
|
||||
|
||||
// Send the confirmation to satisfy the confirmation requirement.
|
||||
select {
|
||||
case c.ConfChan <- &chainntnfs.TxConfirmation{}:
|
||||
case <-time.After(time.Second * 1):
|
||||
t.Fatalf("unable to send confirmation")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
package lnwallet
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
const (
|
||||
// minRequiredConfs is the minimum number of confirmations we'll
|
||||
// require for channel operations.
|
||||
minRequiredConfs = 1
|
||||
|
||||
// maxRequiredConfs is the maximum number of confirmations we'll
|
||||
// require for channel operations.
|
||||
maxRequiredConfs = 6
|
||||
|
||||
// maxChannelSize is the maximum expected channel size in satoshis.
|
||||
// This matches MaxBtcFundingAmount (0.16777215 BTC).
|
||||
maxChannelSize = 16777215
|
||||
)
|
||||
|
||||
// ScaleNumConfs returns a linearly scaled number of confirmations based on the
|
||||
// provided channel amount and push amount (for funding transactions). The push
|
||||
// amount represents additional risk when receiving funds.
|
||||
func ScaleNumConfs(chanAmt btcutil.Amount, pushAmt lnwire.MilliSatoshi) uint16 {
|
||||
// For wumbo channels, always require maximum confirmations.
|
||||
if chanAmt > maxChannelSize {
|
||||
return maxRequiredConfs
|
||||
}
|
||||
|
||||
// Calculate total stake: channel amount + push amount. The push amount
|
||||
// represents value at risk for the receiver.
|
||||
maxChannelSizeMsat := lnwire.NewMSatFromSatoshis(maxChannelSize)
|
||||
stake := lnwire.NewMSatFromSatoshis(chanAmt) + pushAmt
|
||||
|
||||
// Scale confirmations linearly based on stake.
|
||||
conf := uint64(maxRequiredConfs) * uint64(stake) /
|
||||
uint64(maxChannelSizeMsat)
|
||||
|
||||
// Bound the result between minRequiredConfs and maxRequiredConfs.
|
||||
if conf < minRequiredConfs {
|
||||
conf = minRequiredConfs
|
||||
}
|
||||
if conf > maxRequiredConfs {
|
||||
conf = maxRequiredConfs
|
||||
}
|
||||
|
||||
return uint16(conf)
|
||||
}
|
||||
|
||||
// FundingConfsForAmounts returns the number of confirmations to wait for a
|
||||
// funding transaction, taking into account both the channel amount and any
|
||||
// pushed amount (which represents additional risk).
|
||||
func FundingConfsForAmounts(chanAmt btcutil.Amount,
|
||||
pushAmt lnwire.MilliSatoshi) uint16 {
|
||||
|
||||
return ScaleNumConfs(chanAmt, pushAmt)
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package lnwallet
|
||||
|
||||
import "github.com/btcsuite/btcd/btcutil"
|
||||
|
||||
// CloseConfsForCapacity returns the number of confirmations to wait
|
||||
// before signaling a cooperative close. Under integration tests, we
|
||||
// always return 1 to keep tests fast and deterministic.
|
||||
func CloseConfsForCapacity(capacity btcutil.Amount) uint32 { //nolint:revive
|
||||
return 1
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
//go:build !integration
|
||||
// +build !integration
|
||||
|
||||
package lnwallet
|
||||
|
||||
import "github.com/btcsuite/btcd/btcutil"
|
||||
|
||||
// CloseConfsForCapacity returns the number of confirmations to wait before
|
||||
// signaling a channel close, scaled by channel capacity. This is used for both
|
||||
// cooperative and force closes. We enforce a minimum of 3 confirmations to
|
||||
// provide better reorg protection, even for small channels.
|
||||
func CloseConfsForCapacity(capacity btcutil.Amount) uint32 {
|
||||
// For cooperative closes, we don't have a push amount to consider,
|
||||
// so we pass 0 for the pushAmt parameter.
|
||||
scaledConfs := uint32(ScaleNumConfs(capacity, 0))
|
||||
|
||||
// Enforce a minimum of 3 confirmations for reorg safety.
|
||||
// This protects against shallow reorgs which are more common.
|
||||
const minCloseConfs = 3
|
||||
if scaledConfs < minCloseConfs {
|
||||
return minCloseConfs
|
||||
}
|
||||
|
||||
return scaledConfs
|
||||
}
|
||||
|
|
@ -1,340 +0,0 @@
|
|||
package lnwallet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
"pgregory.net/rapid"
|
||||
)
|
||||
|
||||
// TestScaleNumConfsProperties tests various properties that ScaleNumConfs
|
||||
// should satisfy using property-based testing.
|
||||
func TestScaleNumConfsProperties(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// The result should always be bounded between the minimum and maximum
|
||||
// number of confirmations regardless of input values.
|
||||
t.Run("bounded_result", func(t *testing.T) {
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
// Generate random channel amount and push amount.
|
||||
chanAmt := rapid.Uint64Range(
|
||||
0, maxChannelSize*10,
|
||||
).Draw(t, "chanAmt")
|
||||
pushAmtSats := rapid.Uint64Range(
|
||||
0, chanAmt,
|
||||
).Draw(t, "pushAmtSats")
|
||||
pushAmt := lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(pushAmtSats),
|
||||
)
|
||||
|
||||
result := ScaleNumConfs(
|
||||
btcutil.Amount(chanAmt), pushAmt,
|
||||
)
|
||||
|
||||
// Check bounds
|
||||
require.GreaterOrEqual(
|
||||
t, result, uint16(minRequiredConfs),
|
||||
"result should be >= minRequiredConfs",
|
||||
)
|
||||
require.LessOrEqual(
|
||||
t, result, uint16(maxRequiredConfs),
|
||||
"result should be <= maxRequiredConfs",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Larger channel amounts and push amounts should require equal or more
|
||||
// confirmations, ensuring the function is monotonically increasing.
|
||||
t.Run("monotonicity", func(t *testing.T) {
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
// Generate two channel amounts where amt1 <= amt2.
|
||||
amt1 := rapid.Uint64Range(
|
||||
0, maxChannelSize,
|
||||
).Draw(t, "amt1")
|
||||
amt2 := rapid.Uint64Range(
|
||||
amt1, maxChannelSize,
|
||||
).Draw(t, "amt2")
|
||||
|
||||
// Generate push amounts proportional to channel size.
|
||||
pushAmt1Sats := rapid.Uint64Range(
|
||||
0, amt1,
|
||||
).Draw(t, "pushAmt1")
|
||||
pushAmt2Sats := rapid.Uint64Range(
|
||||
pushAmt1Sats, amt2,
|
||||
).Draw(t, "pushAmt2")
|
||||
|
||||
pushAmt1 := lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(pushAmt1Sats),
|
||||
)
|
||||
pushAmt2 := lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(pushAmt2Sats),
|
||||
)
|
||||
|
||||
confs1 := ScaleNumConfs(btcutil.Amount(amt1), pushAmt1)
|
||||
confs2 := ScaleNumConfs(btcutil.Amount(amt2), pushAmt2)
|
||||
|
||||
// Larger or equal stake should require equal or more
|
||||
// confirmations.
|
||||
require.GreaterOrEqual(
|
||||
t, confs2, confs1,
|
||||
"larger amount should require equal or "+
|
||||
"more confirmations",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Wumbo channels (those exceeding the max standard channel size) should
|
||||
// always require the maximum number of confirmations for safety.
|
||||
t.Run("wumbo_max_confs", func(t *testing.T) {
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
// Generate wumbo channel amount (above maxChannelSize).
|
||||
wumboAmt := rapid.Uint64Range(
|
||||
maxChannelSize+1, maxChannelSize*100,
|
||||
).Draw(t, "wumboAmt")
|
||||
pushAmtSats := rapid.Uint64Range(
|
||||
0, wumboAmt,
|
||||
).Draw(t, "pushAmtSats")
|
||||
pushAmt := lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(pushAmtSats),
|
||||
)
|
||||
|
||||
result := ScaleNumConfs(
|
||||
btcutil.Amount(wumboAmt), pushAmt,
|
||||
)
|
||||
|
||||
require.Equal(
|
||||
t, uint16(maxRequiredConfs), result,
|
||||
"wumbo channels should always get "+
|
||||
"max confirmations",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Zero channel amounts should always result in the minimum number of
|
||||
// confirmations since there's no value at risk.
|
||||
t.Run("zero_gets_min", func(t *testing.T) {
|
||||
result := ScaleNumConfs(0, 0)
|
||||
require.Equal(
|
||||
t, uint16(minRequiredConfs), result,
|
||||
"zero amount should get minimum confirmations",
|
||||
)
|
||||
})
|
||||
|
||||
// The function should be deterministic, always returning the same
|
||||
// output for the same input values.
|
||||
t.Run("determinism", func(t *testing.T) {
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
chanAmt := rapid.Uint64Range(
|
||||
0, maxChannelSize*2,
|
||||
).Draw(t, "chanAmt")
|
||||
pushAmtSats := rapid.Uint64Range(
|
||||
0, chanAmt,
|
||||
).Draw(t, "pushAmtSats")
|
||||
pushAmt := lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(pushAmtSats),
|
||||
)
|
||||
|
||||
// Call multiple times with same inputs.
|
||||
result1 := ScaleNumConfs(
|
||||
btcutil.Amount(chanAmt), pushAmt,
|
||||
)
|
||||
result2 := ScaleNumConfs(
|
||||
btcutil.Amount(chanAmt), pushAmt,
|
||||
)
|
||||
result3 := ScaleNumConfs(
|
||||
btcutil.Amount(chanAmt), pushAmt,
|
||||
)
|
||||
|
||||
require.Equal(
|
||||
t, result1, result2,
|
||||
"function should be deterministic",
|
||||
)
|
||||
require.Equal(
|
||||
t, result2, result3,
|
||||
"function should be deterministic",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Adding a push amount to a channel should require equal or more
|
||||
// confirmations compared to the same channel without a push amount.
|
||||
t.Run("push_amount_effect", func(t *testing.T) {
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
// Fix channel amount, vary push amount
|
||||
chanAmt := rapid.Uint64Range(
|
||||
1, maxChannelSize,
|
||||
).Draw(t, "chanAmt")
|
||||
pushAmt1Sats := rapid.Uint64Range(
|
||||
0, chanAmt/2,
|
||||
).Draw(t, "pushAmt1")
|
||||
pushAmt2Sats := rapid.Uint64Range(
|
||||
pushAmt1Sats, chanAmt,
|
||||
).Draw(t, "pushAmt2")
|
||||
|
||||
pushAmt1 := lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(pushAmt1Sats),
|
||||
)
|
||||
pushAmt2 := lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(pushAmt2Sats),
|
||||
)
|
||||
|
||||
confs1 := ScaleNumConfs(
|
||||
btcutil.Amount(chanAmt), pushAmt1,
|
||||
)
|
||||
confs2 := ScaleNumConfs(
|
||||
btcutil.Amount(chanAmt), pushAmt2,
|
||||
)
|
||||
|
||||
// More push amount should require equal or more
|
||||
// confirmations.
|
||||
require.GreaterOrEqual(
|
||||
t, confs2, confs1,
|
||||
"larger push amount should "+
|
||||
"require equal or more confirmations",
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestScaleNumConfsKnownValues tests ScaleNumConfs with specific known values
|
||||
// to ensure the scaling formula works as expected.
|
||||
func TestScaleNumConfsKnownValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
chanAmt btcutil.Amount
|
||||
pushAmt lnwire.MilliSatoshi
|
||||
expected uint16
|
||||
}{
|
||||
{
|
||||
name: "zero amounts",
|
||||
chanAmt: 0,
|
||||
pushAmt: 0,
|
||||
expected: minRequiredConfs,
|
||||
},
|
||||
{
|
||||
name: "tiny channel",
|
||||
chanAmt: 1000,
|
||||
pushAmt: 0,
|
||||
expected: minRequiredConfs,
|
||||
},
|
||||
{
|
||||
name: "small channel no push",
|
||||
chanAmt: 100_000,
|
||||
pushAmt: 0,
|
||||
expected: minRequiredConfs,
|
||||
},
|
||||
{
|
||||
name: "half max channel no push",
|
||||
chanAmt: maxChannelSize / 2,
|
||||
pushAmt: 0,
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "max channel no push",
|
||||
chanAmt: maxChannelSize,
|
||||
pushAmt: 0,
|
||||
expected: maxRequiredConfs,
|
||||
},
|
||||
{
|
||||
name: "wumbo channel",
|
||||
chanAmt: maxChannelSize * 2,
|
||||
pushAmt: 0,
|
||||
expected: maxRequiredConfs,
|
||||
},
|
||||
{
|
||||
name: "small channel with push",
|
||||
chanAmt: 100_000,
|
||||
pushAmt: lnwire.NewMSatFromSatoshis(50_000),
|
||||
expected: minRequiredConfs,
|
||||
},
|
||||
{
|
||||
name: "medium channel with significant push",
|
||||
chanAmt: maxChannelSize / 4,
|
||||
pushAmt: lnwire.NewMSatFromSatoshis(
|
||||
maxChannelSize / 4,
|
||||
),
|
||||
expected: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ScaleNumConfs(tc.chanAmt, tc.pushAmt)
|
||||
|
||||
require.Equal(
|
||||
t, tc.expected, result,
|
||||
"chanAmt=%d, pushAmt=%d", tc.chanAmt,
|
||||
tc.pushAmt,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFundingConfsForAmounts verifies that FundingConfsForAmounts is a simple
|
||||
// wrapper around ScaleNumConfs.
|
||||
func TestFundingConfsForAmounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
chanAmt := rapid.Uint64Range(
|
||||
0, maxChannelSize*2,
|
||||
).Draw(t, "chanAmt")
|
||||
pushAmtSats := rapid.Uint64Range(
|
||||
0, chanAmt,
|
||||
).Draw(t, "pushAmtSats")
|
||||
pushAmt := lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(pushAmtSats),
|
||||
)
|
||||
|
||||
// Both functions should return the same result.
|
||||
scaleResult := ScaleNumConfs(btcutil.Amount(chanAmt), pushAmt)
|
||||
fundingResult := FundingConfsForAmounts(
|
||||
btcutil.Amount(chanAmt), pushAmt,
|
||||
)
|
||||
|
||||
require.Equal(
|
||||
t, scaleResult, fundingResult,
|
||||
"FundingConfsForAmounts should return "+
|
||||
"same result as ScaleNumConfs",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCloseConfsForCapacity verifies that CloseConfsForCapacity correctly
|
||||
// wraps ScaleNumConfs with zero push amount and enforces a minimum of 3
|
||||
// confirmations for reorg safety.
|
||||
func TestCloseConfsForCapacity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
capacity := rapid.Uint64Range(
|
||||
0, maxChannelSize*2,
|
||||
).Draw(t, "capacity")
|
||||
|
||||
// CloseConfsForCapacity should be equivalent to ScaleNumConfs
|
||||
// with 0 push, but with a minimum of 3 confirmations enforced
|
||||
// for reorg safety.
|
||||
closeConfs := CloseConfsForCapacity(btcutil.Amount(capacity))
|
||||
scaleConfs := ScaleNumConfs(btcutil.Amount(capacity), 0)
|
||||
|
||||
// The result should be at least the scaled value, but with a
|
||||
// minimum of 3 confirmations.
|
||||
const minCloseConfs = 3
|
||||
expectedConfs := uint32(scaleConfs)
|
||||
if expectedConfs < minCloseConfs {
|
||||
expectedConfs = minCloseConfs
|
||||
}
|
||||
|
||||
require.Equal(
|
||||
t, expectedConfs, closeConfs,
|
||||
"CloseConfsForCapacity should match "+
|
||||
"ScaleNumConfs with 0 push amount, "+
|
||||
"but with minimum of 3 confs",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -370,12 +370,6 @@ type Config struct {
|
|||
// closure initiated by the remote peer.
|
||||
CoopCloseTargetConfs uint32
|
||||
|
||||
// ChannelCloseConfs is an optional override for the number of
|
||||
// confirmations required for channel closes. When set, this overrides
|
||||
// the normal capacity-based scaling. This is only available in
|
||||
// dev/integration builds for testing purposes.
|
||||
ChannelCloseConfs fn.Option[uint32]
|
||||
|
||||
// ServerPubKey is the serialized, compressed public key of our lnd node.
|
||||
// It is used to determine which policy (channel edge) to pass to the
|
||||
// ChannelLink.
|
||||
|
|
@ -4450,22 +4444,9 @@ func (p *Brontide) finalizeChanClosure(chanCloser *chancloser.ChanCloser) {
|
|||
localOut := chanCloser.LocalCloseOutput()
|
||||
remoteOut := chanCloser.RemoteCloseOutput()
|
||||
auxOut := chanCloser.AuxOutputs()
|
||||
|
||||
// Determine the number of confirmations to wait before signaling a
|
||||
// successful cooperative close, scaled by channel capacity (see
|
||||
// CloseConfsForCapacity). Check if we have a config override for
|
||||
// testing purposes.
|
||||
chanCapacity := chanCloser.Channel().Capacity
|
||||
numConfs := p.cfg.ChannelCloseConfs.UnwrapOrFunc(func() uint32 {
|
||||
// No override, use normal capacity-based scaling.
|
||||
return lnwallet.CloseConfsForCapacity(chanCapacity)
|
||||
})
|
||||
|
||||
// Register for full confirmation to send the final update.
|
||||
closeScript := closingTx.TxOut[0].PkScript
|
||||
go WaitForChanToClose(
|
||||
chanCloser.NegotiationHeight(), notifier, errChan,
|
||||
&chanPoint, &closingTxid, closeScript, numConfs, func() {
|
||||
&chanPoint, &closingTxid, closingTx.TxOut[0].PkScript, func() {
|
||||
// Respond to the local subsystem which requested the
|
||||
// channel closure.
|
||||
if closeReq != nil {
|
||||
|
|
@ -4488,14 +4469,14 @@ func (p *Brontide) finalizeChanClosure(chanCloser *chancloser.ChanCloser) {
|
|||
// the function, then it will be sent over the errChan.
|
||||
func WaitForChanToClose(bestHeight uint32, notifier chainntnfs.ChainNotifier,
|
||||
errChan chan error, chanPoint *wire.OutPoint,
|
||||
closingTxID *chainhash.Hash, closeScript []byte, numConfs uint32,
|
||||
cb func()) {
|
||||
closingTxID *chainhash.Hash, closeScript []byte, cb func()) {
|
||||
|
||||
peerLog.Infof("Waiting for confirmation of close of ChannelPoint(%v) "+
|
||||
"with txid: %v", chanPoint, closingTxID)
|
||||
|
||||
// TODO(roasbeef): add param for num needed confs
|
||||
confNtfn, err := notifier.RegisterConfirmationsNtfn(
|
||||
closingTxID, closeScript, numConfs, bestHeight,
|
||||
closingTxID, closeScript, 1, bestHeight,
|
||||
)
|
||||
if err != nil {
|
||||
if errChan != nil {
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ const (
|
|||
// creating incompatibilities during the upgrade process. For some time
|
||||
// LND has used an explicit default final CLTV delta of 40 blocks for
|
||||
// bitcoin, though we now clamp the lower end of this
|
||||
// range for user-chosen deltas to 24 blocks to be conservative.
|
||||
MinCLTVDelta = 24
|
||||
// range for user-chosen deltas to 18 blocks to be conservative.
|
||||
MinCLTVDelta = 18
|
||||
|
||||
// MaxCLTVDelta is the maximum CLTV value accepted by LND for all
|
||||
// timelock deltas.
|
||||
|
|
|
|||
|
|
@ -2807,16 +2807,9 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
|
|||
|
||||
errChan = make(chan error, 1)
|
||||
notifier := r.server.cc.ChainNotifier
|
||||
|
||||
// For force closes, we notify the RPC client immediately after
|
||||
// 1 confirmation. The actual security-critical confirmation
|
||||
// waiting is handled by the channel arbitrator.
|
||||
numConfs := uint32(1)
|
||||
|
||||
go peer.WaitForChanToClose(
|
||||
uint32(bestHeight), notifier, errChan, chanPoint,
|
||||
&closingTxid, closingTx.TxOut[0].PkScript, numConfs,
|
||||
func() {
|
||||
&closingTxid, closingTx.TxOut[0].PkScript, func() {
|
||||
// Respond to the local subsystem which
|
||||
// requested the channel closure.
|
||||
updateChan <- &peer.ChannelCloseUpdate{
|
||||
|
|
|
|||
|
|
@ -1865,7 +1865,7 @@
|
|||
; DefaultIncomingBroadcastDelta set by lnd, otherwise the channel will be force
|
||||
; closed anyway. A warning will be logged on startup if this value is not large
|
||||
; enough to prevent force closes.
|
||||
; invoices.holdexpirydelta=18
|
||||
; invoices.holdexpirydelta=12
|
||||
|
||||
[routing]
|
||||
|
||||
|
|
|
|||
52
server.go
52
server.go
|
|
@ -1361,10 +1361,9 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
|
|||
|
||||
return &pc.Incoming
|
||||
},
|
||||
AuxLeafStore: implCfg.AuxLeafStore,
|
||||
AuxSigner: implCfg.AuxSigner,
|
||||
AuxResolver: implCfg.AuxContractResolver,
|
||||
ChannelCloseConfs: s.cfg.Dev.ChannelCloseConfs(),
|
||||
AuxLeafStore: implCfg.AuxLeafStore,
|
||||
AuxSigner: implCfg.AuxSigner,
|
||||
AuxResolver: implCfg.AuxContractResolver,
|
||||
}, dbs.ChanStateDB)
|
||||
|
||||
// Select the configuration and funding parameters for Bitcoin.
|
||||
|
|
@ -1469,6 +1468,16 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
|
|||
DefaultMinHtlcIn: cc.MinHtlcIn,
|
||||
NumRequiredConfs: func(chanAmt btcutil.Amount,
|
||||
pushAmt lnwire.MilliSatoshi) uint16 {
|
||||
// For large channels we increase the number
|
||||
// of confirmations we require for the
|
||||
// channel to be considered open. As it is
|
||||
// always the responder that gets to choose
|
||||
// value, the pushAmt is value being pushed
|
||||
// to us. This means we have more to lose
|
||||
// in the case this gets re-orged out, and
|
||||
// we will require more confirmations before
|
||||
// we consider it open.
|
||||
|
||||
// In case the user has explicitly specified
|
||||
// a default value for the number of
|
||||
// confirmations, we use it.
|
||||
|
|
@ -1477,17 +1486,29 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
|
|||
return defaultConf
|
||||
}
|
||||
|
||||
// Otherwise, scale the number of confirmations based on
|
||||
// the channel amount and push amount. For large
|
||||
// channels we increase the number of
|
||||
// confirmations we require for the channel to be
|
||||
// considered open. As it is always the
|
||||
// responder that gets to choose value, the
|
||||
// pushAmt is value being pushed to us. This
|
||||
// means we have more to lose in the case this
|
||||
// gets re-orged out, and we will require more
|
||||
// confirmations before we consider it open.
|
||||
return lnwallet.FundingConfsForAmounts(chanAmt, pushAmt)
|
||||
minConf := uint64(3)
|
||||
maxConf := uint64(6)
|
||||
|
||||
// If this is a wumbo channel, then we'll require the
|
||||
// max amount of confirmations.
|
||||
if chanAmt > MaxFundingAmount {
|
||||
return uint16(maxConf)
|
||||
}
|
||||
|
||||
// If not we return a value scaled linearly
|
||||
// between 3 and 6, depending on channel size.
|
||||
// TODO(halseth): Use 1 as minimum?
|
||||
maxChannelSize := uint64(
|
||||
lnwire.NewMSatFromSatoshis(MaxFundingAmount))
|
||||
stake := lnwire.NewMSatFromSatoshis(chanAmt) + pushAmt
|
||||
conf := maxConf * uint64(stake) / maxChannelSize
|
||||
if conf < minConf {
|
||||
conf = minConf
|
||||
}
|
||||
if conf > maxConf {
|
||||
conf = maxConf
|
||||
}
|
||||
return uint16(conf)
|
||||
},
|
||||
RequiredRemoteDelay: func(chanAmt btcutil.Amount) uint16 {
|
||||
// We scale the remote CSV delay (the time the
|
||||
|
|
@ -4389,7 +4410,6 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
|
|||
MaxOutgoingCltvExpiry: s.cfg.MaxOutgoingCltvExpiry,
|
||||
MaxChannelFeeAllocation: s.cfg.MaxChannelFeeAllocation,
|
||||
CoopCloseTargetConfs: s.cfg.CoopCloseTargetConfs,
|
||||
ChannelCloseConfs: s.cfg.Dev.ChannelCloseConfs(),
|
||||
MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte(
|
||||
s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(),
|
||||
ChannelCommitInterval: s.cfg.ChannelCommitInterval,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const (
|
|||
// We adhere to the recommendation in BOLT 02 for terminal payments.
|
||||
// See also:
|
||||
// https://github.com/lightning/bolts/blob/master/02-peer-protocol.md
|
||||
DefaultAssumedFinalCLTVDelta = 24
|
||||
DefaultAssumedFinalCLTVDelta = 18
|
||||
|
||||
// feeRateParts is the total number of parts used to express fee rates.
|
||||
feeRateParts = 1e6
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue