contractcourt/channel_arbitrator test: add unit tests
This commit adds MVP unit tests for the following scenarios in the ChannelArbitrator: 1) A cooperative close is confirmed. 2) A remote force close is confirmed. 3) A local force close is requested and confirmed. 4) A local force close is requested, but a remote force close gets confirmed.
This commit is contained in:
parent
84f06959f3
commit
b2949bd728
@ -1 +1,341 @@
|
||||
package contractcourt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||
"github.com/roasbeef/btcd/wire"
|
||||
)
|
||||
|
||||
type mockChainIO struct{}
|
||||
|
||||
func (*mockChainIO) GetBestBlock() (*chainhash.Hash, int32, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (*mockChainIO) GetUtxo(op *wire.OutPoint,
|
||||
heightHint uint32) (*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
|
||||
}
|
||||
|
||||
func createTestChannelArbitrator() (*ChannelArbitrator, chan struct{}, func(), error) {
|
||||
blockEpoch := &chainntnfs.BlockEpochEvent{
|
||||
Cancel: func() {},
|
||||
}
|
||||
|
||||
chanPoint := wire.OutPoint{}
|
||||
shortChanID := lnwire.ShortChannelID{}
|
||||
chanEvents := &ChainEventSubscription{
|
||||
RemoteUnilateralClosure: make(chan *lnwallet.UnilateralCloseSummary, 1),
|
||||
LocalUnilateralClosure: make(chan *LocalUnilateralCloseInfo, 1),
|
||||
CooperativeClosure: make(chan struct{}, 1),
|
||||
ContractBreach: make(chan *lnwallet.BreachRetribution, 1),
|
||||
}
|
||||
|
||||
chainIO := &mockChainIO{}
|
||||
chainArbCfg := ChainArbitratorConfig{
|
||||
ChainIO: chainIO,
|
||||
PublishTx: func(*wire.MsgTx) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// 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() error {
|
||||
return nil
|
||||
},
|
||||
|
||||
ChainArbitratorConfig: chainArbCfg,
|
||||
ChainEvents: chanEvents,
|
||||
}
|
||||
testLog, cleanUp, err := newTestBoltArbLog(
|
||||
testChainHash, testChanPoint1,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("unable to create test log: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
return NewChannelArbitrator(arbCfg, nil, testLog),
|
||||
resolvedChan, cleanUp, nil
|
||||
}
|
||||
|
||||
// assertState checks that the ChannelArbitrator is in the state we expect it
|
||||
// to be.
|
||||
func assertState(t *testing.T, c *ChannelArbitrator, expected ArbitratorState) {
|
||||
if c.state != expected {
|
||||
t.Fatalf("expected state %v, was %v", expected, c.state)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelArbitratorCooperativeClose tests that the ChannelArbitertor
|
||||
// correctly does nothing in case a cooperative close is confirmed.
|
||||
func TestChannelArbitratorCooperativeClose(t *testing.T) {
|
||||
chanArb, _, cleanUp, err := createTestChannelArbitrator()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
||||
}
|
||||
defer cleanUp()
|
||||
|
||||
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.
|
||||
assertState(t, chanArb, StateDefault)
|
||||
|
||||
// Cooperative close should do nothing.
|
||||
// TODO: this will change?
|
||||
chanArb.cfg.ChainEvents.CooperativeClosure <- struct{}{}
|
||||
assertState(t, chanArb, StateDefault)
|
||||
}
|
||||
|
||||
// TestChannelArbitratorRemoteForceClose checks that the ChannelArbitrotor goes
|
||||
// through the expected states if a remote force close is observed in the
|
||||
// chain.
|
||||
func TestChannelArbitratorRemoteForceClose(t *testing.T) {
|
||||
chanArb, resolved, cleanUp, err := createTestChannelArbitrator()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
||||
}
|
||||
defer cleanUp()
|
||||
|
||||
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.
|
||||
assertState(t, chanArb, 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 <- uniClose
|
||||
|
||||
// It should mark the channel as resolved.
|
||||
select {
|
||||
case <-resolved:
|
||||
// Expected.
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("contract was not resolved")
|
||||
}
|
||||
|
||||
// TODO: intermediate states.
|
||||
// We expect the ChannelArbitrator to end up in the the resolved state.
|
||||
assertState(t, chanArb, StateFullyResolved)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
chanArb, resolved, cleanUp, err := createTestChannelArbitrator()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
||||
}
|
||||
defer cleanUp()
|
||||
|
||||
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.
|
||||
assertState(t, chanArb, 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,
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-respChan:
|
||||
case err := <-errChan:
|
||||
t.Fatalf("error force closing channel: %v", err)
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatalf("did not receive reponse")
|
||||
}
|
||||
|
||||
// After broadcasting the close tx, it should be in state
|
||||
// StateCommitmentBroadcasted.
|
||||
assertState(t, chanArb, StateCommitmentBroadcasted)
|
||||
|
||||
// Now notify about the local force close getting confirmed.
|
||||
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
||||
&chainntnfs.SpendDetail{},
|
||||
&lnwallet.LocalForceCloseSummary{
|
||||
CloseTx: &wire.MsgTx{},
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
},
|
||||
}
|
||||
// It should mark the channel as resolved.
|
||||
select {
|
||||
case <-resolved:
|
||||
// Expected.
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("contract was not resolved")
|
||||
}
|
||||
|
||||
// And end up in the StateFullyResolved state.
|
||||
// TODO: intermediate states as well.
|
||||
assertState(t, chanArb, StateFullyResolved)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
chanArb, resolved, cleanUp, err := createTestChannelArbitrator()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create ChannelArbitrator: %v", err)
|
||||
}
|
||||
defer cleanUp()
|
||||
|
||||
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.
|
||||
assertState(t, chanArb, 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,
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Wait for a response to the force close.
|
||||
select {
|
||||
case <-respChan:
|
||||
case err := <-errChan:
|
||||
t.Fatalf("error force closing channel: %v", err)
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatalf("no response received")
|
||||
}
|
||||
|
||||
// The state should be StateCommitmentBroadcasted.
|
||||
assertState(t, chanArb, 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 <- uniClose
|
||||
|
||||
// It should resolve.
|
||||
select {
|
||||
case <-resolved:
|
||||
// Expected.
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatalf("contract was not resolved")
|
||||
}
|
||||
|
||||
// And we expect it to end up in StateFullyResolved.
|
||||
// TODO: intermediate states as well.
|
||||
assertState(t, chanArb, StateFullyResolved)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user