multi: add new config option `upfront-shutdown-address`
What changed, and why it matters
This commit adds a new optional configuration setting, upfront-shutdown-address, to the Lightning Network Daemon (LND). When set, it tells LND to send cooperative channel-closing funds to a specific address by default. It only takes effect if the other peer also supports the upfront shutdown feature; otherwise channel opens fail. The change is a feature addition, not a fix for an active vulnerability, and it does not appear to introduce a security flaw on its own.
No immediate security action required. Operators who enable upfront-shutdown-address should verify the address is valid and under their control, and be aware that channel opens to non-supporting peers will fail. Reviewers may want to confirm that ParseUpfrontShutdownAddress rejects invalid or non-standard scripts and that the global default cannot accidentally override an explicit per-channel address.
Security signals we found
New configuration option for cooperative close payout address
Default shutdown script only applied when peer advertises upfront-shutdown feature support
Per-channel and channel-acceptor values override the global default
Address parsing uses existing chancloser.ParseUpfrontShutdownAddress routine
No change to cryptographic, network, or access-control logic
Evidence from the diff
The patch introduces a global UpfrontShutdownAddr config option parsed at startup via chancloser.ParseUpfrontShutdownAddress and propagated into the funding manager as ShutdownScript. For both funders (handleInitFundingMsg) and fundees (fundeeProcessOpenChannel), if no per-channel shutdown script is provided, the configured default is used. The existing getUpfrontShutdownScript logic then either uses the script or, when EnableUpfrontShutdown is true and no script is set, selects a wallet address. The commit also renames peer.chooseAddr to peer.ChooseAddr so it can be reused from server.go. There is no evidence in the diff of a vulnerability being fixed or introduced; it is a user-facing feature that makes upfront shutdown addresses easier to configure.
Changed components
config.gofunding/manager.gopeer/brontide.gosample-lnd.confserver.goInspect captured patch +62 / −6
diff --git a/config.go b/config.go
index 6d5bc54..e9ce410 100644
--- a/config.go
+++ b/config.go
@@ -537,6 +537,15 @@ type Config struct {
// NoDisconnectOnPongFailure controls if we'll disconnect if a peer
// doesn't respond to a pong in time.
NoDisconnectOnPongFailure bool `long:"no-disconnect-on-pong-failure" description:"If true, a peer will *not* be disconnected if a pong is not received in time or is mismatched. Defaults to false, meaning peers *will* be disconnected on pong failure."`
+
+ // UpfrontShutdownAddr specifies an address that our funds will be paid
+ // out to on cooperative channel close. This applies to all new channel
+ // opens unless overridden by an option in openchannel or by a channel
+ // acceptor.
+ // Note: If this field is set when opening a channel with a peer that
+ // does not advertise support for the upfront shutdown feature, the
+ // channel open will fail.
+ UpfrontShutdownAddr string `long:"upfront-shutdown-address" description:"The address to which funds will be paid out during a cooperative channel close. This applies to all channels opened after this option is set, unless overridden for a specific channel opening. Note: If this option is set, any channel opening will fail if the peer does not explicitly advertise support for the upfront-shutdown feature bit."`
}
// GRPCConfig holds the configuration options for the gRPC server.
diff --git a/funding/manager.go b/funding/manager.go
index 8176e6a..5711ed8 100644
--- a/funding/manager.go
+++ b/funding/manager.go
@@ -573,6 +573,10 @@ type Config struct {
// implementations to inject and process custom records over channel
// related wire messages.
AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator]
+
+ // ShutdownScript is an optional upfront-shutdown script to which our
+ // funds should be paid on a cooperative close.
+ ShutdownScript fn.Option[lnwire.DeliveryAddress]
}
// Manager acts as an orchestrator/bridge between the wallet's
@@ -1760,12 +1764,24 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
return
}
+ // If the fundee didn't provide an upfront-shutdown address via
+ // the channel acceptor, fall back to the configured shutdown
+ // script (if any).
+ shutdownScript := acceptorResp.UpfrontShutdown
+ if len(shutdownScript) == 0 {
+ f.cfg.ShutdownScript.WhenSome(
+ func(script lnwire.DeliveryAddress) {
+ shutdownScript = script
+ },
+ )
+ }
+
// Check whether the peer supports upfront shutdown, and get a new
// wallet address if our node is configured to set shutdown addresses by
// default. We use the upfront shutdown script provided by our channel
// acceptor (if any) in lieu of user input.
shutdown, err := getUpfrontShutdownScript(
- f.cfg.EnableUpfrontShutdown, peer, acceptorResp.UpfrontShutdown,
+ f.cfg.EnableUpfrontShutdown, peer, shutdownScript,
f.selectShutdownScript,
)
if err != nil {
@@ -4849,12 +4865,23 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
}
}
+ // If the funder did not provide an upfront-shutdown address, fall back
+ // to the configured shutdown script (if any).
+ shutdownScript := msg.ShutdownScript
+ if len(shutdownScript) == 0 {
+ f.cfg.ShutdownScript.WhenSome(
+ func(script lnwire.DeliveryAddress) {
+ shutdownScript = script
+ },
+ )
+ }
+
// 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(
- f.cfg.EnableUpfrontShutdown, msg.Peer, msg.ShutdownScript,
+ f.cfg.EnableUpfrontShutdown, msg.Peer, shutdownScript,
f.selectShutdownScript,
)
if err != nil {
diff --git a/peer/brontide.go b/peer/brontide.go
index 4a196fb..8d02ca6 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -3589,9 +3589,9 @@ func (p *Brontide) initNegotiateChanCloser(req *htlcswitch.ChanClose,
return nil
}
-// chooseAddr returns the provided address if it is non-zero length, otherwise
+// ChooseAddr returns the provided address if it is non-zero length, otherwise
// None.
-func chooseAddr(addr lnwire.DeliveryAddress) fn.Option[lnwire.DeliveryAddress] {
+func ChooseAddr(addr lnwire.DeliveryAddress) fn.Option[lnwire.DeliveryAddress] {
if len(addr) == 0 {
return fn.None[lnwire.DeliveryAddress]()
}
@@ -3930,10 +3930,10 @@ func (p *Brontide) initRbfChanCloser(
ChanType: channel.ChanType(),
DefaultFeeRate: defaultFeePerKw.FeePerVByte(),
ThawHeight: fn.Some(thawHeight),
- RemoteUpfrontShutdown: chooseAddr(
+ RemoteUpfrontShutdown: ChooseAddr(
channel.RemoteUpfrontShutdownScript(),
),
- LocalUpfrontShutdown: chooseAddr(
+ LocalUpfrontShutdown: ChooseAddr(
channel.LocalUpfrontShutdownScript(),
),
NewDeliveryScript: func() (lnwire.DeliveryAddress, error) {
diff --git a/sample-lnd.conf b/sample-lnd.conf
index c3b3a96..ed5dabd 100644
--- a/sample-lnd.conf
+++ b/sample-lnd.conf
@@ -589,6 +589,15 @@
; pong failure.
; no-disconnect-on-pong-failure=false
+; The address to which funds will be paid out during a cooperative channel
+; close. This applies to all channels opened after this option is set, unless
+; overridden for a specific channel opening.
+;
+; Note: If this option is set, any channel opening will fail if the peer does
+; not explicitly advertise support for the upfront-shutdown feature bit.
+; upfront-shutdown-address=
+
+
[fee]
; Optional URL for external fee estimation. If no URL is specified, the method
diff --git a/server.go b/server.go
index d0289de..3c011f8 100644
--- a/server.go
+++ b/server.go
@@ -61,6 +61,7 @@ import (
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
+ "github.com/lightningnetwork/lnd/lnwallet/chancloser"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
"github.com/lightningnetwork/lnd/lnwire"
@@ -1445,6 +1446,15 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
devCfg, reservationTimeout, zombieSweeperInterval)
}
+ // Attempt to parse the provided upfront-shutdown address (if any).
+ script, err := chancloser.ParseUpfrontShutdownAddress(
+ cfg.UpfrontShutdownAddr, cfg.ActiveNetParams.Params,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("error parsing upfront shutdown: %w",
+ err)
+ }
+
//nolint:ll
s.fundingMgr, err = funding.NewFundingManager(funding.Config{
Dev: devCfg,
@@ -1623,6 +1633,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
AuxSigner: implCfg.AuxSigner,
AuxResolver: implCfg.AuxContractResolver,
AuxChannelNegotiator: implCfg.AuxChannelNegotiator,
+ ShutdownScript: peer.ChooseAddr(script),
})
if err != nil {
return nil, err
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.