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.
This commit is contained in:
Johan T. Halseth 2017-07-14 21:05:55 +02:00
parent b9516b7cdd
commit e8e87322dd
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
5 changed files with 1286 additions and 344 deletions

View File

@ -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{})

182
mock.go Normal file
View File

@ -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
}

511
peer.go
View File

@ -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

View File

@ -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")
}
}

268
test_utils.go Normal file
View File

@ -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
}