Skip to content

Commit 40bf583

Browse files
committed
feat: Add mainnet e2e tests and server entry point
1 parent 1fc9c12 commit 40bf583

5 files changed

Lines changed: 722 additions & 64 deletions

File tree

.github/workflows/e2e-mainnet.yaml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: e2e-mainnet
2+
3+
on:
4+
push:
5+
branches:
6+
- '**'
7+
paths:
8+
- 'neutrino_server/**'
9+
- '.github/workflows/e2e-mainnet.yaml'
10+
pull_request:
11+
paths:
12+
- 'neutrino_server/**'
13+
- '.github/workflows/e2e-mainnet.yaml'
14+
workflow_dispatch:
15+
inputs:
16+
min_block_height:
17+
description: 'Minimum block height to sync before running tests'
18+
required: false
19+
default: '100000'
20+
sync_timeout:
21+
description: 'Maximum sync timeout (e.g., 15m, 30m, 1h)'
22+
required: false
23+
default: '15m'
24+
25+
# Only run one e2e test at a time
26+
concurrency:
27+
group: e2e-mainnet
28+
cancel-in-progress: false
29+
30+
jobs:
31+
e2e-mainnet:
32+
runs-on: ubuntu-latest
33+
timeout-minutes: 30
34+
35+
steps:
36+
- uses: actions/checkout@v4
37+
38+
- name: Set up Go
39+
uses: actions/setup-go@v5
40+
with:
41+
go-version: '1.21'
42+
cache-dependency-path: neutrino_server/go.sum
43+
44+
- name: Install dependencies
45+
working-directory: neutrino_server
46+
run: go mod download
47+
48+
- name: Run mainnet e2e tests
49+
working-directory: neutrino_server
50+
run: |
51+
go test -tags=e2e -v -timeout 30m ./e2e/...
52+
env:
53+
# These can be used by the test if we add env var support
54+
MIN_BLOCK_HEIGHT: ${{ github.event.inputs.min_block_height || '100000' }}
55+
SYNC_TIMEOUT: ${{ github.event.inputs.sync_timeout || '15m' }}
56+
57+
- name: Upload test logs on failure
58+
if: failure()
59+
uses: actions/upload-artifact@v4
60+
with:
61+
name: e2e-logs
62+
path: |
63+
neutrino_server/e2e/*.log
64+
retention-days: 7

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
- Main entry point (`cmd/neutrinod/main.go`) for building the standalone binary
12+
- Mainnet end-to-end tests (`e2e/mainnet_test.go`) that:
13+
- Build and run the actual neutrinod binary against mainnet in isolation
14+
- Use random available port to avoid conflicts with running instances
15+
- Create temporary data directory for each test run
16+
- Wait for blockchain sync to at least height 100,000
17+
- Verify API endpoints with real blockchain data (genesis block, block 100000, etc.)
18+
- Test address watching and UTXO queries with historical Bitcoin addresses
19+
- Properly cleanup server process and temporary files after tests
20+
- GitHub workflow for automated mainnet e2e tests (`.github/workflows/e2e-mainnet.yaml`)
21+
- Runs on pushes to any branch that modify neutrino_server files
22+
- Can be triggered manually with configurable sync parameters
23+
24+
### Changed
25+
- README updated with real Bitcoin addresses for examples:
26+
- Satoshi's address from block 9: `12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S`
27+
- Hal Finney's address (first BTC recipient): `1Q2TWHE3GMdB6BZKafqwxXtWAWgFt5Jvm3`
28+
- README now includes e2e test documentation with usage instructions
29+
- Documented the need for `-count=1` flag to disable Go test caching for fresh e2e runs
30+
1031
### Fixed
1132
- Removed unnecessary nil check in rescan_test.go flagged by staticcheck SA4031
1233
- Release workflow now requires pre-commit and test checks to pass before creating release

README.md

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,32 +153,35 @@ Response:
153153
Add an address to watch for transactions:
154154

155155
```bash
156+
# Watch Satoshi's known address from block 9
156157
curl -X POST http://localhost:8334/v1/watch/address \
157158
-H "Content-Type: application/json" \
158-
-d '{"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"}'
159+
-d '{"address": "12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S"}'
159160
```
160161

161162
### Get UTXOs
162163

163164
Query UTXOs for a list of addresses:
164165

165166
```bash
167+
# Query UTXOs for historical Bitcoin addresses
168+
# (requires prior rescan to populate UTXO set)
166169
curl -X POST http://localhost:8334/v1/utxos \
167170
-H "Content-Type: application/json" \
168-
-d '{"addresses": ["bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"]}'
171+
-d '{"addresses": ["12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", "1Q2TWHE3GMdB6BZKafqwxXtWAWgFt5Jvm3"]}'
169172
```
170173

171174
Response:
172175
```json
173176
{
174177
"utxos": [
175178
{
176-
"txid": "a7c4d8e2f5b9c3e6f8a1d4b7e9c2f5a8b3d6e9f2c5a8b1d4e7f9c2e5a8b3d6e9",
179+
"txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
177180
"vout": 0,
178-
"value": 100000,
179-
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
180-
"scriptpubkey": "0014...",
181-
"height": 820000
181+
"value": 5000000000,
182+
"address": "1Q2TWHE3GMdB6BZKafqwxXtWAWgFt5Jvm3",
183+
"scriptpubkey": "76a914...",
184+
"height": 170
182185
}
183186
]
184187
}
@@ -189,11 +192,12 @@ Response:
189192
Trigger a blockchain rescan from a specific height:
190193

191194
```bash
195+
# Rescan from block 0 for Satoshi's address
192196
curl -X POST http://localhost:8334/v1/rescan \
193197
-H "Content-Type: application/json" \
194198
-d '{
195-
"start_height": 800000,
196-
"addresses": ["bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"]
199+
"start_height": 0,
200+
"addresses": ["12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S"]
197201
}'
198202
```
199203

@@ -244,8 +248,22 @@ go test -v -race -coverprofile=coverage.out ./...
244248

245249
# View coverage report
246250
go tool cover -html=coverage.out
251+
252+
# Run mainnet e2e tests (requires network access, ~15-20 min)
253+
# Note: Use -count=1 to disable test caching and force a fresh run
254+
go test -tags=e2e -v -count=1 -timeout 30m ./e2e/...
247255
```
248256

257+
The e2e tests will:
258+
1. Build and start the neutrinod binary on a random available port
259+
2. Use a fresh temporary data directory for each run
260+
3. Connect to mainnet peers and sync block headers/filters
261+
4. Verify API endpoints with real blockchain data (genesis block, block 100000, etc.)
262+
5. Test address watching and UTXO queries
263+
6. Properly cleanup the server process and temporary files
264+
265+
**Note:** Go caches test results by default. To force a fresh run every time, use the `-count=1` flag as shown above.
266+
249267
### Pre-commit Hooks
250268

251269
Install pre-commit hooks for code quality:
Lines changed: 53 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
/*
2-
Neutrino server main entry point.
3-
4-
This server wraps the lightninglabs/neutrino BIP157/BIP158 light client
5-
with a REST API for use by JoinMarket maker/taker clients.
2+
neutrinod is a standalone REST API server for neutrino, a privacy-preserving
3+
Bitcoin light client using BIP157/BIP158 compact block filters.
64
*/
75
package main
86

97
import (
8+
"context"
109
"flag"
1110
"fmt"
1211
"net/http"
@@ -23,65 +22,49 @@ import (
2322
)
2423

2524
var (
26-
// Build-time variables (set via -ldflags)
27-
version = "dev"
28-
buildTime = "unknown"
29-
commit = "unknown"
30-
31-
// Upstream neutrino version
32-
neutrinoVersion = "v0.16.0"
33-
34-
// Command-line flags
35-
network = flag.String("network", "mainnet", "Bitcoin network (mainnet, testnet, regtest, signet)")
36-
dataDir = flag.String("datadir", "/data/neutrino", "Data directory for headers and filters")
37-
listenAddr = flag.String("listen", "0.0.0.0:8334", "REST API listen address")
38-
torProxy = flag.String("proxy", "", "Tor SOCKS5 proxy address (e.g., 127.0.0.1:9050)")
39-
connectPeers = flag.String("connect", "", "Comma-separated list of peers to connect to")
40-
logLevel = flag.String("loglevel", "info", "Log level (trace, debug, info, warn, error)")
41-
maxPeers = flag.Int("maxpeers", 8, "Maximum number of peers to connect to")
42-
banDuration = flag.Duration("banduration", 24*time.Hour, "Duration to ban misbehaving peers")
43-
filterCacheSize = flag.Int("filtercache", 4096, "Size of filter cache (number of filters)")
44-
showVersion = flag.Bool("version", false, "Show version information and exit")
25+
// Version is set at build time via ldflags
26+
version = "dev"
4527
)
4628

4729
func main() {
30+
// Parse command line flags
31+
network := flag.String("network", getEnv("NETWORK", "mainnet"), "Bitcoin network (mainnet, testnet, regtest, signet)")
32+
listen := flag.String("listen", getEnv("LISTEN_ADDR", "0.0.0.0:8334"), "REST API listen address")
33+
dataDir := flag.String("datadir", getEnv("DATA_DIR", "/data/neutrino"), "Data directory for headers and filters")
34+
logLevel := flag.String("loglevel", getEnv("LOG_LEVEL", "info"), "Log level (trace, debug, info, warn, error)")
35+
connectPeers := flag.String("connect", getEnv("CONNECT_PEERS", ""), "Comma-separated list of peers to connect to")
36+
showVersion := flag.Bool("version", false, "Show version and exit")
4837
flag.Parse()
4938

50-
// Show version if requested
5139
if *showVersion {
52-
fmt.Printf("Neutrino API Server\n")
53-
fmt.Printf(" Version: %s\n", version)
54-
fmt.Printf(" Neutrino: %s\n", neutrinoVersion)
55-
fmt.Printf(" Build time: %s\n", buildTime)
56-
fmt.Printf(" Commit: %s\n", commit)
57-
fmt.Printf(" Go version: %s\n", "go1.21")
40+
fmt.Printf("neutrinod %s\n", version)
5841
os.Exit(0)
5942
}
6043

61-
// Setup logging
44+
// Set up logging
6245
backend := btclog.NewBackend(os.Stdout)
6346
logger := backend.Logger("MAIN")
6447
level, _ := btclog.LevelFromString(*logLevel)
6548
logger.SetLevel(level)
6649

67-
logger.Infof("Starting Neutrino API Server %s (neutrino %s)", version, neutrinoVersion)
68-
logger.Infof("Listening on %s for %s network", *listenAddr, *network)
50+
logger.Infof("Starting neutrinod %s", version)
51+
logger.Infof("Network: %s", *network)
52+
logger.Infof("Listen address: %s", *listen)
53+
logger.Infof("Data directory: %s", *dataDir)
6954

70-
// Create data directory
55+
// Ensure data directory exists
7156
if err := os.MkdirAll(*dataDir, 0750); err != nil {
7257
logger.Errorf("Failed to create data directory: %v", err)
7358
os.Exit(1)
7459
}
7560

76-
// Initialize neutrino node
61+
// Create neutrino node
7762
nodeConfig := &neutrino.Config{
7863
Network: *network,
7964
DataDir: *dataDir,
80-
TorProxy: *torProxy,
8165
ConnectPeers: *connectPeers,
82-
MaxPeers: *maxPeers,
83-
BanDuration: *banDuration,
84-
FilterCacheSize: *filterCacheSize,
66+
MaxPeers: 8,
67+
FilterCacheSize: 5000,
8568
Logger: backend,
8669
LogLevel: *logLevel,
8770
}
@@ -98,43 +81,58 @@ func main() {
9881
os.Exit(1)
9982
}
10083

101-
// Setup REST API
84+
// Create API handler
85+
apiLogger := backend.Logger("API")
86+
apiLogger.SetLevel(level)
87+
handler := api.NewHandler(node, apiLogger)
88+
89+
// Set up router
10290
router := mux.NewRouter()
103-
apiHandler := api.NewHandler(node, logger)
104-
apiHandler.RegisterRoutes(router)
91+
handler.RegisterRoutes(router)
10592

10693
// Create HTTP server
10794
server := &http.Server{
108-
Addr: *listenAddr,
95+
Addr: *listen,
10996
Handler: router,
11097
ReadTimeout: 30 * time.Second,
111-
WriteTimeout: 60 * time.Second,
112-
IdleTimeout: 120 * time.Second,
98+
WriteTimeout: 30 * time.Second,
99+
IdleTimeout: 60 * time.Second,
113100
}
114101

115-
// Start HTTP server in goroutine
102+
// Start HTTP server in background
116103
go func() {
117-
logger.Infof("REST API listening on %s", *listenAddr)
118-
if err := server.ListenAndServe(); err != http.ErrServerClosed {
104+
logger.Infof("HTTP server listening on %s", *listen)
105+
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
119106
logger.Errorf("HTTP server error: %v", err)
120107
}
121108
}()
122109

123110
// Wait for shutdown signal
124-
sigChan := make(chan os.Signal, 1)
125-
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
126-
<-sigChan
111+
quit := make(chan os.Signal, 1)
112+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
113+
<-quit
127114

128115
logger.Info("Shutting down...")
129116

130-
// Graceful shutdown
131-
if err := server.Close(); err != nil {
132-
logger.Warnf("HTTP server shutdown error: %v", err)
117+
// Graceful shutdown with timeout
118+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
119+
defer cancel()
120+
121+
if err := server.Shutdown(ctx); err != nil {
122+
logger.Errorf("HTTP server shutdown error: %v", err)
133123
}
134124

135125
if err := node.Stop(); err != nil {
136-
logger.Warnf("Neutrino node shutdown error: %v", err)
126+
logger.Errorf("Neutrino node shutdown error: %v", err)
137127
}
138128

139129
logger.Info("Shutdown complete")
140130
}
131+
132+
// getEnv returns the value of an environment variable or a default value.
133+
func getEnv(key, defaultValue string) string {
134+
if value := os.Getenv(key); value != "" {
135+
return value
136+
}
137+
return defaultValue
138+
}

0 commit comments

Comments
 (0)