From b9516b7cddc85a59b61c39267516f6c0e60de37e Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 14 Jul 2017 21:04:29 +0200 Subject: [PATCH 1/2] lnwallet: make CreateCloseProposal take absolute fee instead of fee rate --- lnwallet/channel.go | 16 ++++++++++------ lnwallet/channel_test.go | 32 ++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 4dce1ffc..e6f6a089 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -3671,14 +3671,14 @@ func (lc *LightningChannel) ForceClose() (*ForceCloseSummary, error) { // // TODO(roasbeef): caller should initiate signal to reject all incoming HTLCs, // settle any in flight. -func (lc *LightningChannel) CreateCloseProposal(feeRate uint64, +func (lc *LightningChannel) CreateCloseProposal(proposedFee uint64, localDeliveryScript, remoteDeliveryScript []byte) ([]byte, uint64, error) { lc.Lock() defer lc.Unlock() - // If we're already closing the channel, then ignore this request. - if lc.status == channelClosing || lc.status == channelClosed { + // If we've already closed the channel, then ignore this request. + if lc.status == channelClosed { // TODO(roasbeef): check to ensure no pending payments return nil, 0, ErrChanClosing } @@ -3686,7 +3686,6 @@ func (lc *LightningChannel) CreateCloseProposal(feeRate uint64, // Subtract the proposed fee from the appropriate balance, taking care // not to persist the adjusted balance, as the feeRate may change // during the channel closing process. - proposedFee := (feeRate * uint64(commitWeight)) / 1000 ourBalance := lc.channelState.LocalBalance theirBalance := lc.channelState.RemoteBalance @@ -3734,7 +3733,7 @@ func (lc *LightningChannel) CreateCloseProposal(feeRate uint64, // signatures including the proper sighash byte. func (lc *LightningChannel) CompleteCooperativeClose(localSig, remoteSig, localDeliveryScript, remoteDeliveryScript []byte, - feeRate uint64) (*wire.MsgTx, error) { + proposedFee uint64) (*wire.MsgTx, error) { lc.Lock() defer lc.Unlock() @@ -3748,7 +3747,6 @@ func (lc *LightningChannel) CompleteCooperativeClose(localSig, remoteSig, // Subtract the proposed fee from the appropriate balance, taking care // not to persist the adjusted balance, as the feeRate may change // during the channel closing process. - proposedFee := (feeRate * uint64(commitWeight)) / 1000 ourBalance := lc.channelState.LocalBalance theirBalance := lc.channelState.RemoteBalance @@ -3947,3 +3945,9 @@ func CreateCooperativeCloseTx(fundingTxIn *wire.TxIn, return closeTx } + +// CalcFee returns the commitment fee to use for the given +// fee rate (fee-per-kw). +func (lc *LightningChannel) CalcFee(feeRate uint64) uint64 { + return (feeRate * uint64(commitWeight)) / 1000 +} diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index c43896b0..dc4cd912 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -737,17 +737,19 @@ func TestCooperativeChannelClosure(t *testing.T) { bobFeeRate := uint64(bobChannel.channelState.FeePerKw) // We'll store with both Alice and Bob creating a new close proposal - // with the same fee rate. + // with the same fee. + aliceFee := aliceChannel.CalcFee(aliceFeeRate) aliceSig, _, err := aliceChannel.CreateCloseProposal( - aliceFeeRate, aliceDeliveryScript, bobDeliveryScript, + aliceFee, aliceDeliveryScript, bobDeliveryScript, ) if err != nil { t.Fatalf("unable to create alice coop close proposal: %v", err) } aliceCloseSig := append(aliceSig, byte(txscript.SigHashAll)) + bobFee := bobChannel.CalcFee(bobFeeRate) bobSig, _, err := bobChannel.CreateCloseProposal( - bobFeeRate, bobDeliveryScript, aliceDeliveryScript, + bobFee, bobDeliveryScript, aliceDeliveryScript, ) if err != nil { t.Fatalf("unable to create bob coop close proposal: %v", err) @@ -759,7 +761,7 @@ func TestCooperativeChannelClosure(t *testing.T) { // transaction is well formed, and the signatures verify. aliceCloseTx, err := bobChannel.CompleteCooperativeClose( bobCloseSig, aliceCloseSig, bobDeliveryScript, - aliceDeliveryScript, bobFeeRate) + aliceDeliveryScript, bobFee) if err != nil { t.Fatalf("unable to complete alice cooperative close: %v", err) } @@ -767,7 +769,7 @@ func TestCooperativeChannelClosure(t *testing.T) { bobCloseTx, err := aliceChannel.CompleteCooperativeClose( aliceCloseSig, bobCloseSig, aliceDeliveryScript, - bobDeliveryScript, aliceFeeRate) + bobDeliveryScript, aliceFee) if err != nil { t.Fatalf("unable to complete bob cooperative close: %v", err) } @@ -1590,14 +1592,16 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { // Both sides currently have over 1 BTC settled as part of their // balances. As a result, performing a cooperative closure now result // in both sides having an output within the closure transaction. - aliceSig, _, err := aliceChannel.CreateCloseProposal(aliceFeeRate, + aliceFee := aliceChannel.CalcFee(aliceFeeRate) + aliceSig, _, err := aliceChannel.CreateCloseProposal(aliceFee, aliceDeliveryScript, bobDeliveryScript) if err != nil { t.Fatalf("unable to close channel: %v", err) } aliceCloseSig := append(aliceSig, byte(txscript.SigHashAll)) - bobSig, _, err := bobChannel.CreateCloseProposal(bobFeeRate, + bobFee := bobChannel.CalcFee(bobFeeRate) + bobSig, _, err := bobChannel.CreateCloseProposal(bobFee, bobDeliveryScript, aliceDeliveryScript) if err != nil { t.Fatalf("unable to close channel: %v", err) @@ -1606,7 +1610,7 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { closeTx, err := bobChannel.CompleteCooperativeClose( bobCloseSig, aliceCloseSig, - bobDeliveryScript, aliceDeliveryScript, bobFeeRate) + bobDeliveryScript, aliceDeliveryScript, bobFee) if err != nil { t.Fatalf("unable to accept channel close: %v", err) } @@ -1628,14 +1632,14 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { // Attempt another cooperative channel closure. It should succeed // without any issues. - aliceSig, _, err = aliceChannel.CreateCloseProposal(aliceFeeRate, + aliceSig, _, err = aliceChannel.CreateCloseProposal(aliceFee, aliceDeliveryScript, bobDeliveryScript) if err != nil { t.Fatalf("unable to close channel: %v", err) } aliceCloseSig = append(aliceSig, byte(txscript.SigHashAll)) - bobSig, _, err = bobChannel.CreateCloseProposal(bobFeeRate, + bobSig, _, err = bobChannel.CreateCloseProposal(bobFee, bobDeliveryScript, aliceDeliveryScript) if err != nil { t.Fatalf("unable to close channel: %v", err) @@ -1644,7 +1648,7 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { closeTx, err = bobChannel.CompleteCooperativeClose( bobCloseSig, aliceCloseSig, - bobDeliveryScript, aliceDeliveryScript, bobFeeRate) + bobDeliveryScript, aliceDeliveryScript, bobFee) if err != nil { t.Fatalf("unable to accept channel close: %v", err) } @@ -1667,14 +1671,14 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { // Our final attempt at another cooperative channel closure. It should // succeed without any issues. - aliceSig, _, err = aliceChannel.CreateCloseProposal(aliceFeeRate, + aliceSig, _, err = aliceChannel.CreateCloseProposal(aliceFee, aliceDeliveryScript, bobDeliveryScript) if err != nil { t.Fatalf("unable to close channel: %v", err) } aliceCloseSig = append(aliceSig, byte(txscript.SigHashAll)) - bobSig, _, err = bobChannel.CreateCloseProposal(bobFeeRate, + bobSig, _, err = bobChannel.CreateCloseProposal(bobFee, bobDeliveryScript, aliceDeliveryScript) if err != nil { t.Fatalf("unable to close channel: %v", err) @@ -1683,7 +1687,7 @@ func TestCooperativeCloseDustAdherence(t *testing.T) { closeTx, err = bobChannel.CompleteCooperativeClose( bobCloseSig, aliceCloseSig, - bobDeliveryScript, aliceDeliveryScript, bobFeeRate) + bobDeliveryScript, aliceDeliveryScript, bobFee) if err != nil { t.Fatalf("unable to accept channel close: %v", err) } From e8e87322dd7c178f22f59888463b937d5cfe19a9 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Fri, 14 Jul 2017 21:05:55 +0200 Subject: [PATCH 2/2] peer: add channel close fee negotiation This commit adds the fee negotiation procedure performed on channel shutdown. The current algorithm picks an ideal a fee based on the FeeEstimator and commit weigth, then accepts the remote's fee if it is at most 50%-200% away from the ideal. The fee negotiation procedure is similar both as sender and receiver of the initial shutdown message, and this commit also make both sides use the same code path for handling these messages. --- fundingmanager_test.go | 149 +----------- mock.go | 182 +++++++++++++++ peer.go | 511 ++++++++++++++++++++++++---------------- peer_test.go | 520 +++++++++++++++++++++++++++++++++++++++++ test_utils.go | 268 +++++++++++++++++++++ 5 files changed, 1286 insertions(+), 344 deletions(-) create mode 100644 mock.go create mode 100644 test_utils.go diff --git a/fundingmanager_test.go b/fundingmanager_test.go index 80ff399b..474e0827 100644 --- a/fundingmanager_test.go +++ b/fundingmanager_test.go @@ -22,14 +22,10 @@ import ( _ "github.com/roasbeef/btcwallet/walletdb/bdb" "github.com/roasbeef/btcd/btcec" - "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" ) -// The block height returned by the mock BlockChainIO's GetBestBlock. -const fundingBroadcastHeight = 123 - var ( privPass = []byte("dummy-pass") @@ -70,143 +66,6 @@ var ( } ) -// mockWalletController is used by the LightningWallet, and let us mock the -// interaction with the bitcoin network. -type mockWalletController struct { - rootKey *btcec.PrivateKey - prevAddres btcutil.Address - publishedTransactions chan *wire.MsgTx -} - -// FetchInputInfo will be called to get info about the inputs to the funding -// transaction. -func (*mockWalletController) FetchInputInfo( - prevOut *wire.OutPoint) (*wire.TxOut, error) { - txOut := &wire.TxOut{ - Value: int64(10 * btcutil.SatoshiPerBitcoin), - PkScript: []byte("dummy"), - } - return txOut, nil -} -func (*mockWalletController) ConfirmedBalance(confs int32, - witness bool) (btcutil.Amount, error) { - return 0, nil -} - -// NewAddress is called to get new addresses for delivery, change etc. -func (m *mockWalletController) NewAddress(addrType lnwallet.AddressType, - change bool) (btcutil.Address, error) { - addr, _ := btcutil.NewAddressPubKey( - m.rootKey.PubKey().SerializeCompressed(), &chaincfg.MainNetParams) - return addr, nil -} -func (*mockWalletController) GetPrivKey(a btcutil.Address) (*btcec.PrivateKey, error) { - return nil, nil -} - -// NewRawKey will be called to get keys to be used for the funding tx and the -// commitment tx. -func (m *mockWalletController) NewRawKey() (*btcec.PublicKey, error) { - return m.rootKey.PubKey(), nil -} - -// FetchRootKey will be called to provide the wallet with a root key. -func (m *mockWalletController) FetchRootKey() (*btcec.PrivateKey, error) { - return m.rootKey, nil -} -func (*mockWalletController) SendOutputs(outputs []*wire.TxOut) (*chainhash.Hash, error) { - return nil, nil -} - -// ListUnspentWitness is called by the wallet when doing coin selection. We just -// need one unspent for the funding transaction. -func (*mockWalletController) ListUnspentWitness(confirms int32) ([]*lnwallet.Utxo, error) { - utxo := &lnwallet.Utxo{ - Value: btcutil.Amount(10 * btcutil.SatoshiPerBitcoin), - OutPoint: wire.OutPoint{ - Hash: chainhash.Hash{}, - Index: 0, - }, - } - var ret []*lnwallet.Utxo - ret = append(ret, utxo) - return ret, nil -} -func (*mockWalletController) ListTransactionDetails() ([]*lnwallet.TransactionDetail, error) { - return nil, nil -} -func (*mockWalletController) LockOutpoint(o wire.OutPoint) {} -func (*mockWalletController) UnlockOutpoint(o wire.OutPoint) {} -func (m *mockWalletController) PublishTransaction(tx *wire.MsgTx) error { - m.publishedTransactions <- tx - return nil -} -func (*mockWalletController) SubscribeTransactions() (lnwallet.TransactionSubscription, error) { - return nil, nil -} -func (*mockWalletController) IsSynced() (bool, error) { - return true, nil -} -func (*mockWalletController) Start() error { - return nil -} -func (*mockWalletController) Stop() error { - return nil -} - -type mockSigner struct { - key *btcec.PrivateKey -} - -func (m *mockSigner) SignOutputRaw(tx *wire.MsgTx, - signDesc *lnwallet.SignDescriptor) ([]byte, error) { - amt := signDesc.Output.Value - witnessScript := signDesc.WitnessScript - privKey := m.key - - sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, - signDesc.InputIndex, amt, witnessScript, txscript.SigHashAll, - privKey) - if err != nil { - return nil, err - } - - return sig[:len(sig)-1], nil -} - -func (m *mockSigner) ComputeInputScript(tx *wire.MsgTx, - signDesc *lnwallet.SignDescriptor) (*lnwallet.InputScript, error) { - witnessScript, err := txscript.WitnessScript(tx, signDesc.SigHashes, - signDesc.InputIndex, signDesc.Output.Value, - signDesc.Output.PkScript, txscript.SigHashAll, m.key, true) - if err != nil { - return nil, err - } - - return &lnwallet.InputScript{ - Witness: witnessScript, - }, nil -} - -type mockChainIO struct{} - -func (*mockChainIO) GetBestBlock() (*chainhash.Hash, int32, error) { - return activeNetParams.GenesisHash, fundingBroadcastHeight, 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 -} - type mockNotifier struct { confChannel chan *chainntnfs.TxConfirmation epochChan chan *chainntnfs.BlockEpoch @@ -250,7 +109,7 @@ type testNode struct { testDir string } -func disableLogger(t *testing.T) { +func disableFndgLogger(t *testing.T) { channeldb.UseLogger(btclog.Disabled) lnwallet.UseLogger(btclog.Disabled) fndgLog = btclog.Disabled @@ -652,7 +511,7 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt, } func TestFundingManagerNormalWorkflow(t *testing.T) { - disableLogger(t) + disableFndgLogger(t) shutdownChannel := make(chan struct{}) @@ -860,7 +719,7 @@ func TestFundingManagerNormalWorkflow(t *testing.T) { } func TestFundingManagerRestartBehavior(t *testing.T) { - disableLogger(t) + disableFndgLogger(t) shutdownChannel := make(chan struct{}) @@ -1095,7 +954,7 @@ func TestFundingManagerRestartBehavior(t *testing.T) { } func TestFundingManagerFundingTimeout(t *testing.T) { - disableLogger(t) + disableFndgLogger(t) shutdownChannel := make(chan struct{}) diff --git a/mock.go b/mock.go new file mode 100644 index 00000000..d88033d2 --- /dev/null +++ b/mock.go @@ -0,0 +1,182 @@ +package main + +import ( + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/roasbeef/btcd/btcec" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcd/txscript" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" +) + +// The block height returned by the mock BlockChainIO's GetBestBlock. +const fundingBroadcastHeight = 123 + +type mockSigner struct { + key *btcec.PrivateKey +} + +func (m *mockSigner) SignOutputRaw(tx *wire.MsgTx, + signDesc *lnwallet.SignDescriptor) ([]byte, error) { + amt := signDesc.Output.Value + witnessScript := signDesc.WitnessScript + privKey := m.key + + sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, + signDesc.InputIndex, amt, witnessScript, txscript.SigHashAll, + privKey) + if err != nil { + return nil, err + } + + return sig[:len(sig)-1], nil +} + +func (m *mockSigner) ComputeInputScript(tx *wire.MsgTx, + signDesc *lnwallet.SignDescriptor) (*lnwallet.InputScript, error) { + witnessScript, err := txscript.WitnessScript(tx, signDesc.SigHashes, + signDesc.InputIndex, signDesc.Output.Value, + signDesc.Output.PkScript, txscript.SigHashAll, m.key, true) + if err != nil { + return nil, err + } + + return &lnwallet.InputScript{ + Witness: witnessScript, + }, nil +} + +type mockNotfier struct { + confChannel chan *chainntnfs.TxConfirmation +} + +func (m *mockNotfier) RegisterConfirmationsNtfn(txid *chainhash.Hash, numConfs, + heightHint uint32) (*chainntnfs.ConfirmationEvent, error) { + return &chainntnfs.ConfirmationEvent{ + Confirmed: m.confChannel, + }, nil +} +func (m *mockNotfier) RegisterBlockEpochNtfn() (*chainntnfs.BlockEpochEvent, + error) { + return nil, nil +} + +func (m *mockNotfier) Start() error { + return nil +} + +func (m *mockNotfier) Stop() error { + return nil +} +func (m *mockNotfier) RegisterSpendNtfn(outpoint *wire.OutPoint, + heightHint uint32) (*chainntnfs.SpendEvent, error) { + return &chainntnfs.SpendEvent{ + Spend: make(chan *chainntnfs.SpendDetail), + Cancel: func() {}, + }, nil +} + +type mockChainIO struct{} + +func (*mockChainIO) GetBestBlock() (*chainhash.Hash, int32, error) { + return activeNetParams.GenesisHash, fundingBroadcastHeight, 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 +} + +// mockWalletController is used by the LightningWallet, and let us mock the +// interaction with the bitcoin network. +type mockWalletController struct { + rootKey *btcec.PrivateKey + prevAddres btcutil.Address + publishedTransactions chan *wire.MsgTx +} + +// FetchInputInfo will be called to get info about the inputs to the funding +// transaction. +func (*mockWalletController) FetchInputInfo( + prevOut *wire.OutPoint) (*wire.TxOut, error) { + txOut := &wire.TxOut{ + Value: int64(10 * btcutil.SatoshiPerBitcoin), + PkScript: []byte("dummy"), + } + return txOut, nil +} +func (*mockWalletController) ConfirmedBalance(confs int32, + witness bool) (btcutil.Amount, error) { + return 0, nil +} + +// NewAddress is called to get new addresses for delivery, change etc. +func (m *mockWalletController) NewAddress(addrType lnwallet.AddressType, + change bool) (btcutil.Address, error) { + addr, _ := btcutil.NewAddressPubKey( + m.rootKey.PubKey().SerializeCompressed(), &chaincfg.MainNetParams) + return addr, nil +} +func (*mockWalletController) GetPrivKey(a btcutil.Address) (*btcec.PrivateKey, error) { + return nil, nil +} + +// NewRawKey will be called to get keys to be used for the funding tx and the +// commitment tx. +func (m *mockWalletController) NewRawKey() (*btcec.PublicKey, error) { + return m.rootKey.PubKey(), nil +} + +// FetchRootKey will be called to provide the wallet with a root key. +func (m *mockWalletController) FetchRootKey() (*btcec.PrivateKey, error) { + return m.rootKey, nil +} +func (*mockWalletController) SendOutputs(outputs []*wire.TxOut) (*chainhash.Hash, error) { + return nil, nil +} + +// ListUnspentWitness is called by the wallet when doing coin selection. We just +// need one unspent for the funding transaction. +func (*mockWalletController) ListUnspentWitness(confirms int32) ([]*lnwallet.Utxo, error) { + utxo := &lnwallet.Utxo{ + Value: btcutil.Amount(10 * btcutil.SatoshiPerBitcoin), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + } + var ret []*lnwallet.Utxo + ret = append(ret, utxo) + return ret, nil +} +func (*mockWalletController) ListTransactionDetails() ([]*lnwallet.TransactionDetail, error) { + return nil, nil +} +func (*mockWalletController) LockOutpoint(o wire.OutPoint) {} +func (*mockWalletController) UnlockOutpoint(o wire.OutPoint) {} +func (m *mockWalletController) PublishTransaction(tx *wire.MsgTx) error { + m.publishedTransactions <- tx + return nil +} +func (*mockWalletController) SubscribeTransactions() (lnwallet.TransactionSubscription, error) { + return nil, nil +} +func (*mockWalletController) IsSynced() (bool, error) { + return true, nil +} +func (*mockWalletController) Start() error { + return nil +} +func (*mockWalletController) Stop() error { + return nil +} diff --git a/peer.go b/peer.go index 1e5ba284..d2faf624 100644 --- a/peer.go +++ b/peer.go @@ -4,6 +4,7 @@ import ( "container/list" "fmt" "net" + "strings" "sync" "sync/atomic" "time" @@ -900,11 +901,19 @@ func (p *peer) channelManager() { deliveryAddrs := make(map[lnwire.ChannelID]*closingScripts) - // shutdownSigs is a map of signatures maintained by the responder in a - // cooperative channel close. This map enables us to respond to - // subsequent steps in the workflow without having to recalculate our - // signature for the channel close transaction. - shutdownSigs := make(map[lnwire.ChannelID][]byte) + // initiator[ShutdownSigs|FeeProposals] holds the + // [signature|feeProposal] for the last ClosingSigned sent to the peer + // by the initiator. This enables us to respond to subsequent steps in + // the workflow without having to recalculate our signature for the + // channel close transaction, and track the sent fee proposals for fee + // negotiation purposes. + initiatorShutdownSigs := make(map[lnwire.ChannelID][]byte) + initiatorFeeProposals := make(map[lnwire.ChannelID]uint64) + + // responder[ShutdownSigs|FeeProposals] is similar to the the maps + // above, just for the responder. + responderShutdownSigs := make(map[lnwire.ChannelID][]byte) + responderFeeProposals := make(map[lnwire.ChannelID]uint64) // TODO(roasbeef): move to cfg closure func genDeliveryScript := func() ([]byte, error) { @@ -1060,6 +1069,16 @@ out: // entry for this channel, then this means that we're // the responder to the workflow. if _, ok := chanShutdowns[req.ChannelID]; !ok { + // Check responderShutdownSigs for an already + // existing shutdown signature for this channel. + // If such a signature exists, it means we + // already have sent a response to a shutdown + // message for this channel, so ignore this one. + _, exists := responderShutdownSigs[req.ChannelID] + if exists { + continue + } + // As we're the responder, we'll need to // generate a delivery script of our own. deliveryScript, err := genDeliveryScript() @@ -1072,11 +1091,12 @@ out: // In this case, we'll send a shutdown message, // and also prep our closing signature for the - // case they fees are immediately agreed upon. - closeSig := p.handleShutdownResponse(req, - deliveryScript) + // case the fees are immediately agreed upon. + closeSig, proposedFee := p.handleShutdownResponse( + req, deliveryScript) if closeSig != nil { - shutdownSigs[chanID] = closeSig + responderShutdownSigs[req.ChannelID] = closeSig + responderFeeProposals[req.ChannelID] = proposedFee } } @@ -1092,30 +1112,55 @@ out: // If it does, then this means we were the initiator of // the channel shutdown procedure. if ok { - // To finalize this shutdown, we'll now send a + shutdownSig := initiatorShutdownSigs[req.ChannelID] + initiatorSig := append(shutdownSig, + byte(txscript.SigHashAll)) + + // To finalize this shtudown, we'll now send a // matching close signed message to the other // party, and broadcast the closing transaction - // to the network. - p.handleInitClosingSigned(localCloseReq, req, - deliveryAddrs[chanID]) - - delete(chanShutdowns, req.ChannelID) - delete(deliveryAddrs, req.ChannelID) + // to the network. If the fees are still being + // negotiated, handleClosingSigned returns the + // signature and proposed fee we sent to the + // peer. In the case fee negotiation was + // complete, and the closing tx was broadcasted, + // closeSig will be nil, and we can delete the + // state associated with this channel shutdown. + closeSig, proposedFee := p.handleClosingSigned( + localCloseReq, req, + deliveryAddrs[chanID], initiatorSig, + initiatorFeeProposals[req.ChannelID]) + if closeSig != nil { + initiatorShutdownSigs[req.ChannelID] = closeSig + initiatorFeeProposals[req.ChannelID] = proposedFee + } else { + delete(initiatorShutdownSigs, req.ChannelID) + delete(initiatorFeeProposals, req.ChannelID) + delete(chanShutdowns, req.ChannelID) + delete(deliveryAddrs, req.ChannelID) + } continue } - // Otherwise, we're the responder to the channel - // shutdown procedure. In this case, we'll mark the - // channel as pending close, and watch the network for - // the ultimate confirmation of the closing - // transaction. - responderSig := append(shutdownSigs[chanID], + shutdownSig := responderShutdownSigs[req.ChannelID] + responderSig := append(shutdownSig, byte(txscript.SigHashAll)) - p.handleResponseClosingSigned(req, responderSig, - deliveryAddrs[chanID]) - delete(shutdownSigs, chanID) - delete(deliveryAddrs, chanID) + // Otherwise, we're the responder to the channel + // shutdown procedure. The procedure will be the same, + // but we don't have a local request to to notify about + // updates, so just pass in nil instead. + closeSig, proposedFee := p.handleClosingSigned(nil, req, + deliveryAddrs[chanID], responderSig, + responderFeeProposals[req.ChannelID]) + if closeSig != nil { + responderShutdownSigs[req.ChannelID] = closeSig + responderFeeProposals[req.ChannelID] = proposedFee + } else { + delete(responderShutdownSigs, req.ChannelID) + delete(responderFeeProposals, req.ChannelID) + delete(deliveryAddrs, chanID) + } case <-p.quit: break out @@ -1174,16 +1219,17 @@ func (p *peer) handleLocalClose(req *htlcswitch.ChanClose, deliveryScript []byte // close workflow receives a Shutdown message. This is the second step in the // cooperative close workflow. This function generates a close transaction with // a proposed fee amount and sends the signed transaction to the initiator. +// Returns the signature used to signed the close proposal, and the proposed +// fee. func (p *peer) handleShutdownResponse(msg *lnwire.Shutdown, - localDeliveryScript []byte) []byte { - + localDeliveryScript []byte) ([]byte, uint64) { p.activeChanMtx.RLock() channel, ok := p.activeChannels[msg.ChannelID] p.activeChanMtx.RUnlock() if !ok { peerLog.Errorf("unable to close channel, ChannelPoint(%v) is "+ "unknown", msg.ChannelID) - return nil + return nil, 0 } // As we just received a shutdown message, we'll also send a shutdown @@ -1191,46 +1237,94 @@ func (p *peer) handleShutdownResponse(msg *lnwire.Shutdown, err := p.sendShutdown(channel, localDeliveryScript) if err != nil { peerLog.Errorf("error while sending shutdown message: %v", err) - return nil + return nil, 0 } // Calculate an initial proposed fee rate for the close transaction. feeRate := p.server.cc.feeEstimator.EstimateFeePerWeight(1) * 1000 - // TODO(roasbeef): actually perform fee negotiation here, only send sig - // if we agree to fee - - // Once both sides agree on a fee, we'll create a signature that closes - // the channel using the agree upon fee rate. + // We propose a fee and send a close proposal to the peer. This will + // start the fee negotiations. Once both sides agree on a fee, we'll + // create a signature that closes the channel using the agreed upon fee. + fee := channel.CalcFee(feeRate) closeSig, proposedFee, err := channel.CreateCloseProposal( - feeRate, localDeliveryScript, msg.Address, + fee, localDeliveryScript, msg.Address, ) if err != nil { peerLog.Errorf("unable to create close proposal: %v", err) - return nil + return nil, 0 } parsedSig, err := btcec.ParseSignature(closeSig, btcec.S256()) if err != nil { peerLog.Errorf("unable to parse signature: %v", err) - return nil + return nil, 0 } // With the closing signature assembled, we'll send the matching close // signed message to the other party so they can broadcast the closing - // transaction. + // transaction if they agree with the fee, or create a new close + // proposal if they don't. closingSigned := lnwire.NewClosingSigned(msg.ChannelID, proposedFee, parsedSig) p.queueMsg(closingSigned, nil) - return closeSig + return closeSig, proposedFee } -// handleInitClosingSigned is called when the initiator in a cooperative -// channel close workflow receives a ClosingSigned message from the responder. -// This method completes the channel close transaction, sends back a -// corresponding ClosingSigned message, then broadcasts the channel close -// transaction. It also performs channel cleanup and reports status back to the -// caller. This is the initiator's final step in the channel close workflow. +// calculateCompromiseFee performs the current fee negotiation algorithm, +// taking into consideration our ideal fee based on current fee environment, +// the fee we last proposed (if any), and the fee proposed by the peer. +func calculateCompromiseFee(ourIdealFee, lastSentFee, peerFee uint64) uint64 { + // We will accept a proposed fee in the interval + // [0.5*ourIdealFee, 2*ourIdealFee]. If the peer's fee doesn't fall in + // this range, we'll propose the average of the peer's fee and our last + // sent fee, as long as it is in this range. + // TODO(halseth): Dynamic fee to determine what we consider min/max for + // timely confirmation. + maxFee := 2 * ourIdealFee + minFee := ourIdealFee / 2 + + // If we didn't propose a fee before, just use our ideal fee value for + // the average calculation. + if lastSentFee == 0 { + lastSentFee = ourIdealFee + } + avgFee := (lastSentFee + peerFee) / 2 + + switch { + case peerFee <= maxFee && peerFee >= minFee: + // Peer fee is in the accepted range. + return peerFee + case avgFee <= maxFee && avgFee >= minFee: + // The peer's fee is not in the accepted range, but the average + // fee is. + return avgFee + case avgFee > maxFee: + // TODO(halseth): We must ensure fee is not higher than the + // current fee on the commitment transaction. + + // We cannot accept the average fee, as it is more than twice + // our own estimate. Set our proposed to the maximum we can + // accept. + return maxFee + default: + // Cannot accept the average, as we consider it too low. + return minFee + } +} + +// handleClosingSigned is called when the a ClosingSigned message is received +// from the peer. If we are the initiator in the shutdown procedure, localReq +// should be set to the local close request. If we are the responder, it should +// be set to nil. +// +// This method sends the necessary ClosingSigned message to continue fee +// negotiation, and in case we agreed on a fee completes the channel close +// transaction, and then broadcasts it. It also performs channel cleanup (and +// reports status back to the caller if this was a local shutdown request). +// +// It returns the signature and the proposed fee included in the ClosingSigned +// sent to the peer. // // Following the broadcast, both the initiator and responder in the channel // closure workflow should watch the blockchain for a confirmation of the @@ -1238,10 +1332,10 @@ func (p *peer) handleShutdownResponse(msg *lnwire.Shutdown, // of an unresponsive remote party, the initiator can either choose to execute // a force closure, or backoff for a period of time, and retry the cooperative // closure. -func (p *peer) handleInitClosingSigned(req *htlcswitch.ChanClose, - msg *lnwire.ClosingSigned, deliveryScripts *closingScripts) { - - chanID := lnwire.NewChanIDFromOutPoint(req.ChanPoint) +func (p *peer) handleClosingSigned(localReq *htlcswitch.ChanClose, + msg *lnwire.ClosingSigned, deliveryScripts *closingScripts, + lastSig []byte, lastFee uint64) ([]byte, uint64) { + chanID := msg.ChannelID p.activeChanMtx.RLock() channel, ok := p.activeChannels[chanID] p.activeChanMtx.RUnlock() @@ -1249,71 +1343,66 @@ func (p *peer) handleInitClosingSigned(req *htlcswitch.ChanClose, err := fmt.Errorf("unable to close channel, ChannelID(%v) is "+ "unknown", chanID) peerLog.Errorf(err.Error()) - req.Err <- err - return + if localReq != nil { + localReq.Err <- err + } + return nil, 0 } - - // Calculate a fee rate that we believe to be fair and will ensure a - // timely confirmation. - // - // TODO(bvu): with a dynamic fee implementation, we will compare this - // to the fee proposed by the responder in their ClosingSigned message. - feeRate := p.server.cc.feeEstimator.EstimateFeePerWeight(1) * 1000 - - // We agree with the proposed channel close transaction and fee rate, - // so generate our signature. - initiatorSig, proposedFee, err := channel.CreateCloseProposal( - feeRate, deliveryScripts.localScript, deliveryScripts.remoteScript, - ) + // We now consider the fee proposed by the peer, together with the fee + // we last proposed (if any). This method will in case more fee + // negotiation is necessary send a new ClosingSigned message to the peer + // with our new proposed fee. In case we can agree on a fee, it will + // assemble the close transaction, and we can go on to broadcasting it. + closeTx, ourSig, ourFee, err := p.negotiateFeeAndCreateCloseTx(channel, + msg, deliveryScripts, lastSig, lastFee) if err != nil { - req.Err <- err - return - } - initSig := append(initiatorSig, byte(txscript.SigHashAll)) - - // Complete coop close transaction with the signatures of the close - // initiator and responder. - responderSig := msg.Signature - respSig := append(responderSig.Serialize(), byte(txscript.SigHashAll)) - closeTx, err := channel.CompleteCooperativeClose(initSig, respSig, - deliveryScripts.localScript, deliveryScripts.remoteScript, - feeRate) - if err != nil { - req.Err <- err - // TODO(roasbeef): send ErrorGeneric to other side - return + if localReq != nil { + localReq.Err <- err + } + return nil, 0 } - // As we're the initiator of this channel shutdown procedure we'll now - // create a mirrored close signed message with our completed signature. - parsedSig, err := btcec.ParseSignature(initSig, btcec.S256()) - if err != nil { - req.Err <- err - return + // If closeTx == nil it means that we did not agree on a fee, but we + // proposed a new fee to the peer. Return the signature used for this + // new proposal, and the fee we proposed, for use when we get a reponse. + if closeTx == nil { + return ourSig, ourFee } - closingSigned := lnwire.NewClosingSigned(chanID, proposedFee, parsedSig) - p.queueMsg(closingSigned, nil) - // Finally, broadcast the closure transaction to the network. + // We agreed on a fee, and we can broadcast the closure transaction to + // the network. peerLog.Infof("Broadcasting cooperative close tx: %v", newLogClosure(func() string { return spew.Sdump(closeTx) })) + + chanPoint := channel.ChannelPoint() if err := p.server.cc.wallet.PublishTransaction(closeTx); err != nil { - peerLog.Errorf("channel close tx from "+ - "ChannelPoint(%v) rejected: %v", - req.ChanPoint, err) - // TODO(roasbeef): send ErrorGeneric to other side - return + // TODO(halseth): Add relevant error types to the + // WalletController interface as this is quite fragile. + if strings.Contains(err.Error(), "already exists") || + strings.Contains(err.Error(), "already have") { + peerLog.Infof("channel close tx from ChannelPoint(%v) "+ + " already exist, probably broadcasted by peer: %v", + chanPoint, err) + } else { + peerLog.Errorf("channel close tx from ChannelPoint(%v) "+ + " rejected: %v", chanPoint, err) + + // TODO(roasbeef): send ErrorGeneric to other side + return nil, 0 + } } // Once we've completed the cooperative channel closure, we'll wipe the // channel so we reject any incoming forward or payment requests via // this channel. - p.server.breachArbiter.settledContracts <- req.ChanPoint + p.server.breachArbiter.settledContracts <- chanPoint if err := p.WipeChannel(channel); err != nil { - req.Err <- err - return + if localReq != nil { + localReq.Err <- err + } + return nil, 0 } // TODO(roasbeef): also add closure height to summary @@ -1323,7 +1412,7 @@ func (p *peer) handleInitClosingSigned(req *htlcswitch.ChanClose, closingTxid := closeTx.TxHash() chanInfo := channel.StateSnapshot() closeSummary := &channeldb.ChannelCloseSummary{ - ChanPoint: *req.ChanPoint, + ChanPoint: *chanPoint, ClosingTXID: closingTxid, RemotePub: &chanInfo.RemoteIdentity, Capacity: chanInfo.Capacity, @@ -1332,144 +1421,168 @@ func (p *peer) handleInitClosingSigned(req *htlcswitch.ChanClose, IsPending: true, } if err := channel.DeleteState(closeSummary); err != nil { - req.Err <- err - return + if localReq != nil { + localReq.Err <- err + } + return nil, 0 } - // Update the caller with a new event detailing the current pending - // state of this request. - req.Updates <- &lnrpc.CloseStatusUpdate{ - Update: &lnrpc.CloseStatusUpdate_ClosePending{ - ClosePending: &lnrpc.PendingUpdate{ - Txid: closingTxid[:], + // If this is a locally requested shutdown, update the caller with a new + // event detailing the current pending state of this request. + if localReq != nil { + localReq.Updates <- &lnrpc.CloseStatusUpdate{ + Update: &lnrpc.CloseStatusUpdate_ClosePending{ + ClosePending: &lnrpc.PendingUpdate{ + Txid: closingTxid[:], + }, }, - }, + } } _, bestHeight, err := p.server.cc.chainIO.GetBestBlock() if err != nil { - req.Err <- err - return + if localReq != nil { + localReq.Err <- err + } + return nil, 0 } // Finally, launch a goroutine which will request to be notified by the // ChainNotifier once the closure transaction obtains a single // confirmation. notifier := p.server.cc.chainNotifier - go waitForChanToClose(uint32(bestHeight), notifier, req.Err, - req.ChanPoint, &closingTxid, func() { + + // If any error happens during waitForChanToClose, forard it to + // localReq. If this channel closure is not locally initiated, localReq + // will be nil, so just ignore the error. + errChan := make(chan error, 1) + if localReq != nil { + errChan = localReq.Err + } + + go waitForChanToClose(uint32(bestHeight), notifier, errChan, + chanPoint, &closingTxid, func() { + // First, we'll mark the database as being fully closed // so we'll no longer watch for its ultimate closure // upon startup. - err := p.server.chanDB.MarkChanFullyClosed(req.ChanPoint) + err := p.server.chanDB.MarkChanFullyClosed(chanPoint) if err != nil { - req.Err <- err + if localReq != nil { + localReq.Err <- err + } return } // Respond to the local subsystem which requested the // channel closure. - req.Updates <- &lnrpc.CloseStatusUpdate{ - Update: &lnrpc.CloseStatusUpdate_ChanClose{ - ChanClose: &lnrpc.ChannelCloseUpdate{ - ClosingTxid: closingTxid[:], - Success: true, + if localReq != nil { + localReq.Updates <- &lnrpc.CloseStatusUpdate{ + Update: &lnrpc.CloseStatusUpdate_ChanClose{ + ChanClose: &lnrpc.ChannelCloseUpdate{ + ClosingTxid: closingTxid[:], + Success: true, + }, }, - }, + } } }) + return nil, 0 } -// handleResponseClosingSigned is called when the responder in a cooperative -// close workflow receives a ClosingSigned message. This function handles the -// finalization of the cooperative close from the perspective of the responder. -func (p *peer) handleResponseClosingSigned(msg *lnwire.ClosingSigned, - respSig []byte, deliveryScripts *closingScripts) { +// negotiateFeeAndCreateCloseTx takes into consideration the closing transaction +// fee proposed by the remote peer in the ClosingSigned message and our +// previously proposed fee (set to 0 if no previous), and continues the fee +// negotiation it process. In case the peer agreed on the same fee as we +// previously sent, it will assemble the close transaction and broadcast it. In +// case the peer propose a fee different from our previous proposal, but that +// can be accepted, a ClosingSigned message with the accepted fee is sent, +// before the closing transaction is broadcasted. In the case where we cannot +// accept the peer's proposed fee, a new fee proposal will be sent. +// +// TODO(halseth): In the case where we cannot accept the fee, and we cannot +// make more proposals, this method should return an error, and we should fail +// the channel. +func (p *peer) negotiateFeeAndCreateCloseTx(channel *lnwallet.LightningChannel, + msg *lnwire.ClosingSigned, deliveryScripts *closingScripts, ourSig []byte, + ourFeeProp uint64) (*wire.MsgTx, []byte, uint64, error) { - p.activeChanMtx.RLock() - channel, ok := p.activeChannels[msg.ChannelID] - p.activeChanMtx.RUnlock() - if !ok { - peerLog.Errorf("unable to close channel, ChannelID(%v) is "+ - "unknown", msg.ChannelID) - return + peerFeeProposal := msg.FeeSatoshis + + // If the fee proposed by the peer is different from what we proposed + // before (or we did not propose anything yet), we must check if we can + // accept the proposal, or if we should negotiate. + if peerFeeProposal != ourFeeProp { + // The peer has suggested a different fee from what we proposed. + // Let's calculate if this one is tolerable. + ourIdealFeeRate := p.server.cc.feeEstimator. + EstimateFeePerWeight(1) * 1000 + ourIdealFee := channel.CalcFee(ourIdealFeeRate) + fee := calculateCompromiseFee(ourIdealFee, ourFeeProp, + peerFeeProposal) + + // Our new proposed fee must be strictly between what we + // proposed before and what the peer proposed. + isAcceptable := false + if fee < peerFeeProposal && fee > ourFeeProp { + isAcceptable = true + } + if fee < ourFeeProp && fee > peerFeeProposal { + isAcceptable = true + } + + if !isAcceptable { + // TODO(halseth): fail channel + } + + // Since the compromise fee is different from the fee we last + // proposed, we must update our proposal. + + // Create a new close proposal with the compromise fee, and + // send this to the peer. + closeSig, proposedFee, err := channel.CreateCloseProposal(fee, + deliveryScripts.localScript, deliveryScripts.remoteScript) + if err != nil { + peerLog.Errorf("unable to create close proposal: %v", + err) + return nil, nil, 0, err + } + parsedSig, err := btcec.ParseSignature(closeSig, btcec.S256()) + if err != nil { + peerLog.Errorf("unable to parse signature: %v", err) + return nil, nil, 0, err + } + closingSigned := lnwire.NewClosingSigned(msg.ChannelID, + proposedFee, parsedSig) + p.queueMsg(closingSigned, nil) + + // If the compromise fee was different from what the peer + // proposed, then we must return and wait for an answer, if not + // we can go on to complete the close transaction. + if fee != peerFeeProposal { + return nil, closeSig, proposedFee, nil + } + + // We accept the fee proposed by the peer, so prepare our + // signature to complete the close transaction. + ourSig = append(closeSig, byte(txscript.SigHashAll)) } - // Now that we have the initiator's signature for the closure - // transaction, we can assemble the final closure transaction, complete - // with our signature. - initiatorSig := msg.Signature - initSig := append(initiatorSig.Serialize(), byte(txscript.SigHashAll)) + // We agreed on a fee, and we have the peer's signature for this fee, + // so we can assemble the close tx. + peerSig := append(msg.Signature.Serialize(), byte(txscript.SigHashAll)) chanPoint := channel.ChannelPoint() - - // Calculate our expected fee rate. - // TODO(roasbeef): should instead use the fee within the message - feeRate := p.server.cc.feeEstimator.EstimateFeePerWeight(1) * 1000 - closeTx, err := channel.CompleteCooperativeClose(respSig, initSig, + closeTx, err := channel.CompleteCooperativeClose(ourSig, peerSig, deliveryScripts.localScript, deliveryScripts.remoteScript, - feeRate) + peerFeeProposal) if err != nil { peerLog.Errorf("unable to complete cooperative "+ "close for ChannelPoint(%v): %v", chanPoint, err) // TODO(roasbeef): send ErrorGeneric to other side - return + return nil, nil, 0, err } - closeTxid := closeTx.TxHash() - - _, bestHeight, err := p.server.cc.chainIO.GetBestBlock() - if err != nil { - peerLog.Errorf("unable to get best height: %v", err) - } - - // Once we've completed the cooperative channel closure, we'll wipe the - // channel so we reject any incoming forward or payment requests via - // this channel. - p.server.breachArbiter.settledContracts <- chanPoint - - // We've just broadcast the transaction which closes the channel, so - // we'll wipe the channel from all our local indexes and also signal to - // the switch that this channel is now closed. - peerLog.Infof("ChannelPoint(%v) is now closed", chanPoint) - if err := p.WipeChannel(channel); err != nil { - peerLog.Errorf("unable to wipe channel: %v", err) - } - - // Clear out the current channel state, marking the channel as being - // closed within the database. - chanInfo := channel.StateSnapshot() - closeSummary := &channeldb.ChannelCloseSummary{ - ChanPoint: *chanPoint, - ClosingTXID: closeTxid, - RemotePub: &chanInfo.RemoteIdentity, - Capacity: chanInfo.Capacity, - SettledBalance: chanInfo.LocalBalance, - CloseType: channeldb.CooperativeClose, - IsPending: true, - } - if err := channel.DeleteState(closeSummary); err != nil { - peerLog.Errorf("unable to delete channel state: %v", err) - return - } - - // Finally, we'll launch a goroutine to watch the network for the - // confirmation of the closing transaction, and mark the channel as - // such within the database (once it's confirmed"). - notifier := p.server.cc.chainNotifier - go waitForChanToClose(uint32(bestHeight), notifier, nil, chanPoint, - &closeTxid, func() { - // Now that the closing transaction has been confirmed, - // we'll mark the database as being fully closed so now - // that we no longer watch for its ultimate closure - // upon startup. - err := p.server.chanDB.MarkChanFullyClosed(chanPoint) - if err != nil { - peerLog.Errorf("unable to mark channel "+ - "as closed: %v", err) - return - } - }, - ) + return closeTx, nil, 0, nil } // waitForChanToClose uses the passed notifier to wait until the channel has diff --git a/peer_test.go b/peer_test.go index 06ab7d0f..aa50b7c3 100644 --- a/peer_test.go +++ b/peer_test.go @@ -1 +1,521 @@ package main + +import ( + "testing" + "time" + + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/htlcswitch" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/roasbeef/btcd/btcec" + "github.com/roasbeef/btcd/txscript" + "github.com/roasbeef/btcd/wire" +) + +func disablePeerLogger(t *testing.T) { + peerLog = btclog.Disabled + srvrLog = btclog.Disabled + lnwallet.UseLogger(btclog.Disabled) + htlcswitch.UseLogger(btclog.Disabled) + channeldb.UseLogger(btclog.Disabled) +} + +// TestPeerChannelClosureAcceptFeeResponder tests the shutdown responder's +// behavior if we can agree on the fee immediately. +func TestPeerChannelClosureAcceptFeeResponder(t *testing.T) { + disablePeerLogger(t) + t.Parallel() + + notifier := &mockNotfier{ + confChannel: make(chan *chainntnfs.TxConfirmation), + } + broadcastTxChan := make(chan *wire.MsgTx) + + responder, responderChan, initiatorChan, cleanUp, err := createTestPeer( + notifier, broadcastTxChan) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + chanID := lnwire.NewChanIDFromOutPoint(responderChan.ChannelPoint()) + + // We send a shutdown request to Alice. She will now be the responding + // node in this shutdown procedure. We first expect Alice to answer this + // shutdown request with a Shutdown message. + responder.shutdownChanReqs <- lnwire.NewShutdown(chanID, dummyDeliveryScript) + + var msg lnwire.Message + select { + case outMsg := <-responder.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive shutdown message") + } + + shutdownMsg, ok := msg.(*lnwire.Shutdown) + if !ok { + t.Fatalf("expected Shutdown message, got %T", msg) + } + + respDeliveryScript := shutdownMsg.Address + + // Alice will thereafter send a ClosingSigned message, indicating her + // proposed closing transaction fee. + select { + case outMsg := <-responder.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive ClosingSigned message") + } + + responderClosingSigned, ok := msg.(*lnwire.ClosingSigned) + if !ok { + t.Fatalf("expected ClosingSigned message, got %T", msg) + } + + // We accept the fee, and send a ClosingSigned with the same fee back, + // so she knows we agreed. + peerFee := responderClosingSigned.FeeSatoshis + initiatorSig, proposedFee, err := initiatorChan.CreateCloseProposal( + peerFee, dummyDeliveryScript, respDeliveryScript) + if err != nil { + t.Fatalf("error creating close proposal: %v", err) + } + + initSig := append(initiatorSig, byte(txscript.SigHashAll)) + parsedSig, err := btcec.ParseSignature(initSig, btcec.S256()) + if err != nil { + t.Fatalf("error parsing signature: %v", err) + } + closingSigned := lnwire.NewClosingSigned(chanID, proposedFee, parsedSig) + responder.closingSignedChanReqs <- closingSigned + + // The responder will now see that we agreed on the fee, and broadcast + // the closing transaction. + select { + case <-broadcastTxChan: + case <-time.After(time.Second * 5): + t.Fatalf("closing tx not broadcast") + } + + // And the initiator should be waiting for a confirmation notification. + notifier.confChannel <- &chainntnfs.TxConfirmation{} +} + +// TestPeerChannelClosureAcceptFeeInitiator tests the shutdown initiator's +// behavior if we can agree on the fee immediately. +func TestPeerChannelClosureAcceptFeeInitiator(t *testing.T) { + disablePeerLogger(t) + t.Parallel() + + notifier := &mockNotfier{ + confChannel: make(chan *chainntnfs.TxConfirmation), + } + broadcastTxChan := make(chan *wire.MsgTx) + + initiator, initiatorChan, responderChan, cleanUp, err := createTestPeer( + notifier, broadcastTxChan) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + // We make the initiator send a shutdown request. + updateChan := make(chan *lnrpc.CloseStatusUpdate, 1) + errChan := make(chan error, 1) + closeCommand := &htlcswitch.ChanClose{ + CloseType: htlcswitch.CloseRegular, + ChanPoint: initiatorChan.ChannelPoint(), + Updates: updateChan, + Err: errChan, + } + initiator.localCloseChanReqs <- closeCommand + + // We should now be getting the shutdown request. + var msg lnwire.Message + select { + case outMsg := <-initiator.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive shutdown request") + } + + shutdownMsg, ok := msg.(*lnwire.Shutdown) + if !ok { + t.Fatalf("expected Shutdown message, got %T", msg) + } + + initiatorDeliveryScript := shutdownMsg.Address + + // We'll answer the shutdown message with our own Shutdown, and then a + // ClosingSigned message. + chanID := shutdownMsg.ChannelID + initiator.shutdownChanReqs <- lnwire.NewShutdown(chanID, + dummyDeliveryScript) + + estimator := lnwallet.StaticFeeEstimator{FeeRate: 50} + feeRate := estimator.EstimateFeePerWeight(1) * 1000 + fee := responderChan.CalcFee(feeRate) + closeSig, proposedFee, err := responderChan.CreateCloseProposal(fee, + dummyDeliveryScript, initiatorDeliveryScript) + if err != nil { + t.Fatalf("unable to create close proposal: %v", err) + } + parsedSig, err := btcec.ParseSignature(closeSig, btcec.S256()) + if err != nil { + t.Fatalf("unable to parse signature: %v", err) + } + + closingSigned := lnwire.NewClosingSigned(shutdownMsg.ChannelID, + proposedFee, parsedSig) + initiator.closingSignedChanReqs <- closingSigned + + // And we expect the initiator to accept the fee, and broadcast the + // closing transaction. + select { + case outMsg := <-initiator.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive closing signed message") + } + + closingSignedMsg, ok := msg.(*lnwire.ClosingSigned) + if !ok { + t.Fatalf("expected ClosingSigned message, got %T", msg) + } + + if closingSignedMsg.FeeSatoshis != proposedFee { + t.Fatalf("expected ClosingSigned fee to be %v, instead got %v", + proposedFee, closingSignedMsg.FeeSatoshis) + } + + // The initiator will now see that we agreed on the fee, and broadcast + // the closing transaction. + select { + case <-broadcastTxChan: + case <-time.After(time.Second * 5): + t.Fatalf("closing tx not broadcast") + } + + // And the initiator should be waiting for a confirmation notification. + notifier.confChannel <- &chainntnfs.TxConfirmation{} +} + +// TestPeerChannelClosureFeeNegotiationsResponder tests the shutdown responder's +// behavior in the case where we must do several rounds of fee negotiation +// before we agree on a fee. +func TestPeerChannelClosureFeeNegotiationsResponder(t *testing.T) { + disablePeerLogger(t) + t.Parallel() + + notifier := &mockNotfier{ + confChannel: make(chan *chainntnfs.TxConfirmation), + } + broadcastTxChan := make(chan *wire.MsgTx) + + responder, responderChan, initiatorChan, cleanUp, err := createTestPeer( + notifier, broadcastTxChan) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + chanID := lnwire.NewChanIDFromOutPoint(responderChan.ChannelPoint()) + + // We send a shutdown request to Alice. She will now be the responding + // node in this shutdown procedure. We first expect Alice to answer this + // shutdown request with a Shutdown message. + responder.shutdownChanReqs <- lnwire.NewShutdown(chanID, + dummyDeliveryScript) + + var msg lnwire.Message + select { + case outMsg := <-responder.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive shutdown message") + } + + shutdownMsg, ok := msg.(*lnwire.Shutdown) + if !ok { + t.Fatalf("expected Shutdown message, got %T", msg) + } + + respDeliveryScript := shutdownMsg.Address + + // Alice will thereafter send a ClosingSigned message, indicating her + // proposed closing transaction fee. + select { + case outMsg := <-responder.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive closing signed message") + } + + responderClosingSigned, ok := msg.(*lnwire.ClosingSigned) + if !ok { + t.Fatalf("expected ClosingSigned message, got %T", msg) + } + + // We don't agree with the fee, and will send back one that's 2.5x. + preferredRespFee := responderClosingSigned.FeeSatoshis + increasedFee := uint64(float64(preferredRespFee) * 2.5) + initiatorSig, proposedFee, err := initiatorChan.CreateCloseProposal( + increasedFee, dummyDeliveryScript, respDeliveryScript, + ) + if err != nil { + t.Fatalf("error creating close proposal: %v", err) + } + + parsedSig, err := btcec.ParseSignature(initiatorSig, btcec.S256()) + if err != nil { + t.Fatalf("error parsing signature: %v", err) + } + closingSigned := lnwire.NewClosingSigned(chanID, proposedFee, parsedSig) + responder.closingSignedChanReqs <- closingSigned + + // The responder will see the new fee we propose, but with current + // settings wont't accept anything over 2*FeeRate. We should get a new + // proposal back, which should have the average fee rate proposed. + select { + case outMsg := <-responder.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive closing signed message") + } + + responderClosingSigned, ok = msg.(*lnwire.ClosingSigned) + if !ok { + t.Fatalf("expected ClosingSigned message, got %T", msg) + } + + avgFee := (preferredRespFee + increasedFee) / 2 + peerFee := responderClosingSigned.FeeSatoshis + if peerFee != avgFee { + t.Fatalf("expected ClosingSigned with fee %v, got %v", + proposedFee, responderClosingSigned.FeeSatoshis) + } + + // We try negotiating a 2.1x fee, which should also be rejected. + increasedFee = uint64(float64(preferredRespFee) * 2.1) + initiatorSig, proposedFee, err = initiatorChan.CreateCloseProposal( + increasedFee, dummyDeliveryScript, respDeliveryScript, + ) + if err != nil { + t.Fatalf("error creating close proposal: %v", err) + } + + parsedSig, err = btcec.ParseSignature(initiatorSig, btcec.S256()) + if err != nil { + t.Fatalf("error parsing signature: %v", err) + } + closingSigned = lnwire.NewClosingSigned(chanID, proposedFee, parsedSig) + responder.closingSignedChanReqs <- closingSigned + + // It still won't be accepted, and we should get a new proposal, the + // average of what we proposed, and what they proposed last time. + select { + case outMsg := <-responder.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive closing signed message") + } + + responderClosingSigned, ok = msg.(*lnwire.ClosingSigned) + if !ok { + t.Fatalf("expected ClosingSigned message, got %T", msg) + } + + avgFee = (peerFee + increasedFee) / 2 + peerFee = responderClosingSigned.FeeSatoshis + if peerFee != avgFee { + t.Fatalf("expected ClosingSigned with fee %v, got %v", + proposedFee, responderClosingSigned.FeeSatoshis) + } + + // Accept fee. + initiatorSig, proposedFee, err = initiatorChan.CreateCloseProposal( + peerFee, dummyDeliveryScript, respDeliveryScript, + ) + if err != nil { + t.Fatalf("error creating close proposal: %v", err) + } + + initSig := append(initiatorSig, byte(txscript.SigHashAll)) + parsedSig, err = btcec.ParseSignature(initSig, btcec.S256()) + if err != nil { + t.Fatalf("error parsing signature: %v", err) + } + closingSigned = lnwire.NewClosingSigned(chanID, proposedFee, parsedSig) + responder.closingSignedChanReqs <- closingSigned + + // The responder will now see that we agreed on the fee, and broadcast + // the closing transaction. + select { + case <-broadcastTxChan: + case <-time.After(time.Second * 5): + t.Fatalf("closing tx not broadcast") + } + + // And the responder should be waiting for a confirmation notification. + notifier.confChannel <- &chainntnfs.TxConfirmation{} +} + +// TestPeerChannelClosureFeeNegotiationsInitiator tests the shutdown initiator's +// behavior in the case where we must do several rounds of fee negotiation +// before we agree on a fee. +func TestPeerChannelClosureFeeNegotiationsInitiator(t *testing.T) { + disablePeerLogger(t) + t.Parallel() + + notifier := &mockNotfier{ + confChannel: make(chan *chainntnfs.TxConfirmation), + } + broadcastTxChan := make(chan *wire.MsgTx) + + initiator, initiatorChan, responderChan, cleanUp, err := createTestPeer( + notifier, broadcastTxChan) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + // We make the initiator send a shutdown request. + updateChan := make(chan *lnrpc.CloseStatusUpdate, 1) + errChan := make(chan error, 1) + closeCommand := &htlcswitch.ChanClose{ + CloseType: htlcswitch.CloseRegular, + ChanPoint: initiatorChan.ChannelPoint(), + Updates: updateChan, + Err: errChan, + } + + initiator.localCloseChanReqs <- closeCommand + + // We should now be getting the shutdown request. + var msg lnwire.Message + select { + case outMsg := <-initiator.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive shutdown request") + } + + shutdownMsg, ok := msg.(*lnwire.Shutdown) + if !ok { + t.Fatalf("expected Shutdown message, got %T", msg) + } + + initiatorDeliveryScript := shutdownMsg.Address + + // We'll answer the shutdown message with our own Shutdown, and then a + // ClosingSigned message. + chanID := lnwire.NewChanIDFromOutPoint(initiatorChan.ChannelPoint()) + respShutdown := lnwire.NewShutdown(chanID, dummyDeliveryScript) + initiator.shutdownChanReqs <- respShutdown + + estimator := lnwallet.StaticFeeEstimator{FeeRate: 50} + initiatorIdealFeeRate := estimator.EstimateFeePerWeight(1) * 1000 + initiatorIdealFee := responderChan.CalcFee(initiatorIdealFeeRate) + increasedFee := uint64(float64(initiatorIdealFee) * 2.5) + closeSig, proposedFee, err := responderChan.CreateCloseProposal( + increasedFee, dummyDeliveryScript, initiatorDeliveryScript, + ) + if err != nil { + t.Fatalf("unable to create close proposal: %v", err) + } + parsedSig, err := btcec.ParseSignature(closeSig, btcec.S256()) + if err != nil { + t.Fatalf("unable to parse signature: %v", err) + } + + closingSigned := lnwire.NewClosingSigned(shutdownMsg.ChannelID, + proposedFee, parsedSig) + initiator.closingSignedChanReqs <- closingSigned + + // And we expect the initiator to reject the fee, and suggest a lower + // one. + select { + case outMsg := <-initiator.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive closing signed") + } + + closingSignedMsg, ok := msg.(*lnwire.ClosingSigned) + if !ok { + t.Fatalf("expected ClosingSigned message, got %T", msg) + } + avgFee := (initiatorIdealFee + increasedFee) / 2 + peerFee := closingSignedMsg.FeeSatoshis + if peerFee != avgFee { + t.Fatalf("expected ClosingSigned fee to be %v, instead got %v", + avgFee, peerFee) + } + + // We try negotiating a 2.1x fee, which should also be rejected. + increasedFee = uint64(float64(initiatorIdealFee) * 2.1) + responderSig, proposedFee, err := responderChan.CreateCloseProposal( + increasedFee, dummyDeliveryScript, initiatorDeliveryScript, + ) + if err != nil { + t.Fatalf("error creating close proposal: %v", err) + } + + parsedSig, err = btcec.ParseSignature(responderSig, btcec.S256()) + if err != nil { + t.Fatalf("error parsing signature: %v", err) + } + + closingSigned = lnwire.NewClosingSigned(chanID, proposedFee, parsedSig) + initiator.closingSignedChanReqs <- closingSigned + + // It still won't be accepted, and we should get a new proposal, the + // average of what we proposed, and what they proposed last time. + select { + case outMsg := <-initiator.outgoingQueue: + msg = outMsg.msg + case <-time.After(time.Second * 5): + t.Fatalf("did not receive closing signed") + } + + initiatorClosingSigned, ok := msg.(*lnwire.ClosingSigned) + if !ok { + t.Fatalf("expected ClosingSigned message, got %T", msg) + } + + avgFee = (peerFee + increasedFee) / 2 + peerFee = initiatorClosingSigned.FeeSatoshis + if peerFee != avgFee { + t.Fatalf("expected ClosingSigned with fee %v, got %v", + proposedFee, initiatorClosingSigned.FeeSatoshis) + } + + // Accept fee. + responderSig, proposedFee, err = responderChan.CreateCloseProposal( + peerFee, dummyDeliveryScript, initiatorDeliveryScript, + ) + if err != nil { + t.Fatalf("error creating close proposal: %v", err) + } + + respSig := append(responderSig, byte(txscript.SigHashAll)) + parsedSig, err = btcec.ParseSignature(respSig, btcec.S256()) + if err != nil { + t.Fatalf("error parsing signature: %v", err) + } + closingSigned = lnwire.NewClosingSigned(chanID, proposedFee, parsedSig) + initiator.closingSignedChanReqs <- closingSigned + + // Wait for closing tx to be broadcasted. + select { + case <-broadcastTxChan: + case <-time.After(time.Second * 5): + t.Fatalf("closing tx not broadcast") + } +} diff --git a/test_utils.go b/test_utils.go new file mode 100644 index 00000000..77e3af03 --- /dev/null +++ b/test_utils.go @@ -0,0 +1,268 @@ +package main + +import ( + "bytes" + "io/ioutil" + "math/rand" + "net" + "os" + + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/htlcswitch" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" + "github.com/roasbeef/btcd/btcec" + "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" +) + +var ( + alicesPrivKey = []byte{ + 0x2b, 0xd8, 0x06, 0xc9, 0x7f, 0x0e, 0x00, 0xaf, + 0x1a, 0x1f, 0xc3, 0x32, 0x8f, 0xa7, 0x63, 0xa9, + 0x26, 0x97, 0x23, 0xc8, 0xdb, 0x8f, 0xac, 0x4f, + 0x93, 0xaf, 0x71, 0xdb, 0x18, 0x6d, 0x6e, 0x90, + } + + bobsPrivKey = []byte{ + 0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda, + 0x63, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17, + 0xd, 0xe7, 0x95, 0xe4, 0xb7, 0x25, 0xb8, 0x4d, + 0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9, + } + + // Use a hard-coded HD seed. + testHdSeed = [32]byte{ + 0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab, + 0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4, + 0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9, + 0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53, + } + + // Just use some arbitrary bytes as delivery script. + dummyDeliveryScript = alicesPrivKey[:] +) + +// createTestPeer creates a channel between two nodes, and returns a peer for +// one of the nodes, together with the channel seen from both nodes. +func createTestPeer(notifier chainntnfs.ChainNotifier, + publTx chan *wire.MsgTx) (*peer, *lnwallet.LightningChannel, + *lnwallet.LightningChannel, func(), error) { + + aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + alicesPrivKey) + bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + bobsPrivKey) + + channelCapacity := btcutil.Amount(10 * 1e8) + channelBal := channelCapacity / 2 + aliceDustLimit := btcutil.Amount(200) + bobDustLimit := btcutil.Amount(1300) + csvTimeoutAlice := uint32(5) + csvTimeoutBob := uint32(4) + + prevOut := &wire.OutPoint{ + Hash: chainhash.Hash(testHdSeed), + Index: 0, + } + fundingTxIn := wire.NewTxIn(prevOut, nil, nil) + + aliceCfg := channeldb.ChannelConfig{ + ChannelConstraints: channeldb.ChannelConstraints{ + DustLimit: aliceDustLimit, + MaxPendingAmount: btcutil.Amount(rand.Int63()), + ChanReserve: btcutil.Amount(rand.Int63()), + MinHTLC: btcutil.Amount(rand.Int63()), + MaxAcceptedHtlcs: uint16(rand.Int31()), + }, + CsvDelay: uint16(csvTimeoutAlice), + MultiSigKey: aliceKeyPub, + RevocationBasePoint: aliceKeyPub, + PaymentBasePoint: aliceKeyPub, + DelayBasePoint: aliceKeyPub, + } + bobCfg := channeldb.ChannelConfig{ + ChannelConstraints: channeldb.ChannelConstraints{ + DustLimit: bobDustLimit, + MaxPendingAmount: btcutil.Amount(rand.Int63()), + ChanReserve: btcutil.Amount(rand.Int63()), + MinHTLC: btcutil.Amount(rand.Int63()), + MaxAcceptedHtlcs: uint16(rand.Int31()), + }, + CsvDelay: uint16(csvTimeoutBob), + MultiSigKey: bobKeyPub, + RevocationBasePoint: bobKeyPub, + PaymentBasePoint: bobKeyPub, + DelayBasePoint: bobKeyPub, + } + + bobRoot := lnwallet.DeriveRevocationRoot(bobKeyPriv, testHdSeed, aliceKeyPub) + bobPreimageProducer := shachain.NewRevocationProducer(bobRoot) + bobFirstRevoke, err := bobPreimageProducer.AtIndex(0) + if err != nil { + return nil, nil, nil, nil, err + } + bobCommitPoint := lnwallet.ComputeCommitmentPoint(bobFirstRevoke[:]) + + aliceRoot := lnwallet.DeriveRevocationRoot(aliceKeyPriv, testHdSeed, bobKeyPub) + alicePreimageProducer := shachain.NewRevocationProducer(aliceRoot) + aliceFirstRevoke, err := alicePreimageProducer.AtIndex(0) + if err != nil { + return nil, nil, nil, nil, err + } + aliceCommitPoint := lnwallet.ComputeCommitmentPoint(aliceFirstRevoke[:]) + + aliceCommitTx, bobCommitTx, err := lnwallet.CreateCommitmentTxns(channelBal, + channelBal, &aliceCfg, &bobCfg, aliceCommitPoint, bobCommitPoint, + fundingTxIn) + if err != nil { + return nil, nil, nil, nil, err + } + + alicePath, err := ioutil.TempDir("", "alicedb") + dbAlice, err := channeldb.Open(alicePath) + if err != nil { + return nil, nil, nil, nil, err + } + + bobPath, err := ioutil.TempDir("", "bobdb") + dbBob, err := channeldb.Open(bobPath) + if err != nil { + return nil, nil, nil, nil, err + } + + var obsfucator [lnwallet.StateHintSize]byte + copy(obsfucator[:], aliceFirstRevoke[:]) + + estimator := &lnwallet.StaticFeeEstimator{FeeRate: 50} + feePerKw := btcutil.Amount(estimator.EstimateFeePerWeight(1) * 1000) + aliceChannelState := &channeldb.OpenChannel{ + LocalChanCfg: aliceCfg, + RemoteChanCfg: bobCfg, + IdentityPub: aliceKeyPub, + FundingOutpoint: *prevOut, + ChanType: channeldb.SingleFunder, + FeePerKw: feePerKw, + IsInitiator: true, + Capacity: channelCapacity, + LocalBalance: channelBal, + RemoteBalance: channelBal, + CommitTx: *aliceCommitTx, + CommitSig: bytes.Repeat([]byte{1}, 71), + RemoteCurrentRevocation: bobCommitPoint, + RevocationProducer: alicePreimageProducer, + RevocationStore: shachain.NewRevocationStore(), + Db: dbAlice, + } + + addr := &net.TCPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: 18555, + } + + if err := aliceChannelState.SyncPending(addr, 0); err != nil { + return nil, nil, nil, nil, err + } + + bobChannelState := &channeldb.OpenChannel{ + LocalChanCfg: bobCfg, + RemoteChanCfg: aliceCfg, + IdentityPub: bobKeyPub, + FeePerKw: feePerKw, + FundingOutpoint: *prevOut, + ChanType: channeldb.SingleFunder, + IsInitiator: false, + Capacity: channelCapacity, + LocalBalance: channelBal, + RemoteBalance: channelBal, + CommitTx: *bobCommitTx, + CommitSig: bytes.Repeat([]byte{1}, 71), + RemoteCurrentRevocation: aliceCommitPoint, + RevocationProducer: bobPreimageProducer, + RevocationStore: shachain.NewRevocationStore(), + Db: dbBob, + } + + addr = &net.TCPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: 18556, + } + + if err := bobChannelState.SyncPending(addr, 0); err != nil { + return nil, nil, nil, nil, err + } + + cleanUpFunc := func() { + os.RemoveAll(bobPath) + os.RemoveAll(alicePath) + } + + aliceSigner := &mockSigner{aliceKeyPriv} + bobSigner := &mockSigner{bobKeyPriv} + + channelAlice, err := lnwallet.NewLightningChannel(aliceSigner, notifier, + estimator, aliceChannelState) + if err != nil { + return nil, nil, nil, nil, err + } + channelBob, err := lnwallet.NewLightningChannel(bobSigner, notifier, + estimator, bobChannelState) + if err != nil { + return nil, nil, nil, nil, err + } + + chainIO := &mockChainIO{} + wallet := &lnwallet.LightningWallet{ + WalletController: &mockWalletController{ + rootKey: aliceKeyPriv, + publishedTransactions: publTx, + }, + } + cc := &chainControl{ + feeEstimator: estimator, + chainIO: chainIO, + chainNotifier: notifier, + wallet: wallet, + } + + breachArbiter := &breachArbiter{ + settledContracts: make(chan *wire.OutPoint, 10), + } + + s := &server{ + chanDB: dbAlice, + cc: cc, + breachArbiter: breachArbiter, + } + s.htlcSwitch = htlcswitch.New(htlcswitch.Config{}) + s.htlcSwitch.Start() + + alicePeer := &peer{ + server: s, + sendQueue: make(chan outgoinMsg, 1), + outgoingQueue: make(chan outgoinMsg, outgoingQueueLen), + + activeChannels: make(map[lnwire.ChannelID]*lnwallet.LightningChannel), + newChannels: make(chan *newChannelMsg, 1), + + localCloseChanReqs: make(chan *htlcswitch.ChanClose), + shutdownChanReqs: make(chan *lnwire.Shutdown), + closingSignedChanReqs: make(chan *lnwire.ClosingSigned), + + localSharedFeatures: nil, + globalSharedFeatures: nil, + + queueQuit: make(chan struct{}), + quit: make(chan struct{}), + } + + chanID := lnwire.NewChanIDFromOutPoint(channelAlice.ChannelPoint()) + alicePeer.activeChannels[chanID] = channelAlice + + go alicePeer.channelManager() + + return alicePeer, channelAlice, channelBob, cleanUpFunc, nil +}