mirror of
https://github.com/muun/recovery.git
synced 2025-11-10 05:59:44 -05:00
Release v2.2.0
This commit is contained in:
341
survey/survey.go
Normal file
341
survey/survey.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/muun/recovery/electrum"
|
||||
)
|
||||
|
||||
type Survey struct {
|
||||
config *Config
|
||||
tasks chan *surveyTask
|
||||
taskWg sync.WaitGroup
|
||||
results chan *Result
|
||||
visited map[string]bool
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
InitialServers []string
|
||||
Workers int
|
||||
SpeedTestDuration time.Duration
|
||||
SpeedTestBatchSize int
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Server string
|
||||
FromPeer string
|
||||
IsWorthy bool
|
||||
Err error
|
||||
Impl string
|
||||
Version string
|
||||
TimeToConnect time.Duration
|
||||
Speed int
|
||||
BatchSupport bool
|
||||
peers []string
|
||||
}
|
||||
|
||||
type surveyTask struct {
|
||||
server string
|
||||
fromPeer string
|
||||
}
|
||||
|
||||
// Values to check whether we're in the same chain (in a previous version, SV servers snuck in)
|
||||
var mainnetSomeTx = "1712426823cc94935287a6834f7982723fbb5c808cbe00ec2cf3f582582be4c5"
|
||||
var mainnetGenesisHash = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
|
||||
|
||||
func NewSurvey(config *Config) *Survey {
|
||||
return &Survey{
|
||||
config: config,
|
||||
tasks: make(chan *surveyTask),
|
||||
results: make(chan *Result),
|
||||
visited: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Survey) Run() []*Result {
|
||||
// Add initial tasks:
|
||||
for _, server := range s.config.InitialServers {
|
||||
s.addTask(server, "")
|
||||
}
|
||||
|
||||
// Start collecting results in background:
|
||||
results := []*Result{}
|
||||
go s.startCollect(&results)
|
||||
|
||||
// Launch workers to process tasks and send back results:
|
||||
for i := 0; i < s.config.Workers; i++ {
|
||||
go s.startWorker()
|
||||
}
|
||||
|
||||
// Wait until there's no tasks left, and signal everyone to stop:
|
||||
s.taskWg.Wait()
|
||||
close(s.tasks)
|
||||
close(s.results)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].IsBetterThan(results[j])
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s *Survey) addTask(server string, fromPeer string) {
|
||||
task := &surveyTask{server, fromPeer}
|
||||
|
||||
if _, ok := s.visited[task.server]; ok {
|
||||
return
|
||||
}
|
||||
s.visited[task.server] = true
|
||||
|
||||
s.taskWg.Add(1)
|
||||
go func() { s.tasks <- task }() // scheduling tasks is non-blocking for users of the type
|
||||
}
|
||||
|
||||
func (s *Survey) notifyResult(result *Result) {
|
||||
s.results <- result
|
||||
s.taskWg.Done()
|
||||
}
|
||||
|
||||
func (s *Survey) startCollect(resultsRef *[]*Result) {
|
||||
for result := range s.results {
|
||||
*resultsRef = append(*resultsRef, result)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Survey) startWorker() {
|
||||
for task := range s.tasks {
|
||||
log("• %s\n", task.server)
|
||||
|
||||
result := s.processTask(task)
|
||||
|
||||
if result.Err != nil {
|
||||
log("✕ %s\n", task.server)
|
||||
} else {
|
||||
log("✓ %s\n", task.server)
|
||||
}
|
||||
|
||||
s.notifyResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Survey) processTask(task *surveyTask) *Result {
|
||||
|
||||
// We're going to perform a number of tests an measurements:
|
||||
//
|
||||
// 1. How much time does it take to establish a connection?
|
||||
// 2. Does the server support batching?
|
||||
// 3. Is the server willing to share its peers? If so, crawl.
|
||||
// 4. How many requests can the server handle in a given time interval?
|
||||
// 5. Did the server fail at any point during testing?
|
||||
//
|
||||
// Each test can result in a closed socket (since Electrum communicates errors by slapping you
|
||||
// in the face with no explanation), so we'll be connecting separately for each attempt.
|
||||
//
|
||||
// When a testing method returns an error, it means the server failed completely and we couldn't
|
||||
// obtain meaningful results (while some internal errors in a test are expected and handled).
|
||||
|
||||
impl, version, timeToConnect, err := testConnection(task)
|
||||
if err != nil {
|
||||
return &Result{Server: task.server, Err: err}
|
||||
}
|
||||
|
||||
isBitcoinMainnet, err := testBitcoinMainnet(task)
|
||||
if err != nil || !isBitcoinMainnet {
|
||||
return &Result{Server: task.server, Err: fmt.Errorf("not on Bitcoin mainnet: %w", err)}
|
||||
}
|
||||
|
||||
batchSupport, err := testBatchSupport(task)
|
||||
if err != nil {
|
||||
return &Result{Server: task.server, Err: err}
|
||||
}
|
||||
|
||||
speed, err := s.measureSpeed(task)
|
||||
if err != nil {
|
||||
return &Result{Server: task.server, Err: err}
|
||||
}
|
||||
|
||||
peers, err := getPeers(task)
|
||||
if err != nil {
|
||||
return &Result{Server: task.server, Err: err}
|
||||
}
|
||||
|
||||
for _, peer := range peers {
|
||||
if strings.Contains(peer, ".onion:") {
|
||||
continue
|
||||
}
|
||||
|
||||
s.addTask(peer, task.server)
|
||||
}
|
||||
|
||||
isWorthy := err == nil &&
|
||||
batchSupport &&
|
||||
timeToConnect.Seconds() < 5.0 &&
|
||||
speed >= int(s.config.SpeedTestDuration.Seconds())
|
||||
|
||||
return &Result{
|
||||
IsWorthy: isWorthy,
|
||||
Server: task.server,
|
||||
FromPeer: task.fromPeer,
|
||||
Impl: impl,
|
||||
Version: version,
|
||||
TimeToConnect: timeToConnect,
|
||||
BatchSupport: batchSupport,
|
||||
Speed: speed,
|
||||
peers: peers,
|
||||
}
|
||||
}
|
||||
|
||||
// testConnection returns the server implementation, protocol version and time to connect
|
||||
func testConnection(task *surveyTask) (string, string, time.Duration, error) {
|
||||
client := electrum.NewClient()
|
||||
|
||||
start := time.Now()
|
||||
err := client.Connect(task.server)
|
||||
if err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
|
||||
return client.ServerImpl, client.ProtoVersion, time.Since(start), nil
|
||||
}
|
||||
|
||||
// testsBlockchain returns whether this server is operating on Bitcoin mainnet
|
||||
func testBitcoinMainnet(task *surveyTask) (bool, error) {
|
||||
client := electrum.NewClient()
|
||||
|
||||
err := client.Connect(task.server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
features, err := client.ServerFeatures()
|
||||
if err != nil || features.GenesisHash != mainnetGenesisHash {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, err = client.GetTransaction(mainnetSomeTx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// testBatchSupport returns whether the server successfully responded to a batched request
|
||||
func testBatchSupport(task *surveyTask) (bool, error) {
|
||||
client := electrum.NewClient()
|
||||
|
||||
err := client.Connect(task.server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, err = client.ListUnspentBatch(createFakeHashes(2))
|
||||
if err != nil {
|
||||
return false, nil // an error here suggests lack of support for this call
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// measureSpeed returns the amount of successful ListUnspentBatch calls in SPEED_TEST_DURATION
|
||||
// seconds. It assumes batch support was verified beforehand.
|
||||
func (s *Survey) measureSpeed(task *surveyTask) (int, error) {
|
||||
client := electrum.NewClient()
|
||||
|
||||
err := client.Connect(task.server)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
responseCount := 0
|
||||
|
||||
for time.Since(start) < s.config.SpeedTestDuration {
|
||||
fakeHashes := createFakeHashes(s.config.SpeedTestBatchSize)
|
||||
|
||||
_, err := client.ListUnspentBatch(fakeHashes) // TODO: is the faking affecting the result?
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
responseCount++
|
||||
}
|
||||
|
||||
return responseCount - 1, nil // the last one was over the time limit
|
||||
}
|
||||
|
||||
// getPeers returns the list of peers from a server, or empty if it doesn't responds to the request
|
||||
func getPeers(task *surveyTask) ([]string, error) {
|
||||
client := electrum.NewClient()
|
||||
|
||||
err := client.Connect(task.server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peers, err := client.ServerPeers()
|
||||
if err != nil {
|
||||
return []string{}, nil // an error here suggests lack of support for this call
|
||||
}
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (r *Result) IsBetterThan(other *Result) bool {
|
||||
if r.Err != nil {
|
||||
return false
|
||||
}
|
||||
if other.Err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if r.IsWorthy != other.IsWorthy {
|
||||
return r.IsWorthy
|
||||
}
|
||||
|
||||
if r.BatchSupport != other.BatchSupport {
|
||||
return r.BatchSupport
|
||||
}
|
||||
|
||||
if r.Speed != other.Speed {
|
||||
return (r.Speed > other.Speed)
|
||||
}
|
||||
|
||||
return (r.TimeToConnect < other.TimeToConnect)
|
||||
}
|
||||
|
||||
func (r *Result) String() string {
|
||||
return fmt.Sprintf(
|
||||
"%s, %s, %s, %v, %v, %d, %v",
|
||||
r.Server,
|
||||
r.Impl,
|
||||
r.Version,
|
||||
r.BatchSupport,
|
||||
r.TimeToConnect.Seconds(),
|
||||
r.Speed,
|
||||
r.Err,
|
||||
)
|
||||
}
|
||||
|
||||
func createFakeHashes(count int) []string {
|
||||
randomBuffer := make([]byte, 32)
|
||||
fakeHashes := make([]string, count)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
rand.Read(randomBuffer)
|
||||
fakeHashes[i] = hex.EncodeToString(randomBuffer)
|
||||
}
|
||||
|
||||
return fakeHashes
|
||||
}
|
||||
|
||||
func log(msg string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, msg, args...)
|
||||
}
|
||||
Reference in New Issue
Block a user