Merge pull request #225 from halseth/channel-close-negotiation2
Add fee negotiation on channel cooperative shutdown.
This commit is contained in:
commit
06782ebdb3
@ -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{})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
182
mock.go
Normal file
182
mock.go
Normal 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
511
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
|
||||
|
520
peer_test.go
520
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")
|
||||
}
|
||||
}
|
||||
|
268
test_utils.go
Normal file
268
test_utils.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user