Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lnd config to sideload headers. #8580

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
6 changes: 4 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1260,8 +1260,10 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser,
"credentials for bitcoind: %v", err)
}
case neutrinoBackendName:
// No need to get RPC parameters.

err = cfg.NeutrinoMode.Validate()
if err != nil {
return nil, mkErr(err.Error())
}
case "nochainbackend":
// Nothing to configure, we're running without any chain
// backend whatsoever (pure signing mode).
Expand Down
54 changes: 54 additions & 0 deletions config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/lightninglabs/neutrino/sideload"
"io"
"io/ioutil"
"net"
"os"
Expand Down Expand Up @@ -1410,6 +1412,15 @@ func initNeutrinoBackend(ctx context.Context, cfg *Config, chainDir string,
PersistToDisk: cfg.NeutrinoMode.PersistFilters,
}

blkHdrSideload, err := parseSideloadOpt(
cfg.NeutrinoMode.BlkHdrSideloadOpt,
)
if err != nil {
return nil, nil, err
}

config.BlkHdrSideload = blkHdrSideload

neutrino.MaxPeers = 8
neutrino.BanDuration = time.Hour * 48
neutrino.UserAgentName = cfg.NeutrinoMode.UserAgentName
Expand Down Expand Up @@ -1438,6 +1449,49 @@ func initNeutrinoBackend(ctx context.Context, cfg *Config, chainDir string,
return neutrinoCS, cleanUp, nil
}

// parseSideloadOpt converts lncfg.SideloadOpt to neutrino.SideloadOpt.
func parseSideloadOpt(opt *lncfg.SideloadOpt) (*neutrino.SideloadOpt, error) {
if !opt.Enable {
return nil, nil
}

var reader io.ReadSeeker
if lnrpc.IsValidURL(opt.SourcePath) {
resp, err := lnrpc.FetchURL(opt.SourcePath)
if err != nil {
return nil, fmt.Errorf(
"failed to fetch URL source path for "+
"sideloading: %v", err)
}

// Create a bytes.Reader over the response body,
// which implements io.ReadSeeker.
reader = bytes.NewReader(resp)
} else {
file, err := os.Open(opt.SourcePath)
if err != nil {
return nil, fmt.Errorf(
"failed to open file for sideloading: %v", err)
}

reader = file
}

var source sideload.SourceType

err := source.UnmarshalText([]byte(opt.SourceType))
if err != nil {
return nil, err
}

return &neutrino.SideloadOpt{
SourceType: source,
Reader: reader,
SkipVerify: opt.SkipVerify,
SideloadRange: opt.SideloadRange,
}, nil
}

// parseHeaderStateAssertion parses the user-specified neutrino header state
// into a headerfs.FilterHeader.
func parseHeaderStateAssertion(state string) (*headerfs.FilterHeader, error) {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,5 @@ replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-d
go 1.19

retract v0.0.2

replace github.com/lightninglabs/neutrino => github.com/chinwendu20/neutrino v0.0.0-20240325184234-41da32452de5
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chinwendu20/neutrino v0.0.0-20240325184234-41da32452de5 h1:K+H1psGIUP9Xx43TkfkT5c22928uqGiq2DJ2tnGhuhk=
github.com/chinwendu20/neutrino v0.0.0-20240325184234-41da32452de5/go.mod h1:q5cAgGBV6xn6yWE0TnHG/1911f6aXjSRmAzgSucbaZQ=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
Expand Down Expand Up @@ -408,8 +410,6 @@ github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
github.com/lib/pq v1.10.4/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.16.0 h1:YNTQG32fPR/Zg0vvJVI65OBH8l3U18LSXXtX91hx0q0=
github.com/lightninglabs/neutrino v0.16.0/go.mod h1:x3OmY2wsA18+Kc3TSV2QpSUewOCiscw2mKpXgZv2kZk=
github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g=
github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo=
github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display h1:pRdza2wleRN1L2fJXd6ZoQ9ZegVFTAb2bOQfruJPKcY=
Expand Down
1 change: 1 addition & 0 deletions itest/lnd_neutrino_sideload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package itest
67 changes: 67 additions & 0 deletions itest/lnd_test.go
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set up this test so that it does not connect to the miner so that I can be sure that blockheaders are fed through the sideload source. The problem with that is that cfheaders would not be updated and lnd waits for the chain to be synced before starting up fully so this test would not work.

I do not know if there are any suggestions around this.

Or maybe I would continue with this when I include cfheaders sideloading in the neutrino side (which I would do after I get feedback and clarification on the latest iteration)

CC: @Roasbeef

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe I would continue with this when I include cfheaders sideloading in the neutrino side (which I would do after I get feedback and clarification on the latest iteration)

Yeah we want both the regular block headers and cfheaders.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/integration/rpctest"
"github.com/lightninglabs/neutrino/sideload"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/port"
Expand Down Expand Up @@ -102,6 +104,14 @@
)
defer harnessTest.Stop()

if harnessTest.IsNeutrinoSideloadTest() {
t.Run("test neutrino sideload", func(t *testing.T) {
testNeutrinoSideload(t, harnessTest)
})

return
}

// Setup standby nodes, Alice and Bob, which will be alive and shared
// among all the test cases.
harnessTest.SetupStandbyNodes()
Expand Down Expand Up @@ -156,6 +166,63 @@
"cases, end height: %d\n", trancheIndex, len(testCases), height)
}

func testNeutrinoSideload(t *testing.T, harness *lntest.HarnessTest) {
if !harness.IsNeutrinoSideloadTest() {
return
}

testCfg := &sideload.TestCfg{
StartHeight: 0,
EndHeight: 1000,
Net: harnessNetParams.Net,
DataType: sideload.BlockHeaders,
}

headers := lntest.GenerateBlockHeaderBytes(
uint32(testCfg.EndHeight-testCfg.StartHeight),
harness.Miner.Harness, t,
)

reader := sideload.GenerateEncodedBinaryReader(
t, testCfg, headers,
)

tempFile, err := os.CreateTemp("", "neutrino")
require.NoError(t, err)

defer func() {
err = tempFile.Close()
require.NoError(t, err)

err = os.Remove(tempFile.Name())
require.NoError(t, err)
}()

_, err = io.Copy(tempFile, reader)
require.NoError(t, err)

_, err = reader.Seek(0, io.SeekStart)
require.NoError(t, err)

extraArgs := []string{
"--neutrino.sideload.enable",
"--neutrino.sideload.sourceType=binary",
"--neutrino.sideload.sourcePath=" + tempFile.Name(),
"--neutrino.sideload.skipVerify",
}

obiNode := harness.NewNode("obi", extraArgs)
d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (bitcoind-notxindex, backend="bitcoind notxindex")

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (bitcoind, backend=bitcoind)

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (bitcoind-etcd, backend=bitcoind dbbackend=etcd)

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (bitcoind-postgres, backend=bitcoind dbbackend=postgres)

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (bitcoind-postgres-nativesql, backend=bitcoind dbbackend=postgres nativesql=true)

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run macOS itest

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (bitcoind-rpcpolling, backend="bitcoind rpcpolling")

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (bitcoind-sqlite, backend=bitcoind dbbackend=sqlite)

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (bitcoind-sqlite-nativesql, backend=bitcoind dbbackend=sqlite nativesql=true)

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (btcd, backend=btcd)

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run ubuntu itests (neutrino, backend=neutrino)

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run windows itest

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / check commits

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run unit tests (btcd unit-cover)

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run unit tests (unit tags="kvdb_sqlite")

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run unit tests (unit tags="kvdb_postgres")

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run unit tests (unit tags="kvdb_etcd")

undefined: d

Check failure on line 215 in itest/lnd_test.go

View workflow job for this annotation

GitHub Actions / run unit tests (btcd unit-race)

undefined: d

request := chainrpc.GetBlockHashRequest{
BlockHeight: int64(testCfg.EndHeight),
}

resp := obiNode.RPC.GetBlockHash(&request)

require.NotZero(t, len(resp.BlockHash))
}

// getTestCaseSplitTranche returns the sub slice of the test cases that should
// be run as the current split tranche as well as the index and slice offset of
// the tranche.
Expand Down
79 changes: 78 additions & 1 deletion lncfg/neutrino.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
package lncfg

import "time"
import (
"time"

"github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/lnrpc"
)

// SideloadOpt holds the configuration options for sideloading headers in
// neutrino.
//
//nolint:lll
type SideloadOpt struct {
Enable bool `long:"enable" description:"Indicates sideloading is enabled"`
SourceType string `long:"sourceType" description:"Indicates the encoding format of the sideload source" choice:"binary"`
SourcePath string `long:"sourcePath" description:"Indicates the path to the sideload source"`
SkipVerify bool `long:"skipVerify" description:"Indicates if to verify headers while sideleoading"`
SideloadRange uint32 `long:"range" description:"Indicates how much headers should be read from the source at a time"`
}

// Neutrino holds the configuration options for the daemon's connection to
// neutrino.
Expand All @@ -19,4 +36,64 @@ type Neutrino struct {
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)"`
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."`
BlkHdrSideloadOpt *SideloadOpt `group:"sideload" namespace:"sideload"`
}

// Validate checks a Neutrino instance's config for correctness, returning nil
// if the instance is uninitialized or its sideload options are valid. Errors
// from sideload option validation are returned.
func (n *Neutrino) Validate() error {
if n == nil {
// Consider nil instance as uninitialized; no validation needed.
return nil
}

// Validate sideload options.
err := n.BlkHdrSideloadOpt.Validate()
if err != nil {
return err // Return validation errors.
}

return nil
}

// Validate checks SideloadOpt for required source type and path, returning
// errors if they're missing or invalid.
func (s *SideloadOpt) Validate() error {
if !s.Enable {
return nil
}

// Require source type for sideloading.
if s.SourceType == "" {
return errors.New("source type required for sideloading " +
"headers.")
}

// Require source path for sideloading.
if s.SourcePath == "" {
return errors.New("source path required for sideloading " +
"headers.")
}

// Check source path validity.
if !validatePath(s.SourcePath) {
return errors.New("invalid source path")
}

return nil
}

// validatePath verifies if a path is a valid URL or an existing file, returning
// true if valid.
func validatePath(path string) bool {
// Check path validity as URL.
if lnrpc.IsValidURL(path) {
return true
}

// Clean/expand the path; check file existence.
path = CleanAndExpandPath(path)

return lnrpc.FileExists(path)
}
15 changes: 0 additions & 15 deletions lnrpc/file_utils.go

This file was deleted.

54 changes: 54 additions & 0 deletions lnrpc/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package lnrpc

import (
"fmt"
"io"
"net/http"
"net/url"
"os"
)

// FileExists reports whether the named file or directory exists.
func FileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}

// IsValidURL checks if the given text is a valid URL.
// Passed text must have a scheme of "http" or "https" format to be valid.
func IsValidURL(text string) bool {
parsedURL, err := url.Parse(text)
if err != nil {
return false
}

// Consider it a valid URL if it has a scheme of http or https.
return parsedURL.Scheme == "http" || parsedURL.Scheme == "https"
}

// FetchURL returns the content fetched from the specified URL in bytes.
func FetchURL(url string) ([]byte, error) {
// Perform the HTTP GET request.
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("fetching URL failed: %w", err)
}
defer resp.Body.Close()

// Check the HTTP response status.
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %d", resp.StatusCode)
}

// Read the response body.
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body failed: %w", err)
}

return body, nil
}
14 changes: 14 additions & 0 deletions lntest/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@ func (h *HarnessTest) ChainBackendName() string {
return h.manager.chainBackend.Name()
}

func (h *HarnessTest) IsNeutrinoSideloadTest() bool {
if !(h.manager.chainBackend.Name() == NeutrinoBackendName) {
return false
}

for _, s := range h.manager.chainBackend.GenArgs() {
if s == "--neutrino.connect="+h.Miner.P2PAddress() {
return false
}
}

return true
}

// Context returns the run context used in this test. Usaually it should be
// managed by the test itself otherwise undefined behaviors will occur. It can
// be used, however, when a test needs to have its own context being managed
Expand Down