From f89a79900ec6e4e89beaf3e424c2fd0a6ced5143 Mon Sep 17 00:00:00 2001 From: Kilombino Date: Sun, 11 Oct 2026 18:06:34 +0000 Subject: [PATCH 01/12] config: allow the neutrino backend neutrino now validates BLAKE2b proof of work itself from btcd's Blake2bHeight, so it is no longer refused. A peer on the SHA256d chain fails at the activation height, where its headers are still v1, so no chain-identity check is needed for it. On regtest and simnet the configured activation height is copied into the btcd parameters handed to neutrino (on a copy of them). Without a known height neutrino is refused. --- config.go | 12 ++++++--- config_blake2b.go | 14 ++++++++++ config_blake2b_test.go | 58 ++++++++++++++++++++++++++++++++---------- docs/blake2b.md | 17 +++++++++---- 4 files changed, 79 insertions(+), 22 deletions(-) diff --git a/config.go b/config.go index 260b3a7896..cce26cf7b8 100644 --- a/config.go +++ b/config.go @@ -1493,10 +1493,14 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, "credentials for bitcoind: %v", err) } case neutrinoBackendName: - return nil, mkErr("bitcoin.node=neutrino is not supported: this " + - "daemon follows the Bitcoin BLAKE2b chain and does not " + - "validate its proof of work itself; use a Bitcoin Knots " + - "bitcoind backend") + // The light client checks BLAKE2b proof of work itself (btcd's + // blockchain package, from the network's Blake2bHeight), so it + // can only follow a network whose height it knows. + if !cfg.ActiveNetParams.Params.Blake2bPowDeployed() { + return nil, mkErr("bitcoin.node=neutrino needs the " + + "network's BLAKE2b activation height: set " + + "bitcoin.blake2b-activation-height") + } case "nochainbackend": // Nothing to configure, we're running without any chain diff --git a/config_blake2b.go b/config_blake2b.go index 2fcf3ee798..ab94b8dd29 100644 --- a/config_blake2b.go +++ b/config_blake2b.go @@ -11,6 +11,10 @@ import ( "github.com/lightningnetwork/lnd/zpay32" ) +// blake2bDefaultTargetShift is Knots' Consensus::Params Blake2bTargetShift +// default, which every network but mainnet (22) uses. +const blake2bDefaultTargetShift = 20 + // applyBlake2bChainConfig applies the Bitcoin BLAKE2b chain-identity options // to the active network parameters once the network has been selected, and // registers the invoice prefix for it. It returns an error for combinations @@ -37,6 +41,16 @@ func applyBlake2bChainConfig(cfg *Config) error { "at 961640") } params.Blake2bActivationHeight = cfg.Bitcoin.Blake2bActivationHeight + + // The light client validates BLAKE2b proof of work itself from the + // btcd parameters, so they must carry this run's height too. A copy, + // so the package-level parameters stay as shipped. + p := *params.Params + p.Blake2bHeight = int32(cfg.Bitcoin.Blake2bActivationHeight) + if p.Blake2bTargetShift == 0 { + p.Blake2bTargetShift = blake2bDefaultTargetShift + } + params.Params = &p } if cfg.Bitcoin.ChainHashOverride != "" { diff --git a/config_blake2b_test.go b/config_blake2b_test.go index 7e94961a20..4a19d4726d 100644 --- a/config_blake2b_test.go +++ b/config_blake2b_test.go @@ -133,9 +133,9 @@ func TestApplyBlake2bChainConfig(t *testing.T) { }) } -// TestBackendChoicesRefused pins that the btcd and neutrino backends are -// rejected at configuration time with a message naming the reason, and that -// bitcoind on mainnet gets past that check. +// TestBackendChoicesRefused pins that the btcd backend is rejected at +// configuration time with a message naming the reason, and that bitcoind and +// neutrino on mainnet get past that check. func TestBackendChoicesRefused(t *testing.T) { validate := func(t *testing.T, node string) error { t.Helper() @@ -151,17 +151,18 @@ func TestBackendChoicesRefused(t *testing.T) { return err } - for _, node := range []string{btcdBackendName, neutrinoBackendName} { - err := validate(t, node) - require.Error(t, err, node) - require.Contains(t, err.Error(), "not supported", node) - require.Contains(t, err.Error(), "BLAKE2b", node) - } + err := validate(t, btcdBackendName) + require.Error(t, err) + require.Contains(t, err.Error(), "not supported") + require.Contains(t, err.Error(), "BLAKE2b") - // bitcoind is accepted by the backend switch; whatever fails later - // (RPC credentials are not configured here) must not be that message. - if err := validate(t, bitcoindBackendName); err != nil { - require.NotContains(t, err.Error(), "not supported") + // bitcoind and neutrino are accepted by the backend switch; whatever + // fails later (RPC credentials or peers are not configured here) must + // not be that message. + for _, node := range []string{bitcoindBackendName, neutrinoBackendName} { + if err := validate(t, node); err != nil { + require.NotContains(t, err.Error(), "not supported", node) + } } } @@ -262,3 +263,34 @@ func TestPeerNetworksOptionsThatContradict(t *testing.T) { require.Contains(t, err.Error(), "contradict") }) } + +// TestNeutrinoBlake2bParams pins that the light client is handed the BLAKE2b +// activation height it validates proof of work from: the btcd parameters' +// own on mainnet, and the configured one on regtest, set on a copy so the +// package-level parameters are left as shipped. +func TestNeutrinoBlake2bParams(t *testing.T) { + t.Run("mainnet", func(t *testing.T) { + cfg := blake2bTestConfig(t, chainreg.BitcoinMainNetParams, + func(c *Config) { c.Bitcoin.Node = neutrinoBackendName }) + require.NoError(t, applyBlake2bChainConfig(cfg)) + require.Equal(t, + int32(cfg.ActiveNetParams.Blake2bActivationHeight), + cfg.ActiveNetParams.Params.Blake2bHeight) + require.Equal(t, uint(22), + cfg.ActiveNetParams.Params.Blake2bTargetShift) + }) + + t.Run("regtest takes the configured height", func(t *testing.T) { + cfg := blake2bTestConfig(t, chainreg.BitcoinRegTestNetParams, + func(c *Config) { + c.Bitcoin.Node = neutrinoBackendName + c.Bitcoin.Blake2bActivationHeight = 150 + }) + require.NoError(t, applyBlake2bChainConfig(cfg)) + require.Equal(t, int32(150), + cfg.ActiveNetParams.Params.Blake2bHeight) + require.Equal(t, uint(20), + cfg.ActiveNetParams.Params.Blake2bTargetShift) + require.Zero(t, chaincfg.RegressionNetParams.Blake2bHeight) + }) +} diff --git a/docs/blake2b.md b/docs/blake2b.md index bfa65bbae8..b859d85ccc 100644 --- a/docs/blake2b.md +++ b/docs/blake2b.md @@ -221,11 +221,18 @@ Neither applies to a node with no channels, which can be upgraded whenever. ## Backends -Only `bitcoin.node=bitcoind`, pointed at a Bitcoin Knots v29.4.1 or later -node which has upgraded, is supported. `btcd` cannot follow the BLAKE2b proof -of work and -`neutrino` would have to validate BLAKE2b proof of work itself against a -network with no filter servers; both are refused at configuration time. +`bitcoin.node=bitcoind`, pointed at a Bitcoin Knots v29.4.1 or later node +which has upgraded, and `bitcoin.node=neutrino` are supported. `btcd` cannot +follow the BLAKE2b proof of work and is refused at configuration time. + +`neutrino` validates BLAKE2b proof of work itself (btcd's `blockchain` +package, from the network's `Blake2bHeight`), so it needs no chain-identity +check: a peer serving the SHA256d chain is refused at the activation height, +where its headers are still v1. It needs peers serving compact block filters, +that is Knots nodes with `blockfilterindex=1` and `peerblockfilters=1`; set +them with `neutrino.connect` or `neutrino.addpeer`, since DNS seeds do not +know which nodes follow this chain. On regtest and simnet +`bitcoin.blake2b-activation-height` is required as with `bitcoind`. ## Configuration From fe871eb27e4dbfd2631f892077a97467356f0714 Mon Sep 17 00:00:00 2001 From: Kilombino Date: Mon, 12 Oct 2026 05:32:46 +0000 Subject: [PATCH 02/12] chainreg: pin the activation block and check the chain's identity under neutrino The light client would follow any most-work chain that passes the BLAKE2b rules from the activation height. On mainnet it now gets a checkpoint at 961640 with the pinned activation hash, on a copy of the parameters, and once its headers reach the activation height the chain-identity check runs against them (through a small adapter that answers the check's calls from neutrino's validated headers) and writes chain-identity.json, as the bitcoind path does. --- chainreg/blake2b_backend.go | 23 ++++++ chainreg/blake2b_neutrino.go | 123 ++++++++++++++++++++++++++++++ chainreg/blake2b_neutrino_test.go | 123 ++++++++++++++++++++++++++++++ chainreg/chainregistry.go | 11 +++ config_builder.go | 16 ++-- 5 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 chainreg/blake2b_neutrino.go create mode 100644 chainreg/blake2b_neutrino_test.go diff --git a/chainreg/blake2b_backend.go b/chainreg/blake2b_backend.go index 4eb1821817..2b8f478a07 100644 --- a/chainreg/blake2b_backend.go +++ b/chainreg/blake2b_backend.go @@ -2,6 +2,7 @@ package chainreg import ( "github.com/btcsuite/btcd/rpcclient" + "github.com/lightninglabs/neutrino" "github.com/lightningnetwork/lnd/lncfg" ) @@ -26,6 +27,28 @@ func chainIdentityStatusPath(cfg *Config) string { func verifyBlake2bBackend(rpc *rpcclient.Client, cfg *Config, quit <-chan struct{}) error { + return verifyBlake2bChain(rpc, cfg, quit) +} + +// verifyBlake2bNeutrino runs the same check against the neutrino light +// client: once its headers reach the activation height, the header there must +// be a v2 header whose id this daemon reproduces, and on mainnet the pinned +// activation block. The status file is written as for bitcoind, so wrappers +// read the same state whichever backend runs. +func verifyBlake2bNeutrino(cs *neutrino.ChainService, cfg *Config, + quit <-chan struct{}) error { + + return verifyBlake2bChain(&neutrinoIdentityRPC{ + src: cs, + chain: bitcoindChainName(cfg.ActiveNetParams.Params), + }, cfg, quit) +} + +// verifyBlake2bChain is the check itself, against whatever answers the +// calls it makes. +func verifyBlake2bChain(rpc blake2bChainRPC, cfg *Config, + quit <-chan struct{}) error { + params := cfg.ActiveNetParams statusPath := chainIdentityStatusPath(cfg) diff --git a/chainreg/blake2b_neutrino.go b/chainreg/blake2b_neutrino.go new file mode 100644 index 0000000000..4341cfa432 --- /dev/null +++ b/chainreg/blake2b_neutrino.go @@ -0,0 +1,123 @@ +package chainreg + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + + "github.com/btcsuite/btcd/btcjson" + bitcoinCfg "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/neutrino/headerfs" +) + +// NeutrinoChainParams returns the btcd parameters handed to the neutrino light +// client: the network's own, plus, where the activation block is pinned +// (mainnet), a checkpoint at the activation height with its hash. The light +// client checks BLAKE2b proof of work itself, but would follow any chain with +// the most work that passes those rules from the activation height on; the +// checkpoint ties it to the Bitcoin BLAKE2b chain's activation block. The +// shared parameters are left untouched: the checkpoint goes on a copy. +func NeutrinoChainParams(params BitcoinNetParams) *bitcoinCfg.Params { + p := *params.Params + if params.Blake2bActivationHash == nil || + params.Blake2bActivationHeight == 0 { + + return &p + } + + height := int32(params.Blake2bActivationHeight) + checkpoints := make([]bitcoinCfg.Checkpoint, 0, len(p.Checkpoints)+1) + for _, c := range p.Checkpoints { + if c.Height != height { + checkpoints = append(checkpoints, c) + } + } + hash := *params.Blake2bActivationHash + checkpoints = append(checkpoints, bitcoinCfg.Checkpoint{ + Height: height, Hash: &hash, + }) + sort.Slice(checkpoints, func(i, j int) bool { + return checkpoints[i].Height < checkpoints[j].Height + }) + p.Checkpoints = checkpoints + + return &p +} + +// neutrinoHeaderSource is what the chain-identity check reads from the light +// client: its best block, and the headers it validated. +type neutrinoHeaderSource interface { + BestBlock() (*headerfs.BlockStamp, error) + GetBlockHash(int64) (*chainhash.Hash, error) + GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error) +} + +// neutrinoIdentityRPC lets the chain-identity check, written against a +// bitcoind backend, run against the light client: it answers the few calls +// the check makes from neutrino's validated headers. Anything else (the +// deployment info, say) is reported as not available, which the check treats +// as the node not saying. +type neutrinoIdentityRPC struct { + src neutrinoHeaderSource + chain string +} + +// GetBlockChainInfo reports the light client's header height under the chain +// name the check expects, so the wait for the activation height works as it +// does with a node. +func (n *neutrinoIdentityRPC) GetBlockChainInfo() ( + *btcjson.GetBlockChainInfoResult, error) { + + best, err := n.src.BestBlock() + if err != nil { + return nil, err + } + + return &btcjson.GetBlockChainInfoResult{ + Chain: n.chain, + Blocks: best.Height, + Headers: best.Height, + }, nil +} + +// GetBlockHash returns the hash of the light client's header at height. +func (n *neutrinoIdentityRPC) GetBlockHash(height int64) (*chainhash.Hash, + error) { + + return n.src.GetBlockHash(height) +} + +// RawRequest answers getblockheader (non-verbose) from the light client's +// headers. +func (n *neutrinoIdentityRPC) RawRequest(method string, + params []json.RawMessage) (json.RawMessage, error) { + + if method != "getblockheader" || len(params) < 1 { + return nil, fmt.Errorf("%s is not available from the neutrino "+ + "light client", method) + } + + var hashStr string + if err := json.Unmarshal(params[0], &hashStr); err != nil { + return nil, err + } + hash, err := chainhash.NewHashFromStr(hashStr) + if err != nil { + return nil, err + } + header, err := n.src.GetBlockHeader(hash) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + if err := header.Serialize(&buf); err != nil { + return nil, err + } + + return json.Marshal(hex.EncodeToString(buf.Bytes())) +} diff --git a/chainreg/blake2b_neutrino_test.go b/chainreg/blake2b_neutrino_test.go new file mode 100644 index 0000000000..2462067b9f --- /dev/null +++ b/chainreg/blake2b_neutrino_test.go @@ -0,0 +1,123 @@ +package chainreg + +import ( + "bytes" + "encoding/hex" + "errors" + "path/filepath" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/neutrino/headerfs" + "github.com/stretchr/testify/require" +) + +// fakeNeutrino serves the chain-identity check from a fixed set of headers, +// as the light client would from the ones it validated. +type fakeNeutrino struct { + height int32 + byHash map[chainhash.Hash]*wire.BlockHeader + byHeight map[int64]chainhash.Hash +} + +func newFakeNeutrino(t *testing.T, height int32, at int64, hexStr string) *fakeNeutrino { + t.Helper() + raw, err := hex.DecodeString(hexStr) + require.NoError(t, err) + var h wire.BlockHeader + require.NoError(t, h.Deserialize(bytes.NewReader(raw))) + hash := h.BlockHash() + return &fakeNeutrino{ + height: height, + byHash: map[chainhash.Hash]*wire.BlockHeader{hash: &h}, + byHeight: map[int64]chainhash.Hash{at: hash}, + } +} + +func (f *fakeNeutrino) BestBlock() (*headerfs.BlockStamp, error) { + return &headerfs.BlockStamp{Height: f.height}, nil +} + +func (f *fakeNeutrino) GetBlockHash(h int64) (*chainhash.Hash, error) { + hash, ok := f.byHeight[h] + if !ok { + return nil, errors.New("no header at that height") + } + return &hash, nil +} + +func (f *fakeNeutrino) GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) { + h, ok := f.byHash[*hash] + if !ok { + return nil, errors.New("unknown header") + } + return h, nil +} + +// TestNeutrinoChainParamsCheckpoint pins that the light client gets a +// checkpoint at the mainnet activation block, on a copy, and nothing extra on +// a network without a pinned activation hash. +func TestNeutrinoChainParamsCheckpoint(t *testing.T) { + before := len(chaincfg.MainNetParams.Checkpoints) + + p := NeutrinoChainParams(BitcoinMainNetParams) + require.Len(t, chaincfg.MainNetParams.Checkpoints, before, + "shared parameters changed") + require.Len(t, p.Checkpoints, before+1) + + last := p.Checkpoints[len(p.Checkpoints)-1] + require.EqualValues(t, Blake2bMainnetActivationHeight, last.Height) + require.Equal(t, *Blake2bMainnetActivationHash, *last.Hash) + for i := 1; i < len(p.Checkpoints); i++ { + require.Less(t, p.Checkpoints[i-1].Height, p.Checkpoints[i].Height) + } + + t4 := NeutrinoChainParams(BitcoinTestNet4Params) + require.Equal(t, chaincfg.TestNet4Params.Checkpoints, t4.Checkpoints) +} + +// TestNeutrinoChainIdentity runs the chain-identity check against the light +// client's headers: the real activation header confirms mainnet and writes +// the status file; the last SHA256d header in its place is refused. +func TestNeutrinoChainIdentity(t *testing.T) { + t.Run("activation header confirms", func(t *testing.T) { + status := filepath.Join(t.TempDir(), ChainIdentityFileName) + rpc := &neutrinoIdentityRPC{ + src: newFakeNeutrino(t, 976700, 961640, activationHeaderHex), + chain: "main", + } + c := newTestChecker(t, rpc, BitcoinMainNetParams, status) + height, err := c.run(make(chan struct{})) + require.NoError(t, err) + require.EqualValues(t, 961640, height) + + st, err := ReadChainIdentityStatus(status) + require.NoError(t, err) + require.Equal(t, ChainIdentityConfirmed, st.State) + require.Equal(t, Blake2bMainnetActivationHash.String(), st.ActivationHash) + }) + + t.Run("classic header refused", func(t *testing.T) { + status := filepath.Join(t.TempDir(), ChainIdentityFileName) + rpc := &neutrinoIdentityRPC{ + src: newFakeNeutrino(t, 976700, 961640, lastSHA256HeaderHex), + chain: "main", + } + c := newTestChecker(t, rpc, BitcoinMainNetParams, status) + _, err := c.run(make(chan struct{})) + var wrong *ErrWrongChain + require.ErrorAs(t, err, &wrong) + + st, err := ReadChainIdentityStatus(status) + require.NoError(t, err) + require.Equal(t, ChainIdentityRefused, st.State) + }) + + t.Run("other calls are not available", func(t *testing.T) { + rpc := &neutrinoIdentityRPC{src: &fakeNeutrino{}, chain: "main"} + _, err := rpc.RawRequest("getdeploymentinfo", nil) + require.Error(t, err) + }) +} diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index e50e2c6a69..39c0269026 100644 --- a/chainreg/chainregistry.go +++ b/chainreg/chainregistry.go @@ -297,6 +297,17 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) { cfg.ActiveNetParams.Params, cfg.NeutrinoCS, ) + // The light client checks BLAKE2b proof of work, and on + // mainnet a checkpoint ties it to the activation block; this + // confirms the chain it ended up on as the bitcoind path does + // (waiting for the activation height, then re-checking), and + // writes the same status file. + if err := verifyBlake2bNeutrino( + cfg.NeutrinoCS, cfg, chainIdentityQuit, + ); err != nil { + return nil, nil, err + } + // Get our best block as a health check. cc.HealthCheck = func() error { _, _, err := cc.ChainSource.GetBestBlock() diff --git a/config_builder.go b/config_builder.go index 6f85ad9eb8..58da6ba25e 100644 --- a/config_builder.go +++ b/config_builder.go @@ -1743,10 +1743,11 @@ func initNeutrinoBackend(ctx context.Context, cfg *Config, chainDir string, // is the inverse of each other. Therefore both cannot be true. For // every other case, the neutrino.validatechannels overwrites the // routing.assumechanvalid value. - if cfg.NeutrinoMode.ValidateChannels && cfg.Routing.AssumeChannelValid { - return nil, nil, fmt.Errorf("can't set both " + - "neutrino.validatechannels and routing." + - "assumechanvalid to true at the same time") + // On this fork neutrino.validatechannels defaults to true: a light + // client would otherwise route over channels nobody checked against + // the chain. routing.assumechanvalid=true still turns the check off. + if cfg.Routing.AssumeChannelValid { + cfg.NeutrinoMode.ValidateChannels = false } cfg.Routing.AssumeChannelValid = !cfg.NeutrinoMode.ValidateChannels @@ -1754,6 +1755,9 @@ func initNeutrinoBackend(ctx context.Context, cfg *Config, chainDir string, if err := cfg.NeutrinoMode.Validate(); err != nil { return nil, nil, err } + if err := checkNeutrinoBlake2b(cfg); err != nil { + return nil, nil, err + } // First we'll open the database file for neutrino, creating the // database if needed. We append the normalized network name here to @@ -1823,7 +1827,9 @@ func initNeutrinoBackend(ctx context.Context, cfg *Config, chainDir string, config := neutrino.Config{ DataDir: dbPath, Database: db, - ChainParams: *cfg.ActiveNetParams.Params, + // With a checkpoint at the BLAKE2b activation block where it + // is pinned (mainnet); see chainreg.NeutrinoChainParams. + ChainParams: *chainreg.NeutrinoChainParams(cfg.ActiveNetParams), AddPeers: cfg.NeutrinoMode.AddPeers, ConnectPeers: cfg.NeutrinoMode.ConnectPeers, Dialer: func(addr net.Addr) (net.Conn, error) { From e64058d23a35c6fc560191ad94b67e4930747d55 Mon Sep 17 00:00:00 2001 From: Kilombino Date: Mon, 12 Oct 2026 05:32:46 +0000 Subject: [PATCH 03/12] config: neutrino peers, channel validation and smaller checks - mainnet and testnet4 need neutrino.connect: the DNS seeds return SHA256d nodes; addpeer alone is allowed with a warning - neutrino.validatechannels defaults to true; routing.assumechanvalid=true turns it off - the header import is refused: it reads 80-byte headers only - testnet4 takes btcd's built-in activation height for the check - bitcoin.blake2b-activation-height is range-checked before int32 - the unknown-backend error lists neutrino - tests for the above, and that regtest/simnet without a height refuse --- config.go | 6 +++- config_blake2b.go | 54 ++++++++++++++++++++++++++++++ config_blake2b_test.go | 74 ++++++++++++++++++++++++++++++++++++++++++ lncfg/neutrino.go | 2 +- 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index cce26cf7b8..9e2c3cdbce 100644 --- a/config.go +++ b/config.go @@ -721,6 +721,9 @@ func DefaultConfig() Config { UserAgentName: neutrino.UserAgentName, UserAgentVersion: neutrino.UserAgentVersion, MaxPeers: defaultNeutrinoMaxPeers, + // Checked against the chain by default on this fork: + // see initNeutrinoBackend. + ValidateChannels: true, }, BlockCacheSize: defaultBlockCacheSize, MaxPendingChannels: lncfg.DefaultMaxPendingChannels, @@ -1508,7 +1511,8 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, default: str := "only bitcoind (a Bitcoin Knots node on the Bitcoin " + - "BLAKE2b chain) and nochainbackend are supported" + "BLAKE2b chain), neutrino and nochainbackend are " + + "supported" return nil, mkErr(str) } diff --git a/config_blake2b.go b/config_blake2b.go index ab94b8dd29..26ae16a414 100644 --- a/config_blake2b.go +++ b/config_blake2b.go @@ -2,6 +2,7 @@ package lnd import ( "fmt" + "math" "path/filepath" "github.com/btcsuite/btcd/txscript" @@ -40,6 +41,12 @@ func applyBlake2bChainConfig(cfg *Config) error { "be set on mainnet: the activation height is fixed " + "at 961640") } + // It ends up in btcd's int32 heights: a value past that would + // turn negative there and fail with a misleading message. + if cfg.Bitcoin.Blake2bActivationHeight > math.MaxInt32 { + return fmt.Errorf("bitcoin.blake2b-activation-height %d is "+ + "out of range", cfg.Bitcoin.Blake2bActivationHeight) + } params.Blake2bActivationHeight = cfg.Bitcoin.Blake2bActivationHeight // The light client validates BLAKE2b proof of work itself from the @@ -53,6 +60,17 @@ func applyBlake2bChainConfig(cfg *Config) error { params.Params = &p } + // The light client takes the activation height from btcd's parameters + // (testnet4's built-in 150308, unless overridden above); the + // chain-identity check, which a bitcoind backend can ask the node + // about, gets the same one. + if cfg.Bitcoin.Node == neutrinoBackendName && + params.Blake2bActivationHeight == 0 && + params.Params.Blake2bPowDeployed() { + + params.Blake2bActivationHeight = uint32(params.Params.Blake2bHeight) + } + if cfg.Bitcoin.ChainHashOverride != "" { if !cfg.Bitcoin.RegTest { return fmt.Errorf("bitcoin.chain-hash-override is only " + @@ -145,3 +163,39 @@ func applyUnifiedSigHash(enabled bool) { // elsewhere, which stay spendable, and replayable. txauthor.LegacySigHashType = txscript.SigHashAll } + +// checkNeutrinoBlake2b holds the neutrino options that matter on the BLAKE2b +// chain. The DNS seeds of mainnet and testnet4 are Bitcoin's and return almost +// only nodes on the SHA256d chain, which the light client bans at the +// activation height, so it needs peers that serve BLAKE2b headers and compact +// filters, named with neutrino.connect. neutrino.addpeer alone still mixes in +// seed peers: allowed, with a warning. The header import reads 80-byte +// headers only and cannot carry a chain past the activation height. +func checkNeutrinoBlake2b(cfg *Config) error { + if cfg.NeutrinoMode.BlockHeadersSource != "" || + cfg.NeutrinoMode.FilterHeadersSource != "" { + + return fmt.Errorf("neutrino.blockheaderssource and " + + "neutrino.filterheaderssource are not supported on the " + + "Bitcoin BLAKE2b chain: the import reads 80-byte headers " + + "only") + } + + if cfg.Bitcoin.IsLocalNetwork() || len(cfg.NeutrinoMode.ConnectPeers) > 0 { + return nil + } + if len(cfg.NeutrinoMode.AddPeers) == 0 { + return fmt.Errorf("bitcoin.node=neutrino on %s needs " + + "neutrino.connect: the DNS seeds return nodes on the " + + "SHA256d chain; name Bitcoin Knots nodes on the BLAKE2b " + + "chain with blockfilterindex=1 and peerblockfilters=1, " + + "ideally your own", cfg.ActiveNetParams.Name) + } + ltndLog.Warnf("neutrino.addpeer without neutrino.connect: peers from "+ + "the DNS seeds, almost all on the SHA256d chain, are dialled "+ + "too (and banned at the BLAKE2b activation height); prefer "+ + "neutrino.connect to Knots nodes on the BLAKE2b chain on %s", + cfg.ActiveNetParams.Name) + + return nil +} diff --git a/config_blake2b_test.go b/config_blake2b_test.go index 4a19d4726d..f4514f450b 100644 --- a/config_blake2b_test.go +++ b/config_blake2b_test.go @@ -294,3 +294,77 @@ func TestNeutrinoBlake2bParams(t *testing.T) { require.Zero(t, chaincfg.RegressionNetParams.Blake2bHeight) }) } + +// TestNeutrinoNeedsHeightOnLocalNetworks pins that neutrino on regtest or +// simnet without bitcoin.blake2b-activation-height is refused at +// configuration time, since the light client could not validate past it. +func TestNeutrinoNeedsHeightOnLocalNetworks(t *testing.T) { + for _, net := range []string{"regtest", "simnet"} { + t.Run(net, func(t *testing.T) { + cfg := DefaultConfig() + cfg.LndDir = t.TempDir() + cfg.Bitcoin.Node = neutrinoBackendName + cfg.Bitcoin.RegTest = net == "regtest" + cfg.Bitcoin.SimNet = net == "simnet" + fileParser := flags.NewParser(&cfg, flags.Default) + flagParser := flags.NewParser(&cfg, flags.Default) + _, err := ValidateConfig( + cfg, signal.Interceptor{}, fileParser, flagParser, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "blake2b-activation-height") + }) + } +} + +// TestNeutrinoTestnet4Height pins that on testnet4 the chain-identity check +// gets the light client's built-in activation height, and that an +// out-of-range configured height is refused before it reaches btcd's int32. +func TestNeutrinoTestnet4Height(t *testing.T) { + cfg := blake2bTestConfig(t, chainreg.BitcoinTestNet4Params, + func(c *Config) { c.Bitcoin.Node = neutrinoBackendName }) + require.NoError(t, applyBlake2bChainConfig(cfg)) + require.EqualValues(t, 150308, cfg.ActiveNetParams.Blake2bActivationHeight) + + cfg = blake2bTestConfig(t, chainreg.BitcoinRegTestNetParams, + func(c *Config) { + c.Bitcoin.Node = neutrinoBackendName + c.Bitcoin.Blake2bActivationHeight = 1 << 31 + }) + require.ErrorContains(t, applyBlake2bChainConfig(cfg), "out of range") +} + +// TestCheckNeutrinoBlake2b covers the peer and header-import rules for +// neutrino on the BLAKE2b chain. +func TestCheckNeutrinoBlake2b(t *testing.T) { + mainnet := func(set func(*Config)) *Config { + return blake2bTestConfig(t, chainreg.BitcoinMainNetParams, + func(c *Config) { + c.Bitcoin.Node = neutrinoBackendName + set(c) + }) + } + + err := checkNeutrinoBlake2b(mainnet(func(*Config) {})) + require.ErrorContains(t, err, "neutrino.connect") + + require.NoError(t, checkNeutrinoBlake2b(mainnet(func(c *Config) { + c.NeutrinoMode.ConnectPeers = []string{"knots.example:8333"} + }))) + + // addpeer alone is allowed, with a warning. + require.NoError(t, checkNeutrinoBlake2b(mainnet(func(c *Config) { + c.NeutrinoMode.AddPeers = []string{"knots.example:8333"} + }))) + + err = checkNeutrinoBlake2b(mainnet(func(c *Config) { + c.NeutrinoMode.ConnectPeers = []string{"knots.example:8333"} + c.NeutrinoMode.BlockHeadersSource = "headers.bin" + })) + require.ErrorContains(t, err, "80-byte") + + // Local networks need no peer list. + regtest := blake2bTestConfig(t, chainreg.BitcoinRegTestNetParams, + func(c *Config) { c.Bitcoin.Node = neutrinoBackendName }) + require.NoError(t, checkNeutrinoBlake2b(regtest)) +} diff --git a/lncfg/neutrino.go b/lncfg/neutrino.go index 62c8b247cb..156d52f864 100644 --- a/lncfg/neutrino.go +++ b/lncfg/neutrino.go @@ -18,7 +18,7 @@ type Neutrino struct { AssertFilterHeader string `long:"assertfilterheader" description:"Optional filter header in height:hash format to assert the state of neutrino's filter header chain on startup. If the assertion does not hold, then the filter header chain will be re-synced from the genesis block."` UserAgentName string `long:"useragentname" description:"Used to help identify ourselves to other bitcoin peers"` UserAgentVersion string `long:"useragentversion" description:"Used to help identify ourselves to other bitcoin peers"` - ValidateChannels bool `long:"validatechannels" description:"Validate every channel in the graph during sync by downloading the containing block. This is the inverse of routing.assumechanvalid, meaning that for Neutrino the validation is turned off by default for massively increased graph sync performance. This speedup comes at the risk of using an unvalidated view of the network for routing. Overwrites the value of routing.assumechanvalid if Neutrino is used. (default: false)"` + ValidateChannels bool `long:"validatechannels" description:"Validate every channel in the graph during sync by downloading the containing block. This is the inverse of routing.assumechanvalid, meaning that for Neutrino the validation is turned off by default for massively increased graph sync performance. This speedup comes at the risk of using an unvalidated view of the network for routing. Overwrites the value of routing.assumechanvalid if Neutrino is used. On the Bitcoin BLAKE2b fork it is on by default; routing.assumechanvalid=true turns it off. (default: true)"` BroadcastTimeout time.Duration `long:"broadcasttimeout" description:"The amount of time to wait before giving up on a transaction broadcast attempt."` PersistFilters bool `long:"persistfilters" description:"Whether compact filters fetched from the P2P network should be persisted to disk."` From 9bb48cb038c42649b46bf0f8c16e707b19ab04df Mon Sep 17 00:00:00 2001 From: Kilombino Date: Mon, 12 Oct 2026 05:32:46 +0000 Subject: [PATCH 04/12] docs: what a neutrino node checks and what it does not --- docs/blake2b.md | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/docs/blake2b.md b/docs/blake2b.md index b859d85ccc..315b322bf5 100644 --- a/docs/blake2b.md +++ b/docs/blake2b.md @@ -225,14 +225,42 @@ Neither applies to a node with no channels, which can be upgraded whenever. which has upgraded, and `bitcoin.node=neutrino` are supported. `btcd` cannot follow the BLAKE2b proof of work and is refused at configuration time. +### neutrino + `neutrino` validates BLAKE2b proof of work itself (btcd's `blockchain` -package, from the network's `Blake2bHeight`), so it needs no chain-identity -check: a peer serving the SHA256d chain is refused at the activation height, -where its headers are still v1. It needs peers serving compact block filters, -that is Knots nodes with `blockfilterindex=1` and `peerblockfilters=1`; set -them with `neutrino.connect` or `neutrino.addpeer`, since DNS seeds do not -know which nodes follow this chain. On regtest and simnet -`bitcoin.blake2b-activation-height` is required as with `bitcoind`. +package, from the network's `Blake2bHeight`). A peer serving the SHA256d chain +is refused at the activation height, where its headers are still v1, and is +banned so it is not dialled again. On mainnet the light client also gets a +checkpoint at block 961640 with the activation block's hash, and once its +headers reach that height the same chain-identity check as with `bitcoind` +runs against them and writes `chain-identity.json`. + +Peers: neutrino needs peers serving compact block filters, that is Knots nodes +on the BLAKE2b chain with `blockfilterindex=1` and `peerblockfilters=1`. +Mainnet and testnet4 use Bitcoin's DNS seeds, which return almost only nodes on +the SHA256d chain, so `neutrino.connect` is required there; `neutrino.addpeer` +alone is accepted with a warning, since it still mixes in seed peers. Point it +at your own Knots node if you have one. + +What a light client does not check: it validates headers and proof of work, +not transactions or scripts. Neither the unified signature hash rule nor the +long coinbase maturity is enforced by it. With this chain's small hashrate a +miner can feed a neutrino node an invalid block more cheaply than on Bitcoin, +which is another reason to `neutrino.connect` to a Knots node you trust. + +Channel announcements are checked against the chain by default on this fork +(`neutrino.validatechannels` defaults to true); `routing.assumechanvalid=true` +turns that off. + +Fees: neutrino has no node to estimate fees from, so mainnet needs `fee.url`. +The public fee estimators follow the SHA256d chain; use one that serves this +chain. + +On regtest and simnet `bitcoin.blake2b-activation-height` is required as with +`bitcoind`. On testnet4 neutrino uses btcd's built-in 150308, which +`bitcoin.blake2b-activation-height` overrides. Importing headers from a file +(`neutrino.blockheaderssource`, `neutrino.filterheaderssource`) is not +supported: the import reads 80-byte headers only. ## Configuration From 69925f5520de2a06da528f4023a3c937ed179054 Mon Sep 17 00:00:00 2001 From: Kilombino Date: Mon, 12 Oct 2026 05:32:46 +0000 Subject: [PATCH 05/12] build: neutrino-blake2b with the ban and trim fixes Pseudo-version of paulscode/neutrino-blake2b#1 until it is merged and tagged v0.17.1-blake2b.1. --- go.mod | 2 ++ go.sum | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 34df71163b..ad79d7f323 100644 --- a/go.mod +++ b/go.mod @@ -227,3 +227,5 @@ replace github.com/btcsuite/btcd => github.com/paulscode/btcd-blake2b v0.25.1-bl replace github.com/btcsuite/btcwallet/wallet/txauthor => github.com/paulscode/btcwallet-blake2b/wallet/txauthor v1.3.5-blake2b.1 replace github.com/lightningnetwork/lnd/fn/v2 => ./fn + +replace github.com/lightninglabs/neutrino => github.com/Kilombino/neutrino-blake2b v0.0.0-20261012051506-4b7072266471 diff --git a/go.sum b/go.sum index 7ab37e2d66..b9eb725938 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Kilombino/neutrino-blake2b v0.0.0-20261012051506-4b7072266471 h1:jgPIdM3sxX4Bhqm7512wapjXPjxqEp/ipSQ/x16HPwY= +github.com/Kilombino/neutrino-blake2b v0.0.0-20261012051506-4b7072266471/go.mod h1:oLkAavtRxNosJmxZFaYC5PO4ZT1XVnh1r/atiPVNTRk= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= @@ -354,8 +356,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= -github.com/lightninglabs/neutrino v0.17.1 h1:lNhgq7ix/N81R6oATroP/kHMzH1qzVVF2dEGcTlN2t4= -github.com/lightninglabs/neutrino v0.17.1/go.mod h1:tcwCgRTGWcaua0L/xzdwllW8eslHDbux4XkiYsivvHE= github.com/lightninglabs/neutrino/cache v1.1.3 h1:rgnabC41W+XaPuBTQrdeFjFCCAVKh1yctAgmb3Se9zA= github.com/lightninglabs/neutrino/cache v1.1.3/go.mod h1:qxkJb+pUxR5p84jl5uIGFCR4dGdFkhNUwMSxw3EUWls= github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display h1:Y2WiPkBS/00EiEg0qp0FhehxnQfk3vv8U6Xt3nN+rTY= From f61b50193b0bbfd9b740cb285629fb1178d39e1a Mon Sep 17 00:00:00 2001 From: Kilombino Date: Mon, 12 Oct 2026 05:37:11 +0000 Subject: [PATCH 06/12] chainreg: stop the chain-identity wait on shutdown A daemon still waiting for its backend to reach the activation height did not stop on SIGTERM: the wait only watched the chain control's own quit channel, which is closed after it returns. That was already so with bitcoind; with neutrino it is easier to hit (only SHA256d peers, all banned). The wait now also ends on the interceptor's shutdown. --- chainreg/blake2b_backend.go | 13 +++++++++++++ chainreg/blake2b_backend_test.go | 28 ++++++++++++++++++++++++++++ chainreg/chainregistry.go | 6 ++++++ config_builder.go | 1 + 4 files changed, 48 insertions(+) diff --git a/chainreg/blake2b_backend.go b/chainreg/blake2b_backend.go index 2b8f478a07..698877ca24 100644 --- a/chainreg/blake2b_backend.go +++ b/chainreg/blake2b_backend.go @@ -49,6 +49,19 @@ func verifyBlake2bNeutrino(cs *neutrino.ChainService, cfg *Config, func verifyBlake2bChain(rpc blake2bChainRPC, cfg *Config, quit <-chan struct{}) error { + if cfg.Shutdown != nil { + either := make(chan struct{}) + stop := quit + go func() { + select { + case <-stop: + case <-cfg.Shutdown: + } + close(either) + }() + quit = either + } + params := cfg.ActiveNetParams statusPath := chainIdentityStatusPath(cfg) diff --git a/chainreg/blake2b_backend_test.go b/chainreg/blake2b_backend_test.go index 2852210340..2fa22437c7 100644 --- a/chainreg/blake2b_backend_test.go +++ b/chainreg/blake2b_backend_test.go @@ -3,6 +3,7 @@ package chainreg import ( "path/filepath" "testing" + "time" "github.com/lightningnetwork/lnd/lncfg" "github.com/stretchr/testify/require" @@ -33,3 +34,30 @@ func TestChainIdentityStatusPathSelection(t *testing.T) { ChainIdentityFileName), chainIdentityStatusPath(cfg)) } + +// TestChainIdentityWaitStopsOnShutdown pins that a daemon still waiting for +// the backend to reach the activation height stops when asked to. +func TestChainIdentityWaitStopsOnShutdown(t *testing.T) { + shutdown := make(chan struct{}) + cfg := &Config{ + Bitcoin: &lncfg.Chain{ChainDir: t.TempDir()}, + ActiveNetParams: BitcoinMainNetParams, + Shutdown: shutdown, + } + // A backend that never gets there. + rpc := &neutrinoIdentityRPC{ + src: &fakeNeutrino{height: 961000}, + chain: "main", + } + + done := make(chan error, 1) + go func() { done <- verifyBlake2bChain(rpc, cfg, make(chan struct{})) }() + close(shutdown) + + select { + case err := <-done: + require.ErrorContains(t, err, "shutting down") + case <-time.After(10 * time.Second): + t.Fatal("the wait did not stop on shutdown") + } +} diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index 39c0269026..6390ad24f4 100644 --- a/chainreg/chainregistry.go +++ b/chainreg/chainregistry.go @@ -80,6 +80,12 @@ type Config struct { // chain. It is expected to stop the daemon. OnChainMismatch func(error) + // Shutdown, when set, ends the chain-identity check's wait for the + // activation height: a daemon still waiting there (a node that never + // gets there, a light client with only SHA256d peers) must stop when + // asked to. + Shutdown <-chan struct{} + // WalletUnlockParams are the parameters that were used for unlocking // the main wallet. WalletUnlockParams *walletunlocker.WalletUnlockParams diff --git a/config_builder.go b/config_builder.go index 58da6ba25e..25b7d2ce5b 100644 --- a/config_builder.go +++ b/config_builder.go @@ -656,6 +656,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, d.logger.Criticalf("Stopping: %v", err) d.interceptor.RequestShutdown() }, + Shutdown: d.interceptor.ShutdownChannel(), } // Let's go ahead and create the partial chain control now that is only From 55b39f346ce3b65ceef933462ab317bfc034f74e Mon Sep 17 00:00:00 2001 From: Kilombino Date: Mon, 12 Oct 2026 22:23:03 +0000 Subject: [PATCH 07/12] go.mod: neutrino-blake2b e3dedd4 (the header store opens again when its index is ahead of the file) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ad79d7f323..657db241ef 100644 --- a/go.mod +++ b/go.mod @@ -228,4 +228,4 @@ replace github.com/btcsuite/btcwallet/wallet/txauthor => github.com/paulscode/bt replace github.com/lightningnetwork/lnd/fn/v2 => ./fn -replace github.com/lightninglabs/neutrino => github.com/Kilombino/neutrino-blake2b v0.0.0-20261012051506-4b7072266471 +replace github.com/lightninglabs/neutrino => github.com/Kilombino/neutrino-blake2b v0.0.0-20261012222229-e3dedd4d1590 diff --git a/go.sum b/go.sum index b9eb725938..d34743bd0b 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Kilombino/neutrino-blake2b v0.0.0-20261012051506-4b7072266471 h1:jgPIdM3sxX4Bhqm7512wapjXPjxqEp/ipSQ/x16HPwY= -github.com/Kilombino/neutrino-blake2b v0.0.0-20261012051506-4b7072266471/go.mod h1:oLkAavtRxNosJmxZFaYC5PO4ZT1XVnh1r/atiPVNTRk= +github.com/Kilombino/neutrino-blake2b v0.0.0-20261012222229-e3dedd4d1590 h1:BHsXn4OdRtsUIPNGh3Vw7f8EtyKW4RiKf/Ss0RcNZkE= +github.com/Kilombino/neutrino-blake2b v0.0.0-20261012222229-e3dedd4d1590/go.mod h1:oLkAavtRxNosJmxZFaYC5PO4ZT1XVnh1r/atiPVNTRk= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= From d63e2a5afa5a2e26330227928f7eea40756f92b3 Mon Sep 17 00:00:00 2001 From: Kilombino Date: Thu, 15 Oct 2026 18:05:36 +0000 Subject: [PATCH 08/12] multi: gofmt config_builder.go --- config_builder.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config_builder.go b/config_builder.go index 25b7d2ce5b..f13dcbb75c 100644 --- a/config_builder.go +++ b/config_builder.go @@ -1826,8 +1826,8 @@ func initNeutrinoBackend(ctx context.Context, cfg *Config, chainDir string, // neutrino light client. We pass in relevant configuration parameters // required. config := neutrino.Config{ - DataDir: dbPath, - Database: db, + DataDir: dbPath, + Database: db, // With a checkpoint at the BLAKE2b activation block where it // is pinned (mainnet); see chainreg.NeutrinoChainParams. ChainParams: *chainreg.NeutrinoChainParams(cfg.ActiveNetParams), From 32cc26de0ad9866ad3aaa636646b91caba45ac5b Mon Sep 17 00:00:00 2001 From: Kilombino Date: Thu, 15 Oct 2026 18:05:36 +0000 Subject: [PATCH 09/12] config: refuse neutrino on mainnet without fee.url at start; validatechannels help and the docs checkNeutrinoBlake2b now refuses mainnet neutrino without fee.url, so it fails at start rather than after the sync to the BLAKE2b activation height (where chainreg checks it). The validatechannels help (and sample-lnd.conf) says it is on by default here, that =false only takes effect in the config file, and that routing.assumechanvalid=true turns it off from the command line. docs/blake2b.md: the SHA256 chain's DNS seeds, and bans that last 48 hours (lnd sets neutrino's ban duration to that) with persistent peers only disconnected. gofmt on config_blake2b.go. --- config_blake2b.go | 16 ++++++++++++---- config_blake2b_test.go | 12 ++++++++++-- docs/blake2b.md | 10 +++++++--- lncfg/neutrino.go | 2 +- sample-lnd.conf | 11 ++++++----- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/config_blake2b.go b/config_blake2b.go index 26ae16a414..fb333b6c0c 100644 --- a/config_blake2b.go +++ b/config_blake2b.go @@ -181,14 +181,22 @@ func checkNeutrinoBlake2b(cfg *Config) error { "only") } + // The same rule chainreg applies when it builds the fee estimator, but + // that runs only once the node has synced to the BLAKE2b activation + // height; refused here it fails at start instead. + if cfg.Bitcoin.MainNet && cfg.Fee.URL == "" { + return fmt.Errorf("--fee.url parameter required when running " + + "neutrino on mainnet") + } + if cfg.Bitcoin.IsLocalNetwork() || len(cfg.NeutrinoMode.ConnectPeers) > 0 { return nil } if len(cfg.NeutrinoMode.AddPeers) == 0 { - return fmt.Errorf("bitcoin.node=neutrino on %s needs " + - "neutrino.connect: the DNS seeds return nodes on the " + - "SHA256d chain; name Bitcoin Knots nodes on the BLAKE2b " + - "chain with blockfilterindex=1 and peerblockfilters=1, " + + return fmt.Errorf("bitcoin.node=neutrino on %s needs "+ + "neutrino.connect: the DNS seeds return nodes on the "+ + "SHA256d chain; name Bitcoin Knots nodes on the BLAKE2b "+ + "chain with blockfilterindex=1 and peerblockfilters=1, "+ "ideally your own", cfg.ActiveNetParams.Name) } ltndLog.Warnf("neutrino.addpeer without neutrino.connect: peers from "+ diff --git a/config_blake2b_test.go b/config_blake2b_test.go index f4514f450b..c81bda1a74 100644 --- a/config_blake2b_test.go +++ b/config_blake2b_test.go @@ -334,18 +334,26 @@ func TestNeutrinoTestnet4Height(t *testing.T) { require.ErrorContains(t, applyBlake2bChainConfig(cfg), "out of range") } -// TestCheckNeutrinoBlake2b covers the peer and header-import rules for +// TestCheckNeutrinoBlake2b covers the peer, fee and header-import rules for // neutrino on the BLAKE2b chain. func TestCheckNeutrinoBlake2b(t *testing.T) { mainnet := func(set func(*Config)) *Config { return blake2bTestConfig(t, chainreg.BitcoinMainNetParams, func(c *Config) { c.Bitcoin.Node = neutrinoBackendName + c.Fee.URL = "https://fees.example/fees.json" set(c) }) } - err := checkNeutrinoBlake2b(mainnet(func(*Config) {})) + // No fee.url on mainnet: refused at start, not after the sync. + err := checkNeutrinoBlake2b(mainnet(func(c *Config) { + c.Fee.URL = "" + c.NeutrinoMode.ConnectPeers = []string{"knots.example:8333"} + })) + require.ErrorContains(t, err, "fee.url") + + err = checkNeutrinoBlake2b(mainnet(func(*Config) {})) require.ErrorContains(t, err, "neutrino.connect") require.NoError(t, checkNeutrinoBlake2b(mainnet(func(c *Config) { diff --git a/docs/blake2b.md b/docs/blake2b.md index 315b322bf5..7aebba5fce 100644 --- a/docs/blake2b.md +++ b/docs/blake2b.md @@ -230,15 +230,19 @@ follow the BLAKE2b proof of work and is refused at configuration time. `neutrino` validates BLAKE2b proof of work itself (btcd's `blockchain` package, from the network's `Blake2bHeight`). A peer serving the SHA256d chain is refused at the activation height, where its headers are still v1, and is -banned so it is not dialled again. On mainnet the light client also gets a +banned for 48 hours (lnd sets neutrino's ban duration to that, whatever +`neutrino.banduration` says) so it is not dialled again meanwhile. A peer +named in `neutrino.connect` or `neutrino.addpeer` is only disconnected, with +an error saying what to check, so a wrong setting here (a regtest activation +height) cannot lock out your own node. On mainnet the light client also gets a checkpoint at block 961640 with the activation block's hash, and once its headers reach that height the same chain-identity check as with `bitcoind` runs against them and writes `chain-identity.json`. Peers: neutrino needs peers serving compact block filters, that is Knots nodes on the BLAKE2b chain with `blockfilterindex=1` and `peerblockfilters=1`. -Mainnet and testnet4 use Bitcoin's DNS seeds, which return almost only nodes on -the SHA256d chain, so `neutrino.connect` is required there; `neutrino.addpeer` +Mainnet and testnet4 use the SHA256 chain's DNS seeds, which return almost only +nodes on the SHA256d chain, so `neutrino.connect` is required there; `neutrino.addpeer` alone is accepted with a warning, since it still mixes in seed peers. Point it at your own Knots node if you have one. diff --git a/lncfg/neutrino.go b/lncfg/neutrino.go index 156d52f864..b244b33d8c 100644 --- a/lncfg/neutrino.go +++ b/lncfg/neutrino.go @@ -18,7 +18,7 @@ type Neutrino struct { AssertFilterHeader string `long:"assertfilterheader" description:"Optional filter header in height:hash format to assert the state of neutrino's filter header chain on startup. If the assertion does not hold, then the filter header chain will be re-synced from the genesis block."` UserAgentName string `long:"useragentname" description:"Used to help identify ourselves to other bitcoin peers"` UserAgentVersion string `long:"useragentversion" description:"Used to help identify ourselves to other bitcoin peers"` - ValidateChannels bool `long:"validatechannels" description:"Validate every channel in the graph during sync by downloading the containing block. This is the inverse of routing.assumechanvalid, meaning that for Neutrino the validation is turned off by default for massively increased graph sync performance. This speedup comes at the risk of using an unvalidated view of the network for routing. Overwrites the value of routing.assumechanvalid if Neutrino is used. On the Bitcoin BLAKE2b fork it is on by default; routing.assumechanvalid=true turns it off. (default: true)"` + ValidateChannels bool `long:"validatechannels" description:"Validate every channel in the graph during sync by downloading the containing block. This is the inverse of routing.assumechanvalid, and overwrites it if Neutrino is used. On the Bitcoin BLAKE2b fork it is on by default (upstream LND leaves it off for a faster graph sync, at the risk of routing on an unvalidated view of the network). neutrino.validatechannels=false only takes effect in the config file, since a boolean flag cannot be set to false on the command line; there, routing.assumechanvalid=true turns validation off. (default: true)"` BroadcastTimeout time.Duration `long:"broadcasttimeout" description:"The amount of time to wait before giving up on a transaction broadcast attempt."` PersistFilters bool `long:"persistfilters" description:"Whether compact filters fetched from the P2P network should be persisted to disk."` diff --git a/sample-lnd.conf b/sample-lnd.conf index e333c08ec5..9acedfe3d1 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -962,11 +962,12 @@ ; neutrino.persistfilters=false ; Validate every channel in the graph during sync by downloading the containing -; block. This is the inverse of routing.assumechanvalid, meaning that for -; Neutrino the validation is turned off by default for massively increased graph -; sync performance. This speedup comes at the risk of using an unvalidated view -; of the network for routing. Overwrites the value of routing.assumechanvalid if -; Neutrino is used. +; block. This is the inverse of routing.assumechanvalid, and overwrites it if +; Neutrino is used. On the Bitcoin BLAKE2b fork it is on by default (upstream LND +; leaves it off for a faster graph sync, at the risk of routing on an unvalidated +; view of the network). Setting it to false only takes effect here, in the config +; file, since a boolean flag cannot be set to false on the command line; there, +; routing.assumechanvalid=true turns validation off. ; neutrino.validatechannels=false ; Headers import for fast initial sync. When configured, neutrino imports block From 348714819c04fa2ead384a95c42837b709de39b1 Mon Sep 17 00:00:00 2001 From: Kilombino Date: Sat, 17 Oct 2026 07:11:47 +0000 Subject: [PATCH 10/12] config: the deprecated feeurl is a fee URL for neutrino on mainnet checkNeutrinoBlake2b refused a mainnet neutrino node without fee.url, but chainreg copies the deprecated feeurl option into fee.url only later, when it builds the fee estimator. A node that still set feeurl= was refused at start although it had a fee URL. Accept either, and test feeurl alone. --- config_blake2b.go | 5 +++-- config_blake2b_test.go | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/config_blake2b.go b/config_blake2b.go index fb333b6c0c..2a2ed7854f 100644 --- a/config_blake2b.go +++ b/config_blake2b.go @@ -183,8 +183,9 @@ func checkNeutrinoBlake2b(cfg *Config) error { // The same rule chainreg applies when it builds the fee estimator, but // that runs only once the node has synced to the BLAKE2b activation - // height; refused here it fails at start instead. - if cfg.Bitcoin.MainNet && cfg.Fee.URL == "" { + // height; refused here it fails at start instead. The deprecated + // feeurl option counts too: chainreg copies it into fee.url later. + if cfg.Bitcoin.MainNet && cfg.Fee.URL == "" && cfg.FeeURL == "" { return fmt.Errorf("--fee.url parameter required when running " + "neutrino on mainnet") } diff --git a/config_blake2b_test.go b/config_blake2b_test.go index c81bda1a74..52a8c6ab5c 100644 --- a/config_blake2b_test.go +++ b/config_blake2b_test.go @@ -353,6 +353,14 @@ func TestCheckNeutrinoBlake2b(t *testing.T) { })) require.ErrorContains(t, err, "fee.url") + // The deprecated feeurl alone is a fee URL too: chainreg copies it + // into fee.url when it builds the fee estimator. + require.NoError(t, checkNeutrinoBlake2b(mainnet(func(c *Config) { + c.Fee.URL = "" + c.FeeURL = "https://fees.example/fees.json" + c.NeutrinoMode.ConnectPeers = []string{"knots.example:8333"} + }))) + err = checkNeutrinoBlake2b(mainnet(func(*Config) {})) require.ErrorContains(t, err, "neutrino.connect") From 98f05c63a3522d956dbeaeaa1bd16ab6a06d833d Mon Sep 17 00:00:00 2001 From: Kilombino Date: Sat, 17 Oct 2026 07:11:47 +0000 Subject: [PATCH 11/12] build: neutrino-blake2b v0.17.1-blake2b.1 neutrino-blake2b#1 is merged and tagged; point the replace at github.com/paulscode/neutrino-blake2b v0.17.1-blake2b.1 instead of the commit on our fork. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 657db241ef..1fabfc1941 100644 --- a/go.mod +++ b/go.mod @@ -228,4 +228,4 @@ replace github.com/btcsuite/btcwallet/wallet/txauthor => github.com/paulscode/bt replace github.com/lightningnetwork/lnd/fn/v2 => ./fn -replace github.com/lightninglabs/neutrino => github.com/Kilombino/neutrino-blake2b v0.0.0-20261012222229-e3dedd4d1590 +replace github.com/lightninglabs/neutrino => github.com/paulscode/neutrino-blake2b v0.17.1-blake2b.1 diff --git a/go.sum b/go.sum index d34743bd0b..23a5e09087 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Kilombino/neutrino-blake2b v0.0.0-20261012222229-e3dedd4d1590 h1:BHsXn4OdRtsUIPNGh3Vw7f8EtyKW4RiKf/Ss0RcNZkE= -github.com/Kilombino/neutrino-blake2b v0.0.0-20261012222229-e3dedd4d1590/go.mod h1:oLkAavtRxNosJmxZFaYC5PO4ZT1XVnh1r/atiPVNTRk= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= @@ -444,6 +442,8 @@ github.com/paulscode/btcwallet-blake2b/wallet/txauthor v1.3.5-blake2b.1 h1:uKixy github.com/paulscode/btcwallet-blake2b/wallet/txauthor v1.3.5-blake2b.1/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU= github.com/paulscode/lightning-fork-bridge v0.1.6 h1:XjRjYLj2Idkz72jW7ETV78yVga9xiPpXxtonDW1imeQ= github.com/paulscode/lightning-fork-bridge v0.1.6/go.mod h1:6fYqFoyd+s8Nx6TwDZEgBs5AHhetgp7ibgjxD3hfCjE= +github.com/paulscode/neutrino-blake2b v0.17.1-blake2b.1 h1:e2c/IcfkK7fWDVsWi/CKlz/REQNscIp54G8y7gte8sw= +github.com/paulscode/neutrino-blake2b v0.17.1-blake2b.1/go.mod h1:oLkAavtRxNosJmxZFaYC5PO4ZT1XVnh1r/atiPVNTRk= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= From a879f42e7cff8072c688271ded13ea9f6a262441 Mon Sep 17 00:00:00 2001 From: Kilombino Date: Sat, 17 Oct 2026 07:11:47 +0000 Subject: [PATCH 12/12] multi: wrap the neutrino lines at 80 columns lnd's linter counts a tab as 8 columns. Wrap the lines this branch added past that, in the config, the config builder, chainreg and their tests. --- chainreg/blake2b_backend_test.go | 4 +++- chainreg/blake2b_neutrino_test.go | 25 +++++++++++++++----- config_blake2b.go | 38 +++++++++++++++++++------------ config_blake2b_test.go | 22 +++++++++++++----- config_builder.go | 4 +++- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/chainreg/blake2b_backend_test.go b/chainreg/blake2b_backend_test.go index 2fa22437c7..cff35af1d2 100644 --- a/chainreg/blake2b_backend_test.go +++ b/chainreg/blake2b_backend_test.go @@ -51,7 +51,9 @@ func TestChainIdentityWaitStopsOnShutdown(t *testing.T) { } done := make(chan error, 1) - go func() { done <- verifyBlake2bChain(rpc, cfg, make(chan struct{})) }() + go func() { + done <- verifyBlake2bChain(rpc, cfg, make(chan struct{})) + }() close(shutdown) select { diff --git a/chainreg/blake2b_neutrino_test.go b/chainreg/blake2b_neutrino_test.go index 2462067b9f..a24970cfd2 100644 --- a/chainreg/blake2b_neutrino_test.go +++ b/chainreg/blake2b_neutrino_test.go @@ -22,7 +22,9 @@ type fakeNeutrino struct { byHeight map[int64]chainhash.Hash } -func newFakeNeutrino(t *testing.T, height int32, at int64, hexStr string) *fakeNeutrino { +func newFakeNeutrino(t *testing.T, height int32, at int64, + hexStr string) *fakeNeutrino { + t.Helper() raw, err := hex.DecodeString(hexStr) require.NoError(t, err) @@ -48,7 +50,9 @@ func (f *fakeNeutrino) GetBlockHash(h int64) (*chainhash.Hash, error) { return &hash, nil } -func (f *fakeNeutrino) GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) { +func (f *fakeNeutrino) GetBlockHeader(hash *chainhash.Hash) ( + *wire.BlockHeader, error) { + h, ok := f.byHash[*hash] if !ok { return nil, errors.New("unknown header") @@ -71,7 +75,9 @@ func TestNeutrinoChainParamsCheckpoint(t *testing.T) { require.EqualValues(t, Blake2bMainnetActivationHeight, last.Height) require.Equal(t, *Blake2bMainnetActivationHash, *last.Hash) for i := 1; i < len(p.Checkpoints); i++ { - require.Less(t, p.Checkpoints[i-1].Height, p.Checkpoints[i].Height) + require.Less( + t, p.Checkpoints[i-1].Height, p.Checkpoints[i].Height, + ) } t4 := NeutrinoChainParams(BitcoinTestNet4Params) @@ -85,7 +91,9 @@ func TestNeutrinoChainIdentity(t *testing.T) { t.Run("activation header confirms", func(t *testing.T) { status := filepath.Join(t.TempDir(), ChainIdentityFileName) rpc := &neutrinoIdentityRPC{ - src: newFakeNeutrino(t, 976700, 961640, activationHeaderHex), + src: newFakeNeutrino( + t, 976700, 961640, activationHeaderHex, + ), chain: "main", } c := newTestChecker(t, rpc, BitcoinMainNetParams, status) @@ -96,13 +104,18 @@ func TestNeutrinoChainIdentity(t *testing.T) { st, err := ReadChainIdentityStatus(status) require.NoError(t, err) require.Equal(t, ChainIdentityConfirmed, st.State) - require.Equal(t, Blake2bMainnetActivationHash.String(), st.ActivationHash) + require.Equal( + t, Blake2bMainnetActivationHash.String(), + st.ActivationHash, + ) }) t.Run("classic header refused", func(t *testing.T) { status := filepath.Join(t.TempDir(), ChainIdentityFileName) rpc := &neutrinoIdentityRPC{ - src: newFakeNeutrino(t, 976700, 961640, lastSHA256HeaderHex), + src: newFakeNeutrino( + t, 976700, 961640, lastSHA256HeaderHex, + ), chain: "main", } c := newTestChecker(t, rpc, BitcoinMainNetParams, status) diff --git a/config_blake2b.go b/config_blake2b.go index 2a2ed7854f..09577595dd 100644 --- a/config_blake2b.go +++ b/config_blake2b.go @@ -44,16 +44,19 @@ func applyBlake2bChainConfig(cfg *Config) error { // It ends up in btcd's int32 heights: a value past that would // turn negative there and fail with a misleading message. if cfg.Bitcoin.Blake2bActivationHeight > math.MaxInt32 { - return fmt.Errorf("bitcoin.blake2b-activation-height %d is "+ - "out of range", cfg.Bitcoin.Blake2bActivationHeight) + return fmt.Errorf("bitcoin.blake2b-activation-height "+ + "%d is out of range", + cfg.Bitcoin.Blake2bActivationHeight) } - params.Blake2bActivationHeight = cfg.Bitcoin.Blake2bActivationHeight + height := cfg.Bitcoin.Blake2bActivationHeight + params.Blake2bActivationHeight = height - // The light client validates BLAKE2b proof of work itself from the - // btcd parameters, so they must carry this run's height too. A copy, - // so the package-level parameters stay as shipped. + // The light client validates BLAKE2b proof of work itself from + // the btcd parameters, so they must carry this run's height + // too. A copy, so the package-level parameters stay as + // shipped. p := *params.Params - p.Blake2bHeight = int32(cfg.Bitcoin.Blake2bActivationHeight) + p.Blake2bHeight = int32(height) if p.Blake2bTargetShift == 0 { p.Blake2bTargetShift = blake2bDefaultTargetShift } @@ -68,7 +71,9 @@ func applyBlake2bChainConfig(cfg *Config) error { params.Blake2bActivationHeight == 0 && params.Params.Blake2bPowDeployed() { - params.Blake2bActivationHeight = uint32(params.Params.Blake2bHeight) + params.Blake2bActivationHeight = uint32( + params.Params.Blake2bHeight, + ) } if cfg.Bitcoin.ChainHashOverride != "" { @@ -176,9 +181,9 @@ func checkNeutrinoBlake2b(cfg *Config) error { cfg.NeutrinoMode.FilterHeadersSource != "" { return fmt.Errorf("neutrino.blockheaderssource and " + - "neutrino.filterheaderssource are not supported on the " + - "Bitcoin BLAKE2b chain: the import reads 80-byte headers " + - "only") + "neutrino.filterheaderssource are not supported on " + + "the Bitcoin BLAKE2b chain: the import reads 80-byte " + + "headers only") } // The same rule chainreg applies when it builds the fee estimator, but @@ -190,15 +195,18 @@ func checkNeutrinoBlake2b(cfg *Config) error { "neutrino on mainnet") } - if cfg.Bitcoin.IsLocalNetwork() || len(cfg.NeutrinoMode.ConnectPeers) > 0 { + if cfg.Bitcoin.IsLocalNetwork() || + len(cfg.NeutrinoMode.ConnectPeers) > 0 { + return nil } if len(cfg.NeutrinoMode.AddPeers) == 0 { return fmt.Errorf("bitcoin.node=neutrino on %s needs "+ "neutrino.connect: the DNS seeds return nodes on the "+ - "SHA256d chain; name Bitcoin Knots nodes on the BLAKE2b "+ - "chain with blockfilterindex=1 and peerblockfilters=1, "+ - "ideally your own", cfg.ActiveNetParams.Name) + "SHA256d chain; name Bitcoin Knots nodes on the "+ + "BLAKE2b chain with blockfilterindex=1 and "+ + "peerblockfilters=1, ideally your own", + cfg.ActiveNetParams.Name) } ltndLog.Warnf("neutrino.addpeer without neutrino.connect: peers from "+ "the DNS seeds, almost all on the SHA256d chain, are dialled "+ diff --git a/config_blake2b_test.go b/config_blake2b_test.go index 52a8c6ab5c..471f3cbdbf 100644 --- a/config_blake2b_test.go +++ b/config_blake2b_test.go @@ -159,9 +159,12 @@ func TestBackendChoicesRefused(t *testing.T) { // bitcoind and neutrino are accepted by the backend switch; whatever // fails later (RPC credentials or peers are not configured here) must // not be that message. - for _, node := range []string{bitcoindBackendName, neutrinoBackendName} { + backends := []string{bitcoindBackendName, neutrinoBackendName} + for _, node := range backends { if err := validate(t, node); err != nil { - require.NotContains(t, err.Error(), "not supported", node) + require.NotContains( + t, err.Error(), "not supported", node, + ) } } } @@ -271,7 +274,9 @@ func TestPeerNetworksOptionsThatContradict(t *testing.T) { func TestNeutrinoBlake2bParams(t *testing.T) { t.Run("mainnet", func(t *testing.T) { cfg := blake2bTestConfig(t, chainreg.BitcoinMainNetParams, - func(c *Config) { c.Bitcoin.Node = neutrinoBackendName }) + func(c *Config) { + c.Bitcoin.Node = neutrinoBackendName + }) require.NoError(t, applyBlake2bChainConfig(cfg)) require.Equal(t, int32(cfg.ActiveNetParams.Blake2bActivationHeight), @@ -309,10 +314,13 @@ func TestNeutrinoNeedsHeightOnLocalNetworks(t *testing.T) { fileParser := flags.NewParser(&cfg, flags.Default) flagParser := flags.NewParser(&cfg, flags.Default) _, err := ValidateConfig( - cfg, signal.Interceptor{}, fileParser, flagParser, + cfg, signal.Interceptor{}, fileParser, + flagParser, ) require.Error(t, err) - require.Contains(t, err.Error(), "blake2b-activation-height") + require.Contains( + t, err.Error(), "blake2b-activation-height", + ) }) } } @@ -324,7 +332,9 @@ func TestNeutrinoTestnet4Height(t *testing.T) { cfg := blake2bTestConfig(t, chainreg.BitcoinTestNet4Params, func(c *Config) { c.Bitcoin.Node = neutrinoBackendName }) require.NoError(t, applyBlake2bChainConfig(cfg)) - require.EqualValues(t, 150308, cfg.ActiveNetParams.Blake2bActivationHeight) + require.EqualValues( + t, 150308, cfg.ActiveNetParams.Blake2bActivationHeight, + ) cfg = blake2bTestConfig(t, chainreg.BitcoinRegTestNetParams, func(c *Config) { diff --git a/config_builder.go b/config_builder.go index f13dcbb75c..04690337cb 100644 --- a/config_builder.go +++ b/config_builder.go @@ -1830,7 +1830,9 @@ func initNeutrinoBackend(ctx context.Context, cfg *Config, chainDir string, Database: db, // With a checkpoint at the BLAKE2b activation block where it // is pinned (mainnet); see chainreg.NeutrinoChainParams. - ChainParams: *chainreg.NeutrinoChainParams(cfg.ActiveNetParams), + ChainParams: *chainreg.NeutrinoChainParams( + cfg.ActiveNetParams, + ), AddPeers: cfg.NeutrinoMode.AddPeers, ConnectPeers: cfg.NeutrinoMode.ConnectPeers, Dialer: func(addr net.Addr) (net.Conn, error) {