0407b37fce
This commit enables the user to specify he is not interested in automatically close channels with pending payments that their corresponding htlcs have timed-out. By requiring a configurable grace period uptime of our node before closing such channels, we give a chance to the other node to properly cancel the htlc and avoid unnecessary on-chain transaction. In mobile it is very important for the user experience as otherwise channels will be force closed more frequently.
1905 lines
54 KiB
Go
1905 lines
54 KiB
Go
package contractcourt
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/coreos/bbolt"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/clock"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
)
|
|
|
|
type mockArbitratorLog struct {
|
|
state ArbitratorState
|
|
newStates chan ArbitratorState
|
|
failLog bool
|
|
failFetch error
|
|
failCommit bool
|
|
failCommitState ArbitratorState
|
|
resolutions *ContractResolutions
|
|
resolvers map[ContractResolver]struct{}
|
|
|
|
commitSet *CommitSet
|
|
|
|
sync.Mutex
|
|
}
|
|
|
|
// A compile time check to ensure mockArbitratorLog meets the ArbitratorLog
|
|
// interface.
|
|
var _ ArbitratorLog = (*mockArbitratorLog)(nil)
|
|
|
|
func (b *mockArbitratorLog) CurrentState() (ArbitratorState, error) {
|
|
return b.state, nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) CommitState(s ArbitratorState) error {
|
|
if b.failCommit && s == b.failCommitState {
|
|
return fmt.Errorf("intentional commit error at state %v",
|
|
b.failCommitState)
|
|
}
|
|
b.state = s
|
|
b.newStates <- s
|
|
return nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) FetchUnresolvedContracts() ([]ContractResolver,
|
|
error) {
|
|
|
|
b.Lock()
|
|
v := make([]ContractResolver, len(b.resolvers))
|
|
idx := 0
|
|
for resolver := range b.resolvers {
|
|
v[idx] = resolver
|
|
idx++
|
|
}
|
|
b.Unlock()
|
|
|
|
return v, nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) InsertUnresolvedContracts(
|
|
resolvers ...ContractResolver) error {
|
|
|
|
b.Lock()
|
|
for _, resolver := range resolvers {
|
|
b.resolvers[resolver] = struct{}{}
|
|
}
|
|
b.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) SwapContract(oldContract,
|
|
newContract ContractResolver) error {
|
|
|
|
b.Lock()
|
|
delete(b.resolvers, oldContract)
|
|
b.resolvers[newContract] = struct{}{}
|
|
b.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) ResolveContract(res ContractResolver) error {
|
|
b.Lock()
|
|
delete(b.resolvers, res)
|
|
b.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) LogContractResolutions(c *ContractResolutions) error {
|
|
if b.failLog {
|
|
return fmt.Errorf("intentional log failure")
|
|
}
|
|
b.resolutions = c
|
|
return nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) FetchContractResolutions() (*ContractResolutions, error) {
|
|
if b.failFetch != nil {
|
|
return nil, b.failFetch
|
|
}
|
|
|
|
return b.resolutions, nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) FetchChainActions() (ChainActionMap, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) InsertConfirmedCommitSet(c *CommitSet) error {
|
|
b.commitSet = c
|
|
return nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) FetchConfirmedCommitSet() (*CommitSet, error) {
|
|
return b.commitSet, nil
|
|
}
|
|
|
|
func (b *mockArbitratorLog) WipeHistory() error {
|
|
return nil
|
|
}
|
|
|
|
// testArbLog is a wrapper around an existing (ideally fully concrete
|
|
// ArbitratorLog) that lets us intercept certain calls like transitioning to a
|
|
// new state.
|
|
type testArbLog struct {
|
|
ArbitratorLog
|
|
|
|
newStates chan ArbitratorState
|
|
}
|
|
|
|
func (t *testArbLog) CommitState(s ArbitratorState) error {
|
|
if err := t.ArbitratorLog.CommitState(s); err != nil {
|
|
return err
|
|
}
|
|
|
|
t.newStates <- s
|
|
|
|
return nil
|
|
}
|
|
|
|
type mockChainIO struct{}
|
|
|
|
var _ lnwallet.BlockChainIO = (*mockChainIO)(nil)
|
|
|
|
func (*mockChainIO) GetBestBlock() (*chainhash.Hash, int32, error) {
|
|
return nil, 0, nil
|
|
}
|
|
|
|
func (*mockChainIO) GetUtxo(op *wire.OutPoint, _ []byte,
|
|
heightHint uint32, _ <-chan struct{}) (*wire.TxOut, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (*mockChainIO) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (*mockChainIO) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
type chanArbTestCtx struct {
|
|
t *testing.T
|
|
|
|
chanArb *ChannelArbitrator
|
|
|
|
cleanUp func()
|
|
|
|
resolvedChan chan struct{}
|
|
|
|
blockEpochs chan *chainntnfs.BlockEpoch
|
|
|
|
incubationRequests chan struct{}
|
|
|
|
resolutions chan []ResolutionMsg
|
|
|
|
log ArbitratorLog
|
|
}
|
|
|
|
func (c *chanArbTestCtx) CleanUp() {
|
|
if err := c.chanArb.Stop(); err != nil {
|
|
c.t.Fatalf("unable to stop chan arb: %v", err)
|
|
}
|
|
|
|
if c.cleanUp != nil {
|
|
c.cleanUp()
|
|
}
|
|
}
|
|
|
|
// AssertStateTransitions asserts that the state machine steps through the
|
|
// passed states in order.
|
|
func (c *chanArbTestCtx) AssertStateTransitions(expectedStates ...ArbitratorState) {
|
|
c.t.Helper()
|
|
|
|
var newStatesChan chan ArbitratorState
|
|
switch log := c.log.(type) {
|
|
case *mockArbitratorLog:
|
|
newStatesChan = log.newStates
|
|
|
|
case *testArbLog:
|
|
newStatesChan = log.newStates
|
|
|
|
default:
|
|
c.t.Fatalf("unable to assert state transitions with %T", log)
|
|
}
|
|
|
|
for _, exp := range expectedStates {
|
|
var state ArbitratorState
|
|
select {
|
|
case state = <-newStatesChan:
|
|
case <-time.After(5 * time.Second):
|
|
c.t.Fatalf("new state not received")
|
|
}
|
|
|
|
if state != exp {
|
|
c.t.Fatalf("expected new state %v, got %v", exp, state)
|
|
}
|
|
}
|
|
}
|
|
|
|
// AssertState checks that the ChannelArbitrator is in the state we expect it
|
|
// to be.
|
|
func (c *chanArbTestCtx) AssertState(expected ArbitratorState) {
|
|
if c.chanArb.state != expected {
|
|
c.t.Fatalf("expected state %v, was %v", expected, c.chanArb.state)
|
|
}
|
|
}
|
|
|
|
// Restart simulates a clean restart of the channel arbitrator, forcing it to
|
|
// walk through it's recovery logic. If this function returns nil, then a
|
|
// restart was successful. Note that the restart process keeps the log in
|
|
// place, in order to simulate proper persistence of the log. The caller can
|
|
// optionally provide a restart closure which will be executed before the
|
|
// resolver is started again, but after it is created.
|
|
func (c *chanArbTestCtx) Restart(restartClosure func(*chanArbTestCtx)) (*chanArbTestCtx, error) {
|
|
if err := c.chanArb.Stop(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newCtx, err := createTestChannelArbitrator(c.t, c.log)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if restartClosure != nil {
|
|
restartClosure(newCtx)
|
|
}
|
|
|
|
if err := newCtx.chanArb.Start(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newCtx, nil
|
|
}
|
|
|
|
func createTestChannelArbitrator(t *testing.T, log ArbitratorLog) (*chanArbTestCtx, error) {
|
|
blockEpochs := make(chan *chainntnfs.BlockEpoch)
|
|
blockEpoch := &chainntnfs.BlockEpochEvent{
|
|
Epochs: blockEpochs,
|
|
Cancel: func() {},
|
|
}
|
|
|
|
chanPoint := wire.OutPoint{}
|
|
shortChanID := lnwire.ShortChannelID{}
|
|
chanEvents := &ChainEventSubscription{
|
|
RemoteUnilateralClosure: make(chan *RemoteUnilateralCloseInfo, 1),
|
|
LocalUnilateralClosure: make(chan *LocalUnilateralCloseInfo, 1),
|
|
CooperativeClosure: make(chan *CooperativeCloseInfo, 1),
|
|
ContractBreach: make(chan *lnwallet.BreachRetribution, 1),
|
|
}
|
|
|
|
resolutionChan := make(chan []ResolutionMsg, 1)
|
|
incubateChan := make(chan struct{})
|
|
|
|
chainIO := &mockChainIO{}
|
|
chainArbCfg := ChainArbitratorConfig{
|
|
ChainIO: chainIO,
|
|
PublishTx: func(*wire.MsgTx) error {
|
|
return nil
|
|
},
|
|
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error {
|
|
resolutionChan <- msgs
|
|
return nil
|
|
},
|
|
OutgoingBroadcastDelta: 5,
|
|
IncomingBroadcastDelta: 5,
|
|
Notifier: &mockNotifier{
|
|
epochChan: make(chan *chainntnfs.BlockEpoch),
|
|
spendChan: make(chan *chainntnfs.SpendDetail),
|
|
confChan: make(chan *chainntnfs.TxConfirmation),
|
|
},
|
|
IncubateOutputs: func(wire.OutPoint,
|
|
*lnwallet.OutgoingHtlcResolution,
|
|
*lnwallet.IncomingHtlcResolution, uint32) error {
|
|
|
|
incubateChan <- struct{}{}
|
|
return nil
|
|
},
|
|
OnionProcessor: &mockOnionProcessor{},
|
|
IsForwardedHTLC: func(chanID lnwire.ShortChannelID,
|
|
htlcIndex uint64) bool {
|
|
|
|
return true
|
|
},
|
|
Clock: clock.NewDefaultClock(),
|
|
}
|
|
|
|
// We'll use the resolvedChan to synchronize on call to
|
|
// MarkChannelResolved.
|
|
resolvedChan := make(chan struct{}, 1)
|
|
|
|
// Next we'll create the matching configuration struct that contains
|
|
// all interfaces and methods the arbitrator needs to do its job.
|
|
arbCfg := ChannelArbitratorConfig{
|
|
ChanPoint: chanPoint,
|
|
ShortChanID: shortChanID,
|
|
BlockEpochs: blockEpoch,
|
|
MarkChannelResolved: func() error {
|
|
resolvedChan <- struct{}{}
|
|
return nil
|
|
},
|
|
ForceCloseChan: func() (*lnwallet.LocalForceCloseSummary, error) {
|
|
summary := &lnwallet.LocalForceCloseSummary{
|
|
CloseTx: &wire.MsgTx{},
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
}
|
|
return summary, nil
|
|
},
|
|
MarkCommitmentBroadcasted: func(_ *wire.MsgTx) error {
|
|
return nil
|
|
},
|
|
MarkChannelClosed: func(*channeldb.ChannelCloseSummary) error {
|
|
return nil
|
|
},
|
|
IsPendingClose: false,
|
|
ChainArbitratorConfig: chainArbCfg,
|
|
ChainEvents: chanEvents,
|
|
}
|
|
|
|
var cleanUp func()
|
|
if log == nil {
|
|
dbDir, err := ioutil.TempDir("", "chanArb")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dbPath := filepath.Join(dbDir, "testdb")
|
|
db, err := bbolt.Open(dbPath, 0600, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
backingLog, err := newBoltArbitratorLog(
|
|
db, arbCfg, chainhash.Hash{}, chanPoint,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cleanUp = func() {
|
|
db.Close()
|
|
os.RemoveAll(dbDir)
|
|
}
|
|
|
|
log = &testArbLog{
|
|
ArbitratorLog: backingLog,
|
|
newStates: make(chan ArbitratorState),
|
|
}
|
|
}
|
|
|
|
htlcSets := make(map[HtlcSetKey]htlcSet)
|
|
|
|
chanArb := NewChannelArbitrator(arbCfg, htlcSets, log)
|
|
|
|
return &chanArbTestCtx{
|
|
t: t,
|
|
chanArb: chanArb,
|
|
cleanUp: cleanUp,
|
|
resolvedChan: resolvedChan,
|
|
resolutions: resolutionChan,
|
|
blockEpochs: blockEpochs,
|
|
log: log,
|
|
incubationRequests: incubateChan,
|
|
}, nil
|
|
}
|
|
|
|
// TestChannelArbitratorCooperativeClose tests that the ChannelArbitertor
|
|
// correctly marks the channel resolved in case a cooperative close is
|
|
// confirmed.
|
|
func TestChannelArbitratorCooperativeClose(t *testing.T) {
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
if err := chanArbCtx.chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := chanArbCtx.chanArb.Stop(); err != nil {
|
|
t.Fatalf("unable to stop chan arb: %v", err)
|
|
}
|
|
}()
|
|
|
|
// It should start out in the default state.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// We set up a channel to detect when MarkChannelClosed is called.
|
|
closeInfos := make(chan *channeldb.ChannelCloseSummary)
|
|
chanArbCtx.chanArb.cfg.MarkChannelClosed = func(
|
|
closeInfo *channeldb.ChannelCloseSummary) error {
|
|
closeInfos <- closeInfo
|
|
return nil
|
|
}
|
|
|
|
// Cooperative close should do trigger a MarkChannelClosed +
|
|
// MarkChannelResolved.
|
|
closeInfo := &CooperativeCloseInfo{
|
|
&channeldb.ChannelCloseSummary{},
|
|
}
|
|
chanArbCtx.chanArb.cfg.ChainEvents.CooperativeClosure <- closeInfo
|
|
|
|
select {
|
|
case c := <-closeInfos:
|
|
if c.CloseType != channeldb.CooperativeClose {
|
|
t.Fatalf("expected cooperative close, got %v", c.CloseType)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("timeout waiting for channel close")
|
|
}
|
|
|
|
// It should mark the channel as resolved.
|
|
select {
|
|
case <-chanArbCtx.resolvedChan:
|
|
// Expected.
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorRemoteForceClose checks that the ChannelArbitrator goes
|
|
// through the expected states if a remote force close is observed in the
|
|
// chain.
|
|
func TestChannelArbitratorRemoteForceClose(t *testing.T) {
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb := chanArbCtx.chanArb
|
|
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer chanArb.Stop()
|
|
|
|
// It should start out in the default state.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// Send a remote force close event.
|
|
commitSpend := &chainntnfs.SpendDetail{
|
|
SpenderTxHash: &chainhash.Hash{},
|
|
}
|
|
|
|
uniClose := &lnwallet.UnilateralCloseSummary{
|
|
SpendDetail: commitSpend,
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
}
|
|
chanArb.cfg.ChainEvents.RemoteUnilateralClosure <- &RemoteUnilateralCloseInfo{
|
|
UnilateralCloseSummary: uniClose,
|
|
CommitSet: CommitSet{
|
|
ConfCommitKey: &RemoteHtlcSet,
|
|
HtlcSets: make(map[HtlcSetKey][]channeldb.HTLC),
|
|
},
|
|
}
|
|
|
|
// It should transition StateDefault -> StateContractClosed ->
|
|
// StateFullyResolved.
|
|
chanArbCtx.AssertStateTransitions(
|
|
StateContractClosed, StateFullyResolved,
|
|
)
|
|
|
|
// It should also mark the channel as resolved.
|
|
select {
|
|
case <-chanArbCtx.resolvedChan:
|
|
// Expected.
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorLocalForceClose tests that the ChannelArbitrator goes
|
|
// through the expected states in case we request it to force close the channel,
|
|
// and the local force close event is observed in chain.
|
|
func TestChannelArbitratorLocalForceClose(t *testing.T) {
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb := chanArbCtx.chanArb
|
|
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer chanArb.Stop()
|
|
|
|
// It should start out in the default state.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// We create a channel we can use to pause the ChannelArbitrator at the
|
|
// point where it broadcasts the close tx, and check its state.
|
|
stateChan := make(chan ArbitratorState)
|
|
chanArb.cfg.PublishTx = func(*wire.MsgTx) error {
|
|
// When the force close tx is being broadcasted, check that the
|
|
// state is correct at that point.
|
|
select {
|
|
case stateChan <- chanArb.state:
|
|
case <-chanArb.quit:
|
|
return fmt.Errorf("exiting")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
errChan := make(chan error, 1)
|
|
respChan := make(chan *wire.MsgTx, 1)
|
|
|
|
// With the channel found, and the request crafted, we'll send over a
|
|
// force close request to the arbitrator that watches this channel.
|
|
chanArb.forceCloseReqs <- &forceCloseReq{
|
|
errResp: errChan,
|
|
closeTx: respChan,
|
|
}
|
|
|
|
// It should transition to StateBroadcastCommit.
|
|
chanArbCtx.AssertStateTransitions(StateBroadcastCommit)
|
|
|
|
// When it is broadcasting the force close, its state should be
|
|
// StateBroadcastCommit.
|
|
select {
|
|
case state := <-stateChan:
|
|
if state != StateBroadcastCommit {
|
|
t.Fatalf("state during PublishTx was %v", state)
|
|
}
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatalf("did not get state update")
|
|
}
|
|
|
|
// After broadcasting, transition should be to
|
|
// StateCommitmentBroadcasted.
|
|
chanArbCtx.AssertStateTransitions(StateCommitmentBroadcasted)
|
|
|
|
select {
|
|
case <-respChan:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
select {
|
|
case err := <-errChan:
|
|
if err != nil {
|
|
t.Fatalf("error force closing channel: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
// After broadcasting the close tx, it should be in state
|
|
// StateCommitmentBroadcasted.
|
|
chanArbCtx.AssertState(StateCommitmentBroadcasted)
|
|
|
|
// Now notify about the local force close getting confirmed.
|
|
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
|
SpendDetail: &chainntnfs.SpendDetail{},
|
|
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
|
CloseTx: &wire.MsgTx{},
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
},
|
|
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
|
}
|
|
|
|
// It should transition StateContractClosed -> StateFullyResolved.
|
|
chanArbCtx.AssertStateTransitions(StateContractClosed, StateFullyResolved)
|
|
|
|
// It should also mark the channel as resolved.
|
|
select {
|
|
case <-chanArbCtx.resolvedChan:
|
|
// Expected.
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorBreachClose tests that the ChannelArbitrator goes
|
|
// through the expected states in case we notice a breach in the chain, and
|
|
// gracefully exits.
|
|
func TestChannelArbitratorBreachClose(t *testing.T) {
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb := chanArbCtx.chanArb
|
|
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := chanArb.Stop(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
// It should start out in the default state.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// Send a breach close event.
|
|
chanArb.cfg.ChainEvents.ContractBreach <- &lnwallet.BreachRetribution{}
|
|
|
|
// It should transition StateDefault -> StateFullyResolved.
|
|
chanArbCtx.AssertStateTransitions(
|
|
StateFullyResolved,
|
|
)
|
|
|
|
// It should also mark the channel as resolved.
|
|
select {
|
|
case <-chanArbCtx.resolvedChan:
|
|
// Expected.
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorLocalForceClosePendingHtlc tests that the
|
|
// ChannelArbitrator goes through the expected states in case we request it to
|
|
// force close a channel that still has an HTLC pending.
|
|
func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
|
// We create a new test context for this channel arb, notice that we
|
|
// pass in a nil ArbitratorLog which means that a default one backed by
|
|
// a real DB will be created. We need this for our test as we want to
|
|
// test proper restart recovery and resolver population.
|
|
chanArbCtx, err := createTestChannelArbitrator(t, nil)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb := chanArbCtx.chanArb
|
|
chanArb.cfg.PreimageDB = newMockWitnessBeacon()
|
|
chanArb.cfg.Registry = &mockRegistry{}
|
|
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer chanArb.Stop()
|
|
|
|
// Create htlcUpdates channel.
|
|
htlcUpdates := make(chan *ContractUpdate)
|
|
|
|
signals := &ContractSignals{
|
|
HtlcUpdates: htlcUpdates,
|
|
ShortChanID: lnwire.ShortChannelID{},
|
|
}
|
|
chanArb.UpdateContractSignals(signals)
|
|
|
|
// Add HTLC to channel arbitrator.
|
|
htlcAmt := 10000
|
|
htlc := channeldb.HTLC{
|
|
Incoming: false,
|
|
Amt: lnwire.MilliSatoshi(htlcAmt),
|
|
HtlcIndex: 99,
|
|
}
|
|
|
|
outgoingDustHtlc := channeldb.HTLC{
|
|
Incoming: false,
|
|
Amt: 100,
|
|
HtlcIndex: 100,
|
|
OutputIndex: -1,
|
|
}
|
|
|
|
incomingDustHtlc := channeldb.HTLC{
|
|
Incoming: true,
|
|
Amt: 105,
|
|
HtlcIndex: 101,
|
|
OutputIndex: -1,
|
|
}
|
|
|
|
htlcSet := []channeldb.HTLC{
|
|
htlc, outgoingDustHtlc, incomingDustHtlc,
|
|
}
|
|
|
|
htlcUpdates <- &ContractUpdate{
|
|
HtlcKey: LocalHtlcSet,
|
|
Htlcs: htlcSet,
|
|
}
|
|
|
|
errChan := make(chan error, 1)
|
|
respChan := make(chan *wire.MsgTx, 1)
|
|
|
|
// With the channel found, and the request crafted, we'll send over a
|
|
// force close request to the arbitrator that watches this channel.
|
|
chanArb.forceCloseReqs <- &forceCloseReq{
|
|
errResp: errChan,
|
|
closeTx: respChan,
|
|
}
|
|
|
|
// The force close request should trigger broadcast of the commitment
|
|
// transaction.
|
|
chanArbCtx.AssertStateTransitions(
|
|
StateBroadcastCommit,
|
|
StateCommitmentBroadcasted,
|
|
)
|
|
select {
|
|
case <-respChan:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
select {
|
|
case err := <-errChan:
|
|
if err != nil {
|
|
t.Fatalf("error force closing channel: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
// Now notify about the local force close getting confirmed.
|
|
closeTx := &wire.MsgTx{
|
|
TxIn: []*wire.TxIn{
|
|
{
|
|
PreviousOutPoint: wire.OutPoint{},
|
|
Witness: [][]byte{
|
|
{0x1},
|
|
{0x2},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
htlcOp := wire.OutPoint{
|
|
Hash: closeTx.TxHash(),
|
|
Index: 0,
|
|
}
|
|
|
|
// Set up the outgoing resolution. Populate SignedTimeoutTx because our
|
|
// commitment transaction got confirmed.
|
|
outgoingRes := lnwallet.OutgoingHtlcResolution{
|
|
Expiry: 10,
|
|
SweepSignDesc: input.SignDescriptor{
|
|
Output: &wire.TxOut{},
|
|
},
|
|
SignedTimeoutTx: &wire.MsgTx{
|
|
TxIn: []*wire.TxIn{
|
|
{
|
|
PreviousOutPoint: htlcOp,
|
|
Witness: [][]byte{{}},
|
|
},
|
|
},
|
|
TxOut: []*wire.TxOut{
|
|
{},
|
|
},
|
|
},
|
|
}
|
|
|
|
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
|
SpendDetail: &chainntnfs.SpendDetail{},
|
|
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
|
CloseTx: closeTx,
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{
|
|
OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
|
|
outgoingRes,
|
|
},
|
|
},
|
|
},
|
|
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
|
CommitSet: CommitSet{
|
|
ConfCommitKey: &LocalHtlcSet,
|
|
HtlcSets: map[HtlcSetKey][]channeldb.HTLC{
|
|
LocalHtlcSet: htlcSet,
|
|
},
|
|
},
|
|
}
|
|
|
|
chanArbCtx.AssertStateTransitions(
|
|
StateContractClosed,
|
|
StateWaitingFullResolution,
|
|
)
|
|
|
|
// We expect an immediate resolution message for the outgoing dust htlc.
|
|
// It is not resolvable on-chain.
|
|
select {
|
|
case msgs := <-chanArbCtx.resolutions:
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("expected 1 message, instead got %v", len(msgs))
|
|
}
|
|
|
|
if msgs[0].HtlcIndex != outgoingDustHtlc.HtlcIndex {
|
|
t.Fatalf("wrong htlc index: expected %v, got %v",
|
|
outgoingDustHtlc.HtlcIndex, msgs[0].HtlcIndex)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("resolution msgs not sent")
|
|
}
|
|
|
|
// We'll grab the old notifier here as our resolvers are still holding
|
|
// a reference to this instance, and a new one will be created when we
|
|
// restart the channel arb below.
|
|
oldNotifier := chanArb.cfg.Notifier.(*mockNotifier)
|
|
|
|
// At this point, in order to simulate a restart, we'll re-create the
|
|
// channel arbitrator. We do this to ensure that all information
|
|
// required to properly resolve this HTLC are populated.
|
|
if err := chanArb.Stop(); err != nil {
|
|
t.Fatalf("unable to stop chan arb: %v", err)
|
|
}
|
|
|
|
// We'll no re-create the resolver, notice that we use the existing
|
|
// arbLog so it carries over the same on-disk state.
|
|
chanArbCtxNew, err := chanArbCtx.Restart(nil)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb = chanArbCtxNew.chanArb
|
|
defer chanArbCtxNew.CleanUp()
|
|
|
|
// Post restart, it should be the case that our resolver was properly
|
|
// supplemented, and we only have a single resolver in the final set.
|
|
if len(chanArb.activeResolvers) != 1 {
|
|
t.Fatalf("expected single resolver, instead got: %v",
|
|
len(chanArb.activeResolvers))
|
|
}
|
|
|
|
// We'll now examine the in-memory state of the active resolvers to
|
|
// ensure t hey were populated properly.
|
|
resolver := chanArb.activeResolvers[0]
|
|
outgoingResolver, ok := resolver.(*htlcOutgoingContestResolver)
|
|
if !ok {
|
|
t.Fatalf("expected outgoing contest resolver, got %vT",
|
|
resolver)
|
|
}
|
|
|
|
// The resolver should have its htlc amt field populated as it.
|
|
if int64(outgoingResolver.htlc.Amt) != int64(htlcAmt) {
|
|
t.Fatalf("wrong htlc amount: expected %v, got %v,",
|
|
htlcAmt, int64(outgoingResolver.htlc.Amt))
|
|
}
|
|
|
|
// htlcOutgoingContestResolver is now active and waiting for the HTLC to
|
|
// expire. It should not yet have passed it on for incubation.
|
|
select {
|
|
case <-chanArbCtx.incubationRequests:
|
|
t.Fatalf("contract should not be incubated yet")
|
|
default:
|
|
}
|
|
|
|
// Send a notification that the expiry height has been reached.
|
|
oldNotifier.epochChan <- &chainntnfs.BlockEpoch{Height: 10}
|
|
|
|
// htlcOutgoingContestResolver is now transforming into a
|
|
// htlcTimeoutResolver and should send the contract off for incubation.
|
|
select {
|
|
case <-chanArbCtx.incubationRequests:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
// Notify resolver that the HTLC output of the commitment has been
|
|
// spent.
|
|
oldNotifier.spendChan <- &chainntnfs.SpendDetail{SpendingTx: closeTx}
|
|
|
|
// Finally, we should also receive a resolution message instructing the
|
|
// switch to cancel back the HTLC.
|
|
select {
|
|
case msgs := <-chanArbCtx.resolutions:
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("expected 1 message, instead got %v", len(msgs))
|
|
}
|
|
|
|
if msgs[0].HtlcIndex != htlc.HtlcIndex {
|
|
t.Fatalf("wrong htlc index: expected %v, got %v",
|
|
htlc.HtlcIndex, msgs[0].HtlcIndex)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("resolution msgs not sent")
|
|
}
|
|
|
|
// As this is our own commitment transaction, the HTLC will go through
|
|
// to the second level. Channel arbitrator should still not be marked
|
|
// as resolved.
|
|
select {
|
|
case <-chanArbCtxNew.resolvedChan:
|
|
t.Fatalf("channel resolved prematurely")
|
|
default:
|
|
}
|
|
|
|
// Notify resolver that the second level transaction is spent.
|
|
oldNotifier.spendChan <- &chainntnfs.SpendDetail{SpendingTx: closeTx}
|
|
|
|
// At this point channel should be marked as resolved.
|
|
chanArbCtxNew.AssertStateTransitions(StateFullyResolved)
|
|
select {
|
|
case <-chanArbCtxNew.resolvedChan:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorLocalForceCloseRemoteConfiremd tests that the
|
|
// ChannelArbitrator behaves as expected in the case where we request a local
|
|
// force close, but a remote commitment ends up being confirmed in chain.
|
|
func TestChannelArbitratorLocalForceCloseRemoteConfirmed(t *testing.T) {
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb := chanArbCtx.chanArb
|
|
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer chanArb.Stop()
|
|
|
|
// It should start out in the default state.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// Create a channel we can use to assert the state when it publishes
|
|
// the close tx.
|
|
stateChan := make(chan ArbitratorState)
|
|
chanArb.cfg.PublishTx = func(*wire.MsgTx) error {
|
|
// When the force close tx is being broadcasted, check that the
|
|
// state is correct at that point.
|
|
select {
|
|
case stateChan <- chanArb.state:
|
|
case <-chanArb.quit:
|
|
return fmt.Errorf("exiting")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
errChan := make(chan error, 1)
|
|
respChan := make(chan *wire.MsgTx, 1)
|
|
|
|
// With the channel found, and the request crafted, we'll send over a
|
|
// force close request to the arbitrator that watches this channel.
|
|
chanArb.forceCloseReqs <- &forceCloseReq{
|
|
errResp: errChan,
|
|
closeTx: respChan,
|
|
}
|
|
|
|
// It should transition to StateBroadcastCommit.
|
|
chanArbCtx.AssertStateTransitions(StateBroadcastCommit)
|
|
|
|
// We expect it to be in state StateBroadcastCommit when publishing
|
|
// the force close.
|
|
select {
|
|
case state := <-stateChan:
|
|
if state != StateBroadcastCommit {
|
|
t.Fatalf("state during PublishTx was %v", state)
|
|
}
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatalf("no state update received")
|
|
}
|
|
|
|
// After broadcasting, transition should be to
|
|
// StateCommitmentBroadcasted.
|
|
chanArbCtx.AssertStateTransitions(StateCommitmentBroadcasted)
|
|
|
|
// Wait for a response to the force close.
|
|
select {
|
|
case <-respChan:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
select {
|
|
case err := <-errChan:
|
|
if err != nil {
|
|
t.Fatalf("error force closing channel: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
// The state should be StateCommitmentBroadcasted.
|
|
chanArbCtx.AssertState(StateCommitmentBroadcasted)
|
|
|
|
// Now notify about the _REMOTE_ commitment getting confirmed.
|
|
commitSpend := &chainntnfs.SpendDetail{
|
|
SpenderTxHash: &chainhash.Hash{},
|
|
}
|
|
uniClose := &lnwallet.UnilateralCloseSummary{
|
|
SpendDetail: commitSpend,
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
}
|
|
chanArb.cfg.ChainEvents.RemoteUnilateralClosure <- &RemoteUnilateralCloseInfo{
|
|
UnilateralCloseSummary: uniClose,
|
|
}
|
|
|
|
// It should transition StateContractClosed -> StateFullyResolved.
|
|
chanArbCtx.AssertStateTransitions(StateContractClosed, StateFullyResolved)
|
|
|
|
// It should resolve.
|
|
select {
|
|
case <-chanArbCtx.resolvedChan:
|
|
// Expected.
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorLocalForceCloseDoubleSpend tests that the
|
|
// ChannelArbitrator behaves as expected in the case where we request a local
|
|
// force close, but we fail broadcasting our commitment because a remote
|
|
// commitment has already been published.
|
|
func TestChannelArbitratorLocalForceDoubleSpend(t *testing.T) {
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb := chanArbCtx.chanArb
|
|
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer chanArb.Stop()
|
|
|
|
// It should start out in the default state.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// Return ErrDoubleSpend when attempting to publish the tx.
|
|
stateChan := make(chan ArbitratorState)
|
|
chanArb.cfg.PublishTx = func(*wire.MsgTx) error {
|
|
// When the force close tx is being broadcasted, check that the
|
|
// state is correct at that point.
|
|
select {
|
|
case stateChan <- chanArb.state:
|
|
case <-chanArb.quit:
|
|
return fmt.Errorf("exiting")
|
|
}
|
|
return lnwallet.ErrDoubleSpend
|
|
}
|
|
|
|
errChan := make(chan error, 1)
|
|
respChan := make(chan *wire.MsgTx, 1)
|
|
|
|
// With the channel found, and the request crafted, we'll send over a
|
|
// force close request to the arbitrator that watches this channel.
|
|
chanArb.forceCloseReqs <- &forceCloseReq{
|
|
errResp: errChan,
|
|
closeTx: respChan,
|
|
}
|
|
|
|
// It should transition to StateBroadcastCommit.
|
|
chanArbCtx.AssertStateTransitions(StateBroadcastCommit)
|
|
|
|
// We expect it to be in state StateBroadcastCommit when publishing
|
|
// the force close.
|
|
select {
|
|
case state := <-stateChan:
|
|
if state != StateBroadcastCommit {
|
|
t.Fatalf("state during PublishTx was %v", state)
|
|
}
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatalf("no state update received")
|
|
}
|
|
|
|
// After broadcasting, transition should be to
|
|
// StateCommitmentBroadcasted.
|
|
chanArbCtx.AssertStateTransitions(StateCommitmentBroadcasted)
|
|
|
|
// Wait for a response to the force close.
|
|
select {
|
|
case <-respChan:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
select {
|
|
case err := <-errChan:
|
|
if err != nil {
|
|
t.Fatalf("error force closing channel: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
// The state should be StateCommitmentBroadcasted.
|
|
chanArbCtx.AssertState(StateCommitmentBroadcasted)
|
|
|
|
// Now notify about the _REMOTE_ commitment getting confirmed.
|
|
commitSpend := &chainntnfs.SpendDetail{
|
|
SpenderTxHash: &chainhash.Hash{},
|
|
}
|
|
uniClose := &lnwallet.UnilateralCloseSummary{
|
|
SpendDetail: commitSpend,
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
}
|
|
chanArb.cfg.ChainEvents.RemoteUnilateralClosure <- &RemoteUnilateralCloseInfo{
|
|
UnilateralCloseSummary: uniClose,
|
|
}
|
|
|
|
// It should transition StateContractClosed -> StateFullyResolved.
|
|
chanArbCtx.AssertStateTransitions(StateContractClosed, StateFullyResolved)
|
|
|
|
// It should resolve.
|
|
select {
|
|
case <-chanArbCtx.resolvedChan:
|
|
// Expected.
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorPersistence tests that the ChannelArbitrator is able to
|
|
// keep advancing the state machine from various states after restart.
|
|
func TestChannelArbitratorPersistence(t *testing.T) {
|
|
// Start out with a log that will fail writing the set of resolutions.
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
failLog: true,
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
chanArb := chanArbCtx.chanArb
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
// It should start in StateDefault.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// Send a remote force close event.
|
|
commitSpend := &chainntnfs.SpendDetail{
|
|
SpenderTxHash: &chainhash.Hash{},
|
|
}
|
|
|
|
uniClose := &lnwallet.UnilateralCloseSummary{
|
|
SpendDetail: commitSpend,
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
}
|
|
chanArb.cfg.ChainEvents.RemoteUnilateralClosure <- &RemoteUnilateralCloseInfo{
|
|
UnilateralCloseSummary: uniClose,
|
|
}
|
|
|
|
// Since writing the resolutions fail, the arbitrator should not
|
|
// advance to the next state.
|
|
time.Sleep(100 * time.Millisecond)
|
|
if log.state != StateDefault {
|
|
t.Fatalf("expected to stay in StateDefault")
|
|
}
|
|
|
|
// Restart the channel arb, this'll use the same long and prior
|
|
// context.
|
|
chanArbCtx, err = chanArbCtx.Restart(nil)
|
|
if err != nil {
|
|
t.Fatalf("unable to restart channel arb: %v", err)
|
|
}
|
|
chanArb = chanArbCtx.chanArb
|
|
|
|
// Again, it should start up in the default state.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// Now we make the log succeed writing the resolutions, but fail when
|
|
// attempting to close the channel.
|
|
log.failLog = false
|
|
chanArb.cfg.MarkChannelClosed = func(*channeldb.ChannelCloseSummary) error {
|
|
return fmt.Errorf("intentional close error")
|
|
}
|
|
|
|
// Send a new remote force close event.
|
|
chanArb.cfg.ChainEvents.RemoteUnilateralClosure <- &RemoteUnilateralCloseInfo{
|
|
UnilateralCloseSummary: uniClose,
|
|
}
|
|
|
|
// Since closing the channel failed, the arbitrator should stay in the
|
|
// default state.
|
|
time.Sleep(100 * time.Millisecond)
|
|
if log.state != StateDefault {
|
|
t.Fatalf("expected to stay in StateDefault")
|
|
}
|
|
|
|
// Restart once again to simulate yet another restart.
|
|
chanArbCtx, err = chanArbCtx.Restart(nil)
|
|
if err != nil {
|
|
t.Fatalf("unable to restart channel arb: %v", err)
|
|
}
|
|
chanArb = chanArbCtx.chanArb
|
|
|
|
// Starts out in StateDefault.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// Now make fetching the resolutions fail.
|
|
log.failFetch = fmt.Errorf("intentional fetch failure")
|
|
chanArb.cfg.ChainEvents.RemoteUnilateralClosure <- &RemoteUnilateralCloseInfo{
|
|
UnilateralCloseSummary: uniClose,
|
|
}
|
|
|
|
// Since logging the resolutions and closing the channel now succeeds,
|
|
// it should advance to StateContractClosed.
|
|
chanArbCtx.AssertStateTransitions(StateContractClosed)
|
|
|
|
// It should not advance further, however, as fetching resolutions
|
|
// failed.
|
|
time.Sleep(100 * time.Millisecond)
|
|
if log.state != StateContractClosed {
|
|
t.Fatalf("expected to stay in StateContractClosed")
|
|
}
|
|
chanArb.Stop()
|
|
|
|
// Create a new arbitrator, and now make fetching resolutions succeed.
|
|
log.failFetch = nil
|
|
chanArbCtx, err = chanArbCtx.Restart(nil)
|
|
if err != nil {
|
|
t.Fatalf("unable to restart channel arb: %v", err)
|
|
}
|
|
defer chanArbCtx.CleanUp()
|
|
|
|
// Finally it should advance to StateFullyResolved.
|
|
chanArbCtx.AssertStateTransitions(StateFullyResolved)
|
|
|
|
// It should also mark the channel as resolved.
|
|
select {
|
|
case <-chanArbCtx.resolvedChan:
|
|
// Expected.
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorForceCloseBreachedChannel tests that the channel
|
|
// arbitrator is able to handle a channel in the process of being force closed
|
|
// is breached by the remote node. In these cases we expect the
|
|
// ChannelArbitrator to gracefully exit, as the breach is handled by other
|
|
// subsystems.
|
|
func TestChannelArbitratorForceCloseBreachedChannel(t *testing.T) {
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
chanArb := chanArbCtx.chanArb
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
// It should start in StateDefault.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// We start by attempting a local force close. We'll return an
|
|
// unexpected publication error, causing the state machine to halt.
|
|
expErr := errors.New("intentional publication error")
|
|
stateChan := make(chan ArbitratorState)
|
|
chanArb.cfg.PublishTx = func(*wire.MsgTx) error {
|
|
// When the force close tx is being broadcasted, check that the
|
|
// state is correct at that point.
|
|
select {
|
|
case stateChan <- chanArb.state:
|
|
case <-chanArb.quit:
|
|
return fmt.Errorf("exiting")
|
|
}
|
|
return expErr
|
|
}
|
|
|
|
errChan := make(chan error, 1)
|
|
respChan := make(chan *wire.MsgTx, 1)
|
|
|
|
// With the channel found, and the request crafted, we'll send over a
|
|
// force close request to the arbitrator that watches this channel.
|
|
chanArb.forceCloseReqs <- &forceCloseReq{
|
|
errResp: errChan,
|
|
closeTx: respChan,
|
|
}
|
|
|
|
// It should transition to StateBroadcastCommit.
|
|
chanArbCtx.AssertStateTransitions(StateBroadcastCommit)
|
|
|
|
// We expect it to be in state StateBroadcastCommit when attempting
|
|
// the force close.
|
|
select {
|
|
case state := <-stateChan:
|
|
if state != StateBroadcastCommit {
|
|
t.Fatalf("state during PublishTx was %v", state)
|
|
}
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatalf("no state update received")
|
|
}
|
|
|
|
// Make sure we get the expected error.
|
|
select {
|
|
case err := <-errChan:
|
|
if err != expErr {
|
|
t.Fatalf("unexpected error force closing channel: %v",
|
|
err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("no response received")
|
|
}
|
|
|
|
// We mimic that the channel is breached while the channel arbitrator
|
|
// is down. This means that on restart it will be started with a
|
|
// pending close channel, of type BreachClose.
|
|
chanArbCtx, err = chanArbCtx.Restart(func(c *chanArbTestCtx) {
|
|
c.chanArb.cfg.IsPendingClose = true
|
|
c.chanArb.cfg.ClosingHeight = 100
|
|
c.chanArb.cfg.CloseType = channeldb.BreachClose
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
defer chanArbCtx.CleanUp()
|
|
|
|
// Finally it should advance to StateFullyResolved.
|
|
chanArbCtx.AssertStateTransitions(StateFullyResolved)
|
|
|
|
// It should also mark the channel as resolved.
|
|
select {
|
|
case <-chanArbCtx.resolvedChan:
|
|
// Expected.
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorCommitFailure tests that the channel arbitrator is able
|
|
// to recover from a failed CommitState call at restart.
|
|
func TestChannelArbitratorCommitFailure(t *testing.T) {
|
|
|
|
testCases := []struct {
|
|
|
|
// closeType is the type of channel close we want ot test.
|
|
closeType channeldb.ClosureType
|
|
|
|
// sendEvent is a function that will send the event
|
|
// corresponding to this test's closeType to the passed
|
|
// ChannelArbitrator.
|
|
sendEvent func(chanArb *ChannelArbitrator)
|
|
|
|
// expectedStates is the states we expect the state machine to
|
|
// go through after a restart and successful log commit.
|
|
expectedStates []ArbitratorState
|
|
}{
|
|
{
|
|
closeType: channeldb.CooperativeClose,
|
|
sendEvent: func(chanArb *ChannelArbitrator) {
|
|
closeInfo := &CooperativeCloseInfo{
|
|
&channeldb.ChannelCloseSummary{},
|
|
}
|
|
chanArb.cfg.ChainEvents.CooperativeClosure <- closeInfo
|
|
},
|
|
expectedStates: []ArbitratorState{StateFullyResolved},
|
|
},
|
|
{
|
|
closeType: channeldb.RemoteForceClose,
|
|
sendEvent: func(chanArb *ChannelArbitrator) {
|
|
commitSpend := &chainntnfs.SpendDetail{
|
|
SpenderTxHash: &chainhash.Hash{},
|
|
}
|
|
|
|
uniClose := &lnwallet.UnilateralCloseSummary{
|
|
SpendDetail: commitSpend,
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
}
|
|
chanArb.cfg.ChainEvents.RemoteUnilateralClosure <- &RemoteUnilateralCloseInfo{
|
|
UnilateralCloseSummary: uniClose,
|
|
}
|
|
},
|
|
expectedStates: []ArbitratorState{StateContractClosed, StateFullyResolved},
|
|
},
|
|
{
|
|
closeType: channeldb.LocalForceClose,
|
|
sendEvent: func(chanArb *ChannelArbitrator) {
|
|
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
|
SpendDetail: &chainntnfs.SpendDetail{},
|
|
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
|
CloseTx: &wire.MsgTx{},
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
},
|
|
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
|
}
|
|
},
|
|
expectedStates: []ArbitratorState{StateContractClosed, StateFullyResolved},
|
|
},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
test := test
|
|
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
failCommit: true,
|
|
|
|
// Set the log to fail on the first expected state
|
|
// after state machine progress for this test case.
|
|
failCommitState: test.expectedStates[0],
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
chanArb := chanArbCtx.chanArb
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
// It should start in StateDefault.
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
closed := make(chan struct{})
|
|
chanArb.cfg.MarkChannelClosed = func(
|
|
*channeldb.ChannelCloseSummary) error {
|
|
close(closed)
|
|
return nil
|
|
}
|
|
|
|
// Send the test event to trigger the state machine.
|
|
test.sendEvent(chanArb)
|
|
|
|
select {
|
|
case <-closed:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("channel was not marked closed")
|
|
}
|
|
|
|
// Since the channel was marked closed in the database, but the
|
|
// commit to the next state failed, the state should still be
|
|
// StateDefault.
|
|
time.Sleep(100 * time.Millisecond)
|
|
if log.state != StateDefault {
|
|
t.Fatalf("expected to stay in StateDefault, instead "+
|
|
"has %v", log.state)
|
|
}
|
|
chanArb.Stop()
|
|
|
|
// Start the arbitrator again, with IsPendingClose reporting
|
|
// the channel closed in the database.
|
|
log.failCommit = false
|
|
chanArbCtx, err = chanArbCtx.Restart(func(c *chanArbTestCtx) {
|
|
c.chanArb.cfg.IsPendingClose = true
|
|
c.chanArb.cfg.ClosingHeight = 100
|
|
c.chanArb.cfg.CloseType = test.closeType
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
// Since the channel is marked closed in the database, it
|
|
// should advance to the expected states.
|
|
chanArbCtx.AssertStateTransitions(test.expectedStates...)
|
|
|
|
// It should also mark the channel as resolved.
|
|
select {
|
|
case <-chanArbCtx.resolvedChan:
|
|
// Expected.
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("contract was not resolved")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorEmptyResolutions makes sure that a channel that is
|
|
// pending close in the database, but haven't had any resolutions logged will
|
|
// not be marked resolved. This situation must be handled to avoid closing
|
|
// channels from earlier versions of the ChannelArbitrator, which didn't have a
|
|
// proper handoff from the ChainWatcher, and we could risk ending up in a state
|
|
// where the channel was closed in the DB, but the resolutions weren't properly
|
|
// written.
|
|
func TestChannelArbitratorEmptyResolutions(t *testing.T) {
|
|
// Start out with a log that will fail writing the set of resolutions.
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
failFetch: errNoResolutions,
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
chanArb := chanArbCtx.chanArb
|
|
chanArb.cfg.IsPendingClose = true
|
|
chanArb.cfg.ClosingHeight = 100
|
|
chanArb.cfg.CloseType = channeldb.RemoteForceClose
|
|
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
|
|
// It should not advance its state beyond StateContractClosed, since
|
|
// fetching resolutions fails.
|
|
chanArbCtx.AssertStateTransitions(StateContractClosed)
|
|
|
|
// It should not advance further, however, as fetching resolutions
|
|
// failed.
|
|
time.Sleep(100 * time.Millisecond)
|
|
if log.state != StateContractClosed {
|
|
t.Fatalf("expected to stay in StateContractClosed")
|
|
}
|
|
chanArb.Stop()
|
|
}
|
|
|
|
// TestChannelArbitratorAlreadyForceClosed ensures that we cannot force close a
|
|
// channel that is already in the process of doing so.
|
|
func TestChannelArbitratorAlreadyForceClosed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We'll create the arbitrator and its backing log to signal that it's
|
|
// already in the process of being force closed.
|
|
log := &mockArbitratorLog{
|
|
state: StateCommitmentBroadcasted,
|
|
}
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb := chanArbCtx.chanArb
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer chanArb.Stop()
|
|
|
|
// Then, we'll create a request to signal a force close request to the
|
|
// channel arbitrator.
|
|
errChan := make(chan error, 1)
|
|
respChan := make(chan *wire.MsgTx, 1)
|
|
|
|
select {
|
|
case chanArb.forceCloseReqs <- &forceCloseReq{
|
|
closeTx: respChan,
|
|
errResp: errChan,
|
|
}:
|
|
case <-chanArb.quit:
|
|
}
|
|
|
|
// Finally, we should ensure that we are not able to do so by seeing
|
|
// the expected errAlreadyForceClosed error.
|
|
select {
|
|
case err = <-errChan:
|
|
if err != errAlreadyForceClosed {
|
|
t.Fatalf("expected errAlreadyForceClosed, got %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected to receive error response")
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorDanglingCommitForceClose tests that if there're HTLCs
|
|
// on the remote party's commitment, but not ours, and they're about to time
|
|
// out, then we'll go on chain so we can cancel back the HTLCs on the incoming
|
|
// commitment.
|
|
func TestChannelArbitratorDanglingCommitForceClose(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type testCase struct {
|
|
htlcExpired bool
|
|
remotePendingHTLC bool
|
|
confCommit HtlcSetKey
|
|
}
|
|
var testCases []testCase
|
|
|
|
testOptions := []bool{true, false}
|
|
confOptions := []HtlcSetKey{
|
|
LocalHtlcSet, RemoteHtlcSet, RemotePendingHtlcSet,
|
|
}
|
|
for _, htlcExpired := range testOptions {
|
|
for _, remotePendingHTLC := range testOptions {
|
|
for _, commitConf := range confOptions {
|
|
switch {
|
|
// If the HTLC is on the remote commitment, and
|
|
// that one confirms, then there's no special
|
|
// behavior, we should play all the HTLCs on
|
|
// that remote commitment as normal.
|
|
case !remotePendingHTLC && commitConf == RemoteHtlcSet:
|
|
fallthrough
|
|
|
|
// If the HTLC is on the remote pending, and
|
|
// that confirms, then we don't have any
|
|
// special actions.
|
|
case remotePendingHTLC && commitConf == RemotePendingHtlcSet:
|
|
continue
|
|
}
|
|
|
|
testCases = append(testCases, testCase{
|
|
htlcExpired: htlcExpired,
|
|
remotePendingHTLC: remotePendingHTLC,
|
|
confCommit: commitConf,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
testCase := testCase
|
|
testName := fmt.Sprintf("testCase: htlcExpired=%v,"+
|
|
"remotePendingHTLC=%v,remotePendingCommitConf=%v",
|
|
testCase.htlcExpired, testCase.remotePendingHTLC,
|
|
testCase.confCommit)
|
|
|
|
t.Run(testName, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
arbLog := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
resolvers: make(map[ContractResolver]struct{}),
|
|
}
|
|
|
|
chanArbCtx, err := createTestChannelArbitrator(
|
|
t, arbLog,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb := chanArbCtx.chanArb
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer chanArb.Stop()
|
|
|
|
// Now that our channel arb has started, we'll set up
|
|
// its contract signals channel so we can send it
|
|
// various HTLC updates for this test.
|
|
htlcUpdates := make(chan *ContractUpdate)
|
|
signals := &ContractSignals{
|
|
HtlcUpdates: htlcUpdates,
|
|
ShortChanID: lnwire.ShortChannelID{},
|
|
}
|
|
chanArb.UpdateContractSignals(signals)
|
|
|
|
htlcKey := RemoteHtlcSet
|
|
if testCase.remotePendingHTLC {
|
|
htlcKey = RemotePendingHtlcSet
|
|
}
|
|
|
|
// Next, we'll send it a new HTLC that is set to expire
|
|
// in 10 blocks, this HTLC will only appear on the
|
|
// commitment transaction of the _remote_ party.
|
|
htlcIndex := uint64(99)
|
|
htlcExpiry := uint32(10)
|
|
danglingHTLC := channeldb.HTLC{
|
|
Incoming: false,
|
|
Amt: 10000,
|
|
HtlcIndex: htlcIndex,
|
|
RefundTimeout: htlcExpiry,
|
|
}
|
|
htlcUpdates <- &ContractUpdate{
|
|
HtlcKey: htlcKey,
|
|
Htlcs: []channeldb.HTLC{danglingHTLC},
|
|
}
|
|
|
|
// At this point, we now have a split commitment state
|
|
// from the PoV of the channel arb. There's now an HTLC
|
|
// that only exists on the commitment transaction of
|
|
// the remote party.
|
|
errChan := make(chan error, 1)
|
|
respChan := make(chan *wire.MsgTx, 1)
|
|
switch {
|
|
// If we want an HTLC expiration trigger, then We'll
|
|
// now mine a block (height 5), which is 5 blocks away
|
|
// (our grace delta) from the expiry of that HTLC.
|
|
case testCase.htlcExpired:
|
|
chanArbCtx.blockEpochs <- &chainntnfs.BlockEpoch{Height: 5}
|
|
|
|
// Otherwise, we'll just trigger a regular force close
|
|
// request.
|
|
case !testCase.htlcExpired:
|
|
chanArb.forceCloseReqs <- &forceCloseReq{
|
|
errResp: errChan,
|
|
closeTx: respChan,
|
|
}
|
|
|
|
}
|
|
|
|
// At this point, the resolver should now have
|
|
// determined that it needs to go to chain in order to
|
|
// block off the redemption path so it can cancel the
|
|
// incoming HTLC.
|
|
chanArbCtx.AssertStateTransitions(
|
|
StateBroadcastCommit,
|
|
StateCommitmentBroadcasted,
|
|
)
|
|
|
|
// Next we'll craft a fake commitment transaction to
|
|
// send to signal that the channel has closed out on
|
|
// chain.
|
|
closeTx := &wire.MsgTx{
|
|
TxIn: []*wire.TxIn{
|
|
{
|
|
PreviousOutPoint: wire.OutPoint{},
|
|
Witness: [][]byte{
|
|
{0x9},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// We'll now signal to the channel arb that the HTLC
|
|
// has fully closed on chain. Our local commit set
|
|
// shows now HTLC on our commitment, but one on the
|
|
// remote commitment. This should result in the HTLC
|
|
// being canalled back. Also note that there're no HTLC
|
|
// resolutions sent since we have none on our
|
|
// commitment transaction.
|
|
uniCloseInfo := &LocalUnilateralCloseInfo{
|
|
SpendDetail: &chainntnfs.SpendDetail{},
|
|
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
|
CloseTx: closeTx,
|
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
},
|
|
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
|
CommitSet: CommitSet{
|
|
ConfCommitKey: &testCase.confCommit,
|
|
HtlcSets: make(map[HtlcSetKey][]channeldb.HTLC),
|
|
},
|
|
}
|
|
|
|
// If the HTLC was meant to expire, then we'll mark the
|
|
// closing transaction at the proper expiry height
|
|
// since our comparison "need to timeout" comparison is
|
|
// based on the confirmation height.
|
|
if testCase.htlcExpired {
|
|
uniCloseInfo.SpendDetail.SpendingHeight = 5
|
|
}
|
|
|
|
// Depending on if we're testing the remote pending
|
|
// commitment or not, we'll populate either a fake
|
|
// dangling remote commitment, or a regular locked in
|
|
// one.
|
|
htlcs := []channeldb.HTLC{danglingHTLC}
|
|
if testCase.remotePendingHTLC {
|
|
uniCloseInfo.CommitSet.HtlcSets[RemotePendingHtlcSet] = htlcs
|
|
} else {
|
|
uniCloseInfo.CommitSet.HtlcSets[RemoteHtlcSet] = htlcs
|
|
}
|
|
|
|
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- uniCloseInfo
|
|
|
|
// The channel arb should now transition to waiting
|
|
// until the HTLCs have been fully resolved.
|
|
chanArbCtx.AssertStateTransitions(
|
|
StateContractClosed,
|
|
StateWaitingFullResolution,
|
|
)
|
|
|
|
// Now that we've sent this signal, we should have that
|
|
// HTLC be canceled back immediately.
|
|
select {
|
|
case msgs := <-chanArbCtx.resolutions:
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("expected 1 message, "+
|
|
"instead got %v", len(msgs))
|
|
}
|
|
|
|
if msgs[0].HtlcIndex != htlcIndex {
|
|
t.Fatalf("wrong htlc index: expected %v, got %v",
|
|
htlcIndex, msgs[0].HtlcIndex)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("resolution msgs not sent")
|
|
}
|
|
|
|
// There's no contract to send a fully resolve message,
|
|
// so instead, we'll mine another block which'll cause
|
|
// it to re-examine its state and realize there're no
|
|
// more HTLCs.
|
|
chanArbCtx.blockEpochs <- &chainntnfs.BlockEpoch{Height: 6}
|
|
chanArbCtx.AssertStateTransitions(StateFullyResolved)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestChannelArbitratorPendingExpiredHTLC tests that if we have pending htlc
|
|
// that is expired we will only go to chain if we are running at least the
|
|
// time defined in PaymentsExpirationGracePeriod.
|
|
// During this time the remote party is expected to send his updates and cancel
|
|
// The htlc.
|
|
func TestChannelArbitratorPendingExpiredHTLC(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We'll create the arbitrator and its backing log in a default state.
|
|
log := &mockArbitratorLog{
|
|
state: StateDefault,
|
|
newStates: make(chan ArbitratorState, 5),
|
|
resolvers: make(map[ContractResolver]struct{}),
|
|
}
|
|
chanArbCtx, err := createTestChannelArbitrator(t, log)
|
|
if err != nil {
|
|
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
|
}
|
|
chanArb := chanArbCtx.chanArb
|
|
|
|
// We'll inject a test clock implementation so we can control the uptime.
|
|
startTime := time.Date(2020, time.February, 3, 13, 0, 0, 0, time.UTC)
|
|
testClock := clock.NewTestClock(startTime)
|
|
chanArb.cfg.Clock = testClock
|
|
|
|
// We also configure the grace period and the IsForwardedHTLC to identify
|
|
// the htlc as our initiated payment.
|
|
chanArb.cfg.PaymentsExpirationGracePeriod = time.Second * 15
|
|
chanArb.cfg.IsForwardedHTLC = func(chanID lnwire.ShortChannelID,
|
|
htlcIndex uint64) bool {
|
|
|
|
return false
|
|
}
|
|
|
|
if err := chanArb.Start(); err != nil {
|
|
t.Fatalf("unable to start ChannelArbitrator: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := chanArb.Stop(); err != nil {
|
|
t.Fatalf("unable to stop chan arb: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Now that our channel arb has started, we'll set up
|
|
// its contract signals channel so we can send it
|
|
// various HTLC updates for this test.
|
|
htlcUpdates := make(chan *ContractUpdate)
|
|
signals := &ContractSignals{
|
|
HtlcUpdates: htlcUpdates,
|
|
ShortChanID: lnwire.ShortChannelID{},
|
|
}
|
|
chanArb.UpdateContractSignals(signals)
|
|
|
|
// Next, we'll send it a new HTLC that is set to expire
|
|
// in 10 blocks.
|
|
htlcIndex := uint64(99)
|
|
htlcExpiry := uint32(10)
|
|
pendingHTLC := channeldb.HTLC{
|
|
Incoming: false,
|
|
Amt: 10000,
|
|
HtlcIndex: htlcIndex,
|
|
RefundTimeout: htlcExpiry,
|
|
}
|
|
htlcUpdates <- &ContractUpdate{
|
|
HtlcKey: RemoteHtlcSet,
|
|
Htlcs: []channeldb.HTLC{pendingHTLC},
|
|
}
|
|
|
|
// We will advance the uptime to 10 seconds which should be still within
|
|
// the grace period and should not trigger going to chain.
|
|
testClock.SetTime(startTime.Add(time.Second * 10))
|
|
chanArbCtx.blockEpochs <- &chainntnfs.BlockEpoch{Height: 5}
|
|
chanArbCtx.AssertState(StateDefault)
|
|
|
|
// We will advance the uptime to 16 seconds which should trigger going
|
|
// to chain.
|
|
testClock.SetTime(startTime.Add(time.Second * 16))
|
|
chanArbCtx.blockEpochs <- &chainntnfs.BlockEpoch{Height: 6}
|
|
chanArbCtx.AssertStateTransitions(
|
|
StateBroadcastCommit,
|
|
StateCommitmentBroadcasted,
|
|
)
|
|
}
|