diff --git a/config.go b/config.go index 6d5bc546a..65454e6b7 100644 --- a/config.go +++ b/config.go @@ -259,6 +259,58 @@ const ( defaultNoDisconnectOnPongFailure = false ) +// validateMaxOutgoingCltvExpiry validates the configured maximum outgoing CLTV +// expiry against the node's default time lock delta. +func validateMaxOutgoingCltvExpiry(maxCltvExpiry, timeLockDelta uint32) error { + if maxCltvExpiry < timeLockDelta { + return fmt.Errorf( + "max-cltv-expiry must be at least %v", timeLockDelta, + ) + } + + if maxCltvExpiry > MaxTimeLockDelta { + return fmt.Errorf( + "max-cltv-expiry must be at most %v", MaxTimeLockDelta, + ) + } + + return nil +} + +// validateCltvDeltaBounds validates a CLTV delta against LND's absolute +// supported bounds. +func validateCltvDeltaBounds(delta uint32) error { + if delta < minTimeLockDelta { + return fmt.Errorf("time lock delta of %v is too small, "+ + "minimum supported is %v", delta, minTimeLockDelta) + } + + if delta > MaxTimeLockDelta { + return fmt.Errorf("time lock delta of %v is too big, "+ + "maximum supported is %v", delta, MaxTimeLockDelta) + } + + return nil +} + +// validateChannelPolicyTimeLockDelta validates an advertised channel policy +// time lock delta against the node's supported forwarding bounds. +func validateChannelPolicyTimeLockDelta(timeLockDelta, + maxOutgoingCltvExpiry uint32) error { + + if err := validateCltvDeltaBounds(timeLockDelta); err != nil { + return err + } + + if timeLockDelta > maxOutgoingCltvExpiry { + return fmt.Errorf("time lock delta of %v exceeds "+ + "max-cltv-expiry of %v", timeLockDelta, + maxOutgoingCltvExpiry) + } + + return nil +} + var ( // DefaultLndDir is the default directory where lnd tries to find its // configuration file and store its data. This is a directory in the @@ -1113,6 +1165,12 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, cfg.MaxCommitFeeRateAnchors) } + if err := validateMaxOutgoingCltvExpiry( + cfg.MaxOutgoingCltvExpiry, cfg.Bitcoin.TimeLockDelta, + ); err != nil { + return nil, mkErr("%v", err) + } + // Validate the Tor config parameters. socks, err := lncfg.ParseAddressString( cfg.Tor.SOCKS, strconv.Itoa(defaultTorSOCKSPort), diff --git a/config_test.go b/config_test.go index 765580749..2136068b5 100644 --- a/config_test.go +++ b/config_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/routing" "github.com/stretchr/testify/require" ) @@ -117,3 +118,63 @@ func TestSupplyEnvValue(t *testing.T) { }) } } + +// TestValidateMaxOutgoingCltvExpiry asserts that max-cltv-expiry accepts +// values within its supported bounds and rejects values outside them. +func TestValidateMaxOutgoingCltvExpiry(t *testing.T) { + t.Parallel() + + cfg := DefaultConfig() + + require.NoError( + t, validateMaxOutgoingCltvExpiry( + htlcswitch.DefaultMaxOutgoingCltvExpiry, + cfg.Bitcoin.TimeLockDelta, + ), + ) + require.NoError(t, validateMaxOutgoingCltvExpiry( + MaxTimeLockDelta, MaxTimeLockDelta, + )) + + err := validateMaxOutgoingCltvExpiry( + cfg.Bitcoin.TimeLockDelta-1, + cfg.Bitcoin.TimeLockDelta, + ) + require.ErrorContains(t, err, "max-cltv-expiry must be at least") + + err = validateMaxOutgoingCltvExpiry( + MaxTimeLockDelta+1, cfg.Bitcoin.TimeLockDelta, + ) + require.ErrorContains(t, err, "max-cltv-expiry must be at most") +} + +// TestValidateChannelPolicyTimeLockDelta asserts that advertised channel +// policy CLTV deltas stay within the node's supported forwarding bounds. +func TestValidateChannelPolicyTimeLockDelta(t *testing.T) { + t.Parallel() + + cfg := DefaultConfig() + + require.NoError(t, validateChannelPolicyTimeLockDelta( + cfg.Bitcoin.TimeLockDelta, cfg.MaxOutgoingCltvExpiry, + )) + require.NoError(t, validateChannelPolicyTimeLockDelta( + cfg.MaxOutgoingCltvExpiry, cfg.MaxOutgoingCltvExpiry, + )) + + err := validateChannelPolicyTimeLockDelta( + minTimeLockDelta-1, cfg.MaxOutgoingCltvExpiry, + ) + require.ErrorContains(t, err, "time lock delta of") + require.ErrorContains(t, err, "is too small") + + err = validateChannelPolicyTimeLockDelta( + MaxTimeLockDelta+1, MaxTimeLockDelta, + ) + require.ErrorContains(t, err, "is too big") + + err = validateChannelPolicyTimeLockDelta( + cfg.MaxOutgoingCltvExpiry+1, cfg.MaxOutgoingCltvExpiry, + ) + require.ErrorContains(t, err, "exceeds max-cltv-expiry") +} diff --git a/rpcserver.go b/rpcserver.go index 64eb40fb8..886b61813 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -8078,14 +8078,10 @@ func (r *rpcServer) UpdateChannelPolicy(ctx context.Context, // We'll also ensure that the user isn't setting a CLTV delta that // won't give outgoing HTLCs enough time to fully resolve if needed. - if req.TimeLockDelta < minTimeLockDelta { - return nil, fmt.Errorf("time lock delta of %v is too small, "+ - "minimum supported is %v", req.TimeLockDelta, - minTimeLockDelta) - } else if req.TimeLockDelta > uint32(MaxTimeLockDelta) { - return nil, fmt.Errorf("time lock delta of %v is too big, "+ - "maximum supported is %v", req.TimeLockDelta, - MaxTimeLockDelta) + if err := validateChannelPolicyTimeLockDelta( + req.TimeLockDelta, r.cfg.MaxOutgoingCltvExpiry, + ); err != nil { + return nil, err } // By default, positive inbound fees are rejected.