multi: Set upfront shutdown from open and accept chanel messages

This commit gets upfront shutdown scripts from openchannel and
acceptchannel wire messages sent from our peer and sets upfront
shutdown scripts in our open and accept channel messages when
the remote peer supports option upfront shutdown and we have
the feature enabled.
This commit is contained in:
carla 2019-12-03 11:38:29 +02:00
parent 77222d8b69
commit 9b35c349de
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
6 changed files with 256 additions and 35 deletions

View File

@ -323,6 +323,8 @@ type config struct {
net tor.Net
EnableUpfrontShutdown bool `long:"enable-upfront-shutdown" description:"If true, option upfront shutdown script will be enabled. If peers that we open channels with support this feature, we will automatically set the script to which cooperative closes should be paid out to on channel open. This offers the partial protection of a channel peer diconnecting from us if cooperative close is attempted with a different script."`
Routing *routing.Conf `group:"routing" namespace:"routing"`
Workers *lncfg.Workers `group:"workers" namespace:"workers"`

View File

@ -9,6 +9,7 @@ import (
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/coreos/bbolt"
@ -93,6 +94,11 @@ var (
// blocks pass without confirmation.
ErrConfirmationTimeout = errors.New("timeout waiting for funding " +
"confirmation")
// errUpfrontShutdownScriptNotSupported is returned if an upfront shutdown
// script is set for a peer that does not support the feature bit.
errUpfrontShutdownScriptNotSupported = errors.New("peer does not support" +
"option upfront shutdown script")
)
// reservationWithCtx encapsulates a pending channel reservation. This wrapper
@ -1261,10 +1267,33 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
return
}
// Check whether the peer supports upfront shutdown, and get a new wallet
// address if our node is configured to set shutdown addresses by default.
// A nil address is set in place of user input, because this channel open
// was not initiated by the user.
shutdown, err := getUpfrontShutdownScript(
fmsg.peer, nil,
func() (lnwire.DeliveryAddress, error) {
addr, err := f.cfg.Wallet.NewAddress(lnwallet.WitnessPubKey, false)
if err != nil {
return nil, err
}
return txscript.PayToAddrScript(addr)
},
)
if err != nil {
f.failFundingFlow(
fmsg.peer, fmsg.msg.PendingChannelID,
fmt.Errorf("getUpfrontShutdownScript error: %v", err),
)
return
}
reservation.SetOurUpfrontShutdown(shutdown)
fndgLog.Infof("Requiring %v confirmations for pendingChan(%x): "+
"amt=%v, push_amt=%v, tweakless=%v", numConfsReq,
"amt=%v, push_amt=%v, tweakless=%v, upfrontShutdown=%x", numConfsReq,
fmsg.msg.PendingChannelID, amt, msg.PushAmount,
tweaklessCommitment)
tweaklessCommitment, msg.UpfrontShutdownScript)
// Generate our required constraints for the remote party.
remoteCsvDelay := f.cfg.RequiredRemoteDelay(amt)
@ -1324,6 +1353,7 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
PubKey: copyPubKey(msg.HtlcPoint),
},
},
UpfrontShutdown: msg.UpfrontShutdownScript,
}
err = reservation.ProcessSingleContribution(remoteContribution)
if err != nil {
@ -1341,20 +1371,21 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
// contribution in the next message of the workflow.
ourContribution := reservation.OurContribution()
fundingAccept := lnwire.AcceptChannel{
PendingChannelID: msg.PendingChannelID,
DustLimit: ourContribution.DustLimit,
MaxValueInFlight: maxValue,
ChannelReserve: chanReserve,
MinAcceptDepth: uint32(numConfsReq),
HtlcMinimum: minHtlc,
CsvDelay: remoteCsvDelay,
MaxAcceptedHTLCs: maxHtlcs,
FundingKey: ourContribution.MultiSigKey.PubKey,
RevocationPoint: ourContribution.RevocationBasePoint.PubKey,
PaymentPoint: ourContribution.PaymentBasePoint.PubKey,
DelayedPaymentPoint: ourContribution.DelayBasePoint.PubKey,
HtlcPoint: ourContribution.HtlcBasePoint.PubKey,
FirstCommitmentPoint: ourContribution.FirstCommitmentPoint,
PendingChannelID: msg.PendingChannelID,
DustLimit: ourContribution.DustLimit,
MaxValueInFlight: maxValue,
ChannelReserve: chanReserve,
MinAcceptDepth: uint32(numConfsReq),
HtlcMinimum: minHtlc,
CsvDelay: remoteCsvDelay,
MaxAcceptedHTLCs: maxHtlcs,
FundingKey: ourContribution.MultiSigKey.PubKey,
RevocationPoint: ourContribution.RevocationBasePoint.PubKey,
PaymentPoint: ourContribution.PaymentBasePoint.PubKey,
DelayedPaymentPoint: ourContribution.DelayBasePoint.PubKey,
HtlcPoint: ourContribution.HtlcBasePoint.PubKey,
FirstCommitmentPoint: ourContribution.FirstCommitmentPoint,
UpfrontShutdownScript: ourContribution.UpfrontShutdown,
}
if err := fmsg.peer.SendMessage(true, &fundingAccept); err != nil {
fndgLog.Errorf("unable to send funding response to peer: %v", err)
@ -1465,6 +1496,7 @@ func (f *fundingManager) handleFundingAccept(fmsg *fundingAcceptMsg) {
PubKey: copyPubKey(msg.HtlcPoint),
},
},
UpfrontShutdown: msg.UpfrontShutdownScript,
}
err = resCtx.reservation.ProcessContribution(remoteContribution)
if err != nil {
@ -2738,6 +2770,49 @@ func (f *fundingManager) initFundingWorkflow(peer lnpeer.Peer, req *openChanReq)
}
}
// getUpfrontShutdownScript takes a user provided script and a getScript
// function which can be used to generate an upfront shutdown script. If our
// peer does not support the feature, this function will error if a non-zero
// script was provided by the user, and return an empty script otherwise. If
// our peer does support the feature, we will return the user provided script
// if non-zero, or a freshly generated script if our node is configured to set
// upfront shutdown scripts automatically.
func getUpfrontShutdownScript(peer lnpeer.Peer, script lnwire.DeliveryAddress,
getScript func() (lnwire.DeliveryAddress, error)) (lnwire.DeliveryAddress,
error) {
// Check whether the remote peer supports upfront shutdown scripts.
remoteUpfrontShutdown := peer.RemoteFeatures().HasFeature(
lnwire.UpfrontShutdownScriptOptional,
)
// If the peer does not support upfront shutdown scripts, and one has been
// provided, return an error because the feature is not supported.
if !remoteUpfrontShutdown && len(script) != 0 {
return nil, errUpfrontShutdownScriptNotSupported
}
// If the peer does not support upfront shutdown, return an empty address.
if !remoteUpfrontShutdown {
return nil, nil
}
// If the user has provided an script and the peer supports the feature,
// return it. Note that user set scripts override the enable upfront
// shutdown flag.
if len(script) > 0 {
return script, nil
}
// If we do not have setting of upfront shutdown script enabled, return
// an empty script.
if !cfg.EnableUpfrontShutdown {
return nil, nil
}
return getScript()
}
// handleInitFundingMsg creates a channel reservation within the daemon's
// wallet, then sends a funding request to the remote peer kicking off the
// funding workflow.
@ -2823,6 +2898,27 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
return
}
// Check whether the peer supports upfront shutdown, and get an address which
// should be used (either a user specified address or a new address from the
// wallet if our node is configured to set shutdown address by default).
shutdown, err := getUpfrontShutdownScript(
msg.peer, msg.openChanReq.shutdownScript,
func() (lnwire.DeliveryAddress, error) {
addr, err := f.cfg.Wallet.NewAddress(lnwallet.WitnessPubKey, false)
if err != nil {
return nil, err
}
return txscript.PayToAddrScript(addr)
},
)
if err != nil {
msg.err <- err
return
}
// Set our upfront shutdown address in the existing reservation.
reservation.SetOurUpfrontShutdown(shutdown)
// Now that we have successfully reserved funds for this channel in the
// wallet, we can fetch the final channel capacity. This is done at
// this point since the final capacity might change in case of
@ -2883,24 +2979,25 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
"tweakless=%v", msg.peer.Address(), chanID, tweaklessCommitment)
fundingOpen := lnwire.OpenChannel{
ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash,
PendingChannelID: chanID,
FundingAmount: capacity,
PushAmount: msg.pushAmt,
DustLimit: ourContribution.DustLimit,
MaxValueInFlight: maxValue,
ChannelReserve: chanReserve,
HtlcMinimum: minHtlc,
FeePerKiloWeight: uint32(commitFeePerKw),
CsvDelay: remoteCsvDelay,
MaxAcceptedHTLCs: maxHtlcs,
FundingKey: ourContribution.MultiSigKey.PubKey,
RevocationPoint: ourContribution.RevocationBasePoint.PubKey,
PaymentPoint: ourContribution.PaymentBasePoint.PubKey,
HtlcPoint: ourContribution.HtlcBasePoint.PubKey,
DelayedPaymentPoint: ourContribution.DelayBasePoint.PubKey,
FirstCommitmentPoint: ourContribution.FirstCommitmentPoint,
ChannelFlags: channelFlags,
ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash,
PendingChannelID: chanID,
FundingAmount: capacity,
PushAmount: msg.pushAmt,
DustLimit: ourContribution.DustLimit,
MaxValueInFlight: maxValue,
ChannelReserve: chanReserve,
HtlcMinimum: minHtlc,
FeePerKiloWeight: uint32(commitFeePerKw),
CsvDelay: remoteCsvDelay,
MaxAcceptedHTLCs: maxHtlcs,
FundingKey: ourContribution.MultiSigKey.PubKey,
RevocationPoint: ourContribution.RevocationBasePoint.PubKey,
PaymentPoint: ourContribution.PaymentBasePoint.PubKey,
HtlcPoint: ourContribution.HtlcBasePoint.PubKey,
DelayedPaymentPoint: ourContribution.DelayBasePoint.PubKey,
FirstCommitmentPoint: ourContribution.FirstCommitmentPoint,
ChannelFlags: channelFlags,
UpfrontShutdownScript: shutdown,
}
if err := msg.peer.SendMessage(true, &fundingOpen); err != nil {
e := fmt.Errorf("Unable to send funding request message: %v",

View File

@ -149,6 +149,7 @@ type testNode struct {
mockNotifier *mockNotifier
testDir string
shutdownChannel chan struct{}
remoteFeatures []lnwire.FeatureBit
remotePeer *testNode
sendMessage func(lnwire.Message) error
@ -189,7 +190,9 @@ func (n *testNode) LocalFeatures() *lnwire.FeatureVector {
}
func (n *testNode) RemoteFeatures() *lnwire.FeatureVector {
return lnwire.NewFeatureVector(nil, nil)
return lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(n.remoteFeatures...), nil,
)
}
func (n *testNode) AddNewChannel(channel *channeldb.OpenChannel,
@ -2948,3 +2951,92 @@ func TestFundingManagerFundAll(t *testing.T) {
}
}
}
// TestGetUpfrontShutdown tests different combinations of inputs for getting a
// shutdown script. It varies whether the peer has the feature set, whether
// the user has provided a script and our local configuration to test that
// GetUpfrontShutdownScript returns the expected outcome.
func TestGetUpfrontShutdownScript(t *testing.T) {
upfrontScript := []byte("upfront script")
generatedScript := []byte("generated script")
getScript := func() (lnwire.DeliveryAddress, error) {
return generatedScript, nil
}
tests := []struct {
name string
getScript func() (lnwire.DeliveryAddress, error)
upfrontScript lnwire.DeliveryAddress
peerEnabled bool
localEnabled bool
expectedScript lnwire.DeliveryAddress
expectedErr error
}{
{
name: "peer disabled, no shutdown",
getScript: getScript,
},
{
name: "peer disabled, upfront provided",
upfrontScript: upfrontScript,
expectedErr: errUpfrontShutdownScriptNotSupported,
},
{
name: "peer enabled, upfront provided",
upfrontScript: upfrontScript,
peerEnabled: true,
expectedScript: upfrontScript,
},
{
name: "peer enabled, local disabled",
peerEnabled: true,
},
{
name: "local enabled, no upfront script",
getScript: getScript,
peerEnabled: true,
localEnabled: true,
expectedScript: generatedScript,
},
{
name: "local enabled, upfront script",
peerEnabled: true,
upfrontScript: upfrontScript,
localEnabled: true,
expectedScript: upfrontScript,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
var mockPeer testNode
// If the remote peer in the test should support upfront shutdown,
// add the feature bit.
if test.peerEnabled {
mockPeer.remoteFeatures = []lnwire.FeatureBit{
lnwire.UpfrontShutdownScriptOptional,
}
}
// Set the command line option in config as needed.
cfg = &config{EnableUpfrontShutdown: test.localEnabled}
addr, err := getUpfrontShutdownScript(
&mockPeer, test.upfrontScript, test.getScript,
)
if err != test.expectedErr {
t.Fatalf("got: %v, expected error: %v", err, test.expectedErr)
}
if !bytes.Equal(addr, test.expectedScript) {
t.Fatalf("expected address: %x, got: %x",
test.expectedScript, addr)
}
})
}
}

View File

@ -44,6 +44,10 @@ type ChannelContribution struct {
// such as the min HTLC, and also all the keys which will be used for
// the duration of the channel.
*channeldb.ChannelConfig
// UpfrontShutdown is an optional address to which the channel should be
// paid out to on cooperative close.
UpfrontShutdown lnwire.DeliveryAddress
}
// toChanConfig returns the raw channel configuration generated by a node's
@ -531,6 +535,14 @@ func (r *ChannelReservation) FundingOutpoint() *wire.OutPoint {
return &r.partialState.FundingOutpoint
}
// SetOurUpfrontShutdown sets the upfront shutdown address on our contribution.
func (r *ChannelReservation) SetOurUpfrontShutdown(shutdown lnwire.DeliveryAddress) {
r.Lock()
defer r.Unlock()
r.ourContribution.UpfrontShutdown = shutdown
}
// Capacity returns the channel capacity for this reservation.
func (r *ChannelReservation) Capacity() btcutil.Amount {
r.RLock()

View File

@ -1217,6 +1217,13 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
// rebroadcast on startup in case we fail.
res.partialState.FundingTxn = fundingTx
// Set optional upfront shutdown scripts on the channel state so that they
// are persisted. These values may be nil.
res.partialState.LocalShutdownScript =
res.ourContribution.UpfrontShutdown
res.partialState.RemoteShutdownScript =
res.theirContribution.UpfrontShutdown
// Add the complete funding transaction to the DB, in its open bucket
// which will be used for the lifetime of this channel.
nodeAddr := res.nodeAddr
@ -1376,6 +1383,13 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
return
}
// Set optional upfront shutdown scripts on the channel state so that they
// are persisted. These values may be nil.
chanState.LocalShutdownScript =
pendingReservation.ourContribution.UpfrontShutdown
chanState.RemoteShutdownScript =
pendingReservation.theirContribution.UpfrontShutdown
// Add the complete funding transaction to the DB, in it's open bucket
// which will be used for the lifetime of this channel.
chanState.LocalChanCfg = pendingReservation.ourContribution.toChanConfig()

View File

@ -3088,6 +3088,10 @@ type openChanReq struct {
// output selected to fund the channel should satisfy.
minConfs int32
// shutdownScript is an optional upfront shutdown script for the channel.
// This value is optional, so may be nil.
shutdownScript lnwire.DeliveryAddress
// TODO(roasbeef): add ability to specify channel constraints as well
updates chan *lnrpc.OpenStatusUpdate