Skip to content

Commit 3a5340a

Browse files
committed
feat: Add addPeers setting to neutrino server
1 parent 820d10a commit 3a5340a

7 files changed

Lines changed: 127 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `addPeers` setting, similar to `connectPeers`, that allows specifying peers to connect to without disabling peer discovery.
13+
1014
## [0.7.0] - 2026-03-11
1115

1216
### Added

neutrino_server/cmd/neutrinod/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func main() {
3333
dataDir := flag.String("datadir", getEnv("DATA_DIR", "/data/neutrino"), "Data directory for headers and filters")
3434
logLevel := flag.String("loglevel", getEnv("LOG_LEVEL", "info"), "Log level (trace, debug, info, warn, error)")
3535
connectPeers := flag.String("connect", getEnv("CONNECT_PEERS", ""), "Comma-separated list of peers to connect to")
36+
addPeers := flag.String("addpeer", getEnv("ADD_PEERS", ""), "Comma-separated list of peers to add while still allowing discovery")
3637
torProxy := flag.String("torproxy", getEnv("TOR_PROXY", ""), "Tor SOCKS5 proxy address (e.g., 127.0.0.1:9050)")
3738
showVersion := flag.Bool("version", false, "Show version and exit")
3839
flag.Parse()
@@ -68,6 +69,7 @@ func main() {
6869
DataDir: *dataDir,
6970
TorProxy: *torProxy,
7071
ConnectPeers: *connectPeers,
72+
AddPeers: *addPeers,
7173
MaxPeers: 8,
7274
Logger: backend,
7375
LogLevel: *logLevel,

neutrino_server/internal/api/handler.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type NodeInterface interface {
3030
WatchAddress(address string) error
3131
Rescan(startHeight int32, addresses []string) error
3232
IsRescanInProgress() bool
33+
RescanStatus() neutrino.RescanStatus
3334
}
3435

3536
// Handler provides REST API endpoints for the neutrino node.
@@ -329,9 +330,14 @@ func (h *Handler) handleRescan(w http.ResponseWriter, r *http.Request) {
329330

330331
// Rescan status endpoint
331332
func (h *Handler) handleGetRescanStatus(w http.ResponseWriter, r *http.Request) {
332-
inProgress := h.node.IsRescanInProgress()
333-
h.jsonResponse(w, map[string]bool{
334-
"in_progress": inProgress,
333+
status := h.node.RescanStatus()
334+
h.jsonResponse(w, map[string]any{
335+
"in_progress": status.InProgress,
336+
"last_started": status.LastStarted,
337+
"last_finished": status.LastFinished,
338+
"last_start_height": status.LastStartHeight,
339+
"last_scanned_tip": status.LastScannedTip,
340+
"last_error": status.LastError,
335341
})
336342
}
337343

neutrino_server/internal/api/handler_test.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ func (m *mockNode) IsRescanInProgress() bool {
7474
return false
7575
}
7676

77+
func (m *mockNode) RescanStatus() neutrino.RescanStatus {
78+
return neutrino.RescanStatus{}
79+
}
80+
7781
func TestHandleGetStatus(t *testing.T) {
7882
backend := btclog.NewBackend(os.Stdout)
7983
logger := backend.Logger("TEST")
@@ -554,12 +558,30 @@ func TestHandleGetRescanStatus_NotInProgress(t *testing.T) {
554558
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
555559
}
556560

557-
var response map[string]bool
561+
var response map[string]any
558562
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
559563
t.Fatalf("could not decode response: %v", err)
560564
}
561565

562-
if response["in_progress"] {
566+
inProgress, ok := response["in_progress"].(bool)
567+
if !ok {
568+
t.Fatalf("expected boolean in_progress, got %T", response["in_progress"])
569+
}
570+
571+
if inProgress {
563572
t.Error("expected in_progress=false")
564573
}
574+
575+
if _, ok := response["last_started"]; !ok {
576+
t.Error("expected last_started in response")
577+
}
578+
if _, ok := response["last_finished"]; !ok {
579+
t.Error("expected last_finished in response")
580+
}
581+
if _, ok := response["last_start_height"]; !ok {
582+
t.Error("expected last_start_height in response")
583+
}
584+
if _, ok := response["last_scanned_tip"]; !ok {
585+
t.Error("expected last_scanned_tip in response")
586+
}
565587
}

neutrino_server/internal/neutrino/node.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Config struct {
3535
DataDir string
3636
TorProxy string
3737
ConnectPeers string
38+
AddPeers string
3839
MaxPeers int
3940
BanDuration time.Duration
4041
FilterCacheSize int
@@ -152,6 +153,19 @@ func (n *Node) Start() error {
152153
FilterCacheSize: uint64(n.config.FilterCacheSize),
153154
}
154155

156+
// Add peers if specified
157+
if n.config.AddPeers != "" {
158+
peers := strings.Split(n.config.AddPeers, ",")
159+
for _, peer := range peers {
160+
peer = strings.TrimSpace(peer)
161+
if peer != "" {
162+
n.logger.Infof("Adding preferred peer: %s", peer)
163+
neutrinoConfig.AddPeers = append(neutrinoConfig.AddPeers, peer)
164+
}
165+
}
166+
n.logger.Infof("Total add peers configured: %d", len(neutrinoConfig.AddPeers))
167+
}
168+
155169
// Add peers if specified
156170
if n.config.ConnectPeers != "" {
157171
peers := strings.Split(n.config.ConnectPeers, ",")
@@ -168,7 +182,11 @@ func (n *Node) Start() error {
168182
// Add DNS seeds if no connect peers specified
169183
if len(neutrinoConfig.ConnectPeers) == 0 {
170184
seeds := getDNSSeeds(n.config.Network)
171-
neutrinoConfig.AddPeers = seeds
185+
if len(neutrinoConfig.AddPeers) > 0 {
186+
neutrinoConfig.AddPeers = append(neutrinoConfig.AddPeers, seeds...)
187+
} else {
188+
neutrinoConfig.AddPeers = seeds
189+
}
172190
n.logger.Infof("No connect peers specified, using %d DNS seeds", len(seeds))
173191
}
174192

@@ -376,6 +394,14 @@ func (n *Node) IsRescanInProgress() bool {
376394
return n.rescanMgr.IsRescanInProgress()
377395
}
378396

397+
// RescanStatus returns detailed rescan lifecycle status.
398+
func (n *Node) RescanStatus() RescanStatus {
399+
if n.rescanMgr == nil {
400+
return RescanStatus{}
401+
}
402+
return n.rescanMgr.GetRescanStatus()
403+
}
404+
379405
// UTXOSpendReport represents information about a UTXO.
380406
type UTXOSpendReport struct {
381407
// If the output is unspent, these fields are populated

neutrino_server/internal/neutrino/rescan.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"sync"
1111
"sync/atomic"
12+
"time"
1213

1314
"github.com/btcsuite/btcd/btcutil"
1415
"github.com/btcsuite/btcd/btcutil/gcs/builder"
@@ -32,6 +33,19 @@ type RescanManager struct {
3233
// rescanInProgress tracks the number of active rescans (atomic).
3334
// Non-zero means a rescan goroutine is running.
3435
rescanInProgress atomic.Int32
36+
37+
statusMu sync.RWMutex
38+
status RescanStatus
39+
}
40+
41+
// RescanStatus exposes rescan lifecycle details for API consumers.
42+
type RescanStatus struct {
43+
InProgress bool `json:"in_progress"`
44+
LastStarted int64 `json:"last_started"`
45+
LastFinished int64 `json:"last_finished"`
46+
LastStartHeight int32 `json:"last_start_height"`
47+
LastScannedTip int32 `json:"last_scanned_tip"`
48+
LastError string `json:"last_error,omitempty"`
3549
}
3650

3751
// NewRescanManager creates a new rescan manager.
@@ -104,6 +118,16 @@ func (r *RescanManager) IsRescanInProgress() bool {
104118
return r.rescanInProgress.Load() > 0
105119
}
106120

121+
// GetRescanStatus returns the current detailed rescan status.
122+
func (r *RescanManager) GetRescanStatus() RescanStatus {
123+
r.statusMu.RLock()
124+
defer r.statusMu.RUnlock()
125+
126+
status := r.status
127+
status.InProgress = r.IsRescanInProgress()
128+
return status
129+
}
130+
107131
// Rescan triggers a rescan from the given height for specified addresses.
108132
// This uses neutrino's block filter-based scanning.
109133
func (r *RescanManager) Rescan(startHeight int32, addresses []string) error {
@@ -129,6 +153,11 @@ func (r *RescanManager) Rescan(startHeight int32, addresses []string) error {
129153
}
130154

131155
r.logger.Infof("Starting rescan from height %d for %d addresses", startHeight, len(addrs))
156+
r.statusMu.Lock()
157+
r.status.LastStarted = time.Now().Unix()
158+
r.status.LastStartHeight = startHeight
159+
r.status.LastError = ""
160+
r.statusMu.Unlock()
132161

133162
// Mark rescan as in-progress so callers can poll /v1/rescan/status.
134163
r.rescanInProgress.Add(1)
@@ -141,7 +170,15 @@ func (r *RescanManager) Rescan(startHeight int32, addresses []string) error {
141170
}
142171

143172
// Scan blocks from startHeight to bestBlock.Height
144-
return r.scanBlocks(startHeight, bestBlock.Height, addrs)
173+
err = r.scanBlocks(startHeight, bestBlock.Height, addrs)
174+
r.statusMu.Lock()
175+
r.status.LastFinished = time.Now().Unix()
176+
r.status.LastScannedTip = bestBlock.Height
177+
if err != nil {
178+
r.status.LastError = err.Error()
179+
}
180+
r.statusMu.Unlock()
181+
return err
145182
}
146183

147184
// scanBlocks scans blocks in the given range for transactions matching the addresses.

neutrino_server/internal/neutrino/rescan_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,26 @@ func TestRescanNilChainService(t *testing.T) {
266266
t.Errorf("expected 'chain service not initialized', got '%s'", err.Error())
267267
}
268268
}
269+
270+
func TestGetRescanStatusDefaults(t *testing.T) {
271+
backend := btclog.NewBackend(nil)
272+
logger := backend.Logger("TEST")
273+
274+
mgr := &RescanManager{
275+
chainParams: &chaincfg.MainNetParams,
276+
logger: logger,
277+
watchedAddrs: make(map[string]btcutil.Address),
278+
utxoSet: make(map[string]UTXO),
279+
}
280+
281+
status := mgr.GetRescanStatus()
282+
if status.InProgress {
283+
t.Error("expected InProgress=false")
284+
}
285+
if status.LastStarted != 0 {
286+
t.Errorf("expected LastStarted=0, got %d", status.LastStarted)
287+
}
288+
if status.LastFinished != 0 {
289+
t.Errorf("expected LastFinished=0, got %d", status.LastFinished)
290+
}
291+
}

0 commit comments

Comments
 (0)