mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-05-13 13:20:08 +02:00
To allow us to write the outcome of our resolver to disk, we add optional resolver reports to the CheckPoint function. Variadic params are used because some checkpoints may have no reports (when the resolver is not yet complete) and some may have two (in the case of a two stage resolution).
302 lines
7.1 KiB
Go
302 lines
7.1 KiB
Go
package contractcourt
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
"github.com/lightningnetwork/lnd/sweep"
|
|
)
|
|
|
|
type commitSweepResolverTestContext struct {
|
|
resolver *commitSweepResolver
|
|
notifier *mockNotifier
|
|
sweeper *mockSweeper
|
|
resolverResultChan chan resolveResult
|
|
t *testing.T
|
|
}
|
|
|
|
func newCommitSweepResolverTestContext(t *testing.T,
|
|
resolution *lnwallet.CommitOutputResolution) *commitSweepResolverTestContext {
|
|
|
|
notifier := &mockNotifier{
|
|
epochChan: make(chan *chainntnfs.BlockEpoch),
|
|
spendChan: make(chan *chainntnfs.SpendDetail),
|
|
confChan: make(chan *chainntnfs.TxConfirmation),
|
|
}
|
|
|
|
sweeper := newMockSweeper()
|
|
|
|
checkPointChan := make(chan struct{}, 1)
|
|
|
|
chainCfg := ChannelArbitratorConfig{
|
|
ChainArbitratorConfig: ChainArbitratorConfig{
|
|
Notifier: notifier,
|
|
Sweeper: sweeper,
|
|
},
|
|
PutResolverReport: func(_ kvdb.RwTx,
|
|
_ *channeldb.ResolverReport) error {
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cfg := ResolverConfig{
|
|
ChannelArbitratorConfig: chainCfg,
|
|
Checkpoint: func(_ ContractResolver,
|
|
_ ...*channeldb.ResolverReport) error {
|
|
|
|
checkPointChan <- struct{}{}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
resolver := newCommitSweepResolver(
|
|
*resolution, 0, wire.OutPoint{}, cfg,
|
|
)
|
|
|
|
return &commitSweepResolverTestContext{
|
|
resolver: resolver,
|
|
notifier: notifier,
|
|
sweeper: sweeper,
|
|
t: t,
|
|
}
|
|
}
|
|
|
|
func (i *commitSweepResolverTestContext) resolve() {
|
|
// Start resolver.
|
|
i.resolverResultChan = make(chan resolveResult, 1)
|
|
go func() {
|
|
nextResolver, err := i.resolver.Resolve()
|
|
i.resolverResultChan <- resolveResult{
|
|
nextResolver: nextResolver,
|
|
err: err,
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (i *commitSweepResolverTestContext) notifyEpoch(height int32) {
|
|
i.notifier.epochChan <- &chainntnfs.BlockEpoch{
|
|
Height: height,
|
|
}
|
|
}
|
|
|
|
func (i *commitSweepResolverTestContext) waitForResult() {
|
|
i.t.Helper()
|
|
|
|
result := <-i.resolverResultChan
|
|
if result.err != nil {
|
|
i.t.Fatal(result.err)
|
|
}
|
|
|
|
if result.nextResolver != nil {
|
|
i.t.Fatal("expected no next resolver")
|
|
}
|
|
}
|
|
|
|
type mockSweeper struct {
|
|
sweptInputs chan input.Input
|
|
updatedInputs chan wire.OutPoint
|
|
sweepErr error
|
|
}
|
|
|
|
func newMockSweeper() *mockSweeper {
|
|
return &mockSweeper{
|
|
sweptInputs: make(chan input.Input),
|
|
updatedInputs: make(chan wire.OutPoint),
|
|
}
|
|
}
|
|
|
|
func (s *mockSweeper) SweepInput(input input.Input, params sweep.Params) (
|
|
chan sweep.Result, error) {
|
|
|
|
s.sweptInputs <- input
|
|
|
|
result := make(chan sweep.Result, 1)
|
|
result <- sweep.Result{
|
|
Tx: &wire.MsgTx{},
|
|
Err: s.sweepErr,
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *mockSweeper) CreateSweepTx(inputs []input.Input, feePref sweep.FeePreference,
|
|
currentBlockHeight uint32) (*wire.MsgTx, error) {
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *mockSweeper) RelayFeePerKW() chainfee.SatPerKWeight {
|
|
return 253
|
|
}
|
|
|
|
func (s *mockSweeper) UpdateParams(input wire.OutPoint,
|
|
params sweep.ParamsUpdate) (chan sweep.Result, error) {
|
|
|
|
s.updatedInputs <- input
|
|
|
|
result := make(chan sweep.Result, 1)
|
|
result <- sweep.Result{
|
|
Tx: &wire.MsgTx{},
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
var _ UtxoSweeper = &mockSweeper{}
|
|
|
|
// TestCommitSweepResolverNoDelay tests resolution of a direct commitment output
|
|
// unencumbered by a time lock.
|
|
func TestCommitSweepResolverNoDelay(t *testing.T) {
|
|
t.Parallel()
|
|
defer timeout(t)()
|
|
|
|
res := lnwallet.CommitOutputResolution{
|
|
SelfOutputSignDesc: input.SignDescriptor{
|
|
Output: &wire.TxOut{
|
|
Value: 100,
|
|
},
|
|
WitnessScript: []byte{0},
|
|
},
|
|
}
|
|
|
|
ctx := newCommitSweepResolverTestContext(t, &res)
|
|
ctx.resolve()
|
|
|
|
ctx.notifier.confChan <- &chainntnfs.TxConfirmation{}
|
|
|
|
// No csv delay, so the input should be swept immediately.
|
|
<-ctx.sweeper.sweptInputs
|
|
|
|
ctx.waitForResult()
|
|
}
|
|
|
|
// testCommitSweepResolverDelay tests resolution of a direct commitment output
|
|
// that is encumbered by a time lock. sweepErr indicates whether the local node
|
|
// fails to sweep the output.
|
|
func testCommitSweepResolverDelay(t *testing.T, sweepErr error) {
|
|
defer timeout(t)()
|
|
|
|
const sweepProcessInterval = 100 * time.Millisecond
|
|
amt := int64(100)
|
|
outpoint := wire.OutPoint{
|
|
Index: 5,
|
|
}
|
|
res := lnwallet.CommitOutputResolution{
|
|
SelfOutputSignDesc: input.SignDescriptor{
|
|
Output: &wire.TxOut{
|
|
Value: amt,
|
|
},
|
|
WitnessScript: []byte{0},
|
|
},
|
|
MaturityDelay: 3,
|
|
SelfOutPoint: outpoint,
|
|
}
|
|
|
|
ctx := newCommitSweepResolverTestContext(t, &res)
|
|
|
|
// Setup whether we expect the sweeper to receive a sweep error in this
|
|
// test case.
|
|
ctx.sweeper.sweepErr = sweepErr
|
|
|
|
report := ctx.resolver.report()
|
|
expectedReport := ContractReport{
|
|
Outpoint: outpoint,
|
|
Type: ReportOutputUnencumbered,
|
|
Amount: btcutil.Amount(amt),
|
|
LimboBalance: btcutil.Amount(amt),
|
|
}
|
|
if *report != expectedReport {
|
|
t.Fatalf("unexpected resolver report. want=%v got=%v",
|
|
expectedReport, report)
|
|
}
|
|
|
|
ctx.resolve()
|
|
|
|
ctx.notifier.confChan <- &chainntnfs.TxConfirmation{
|
|
BlockHeight: testInitialBlockHeight - 1,
|
|
}
|
|
|
|
// Allow resolver to process confirmation.
|
|
time.Sleep(sweepProcessInterval)
|
|
|
|
// Expect report to be updated.
|
|
report = ctx.resolver.report()
|
|
if report.MaturityHeight != testInitialBlockHeight+2 {
|
|
t.Fatal("report maturity height incorrect")
|
|
}
|
|
|
|
// Notify initial block height. The csv lock is still in effect, so we
|
|
// don't expect any sweep to happen yet.
|
|
ctx.notifyEpoch(testInitialBlockHeight)
|
|
|
|
select {
|
|
case <-ctx.sweeper.sweptInputs:
|
|
t.Fatal("no sweep expected")
|
|
case <-time.After(sweepProcessInterval):
|
|
}
|
|
|
|
// A new block arrives. The commit tx confirmed at height -1 and the csv
|
|
// is 3, so a spend will be valid in the first block after height +1.
|
|
ctx.notifyEpoch(testInitialBlockHeight + 1)
|
|
|
|
<-ctx.sweeper.sweptInputs
|
|
|
|
ctx.waitForResult()
|
|
|
|
// If this test case generates a sweep error, we don't expect to be
|
|
// able to recover anything. This might happen if the local commitment
|
|
// output was swept by a justice transaction by the remote party.
|
|
expectedRecoveredBalance := btcutil.Amount(amt)
|
|
if sweepErr != nil {
|
|
expectedRecoveredBalance = 0
|
|
}
|
|
|
|
report = ctx.resolver.report()
|
|
expectedReport = ContractReport{
|
|
Outpoint: outpoint,
|
|
Type: ReportOutputUnencumbered,
|
|
Amount: btcutil.Amount(amt),
|
|
MaturityHeight: testInitialBlockHeight + 2,
|
|
RecoveredBalance: expectedRecoveredBalance,
|
|
}
|
|
if *report != expectedReport {
|
|
t.Fatalf("unexpected resolver report. want=%v got=%v",
|
|
expectedReport, report)
|
|
}
|
|
|
|
}
|
|
|
|
// TestCommitSweepResolverDelay tests resolution of a direct commitment output
|
|
// that is encumbered by a time lock.
|
|
func TestCommitSweepResolverDelay(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
sweepErr error
|
|
}{{
|
|
name: "success",
|
|
sweepErr: nil,
|
|
}, {
|
|
name: "remote spend",
|
|
sweepErr: sweep.ErrRemoteSpend,
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
ok := t.Run(tc.name, func(t *testing.T) {
|
|
testCommitSweepResolverDelay(t, tc.sweepErr)
|
|
})
|
|
if !ok {
|
|
break
|
|
}
|
|
}
|
|
}
|