From a7c90287970e340faa9539c4e28521e3b03cf604 Mon Sep 17 00:00:00 2001 From: Tee8z Date: Mon, 1 Jun 2026 18:44:42 -0400 Subject: [PATCH 1/2] feat(config): support custom signet block time --- config.go | 83 +++++++++++++++++ config_test.go | 243 ++++++++++++++++++++++++++++++++++++++++++++++++ lncfg/chain.go | 18 ++-- sample-lnd.conf | 12 ++- 4 files changed, 347 insertions(+), 9 deletions(-) diff --git a/config.go b/config.go index bffebc36973..ffb33eb140b 100644 --- a/config.go +++ b/config.go @@ -1316,6 +1316,12 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, numNets++ cfg.ActiveNetParams = chainreg.BitcoinSigNetParams + err := validateSigNetBackendOptions(cfg.Bitcoin) + if err != nil { + return nil, mkErr("error validating bitcoin "+ + "params: %v", err) + } + // Let the user overwrite the default signet parameters. // The challenge defines the actual signet network to // join and the seed nodes are needed for network @@ -1349,6 +1355,20 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, chainParams := chaincfg.CustomSignetParams( sigNetChallenge, sigNetSeeds, ) + if cfg.Bitcoin.SigNetBlockTime != 0 { + if cfg.Bitcoin.SigNetChallenge == "" { + return nil, mkErr("signet block time " + + "requires custom signet challenge") + } + + err := applySigNetBlockTime( + &chainParams, cfg.Bitcoin.SigNetBlockTime, + ) + if err != nil { + return nil, mkErr("invalid signet block "+ + "time: %v", err) + } + } cfg.ActiveNetParams.Params = &chainParams } if numNets > 1 { @@ -2501,6 +2521,69 @@ func configToFlatMap(cfg Config) (map[string]string, return result, deprecated, nil } +// validateSigNetBackendOptions validates custom signet options against the +// selected chain backend. +func validateSigNetBackendOptions(cfg *lncfg.Chain) error { + if cfg == nil { + return fmt.Errorf("bitcoin config cannot be nil") + } + + if !cfg.SigNet { + return nil + } + + switch cfg.Node { + case bitcoindBackendName: + switch { + case cfg.SigNetChallenge != "": + return fmt.Errorf("bitcoin.signetchallenge must not " + + "be set with bitcoin.node=bitcoind; " + + "configure custom signet consensus options " + + "on bitcoind instead") + + case cfg.SigNetBlockTime != 0: + return fmt.Errorf("bitcoin.signetblocktime must not " + + "be set with bitcoin.node=bitcoind; " + + "configure custom signet consensus options " + + "on bitcoind instead") + } + + case btcdBackendName: + if cfg.SigNetBlockTime != 0 { + return fmt.Errorf("bitcoin.signetblocktime is not " + + "supported with bitcoin.node=btcd; btcd does " + + "not currently support custom signet block " + + "intervals") + } + } + + return nil +} + +// applySigNetBlockTime updates the expected block interval used by custom +// signet header validation. This is needed for custom signets whose backing +// bitcoind nodes were started with -signetblocktime. +func applySigNetBlockTime(params *chaincfg.Params, + blockTime time.Duration) error { + + if params == nil { + return fmt.Errorf("params cannot be nil") + } + + if blockTime < time.Second { + return fmt.Errorf("must be at least one second") + } + + if blockTime > params.TargetTimespan { + return fmt.Errorf("must not exceed target timespan %v", + params.TargetTimespan) + } + + params.TargetTimePerBlock = blockTime + + return nil +} + // logWarningsForDeprecation logs a warning if a deprecated config option is // set. func logWarningsForDeprecation(cfg Config) { diff --git a/config_test.go b/config_test.go index 0233a27f185..4622b7acdac 100644 --- a/config_test.go +++ b/config_test.go @@ -3,17 +3,50 @@ package lnd import ( "fmt" "testing" + "time" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + flags "github.com/jessevdk/go-flags" + "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/routing" + "github.com/lightningnetwork/lnd/signal" "github.com/stretchr/testify/require" ) +// testSigNetChallengeHex is OP_TRUE encoded as Bitcoin Script hex. +const testSigNetChallengeHex = "51" + var ( testPassword = "testpassword" redactedPassword = "[redacted]" ) +// testSigNetChallenge is OP_TRUE encoded as Bitcoin Script bytes. +var testSigNetChallenge = []byte{txscript.OP_TRUE} + +// validateTestConfig runs ValidateConfig with isolated test logging and closes +// the log rotator that ValidateConfig starts on successful validation. +func validateTestConfig(t *testing.T, cfg Config) (*Config, error) { + t.Helper() + + cfg.SubLogMgr = build.NewSubLoggerManager() + + fileParser := flags.NewParser(&cfg, flags.Default) + flagParser := flags.NewParser(&cfg, flags.Default) + cleanCfg, err := ValidateConfig( + cfg, signal.Interceptor{}, fileParser, flagParser, + ) + if err != nil { + return cleanCfg, err + } + + require.NoError(t, cleanCfg.LogRotator.Close()) + + return cleanCfg, nil +} + // TestConfigToFlatMap tests that the configToFlatMap function works as // expected on the default configuration. func TestConfigToFlatMap(t *testing.T) { @@ -116,6 +149,216 @@ func TestSupplyEnvValue(t *testing.T) { } } +// TestApplySigNetBlockTime tests that custom signet block times update the +// target block interval used for header difficulty validation. +func TestApplySigNetBlockTime(t *testing.T) { + t.Parallel() + + t.Run("valid block time", func(t *testing.T) { + t.Parallel() + + params := chaincfg.CustomSignetParams( + chaincfg.DefaultSignetChallenge, + chaincfg.DefaultSignetDNSSeeds, + ) + require.NoError( + t, applySigNetBlockTime(¶ms, 30*time.Second), + ) + + require.Equal(t, 30*time.Second, params.TargetTimePerBlock) + require.Equal(t, 14*24*time.Hour, params.TargetTimespan) + require.Equal( + t, int64(40320), + int64(params.TargetTimespan/params.TargetTimePerBlock), + ) + require.False(t, params.ReduceMinDifficulty) + }) + + t.Run("nil params", func(t *testing.T) { + t.Parallel() + + err := applySigNetBlockTime(nil, 30*time.Second) + require.ErrorContains(t, err, "params cannot be nil") + }) + + t.Run("sub-second block time", func(t *testing.T) { + t.Parallel() + + params := chaincfg.CustomSignetParams( + chaincfg.DefaultSignetChallenge, + chaincfg.DefaultSignetDNSSeeds, + ) + err := applySigNetBlockTime(¶ms, time.Millisecond) + require.ErrorContains(t, err, "at least one second") + }) + + t.Run("block time exceeds target timespan", func(t *testing.T) { + t.Parallel() + + params := chaincfg.CustomSignetParams( + chaincfg.DefaultSignetChallenge, + chaincfg.DefaultSignetDNSSeeds, + ) + blockTime := params.TargetTimespan + time.Second + err := applySigNetBlockTime(¶ms, blockTime) + require.ErrorContains(t, err, "must not exceed target timespan") + }) +} + +// TestValidateConfigSigNetBlockTime tests that custom signet block times are +// only applied as an optional addition to a custom signet challenge. +func TestValidateConfigSigNetBlockTime(t *testing.T) { + tests := []struct { + name string + challenge string + blockTime time.Duration + expectError string + expectTime time.Duration + }{ + { + name: "default signet", + expectTime: chaincfg.SigNetParams.TargetTimePerBlock, + }, + { + name: "custom challenge only", + challenge: testSigNetChallengeHex, + expectTime: chaincfg.SigNetParams.TargetTimePerBlock, + }, + { + name: "custom challenge with block time", + challenge: testSigNetChallengeHex, + blockTime: 30 * time.Second, + expectTime: 30 * time.Second, + }, + { + name: "block time without custom challenge", + blockTime: 30 * time.Second, + expectError: "requires custom signet challenge", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := DefaultConfig() + cfg.LndDir = t.TempDir() + cfg.Bitcoin.Node = neutrinoBackendName + cfg.Bitcoin.SigNet = true + cfg.Bitcoin.SigNetChallenge = tc.challenge + cfg.Bitcoin.SigNetBlockTime = tc.blockTime + + cleanCfg, err := validateTestConfig(t, cfg) + + if tc.expectError != "" { + require.ErrorContains(t, err, tc.expectError) + return + } + + require.NoError(t, err) + require.Equal( + t, tc.expectTime, + cleanCfg.ActiveNetParams.Params. + TargetTimePerBlock, + ) + }) + } +} + +// TestValidateConfigSigNetBackendOptions tests that custom signet options are +// only accepted for backends that can use them. +func TestValidateConfigSigNetBackendOptions(t *testing.T) { + err := validateSigNetBackendOptions(nil) + require.ErrorContains(t, err, "bitcoin config cannot be nil") + + tests := []struct { + name string + node string + challenge string + blockTime time.Duration + expectError string + expectNet chaincfg.Params + }{ + { + name: "bitcoind default signet", + node: bitcoindBackendName, + expectNet: chaincfg.SigNetParams, + }, + { + name: "bitcoind custom challenge", + node: bitcoindBackendName, + challenge: testSigNetChallengeHex, + expectError: "bitcoin.signetchallenge must not be " + + "set with bitcoin.node=bitcoind", + }, + { + name: "bitcoind custom challenge and block time", + node: bitcoindBackendName, + challenge: testSigNetChallengeHex, + blockTime: 30 * time.Second, + expectError: "bitcoin.signetchallenge must not be " + + "set with bitcoin.node=bitcoind", + }, + { + name: "bitcoind custom block time", + node: bitcoindBackendName, + blockTime: 30 * time.Second, + expectError: "bitcoin.signetblocktime must not be " + + "set with bitcoin.node=bitcoind", + }, + { + name: "btcd custom challenge", + node: btcdBackendName, + challenge: testSigNetChallengeHex, + expectNet: chaincfg.CustomSignetParams( + testSigNetChallenge, + chaincfg.DefaultSignetDNSSeeds, + ), + }, + { + name: "btcd custom block time", + node: btcdBackendName, + challenge: testSigNetChallengeHex, + blockTime: 30 * time.Second, + expectError: "bitcoin.signetblocktime is not " + + "supported with bitcoin.node=btcd", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := DefaultConfig() + cfg.LndDir = t.TempDir() + cfg.Bitcoin.Node = tc.node + cfg.Bitcoin.SigNet = true + cfg.Bitcoin.SigNetChallenge = tc.challenge + cfg.Bitcoin.SigNetBlockTime = tc.blockTime + cfg.BtcdMode.RPCUser = "user" + cfg.BtcdMode.RPCPass = "pass" + cfg.BitcoindMode.RPCUser = "user" + cfg.BitcoindMode.RPCPass = "pass" + cfg.BitcoindMode.RPCPolling = true + + cleanCfg, err := validateTestConfig(t, cfg) + + if tc.expectError != "" { + require.ErrorContains(t, err, tc.expectError) + return + } + + require.NoError(t, err) + require.Equal(t, tc.node, cleanCfg.Bitcoin.Node) + require.Equal( + t, tc.expectNet.Net, + cleanCfg.ActiveNetParams.Params.Net, + ) + require.Equal( + t, tc.expectNet.TargetTimePerBlock, + cleanCfg.ActiveNetParams.Params. + TargetTimePerBlock, + ) + }) + } +} + // TestValidateConfigTrickleDelay tests that the TrickleDelay configuration // is properly validated and defaulted in ValidateConfig. This test directly // verifies the validation logic without going through the full ValidateConfig diff --git a/lncfg/chain.go b/lncfg/chain.go index 3e6fdfdd868..7c3dfed69bf 100644 --- a/lncfg/chain.go +++ b/lncfg/chain.go @@ -2,6 +2,7 @@ package lncfg import ( "fmt" + "time" "github.com/lightningnetwork/lnd/lnwire" ) @@ -15,14 +16,15 @@ type Chain struct { Node string `long:"node" description:"The blockchain interface to use." choice:"btcd" choice:"bitcoind" choice:"neutrino" choice:"nochainbackend"` - MainNet bool `long:"mainnet" description:"Use the main network"` - TestNet3 bool `long:"testnet" description:"Use the test network"` - TestNet4 bool `long:"testnet4" description:"Use the testnet4 test network"` - SimNet bool `long:"simnet" description:"Use the simulation test network"` - RegTest bool `long:"regtest" description:"Use the regression test network"` - SigNet bool `long:"signet" description:"Use the signet test network"` - SigNetChallenge string `long:"signetchallenge" description:"Connect to a custom signet network defined by this challenge instead of using the global default signet test network -- Can be specified multiple times"` - SigNetSeedNode []string `long:"signetseednode" description:"Specify a seed node for the signet network instead of using the global default signet network seed nodes"` + MainNet bool `long:"mainnet" description:"Use the main network"` + TestNet3 bool `long:"testnet" description:"Use the test network"` + TestNet4 bool `long:"testnet4" description:"Use the testnet4 test network"` + SimNet bool `long:"simnet" description:"Use the simulation test network"` + RegTest bool `long:"regtest" description:"Use the regression test network"` + SigNet bool `long:"signet" description:"Use the signet test network"` + SigNetChallenge string `long:"signetchallenge" description:"Connect to a custom signet network defined by this challenge instead of using the global default signet test network -- Can be specified multiple times. Do not set this with the bitcoind backend -- configure the custom signet on bitcoind instead."` + SigNetBlockTime time.Duration `long:"signetblocktime" description:"Override the expected block interval for a custom signet network. This must match the signet node's signetblocktime setting and is only supported with the neutrino backend."` + SigNetSeedNode []string `long:"signetseednode" description:"Specify a seed node for the signet network instead of using the global default signet network seed nodes"` DefaultNumChanConfs int `long:"defaultchanconfs" description:"The default number of confirmations a channel must have before it's considered open. If this is not set, we will scale the value according to the channel size."` DefaultRemoteDelay int `long:"defaultremotedelay" description:"The default number of blocks we will require our channel counterparty to wait before accessing its funds in case of unilateral close. If this is not set, we will scale the value according to the channel size."` diff --git a/sample-lnd.conf b/sample-lnd.conf index f881c1174e9..167e15e7814 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -668,9 +668,19 @@ ; bitcoin.signet=false ; Connect to a custom signet network defined by this challenge instead of using -; the global default signet test network -- Can be specified multiple times +; the global default signet test network -- Can be specified multiple times. +; +; NOTE: Do not set this when bitcoin.node=bitcoind. For bitcoind-backed custom +; signets, configure signetchallenge on bitcoind instead. ; bitcoin.signetchallenge= +; Override the expected block interval for a custom signet network. This must +; match the signet node's signetblocktime setting and is only supported with +; bitcoin.node=neutrino. Do not set this when bitcoin.node=bitcoind; configure +; signetblocktime on bitcoind instead. This is not currently supported with +; bitcoin.node=btcd. +; bitcoin.signetblocktime= + ; Specify a seed node for the signet network instead of using the global default ; signet network seed nodes ; Default: From d8da2424ac714e400bf7aa260facc22b4d364fd8 Mon Sep 17 00:00:00 2001 From: Tee8z Date: Mon, 1 Jun 2026 18:44:58 -0400 Subject: [PATCH 2/2] docs(release-notes): add custom signet block time note --- docs/release-notes/release-notes-0.22.0.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md index 45e76051922..624e4e373f5 100644 --- a/docs/release-notes/release-notes-0.22.0.md +++ b/docs/release-notes/release-notes-0.22.0.md @@ -38,6 +38,12 @@ ## Functional Enhancements +* A new [`bitcoin.signetblocktime` + config option](https://github.com/lightningnetwork/lnd/pull/10864) allows + neutrino-backed custom signet nodes to override the expected block interval + used for header validation, matching Bitcoin Core's `-signetblocktime` + setting. + ## RPC Additions * The `routerrpc.EstimateRouteFee` RPC now supports [restricting fee estimates @@ -89,3 +95,4 @@ * Boris Nagaev * Erick Cestari +* Tee8z