diff --git a/cmd/walletd/config.go b/cmd/walletd/config.go index adfadfd..3b93294 100644 --- a/cmd/walletd/config.go +++ b/cmd/walletd/config.go @@ -185,9 +185,9 @@ func setAdvancedConfig() { setListenAddress("HTTP Address", &cfg.HTTP.Address) fmt.Println("") - fmt.Println("The gateway address is used to connect to the Sia network.") + fmt.Println("The syncer address is used to connect to the Sia network.") fmt.Println("It should be reachable from other Sia nodes.") - setListenAddress("Gateway Address", &cfg.Consensus.GatewayAddress) + setListenAddress("Syncer Address", &cfg.Syncer.Address) fmt.Println("") fmt.Println("Index mode determines how much of the blockchain to store.") diff --git a/cmd/walletd/main.go b/cmd/walletd/main.go index a0596f5..a7b90d2 100644 --- a/cmd/walletd/main.go +++ b/cmd/walletd/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "net" "os" "os/signal" "path/filepath" @@ -19,7 +18,6 @@ import ( "go.sia.tech/walletd/wallet" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "golang.org/x/term" "gopkg.in/yaml.v3" "lukechampine.com/flagg" ) @@ -60,10 +58,12 @@ var cfg = config.Config{ Address: "localhost:9980", Password: os.Getenv("WALLETD_API_PASSWORD"), }, + Syncer: config.Syncer{ + Address: ":9981", + Bootstrap: true, + }, Consensus: config.Consensus{ - Network: "mainnet", - GatewayAddress: ":9981", - Bootstrap: true, + Network: "mainnet", }, Index: config.Index{ Mode: wallet.IndexModePersonal, @@ -90,16 +90,20 @@ func check(context string, err error) { } } -func getAPIPassword() string { - apiPassword := cfg.HTTP.Password - if apiPassword == "" { - fmt.Print("Enter API password: ") - pw, err := term.ReadPassword(int(os.Stdin.Fd())) - fmt.Println() - check("Could not read API password:", err) - apiPassword = string(pw) +func mustSetAPIPassword() { + // retry until a valid API password is entered + for { + fmt.Println("Please choose a password to unlock walletd.") + fmt.Println("This password will be required to access the admin UI in your web browser.") + fmt.Println("(The password must be at least 4 characters.)") + cfg.HTTP.Password = readPasswordInput("Enter password") + if len(cfg.HTTP.Password) >= 4 { + break + } + + fmt.Println(wrapANSI("\033[31m", "Password must be at least 4 characters!", "\033[0m")) + fmt.Println("") } - return apiPassword } // tryLoadConfig loads the config file specified by the WALLETD_CONFIG_FILE. If @@ -192,10 +196,10 @@ func main() { rootCmd.StringVar(&cfg.Directory, "dir", cfg.Directory, "directory to store node state in") rootCmd.StringVar(&cfg.HTTP.Address, "http", cfg.HTTP.Address, "address to serve API on") - rootCmd.StringVar(&cfg.Consensus.GatewayAddress, "addr", cfg.Consensus.GatewayAddress, "p2p address to listen on") + rootCmd.StringVar(&cfg.Syncer.Address, "addr", cfg.Syncer.Address, "p2p address to listen on") rootCmd.StringVar(&cfg.Consensus.Network, "network", cfg.Consensus.Network, "network to connect to") - rootCmd.BoolVar(&cfg.Consensus.EnableUPNP, "upnp", cfg.Consensus.EnableUPNP, "attempt to forward ports and discover IP with UPnP") - rootCmd.BoolVar(&cfg.Consensus.Bootstrap, "bootstrap", cfg.Consensus.Bootstrap, "attempt to bootstrap the network") + rootCmd.BoolVar(&cfg.Syncer.EnableUPnP, "upnp", cfg.Syncer.EnableUPnP, "attempt to forward ports and discover IP with UPnP") + rootCmd.BoolVar(&cfg.Syncer.Bootstrap, "bootstrap", cfg.Syncer.Bootstrap, "attempt to bootstrap the network") rootCmd.StringVar(&indexModeStr, "index.mode", indexModeStr, "address index mode (personal, full, none)") rootCmd.IntVar(&cfg.Index.BatchSize, "index.batch", cfg.Index.BatchSize, "max number of blocks to index at a time. Increasing this will increase scan speed, but also increase memory and cpu usage.") @@ -232,11 +236,7 @@ func main() { stdoutFatalError("failed to create directory: " + err.Error()) } - apiPassword := getAPIPassword() - l, err := net.Listen("tcp", cfg.HTTP.Address) - if err != nil { - stdoutFatalError("failed to start HTTP server: " + err.Error()) - } + mustSetAPIPassword() var logCores []zapcore.Core if cfg.Log.StdOut.Enabled { @@ -305,18 +305,9 @@ func main() { log.Fatal("failed to parse index mode", zap.Error(err)) } - n, err := newNode(cfg, log) - if err != nil { - log.Fatal("failed to create node", zap.Error(err)) + if err := runNode(ctx, cfg, log); err != nil { + log.Fatal("failed to run node", zap.Error(err)) } - defer n.Close() - - stop := n.Start() - go startWeb(l, n, apiPassword) - log.Info("walletd started", zap.String("version", build.Version()), zap.String("network", cfg.Consensus.Network), zap.String("commit", build.Commit()), zap.Time("buildDate", build.Time())) - <-ctx.Done() - log.Info("shutting down") - stop() case versionCmd: if len(cmd.Args()) != 0 { cmd.Usage() @@ -357,7 +348,8 @@ func main() { log.Fatal(err) } - c := api.NewClient("http://"+cfg.HTTP.Address+"/api", getAPIPassword()) + mustSetAPIPassword() + c := api.NewClient("http://"+cfg.HTTP.Address+"/api", cfg.HTTP.Password) runCPUMiner(c, minerAddr, minerBlocks) } } diff --git a/cmd/walletd/node.go b/cmd/walletd/node.go index f12be24..62aaea5 100644 --- a/cmd/walletd/node.go +++ b/cmd/walletd/node.go @@ -5,8 +5,10 @@ import ( "errors" "fmt" "net" + "net/http" "path/filepath" "strconv" + "strings" "time" "go.sia.tech/core/consensus" @@ -15,148 +17,88 @@ import ( "go.sia.tech/coreutils" "go.sia.tech/coreutils/chain" "go.sia.tech/coreutils/syncer" + "go.sia.tech/jape" + "go.sia.tech/walletd/api" + "go.sia.tech/walletd/build" "go.sia.tech/walletd/config" "go.sia.tech/walletd/persist/sqlite" "go.sia.tech/walletd/wallet" + "go.sia.tech/web/walletd" "go.uber.org/zap" "lukechampine.com/upnp" ) -var mainnetBootstrap = []string{ - "108.227.62.195:9981", - "139.162.81.190:9991", - "144.217.7.188:9981", - "147.182.196.252:9981", - "15.235.85.30:9981", - "167.235.234.84:9981", - "173.235.144.230:9981", - "198.98.53.144:7791", - "199.27.255.169:9981", - "2.136.192.200:9981", - "213.159.50.43:9981", - "24.253.116.61:9981", - "46.249.226.103:9981", - "5.165.236.113:9981", - "5.252.226.131:9981", - "54.38.120.222:9981", - "62.210.136.25:9981", - "63.135.62.123:9981", - "65.21.93.245:9981", - "75.165.149.114:9981", - "77.51.200.125:9981", - "81.6.58.121:9981", - "83.194.193.156:9981", - "84.39.246.63:9981", - "87.99.166.34:9981", - "91.214.242.11:9981", - "93.105.88.181:9981", - "93.180.191.86:9981", - "94.130.220.162:9981", -} - -var zenBootstrap = []string{ - "147.135.16.182:9881", - "147.135.39.109:9881", - "51.81.208.10:9881", -} - -var anagamiBootstrap = []string{ - "147.135.16.182:9781", - "98.180.237.163:9981", - "98.180.237.163:11981", - "98.180.237.163:10981", - "94.130.139.59:9801", - "84.86.11.238:9801", - "69.131.14.86:9981", - "68.108.89.92:9981", - "62.30.63.93:9981", - "46.173.150.154:9111", - "195.252.198.117:9981", - "174.174.206.214:9981", - "172.58.232.54:9981", - "172.58.229.31:9981", - "172.56.200.90:9981", - "172.56.162.155:9981", - "163.172.13.180:9981", - "154.47.25.194:9981", - "138.201.19.49:9981", - "100.34.20.44:9981", -} - -type node struct { - chainStore *coreutils.BoltChainDB - cm *chain.Manager - - store *sqlite.Store - s *syncer.Syncer - wm *wallet.Manager - - Start func() (stop func()) -} - -// Close shuts down the node and closes its database. -func (n *node) Close() error { - n.wm.Close() - n.chainStore.Close() - return n.store.Close() +func setupUPNP(ctx context.Context, port uint16, log *zap.Logger) (string, error) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + d, err := upnp.Discover(ctx) + if err != nil { + return "", fmt.Errorf("couldn't discover UPnP router: %w", err) + } else if !d.IsForwarded(port, "TCP") { + if err := d.Forward(uint16(port), "TCP", "walletd"); err != nil { + log.Debug("couldn't forward port", zap.Error(err)) + } else { + log.Debug("upnp: forwarded p2p port", zap.Uint16("port", port)) + } + } + return d.ExternalIP() } -func newNode(cfg config.Config, log *zap.Logger) (*node, error) { +func runNode(ctx context.Context, cfg config.Config, log *zap.Logger) error { var network *consensus.Network var genesisBlock types.Block var bootstrapPeers []string switch cfg.Consensus.Network { case "mainnet": network, genesisBlock = chain.Mainnet() - bootstrapPeers = mainnetBootstrap + bootstrapPeers = syncer.MainnetBootstrapPeers case "zen": network, genesisBlock = chain.TestnetZen() - bootstrapPeers = zenBootstrap - case "anagami": - network, genesisBlock = TestnetAnagami() - bootstrapPeers = anagamiBootstrap + bootstrapPeers = syncer.ZenBootstrapPeers default: - return nil, errors.New("invalid network: must be one of 'mainnet', 'zen', or 'anagami'") + return errors.New("invalid network: must be one of 'mainnet', 'zen', or 'anagami'") } bdb, err := coreutils.OpenBoltChainDB(filepath.Join(cfg.Directory, "consensus.db")) if err != nil { - return nil, fmt.Errorf("failed to open consensus database: %w", err) + return fmt.Errorf("failed to open consensus database: %w", err) } + defer bdb.Close() + dbstore, tipState, err := chain.NewDBStore(bdb, network, genesisBlock) if err != nil { - return nil, fmt.Errorf("failed to create chain store: %w", err) + return fmt.Errorf("failed to create chain store: %w", err) } cm := chain.NewManager(dbstore, tipState) - l, err := net.Listen("tcp", cfg.Consensus.GatewayAddress) + syncerListener, err := net.Listen("tcp", cfg.Syncer.Address) + if err != nil { + return fmt.Errorf("failed to listen on %q: %w", cfg.Syncer.Address, err) + } + defer syncerListener.Close() + + httpListener, err := net.Listen("tcp", cfg.HTTP.Address) if err != nil { - return nil, err + return fmt.Errorf("failed to listen on %q: %w", cfg.HTTP.Address, err) } - syncerAddr := l.Addr().String() - if cfg.Consensus.EnableUPNP { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if d, err := upnp.Discover(ctx); err != nil { - log.Debug("couldn't discover UPnP router", zap.Error(err)) + defer httpListener.Close() + + syncerAddr := syncerListener.Addr().String() + if cfg.Syncer.EnableUPnP { + _, portStr, _ := net.SplitHostPort(cfg.Syncer.Address) + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return fmt.Errorf("failed to parse syncer port: %w", err) + } + + ip, err := setupUPNP(context.Background(), uint16(port), log) + if err != nil { + log.Warn("failed to set up UPnP", zap.Error(err)) } else { - _, portStr, _ := net.SplitHostPort(cfg.Consensus.GatewayAddress) - port, _ := strconv.Atoi(portStr) - if !d.IsForwarded(uint16(port), "TCP") { - if err := d.Forward(uint16(port), "TCP", "walletd"); err != nil { - log.Debug("couldn't forward port", zap.Error(err)) - } else { - log.Debug("upnp: forwarded p2p port", zap.Int("port", port)) - } - } - if ip, err := d.ExternalIP(); err != nil { - log.Debug("couldn't determine external IP", zap.Error(err)) - } else { - log.Debug("external IP is", zap.String("ip", ip)) - syncerAddr = net.JoinHostPort(ip, portStr) - } + syncerAddr = net.JoinHostPort(ip, portStr) } } + // peers will reject us if our hostname is empty or unspecified, so use loopback host, port, _ := net.SplitHostPort(syncerAddr) if ip := net.ParseIP(host); ip == nil || ip.IsUnspecified() { @@ -165,25 +107,26 @@ func newNode(cfg config.Config, log *zap.Logger) (*node, error) { store, err := sqlite.OpenDatabase(filepath.Join(cfg.Directory, "walletd.sqlite3"), log.Named("sqlite3")) if err != nil { - return nil, fmt.Errorf("failed to open wallet database: %w", err) + return fmt.Errorf("failed to open wallet database: %w", err) } + defer store.Close() - if cfg.Consensus.Bootstrap { + if cfg.Syncer.Bootstrap { for _, peer := range bootstrapPeers { if err := store.AddPeer(peer); err != nil { - return nil, fmt.Errorf("failed to add bootstrap peer '%s': %w", peer, err) + return fmt.Errorf("failed to add bootstrap peer %q: %w", peer, err) } } - for _, peer := range cfg.Consensus.Peers { + for _, peer := range cfg.Syncer.Peers { if err := store.AddPeer(peer); err != nil { - return nil, fmt.Errorf("failed to add peer '%s': %w", peer, err) + return fmt.Errorf("failed to add peer %q: %w", peer, err) } } } ps, err := sqlite.NewPeerStore(store) if err != nil { - return nil, fmt.Errorf("failed to create peer store: %w", err) + return fmt.Errorf("failed to create peer store: %w", err) } header := gateway.Header{ @@ -192,28 +135,33 @@ func newNode(cfg config.Config, log *zap.Logger) (*node, error) { NetAddress: syncerAddr, } - s := syncer.New(l, cm, ps, header, syncer.WithLogger(log.Named("syncer"))) + s := syncer.New(syncerListener, cm, ps, header, syncer.WithLogger(log.Named("syncer"))) + defer s.Close() + go s.Run(ctx) + wm, err := wallet.NewManager(cm, store, wallet.WithLogger(log.Named("wallet")), wallet.WithIndexMode(cfg.Index.Mode), wallet.WithSyncBatchSize(cfg.Index.BatchSize)) if err != nil { - return nil, fmt.Errorf("failed to create wallet manager: %w", err) + return fmt.Errorf("failed to create wallet manager: %w", err) } - return &node{ - chainStore: bdb, - cm: cm, - store: store, - s: s, - wm: wm, - Start: func() func() { - ch := make(chan struct{}) - go func() { - s.Run() - close(ch) - }() - return func() { - l.Close() - <-ch - bdb.Close() + + api := jape.BasicAuth(cfg.HTTP.Password)(api.NewServer(cm, s, wm)) + web := walletd.Handler() + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/api") { + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") + api.ServeHTTP(w, r) + return } - }, - }, nil + web.ServeHTTP(w, r) + }), + ReadTimeout: 10 * time.Second, + } + defer server.Close() + go server.Serve(httpListener) + + log.Info("node started", zap.Stringer("syncer", syncerListener.Addr()), zap.Stringer("http", httpListener.Addr()), zap.String("version", build.Version()), zap.String("commit", build.Commit())) + <-ctx.Done() + log.Info("shutting down") + return nil } diff --git a/cmd/walletd/testnet.go b/cmd/walletd/testnet.go deleted file mode 100644 index dc9c706..0000000 --- a/cmd/walletd/testnet.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "time" - - "go.sia.tech/core/consensus" - "go.sia.tech/core/types" -) - -// TestnetAnagami returns the chain parameters and genesis block for the "Anagami" -// testnet chain. -func TestnetAnagami() (*consensus.Network, types.Block) { - n := &consensus.Network{ - Name: "anagami", - - InitialCoinbase: types.Siacoins(300000), - MinimumCoinbase: types.Siacoins(300000), - InitialTarget: types.BlockID{3: 1}, - } - - n.HardforkDevAddr.Height = 1 - n.HardforkDevAddr.OldAddress = types.Address{} - n.HardforkDevAddr.NewAddress = types.Address{} - - n.HardforkTax.Height = 2 - - n.HardforkStorageProof.Height = 3 - - n.HardforkOak.Height = 5 - n.HardforkOak.FixHeight = 8 - n.HardforkOak.GenesisTimestamp = time.Unix(1702300000, 0) // Dec 11, 2023 @ 13:06 GMT - - n.HardforkASIC.Height = 13 - n.HardforkASIC.OakTime = 10 * time.Minute - n.HardforkASIC.OakTarget = n.InitialTarget - - n.HardforkFoundation.Height = 21 - n.HardforkFoundation.PrimaryAddress, _ = types.ParseAddress("addr:5949fdf56a7c18ba27f6526f22fd560526ce02a1bd4fa3104938ab744b69cf63b6b734b8341f") - n.HardforkFoundation.FailsafeAddress = n.HardforkFoundation.PrimaryAddress - - n.HardforkV2.AllowHeight = 2016 // ~2 weeks in - n.HardforkV2.RequireHeight = 2016 + 288 // ~2 days later - - b := types.Block{ - Timestamp: n.HardforkOak.GenesisTimestamp, - Transactions: []types.Transaction{{ - SiacoinOutputs: []types.SiacoinOutput{{ - Address: n.HardforkFoundation.PrimaryAddress, - Value: types.Siacoins(1).Mul64(1e12), - }}, - SiafundOutputs: []types.SiafundOutput{{ - Address: n.HardforkFoundation.PrimaryAddress, - Value: 10000, - }}, - }}, - } - - return n, b -} diff --git a/cmd/walletd/web.go b/cmd/walletd/web.go deleted file mode 100644 index c6de95c..0000000 --- a/cmd/walletd/web.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "net" - "net/http" - "strings" - - "go.sia.tech/jape" - "go.sia.tech/walletd/api" - "go.sia.tech/web/walletd" -) - -func startWeb(l net.Listener, node *node, password string) error { - renter := api.NewServer(node.cm, node.s, node.wm) - api := jape.BasicAuth(password)(renter) - web := walletd.Handler() - return http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/api") { - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") - api.ServeHTTP(w, r) - return - } - web.ServeHTTP(w, r) - })) -} diff --git a/config/config.go b/config/config.go index b8d3f14..7372714 100644 --- a/config/config.go +++ b/config/config.go @@ -9,13 +9,17 @@ type ( Password string `yaml:"password,omitempty"` } + // Syncer contains the configuration for the consensus set syncer. + Syncer struct { + Address string `yaml:"address,omitempty"` + Bootstrap bool `yaml:"bootstrap,omitempty"` + EnableUPnP bool `yaml:"enableUPnP,omitempty"` + Peers []string `yaml:"peers,omitempty"` + } + // Consensus contains the configuration for the consensus set. Consensus struct { - Network string `yaml:"network,omitempty"` - GatewayAddress string `yaml:"gatewayAddress,omitempty"` - Bootstrap bool `yaml:"bootstrap,omitempty"` - Peers []string `yaml:"peers,omitempty"` - EnableUPNP bool `yaml:"enableUPnP,omitempty"` + Network string `yaml:"network,omitempty"` } // Index contains the configuration for the blockchain indexer @@ -56,6 +60,7 @@ type ( HTTP HTTP `yaml:"http,omitempty"` Consensus Consensus `yaml:"consensus,omitempty"` + Syncer Syncer `yaml:"syncer,omitempty"` Log Log `yaml:"log,omitempty"` Index Index `yaml:"index,omitempty"` }