itest: add neutrino headers import integration test
What changed, and why it matters
This commit adds a new automated test for the LND Lightning node software. It does not change any production code; it only verifies that a neutrino-based node can import block headers from pre-built files and then continue syncing over the peer-to-peer network. There is no security fix or vulnerability present in this change.
No security action required. This is a test-only addition. Reviewers may optionally confirm the new test passes in CI and that the imported file paths and metadata format align with the current neutrino `chainimport` API.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces an integration test, testNeutrinoHeadersImport, plus its registration in the test list. The test mines 50 regtest blocks, spins up a reference neutrino node, copies its block_headers.bin and reg_filter_headers.bin files, prepends chain-import metadata using chainimport.AddHeadersImportMetadata, starts a second node with --neutrino.blockheaderssource and --neutrino.filterheaderssource pointing to those files, and asserts the import node reaches the chain tip and continues syncing newly mined blocks via P2P. The diff is purely additive test code under the integration build tag.
Changed components
itest/list_on_test.goitest/lnd_neutrino_headers_import_test.goInspect captured patch +137 / −0
diff --git a/itest/list_on_test.go b/itest/list_on_test.go
index c8e4244..21b3703 100644
--- a/itest/list_on_test.go
+++ b/itest/list_on_test.go
@@ -234,6 +234,10 @@ var allTestCases = []*lntest.TestCase{
Name: "neutrino kit",
TestFunc: testNeutrino,
},
+ {
+ Name: "neutrino headers import",
+ TestFunc: testNeutrinoHeadersImport,
+ },
{
Name: "etcd failover",
TestFunc: testEtcdFailover,
diff --git a/itest/lnd_neutrino_headers_import_test.go b/itest/lnd_neutrino_headers_import_test.go
new file mode 100644
index 0000000..46cd0c0
--- /dev/null
+++ b/itest/lnd_neutrino_headers_import_test.go
@@ -0,0 +1,133 @@
+//go:build integration
+
+package itest
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/btcsuite/btcd/chaincfg"
+ "github.com/lightninglabs/neutrino/chainimport"
+ "github.com/lightninglabs/neutrino/headerfs"
+ "github.com/lightningnetwork/lnd/lntest"
+ "github.com/stretchr/testify/require"
+)
+
+// testNeutrinoHeadersImport verifies that a neutrino node can import block
+// and filter headers from pre-built files, allowing it to sync faster than
+// downloading headers one-by-one via P2P.
+func testNeutrinoHeadersImport(ht *lntest.HarnessTest) {
+ // This test only applies to the neutrino backend.
+ if !ht.IsNeutrinoBackend() {
+ ht.Skipf("skipping neutrino headers import test " +
+ "for non-neutrino backend")
+ }
+
+ // Mine blocks so there is a meaningful chain to sync.
+ const numBlocks = 50
+ ht.MineBlocks(numBlocks)
+
+ // Start a reference node that syncs normally via P2P. NewNode waits
+ // for blockchain sync automatically.
+ refNode := ht.NewNode("Reference", nil)
+ refInfo := refNode.RPC.GetInfo()
+ require.True(ht, refInfo.SyncedToChain,
+ "reference node should be synced")
+
+ bestHeight := refInfo.BlockHeight
+
+ // Locate the header files created by the reference node's neutrino
+ // backend. These are stored as flat binary files in the chain data
+ // directory.
+ netName := chaincfg.RegressionNetParams.Name
+ chainDir := filepath.Join(
+ refNode.Cfg.DataDir, "chain", "bitcoin", netName,
+ )
+ blockHeadersPath := filepath.Join(chainDir, "block_headers.bin")
+ filterHeadersPath := filepath.Join(
+ chainDir, "reg_filter_headers.bin",
+ )
+
+ // Verify the header files exist.
+ _, err := os.Stat(blockHeadersPath)
+ require.NoError(ht, err, "block_headers.bin not found")
+ _, err = os.Stat(filterHeadersPath)
+ require.NoError(ht, err, "reg_filter_headers.bin not found")
+
+ // Copy the header files to a temporary directory for import.
+ importDir := ht.T.TempDir()
+ blockImportPath := filepath.Join(importDir, "block_headers.bin")
+ filterImportPath := filepath.Join(
+ importDir, "reg_filter_headers.bin",
+ )
+
+ copyFile(ht, blockHeadersPath, blockImportPath)
+ copyFile(ht, filterHeadersPath, filterImportPath)
+
+ // Add import metadata to the copied files. The metadata prepend
+ // includes network magic, version, header type, and start height.
+ // This is required by neutrino's chainimport package.
+ err = chainimport.AddHeadersImportMetadata(
+ blockImportPath, chaincfg.RegressionNetParams.Net,
+ 0, headerfs.Block, 0,
+ )
+ require.NoError(ht, err, "failed to add block header metadata")
+
+ err = chainimport.AddHeadersImportMetadata(
+ filterImportPath, chaincfg.RegressionNetParams.Net,
+ 0, headerfs.RegularFilter, 0,
+ )
+ require.NoError(ht, err, "failed to add filter header metadata")
+
+ // Shut down the reference node to free resources.
+ ht.Shutdown(refNode)
+
+ // Start a new node configured to import headers from the prepared
+ // files. The node imports the headers from file before falling back
+ // to P2P sync for any remaining blocks.
+ importArgs := []string{
+ "--neutrino.blockheaderssource=" + blockImportPath,
+ "--neutrino.filterheaderssource=" + filterImportPath,
+ }
+ importNode := ht.NewNode("Import", importArgs)
+
+ // Verify the import node synced to the chain.
+ importInfo := importNode.RPC.GetInfo()
+ require.True(ht, importInfo.SyncedToChain,
+ "import node should be synced to chain")
+ require.GreaterOrEqual(
+ ht, importInfo.BlockHeight, bestHeight,
+ "import node should have at least the same height as "+
+ "the reference node",
+ )
+
+ // Mine additional blocks using the miner directly and verify the
+ // import node picks them up via P2P sync (hybrid import + P2P
+ // sync). We use the miner directly because MineBlocks asserts all
+ // active nodes are synced after each block, which can race with
+ // neutrino's P2P sync.
+ const additionalBlocks = 10
+ ht.Miner().MineBlocks(additionalBlocks)
+
+ // Wait for the import node to see the new blocks.
+ expectedHeight := int32(bestHeight) + additionalBlocks
+ ht.WaitForNodeBlockHeight(importNode, expectedHeight)
+}
+
+// copyFile copies a file from src to dst.
+func copyFile(ht *lntest.HarnessTest, src, dst string) {
+ srcFile, err := os.Open(src)
+ require.NoError(ht, err, "failed to open source file: %s", src)
+ defer srcFile.Close()
+
+ dstFile, err := os.Create(dst)
+ require.NoError(ht, err, "failed to create dest file: %s", dst)
+ defer dstFile.Close()
+
+ _, err = io.Copy(dstFile, srcFile)
+ require.NoError(ht, err, "failed to copy file")
+
+ err = dstFile.Sync()
+ require.NoError(ht, err, "failed to sync dest file")
+}
Why this scored 15/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.