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/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) } 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 +}