Add files via upload

This commit is contained in:
Jonathan Combs 2024-05-23 02:29:06 -04:00 committed by GitHub
parent 1bd1568d0c
commit 63c0338db4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 6300 additions and 0 deletions

337
.github/workflows/api.go vendored Normal file
View File

@ -0,0 +1,337 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"context"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/rpc"
)
// apis returns the collection of built-in RPC APIs.
func (n *Node) apis() []rpc.API {
return []rpc.API{
{
Namespace: "admin",
Service: &adminAPI{n},
}, {
Namespace: "debug",
Service: debug.Handler,
}, {
Namespace: "web3",
Service: &web3API{n},
},
}
}
// adminAPI is the collection of administrative API methods exposed over
// both secure and unsecure RPC channels.
type adminAPI struct {
node *Node // Node interfaced by this API
}
// AddPeer requests connecting to a remote node, and also maintaining the new
// connection at all times, even reconnecting if it is lost.
func (api *adminAPI) AddPeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
return false, ErrNodeStopped
}
// Try to add the url as a static peer and return
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
return false, fmt.Errorf("invalid enode: %v", err)
}
server.AddPeer(node)
return true, nil
}
// RemovePeer disconnects from a remote node if the connection exists
func (api *adminAPI) RemovePeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
return false, ErrNodeStopped
}
// Try to remove the url as a static peer and return
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
return false, fmt.Errorf("invalid enode: %v", err)
}
server.RemovePeer(node)
return true, nil
}
// AddTrustedPeer allows a remote node to always connect, even if slots are full
func (api *adminAPI) AddTrustedPeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
return false, ErrNodeStopped
}
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
return false, fmt.Errorf("invalid enode: %v", err)
}
server.AddTrustedPeer(node)
return true, nil
}
// RemoveTrustedPeer removes a remote node from the trusted peer set, but it
// does not disconnect it automatically.
func (api *adminAPI) RemoveTrustedPeer(url string) (bool, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
return false, ErrNodeStopped
}
node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil {
return false, fmt.Errorf("invalid enode: %v", err)
}
server.RemoveTrustedPeer(node)
return true, nil
}
// PeerEvents creates an RPC subscription which receives peer events from the
// node's p2p.Server
func (api *adminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription, error) {
// Make sure the server is running, fail otherwise
server := api.node.Server()
if server == nil {
return nil, ErrNodeStopped
}
// Create the subscription
notifier, supported := rpc.NotifierFromContext(ctx)
if !supported {
return nil, rpc.ErrNotificationsUnsupported
}
rpcSub := notifier.CreateSubscription()
go func() {
events := make(chan *p2p.PeerEvent)
sub := server.SubscribeEvents(events)
defer sub.Unsubscribe()
for {
select {
case event := <-events:
notifier.Notify(rpcSub.ID, event)
case <-sub.Err():
return
case <-rpcSub.Err():
return
case <-notifier.Closed():
return
}
}
}()
return rpcSub, nil
}
// StartHTTP starts the HTTP RPC API server.
func (api *adminAPI) StartHTTP(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
// Determine host and port.
if host == nil {
h := DefaultHTTPHost
if api.node.config.HTTPHost != "" {
h = api.node.config.HTTPHost
}
host = &h
}
if port == nil {
port = &api.node.config.HTTPPort
}
// Determine config.
config := httpConfig{
CorsAllowedOrigins: api.node.config.HTTPCors,
Vhosts: api.node.config.HTTPVirtualHosts,
Modules: api.node.config.HTTPModules,
rpcEndpointConfig: rpcEndpointConfig{
batchItemLimit: api.node.config.BatchRequestLimit,
batchResponseSizeLimit: api.node.config.BatchResponseMaxSize,
},
}
if cors != nil {
config.CorsAllowedOrigins = nil
for _, origin := range strings.Split(*cors, ",") {
config.CorsAllowedOrigins = append(config.CorsAllowedOrigins, strings.TrimSpace(origin))
}
}
if vhosts != nil {
config.Vhosts = nil
for _, vhost := range strings.Split(*host, ",") {
config.Vhosts = append(config.Vhosts, strings.TrimSpace(vhost))
}
}
if apis != nil {
config.Modules = nil
for _, m := range strings.Split(*apis, ",") {
config.Modules = append(config.Modules, strings.TrimSpace(m))
}
}
if err := api.node.http.setListenAddr(*host, *port); err != nil {
return false, err
}
if err := api.node.http.enableRPC(api.node.rpcAPIs, config); err != nil {
return false, err
}
if err := api.node.http.start(); err != nil {
return false, err
}
return true, nil
}
// StartRPC starts the HTTP RPC API server.
// Deprecated: use StartHTTP instead.
func (api *adminAPI) StartRPC(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
log.Warn("Deprecation warning", "method", "admin.StartRPC", "use-instead", "admin.StartHTTP")
return api.StartHTTP(host, port, cors, apis, vhosts)
}
// StopHTTP shuts down the HTTP server.
func (api *adminAPI) StopHTTP() (bool, error) {
api.node.http.stop()
return true, nil
}
// StopRPC shuts down the HTTP server.
// Deprecated: use StopHTTP instead.
func (api *adminAPI) StopRPC() (bool, error) {
log.Warn("Deprecation warning", "method", "admin.StopRPC", "use-instead", "admin.StopHTTP")
return api.StopHTTP()
}
// StartWS starts the websocket RPC API server.
func (api *adminAPI) StartWS(host *string, port *int, allowedOrigins *string, apis *string) (bool, error) {
api.node.lock.Lock()
defer api.node.lock.Unlock()
// Determine host and port.
if host == nil {
h := DefaultWSHost
if api.node.config.WSHost != "" {
h = api.node.config.WSHost
}
host = &h
}
if port == nil {
port = &api.node.config.WSPort
}
// Determine config.
config := wsConfig{
Modules: api.node.config.WSModules,
Origins: api.node.config.WSOrigins,
// ExposeAll: api.node.config.WSExposeAll,
rpcEndpointConfig: rpcEndpointConfig{
batchItemLimit: api.node.config.BatchRequestLimit,
batchResponseSizeLimit: api.node.config.BatchResponseMaxSize,
},
}
if apis != nil {
config.Modules = nil
for _, m := range strings.Split(*apis, ",") {
config.Modules = append(config.Modules, strings.TrimSpace(m))
}
}
if allowedOrigins != nil {
config.Origins = nil
for _, origin := range strings.Split(*allowedOrigins, ",") {
config.Origins = append(config.Origins, strings.TrimSpace(origin))
}
}
// Enable WebSocket on the server.
server := api.node.wsServerForPort(*port, false)
if err := server.setListenAddr(*host, *port); err != nil {
return false, err
}
openApis, _ := api.node.getAPIs()
if err := server.enableWS(openApis, config); err != nil {
return false, err
}
if err := server.start(); err != nil {
return false, err
}
api.node.http.log.Info("WebSocket endpoint opened", "url", api.node.WSEndpoint())
return true, nil
}
// StopWS terminates all WebSocket servers.
func (api *adminAPI) StopWS() (bool, error) {
api.node.http.stopWS()
api.node.ws.stop()
return true, nil
}
// Peers retrieves all the information we know about each individual peer at the
// protocol granularity.
func (api *adminAPI) Peers() ([]*p2p.PeerInfo, error) {
server := api.node.Server()
if server == nil {
return nil, ErrNodeStopped
}
return server.PeersInfo(), nil
}
// NodeInfo retrieves all the information we know about the host node at the
// protocol granularity.
func (api *adminAPI) NodeInfo() (*p2p.NodeInfo, error) {
server := api.node.Server()
if server == nil {
return nil, ErrNodeStopped
}
return server.NodeInfo(), nil
}
// Datadir retrieves the current data directory the node is using.
func (api *adminAPI) Datadir() string {
return api.node.DataDir()
}
// web3API offers helper utils
type web3API struct {
stack *Node
}
// ClientVersion returns the node name
func (s *web3API) ClientVersion() string {
return s.stack.Server().Name
}
// Sha3 applies the ethereum sha3 implementation on the input.
// It assumes the input is hex encoded.
func (s *web3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
return crypto.Keccak256(input)
}

355
.github/workflows/api_test.go vendored Normal file
View File

@ -0,0 +1,355 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"bytes"
"io"
"net"
"net/http"
"net/url"
"strings"
"testing"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/assert"
)
// This test uses the admin_startRPC and admin_startWS APIs,
// checking whether the HTTP server is started correctly.
func TestStartRPC(t *testing.T) {
type test struct {
name string
cfg Config
fn func(*testing.T, *Node, *adminAPI)
// Checks. These run after the node is configured and all API calls have been made.
wantReachable bool // whether the HTTP server should be reachable at all
wantHandlers bool // whether RegisterHandler handlers should be accessible
wantRPC bool // whether JSON-RPC/HTTP should be accessible
wantWS bool // whether JSON-RPC/WS should be accessible
}
tests := []test{
{
name: "all off",
cfg: Config{},
fn: func(t *testing.T, n *Node, api *adminAPI) {
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "rpc enabled through config",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *adminAPI) {
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: false,
},
{
name: "rpc enabled through API",
cfg: Config{},
fn: func(t *testing.T, n *Node, api *adminAPI) {
_, err := api.StartHTTP(sp("127.0.0.1"), ip(0), nil, nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: false,
},
{
name: "rpc start again after failure",
cfg: Config{},
fn: func(t *testing.T, n *Node, api *adminAPI) {
// Listen on a random port.
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal("can't listen:", err)
}
defer listener.Close()
port := listener.Addr().(*net.TCPAddr).Port
// Now try to start RPC on that port. This should fail.
_, err = api.StartHTTP(sp("127.0.0.1"), ip(port), nil, nil, nil)
if err == nil {
t.Fatal("StartHTTP should have failed on port", port)
}
// Try again after unblocking the port. It should work this time.
listener.Close()
_, err = api.StartHTTP(sp("127.0.0.1"), ip(port), nil, nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: false,
},
{
name: "rpc stopped through API",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *adminAPI) {
_, err := api.StopHTTP()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "rpc stopped twice",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *adminAPI) {
_, err := api.StopHTTP()
assert.NoError(t, err)
_, err = api.StopHTTP()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "ws enabled through config",
cfg: Config{WSHost: "127.0.0.1"},
wantReachable: true,
wantHandlers: false,
wantRPC: false,
wantWS: true,
},
{
name: "ws enabled through API",
cfg: Config{},
fn: func(t *testing.T, n *Node, api *adminAPI) {
_, err := api.StartWS(sp("127.0.0.1"), ip(0), nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: false,
wantRPC: false,
wantWS: true,
},
{
name: "ws stopped through API",
cfg: Config{WSHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *adminAPI) {
_, err := api.StopWS()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "ws stopped twice",
cfg: Config{WSHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *adminAPI) {
_, err := api.StopWS()
assert.NoError(t, err)
_, err = api.StopWS()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "ws enabled after RPC",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *adminAPI) {
wsport := n.http.port
_, err := api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: true,
},
{
name: "ws enabled after RPC then stopped",
cfg: Config{HTTPHost: "127.0.0.1"},
fn: func(t *testing.T, n *Node, api *adminAPI) {
wsport := n.http.port
_, err := api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil)
assert.NoError(t, err)
_, err = api.StopWS()
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: false,
},
{
name: "rpc stopped with ws enabled",
fn: func(t *testing.T, n *Node, api *adminAPI) {
_, err := api.StartHTTP(sp("127.0.0.1"), ip(0), nil, nil, nil)
assert.NoError(t, err)
wsport := n.http.port
_, err = api.StartWS(sp("127.0.0.1"), ip(wsport), nil, nil)
assert.NoError(t, err)
_, err = api.StopHTTP()
assert.NoError(t, err)
},
wantReachable: false,
wantHandlers: false,
wantRPC: false,
wantWS: false,
},
{
name: "rpc enabled after ws",
fn: func(t *testing.T, n *Node, api *adminAPI) {
_, err := api.StartWS(sp("127.0.0.1"), ip(0), nil, nil)
assert.NoError(t, err)
wsport := n.http.port
_, err = api.StartHTTP(sp("127.0.0.1"), ip(wsport), nil, nil, nil)
assert.NoError(t, err)
},
wantReachable: true,
wantHandlers: true,
wantRPC: true,
wantWS: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// Apply some sane defaults.
config := test.cfg
// config.Logger = testlog.Logger(t, log.LvlDebug)
config.P2P.NoDiscovery = true
if config.HTTPTimeouts == (rpc.HTTPTimeouts{}) {
config.HTTPTimeouts = rpc.DefaultHTTPTimeouts
}
// Create Node.
stack, err := New(&config)
if err != nil {
t.Fatal("can't create node:", err)
}
defer stack.Close()
// Register the test handler.
stack.RegisterHandler("test", "/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}))
if err := stack.Start(); err != nil {
t.Fatal("can't start node:", err)
}
// Run the API call hook.
if test.fn != nil {
test.fn(t, stack, &adminAPI{stack})
}
// Check if the HTTP endpoints are available.
baseURL := stack.HTTPEndpoint()
reachable := checkReachable(baseURL)
handlersAvailable := checkBodyOK(baseURL + "/test")
rpcAvailable := checkRPC(baseURL)
wsAvailable := checkRPC(strings.Replace(baseURL, "http://", "ws://", 1))
if reachable != test.wantReachable {
t.Errorf("HTTP server is %sreachable, want it %sreachable", not(reachable), not(test.wantReachable))
}
if handlersAvailable != test.wantHandlers {
t.Errorf("RegisterHandler handlers %savailable, want them %savailable", not(handlersAvailable), not(test.wantHandlers))
}
if rpcAvailable != test.wantRPC {
t.Errorf("HTTP RPC %savailable, want it %savailable", not(rpcAvailable), not(test.wantRPC))
}
if wsAvailable != test.wantWS {
t.Errorf("WS RPC %savailable, want it %savailable", not(wsAvailable), not(test.wantWS))
}
})
}
}
// checkReachable checks if the TCP endpoint in rawurl is open.
func checkReachable(rawurl string) bool {
u, err := url.Parse(rawurl)
if err != nil {
panic(err)
}
conn, err := net.Dial("tcp", u.Host)
if err != nil {
return false
}
conn.Close()
return true
}
// checkBodyOK checks whether the given HTTP URL responds with 200 OK and body "OK".
func checkBodyOK(url string) bool {
resp, err := http.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return false
}
buf := make([]byte, 2)
if _, err = io.ReadFull(resp.Body, buf); err != nil {
return false
}
return bytes.Equal(buf, []byte("OK"))
}
// checkRPC checks whether JSON-RPC works against the given URL.
func checkRPC(url string) bool {
c, err := rpc.Dial(url)
if err != nil {
return false
}
defer c.Close()
_, err = c.SupportedModules()
return err == nil
}
// string/int pointer helpers.
func sp(s string) *string { return &s }
func ip(i int) *int { return &i }
func not(ok bool) string {
if ok {
return ""
}
return "not "
}

BIN
.github/workflows/auth0-integration.pdf vendored Normal file

Binary file not shown.

480
.github/workflows/config.go vendored Normal file
View File

@ -0,0 +1,480 @@
// Copyright 2014 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"crypto/ecdsa"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
const (
datadirPrivateKey = "nodekey" // Path within the datadir to the node's private key
datadirJWTKey = "jwtsecret" // Path within the datadir to the node's jwt secret
datadirDefaultKeyStore = "keystore" // Path within the datadir to the keystore
datadirStaticNodes = "static-nodes.json" // Path within the datadir to the static node list
datadirTrustedNodes = "trusted-nodes.json" // Path within the datadir to the trusted node list
datadirNodeDatabase = "nodes" // Path within the datadir to store the node infos
)
// Config represents a small collection of configuration values to fine tune the
// P2P network layer of a protocol stack. These values can be further extended by
// all registered services.
type Config struct {
// Name sets the instance name of the node. It must not contain the / character and is
// used in the devp2p node identifier. The instance name of geth is "geth". If no
// value is specified, the basename of the current executable is used.
Name string `toml:"-"`
// UserIdent, if set, is used as an additional component in the devp2p node identifier.
UserIdent string `toml:",omitempty"`
// Version should be set to the version number of the program. It is used
// in the devp2p node identifier.
Version string `toml:"-"`
// DataDir is the file system folder the node should use for any data storage
// requirements. The configured data directory will not be directly shared with
// registered services, instead those can use utility methods to create/access
// databases or flat files. This enables ephemeral nodes which can fully reside
// in memory.
DataDir string
// Configuration of peer-to-peer networking.
P2P p2p.Config
// KeyStoreDir is the file system folder that contains private keys. The directory can
// be specified as a relative path, in which case it is resolved relative to the
// current directory.
//
// If KeyStoreDir is empty, the default location is the "keystore" subdirectory of
// DataDir. If DataDir is unspecified and KeyStoreDir is empty, an ephemeral directory
// is created by New and destroyed when the node is stopped.
KeyStoreDir string `toml:",omitempty"`
// ExternalSigner specifies an external URI for a clef-type signer.
ExternalSigner string `toml:",omitempty"`
// UseLightweightKDF lowers the memory and CPU requirements of the key store
// scrypt KDF at the expense of security.
UseLightweightKDF bool `toml:",omitempty"`
// InsecureUnlockAllowed allows user to unlock accounts in unsafe http environment.
InsecureUnlockAllowed bool `toml:",omitempty"`
// NoUSB disables hardware wallet monitoring and connectivity.
// Deprecated: USB monitoring is disabled by default and must be enabled explicitly.
NoUSB bool `toml:",omitempty"`
// USB enables hardware wallet monitoring and connectivity.
USB bool `toml:",omitempty"`
// SmartCardDaemonPath is the path to the smartcard daemon's socket.
SmartCardDaemonPath string `toml:",omitempty"`
// IPCPath is the requested location to place the IPC endpoint. If the path is
// a simple file name, it is placed inside the data directory (or on the root
// pipe path on Windows), whereas if it's a resolvable path name (absolute or
// relative), then that specific path is enforced. An empty path disables IPC.
IPCPath string
// HTTPHost is the host interface on which to start the HTTP RPC server. If this
// field is empty, no HTTP API endpoint will be started.
HTTPHost string
// HTTPPort is the TCP port number on which to start the HTTP RPC server. The
// default zero value is/ valid and will pick a port number randomly (useful
// for ephemeral nodes).
HTTPPort int `toml:",omitempty"`
// HTTPCors is the Cross-Origin Resource Sharing header to send to requesting
// clients. Please be aware that CORS is a browser enforced security, it's fully
// useless for custom HTTP clients.
HTTPCors []string `toml:",omitempty"`
// HTTPVirtualHosts is the list of virtual hostnames which are allowed on incoming requests.
// This is by default {'localhost'}. Using this prevents attacks like
// DNS rebinding, which bypasses SOP by simply masquerading as being within the same
// origin. These attacks do not utilize CORS, since they are not cross-domain.
// By explicitly checking the Host-header, the server will not allow requests
// made against the server with a malicious host domain.
// Requests using ip address directly are not affected
HTTPVirtualHosts []string `toml:",omitempty"`
// HTTPModules is a list of API modules to expose via the HTTP RPC interface.
// If the module list is empty, all RPC API endpoints designated public will be
// exposed.
HTTPModules []string
// HTTPTimeouts allows for customization of the timeout values used by the HTTP RPC
// interface.
HTTPTimeouts rpc.HTTPTimeouts
// HTTPPathPrefix specifies a path prefix on which http-rpc is to be served.
HTTPPathPrefix string `toml:",omitempty"`
// AuthAddr is the listening address on which authenticated APIs are provided.
AuthAddr string `toml:",omitempty"`
// AuthPort is the port number on which authenticated APIs are provided.
AuthPort int `toml:",omitempty"`
// AuthVirtualHosts is the list of virtual hostnames which are allowed on incoming requests
// for the authenticated api. This is by default {'localhost'}.
AuthVirtualHosts []string `toml:",omitempty"`
// WSHost is the host interface on which to start the websocket RPC server. If
// this field is empty, no websocket API endpoint will be started.
WSHost string
// WSPort is the TCP port number on which to start the websocket RPC server. The
// default zero value is/ valid and will pick a port number randomly (useful for
// ephemeral nodes).
WSPort int `toml:",omitempty"`
// WSPathPrefix specifies a path prefix on which ws-rpc is to be served.
WSPathPrefix string `toml:",omitempty"`
// WSOrigins is the list of domain to accept websocket requests from. Please be
// aware that the server can only act upon the HTTP request the client sends and
// cannot verify the validity of the request header.
WSOrigins []string `toml:",omitempty"`
// WSModules is a list of API modules to expose via the websocket RPC interface.
// If the module list is empty, all RPC API endpoints designated public will be
// exposed.
WSModules []string
// WSExposeAll exposes all API modules via the WebSocket RPC interface rather
// than just the public ones.
//
// *WARNING* Only set this if the node is running in a trusted network, exposing
// private APIs to untrusted users is a major security risk.
WSExposeAll bool `toml:",omitempty"`
// GraphQLCors is the Cross-Origin Resource Sharing header to send to requesting
// clients. Please be aware that CORS is a browser enforced security, it's fully
// useless for custom HTTP clients.
GraphQLCors []string `toml:",omitempty"`
// GraphQLVirtualHosts is the list of virtual hostnames which are allowed on incoming requests.
// This is by default {'localhost'}. Using this prevents attacks like
// DNS rebinding, which bypasses SOP by simply masquerading as being within the same
// origin. These attacks do not utilize CORS, since they are not cross-domain.
// By explicitly checking the Host-header, the server will not allow requests
// made against the server with a malicious host domain.
// Requests using ip address directly are not affected
GraphQLVirtualHosts []string `toml:",omitempty"`
// Logger is a custom logger to use with the p2p.Server.
Logger log.Logger `toml:",omitempty"`
oldGethResourceWarning bool
// AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC.
AllowUnprotectedTxs bool `toml:",omitempty"`
// BatchRequestLimit is the maximum number of requests in a batch.
BatchRequestLimit int `toml:",omitempty"`
// BatchResponseMaxSize is the maximum number of bytes returned from a batched rpc call.
BatchResponseMaxSize int `toml:",omitempty"`
// JWTSecret is the path to the hex-encoded jwt secret.
JWTSecret string `toml:",omitempty"`
// EnablePersonal enables the deprecated personal namespace.
EnablePersonal bool `toml:"-"`
DBEngine string `toml:",omitempty"`
}
// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into
// account the set data folders as well as the designated platform we're currently
// running on.
func (c *Config) IPCEndpoint() string {
// Short circuit if IPC has not been enabled
if c.IPCPath == "" {
return ""
}
// On windows we can only use plain top-level pipes
if runtime.GOOS == "windows" {
if strings.HasPrefix(c.IPCPath, `\\.\pipe\`) {
return c.IPCPath
}
return `\\.\pipe\` + c.IPCPath
}
// Resolve names into the data directory full paths otherwise
if filepath.Base(c.IPCPath) == c.IPCPath {
if c.DataDir == "" {
return filepath.Join(os.TempDir(), c.IPCPath)
}
return filepath.Join(c.DataDir, c.IPCPath)
}
return c.IPCPath
}
// NodeDB returns the path to the discovery node database.
func (c *Config) NodeDB() string {
if c.DataDir == "" {
return "" // ephemeral
}
return c.ResolvePath(datadirNodeDatabase)
}
// DefaultIPCEndpoint returns the IPC path used by default.
func DefaultIPCEndpoint(clientIdentifier string) string {
if clientIdentifier == "" {
clientIdentifier = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
if clientIdentifier == "" {
panic("empty executable name")
}
}
config := &Config{DataDir: DefaultDataDir(), IPCPath: clientIdentifier + ".ipc"}
return config.IPCEndpoint()
}
// HTTPEndpoint resolves an HTTP endpoint based on the configured host interface
// and port parameters.
func (c *Config) HTTPEndpoint() string {
if c.HTTPHost == "" {
return ""
}
return net.JoinHostPort(c.HTTPHost, fmt.Sprintf("%d", c.HTTPPort))
}
// DefaultHTTPEndpoint returns the HTTP endpoint used by default.
func DefaultHTTPEndpoint() string {
config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort, AuthPort: DefaultAuthPort}
return config.HTTPEndpoint()
}
// WSEndpoint resolves a websocket endpoint based on the configured host interface
// and port parameters.
func (c *Config) WSEndpoint() string {
if c.WSHost == "" {
return ""
}
return net.JoinHostPort(c.WSHost, fmt.Sprintf("%d", c.WSPort))
}
// DefaultWSEndpoint returns the websocket endpoint used by default.
func DefaultWSEndpoint() string {
config := &Config{WSHost: DefaultWSHost, WSPort: DefaultWSPort}
return config.WSEndpoint()
}
// ExtRPCEnabled returns the indicator whether node enables the external
// RPC(http, ws or graphql).
func (c *Config) ExtRPCEnabled() bool {
return c.HTTPHost != "" || c.WSHost != ""
}
// NodeName returns the devp2p node identifier.
func (c *Config) NodeName() string {
name := c.name()
// Backwards compatibility: previous versions used title-cased "Geth", keep that.
if name == "geth" || name == "geth-testnet" {
name = "Geth"
}
if c.UserIdent != "" {
name += "/" + c.UserIdent
}
if c.Version != "" {
name += "/v" + c.Version
}
name += "/" + runtime.GOOS + "-" + runtime.GOARCH
name += "/" + runtime.Version()
return name
}
func (c *Config) name() string {
if c.Name == "" {
progname := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
if progname == "" {
panic("empty executable name, set Config.Name")
}
return progname
}
return c.Name
}
// These resources are resolved differently for "geth" instances.
var isOldGethResource = map[string]bool{
"chaindata": true,
"nodes": true,
"nodekey": true,
"static-nodes.json": false, // no warning for these because they have their
"trusted-nodes.json": false, // own separate warning.
}
// ResolvePath resolves path in the instance directory.
func (c *Config) ResolvePath(path string) string {
if filepath.IsAbs(path) {
return path
}
if c.DataDir == "" {
return ""
}
// Backwards-compatibility: ensure that data directory files created
// by geth 1.4 are used if they exist.
if warn, isOld := isOldGethResource[path]; isOld {
oldpath := ""
if c.name() == "geth" {
oldpath = filepath.Join(c.DataDir, path)
}
if oldpath != "" && common.FileExist(oldpath) {
if warn && !c.oldGethResourceWarning {
c.oldGethResourceWarning = true
log.Warn("Using deprecated resource file, please move this file to the 'geth' subdirectory of datadir.", "file", oldpath)
}
return oldpath
}
}
return filepath.Join(c.instanceDir(), path)
}
func (c *Config) instanceDir() string {
if c.DataDir == "" {
return ""
}
return filepath.Join(c.DataDir, c.name())
}
// NodeKey retrieves the currently configured private key of the node, checking
// first any manually set key, falling back to the one found in the configured
// data folder. If no key can be found, a new one is generated.
func (c *Config) NodeKey() *ecdsa.PrivateKey {
// Use any specifically configured key.
if c.P2P.PrivateKey != nil {
return c.P2P.PrivateKey
}
// Generate ephemeral key if no datadir is being used.
if c.DataDir == "" {
key, err := crypto.GenerateKey()
if err != nil {
log.Crit(fmt.Sprintf("Failed to generate ephemeral node key: %v", err))
}
return key
}
keyfile := c.ResolvePath(datadirPrivateKey)
if key, err := crypto.LoadECDSA(keyfile); err == nil {
return key
}
// No persistent key found, generate and store a new one.
key, err := crypto.GenerateKey()
if err != nil {
log.Crit(fmt.Sprintf("Failed to generate node key: %v", err))
}
instanceDir := filepath.Join(c.DataDir, c.name())
if err := os.MkdirAll(instanceDir, 0700); err != nil {
log.Error(fmt.Sprintf("Failed to persist node key: %v", err))
return key
}
keyfile = filepath.Join(instanceDir, datadirPrivateKey)
if err := crypto.SaveECDSA(keyfile, key); err != nil {
log.Error(fmt.Sprintf("Failed to persist node key: %v", err))
}
return key
}
// checkLegacyFiles inspects the datadir for signs of legacy static-nodes
// and trusted-nodes files. If they exist it raises an error.
func (c *Config) checkLegacyFiles() {
c.checkLegacyFile(c.ResolvePath(datadirStaticNodes))
c.checkLegacyFile(c.ResolvePath(datadirTrustedNodes))
}
// checkLegacyFile will only raise an error if a file at the given path exists.
func (c *Config) checkLegacyFile(path string) {
// Short circuit if no node config is present
if c.DataDir == "" {
return
}
if _, err := os.Stat(path); err != nil {
return
}
logger := c.Logger
if logger == nil {
logger = log.Root()
}
switch fname := filepath.Base(path); fname {
case "static-nodes.json":
logger.Error("The static-nodes.json file is deprecated and ignored. Use P2P.StaticNodes in config.toml instead.")
case "trusted-nodes.json":
logger.Error("The trusted-nodes.json file is deprecated and ignored. Use P2P.TrustedNodes in config.toml instead.")
default:
// We shouldn't wind up here, but better print something just in case.
logger.Error("Ignoring deprecated file.", "file", path)
}
}
// KeyDirConfig determines the settings for keydirectory
func (c *Config) KeyDirConfig() (string, error) {
var (
keydir string
err error
)
switch {
case filepath.IsAbs(c.KeyStoreDir):
keydir = c.KeyStoreDir
case c.DataDir != "":
if c.KeyStoreDir == "" {
keydir = filepath.Join(c.DataDir, datadirDefaultKeyStore)
} else {
keydir, err = filepath.Abs(c.KeyStoreDir)
}
case c.KeyStoreDir != "":
keydir, err = filepath.Abs(c.KeyStoreDir)
}
return keydir, err
}
// GetKeyStoreDir retrieves the key directory and will create
// and ephemeral one if necessary.
func (c *Config) GetKeyStoreDir() (string, bool, error) {
keydir, err := c.KeyDirConfig()
if err != nil {
return "", false, err
}
isEphemeral := false
if keydir == "" {
// There is no datadir.
keydir, err = os.MkdirTemp("", "go-ethereum-keystore")
isEphemeral = true
}
if err != nil {
return "", false, err
}
if err := os.MkdirAll(keydir, 0700); err != nil {
return "", false, err
}
return keydir, isEphemeral, nil
}

153
.github/workflows/config_test.go vendored Normal file
View File

@ -0,0 +1,153 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"bytes"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p"
)
// Tests that datadirs can be successfully created, be them manually configured
// ones or automatically generated temporary ones.
func TestDatadirCreation(t *testing.T) {
// Create a temporary data dir and check that it can be used by a node
dir := t.TempDir()
node, err := New(&Config{DataDir: dir})
if err != nil {
t.Fatalf("failed to create stack with existing datadir: %v", err)
}
if err := node.Close(); err != nil {
t.Fatalf("failed to close node: %v", err)
}
// Generate a long non-existing datadir path and check that it gets created by a node
dir = filepath.Join(dir, "a", "b", "c", "d", "e", "f")
node, err = New(&Config{DataDir: dir})
if err != nil {
t.Fatalf("failed to create stack with creatable datadir: %v", err)
}
if err := node.Close(); err != nil {
t.Fatalf("failed to close node: %v", err)
}
if _, err := os.Stat(dir); err != nil {
t.Fatalf("freshly created datadir not accessible: %v", err)
}
// Verify that an impossible datadir fails creation
file, err := os.CreateTemp("", "")
if err != nil {
t.Fatalf("failed to create temporary file: %v", err)
}
defer func() {
file.Close()
os.Remove(file.Name())
}()
dir = filepath.Join(file.Name(), "invalid/path")
_, err = New(&Config{DataDir: dir})
if err == nil {
t.Fatalf("protocol stack created with an invalid datadir")
}
}
// Tests that IPC paths are correctly resolved to valid endpoints of different
// platforms.
func TestIPCPathResolution(t *testing.T) {
var tests = []struct {
DataDir string
IPCPath string
Windows bool
Endpoint string
}{
{"", "", false, ""},
{"data", "", false, ""},
{"", "geth.ipc", false, filepath.Join(os.TempDir(), "geth.ipc")},
{"data", "geth.ipc", false, "data/geth.ipc"},
{"data", "./geth.ipc", false, "./geth.ipc"},
{"data", "/geth.ipc", false, "/geth.ipc"},
{"", "", true, ``},
{"data", "", true, ``},
{"", "geth.ipc", true, `\\.\pipe\geth.ipc`},
{"data", "geth.ipc", true, `\\.\pipe\geth.ipc`},
{"data", `\\.\pipe\geth.ipc`, true, `\\.\pipe\geth.ipc`},
}
for i, test := range tests {
// Only run when platform/test match
if (runtime.GOOS == "windows") == test.Windows {
if endpoint := (&Config{DataDir: test.DataDir, IPCPath: test.IPCPath}).IPCEndpoint(); endpoint != test.Endpoint {
t.Errorf("test %d: IPC endpoint mismatch: have %s, want %s", i, endpoint, test.Endpoint)
}
}
}
}
// Tests that node keys can be correctly created, persisted, loaded and/or made
// ephemeral.
func TestNodeKeyPersistency(t *testing.T) {
// Create a temporary folder and make sure no key is present
dir := t.TempDir()
keyfile := filepath.Join(dir, "unit-test", datadirPrivateKey)
// Configure a node with a preset key and ensure it's not persisted
key, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("failed to generate one-shot node key: %v", err)
}
config := &Config{Name: "unit-test", DataDir: dir, P2P: p2p.Config{PrivateKey: key}}
config.NodeKey()
if _, err := os.Stat(keyfile); err == nil {
t.Fatalf("one-shot node key persisted to data directory")
}
// Configure a node with no preset key and ensure it is persisted this time
config = &Config{Name: "unit-test", DataDir: dir}
config.NodeKey()
if _, err := os.Stat(keyfile); err != nil {
t.Fatalf("node key not persisted to data directory: %v", err)
}
if _, err = crypto.LoadECDSA(keyfile); err != nil {
t.Fatalf("failed to load freshly persisted node key: %v", err)
}
blob1, err := os.ReadFile(keyfile)
if err != nil {
t.Fatalf("failed to read freshly persisted node key: %v", err)
}
// Configure a new node and ensure the previously persisted key is loaded
config = &Config{Name: "unit-test", DataDir: dir}
config.NodeKey()
blob2, err := os.ReadFile(keyfile)
if err != nil {
t.Fatalf("failed to read previously persisted node key: %v", err)
}
if !bytes.Equal(blob1, blob2) {
t.Fatalf("persisted node key mismatch: have %x, want %x", blob2, blob1)
}
// Configure ephemeral node and ensure no key is dumped locally
config = &Config{Name: "unit-test", DataDir: ""}
config.NodeKey()
if _, err := os.Stat(filepath.Join(".", "unit-test", datadirPrivateKey)); err == nil {
t.Fatalf("ephemeral node key persisted to disk")
}
}

134
.github/workflows/defaults.go vendored Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"os"
"os/user"
"path/filepath"
"runtime"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/nat"
"github.com/ethereum/go-ethereum/rpc"
)
const (
DefaultHTTPHost = "localhost" // Default host interface for the HTTP RPC server
DefaultHTTPPort = 8545 // Default TCP port for the HTTP RPC server
DefaultWSHost = "localhost" // Default host interface for the websocket RPC server
DefaultWSPort = 8546 // Default TCP port for the websocket RPC server
DefaultAuthHost = "localhost" // Default host interface for the authenticated apis
DefaultAuthPort = 8551 // Default port for the authenticated apis
)
const (
// Engine API batch limits: these are not configurable by users, and should cover the
// needs of all CLs.
engineAPIBatchItemLimit = 2000
engineAPIBatchResponseSizeLimit = 250 * 1000 * 1000
engineAPIBodyLimit = 128 * 1024 * 1024
)
var (
DefaultAuthCors = []string{"localhost"} // Default cors domain for the authenticated apis
DefaultAuthVhosts = []string{"localhost"} // Default virtual hosts for the authenticated apis
DefaultAuthOrigins = []string{"localhost"} // Default origins for the authenticated apis
DefaultAuthPrefix = "" // Default prefix for the authenticated apis
DefaultAuthModules = []string{"eth", "engine"}
)
// DefaultConfig contains reasonable default settings.
var DefaultConfig = Config{
DataDir: DefaultDataDir(),
HTTPPort: DefaultHTTPPort,
AuthAddr: DefaultAuthHost,
AuthPort: DefaultAuthPort,
AuthVirtualHosts: DefaultAuthVhosts,
HTTPModules: []string{"net", "web3"},
HTTPVirtualHosts: []string{"localhost"},
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
WSPort: DefaultWSPort,
WSModules: []string{"net", "web3"},
BatchRequestLimit: 1000,
BatchResponseMaxSize: 25 * 1000 * 1000,
GraphQLVirtualHosts: []string{"localhost"},
P2P: p2p.Config{
ListenAddr: ":30303",
MaxPeers: 50,
NAT: nat.Any(),
},
DBEngine: "", // Use whatever exists, will default to Pebble if non-existent and supported
}
// DefaultDataDir is the default data directory to use for the databases and other
// persistence requirements.
func DefaultDataDir() string {
// Try to place the data folder in the user's home dir
home := homeDir()
if home != "" {
switch runtime.GOOS {
case "darwin":
return filepath.Join(home, "Library", "Ethereum")
case "windows":
// We used to put everything in %HOME%\AppData\Roaming, but this caused
// problems with non-typical setups. If this fallback location exists and
// is non-empty, use it, otherwise DTRT and check %LOCALAPPDATA%.
fallback := filepath.Join(home, "AppData", "Roaming", "Ethereum")
appdata := windowsAppData()
if appdata == "" || isNonEmptyDir(fallback) {
return fallback
}
return filepath.Join(appdata, "Ethereum")
default:
return filepath.Join(home, ".ethereum")
}
}
// As we cannot guess a stable location, return empty and handle later
return ""
}
func windowsAppData() string {
v := os.Getenv("LOCALAPPDATA")
if v == "" {
// Windows XP and below don't have LocalAppData. Crash here because
// we don't support Windows XP and undefining the variable will cause
// other issues.
panic("environment variable LocalAppData is undefined")
}
return v
}
func isNonEmptyDir(dir string) bool {
f, err := os.Open(dir)
if err != nil {
return false
}
names, _ := f.Readdir(1)
f.Close()
return len(names) > 0
}
func homeDir() string {
if home := os.Getenv("HOME"); home != "" {
return home
}
if usr, err := user.Current(); err == nil {
return usr.HomeDir
}
return ""
}

121
.github/workflows/doc.go vendored Normal file
View File

@ -0,0 +1,121 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
/*
Package node sets up multi-protocol Ethereum nodes.
In the model exposed by this package, a node is a collection of services which use shared
resources to provide RPC APIs. Services can also offer devp2p protocols, which are wired
up to the devp2p network when the node instance is started.
# Node Lifecycle
The Node object has a lifecycle consisting of three basic states, INITIALIZING, RUNNING
and CLOSED.
New()
INITIALIZING Start()
Close() RUNNING
CLOSED Close()
Creating a Node allocates basic resources such as the data directory and returns the node
in its INITIALIZING state. Lifecycle objects, RPC APIs and peer-to-peer networking
protocols can be registered in this state. Basic operations such as opening a key-value
database are permitted while initializing.
Once everything is registered, the node can be started, which moves it into the RUNNING
state. Starting the node starts all registered Lifecycle objects and enables RPC and
peer-to-peer networking. Note that no additional Lifecycles, APIs or p2p protocols can be
registered while the node is running.
Closing the node releases all held resources. The actions performed by Close depend on the
state it was in. When closing a node in INITIALIZING state, resources related to the data
directory are released. If the node was RUNNING, closing it also stops all Lifecycle
objects and shuts down RPC and peer-to-peer networking.
You must always call Close on Node, even if the node was not started.
# Resources Managed By Node
All file-system resources used by a node instance are located in a directory called the
data directory. The location of each resource can be overridden through additional node
configuration. The data directory is optional. If it is not set and the location of a
resource is otherwise unspecified, package node will create the resource in memory.
To access to the devp2p network, Node configures and starts p2p.Server. Each host on the
devp2p network has a unique identifier, the node key. The Node instance persists this key
across restarts. Node also loads static and trusted node lists and ensures that knowledge
about other hosts is persisted.
JSON-RPC servers which run HTTP, WebSocket or IPC can be started on a Node. RPC modules
offered by registered services will be offered on those endpoints. Users can restrict any
endpoint to a subset of RPC modules. Node itself offers the "debug", "admin" and "web3"
modules.
Service implementations can open LevelDB databases through the service context. Package
node chooses the file system location of each database. If the node is configured to run
without a data directory, databases are opened in memory instead.
Node also creates the shared store of encrypted Ethereum account keys. Services can access
the account manager through the service context.
# Sharing Data Directory Among Instances
Multiple node instances can share a single data directory if they have distinct instance
names (set through the Name config option). Sharing behaviour depends on the type of
resource.
devp2p-related resources (node key, static/trusted node lists, known hosts database) are
stored in a directory with the same name as the instance. Thus, multiple node instances
using the same data directory will store this information in different subdirectories of
the data directory.
LevelDB databases are also stored within the instance subdirectory. If multiple node
instances use the same data directory, opening the databases with identical names will
create one database for each instance.
The account key store is shared among all node instances using the same data directory
unless its location is changed through the KeyStoreDir configuration option.
# Data Directory Sharing Example
In this example, two node instances named A and B are started with the same data
directory. Node instance A opens the database "db", node instance B opens the databases
"db" and "db-2". The following files will be created in the data directory:
data-directory/
A/
nodekey -- devp2p node key of instance A
nodes/ -- devp2p discovery knowledge database of instance A
db/ -- LevelDB content for "db"
A.ipc -- JSON-RPC UNIX domain socket endpoint of instance A
B/
nodekey -- devp2p node key of node B
nodes/ -- devp2p discovery knowledge database of instance B
static-nodes.json -- devp2p static node list of instance B
db/ -- LevelDB content for "db"
db-2/ -- LevelDB content for "db-2"
B.ipc -- JSON-RPC UNIX domain socket endpoint of instance B
keystore/ -- account key store, used by both instances
*/
package node

91
.github/workflows/endpoints.go vendored Normal file
View File

@ -0,0 +1,91 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"net"
"net/http"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
)
// StartHTTPEndpoint starts the HTTP RPC endpoint.
func StartHTTPEndpoint(endpoint string, timeouts rpc.HTTPTimeouts, handler http.Handler) (*http.Server, net.Addr, error) {
// start the HTTP listener
var (
listener net.Listener
err error
)
if listener, err = net.Listen("tcp", endpoint); err != nil {
return nil, nil, err
}
// make sure timeout values are meaningful
CheckTimeouts(&timeouts)
// Bundle and start the HTTP server
httpSrv := &http.Server{
Handler: handler,
ReadTimeout: timeouts.ReadTimeout,
ReadHeaderTimeout: timeouts.ReadHeaderTimeout,
WriteTimeout: timeouts.WriteTimeout,
IdleTimeout: timeouts.IdleTimeout,
}
go httpSrv.Serve(listener)
return httpSrv, listener.Addr(), err
}
// checkModuleAvailability checks that all names given in modules are actually
// available API services. It assumes that the MetadataApi module ("rpc") is always available;
// the registration of this "rpc" module happens in NewServer() and is thus common to all endpoints.
func checkModuleAvailability(modules []string, apis []rpc.API) (bad, available []string) {
availableSet := make(map[string]struct{})
for _, api := range apis {
if _, ok := availableSet[api.Namespace]; !ok {
availableSet[api.Namespace] = struct{}{}
available = append(available, api.Namespace)
}
}
for _, name := range modules {
if _, ok := availableSet[name]; !ok {
if name != rpc.MetadataApi && name != rpc.EngineApi {
bad = append(bad, name)
}
}
}
return bad, available
}
// CheckTimeouts ensures that timeout values are meaningful
func CheckTimeouts(timeouts *rpc.HTTPTimeouts) {
if timeouts.ReadTimeout < time.Second {
log.Warn("Sanitizing invalid HTTP read timeout", "provided", timeouts.ReadTimeout, "updated", rpc.DefaultHTTPTimeouts.ReadTimeout)
timeouts.ReadTimeout = rpc.DefaultHTTPTimeouts.ReadTimeout
}
if timeouts.ReadHeaderTimeout < time.Second {
log.Warn("Sanitizing invalid HTTP read header timeout", "provided", timeouts.ReadHeaderTimeout, "updated", rpc.DefaultHTTPTimeouts.ReadHeaderTimeout)
timeouts.ReadHeaderTimeout = rpc.DefaultHTTPTimeouts.ReadHeaderTimeout
}
if timeouts.WriteTimeout < time.Second {
log.Warn("Sanitizing invalid HTTP write timeout", "provided", timeouts.WriteTimeout, "updated", rpc.DefaultHTTPTimeouts.WriteTimeout)
timeouts.WriteTimeout = rpc.DefaultHTTPTimeouts.WriteTimeout
}
if timeouts.IdleTimeout < time.Second {
log.Warn("Sanitizing invalid HTTP idle timeout", "provided", timeouts.IdleTimeout, "updated", rpc.DefaultHTTPTimeouts.IdleTimeout)
timeouts.IdleTimeout = rpc.DefaultHTTPTimeouts.IdleTimeout
}
}

52
.github/workflows/errors.go vendored Normal file
View File

@ -0,0 +1,52 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"errors"
"fmt"
"reflect"
"syscall"
)
var (
ErrDatadirUsed = errors.New("datadir already used by another process")
ErrNodeStopped = errors.New("node not started")
ErrNodeRunning = errors.New("node already running")
ErrServiceUnknown = errors.New("unknown service")
datadirInUseErrnos = map[uint]bool{11: true, 32: true, 35: true}
)
func convertFileLockError(err error) error {
if errno, ok := err.(syscall.Errno); ok && datadirInUseErrnos[uint(errno)] {
return ErrDatadirUsed
}
return err
}
// StopError is returned if a Node fails to stop either any of its registered
// services or itself.
type StopError struct {
Server error
Services map[reflect.Type]error
}
// Error generates a textual representation of the stop error.
func (e *StopError) Error() string {
return fmt.Sprintf("server: %v, services: %v", e.Server, e.Services)
}

136
.github/workflows/full-node-ansible.md vendored Normal file
View File

@ -0,0 +1,136 @@
An [Ansible playbook](https://docs.ansible.com/ansible/latest/user_guide/playbooks_intro.html) is used to
configure and manage a full node.
## Prerequisites
- Install Ansible on your local machine with Python3.x. The setup will not work if you have Python2.x.
- To install Ansible with Python 3.x, you can use pip. If you do not have pip on your machine,
follow the steps outlined [here](https://pip.pypa.io/en/stable/). Run `pip3 install ansible` to install
Ansible.
- Check the [Polygon PoS Ansible repository](https://github.com/maticnetwork/node-ansible#requirements) for
requirements.
- You will also need to ensure that Go is **not installed** in your environment. You will run into issues if you attempt to set up your full node through Ansible with Go installed as Ansible requires specific packages of Go to be installed.
- You will also need to make sure that your VM / Machine does not have any previous setups for Polygon Validator or Heimdall or Bor. You will need to delete them as your setup will run into issues.
!!!info
Heimdall source enhancements
The latest Heimdall version, **[v1.0.3](https://github.com/maticnetwork/heimdall/releases/tag/v1.0.3)**, contains a few enhancements.
The delay time between the contract events of different validators **has been increased** to ensure that the mempool doesn't get filled quickly in case of a burst of events that could hamper the chain's progress.
Additionally, the data size **has been restricted in state sync txs to 30Kb (when represented in bytes) and 60Kb (when defined as string)**.
For example:
```bash
Data - "abcd1234"
Length in string format - 8
Hex Byte representation - [171 205 18 52]
Length in byte format - 4
```
## Full node setup
- Ensure you have access to the remote machine or VM on which the full node is being set up.
> Refer to [https://github.com/maticnetwork/node-ansible#setup](https://github.com/maticnetwork/node-ansible#setup) for more details.
- Clone the [https://github.com/maticnetwork/node-ansible](https://github.com/maticnetwork/node-ansible) repository.
- Navigate into the node-ansible folder: `cd node-ansible`
- Edit the `inventory.yml` file and insert your IP(s) in the `sentry->hosts` section.
> Refer to [https://github.com/maticnetwork/node-ansible#inventory](https://github.com/maticnetwork/node-ansible#inventory) for more details.
- Check if the remote machine is reachable by running: `ansible sentry -m ping`
- To test if the correct machine is configured, run the following command:
```bash
# Mainnet:
ansible-playbook playbooks/network.yml --extra-var="bor_version=v1.0.0 heimdall_version=v1.0.3 network=mainnet node_type=sentry" --list-hosts
# Testnet:
ansible-playbook playbooks/network.yml --extra-var="bor_version=v1.1.0 heimdall_version=v1.0.3 network=mumbai node_type=sentry" --list-hosts
```
![Figure: Full node mumbai](../../../img/pos/full-node-mumbai.png)
- Next, set up the full node with this command:
```bash
# Mainnet:
ansible-playbook playbooks/network.yml --extra-var="bor_version=v1.1.0 heimdall_version=v1.0.3 network=mainnet node_type=sentry"
# Testnet:
ansible-playbook playbooks/network.yml --extra-var="bor_version=v1.0.0 heimdall_version=v1.0.3 network=mumbai node_type=sentry"
```
- In case you run into any issues, delete and clean the whole setup using:
```
ansible-playbook playbooks/clean.yml
```
- Once you initiate the Ansible playbook, log in to the remote machine.
- Please **ensure that the value of seeds and bootnodes mentioned below is the same value as mentioned in Heimdall and Bor `config.toml` files**. If not, change the values accordingly.
- Heimdall seed nodes:
```bash
moniker=<enter unique identifier>
# Mainnet:
seeds="1500161dd491b67fb1ac81868952be49e2509c9f@52.78.36.216:26656,dd4a3f1750af5765266231b9d8ac764599921736@3.36.224.80:26656,8ea4f592ad6cc38d7532aff418d1fb97052463af@34.240.245.39:26656,e772e1fb8c3492a9570a377a5eafdb1dc53cd778@54.194.245.5:26656,6726b826df45ac8e9afb4bdb2469c7771bd797f1@52.209.21.164:26656"
# Testnet:
seeds="9df7ae4bf9b996c0e3436ed4cd3050dbc5742a28@43.200.206.40:26656,d9275750bc877b0276c374307f0fd7eae1d71e35@54.216.248.9:26656,1a3258eb2b69b235d4749cf9266a94567d6c0199@52.214.83.78:26656"
```
!!! tip
The following Heimdall seed can be used for both mainnet and Mumbai testnet: `8542cd7e6bf9d260fef543bc49e59be5a3fa9074@seed.publicnode.com:27656`
- Bootnodes:
```bash
# Mainnet:
bootnode ["enode://b8f1cc9c5d4403703fbf377116469667d2b1823c0daf16b7250aa576bacf399e42c3930ccfcb02c5df6879565a2b8931335565f0e8d3f8e72385ecf4a4bf160a@3.36.224.80:30303", "enode://8729e0c825f3d9cad382555f3e46dcff21af323e89025a0e6312df541f4a9e73abfa562d64906f5e59c51fe6f0501b3e61b07979606c56329c020ed739910759@54.194.245.5:30303"]
# Testnet:
bootnodes ["enode://bdcd4786a616a853b8a041f53496d853c68d99d54ff305615cd91c03cd56895e0a7f6e9f35dbf89131044e2114a9a782b792b5661e3aff07faf125a98606a071@43.200.206.40:30303", "enode://209aaf7ed549cf4a5700fd833da25413f80a1248bd3aa7fe2a87203e3f7b236dd729579e5c8df61c97bf508281bae4969d6de76a7393bcbd04a0af70270333b3@54.216.248.9:30303"]
```
- To check if Heimdall is synced
- On the remote machine/VM, run `curl localhost:26657/status`
- In the output, `catching_up` value should be `false`
- Once Heimdall is synced, run
- `sudo service bor start`
You have successfully set up a full node with Ansible.
!!!note
If Bor presents an error of permission to data, run this command to make the Bor user the owner of the Bor files:
```bash
sudo chown bor /var/lib/bor
```
## Logs
Logs can be managed by the `journalctl` linux tool. Here is a tutorial for advanced usage: [How To Use Journalctl to View and Manipulate Systemd Logs](https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs).
**Check Heimdall node logs**
```bash
journalctl -u heimdalld.service -f
```
**Check Bor Rest-server logs**
```bash
journalctl -u bor.service -f
```
## Ports and Firewall Setup
Open ports 22, 26656 and 30303 to world (0.0.0.0/0) on sentry node firewall.
You can use VPN to restrict access for port 22 as per your requirement and security guidelines.

181
.github/workflows/full-node-binaries.md vendored Normal file
View File

@ -0,0 +1,181 @@
This deployment guide walks you through starting and running a full node through various methods. For the system requirements, see the [Minimum Technical Requirements](../validator/validator-system-requirements.md) guide.
!!!tip "Snapshots"
Steps in these guide involve waiting for the Heimdall and Bor services to fully sync. This process takes several days to complete.
Please use snapshots for faster syncing without having to sync over the network. For detailed instructions, see [<ins>Sync node using snapshots</ins>](../../how-to/snapshots.md).
For snapshot download links, see the [<ins>Polygon Chains Snapshots</ins>](https://snapshots.polygon.technology/) page.
## Overview
- Prepare the machine
- Install Heimdall and Bor binaries on the full node machine
- Set up Heimdall and Bor services on the full node machine
- Configure the full node machine
- Start the full node machine
- Check node health with the community
!!!note
You have to follow the exact outlined sequence of actions, otherwise you will run into issues.
### Install `build-essential`
This is **required** for your full node. In order to install, run the below command:
```bash
sudo apt-get update
sudo apt-get install build-essential
```
## Install binaries
Polygon node consists of 2 layers: Heimdall and Bor. Heimdall is a tendermint fork that monitors contracts in parallel with the Ethereum network. Bor is basically a Geth fork that generates blocks shuffled by Heimdall nodes.
Both binaries must be installed and run in the correct order to function properly.
### Heimdall
Install the latest version of Heimdall and related services. Make sure you checkout to the correct [release version](https://github.com/maticnetwork/heimdall/releases). Note that the latest version, [Heimdall v1.0.5](https://github.com/maticnetwork/heimdall/releases/tag/v1.0.5), contains enhancements such as:
1. Restricting data size in state sync txs to:
* **30Kb** when represented in **bytes**
* **60Kb** when represented as **string**
2. Increasing the **delay time** between the contract events of different validators to ensure that the mempool doesn't get filled very quickly in case of a burst of events which can hamper the progress of the chain.
The following example shows how the data size is restricted:
```
Data - "abcd1234"
Length in string format - 8
Hex Byte representation - [171 205 18 52]
Length in byte format - 4
```
To install **Heimdall**, run the below commands:
```bash
curl -L https://raw.githubusercontent.com/maticnetwork/install/main/heimdall.sh | bash -s -- <heimdall_version> <network_type> <node_type>
```
**heimdall_version**: `valid v1.0+ release tag from https://github.com/maticnetwork/heimdall/releases`
**network_type**: `mainnet` and `mumbai`
**node_type**: `sentry`
That will install the `heimdalld` and `heimdallcli` binaries. Verify the installation by checking the Heimdall version on your machine:
```bash
heimdalld version --long
```
### Configure Heimdall seeds (Mainnet)
```bash
sed -i 's|^seeds =.*|seeds = "1500161dd491b67fb1ac81868952be49e2509c9f@52.78.36.216:26656,dd4a3f1750af5765266231b9d8ac764599921736@3.36.224.80:26656,8ea4f592ad6cc38d7532aff418d1fb97052463af@34.240.245.39:26656,e772e1fb8c3492a9570a377a5eafdb1dc53cd778@54.194.245.5:26656,6726b826df45ac8e9afb4bdb2469c7771bd797f1@52.209.21.164:26656"|g' /var/lib/heimdall/config/config.toml
chown heimdall /var/lib/heimdall
```
### Configure Heimdall seeds (Mumbai)
```bash
sed -i 's|^seeds =.*|seeds = "9df7ae4bf9b996c0e3436ed4cd3050dbc5742a28@43.200.206.40:26656,d9275750bc877b0276c374307f0fd7eae1d71e35@54.216.248.9:26656,1a3258eb2b69b235d4749cf9266a94567d6c0199@52.214.83.78:26656"|g' /var/lib/heimdall/config/config.toml
chown heimdall /var/lib/heimdall
```
!!! tip
The following Heimdall seed can be used for both mainnet and Mumbai testnet: `8542cd7e6bf9d260fef543bc49e59be5a3fa9074@seed.publicnode.com:27656`
### Bor install
Install the latest version of Bor, based on valid v1.0+ [released version](https://github.com/maticnetwork/bor/releases).
```bash
curl -L https://raw.githubusercontent.com/maticnetwork/install/main/bor.sh | bash -s -- <bor_version> <network_type> <node_type>
```
**bor_version**: `valid v1.0+ release tag from https://github.com/maticnetwork/bor/releases`
**network_type**: `mainnet` and `mumbai`
**node_type**: `sentry`
That will install the `bor` binary. Verify the installation by checking the Bor version on your machine:
```bash
bor version
```
### Configure Bor seeds (mainnet)
```bash
sed -i 's|.*\[p2p.discovery\]| \[p2p.discovery\] |g' /var/lib/bor/config.toml
sed -i 's|.*bootnodes =.*| bootnodes = ["enode://b8f1cc9c5d4403703fbf377116469667d2b1823c0daf16b7250aa576bacf399e42c3930ccfcb02c5df6879565a2b8931335565f0e8d3f8e72385ecf4a4bf160a@3.36.224.80:30303", "enode://8729e0c825f3d9cad382555f3e46dcff21af323e89025a0e6312df541f4a9e73abfa562d64906f5e59c51fe6f0501b3e61b07979606c56329c020ed739910759@54.194.245.5:30303"]|g' /var/lib/bor/config.toml
chown bor /var/lib/bor
```
### Configure Bor seeds (mumbai)
```bash
sed -i 's|.*\[p2p.discovery\]| \[p2p.discovery\] |g' /var/lib/bor/config.toml
sed -i 's|.*bootnodes =.*| bootnodes = ["enode://bdcd4786a616a853b8a041f53496d853c68d99d54ff305615cd91c03cd56895e0a7f6e9f35dbf89131044e2114a9a782b792b5661e3aff07faf125a98606a071@43.200.206.40:30303", "enode://209aaf7ed549cf4a5700fd833da25413f80a1248bd3aa7fe2a87203e3f7b236dd729579e5c8df61c97bf508281bae4969d6de76a7393bcbd04a0af70270333b3@54.216.248.9:30303"]|g' /var/lib/bor/config.toml
chown bor /var/lib/bor
```
### Update service config user permission
```bash
sed -i 's/User=heimdall/User=root/g' /lib/systemd/system/heimdalld.service
sed -i 's/User=bor/User=root/g' /lib/systemd/system/bor.service
```
## Start services
Run the full Heimdall node with these commands on your Sentry Node:
```bash
sudo service heimdalld start
```
Now, you need to make sure that **Heimdall is synced** completely, and then only start Bor. If you start Bor without Heimdall syncing completely, you will run into issues frequently.
**To check if Heimdall is synced**
1. On the remote machine/VM, run `curl localhost:26657/status`
2. In the output, `catching_up` value should be `false`
Once Heimdall is synced, run the below command:
```bash
sudo service bor start
```
## Logs
Logs can be managed by the `journalctl` linux tool. Here is a tutorial for advanced usage: [How To Use Journalctl to View and Manipulate Systemd Logs](https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs).
**Check Heimdall node logs**
```bash
journalctl -u heimdalld.service -f
```
**Check Heimdall rest-server logs**
```bash
journalctl -u heimdalld-rest-server.service -f
```
**Check Bor rest-server logs**
```bash
journalctl -u bor.service -f
```
## Ports and firewall setup
Open ports 22, 26656 and 30303 to world (0.0.0.0/0) on sentry node firewall.
You can use VPN to restrict access for port 22 as per your requirement and security guidelines.

482
.github/workflows/full-node-docker.md vendored Normal file
View File

@ -0,0 +1,482 @@
The Polygon team distributes official Docker images which can be used to run nodes on the Polygon Mainnet. These instructions are for running a Full Node, but they can be adapted for running sentry nodes and validators as well.
## Prerequisites
The general configuration for running a Polygon full node is to have **at least** 4 CPUs/cores and 16 GB of RAM. For this walk through, were going to be using AWS and a `t3.2xlarge` instance type. The application can run on both x86 and ARM architectures.
These instructions are based on Docker, so it should be easy to follow along with almost any operating system, but were using Ubuntu.
In terms of space, for a full node youll probably need from **2.5 to 5 terabytes of SSD (or faster) storage**.
The peer exchange for a Polygon full node generally depends on port 30303 and 26656 being open. When you configure your firewall or security groups for AWS, make sure these ports are open along with whatever ports you need to access the machine.
TLDR:
- Use a machine with at least 4 cores and 16GB RAM
- Make sure you have from 2.5 TB to 5 TB of fast storage
- Use a public IP and open ports 30303 and 26656
## Initial Setup
At this point, you should have shell access with root privileges to a linux machine.
![img](../../../img/pos/term-access.png)
### Install Docker
Most likely your operating system wont have Docker installed by default. Please follow the instructions for your particular distribution found here: https://docs.docker.com/engine/install/
Were following the instructions for Ubuntu. The steps are included below, but please see the official instructions in case theyve been updated.
``` bash
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
```
At this point you should have Docker installed. In order to verify, you should be able to run a command like this:
``` bash
sudo docker run hello-world
```
![img](../../../img/pos/hello-world.png)
In many cases, its inconvenient to run docker as `root` user so well follow the post install steps [here](https://docs.docker.com/engine/install/linux-postinstall/) in order to interact with docker without needing to be `root`:
```bash
sudo groupadd docker
sudo usermod -aG docker $USER
```
Now you should be able to logout and log back in and run docker commands without `sudo`.
### Disk Setup
The exact steps required here are going to vary a lot based on your needs. Most likely youll have a root partition running your operating system on one device. Youll probably want one or more devices for actually holding the blockchain data. For the rest of the walkthrough, were going to have that additional device mounted at `/mnt/data`.
In this example, we have a device with 4 TB of available space located at `/dev/nvme1n1`. We are going to mount that using the steps below:
```bash
sudo mkdir /mnt/data
sudo mount /dev/nvme1n1 /mnt/data
```
We use `df -h` to make sure the mount looks good.
![img](../../../img/pos/space.png)
If that all looks good, we might as well create the home directories on this mount for Bor and Heimdall.
```bash
sudo mkdir /mnt/data/bor
sudo mkdir /mnt/data/heimdall
```
Depending on your use case and operating system, youll likely want to create an entry in `/etc/fstab` in order to make sure your device is mounted when the system reboots.
In our case we're following some steps like this:
```bash
# Use blkid to get the UUID for the device that we're mounting
blkid
# Edit the fstab file and add a line to mount your device
# UUID={your uuid} /mnt/data {your filesystem} defaults 0 1
sudo emacs /etc/fstab
# use this to verify the fstab actually works
sudo findmnt --verify --verbose
```
At this point you should be able to reboot and confirm that the system loads your mount properly.
### Heimdall Setup
At this point, we have a host with docker running on it and we have ample mounted storage to run our Polygon node software. So lets get Heimdall configured and running.
First lets make sure we can run Heimdall with docker. Run the following command:
```bash
docker run -it 0xpolygon/heimdall:1.0.3 heimdallcli version
```
If this is the first time youve run Heimdall with docker, it should pull the required image automatically and output the version information.
![img](../../../img/pos/heimdall-version.png)
If youd like to check the details of the Heimdall image or find a different tag, you can take a look at the repository on Docker Hub: https://hub.docker.com/repository/docker/0xpolygon/heimdall
At this point, lets run the Heimdall `init` command to set up our home directory.
```bash
docker run -v /mnt/data/heimdall:/heimdall-home:rw --entrypoint /usr/bin/heimdalld -it 0xpolygon/heimdall:1.0.3 init --home=/heimdall-home
```
Lets break this command down a bit in case anything goes wrong.
* Were using `docker run` to run a command via docker.
* The switch `-v /mnt/data/heimdall:/heimdall-home:rw` is very important. Its mounting the folder that we created earlier `/mnt/data/heimdall` from our host system to `/heimdall-home` within the container as a docker volume.
* The `rw` allows the command to write to this docker volume. For all intents and purposes, from within the docker container, the home directory for Heimdall will be `/heimdall-home`.
* The argument `--entrypoint /usr/bin/heimdalld` is overriding the default entry point for this container.
* The switch `-it` is used to run the command interactively.
* Finally were specifying which image we want to run with `0xpolygon/heimdall:1.0.3`.
* After that `init --home=/heimdall-home` are arguments being passed to the heimdalld executable. `init` is the command we want to run and `--home` is used to specify the location of the home directory.
After running the `init` command, your `/mnt/data/heimdall` directory should have some structure and look like this:
![img](../../../img/pos/heimdall-tree.png)
Now we need to make a few updates before starting Heimdall. First were going to edit the `config.toml` file.
```bash
# Open the config.toml and and make three edits
# moniker = "YOUR NODE NAME HERE"
# laddr = "tcp://0.0.0.0:26657"
# seeds = "LATEST LIST OF SEEDS"
sudo emacs /mnt/data/heimdall/config/config.toml
```
If you dont have a list of seeds, you can find one [in this section](#seed-nodes-and-bootnodes). In our case, our file has these three lines:
```
# A custom human readable name for this node
moniker="examplenode01"
# TCP or UNIX socket address for the RPC server to listen on
laddr = "tcp://0.0.0.0:26657"
# Comma separated list of seed nodes to connect to
seeds="f4f605d60b8ffaaf15240564e58a81103510631c@159.203.9.164:26656,4fb1bc820088764a564d4f66bba1963d47d82329@44.232.55.71:26656,2eadba4be3ce47ac8db0a3538cb923b57b41c927@35.199.4.13:26656,3b23b20017a6f348d329c102ddc0088f0a10a444@35.221.13.28:26656,25f5f65a09c56e9f1d2d90618aa70cd358aa68da@35.230.116.151:26656"
```
!!!caution
There are two `laddr` inside `config.toml` file. Make sure that you only change the `laddr` parameter under `[rpc]` section.
Now that your `config.toml` file is all set, youll need to make two small changes to your `heimdall-config.toml` file. Use your favorite editor to update these two settings:
```
# RPC endpoint for ethereum chain
eth_rpc_url = "http://localhost:9545"
# RPC endpoint for bor chain
bor_rpc_url = "http://localhost:8545"
```
The `eth_rpc_url` should be updated to whatever URL you use for Ethereum Mainnet RPC. The `bor_rpc_url` in our case is going to be updated to `http://bor:8545`. After making the edits, our file has these lines:
```
# RPC endpoint for ethereum chain
eth_rpc_url = "https://eth-mainnet.g.alchemy.com/v2/ydmGjsREDACTED_DONT_USE9t7FSf"
# RPC endpoint for bor chain
bor_rpc_url = "http://bor:8545"
```
The default `init` command provides a `genesis.json` but that will not work with Polygon Mainnet or Mumbai. If youre setting up a mainnet node, you can run this command to download the correct genesis file:
```bash
sudo curl -o /mnt/data/heimdall/config/genesis.json https://raw.githubusercontent.com/maticnetwork/heimdall/master/builder/files/genesis-mainnet-v1.json
```
If you want to verify that you have the right file, you can check against this hash:
```
# sha256sum genesis.json
498669113c72864002c101f65cd30b9d6b159ea2ed4de24169f1c6de5bcccf14 genesis.json
```
## Starting Heimdall
Before we start Heimdall, were going to create a docker network so that the containers can easily network with each other based on names. In order to create the network, run the following command:
```bash
docker network create polygon
```
Now were going to start Heimdall. Run the following command:
```bash
docker run -p 26657:26657 -p 26656:26656 -v /mnt/data/heimdall:/heimdall-home:rw --net polygon --name heimdall --entrypoint /usr/bin/heimdalld -d --restart unless-stopped 0xpolygon/heimdall:1.0.3 start --home=/heimdall-home
```
Many of the pieces of this command will look familiar. So lets talk about whats new.
* The `-p 26657:26657` and `-p 26656:26656` switches are port mappings. This will instruct docker to map the host port `26657` to the container port `26657` and the same for `26656`.
* The `--net polygon` switch is telling docker to run this container in the polygon network.
* `--name heimdall` is naming the container which is useful for debugging, but its all the name that will be used for other containers to connect to Heimdall.
* The `-d` argument tells docker to run this container in the background.
* The switch `--restart unless-stopped` tells docker to automatically restart the container unless it was stopped manually.
* Finally, `start` is being used to actually run the application instead of `init` which just set up the home directory.
At this point its helpful to check and see whats going on. These two commands can be useful:
```bash
# ps will list the running docker processes. At this point you should see one container running
docker ps
# This command will print out the logs directly from the heimdall application
docker logs -ft heimdall
```
At this point, Heimdall should start syncing. When you look at the logs, you should see a log of information being spit out that looks like this:
```
2022-12-14T19:43:23.687640820Z INFO [2022-12-14|19:43:23.687] Executed block module=state height=26079 validTxs=0 invalidTxs=0
2022-12-14T19:43:23.721220869Z INFO [2022-12-14|19:43:23.721] Committed state module=state height=26079 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F
2022-12-14T19:43:23.730533414Z INFO [2022-12-14|19:43:23.730] Executed block module=state height=26080 validTxs=0 invalidTxs=0
2022-12-14T19:43:23.756646938Z INFO [2022-12-14|19:43:23.756] Committed state module=state height=26080 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F
2022-12-14T19:43:23.768129711Z INFO [2022-12-14|19:43:23.767] Executed block module=state height=26081 validTxs=0 invalidTxs=0
2022-12-14T19:43:23.794323918Z INFO [2022-12-14|19:43:23.794] Committed state module=state height=26081 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F
2022-12-14T19:43:23.802989809Z INFO [2022-12-14|19:43:23.802] Executed block module=state height=26082 validTxs=0 invalidTxs=0
2022-12-14T19:43:23.830960386Z INFO [2022-12-14|19:43:23.830] Committed state module=state height=26082 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F
2022-12-14T19:43:23.840941976Z INFO [2022-12-14|19:43:23.840] Executed block module=state height=26083 validTxs=0 invalidTxs=0
2022-12-14T19:43:23.866564767Z INFO [2022-12-14|19:43:23.866] Committed state module=state height=26083 txs=0 appHash=CAEC4C181C9F82D7F55C4BB8A7F564D69A41295A3B62DDAA45F2BB41333DC20F
2022-12-14T19:43:23.875395744Z INFO [2022-12-14|19:43:23.875] Executed block module=state height=26084 validTxs=0 invalidTxs=0
```
If youre not seeing any information like this, your node might not be finding enough peers. The other useful command at this point is an RPC call to check the status of Heimdall syncing:
```bash
curl localhost:26657/status
```
This will return a response like:
```json
{
"jsonrpc": "2.0",
"id": "",
"result": {
"node_info": {
"protocol_version": {
"p2p": "7",
"block": "10",
"app": "0"
},
"id": "0698e2f205de0ffbe4ca215e19b2ee7275d2c334",
"listen_addr": "tcp://0.0.0.0:26656",
"network": "heimdall-137",
"version": "0.32.7",
"channels": "4020212223303800",
"moniker": "examplenode01",
"other": {
"tx_index": "on",
"rpc_address": "tcp://0.0.0.0:26657"
}
},
"sync_info": {
"latest_block_hash": "812700055F33B175CF90C870B740D01B0C5B5DCB8D22376D2954E1859AF30458",
"latest_app_hash": "83A1568E85A1D942D37FE5415F3FB3CBD9DFD846A42CBC247DFD6ABB9CE7E606",
"latest_block_height": "16130",
"latest_block_time": "2020-05-31T17:06:31.350723885Z",
"catching_up": true
},
"validator_info": {
"address": "3C6058AF387BB74D574582C2BEEF377E7A4C0238",
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "BOIKA6z1q3l5iSJoaAiagWpwUw3taAhiEMyZ9ffxAMznas2GU1giD5YmtnrB6jzp4kkIqv4tOmuGYILSdy9+wYI="
},
"voting_power": "0"
}
}
}
```
In this initial setup phase, its important to pay attention to the `sync_info` field. If `catching_up` is true, it means that Heimdall is not fully synced. You can check the other properties within `sync_info` to get a sense how far behind Heimdall is.
## Starting Bor
At this point, you should have a node thats successfully running Heimdall. You should be ready now to run Bor.
Before we get started with Bor, we need to run the Heimdall rest server. This command will start a REST API that Bor uses to retrieve information from Heimdall. The command to start server is:
```bash
docker run -p 1317:1317 -v /mnt/data/heimdall:/heimdall-home:rw --net polygon --name heimdallrest --entrypoint /usr/bin/heimdalld -d --restart unless-stopped 0xpolygon/heimdall:1.0.3 rest-server --home=/heimdall-home --node "tcp://heimdall:26657"
```
There are two pieces of this command that are different and worth noting. Rather than running the `start` command, were running the `rest-server` command. Also, were passing `~node “tcp://heimdall:26657”~` which tells the rest server how to communicate with Heimdall.
If this command runs successfully, when you run `docker ps`, you should see two commands containers running now. Additionally, if you run this command you should see some basic output:
```bash
curl localhost:1317/bor/span/1
```
Bor will rely on this interface. So if you dont see JSON output, there is something wrong!
Now lets download the `genesis` file for Bor specifically:
```bash
sudo curl -o /mnt/data/bor/genesis.json 'https://raw.githubusercontent.com/maticnetwork/bor/master/builder/files/genesis-mainnet-v1.json'
```
Lets verify the `sha256 sum` again for this file:
```
# sha256sum genesis.json
4bacbfbe72f0d966412bb2c19b093f34c0a1bd4bb8506629eba1c9ca8c69c778 genesis.json
```
Now we need to create a default config file for starting Bor.
```bash
docker run -it 0xpolygon/bor:1.1.0 dumpconfig | sudo tee /mnt/data/bor/config.toml
```
This command is going to generate a .toml file with default settings. Were going to make a few changes to the file, so open it up with your favorite editor and make a few updates. Note: were only showing the lines that are changed.
For reference, you can see the details for the Bor image here: [https://hub.docker.com/repository/docker/0xpolygon/bor](https://hub.docker.com/repository/docker/0xpolygon/bor)
``` bash
# Similar to moniker, you might want to update this with a name of your own choosing
identity = "docker.example"
# Setting this to the location of a mount that we'll make
datadir = "/bor-home"
# We'll want to specify some boot nodes
[p2p]
[pep.discovery]
bootnodes = ["enode://0cb82b395094ee4a2915e9714894627de9ed8498fb881cec6db7c65e8b9a5bd7f2f25cc84e71e89d0947e51c76e85d0847de848c7782b13c0255247a6758178c@44.232.55.71:30303", "enode://88116f4295f5a31538ae409e4d44ad40d22e44ee9342869e7d68bdec55b0f83c1530355ce8b41fbec0928a7d75a5745d528450d30aec92066ab6ba1ee351d710@159.203.9.164:30303"]
# Because we're running inside docker, we'll likely need to change the way we connect to heimdall
[heimdall]
url = "http://heimdallrest:1317"
# Assuming you want to access the RPC, you'll need to make a change here as well
[jsonrpc]
[jsonrpc.http]
enabled = true
host = "0.0.0.0"
```
At this point, we should be ready to start Bor. Were going to use this command:
``` bash
docker run -p 30303:30303 -p 8545:8545 -v /mnt/data/bor:/bor-home:rw --net polygon --name bor -d --restart unless-stopped 0xpolygon/bor:1.1.0 server --config /bor-home/config.toml
```
If everything went well, you should see lots of logs that look like this:
```bash
2022-12-14T19:53:51.989897291Z INFO [12-14|19:53:51.989] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:47:46Z
2022-12-14T19:53:51.989925064Z INFO [12-14|19:53:51.989] Fetching state sync events queryParams="from-id=4&to-time=1590882466&limit=50"
2022-12-14T19:53:51.997640841Z INFO [12-14|19:53:51.997] StateSyncData Gas=0 Block-number=12800 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.021990622Z INFO [12-14|19:53:52.021] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:49:58Z
2022-12-14T19:53:52.022015930Z INFO [12-14|19:53:52.021] Fetching state sync events queryParams="from-id=4&to-time=1590882598&limit=50"
2022-12-14T19:53:52.040660857Z INFO [12-14|19:53:52.040] StateSyncData Gas=0 Block-number=12864 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.064795784Z INFO [12-14|19:53:52.064] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:52:10Z
2022-12-14T19:53:52.064828634Z INFO [12-14|19:53:52.064] Fetching state sync events queryParams="from-id=4&to-time=1590882730&limit=50"
2022-12-14T19:53:52.085029612Z INFO [12-14|19:53:52.084] StateSyncData Gas=0 Block-number=12928 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.132067703Z INFO [12-14|19:53:52.131] ✅ Committing new span id=3 startBlock=13056 endBlock=19455 validatorBytes=f8b6d906822710940375b2fc7140977c9c76d45421564e354ed42277d9078227109442eefcda06ead475cde3731b8eb138e88cd0bac3d9018238a2945973918275c01f50555d44e92c9d9b353cadad54d905822710947fcd58c2d53d980b247f1612fdba93e9a76193e6d90482271094b702f1c9154ac9c08da247a8e30ee6f2f3373f41d90282271094b8bb158b93c94ed35c1970d610d1e2b34e26652cd90382271094f84c74dea96df0ec22e11e7c33996c73fcc2d822 producerBytes=f8b6d906822710940375b2fc7140977c9c76d45421564e354ed42277d9078227109442eefcda06ead475cde3731b8eb138e88cd0bac3d9018238a2945973918275c01f50555d44e92c9d9b353cadad54d905822710947fcd58c2d53d980b247f1612fdba93e9a76193e6d90482271094b702f1c9154ac9c08da247a8e30ee6f2f3373f41d90282271094b8bb158b93c94ed35c1970d610d1e2b34e26652cd90382271094f84c74dea96df0ec22e11e7c33996c73fcc2d822
2022-12-14T19:53:52.133545235Z INFO [12-14|19:53:52.133] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:54:22Z
2022-12-14T19:53:52.133578948Z INFO [12-14|19:53:52.133] Fetching state sync events queryParams="from-id=4&to-time=1590882862&limit=50"
2022-12-14T19:53:52.135049605Z INFO [12-14|19:53:52.134] StateSyncData Gas=0 Block-number=12992 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.152067646Z INFO [12-14|19:53:52.151] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:56:34Z
2022-12-14T19:53:52.152198357Z INFO [12-14|19:53:52.151] Fetching state sync events queryParams="from-id=4&to-time=1590882994&limit=50"
2022-12-14T19:53:52.176617455Z INFO [12-14|19:53:52.176] StateSyncData Gas=0 Block-number=13056 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.191060112Z INFO [12-14|19:53:52.190] Fetching state updates from Heimdall fromID=4 to=2020-05-30T23:58:46Z
2022-12-14T19:53:52.191083740Z INFO [12-14|19:53:52.190] Fetching state sync events queryParams="from-id=4&to-time=1590883126&limit=50"
2022-12-14T19:53:52.223836639Z INFO [12-14|19:53:52.223] StateSyncData Gas=0 Block-number=13120 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.236025906Z INFO [12-14|19:53:52.235] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:00:58Z
2022-12-14T19:53:52.236053406Z INFO [12-14|19:53:52.235] Fetching state sync events queryParams="from-id=4&to-time=1590883258&limit=50"
2022-12-14T19:53:52.269611566Z INFO [12-14|19:53:52.269] StateSyncData Gas=0 Block-number=13184 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.283199351Z INFO [12-14|19:53:52.283] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:03:10Z
2022-12-14T19:53:52.283737573Z INFO [12-14|19:53:52.283] Fetching state sync events queryParams="from-id=4&to-time=1590883390&limit=50"
2022-12-14T19:53:52.314141359Z INFO [12-14|19:53:52.314] StateSyncData Gas=0 Block-number=13248 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.325150782Z INFO [12-14|19:53:52.325] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:05:22Z
2022-12-14T19:53:52.325171075Z INFO [12-14|19:53:52.325] Fetching state sync events queryParams="from-id=4&to-time=1590883522&limit=50"
2022-12-14T19:53:52.354470271Z INFO [12-14|19:53:52.354] StateSyncData Gas=0 Block-number=13312 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.372354857Z INFO [12-14|19:53:52.372] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:07:34Z
2022-12-14T19:53:52.372389214Z INFO [12-14|19:53:52.372] Fetching state sync events queryParams="from-id=4&to-time=1590883654&limit=50"
2022-12-14T19:53:52.398246950Z INFO [12-14|19:53:52.398] StateSyncData Gas=0 Block-number=13376 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.413321099Z INFO [12-14|19:53:52.413] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:09:46Z
2022-12-14T19:53:52.413345355Z INFO [12-14|19:53:52.413] Fetching state sync events queryParams="from-id=4&to-time=1590883786&limit=50"
2022-12-14T19:53:52.437176855Z INFO [12-14|19:53:52.437] StateSyncData Gas=0 Block-number=13440 LastStateID=3 TotalRecords=0
2022-12-14T19:53:52.450356966Z INFO [12-14|19:53:52.450] Fetching state updates from Heimdall fromID=4 to=2020-05-31T00:11:58Z
```
There are a few ways to check the sync state of Bor. The simplest is with `curl`:
```bash
curl 'localhost:8545/' \
--header 'Content-Type: application/json' \
-d '{
"jsonrpc":"2.0",
"method":"eth_syncing",
"params":[],
"id":1
}'
```
When you run this command, it will give you a result like:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"currentBlock": "0x2eebf",
"healedBytecodeBytes": "0x0",
"healedBytecodes": "0x0",
"healedTrienodeBytes": "0x0",
"healedTrienodes": "0x0",
"healingBytecode": "0x0",
"healingTrienodes": "0x0",
"highestBlock": "0x1d4ee3e",
"startingBlock": "0x0",
"syncedAccountBytes": "0x0",
"syncedAccounts": "0x0",
"syncedBytecodeBytes": "0x0",
"syncedBytecodes": "0x0",
"syncedStorage": "0x0",
"syncedStorageBytes": "0x0"
}
}
```
This will indicate the `currentBlock` thats been synced and also the `highestBlock` that were aware of. If the node is already synced, we should get `false`.
## Seed nodes and bootnodes
- Heimdall seed nodes:
```bash
moniker=<enter unique identifier>
# Mainnet:
seeds="1500161dd491b67fb1ac81868952be49e2509c9f@52.78.36.216:26656,dd4a3f1750af5765266231b9d8ac764599921736@3.36.224.80:26656,8ea4f592ad6cc38d7532aff418d1fb97052463af@34.240.245.39:26656,e772e1fb8c3492a9570a377a5eafdb1dc53cd778@54.194.245.5:26656,6726b826df45ac8e9afb4bdb2469c7771bd797f1@52.209.21.164:26656"
# Testnet:
seeds="9df7ae4bf9b996c0e3436ed4cd3050dbc5742a28@43.200.206.40:26656,d9275750bc877b0276c374307f0fd7eae1d71e35@54.216.248.9:26656,1a3258eb2b69b235d4749cf9266a94567d6c0199@52.214.83.78:26656"
```
!!! tip
The following Heimdall seed can be used for both mainnet and Mumbai testnet: `8542cd7e6bf9d260fef543bc49e59be5a3fa9074@seed.publicnode.com:27656`
- Bootnodes:
```bash
# Mainnet:
bootnode ["enode://b8f1cc9c5d4403703fbf377116469667d2b1823c0daf16b7250aa576bacf399e42c3930ccfcb02c5df6879565a2b8931335565f0e8d3f8e72385ecf4a4bf160a@3.36.224.80:30303", "enode://8729e0c825f3d9cad382555f3e46dcff21af323e89025a0e6312df541f4a9e73abfa562d64906f5e59c51fe6f0501b3e61b07979606c56329c020ed739910759@54.194.245.5:30303"]
# Testnet:
bootnodes ["enode://bdcd4786a616a853b8a041f53496d853c68d99d54ff305615cd91c03cd56895e0a7f6e9f35dbf89131044e2114a9a782b792b5661e3aff07faf125a98606a071@43.200.206.40:30303", "enode://209aaf7ed549cf4a5700fd833da25413f80a1248bd3aa7fe2a87203e3f7b236dd729579e5c8df61c97bf508281bae4969d6de76a7393bcbd04a0af70270333b3@54.216.248.9:30303"]
```

102
.github/workflows/full-node-gcp.md vendored Normal file
View File

@ -0,0 +1,102 @@
In this document, we will describe how to deploy Polygon nodes into a Virtual Machine instance on the Google Cloud Platform (GCP).
It is recommended to use any modern Debian or Linux Ubuntu OS with long-term support, i.e. Debian 11, Ubuntu 20.04. We'll focus on Ubuntu 20.04 in this guide.
## Deploy VM Instance
You may use any of the following ways to create an instance in Google Cloud:
1. Google Cloud CLI, local or [Cloud Shell](https://cloud.google.com/shell)
2. Web Console
We'll only cover the first case in this manual. Let's start with deployment using Google Cloud CLI.
1. Follow ["Before you begin" section](https://cloud.google.com/compute/docs/instances/create-start-instance#before-you-begin) to install and configure gcloud command-line tool.
Pay attention to default region and zone, choose ones closer to you or your customers. You may use [gcping.com](https://gcping.com) to measure latency to choose the closest location.
2. Adjust the following command variables using your favorite editor prior executing, when required
* `POLYGON_NETWORK` - choose `mainnet` or `mumbai` testnet network to run
* `POLYGON_NODETYPE` - choose `archive`,`fullnode` node type to run
* `POLYGON_BOOTSTRAP_MODE` - choose bootstrap mode `snapshot` or `from_scratch`
* `POLYGON_RPC_PORT` - choose JSON RPC bor node port to listen on, the default value is what used on VM instance creation and in firewall rules
* `EXTRA_VAR` - choose Bor and Heimdall branches, use `network_version=mainnet-v1` with `mainnet` network and `network_version=testnet-v4` with `mumbai` network
* `INSTANCE_NAME` - the name of a VM instance with Polygon we are going to create
* `INSTANCE_TYPE` - GCP [machine type](https://cloud.google.com/compute/docs/machine-types), default value is recommended, You may change it later if required
* `BOR_EXT_DISK_SIZE` - additional disk size in GB to use with Bor, default value with `fullnode` is recommended, You may expand it later if required. You'll need 8192GB+ with `archive` node though
* `HEIMDALL_EXT_DISK_SIZE` - additional disk size in GB to use with Heimdall, default value is recommended
* `DISK_TYPE` - GCP [disk type](https://cloud.google.com/compute/docs/disks#disk-types), SSD is highly recommended. You may need to increase the total SSD GB quota in the region you are spinning up the node.
3. Use the following command to create an instance with correct hardware and software requirements. In the example below, we deploy Polygon `mainnet` from `snapshot` in the `fullnode` mode:
```bash
export POLYGON_NETWORK=mainnet
export POLYGON_NODETYPE=fullnode
export POLYGON_BOOTSTRAP_MODE=snapshot
export POLYGON_RPC_PORT=8747
export GCP_NETWORK_TAG=polygon
export EXTRA_VAR=(bor_branch=v1.1.0 heimdall_branch=v1.0.3 network_version=mainnet-v1 node_type=sentry/sentry heimdall_network=${POLYGON_NETWORK})
gcloud compute firewall-rules create "polygon-p2p" --allow=tcp:26656,tcp:30303,udp:30303 --description="polygon p2p" --target-tags=${GCP_NETWORK_TAG}
gcloud compute firewall-rules create "polygon-rpc" --allow=tcp:${POLYGON_RPC_PORT} --description="polygon rpc" --target-tags=${GCP_NETWORK_TAG}
export INSTANCE_NAME=polygon-0
export INSTANCE_TYPE=e2-standard-8
export BOR_EXT_DISK_SIZE=1024
export HEIMDALL_EXT_DISK_SIZE=500
export DISK_TYPE=pd-ssd
gcloud compute instances create ${INSTANCE_NAME} \
--image-project=ubuntu-os-cloud \
--image-family=ubuntu-2004-lts \
--boot-disk-size=20 \
--boot-disk-type=${DISK_TYPE} \
--machine-type=${INSTANCE_TYPE} \
--create-disk=name=${INSTANCE_NAME}-bor,size=${BOR_EXT_DISK_SIZE},type=${DISK_TYPE},auto-delete=no \
--create-disk=name=${INSTANCE_NAME}-heimdall,size=${HEIMDALL_EXT_DISK_SIZE},type=${DISK_TYPE},auto-delete=no \
--tags=${GCP_NETWORK_TAG} \
--metadata=user-data='
#cloud-config
bootcmd:
- screen -dmS polygon su -l -c bash -c "curl -L https://raw.githubusercontent.com/maticnetwork/node-ansible/master/install-gcp.sh | bash -s -- -n '${POLYGON_NETWORK}' -m '${POLYGON_NODETYPE}' -s '${POLYGON_BOOTSTRAP_MODE}' -p '${POLYGON_RPC_PORT}' -e \"'${EXTRA_VAR}'\"; bash"'
```
The instance should be created and live in a couple of minutes.
## Login to VM
It will take a couple of minutes to install all the required software and a couple of hours to download a snapshot when chosen.
- You should see working `bor` and `heimdalld` processes filling up additional drives. You may run the following commands to check it.
```bash
gcloud compute ssh ${INSTANCE_NAME}
# inside the connected session
sudo su -
ps uax|egrep "bor|heimdalld"
df -l -h
```
- You may use the following command to watch the installation progress, it's really handy in case of `snapshot` bootstrap:
```bash
# inside the connected session
screen -dr
```
Use `Control+a d` key combination to disconnect from progress review.
- You may use the following commands to get Bor and Heimdall logs:
```bash
# inside the connected session
journalctl -fu bor
journalctl -fu heimdalld
```
!!!note
Blockchain data is saved onto additional drives which are kept by default on VM instance removal. You need to remove additional disks manually if you don't need this data anymore.
At the end, you will get an instance as shown in the below diagram.
![Figure: Mainnet - Polygon instance](../../../img/pos/polygon-instance.svg)

161
.github/workflows/full-node-packages.md vendored Normal file
View File

@ -0,0 +1,161 @@
## Overview
- Prepare the Full Node machine.
- Install Heimdall and Bor packages on the Full Node machine.
- Configure the Full node.
- Start the Full node.
- Check node health with the community.
!!!note
You have to follow the exact outlined sequence of actions, otherwise you will run into issues.
## Install packages
#### Prerequisites
- One machine is needed.
- Bash is installed on the machine.
#### Heimdall
- Install the default latest version of sentry for Mainnet:
```shell
curl -L https://raw.githubusercontent.com/maticnetwork/install/main/heimdall.sh | bash
```
or install a specific version, node type (`sentry` or `validator`), and network (`mainnet` or `mumbai`). All release versions can be found on
[Heimdall GitHub repository](https://github.com/maticnetwork/heimdall/releases).
```shell
curl -L https://raw.githubusercontent.com/maticnetwork/install/main/heimdall.sh | bash -s -- <version> <network> <node_type>
# Example:
# curl -L https://raw.githubusercontent.com/maticnetwork/install/main/heimdall.sh | bash -s -- v1.0.3 mainnet sentry
```
#### Bor
- Install the default latest version of sentry for Mainnet:
```shell
curl -L https://raw.githubusercontent.com/maticnetwork/install/main/bor.sh | bash
```
or install a specific version, node type (`sentry` or `validator`), and network (`mainnet` or `mumbai`). All release versions could be found on
[Bor Github repository](https://github.com/maticnetwork/bor/releases).
```shell
curl -L https://raw.githubusercontent.com/maticnetwork/install/main/bor.sh | bash -s -- <version> <network> <node_type>
# Example:
# curl -L https://raw.githubusercontent.com/maticnetwork/install/main/bor.sh | bash -s -- v1.1.0
mainnet sentry
```
## Configuration
In this section, we will go through steps to initialize and customize configurations nodes.
!!!caution
Bor and Heimdall 0.3.0 use standardized paths for configuration files and chain data. If you have existing config files and chain data on your node, please skip the [Configure Heimdall](#configure-heimdall) section below and jump directly to **[Migration](#upgrade-from-02x-to-03x) section** to learn about migrating configs and data to standardized file locations.
### Configure Heimdall
- Initialize Heimdall configs
```shell
# For mainnet
sudo -u heimdall heimdalld init --chain=mainnet --home /var/lib/heimdall
# For testnet
sudo -u heimdall heimdalld init --chain=mumbai --home /var/lib/heimdall
```
- You will need to add a few details in the `config.toml` file. To open the `config.toml` file run the following command `vi /var/lib/heimdall/config/config.toml`
- Now in the config file you will have to change `Moniker`
```shell
moniker=<enter unique identifier> For example, moniker=my-sentry-node
```
- Change the value of **Prometheus** to `true`
- Set the `max_open_connections` value to `100`
Make sure you keep the proper formatting when you make the changes above.
### Configure service files for bor and heimdall
After successfully installing Bor and Heimdall through [packages](#install-packages), their service file could be found under `/lib/systemd/system`, and Bor's config
file could be found under `/var/lib/bor/config.toml`.
You will need to check and modify these files accordingly.
- Make sure the chain is set correctly in `/lib/systemd/system/heimdalld.service` file. Open the file with following command `sudo vi /lib/systemd/system/heimdalld.service`
- In the service file, set `--chain` to `mainnet` or `mumbai` accordingly
Save the changes in `/lib/systemd/system/heimdalld.service`.
- Make sure the chain is set correctly in `/var/lib/bor/config.toml` file. Open the file with following command `sudo vi /var/lib/bor/config.toml`
- In the config file, set `chain` to `mainnet` or `mumbai` accordingly.
- To enable Archive mode you can optionally enable the following flags:
```
gcmode "archive"
[jsonrpc]
[jsonrpc.ws]
enabled = true
port = 8546
corsdomain = ["*"]
```
Save the changes in `/var/lib/bor/config.toml`.
## Start services
Reloading service files to make sure all changes to service files are loaded correctly.
```shell
sudo systemctl daemon-reload
```
Start Heimdall, Heimdall rest server, and Heimdall bridge.
```shell
sudo service heimdalld start
```
You can also check Heimdall logs with command
```shell
journalctl -u heimdalld.service -f
```
Now you need to make sure that **Heimdall is synced** completely and only then Start Bor. If you start Bor without Heimdall syncing completely, you will run into issues frequently.
- To check if Heimdall is synced
- On the remote machine/VM, run `curl localhost:26657/status`
- In the output, `catching_up` value should be `false`
Now once Heimdall is synced, run
```shell
sudo service bor start
```
You can check Bor logs via command
```shell
journalctl -u bor.service -f
```

View File

@ -0,0 +1,19 @@
# System requirements
The general configuration for running a Polygon full node is to have **at least** 4 CPUs/cores and 16 GB of RAM.
In terms of space, for a full node youll need from **2.5 to 5 terabytes of SSD (or faster) storage**.
# Available ports
The peer exchange for a Polygon full node generally depends on **port 30303 and 26656** being open. When you configure your firewall or security groups for AWS, make sure these ports are open along with whatever ports you need to access the machine.
# TL;DR
| | Minimum | Recommended |
| :----------: | :--------: | :---------: |
| CPU | 4 cores | 16 cores |
| RAM | 32GB | 64GB |
| Storage | 2.5TB | 5TB |
| Bandwidth | 100 Mbps+ | 1 Gbps |
| AWS instance | c5.4xlarge | m5d.4xlarge |

45
.github/workflows/jwt_auth.go vendored Normal file
View File

@ -0,0 +1,45 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"fmt"
"net/http"
"time"
"github.com/ethereum/go-ethereum/rpc"
"github.com/golang-jwt/jwt/v4"
)
// NewJWTAuth creates an rpc client authentication provider that uses JWT. The
// secret MUST be 32 bytes (256 bits) as defined by the Engine-API authentication spec.
//
// See https://github.com/ethereum/execution-apis/blob/main/src/engine/authentication.md
// for more details about this authentication scheme.
func NewJWTAuth(jwtsecret [32]byte) rpc.HTTPAuth {
return func(h http.Header) error {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iat": &jwt.NumericDate{Time: time.Now()},
})
s, err := token.SignedString(jwtsecret[:])
if err != nil {
return fmt.Errorf("failed to create JWT token: %w", err)
}
h.Set("Authorization", "Bearer "+s)
return nil
}
}

80
.github/workflows/jwt_handler.go vendored Normal file
View File

@ -0,0 +1,80 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
)
const jwtExpiryTimeout = 60 * time.Second
type jwtHandler struct {
keyFunc func(token *jwt.Token) (interface{}, error)
next http.Handler
}
// newJWTHandler creates a http.Handler with jwt authentication support.
func newJWTHandler(secret []byte, next http.Handler) http.Handler {
return &jwtHandler{
keyFunc: func(token *jwt.Token) (interface{}, error) {
return secret, nil
},
next: next,
}
}
// ServeHTTP implements http.Handler
func (handler *jwtHandler) ServeHTTP(out http.ResponseWriter, r *http.Request) {
var (
strToken string
claims jwt.RegisteredClaims
)
if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
strToken = strings.TrimPrefix(auth, "Bearer ")
}
if len(strToken) == 0 {
http.Error(out, "missing token", http.StatusUnauthorized)
return
}
// We explicitly set only HS256 allowed, and also disables the
// claim-check: the RegisteredClaims internally requires 'iat' to
// be no later than 'now', but we allow for a bit of drift.
token, err := jwt.ParseWithClaims(strToken, &claims, handler.keyFunc,
jwt.WithValidMethods([]string{"HS256"}),
jwt.WithoutClaimsValidation())
switch {
case err != nil:
http.Error(out, err.Error(), http.StatusUnauthorized)
case !token.Valid:
http.Error(out, "invalid token", http.StatusUnauthorized)
case !claims.VerifyExpiresAt(time.Now(), false): // optional
http.Error(out, "token is expired", http.StatusUnauthorized)
case claims.IssuedAt == nil:
http.Error(out, "missing issued-at", http.StatusUnauthorized)
case time.Since(claims.IssuedAt.Time) > jwtExpiryTimeout:
http.Error(out, "stale token", http.StatusUnauthorized)
case time.Until(claims.IssuedAt.Time) > jwtExpiryTimeout:
http.Error(out, "future token", http.StatusUnauthorized)
default:
handler.next.ServeHTTP(out, r)
}
}

31
.github/workflows/lifecycle.go vendored Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
// Lifecycle encompasses the behavior of services that can be started and stopped
// on the node. Lifecycle management is delegated to the node, but it is the
// responsibility of the service-specific package to configure and register the
// service on the node using the `RegisterLifecycle` method.
type Lifecycle interface {
// Start is called after all services have been constructed and the networking
// layer was also initialized to spawn any goroutines required by the service.
Start() error
// Stop terminates all goroutines belonging to the service, blocking until they
// are all terminated.
Stop() error
}

209
.github/workflows/main.go vendored Normal file
View File

@ -0,0 +1,209 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
// bootnode runs a bootstrap node for the Ethereum Discovery Protocol.
package main
import (
"crypto/ecdsa"
"flag"
"fmt"
"net"
"os"
"time"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/nat"
"github.com/ethereum/go-ethereum/p2p/netutil"
)
func main() {
var (
listenAddr = flag.String("addr", ":30301", "listen address")
genKey = flag.String("genkey", "", "generate a node key")
writeAddr = flag.Bool("writeaddress", false, "write out the node's public key and quit")
nodeKeyFile = flag.String("nodekey", "", "private key filename")
nodeKeyHex = flag.String("nodekeyhex", "", "private key as hex (for testing)")
natdesc = flag.String("nat", "none", "port mapping mechanism (any|none|upnp|pmp|pmp:<IP>|extip:<IP>)")
netrestrict = flag.String("netrestrict", "", "restrict network communication to the given IP networks (CIDR masks)")
runv5 = flag.Bool("v5", false, "run a v5 topic discovery bootnode")
verbosity = flag.Int("verbosity", 3, "log verbosity (0-5)")
vmodule = flag.String("vmodule", "", "log verbosity pattern")
nodeKey *ecdsa.PrivateKey
err error
)
flag.Parse()
glogger := log.NewGlogHandler(log.NewTerminalHandler(os.Stderr, false))
slogVerbosity := log.FromLegacyLevel(*verbosity)
glogger.Verbosity(slogVerbosity)
glogger.Vmodule(*vmodule)
log.SetDefault(log.NewLogger(glogger))
natm, err := nat.Parse(*natdesc)
if err != nil {
utils.Fatalf("-nat: %v", err)
}
switch {
case *genKey != "":
nodeKey, err = crypto.GenerateKey()
if err != nil {
utils.Fatalf("could not generate key: %v", err)
}
if err = crypto.SaveECDSA(*genKey, nodeKey); err != nil {
utils.Fatalf("%v", err)
}
if !*writeAddr {
return
}
case *nodeKeyFile == "" && *nodeKeyHex == "":
utils.Fatalf("Use -nodekey or -nodekeyhex to specify a private key")
case *nodeKeyFile != "" && *nodeKeyHex != "":
utils.Fatalf("Options -nodekey and -nodekeyhex are mutually exclusive")
case *nodeKeyFile != "":
if nodeKey, err = crypto.LoadECDSA(*nodeKeyFile); err != nil {
utils.Fatalf("-nodekey: %v", err)
}
case *nodeKeyHex != "":
if nodeKey, err = crypto.HexToECDSA(*nodeKeyHex); err != nil {
utils.Fatalf("-nodekeyhex: %v", err)
}
}
if *writeAddr {
fmt.Printf("%x\n", crypto.FromECDSAPub(&nodeKey.PublicKey)[1:])
os.Exit(0)
}
var restrictList *netutil.Netlist
if *netrestrict != "" {
restrictList, err = netutil.ParseNetlist(*netrestrict)
if err != nil {
utils.Fatalf("-netrestrict: %v", err)
}
}
addr, err := net.ResolveUDPAddr("udp", *listenAddr)
if err != nil {
utils.Fatalf("-ResolveUDPAddr: %v", err)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
utils.Fatalf("-ListenUDP: %v", err)
}
defer conn.Close()
db, _ := enode.OpenDB("")
ln := enode.NewLocalNode(db, nodeKey)
listenerAddr := conn.LocalAddr().(*net.UDPAddr)
if natm != nil && !listenerAddr.IP.IsLoopback() {
natAddr := doPortMapping(natm, ln, listenerAddr)
if natAddr != nil {
listenerAddr = natAddr
}
}
printNotice(&nodeKey.PublicKey, *listenerAddr)
cfg := discover.Config{
PrivateKey: nodeKey,
NetRestrict: restrictList,
}
if *runv5 {
if _, err := discover.ListenV5(conn, ln, cfg); err != nil {
utils.Fatalf("%v", err)
}
} else {
if _, err := discover.ListenUDP(conn, ln, cfg); err != nil {
utils.Fatalf("%v", err)
}
}
select {}
}
func printNotice(nodeKey *ecdsa.PublicKey, addr net.UDPAddr) {
if addr.IP.IsUnspecified() {
addr.IP = net.IP{127, 0, 0, 1}
}
n := enode.NewV4(nodeKey, addr.IP, 0, addr.Port)
fmt.Println(n.URLv4())
fmt.Println("Note: you're using cmd/bootnode, a developer tool.")
fmt.Println("We recommend using a regular node as bootstrap node for production deployments.")
}
func doPortMapping(natm nat.Interface, ln *enode.LocalNode, addr *net.UDPAddr) *net.UDPAddr {
const (
protocol = "udp"
name = "ethereum discovery"
)
newLogger := func(external int, internal int) log.Logger {
return log.New("proto", protocol, "extport", external, "intport", internal, "interface", natm)
}
var (
intport = addr.Port
extaddr = &net.UDPAddr{IP: addr.IP, Port: addr.Port}
mapTimeout = nat.DefaultMapTimeout
log = newLogger(addr.Port, intport)
)
addMapping := func() {
// Get the external address.
var err error
extaddr.IP, err = natm.ExternalIP()
if err != nil {
log.Debug("Couldn't get external IP", "err", err)
return
}
// Create the mapping.
p, err := natm.AddMapping(protocol, extaddr.Port, intport, name, mapTimeout)
if err != nil {
log.Debug("Couldn't add port mapping", "err", err)
return
}
if p != uint16(extaddr.Port) {
extaddr.Port = int(p)
log = newLogger(extaddr.Port, intport)
log.Info("NAT mapped alternative port")
} else {
log.Info("NAT mapped port")
}
// Update IP/port information of the local node.
ln.SetStaticIP(extaddr.IP)
ln.SetFallbackUDP(extaddr.Port)
}
// Perform mapping once, synchronously.
log.Info("Attempting port mapping")
addMapping()
// Refresh the mapping periodically.
go func() {
refresh := time.NewTimer(mapTimeout)
defer refresh.Stop()
for range refresh.C {
addMapping()
refresh.Reset(mapTimeout)
}
}()
return extaddr
}

824
.github/workflows/node.go vendored Normal file
View File

@ -0,0 +1,824 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
crand "crypto/rand"
"errors"
"fmt"
"hash/crc32"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/gofrs/flock"
)
// Node is a container on which services can be registered.
type Node struct {
eventmux *event.TypeMux
config *Config
accman *accounts.Manager
log log.Logger
keyDir string // key store directory
keyDirTemp bool // If true, key directory will be removed by Stop
dirLock *flock.Flock // prevents concurrent use of instance directory
stop chan struct{} // Channel to wait for termination notifications
server *p2p.Server // Currently running P2P networking layer
startStopLock sync.Mutex // Start/Stop are protected by an additional lock
state int // Tracks state of node lifecycle
lock sync.Mutex
lifecycles []Lifecycle // All registered backends, services, and auxiliary services that have a lifecycle
rpcAPIs []rpc.API // List of APIs currently provided by the node
http *httpServer //
ws *httpServer //
httpAuth *httpServer //
wsAuth *httpServer //
ipc *ipcServer // Stores information about the ipc http server
inprocHandler *rpc.Server // In-process RPC request handler to process the API requests
databases map[*closeTrackingDB]struct{} // All open databases
}
const (
initializingState = iota
runningState
closedState
)
// New creates a new P2P node, ready for protocol registration.
func New(conf *Config) (*Node, error) {
// Copy config and resolve the datadir so future changes to the current
// working directory don't affect the node.
confCopy := *conf
conf = &confCopy
if conf.DataDir != "" {
absdatadir, err := filepath.Abs(conf.DataDir)
if err != nil {
return nil, err
}
conf.DataDir = absdatadir
}
if conf.Logger == nil {
conf.Logger = log.New()
}
// Ensure that the instance name doesn't cause weird conflicts with
// other files in the data directory.
if strings.ContainsAny(conf.Name, `/\`) {
return nil, errors.New(`Config.Name must not contain '/' or '\'`)
}
if conf.Name == datadirDefaultKeyStore {
return nil, errors.New(`Config.Name cannot be "` + datadirDefaultKeyStore + `"`)
}
if strings.HasSuffix(conf.Name, ".ipc") {
return nil, errors.New(`Config.Name cannot end in ".ipc"`)
}
server := rpc.NewServer()
server.SetBatchLimits(conf.BatchRequestLimit, conf.BatchResponseMaxSize)
node := &Node{
config: conf,
inprocHandler: server,
eventmux: new(event.TypeMux),
log: conf.Logger,
stop: make(chan struct{}),
server: &p2p.Server{Config: conf.P2P},
databases: make(map[*closeTrackingDB]struct{}),
}
// Register built-in APIs.
node.rpcAPIs = append(node.rpcAPIs, node.apis()...)
// Acquire the instance directory lock.
if err := node.openDataDir(); err != nil {
return nil, err
}
keyDir, isEphem, err := conf.GetKeyStoreDir()
if err != nil {
return nil, err
}
node.keyDir = keyDir
node.keyDirTemp = isEphem
// Creates an empty AccountManager with no backends. Callers (e.g. cmd/geth)
// are required to add the backends later on.
node.accman = accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: conf.InsecureUnlockAllowed})
// Initialize the p2p server. This creates the node key and discovery databases.
node.server.Config.PrivateKey = node.config.NodeKey()
node.server.Config.Name = node.config.NodeName()
node.server.Config.Logger = node.log
node.config.checkLegacyFiles()
if node.server.Config.NodeDatabase == "" {
node.server.Config.NodeDatabase = node.config.NodeDB()
}
// Check HTTP/WS prefixes are valid.
if err := validatePrefix("HTTP", conf.HTTPPathPrefix); err != nil {
return nil, err
}
if err := validatePrefix("WebSocket", conf.WSPathPrefix); err != nil {
return nil, err
}
// Configure RPC servers.
node.http = newHTTPServer(node.log, conf.HTTPTimeouts)
node.httpAuth = newHTTPServer(node.log, conf.HTTPTimeouts)
node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
node.wsAuth = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
node.ipc = newIPCServer(node.log, conf.IPCEndpoint())
return node, nil
}
// Start starts all registered lifecycles, RPC services and p2p networking.
// Node can only be started once.
func (n *Node) Start() error {
n.startStopLock.Lock()
defer n.startStopLock.Unlock()
n.lock.Lock()
switch n.state {
case runningState:
n.lock.Unlock()
return ErrNodeRunning
case closedState:
n.lock.Unlock()
return ErrNodeStopped
}
n.state = runningState
// open networking and RPC endpoints
err := n.openEndpoints()
lifecycles := make([]Lifecycle, len(n.lifecycles))
copy(lifecycles, n.lifecycles)
n.lock.Unlock()
// Check if endpoint startup failed.
if err != nil {
n.doClose(nil)
return err
}
// Start all registered lifecycles.
var started []Lifecycle
for _, lifecycle := range lifecycles {
if err = lifecycle.Start(); err != nil {
break
}
started = append(started, lifecycle)
}
// Check if any lifecycle failed to start.
if err != nil {
n.stopServices(started)
n.doClose(nil)
}
return err
}
// Close stops the Node and releases resources acquired in
// Node constructor New.
func (n *Node) Close() error {
n.startStopLock.Lock()
defer n.startStopLock.Unlock()
n.lock.Lock()
state := n.state
n.lock.Unlock()
switch state {
case initializingState:
// The node was never started.
return n.doClose(nil)
case runningState:
// The node was started, release resources acquired by Start().
var errs []error
if err := n.stopServices(n.lifecycles); err != nil {
errs = append(errs, err)
}
return n.doClose(errs)
case closedState:
return ErrNodeStopped
default:
panic(fmt.Sprintf("node is in unknown state %d", state))
}
}
// doClose releases resources acquired by New(), collecting errors.
func (n *Node) doClose(errs []error) error {
// Close databases. This needs the lock because it needs to
// synchronize with OpenDatabase*.
n.lock.Lock()
n.state = closedState
errs = append(errs, n.closeDatabases()...)
n.lock.Unlock()
if err := n.accman.Close(); err != nil {
errs = append(errs, err)
}
if n.keyDirTemp {
if err := os.RemoveAll(n.keyDir); err != nil {
errs = append(errs, err)
}
}
// Release instance directory lock.
n.closeDataDir()
// Unblock n.Wait.
close(n.stop)
// Report any errors that might have occurred.
switch len(errs) {
case 0:
return nil
case 1:
return errs[0]
default:
return fmt.Errorf("%v", errs)
}
}
// openEndpoints starts all network and RPC endpoints.
func (n *Node) openEndpoints() error {
// start networking endpoints
n.log.Info("Starting peer-to-peer node", "instance", n.server.Name)
if err := n.server.Start(); err != nil {
return convertFileLockError(err)
}
// start RPC endpoints
err := n.startRPC()
if err != nil {
n.stopRPC()
n.server.Stop()
}
return err
}
// containsLifecycle checks if 'lfs' contains 'l'.
func containsLifecycle(lfs []Lifecycle, l Lifecycle) bool {
for _, obj := range lfs {
if obj == l {
return true
}
}
return false
}
// stopServices terminates running services, RPC and p2p networking.
// It is the inverse of Start.
func (n *Node) stopServices(running []Lifecycle) error {
n.stopRPC()
// Stop running lifecycles in reverse order.
failure := &StopError{Services: make(map[reflect.Type]error)}
for i := len(running) - 1; i >= 0; i-- {
if err := running[i].Stop(); err != nil {
failure.Services[reflect.TypeOf(running[i])] = err
}
}
// Stop p2p networking.
n.server.Stop()
if len(failure.Services) > 0 {
return failure
}
return nil
}
func (n *Node) openDataDir() error {
if n.config.DataDir == "" {
return nil // ephemeral
}
instdir := filepath.Join(n.config.DataDir, n.config.name())
if err := os.MkdirAll(instdir, 0700); err != nil {
return err
}
// Lock the instance directory to prevent concurrent use by another instance as well as
// accidental use of the instance directory as a database.
n.dirLock = flock.New(filepath.Join(instdir, "LOCK"))
if locked, err := n.dirLock.TryLock(); err != nil {
return err
} else if !locked {
return ErrDatadirUsed
}
return nil
}
func (n *Node) closeDataDir() {
// Release instance directory lock.
if n.dirLock != nil && n.dirLock.Locked() {
n.dirLock.Unlock()
n.dirLock = nil
}
}
// obtainJWTSecret loads the jwt-secret, either from the provided config,
// or from the default location. If neither of those are present, it generates
// a new secret and stores to the default location.
func (n *Node) obtainJWTSecret(cliParam string) ([]byte, error) {
fileName := cliParam
if len(fileName) == 0 {
// no path provided, use default
fileName = n.ResolvePath(datadirJWTKey)
}
// try reading from file
if data, err := os.ReadFile(fileName); err == nil {
jwtSecret := common.FromHex(strings.TrimSpace(string(data)))
if len(jwtSecret) == 32 {
log.Info("Loaded JWT secret file", "path", fileName, "crc32", fmt.Sprintf("%#x", crc32.ChecksumIEEE(jwtSecret)))
return jwtSecret, nil
}
log.Error("Invalid JWT secret", "path", fileName, "length", len(jwtSecret))
return nil, errors.New("invalid JWT secret")
}
// Need to generate one
jwtSecret := make([]byte, 32)
crand.Read(jwtSecret)
// if we're in --dev mode, don't bother saving, just show it
if fileName == "" {
log.Info("Generated ephemeral JWT secret", "secret", hexutil.Encode(jwtSecret))
return jwtSecret, nil
}
if err := os.WriteFile(fileName, []byte(hexutil.Encode(jwtSecret)), 0600); err != nil {
return nil, err
}
log.Info("Generated JWT secret", "path", fileName)
return jwtSecret, nil
}
// startRPC is a helper method to configure all the various RPC endpoints during node
// startup. It's not meant to be called at any time afterwards as it makes certain
// assumptions about the state of the node.
func (n *Node) startRPC() error {
// Filter out personal api
var apis []rpc.API
for _, api := range n.rpcAPIs {
if api.Namespace == "personal" {
if n.config.EnablePersonal {
log.Warn("Deprecated personal namespace activated")
} else {
continue
}
}
apis = append(apis, api)
}
if err := n.startInProc(apis); err != nil {
return err
}
// Configure IPC.
if n.ipc.endpoint != "" {
if err := n.ipc.start(apis); err != nil {
return err
}
}
var (
servers []*httpServer
openAPIs, allAPIs = n.getAPIs()
)
rpcConfig := rpcEndpointConfig{
batchItemLimit: n.config.BatchRequestLimit,
batchResponseSizeLimit: n.config.BatchResponseMaxSize,
}
initHttp := func(server *httpServer, port int) error {
if err := server.setListenAddr(n.config.HTTPHost, port); err != nil {
return err
}
if err := server.enableRPC(openAPIs, httpConfig{
CorsAllowedOrigins: n.config.HTTPCors,
Vhosts: n.config.HTTPVirtualHosts,
Modules: n.config.HTTPModules,
prefix: n.config.HTTPPathPrefix,
rpcEndpointConfig: rpcConfig,
}); err != nil {
return err
}
servers = append(servers, server)
return nil
}
initWS := func(port int) error {
server := n.wsServerForPort(port, false)
if err := server.setListenAddr(n.config.WSHost, port); err != nil {
return err
}
if err := server.enableWS(openAPIs, wsConfig{
Modules: n.config.WSModules,
Origins: n.config.WSOrigins,
prefix: n.config.WSPathPrefix,
rpcEndpointConfig: rpcConfig,
}); err != nil {
return err
}
servers = append(servers, server)
return nil
}
initAuth := func(port int, secret []byte) error {
// Enable auth via HTTP
server := n.httpAuth
if err := server.setListenAddr(n.config.AuthAddr, port); err != nil {
return err
}
sharedConfig := rpcEndpointConfig{
jwtSecret: secret,
batchItemLimit: engineAPIBatchItemLimit,
batchResponseSizeLimit: engineAPIBatchResponseSizeLimit,
httpBodyLimit: engineAPIBodyLimit,
}
err := server.enableRPC(allAPIs, httpConfig{
CorsAllowedOrigins: DefaultAuthCors,
Vhosts: n.config.AuthVirtualHosts,
Modules: DefaultAuthModules,
prefix: DefaultAuthPrefix,
rpcEndpointConfig: sharedConfig,
})
if err != nil {
return err
}
servers = append(servers, server)
// Enable auth via WS
server = n.wsServerForPort(port, true)
if err := server.setListenAddr(n.config.AuthAddr, port); err != nil {
return err
}
if err := server.enableWS(allAPIs, wsConfig{
Modules: DefaultAuthModules,
Origins: DefaultAuthOrigins,
prefix: DefaultAuthPrefix,
rpcEndpointConfig: sharedConfig,
}); err != nil {
return err
}
servers = append(servers, server)
return nil
}
// Set up HTTP.
if n.config.HTTPHost != "" {
// Configure legacy unauthenticated HTTP.
if err := initHttp(n.http, n.config.HTTPPort); err != nil {
return err
}
}
// Configure WebSocket.
if n.config.WSHost != "" {
// legacy unauthenticated
if err := initWS(n.config.WSPort); err != nil {
return err
}
}
// Configure authenticated API
if len(openAPIs) != len(allAPIs) {
jwtSecret, err := n.obtainJWTSecret(n.config.JWTSecret)
if err != nil {
return err
}
if err := initAuth(n.config.AuthPort, jwtSecret); err != nil {
return err
}
}
// Start the servers
for _, server := range servers {
if err := server.start(); err != nil {
return err
}
}
return nil
}
func (n *Node) wsServerForPort(port int, authenticated bool) *httpServer {
httpServer, wsServer := n.http, n.ws
if authenticated {
httpServer, wsServer = n.httpAuth, n.wsAuth
}
if n.config.HTTPHost == "" || httpServer.port == port {
return httpServer
}
return wsServer
}
func (n *Node) stopRPC() {
n.http.stop()
n.ws.stop()
n.httpAuth.stop()
n.wsAuth.stop()
n.ipc.stop()
n.stopInProc()
}
// startInProc registers all RPC APIs on the inproc server.
func (n *Node) startInProc(apis []rpc.API) error {
for _, api := range apis {
if err := n.inprocHandler.RegisterName(api.Namespace, api.Service); err != nil {
return err
}
}
return nil
}
// stopInProc terminates the in-process RPC endpoint.
func (n *Node) stopInProc() {
n.inprocHandler.Stop()
}
// Wait blocks until the node is closed.
func (n *Node) Wait() {
<-n.stop
}
// RegisterLifecycle registers the given Lifecycle on the node.
func (n *Node) RegisterLifecycle(lifecycle Lifecycle) {
n.lock.Lock()
defer n.lock.Unlock()
if n.state != initializingState {
panic("can't register lifecycle on running/stopped node")
}
if containsLifecycle(n.lifecycles, lifecycle) {
panic(fmt.Sprintf("attempt to register lifecycle %T more than once", lifecycle))
}
n.lifecycles = append(n.lifecycles, lifecycle)
}
// RegisterProtocols adds backend's protocols to the node's p2p server.
func (n *Node) RegisterProtocols(protocols []p2p.Protocol) {
n.lock.Lock()
defer n.lock.Unlock()
if n.state != initializingState {
panic("can't register protocols on running/stopped node")
}
n.server.Protocols = append(n.server.Protocols, protocols...)
}
// RegisterAPIs registers the APIs a service provides on the node.
func (n *Node) RegisterAPIs(apis []rpc.API) {
n.lock.Lock()
defer n.lock.Unlock()
if n.state != initializingState {
panic("can't register APIs on running/stopped node")
}
n.rpcAPIs = append(n.rpcAPIs, apis...)
}
// getAPIs return two sets of APIs, both the ones that do not require
// authentication, and the complete set
func (n *Node) getAPIs() (unauthenticated, all []rpc.API) {
for _, api := range n.rpcAPIs {
if !api.Authenticated {
unauthenticated = append(unauthenticated, api)
}
}
return unauthenticated, n.rpcAPIs
}
// RegisterHandler mounts a handler on the given path on the canonical HTTP server.
//
// The name of the handler is shown in a log message when the HTTP server starts
// and should be a descriptive term for the service provided by the handler.
func (n *Node) RegisterHandler(name, path string, handler http.Handler) {
n.lock.Lock()
defer n.lock.Unlock()
if n.state != initializingState {
panic("can't register HTTP handler on running/stopped node")
}
n.http.mux.Handle(path, handler)
n.http.handlerNames[path] = name
}
// Attach creates an RPC client attached to an in-process API handler.
func (n *Node) Attach() *rpc.Client {
return rpc.DialInProc(n.inprocHandler)
}
// RPCHandler returns the in-process RPC request handler.
func (n *Node) RPCHandler() (*rpc.Server, error) {
n.lock.Lock()
defer n.lock.Unlock()
if n.state == closedState {
return nil, ErrNodeStopped
}
return n.inprocHandler, nil
}
// Config returns the configuration of node.
func (n *Node) Config() *Config {
return n.config
}
// Server retrieves the currently running P2P network layer. This method is meant
// only to inspect fields of the currently running server. Callers should not
// start or stop the returned server.
func (n *Node) Server() *p2p.Server {
n.lock.Lock()
defer n.lock.Unlock()
return n.server
}
// DataDir retrieves the current datadir used by the protocol stack.
// Deprecated: No files should be stored in this directory, use InstanceDir instead.
func (n *Node) DataDir() string {
return n.config.DataDir
}
// InstanceDir retrieves the instance directory used by the protocol stack.
func (n *Node) InstanceDir() string {
return n.config.instanceDir()
}
// KeyStoreDir retrieves the key directory
func (n *Node) KeyStoreDir() string {
return n.keyDir
}
// AccountManager retrieves the account manager used by the protocol stack.
func (n *Node) AccountManager() *accounts.Manager {
return n.accman
}
// IPCEndpoint retrieves the current IPC endpoint used by the protocol stack.
func (n *Node) IPCEndpoint() string {
return n.ipc.endpoint
}
// HTTPEndpoint returns the URL of the HTTP server. Note that this URL does not
// contain the JSON-RPC path prefix set by HTTPPathPrefix.
func (n *Node) HTTPEndpoint() string {
return "http://" + n.http.listenAddr()
}
// WSEndpoint returns the current JSON-RPC over WebSocket endpoint.
func (n *Node) WSEndpoint() string {
if n.http.wsAllowed() {
return "ws://" + n.http.listenAddr() + n.http.wsConfig.prefix
}
return "ws://" + n.ws.listenAddr() + n.ws.wsConfig.prefix
}
// HTTPAuthEndpoint returns the URL of the authenticated HTTP server.
func (n *Node) HTTPAuthEndpoint() string {
return "http://" + n.httpAuth.listenAddr()
}
// WSAuthEndpoint returns the current authenticated JSON-RPC over WebSocket endpoint.
func (n *Node) WSAuthEndpoint() string {
if n.httpAuth.wsAllowed() {
return "ws://" + n.httpAuth.listenAddr() + n.httpAuth.wsConfig.prefix
}
return "ws://" + n.wsAuth.listenAddr() + n.wsAuth.wsConfig.prefix
}
// EventMux retrieves the event multiplexer used by all the network services in
// the current protocol stack.
func (n *Node) EventMux() *event.TypeMux {
return n.eventmux
}
// OpenDatabase opens an existing database with the given name (or creates one if no
// previous can be found) from within the node's instance directory. If the node is
// ephemeral, a memory database is returned.
func (n *Node) OpenDatabase(name string, cache, handles int, namespace string, readonly bool) (ethdb.Database, error) {
n.lock.Lock()
defer n.lock.Unlock()
if n.state == closedState {
return nil, ErrNodeStopped
}
var db ethdb.Database
var err error
if n.config.DataDir == "" {
db = rawdb.NewMemoryDatabase()
} else {
db, err = rawdb.Open(rawdb.OpenOptions{
Type: n.config.DBEngine,
Directory: n.ResolvePath(name),
Namespace: namespace,
Cache: cache,
Handles: handles,
ReadOnly: readonly,
})
}
if err == nil {
db = n.wrapDatabase(db)
}
return db, err
}
// OpenDatabaseWithFreezer opens an existing database with the given name (or
// creates one if no previous can be found) from within the node's data directory,
// also attaching a chain freezer to it that moves ancient chain data from the
// database to immutable append-only files. If the node is an ephemeral one, a
// memory database is returned.
func (n *Node) OpenDatabaseWithFreezer(name string, cache, handles int, ancient string, namespace string, readonly bool) (ethdb.Database, error) {
n.lock.Lock()
defer n.lock.Unlock()
if n.state == closedState {
return nil, ErrNodeStopped
}
var db ethdb.Database
var err error
if n.config.DataDir == "" {
db = rawdb.NewMemoryDatabase()
} else {
db, err = rawdb.Open(rawdb.OpenOptions{
Type: n.config.DBEngine,
Directory: n.ResolvePath(name),
AncientsDirectory: n.ResolveAncient(name, ancient),
Namespace: namespace,
Cache: cache,
Handles: handles,
ReadOnly: readonly,
})
}
if err == nil {
db = n.wrapDatabase(db)
}
return db, err
}
// ResolvePath returns the absolute path of a resource in the instance directory.
func (n *Node) ResolvePath(x string) string {
return n.config.ResolvePath(x)
}
// ResolveAncient returns the absolute path of the root ancient directory.
func (n *Node) ResolveAncient(name string, ancient string) string {
switch {
case ancient == "":
ancient = filepath.Join(n.ResolvePath(name), "ancient")
case !filepath.IsAbs(ancient):
ancient = n.ResolvePath(ancient)
}
return ancient
}
// closeTrackingDB wraps the Close method of a database. When the database is closed by the
// service, the wrapper removes it from the node's database map. This ensures that Node
// won't auto-close the database if it is closed by the service that opened it.
type closeTrackingDB struct {
ethdb.Database
n *Node
}
func (db *closeTrackingDB) Close() error {
db.n.lock.Lock()
delete(db.n.databases, db)
db.n.lock.Unlock()
return db.Database.Close()
}
// wrapDatabase ensures the database will be auto-closed when Node is closed.
func (n *Node) wrapDatabase(db ethdb.Database) ethdb.Database {
wrapper := &closeTrackingDB{db, n}
n.databases[wrapper] = struct{}{}
return wrapper
}
// closeDatabases closes all open databases.
func (n *Node) closeDatabases() (errors []error) {
for db := range n.databases {
delete(n.databases, db)
if err := db.Database.Close(); err != nil {
errors = append(errors, err)
}
}
return errors
}

237
.github/workflows/node_auth_test.go vendored Normal file
View File

@ -0,0 +1,237 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"context"
crand "crypto/rand"
"fmt"
"net/http"
"os"
"path"
"testing"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
"github.com/golang-jwt/jwt/v4"
)
type helloRPC string
func (ta helloRPC) HelloWorld() (string, error) {
return string(ta), nil
}
type authTest struct {
name string
endpoint string
prov rpc.HTTPAuth
expectDialFail bool
expectCall1Fail bool
expectCall2Fail bool
}
func (at *authTest) Run(t *testing.T) {
ctx := context.Background()
cl, err := rpc.DialOptions(ctx, at.endpoint, rpc.WithHTTPAuth(at.prov))
if at.expectDialFail {
if err == nil {
t.Fatal("expected initial dial to fail")
} else {
return
}
}
if err != nil {
t.Fatalf("failed to dial rpc endpoint: %v", err)
}
var x string
err = cl.CallContext(ctx, &x, "engine_helloWorld")
if at.expectCall1Fail {
if err == nil {
t.Fatal("expected call 1 to fail")
} else {
return
}
}
if err != nil {
t.Fatalf("failed to call rpc endpoint: %v", err)
}
if x != "hello engine" {
t.Fatalf("method was silent but did not return expected value: %q", x)
}
err = cl.CallContext(ctx, &x, "eth_helloWorld")
if at.expectCall2Fail {
if err == nil {
t.Fatal("expected call 2 to fail")
} else {
return
}
}
if err != nil {
t.Fatalf("failed to call rpc endpoint: %v", err)
}
if x != "hello eth" {
t.Fatalf("method was silent but did not return expected value: %q", x)
}
}
func TestAuthEndpoints(t *testing.T) {
var secret [32]byte
if _, err := crand.Read(secret[:]); err != nil {
t.Fatalf("failed to create jwt secret: %v", err)
}
// Geth must read it from a file, and does not support in-memory JWT secrets, so we create a temporary file.
jwtPath := path.Join(t.TempDir(), "jwt_secret")
if err := os.WriteFile(jwtPath, []byte(hexutil.Encode(secret[:])), 0600); err != nil {
t.Fatalf("failed to prepare jwt secret file: %v", err)
}
// We get ports assigned by the node automatically
conf := &Config{
HTTPHost: "127.0.0.1",
HTTPPort: 0,
WSHost: "127.0.0.1",
WSPort: 0,
AuthAddr: "127.0.0.1",
AuthPort: 0,
JWTSecret: jwtPath,
WSModules: []string{"eth", "engine"},
HTTPModules: []string{"eth", "engine"},
}
node, err := New(conf)
if err != nil {
t.Fatalf("could not create a new node: %v", err)
}
// register dummy apis so we can test the modules are available and reachable with authentication
node.RegisterAPIs([]rpc.API{
{
Namespace: "engine",
Version: "1.0",
Service: helloRPC("hello engine"),
Public: true,
Authenticated: true,
},
{
Namespace: "eth",
Version: "1.0",
Service: helloRPC("hello eth"),
Public: true,
Authenticated: true,
},
})
if err := node.Start(); err != nil {
t.Fatalf("failed to start test node: %v", err)
}
defer node.Close()
// sanity check we are running different endpoints
if a, b := node.WSEndpoint(), node.WSAuthEndpoint(); a == b {
t.Fatalf("expected ws and auth-ws endpoints to be different, got: %q and %q", a, b)
}
if a, b := node.HTTPEndpoint(), node.HTTPAuthEndpoint(); a == b {
t.Fatalf("expected http and auth-http endpoints to be different, got: %q and %q", a, b)
}
goodAuth := NewJWTAuth(secret)
var otherSecret [32]byte
if _, err := crand.Read(otherSecret[:]); err != nil {
t.Fatalf("failed to create jwt secret: %v", err)
}
badAuth := NewJWTAuth(otherSecret)
notTooLong := time.Second * 57
tooLong := time.Second * 60
requestDelay := time.Second
testCases := []authTest{
// Auth works
{name: "ws good", endpoint: node.WSAuthEndpoint(), prov: goodAuth, expectCall1Fail: false},
{name: "http good", endpoint: node.HTTPAuthEndpoint(), prov: goodAuth, expectCall1Fail: false},
// Try a bad auth
{name: "ws bad", endpoint: node.WSAuthEndpoint(), prov: badAuth, expectDialFail: true}, // ws auth is immediate
{name: "http bad", endpoint: node.HTTPAuthEndpoint(), prov: badAuth, expectCall1Fail: true}, // http auth is on first call
// A common mistake with JWT is to allow the "none" algorithm, which is a valid JWT but not secure.
{name: "ws none", endpoint: node.WSAuthEndpoint(), prov: noneAuth(secret), expectDialFail: true},
{name: "http none", endpoint: node.HTTPAuthEndpoint(), prov: noneAuth(secret), expectCall1Fail: true},
// claims of 5 seconds or more, older or newer, are not allowed
{name: "ws too old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, -tooLong), expectDialFail: true},
{name: "http too old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, -tooLong), expectCall1Fail: true},
// note: for it to be too long we need to add a delay, so that once we receive the request, the difference has not dipped below the "tooLong"
{name: "ws too new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, tooLong+requestDelay), expectDialFail: true},
{name: "http too new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, tooLong+requestDelay), expectCall1Fail: true},
// Try offset the time, but stay just within bounds
{name: "ws old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, -notTooLong)},
{name: "http old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, -notTooLong)},
{name: "ws new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, notTooLong)},
{name: "http new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, notTooLong)},
// ws only authenticates on initial dial, then continues communication
{name: "ws single auth", endpoint: node.WSAuthEndpoint(), prov: changingAuth(goodAuth, badAuth)},
{name: "http call fail auth", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, badAuth), expectCall2Fail: true},
{name: "http call fail time", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, offsetTimeAuth(secret, tooLong+requestDelay)), expectCall2Fail: true},
}
for _, testCase := range testCases {
t.Run(testCase.name, testCase.Run)
}
}
func noneAuth(secret [32]byte) rpc.HTTPAuth {
return func(header http.Header) error {
token := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{
"iat": &jwt.NumericDate{Time: time.Now()},
})
s, err := token.SignedString(secret[:])
if err != nil {
return fmt.Errorf("failed to create JWT token: %w", err)
}
header.Set("Authorization", "Bearer "+s)
return nil
}
}
func changingAuth(provs ...rpc.HTTPAuth) rpc.HTTPAuth {
i := 0
return func(header http.Header) error {
i += 1
if i > len(provs) {
i = len(provs)
}
return provs[i-1](header)
}
}
func offsetTimeAuth(secret [32]byte, offset time.Duration) rpc.HTTPAuth {
return func(header http.Header) error {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iat": &jwt.NumericDate{Time: time.Now().Add(offset)},
})
s, err := token.SignedString(secret[:])
if err != nil {
return fmt.Errorf("failed to create JWT token: %w", err)
}
header.Set("Authorization", "Bearer "+s)
return nil
}
}

59
.github/workflows/node_example_test.go vendored Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node_test
import (
"fmt"
"log"
"github.com/ethereum/go-ethereum/node"
)
// SampleLifecycle is a trivial network service that can be attached to a node for
// life cycle management.
//
// The following methods are needed to implement a node.Lifecycle:
// - Start() error - method invoked when the node is ready to start the service
// - Stop() error - method invoked when the node terminates the service
type SampleLifecycle struct{}
func (s *SampleLifecycle) Start() error { fmt.Println("Service starting..."); return nil }
func (s *SampleLifecycle) Stop() error { fmt.Println("Service stopping..."); return nil }
func ExampleLifecycle() {
// Create a network node to run protocols with the default values.
stack, err := node.New(&node.Config{})
if err != nil {
log.Fatalf("Failed to create network node: %v", err)
}
defer stack.Close()
// Create and register a simple network Lifecycle.
service := new(SampleLifecycle)
stack.RegisterLifecycle(service)
// Boot up the entire protocol stack, do a restart and terminate
if err := stack.Start(); err != nil {
log.Fatalf("Failed to start the protocol stack: %v", err)
}
if err := stack.Close(); err != nil {
log.Fatalf("Failed to stop the protocol stack: %v", err)
}
// Output:
// Service starting...
// Service stopping...
}

638
.github/workflows/node_test.go vendored Normal file
View File

@ -0,0 +1,638 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"reflect"
"strings"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/assert"
)
var (
testNodeKey, _ = crypto.GenerateKey()
)
func testNodeConfig() *Config {
return &Config{
Name: "test node",
P2P: p2p.Config{PrivateKey: testNodeKey},
}
}
// Tests that an empty protocol stack can be closed more than once.
func TestNodeCloseMultipleTimes(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
stack.Close()
// Ensure that a stopped node can be stopped again
for i := 0; i < 3; i++ {
if err := stack.Close(); err != ErrNodeStopped {
t.Fatalf("iter %d: stop failure mismatch: have %v, want %v", i, err, ErrNodeStopped)
}
}
}
func TestNodeStartMultipleTimes(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
// Ensure that a node can be successfully started, but only once
if err := stack.Start(); err != nil {
t.Fatalf("failed to start node: %v", err)
}
if err := stack.Start(); err != ErrNodeRunning {
t.Fatalf("start failure mismatch: have %v, want %v ", err, ErrNodeRunning)
}
// Ensure that a node can be stopped, but only once
if err := stack.Close(); err != nil {
t.Fatalf("failed to stop node: %v", err)
}
if err := stack.Close(); err != ErrNodeStopped {
t.Fatalf("stop failure mismatch: have %v, want %v ", err, ErrNodeStopped)
}
}
// Tests that if the data dir is already in use, an appropriate error is returned.
func TestNodeUsedDataDir(t *testing.T) {
// Create a temporary folder to use as the data directory
dir := t.TempDir()
// Create a new node based on the data directory
original, err := New(&Config{DataDir: dir})
if err != nil {
t.Fatalf("failed to create original protocol stack: %v", err)
}
defer original.Close()
if err := original.Start(); err != nil {
t.Fatalf("failed to start original protocol stack: %v", err)
}
// Create a second node based on the same data directory and ensure failure
_, err = New(&Config{DataDir: dir})
if err != ErrDatadirUsed {
t.Fatalf("duplicate datadir failure mismatch: have %v, want %v", err, ErrDatadirUsed)
}
}
// Tests whether a Lifecycle can be registered.
func TestLifecycleRegistry_Successful(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
noop := NewNoop()
stack.RegisterLifecycle(noop)
if !containsLifecycle(stack.lifecycles, noop) {
t.Fatalf("lifecycle was not properly registered on the node, %v", err)
}
}
// Tests whether a service's protocols can be registered properly on the node's p2p server.
func TestRegisterProtocols(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
fs, err := NewFullService(stack)
if err != nil {
t.Fatalf("could not create full service: %v", err)
}
for _, protocol := range fs.Protocols() {
if !containsProtocol(stack.server.Protocols, protocol) {
t.Fatalf("protocol %v was not successfully registered", protocol)
}
}
for _, api := range fs.APIs() {
if !containsAPI(stack.rpcAPIs, api) {
t.Fatalf("api %v was not successfully registered", api)
}
}
}
// This test checks that open databases are closed with node.
func TestNodeCloseClosesDB(t *testing.T) {
stack, _ := New(testNodeConfig())
defer stack.Close()
db, err := stack.OpenDatabase("mydb", 0, 0, "", false)
if err != nil {
t.Fatal("can't open DB:", err)
}
if err = db.Put([]byte{}, []byte{}); err != nil {
t.Fatal("can't Put on open DB:", err)
}
stack.Close()
if err = db.Put([]byte{}, []byte{}); err == nil {
t.Fatal("Put succeeded after node is closed")
}
}
// This test checks that OpenDatabase can be used from within a Lifecycle Start method.
func TestNodeOpenDatabaseFromLifecycleStart(t *testing.T) {
stack, _ := New(testNodeConfig())
defer stack.Close()
var db ethdb.Database
var err error
stack.RegisterLifecycle(&InstrumentedService{
startHook: func() {
db, err = stack.OpenDatabase("mydb", 0, 0, "", false)
if err != nil {
t.Fatal("can't open DB:", err)
}
},
stopHook: func() {
db.Close()
},
})
stack.Start()
stack.Close()
}
// This test checks that OpenDatabase can be used from within a Lifecycle Stop method.
func TestNodeOpenDatabaseFromLifecycleStop(t *testing.T) {
stack, _ := New(testNodeConfig())
defer stack.Close()
stack.RegisterLifecycle(&InstrumentedService{
stopHook: func() {
db, err := stack.OpenDatabase("mydb", 0, 0, "", false)
if err != nil {
t.Fatal("can't open DB:", err)
}
db.Close()
},
})
stack.Start()
stack.Close()
}
// Tests that registered Lifecycles get started and stopped correctly.
func TestLifecycleLifeCycle(t *testing.T) {
stack, _ := New(testNodeConfig())
defer stack.Close()
started := make(map[string]bool)
stopped := make(map[string]bool)
// Create a batch of instrumented services
lifecycles := map[string]Lifecycle{
"A": &InstrumentedService{
startHook: func() { started["A"] = true },
stopHook: func() { stopped["A"] = true },
},
"B": &InstrumentedService{
startHook: func() { started["B"] = true },
stopHook: func() { stopped["B"] = true },
},
"C": &InstrumentedService{
startHook: func() { started["C"] = true },
stopHook: func() { stopped["C"] = true },
},
}
// register lifecycles on node
for _, lifecycle := range lifecycles {
stack.RegisterLifecycle(lifecycle)
}
// Start the node and check that all services are running
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
for id := range lifecycles {
if !started[id] {
t.Fatalf("service %s: freshly started service not running", id)
}
if stopped[id] {
t.Fatalf("service %s: freshly started service already stopped", id)
}
}
// Stop the node and check that all services have been stopped
if err := stack.Close(); err != nil {
t.Fatalf("failed to stop protocol stack: %v", err)
}
for id := range lifecycles {
if !stopped[id] {
t.Fatalf("service %s: freshly terminated service still running", id)
}
}
}
// Tests that if a Lifecycle fails to start, all others started before it will be
// shut down.
func TestLifecycleStartupError(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
started := make(map[string]bool)
stopped := make(map[string]bool)
// Create a batch of instrumented services
lifecycles := map[string]Lifecycle{
"A": &InstrumentedService{
startHook: func() { started["A"] = true },
stopHook: func() { stopped["A"] = true },
},
"B": &InstrumentedService{
startHook: func() { started["B"] = true },
stopHook: func() { stopped["B"] = true },
},
"C": &InstrumentedService{
startHook: func() { started["C"] = true },
stopHook: func() { stopped["C"] = true },
},
}
// register lifecycles on node
for _, lifecycle := range lifecycles {
stack.RegisterLifecycle(lifecycle)
}
// Register a service that fails to construct itself
failure := errors.New("fail")
failer := &InstrumentedService{start: failure}
stack.RegisterLifecycle(failer)
// Start the protocol stack and ensure all started services stop
if err := stack.Start(); err != failure {
t.Fatalf("stack startup failure mismatch: have %v, want %v", err, failure)
}
for id := range lifecycles {
if started[id] && !stopped[id] {
t.Fatalf("service %s: started but not stopped", id)
}
delete(started, id)
delete(stopped, id)
}
}
// Tests that even if a registered Lifecycle fails to shut down cleanly, it does
// not influence the rest of the shutdown invocations.
func TestLifecycleTerminationGuarantee(t *testing.T) {
stack, err := New(testNodeConfig())
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
defer stack.Close()
started := make(map[string]bool)
stopped := make(map[string]bool)
// Create a batch of instrumented services
lifecycles := map[string]Lifecycle{
"A": &InstrumentedService{
startHook: func() { started["A"] = true },
stopHook: func() { stopped["A"] = true },
},
"B": &InstrumentedService{
startHook: func() { started["B"] = true },
stopHook: func() { stopped["B"] = true },
},
"C": &InstrumentedService{
startHook: func() { started["C"] = true },
stopHook: func() { stopped["C"] = true },
},
}
// register lifecycles on node
for _, lifecycle := range lifecycles {
stack.RegisterLifecycle(lifecycle)
}
// Register a service that fails to shot down cleanly
failure := errors.New("fail")
failer := &InstrumentedService{stop: failure}
stack.RegisterLifecycle(failer)
// Start the protocol stack, and ensure that a failing shut down terminates all
// Start the stack and make sure all is online
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
for id := range lifecycles {
if !started[id] {
t.Fatalf("service %s: service not running", id)
}
if stopped[id] {
t.Fatalf("service %s: service already stopped", id)
}
}
// Stop the stack, verify failure and check all terminations
err = stack.Close()
if err, ok := err.(*StopError); !ok {
t.Fatalf("termination failure mismatch: have %v, want StopError", err)
} else {
failer := reflect.TypeOf(&InstrumentedService{})
if err.Services[failer] != failure {
t.Fatalf("failer termination failure mismatch: have %v, want %v", err.Services[failer], failure)
}
if len(err.Services) != 1 {
t.Fatalf("failure count mismatch: have %d, want %d", len(err.Services), 1)
}
}
for id := range lifecycles {
if !stopped[id] {
t.Fatalf("service %s: service not terminated", id)
}
delete(started, id)
delete(stopped, id)
}
stack.server = &p2p.Server{}
stack.server.PrivateKey = testNodeKey
}
// Tests whether a handler can be successfully mounted on the canonical HTTP server
// on the given prefix
func TestRegisterHandler_Successful(t *testing.T) {
node := createNode(t, 7878, 7979)
defer node.Close()
// create and mount handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("success"))
})
node.RegisterHandler("test", "/test", handler)
// start node
if err := node.Start(); err != nil {
t.Fatalf("could not start node: %v", err)
}
// create HTTP request
httpReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:7878/test", nil)
if err != nil {
t.Error("could not issue new http request ", err)
}
// check response
resp := doHTTPRequest(t, httpReq)
buf := make([]byte, 7)
_, err = io.ReadFull(resp.Body, buf)
if err != nil {
t.Fatalf("could not read response: %v", err)
}
assert.Equal(t, "success", string(buf))
}
// Tests that the given handler will not be successfully mounted since no HTTP server
// is enabled for RPC
func TestRegisterHandler_Unsuccessful(t *testing.T) {
node, err := New(&DefaultConfig)
if err != nil {
t.Fatalf("could not create new node: %v", err)
}
// create and mount handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("success"))
})
node.RegisterHandler("test", "/test", handler)
}
// Tests whether websocket requests can be handled on the same port as a regular http server.
func TestWebsocketHTTPOnSamePort_WebsocketRequest(t *testing.T) {
node := startHTTP(t, 0, 0)
defer node.Close()
ws := strings.Replace(node.HTTPEndpoint(), "http://", "ws://", 1)
if node.WSEndpoint() != ws {
t.Fatalf("endpoints should be the same")
}
if !checkRPC(ws) {
t.Fatalf("ws request failed")
}
if !checkRPC(node.HTTPEndpoint()) {
t.Fatalf("http request failed")
}
}
func TestWebsocketHTTPOnSeparatePort_WSRequest(t *testing.T) {
// try and get a free port
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal("can't listen:", err)
}
port := listener.Addr().(*net.TCPAddr).Port
listener.Close()
node := startHTTP(t, 0, port)
defer node.Close()
wsOnHTTP := strings.Replace(node.HTTPEndpoint(), "http://", "ws://", 1)
ws := fmt.Sprintf("ws://127.0.0.1:%d", port)
if node.WSEndpoint() == wsOnHTTP {
t.Fatalf("endpoints should not be the same")
}
// ensure ws endpoint matches the expected endpoint
if node.WSEndpoint() != ws {
t.Fatalf("ws endpoint is incorrect: expected %s, got %s", ws, node.WSEndpoint())
}
if !checkRPC(ws) {
t.Fatalf("ws request failed")
}
if !checkRPC(node.HTTPEndpoint()) {
t.Fatalf("http request failed")
}
}
type rpcPrefixTest struct {
httpPrefix, wsPrefix string
// These lists paths on which JSON-RPC should be served / not served.
wantHTTP []string
wantNoHTTP []string
wantWS []string
wantNoWS []string
}
func TestNodeRPCPrefix(t *testing.T) {
t.Parallel()
tests := []rpcPrefixTest{
// both off
{
httpPrefix: "", wsPrefix: "",
wantHTTP: []string{"/", "/?p=1"},
wantNoHTTP: []string{"/test", "/test?p=1"},
wantWS: []string{"/", "/?p=1"},
wantNoWS: []string{"/test", "/test?p=1"},
},
// only http prefix
{
httpPrefix: "/testprefix", wsPrefix: "",
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
wantWS: []string{"/", "/?p=1"},
wantNoWS: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
},
// only ws prefix
{
httpPrefix: "", wsPrefix: "/testprefix",
wantHTTP: []string{"/", "/?p=1"},
wantNoHTTP: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
},
// both set
{
httpPrefix: "/testprefix", wsPrefix: "/testprefix",
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
},
}
for _, test := range tests {
test := test
name := fmt.Sprintf("http=%s ws=%s", test.httpPrefix, test.wsPrefix)
t.Run(name, func(t *testing.T) {
cfg := &Config{
HTTPHost: "127.0.0.1",
HTTPPathPrefix: test.httpPrefix,
WSHost: "127.0.0.1",
WSPathPrefix: test.wsPrefix,
}
node, err := New(cfg)
if err != nil {
t.Fatal("can't create node:", err)
}
defer node.Close()
if err := node.Start(); err != nil {
t.Fatal("can't start node:", err)
}
test.check(t, node)
})
}
}
func (test rpcPrefixTest) check(t *testing.T, node *Node) {
t.Helper()
httpBase := "http://" + node.http.listenAddr()
wsBase := "ws://" + node.http.listenAddr()
if node.WSEndpoint() != wsBase+test.wsPrefix {
t.Errorf("Error: node has wrong WSEndpoint %q", node.WSEndpoint())
}
for _, path := range test.wantHTTP {
resp := rpcRequest(t, httpBase+path, testMethod)
if resp.StatusCode != 200 {
t.Errorf("Error: %s: bad status code %d, want 200", path, resp.StatusCode)
}
}
for _, path := range test.wantNoHTTP {
resp := rpcRequest(t, httpBase+path, testMethod)
if resp.StatusCode != 404 {
t.Errorf("Error: %s: bad status code %d, want 404", path, resp.StatusCode)
}
}
for _, path := range test.wantWS {
err := wsRequest(t, wsBase+path)
if err != nil {
t.Errorf("Error: %s: WebSocket connection failed: %v", path, err)
}
}
for _, path := range test.wantNoWS {
err := wsRequest(t, wsBase+path)
if err == nil {
t.Errorf("Error: %s: WebSocket connection succeeded for path in wantNoWS", path)
}
}
}
func createNode(t *testing.T, httpPort, wsPort int) *Node {
conf := &Config{
HTTPHost: "127.0.0.1",
HTTPPort: httpPort,
WSHost: "127.0.0.1",
WSPort: wsPort,
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
}
node, err := New(conf)
if err != nil {
t.Fatalf("could not create a new node: %v", err)
}
return node
}
func startHTTP(t *testing.T, httpPort, wsPort int) *Node {
node := createNode(t, httpPort, wsPort)
err := node.Start()
if err != nil {
t.Fatalf("could not start http service on node: %v", err)
}
return node
}
func doHTTPRequest(t *testing.T, req *http.Request) *http.Response {
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
t.Fatalf("could not issue a GET request to the given endpoint: %v", err)
}
t.Cleanup(func() { resp.Body.Close() })
return resp
}
func containsProtocol(stackProtocols []p2p.Protocol, protocol p2p.Protocol) bool {
for _, a := range stackProtocols {
if reflect.DeepEqual(a, protocol) {
return true
}
}
return false
}
func containsAPI(stackAPIs []rpc.API, api rpc.API) bool {
for _, a := range stackAPIs {
if reflect.DeepEqual(a, api) {
return true
}
}
return false
}

651
.github/workflows/rpcstack.go vendored Normal file
View File

@ -0,0 +1,651 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"compress/gzip"
"context"
"fmt"
"io"
"net"
"net/http"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/rs/cors"
)
// httpConfig is the JSON-RPC/HTTP configuration.
type httpConfig struct {
Modules []string
CorsAllowedOrigins []string
Vhosts []string
prefix string // path prefix on which to mount http handler
rpcEndpointConfig
}
// wsConfig is the JSON-RPC/Websocket configuration
type wsConfig struct {
Origins []string
Modules []string
prefix string // path prefix on which to mount ws handler
rpcEndpointConfig
}
type rpcEndpointConfig struct {
jwtSecret []byte // optional JWT secret
batchItemLimit int
batchResponseSizeLimit int
httpBodyLimit int
}
type rpcHandler struct {
http.Handler
server *rpc.Server
}
type httpServer struct {
log log.Logger
timeouts rpc.HTTPTimeouts
mux http.ServeMux // registered handlers go here
mu sync.Mutex
server *http.Server
listener net.Listener // non-nil when server is running
// HTTP RPC handler things.
httpConfig httpConfig
httpHandler atomic.Value // *rpcHandler
// WebSocket handler things.
wsConfig wsConfig
wsHandler atomic.Value // *rpcHandler
// These are set by setListenAddr.
endpoint string
host string
port int
handlerNames map[string]string
}
const (
shutdownTimeout = 5 * time.Second
)
func newHTTPServer(log log.Logger, timeouts rpc.HTTPTimeouts) *httpServer {
h := &httpServer{log: log, timeouts: timeouts, handlerNames: make(map[string]string)}
h.httpHandler.Store((*rpcHandler)(nil))
h.wsHandler.Store((*rpcHandler)(nil))
return h
}
// setListenAddr configures the listening address of the server.
// The address can only be set while the server isn't running.
func (h *httpServer) setListenAddr(host string, port int) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.listener != nil && (host != h.host || port != h.port) {
return fmt.Errorf("HTTP server already running on %s", h.endpoint)
}
h.host, h.port = host, port
h.endpoint = net.JoinHostPort(host, fmt.Sprintf("%d", port))
return nil
}
// listenAddr returns the listening address of the server.
func (h *httpServer) listenAddr() string {
h.mu.Lock()
defer h.mu.Unlock()
if h.listener != nil {
return h.listener.Addr().String()
}
return h.endpoint
}
// start starts the HTTP server if it is enabled and not already running.
func (h *httpServer) start() error {
h.mu.Lock()
defer h.mu.Unlock()
if h.endpoint == "" || h.listener != nil {
return nil // already running or not configured
}
// Initialize the server.
h.server = &http.Server{Handler: h}
if h.timeouts != (rpc.HTTPTimeouts{}) {
CheckTimeouts(&h.timeouts)
h.server.ReadTimeout = h.timeouts.ReadTimeout
h.server.ReadHeaderTimeout = h.timeouts.ReadHeaderTimeout
h.server.WriteTimeout = h.timeouts.WriteTimeout
h.server.IdleTimeout = h.timeouts.IdleTimeout
}
// Start the server.
listener, err := net.Listen("tcp", h.endpoint)
if err != nil {
// If the server fails to start, we need to clear out the RPC and WS
// configuration so they can be configured another time.
h.disableRPC()
h.disableWS()
return err
}
h.listener = listener
go h.server.Serve(listener)
if h.wsAllowed() {
url := fmt.Sprintf("ws://%v", listener.Addr())
if h.wsConfig.prefix != "" {
url += h.wsConfig.prefix
}
h.log.Info("WebSocket enabled", "url", url)
}
// if server is websocket only, return after logging
if !h.rpcAllowed() {
return nil
}
// Log http endpoint.
h.log.Info("HTTP server started",
"endpoint", listener.Addr(), "auth", (h.httpConfig.jwtSecret != nil),
"prefix", h.httpConfig.prefix,
"cors", strings.Join(h.httpConfig.CorsAllowedOrigins, ","),
"vhosts", strings.Join(h.httpConfig.Vhosts, ","),
)
// Log all handlers mounted on server.
var paths []string
for path := range h.handlerNames {
paths = append(paths, path)
}
sort.Strings(paths)
logged := make(map[string]bool, len(paths))
for _, path := range paths {
name := h.handlerNames[path]
if !logged[name] {
log.Info(name+" enabled", "url", "http://"+listener.Addr().String()+path)
logged[name] = true
}
}
return nil
}
func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// check if ws request and serve if ws enabled
ws := h.wsHandler.Load().(*rpcHandler)
if ws != nil && isWebsocket(r) {
if checkPath(r, h.wsConfig.prefix) {
ws.ServeHTTP(w, r)
}
return
}
// if http-rpc is enabled, try to serve request
rpc := h.httpHandler.Load().(*rpcHandler)
if rpc != nil {
// First try to route in the mux.
// Requests to a path below root are handled by the mux,
// which has all the handlers registered via Node.RegisterHandler.
// These are made available when RPC is enabled.
muxHandler, pattern := h.mux.Handler(r)
if pattern != "" {
muxHandler.ServeHTTP(w, r)
return
}
if checkPath(r, h.httpConfig.prefix) {
rpc.ServeHTTP(w, r)
return
}
}
w.WriteHeader(http.StatusNotFound)
}
// checkPath checks whether a given request URL matches a given path prefix.
func checkPath(r *http.Request, path string) bool {
// if no prefix has been specified, request URL must be on root
if path == "" {
return r.URL.Path == "/"
}
// otherwise, check to make sure prefix matches
return len(r.URL.Path) >= len(path) && r.URL.Path[:len(path)] == path
}
// validatePrefix checks if 'path' is a valid configuration value for the RPC prefix option.
func validatePrefix(what, path string) error {
if path == "" {
return nil
}
if path[0] != '/' {
return fmt.Errorf(`%s RPC path prefix %q does not contain leading "/"`, what, path)
}
if strings.ContainsAny(path, "?#") {
// This is just to avoid confusion. While these would match correctly (i.e. they'd
// match if URL-escaped into path), it's not easy to understand for users when
// setting that on the command line.
return fmt.Errorf("%s RPC path prefix %q contains URL meta-characters", what, path)
}
return nil
}
// stop shuts down the HTTP server.
func (h *httpServer) stop() {
h.mu.Lock()
defer h.mu.Unlock()
h.doStop()
}
func (h *httpServer) doStop() {
if h.listener == nil {
return // not running
}
// Shut down the server.
httpHandler := h.httpHandler.Load().(*rpcHandler)
wsHandler := h.wsHandler.Load().(*rpcHandler)
if httpHandler != nil {
h.httpHandler.Store((*rpcHandler)(nil))
httpHandler.server.Stop()
}
if wsHandler != nil {
h.wsHandler.Store((*rpcHandler)(nil))
wsHandler.server.Stop()
}
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
err := h.server.Shutdown(ctx)
if err != nil && err == ctx.Err() {
h.log.Warn("HTTP server graceful shutdown timed out")
h.server.Close()
}
h.listener.Close()
h.log.Info("HTTP server stopped", "endpoint", h.listener.Addr())
// Clear out everything to allow re-configuring it later.
h.host, h.port, h.endpoint = "", 0, ""
h.server, h.listener = nil, nil
}
// enableRPC turns on JSON-RPC over HTTP on the server.
func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.rpcAllowed() {
return fmt.Errorf("JSON-RPC over HTTP is already enabled")
}
// Create RPC server and handler.
srv := rpc.NewServer()
srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit)
if config.httpBodyLimit > 0 {
srv.SetHTTPBodyLimit(config.httpBodyLimit)
}
if err := RegisterApis(apis, config.Modules, srv); err != nil {
return err
}
h.httpConfig = config
h.httpHandler.Store(&rpcHandler{
Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret),
server: srv,
})
return nil
}
// disableRPC stops the HTTP RPC handler. This is internal, the caller must hold h.mu.
func (h *httpServer) disableRPC() bool {
handler := h.httpHandler.Load().(*rpcHandler)
if handler != nil {
h.httpHandler.Store((*rpcHandler)(nil))
handler.server.Stop()
}
return handler != nil
}
// enableWS turns on JSON-RPC over WebSocket on the server.
func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.wsAllowed() {
return fmt.Errorf("JSON-RPC over WebSocket is already enabled")
}
// Create RPC server and handler.
srv := rpc.NewServer()
srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit)
if config.httpBodyLimit > 0 {
srv.SetHTTPBodyLimit(config.httpBodyLimit)
}
if err := RegisterApis(apis, config.Modules, srv); err != nil {
return err
}
h.wsConfig = config
h.wsHandler.Store(&rpcHandler{
Handler: NewWSHandlerStack(srv.WebsocketHandler(config.Origins), config.jwtSecret),
server: srv,
})
return nil
}
// stopWS disables JSON-RPC over WebSocket and also stops the server if it only serves WebSocket.
func (h *httpServer) stopWS() {
h.mu.Lock()
defer h.mu.Unlock()
if h.disableWS() {
if !h.rpcAllowed() {
h.doStop()
}
}
}
// disableWS disables the WebSocket handler. This is internal, the caller must hold h.mu.
func (h *httpServer) disableWS() bool {
ws := h.wsHandler.Load().(*rpcHandler)
if ws != nil {
h.wsHandler.Store((*rpcHandler)(nil))
ws.server.Stop()
}
return ws != nil
}
// rpcAllowed returns true when JSON-RPC over HTTP is enabled.
func (h *httpServer) rpcAllowed() bool {
return h.httpHandler.Load().(*rpcHandler) != nil
}
// wsAllowed returns true when JSON-RPC over WebSocket is enabled.
func (h *httpServer) wsAllowed() bool {
return h.wsHandler.Load().(*rpcHandler) != nil
}
// isWebsocket checks the header of an http request for a websocket upgrade request.
func isWebsocket(r *http.Request) bool {
return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") &&
strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade")
}
// NewHTTPHandlerStack returns wrapped http-related handlers
func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string, jwtSecret []byte) http.Handler {
// Wrap the CORS-handler within a host-handler
handler := newCorsHandler(srv, cors)
handler = newVHostHandler(vhosts, handler)
if len(jwtSecret) != 0 {
handler = newJWTHandler(jwtSecret, handler)
}
return newGzipHandler(handler)
}
// NewWSHandlerStack returns a wrapped ws-related handler.
func NewWSHandlerStack(srv http.Handler, jwtSecret []byte) http.Handler {
if len(jwtSecret) != 0 {
return newJWTHandler(jwtSecret, srv)
}
return srv
}
func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler {
// disable CORS support if user has not specified a custom CORS configuration
if len(allowedOrigins) == 0 {
return srv
}
c := cors.New(cors.Options{
AllowedOrigins: allowedOrigins,
AllowedMethods: []string{http.MethodPost, http.MethodGet},
AllowedHeaders: []string{"*"},
MaxAge: 600,
})
return c.Handler(srv)
}
// virtualHostHandler is a handler which validates the Host-header of incoming requests.
// Using virtual hosts can help prevent DNS rebinding attacks, where a 'random' domain name points to
// the service ip address (but without CORS headers). By verifying the targeted virtual host, we can
// ensure that it's a destination that the node operator has defined.
type virtualHostHandler struct {
vhosts map[string]struct{}
next http.Handler
}
func newVHostHandler(vhosts []string, next http.Handler) http.Handler {
vhostMap := make(map[string]struct{})
for _, allowedHost := range vhosts {
vhostMap[strings.ToLower(allowedHost)] = struct{}{}
}
return &virtualHostHandler{vhostMap, next}
}
// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler
func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// if r.Host is not set, we can continue serving since a browser would set the Host header
if r.Host == "" {
h.next.ServeHTTP(w, r)
return
}
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
// Either invalid (too many colons) or no port specified
host = r.Host
}
if ipAddr := net.ParseIP(host); ipAddr != nil {
// It's an IP address, we can serve that
h.next.ServeHTTP(w, r)
return
}
// Not an IP address, but a hostname. Need to validate
if _, exist := h.vhosts["*"]; exist {
h.next.ServeHTTP(w, r)
return
}
if _, exist := h.vhosts[host]; exist {
h.next.ServeHTTP(w, r)
return
}
http.Error(w, "invalid host specified", http.StatusForbidden)
}
var gzPool = sync.Pool{
New: func() interface{} {
w := gzip.NewWriter(io.Discard)
return w
},
}
type gzipResponseWriter struct {
resp http.ResponseWriter
gz *gzip.Writer
contentLength uint64 // total length of the uncompressed response
written uint64 // amount of written bytes from the uncompressed response
hasLength bool // true if uncompressed response had Content-Length
inited bool // true after init was called for the first time
}
// init runs just before response headers are written. Among other things, this function
// also decides whether compression will be applied at all.
func (w *gzipResponseWriter) init() {
if w.inited {
return
}
w.inited = true
hdr := w.resp.Header()
length := hdr.Get("content-length")
if len(length) > 0 {
if n, err := strconv.ParseUint(length, 10, 64); err != nil {
w.hasLength = true
w.contentLength = n
}
}
// Setting Transfer-Encoding to "identity" explicitly disables compression. net/http
// also recognizes this header value and uses it to disable "chunked" transfer
// encoding, trimming the header from the response. This means downstream handlers can
// set this without harm, even if they aren't wrapped by newGzipHandler.
//
// In go-ethereum, we use this signal to disable compression for certain error
// responses which are flushed out close to the write deadline of the response. For
// these cases, we want to avoid chunked transfer encoding and compression because
// they require additional output that may not get written in time.
passthrough := hdr.Get("transfer-encoding") == "identity"
if !passthrough {
w.gz = gzPool.Get().(*gzip.Writer)
w.gz.Reset(w.resp)
hdr.Del("content-length")
hdr.Set("content-encoding", "gzip")
}
}
func (w *gzipResponseWriter) Header() http.Header {
return w.resp.Header()
}
func (w *gzipResponseWriter) WriteHeader(status int) {
w.init()
w.resp.WriteHeader(status)
}
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
w.init()
if w.gz == nil {
// Compression is disabled.
return w.resp.Write(b)
}
n, err := w.gz.Write(b)
w.written += uint64(n)
if w.hasLength && w.written >= w.contentLength {
// The HTTP handler has finished writing the entire uncompressed response. Close
// the gzip stream to ensure the footer will be seen by the client in case the
// response is flushed after this call to write.
err = w.gz.Close()
}
return n, err
}
func (w *gzipResponseWriter) Flush() {
if w.gz != nil {
w.gz.Flush()
}
if f, ok := w.resp.(http.Flusher); ok {
f.Flush()
}
}
func (w *gzipResponseWriter) close() {
if w.gz == nil {
return
}
w.gz.Close()
gzPool.Put(w.gz)
w.gz = nil
}
func newGzipHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
wrapper := &gzipResponseWriter{resp: w}
defer wrapper.close()
next.ServeHTTP(wrapper, r)
})
}
type ipcServer struct {
log log.Logger
endpoint string
mu sync.Mutex
listener net.Listener
srv *rpc.Server
}
func newIPCServer(log log.Logger, endpoint string) *ipcServer {
return &ipcServer{log: log, endpoint: endpoint}
}
// Start starts the httpServer's http.Server
func (is *ipcServer) start(apis []rpc.API) error {
is.mu.Lock()
defer is.mu.Unlock()
if is.listener != nil {
return nil // already running
}
listener, srv, err := rpc.StartIPCEndpoint(is.endpoint, apis)
if err != nil {
is.log.Warn("IPC opening failed", "url", is.endpoint, "error", err)
return err
}
is.log.Info("IPC endpoint opened", "url", is.endpoint)
is.listener, is.srv = listener, srv
return nil
}
func (is *ipcServer) stop() error {
is.mu.Lock()
defer is.mu.Unlock()
if is.listener == nil {
return nil // not running
}
err := is.listener.Close()
is.srv.Stop()
is.listener, is.srv = nil, nil
is.log.Info("IPC endpoint closed", "url", is.endpoint)
return err
}
// RegisterApis checks the given modules' availability, generates an allowlist based on the allowed modules,
// and then registers all of the APIs exposed by the services.
func RegisterApis(apis []rpc.API, modules []string, srv *rpc.Server) error {
if bad, available := checkModuleAvailability(modules, apis); len(bad) > 0 {
log.Error("Unavailable modules in HTTP API list", "unavailable", bad, "available", available)
}
// Generate the allow list based on the allowed modules
allowList := make(map[string]bool)
for _, module := range modules {
allowList[module] = true
}
// Register all the APIs exposed by the services
for _, api := range apis {
if allowList[api.Namespace] || len(allowList) == 0 {
if err := srv.RegisterName(api.Namespace, api.Service); err != nil {
return err
}
}
}
return nil
}

616
.github/workflows/rpcstack_test.go vendored Normal file
View File

@ -0,0 +1,616 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package node
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
"github.com/ethereum/go-ethereum/internal/testlog"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/golang-jwt/jwt/v4"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
)
const testMethod = "rpc_modules"
// TestCorsHandler makes sure CORS are properly handled on the http server.
func TestCorsHandler(t *testing.T) {
srv := createAndStartServer(t, &httpConfig{CorsAllowedOrigins: []string{"test", "test.com"}}, false, &wsConfig{}, nil)
defer srv.stop()
url := "http://" + srv.listenAddr()
resp := rpcRequest(t, url, testMethod, "origin", "test.com")
assert.Equal(t, "test.com", resp.Header.Get("Access-Control-Allow-Origin"))
resp2 := rpcRequest(t, url, testMethod, "origin", "bad")
assert.Equal(t, "", resp2.Header.Get("Access-Control-Allow-Origin"))
}
// TestVhosts makes sure vhosts are properly handled on the http server.
func TestVhosts(t *testing.T) {
srv := createAndStartServer(t, &httpConfig{Vhosts: []string{"test"}}, false, &wsConfig{}, nil)
defer srv.stop()
url := "http://" + srv.listenAddr()
resp := rpcRequest(t, url, testMethod, "host", "test")
assert.Equal(t, resp.StatusCode, http.StatusOK)
resp2 := rpcRequest(t, url, testMethod, "host", "bad")
assert.Equal(t, resp2.StatusCode, http.StatusForbidden)
}
type originTest struct {
spec string
expOk []string
expFail []string
}
// splitAndTrim splits input separated by a comma
// and trims excessive white space from the substrings.
// Copied over from flags.go
func splitAndTrim(input string) (ret []string) {
l := strings.Split(input, ",")
for _, r := range l {
r = strings.TrimSpace(r)
if len(r) > 0 {
ret = append(ret, r)
}
}
return ret
}
// TestWebsocketOrigins makes sure the websocket origins are properly handled on the websocket server.
func TestWebsocketOrigins(t *testing.T) {
tests := []originTest{
{
spec: "*", // allow all
expOk: []string{"", "http://test", "https://test", "http://test:8540", "https://test:8540",
"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
},
{
spec: "test",
expOk: []string{"http://test", "https://test", "http://test:8540", "https://test:8540"},
expFail: []string{"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
},
// scheme tests
{
spec: "https://test",
expOk: []string{"https://test", "https://test:9999"},
expFail: []string{
"test", // no scheme, required by spec
"http://test", // wrong scheme
"http://test.foo", "https://a.test.x", // subdomain variations
"http://testx:8540", "https://xtest:8540"},
},
// ip tests
{
spec: "https://12.34.56.78",
expOk: []string{"https://12.34.56.78", "https://12.34.56.78:8540"},
expFail: []string{
"http://12.34.56.78", // wrong scheme
"http://12.34.56.78:443", // wrong scheme
"http://1.12.34.56.78", // wrong 'domain name'
"http://12.34.56.78.a", // wrong 'domain name'
"https://87.65.43.21", "http://87.65.43.21:8540", "https://87.65.43.21:8540"},
},
// port tests
{
spec: "test:8540",
expOk: []string{"http://test:8540", "https://test:8540"},
expFail: []string{
"http://test", "https://test", // spec says port required
"http://test:8541", "https://test:8541", // wrong port
"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
},
// scheme and port
{
spec: "https://test:8540",
expOk: []string{"https://test:8540"},
expFail: []string{
"https://test", // missing port
"http://test", // missing port, + wrong scheme
"http://test:8540", // wrong scheme
"http://test:8541", "https://test:8541", // wrong port
"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
},
// several allowed origins
{
spec: "localhost,http://127.0.0.1",
expOk: []string{"localhost", "http://localhost", "https://localhost:8443",
"http://127.0.0.1", "http://127.0.0.1:8080"},
expFail: []string{
"https://127.0.0.1", // wrong scheme
"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
},
}
for _, tc := range tests {
srv := createAndStartServer(t, &httpConfig{}, true, &wsConfig{Origins: splitAndTrim(tc.spec)}, nil)
url := fmt.Sprintf("ws://%v", srv.listenAddr())
for _, origin := range tc.expOk {
if err := wsRequest(t, url, "Origin", origin); err != nil {
t.Errorf("spec '%v', origin '%v': expected ok, got %v", tc.spec, origin, err)
}
}
for _, origin := range tc.expFail {
if err := wsRequest(t, url, "Origin", origin); err == nil {
t.Errorf("spec '%v', origin '%v': expected not to allow, got ok", tc.spec, origin)
}
}
srv.stop()
}
}
// TestIsWebsocket tests if an incoming websocket upgrade request is handled properly.
func TestIsWebsocket(t *testing.T) {
r, _ := http.NewRequest(http.MethodGet, "/", nil)
assert.False(t, isWebsocket(r))
r.Header.Set("upgrade", "websocket")
assert.False(t, isWebsocket(r))
r.Header.Set("connection", "upgrade")
assert.True(t, isWebsocket(r))
r.Header.Set("connection", "upgrade,keep-alive")
assert.True(t, isWebsocket(r))
r.Header.Set("connection", " UPGRADE,keep-alive")
assert.True(t, isWebsocket(r))
}
func Test_checkPath(t *testing.T) {
tests := []struct {
req *http.Request
prefix string
expected bool
}{
{
req: &http.Request{URL: &url.URL{Path: "/test"}},
prefix: "/test",
expected: true,
},
{
req: &http.Request{URL: &url.URL{Path: "/testing"}},
prefix: "/test",
expected: true,
},
{
req: &http.Request{URL: &url.URL{Path: "/"}},
prefix: "/test",
expected: false,
},
{
req: &http.Request{URL: &url.URL{Path: "/fail"}},
prefix: "/test",
expected: false,
},
{
req: &http.Request{URL: &url.URL{Path: "/"}},
prefix: "",
expected: true,
},
{
req: &http.Request{URL: &url.URL{Path: "/fail"}},
prefix: "",
expected: false,
},
{
req: &http.Request{URL: &url.URL{Path: "/"}},
prefix: "/",
expected: true,
},
{
req: &http.Request{URL: &url.URL{Path: "/testing"}},
prefix: "/",
expected: true,
},
}
for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
assert.Equal(t, tt.expected, checkPath(tt.req, tt.prefix))
})
}
}
func createAndStartServer(t *testing.T, conf *httpConfig, ws bool, wsConf *wsConfig, timeouts *rpc.HTTPTimeouts) *httpServer {
t.Helper()
if timeouts == nil {
timeouts = &rpc.DefaultHTTPTimeouts
}
srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), *timeouts)
assert.NoError(t, srv.enableRPC(apis(), *conf))
if ws {
assert.NoError(t, srv.enableWS(nil, *wsConf))
}
assert.NoError(t, srv.setListenAddr("localhost", 0))
assert.NoError(t, srv.start())
return srv
}
// wsRequest attempts to open a WebSocket connection to the given URL.
func wsRequest(t *testing.T, url string, extraHeaders ...string) error {
t.Helper()
//t.Logf("checking WebSocket on %s (origin %q)", url, browserOrigin)
headers := make(http.Header)
// Apply extra headers.
if len(extraHeaders)%2 != 0 {
panic("odd extraHeaders length")
}
for i := 0; i < len(extraHeaders); i += 2 {
key, value := extraHeaders[i], extraHeaders[i+1]
headers.Set(key, value)
}
conn, _, err := websocket.DefaultDialer.Dial(url, headers)
if conn != nil {
conn.Close()
}
return err
}
// rpcRequest performs a JSON-RPC request to the given URL.
func rpcRequest(t *testing.T, url, method string, extraHeaders ...string) *http.Response {
t.Helper()
body := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"%s","params":[]}`, method)
return baseRpcRequest(t, url, body, extraHeaders...)
}
func batchRpcRequest(t *testing.T, url string, methods []string, extraHeaders ...string) *http.Response {
reqs := make([]string, len(methods))
for i, m := range methods {
reqs[i] = fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"%s","params":[]}`, m)
}
body := fmt.Sprintf(`[%s]`, strings.Join(reqs, ","))
return baseRpcRequest(t, url, body, extraHeaders...)
}
func baseRpcRequest(t *testing.T, url, bodyStr string, extraHeaders ...string) *http.Response {
t.Helper()
// Create the request.
body := bytes.NewReader([]byte(bodyStr))
req, err := http.NewRequest(http.MethodPost, url, body)
if err != nil {
t.Fatal("could not create http request:", err)
}
req.Header.Set("content-type", "application/json")
req.Header.Set("accept-encoding", "identity")
// Apply extra headers.
if len(extraHeaders)%2 != 0 {
panic("odd extraHeaders length")
}
for i := 0; i < len(extraHeaders); i += 2 {
key, value := extraHeaders[i], extraHeaders[i+1]
if strings.EqualFold(key, "host") {
req.Host = value
} else {
req.Header.Set(key, value)
}
}
// Perform the request.
t.Logf("checking RPC/HTTP on %s %v", url, extraHeaders)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { resp.Body.Close() })
return resp
}
type testClaim map[string]interface{}
func (testClaim) Valid() error {
return nil
}
func TestJWT(t *testing.T) {
var secret = []byte("secret")
issueToken := func(secret []byte, method jwt.SigningMethod, input map[string]interface{}) string {
if method == nil {
method = jwt.SigningMethodHS256
}
ss, _ := jwt.NewWithClaims(method, testClaim(input)).SignedString(secret)
return ss
}
cfg := rpcEndpointConfig{jwtSecret: []byte("secret")}
httpcfg := &httpConfig{rpcEndpointConfig: cfg}
wscfg := &wsConfig{Origins: []string{"*"}, rpcEndpointConfig: cfg}
srv := createAndStartServer(t, httpcfg, true, wscfg, nil)
wsUrl := fmt.Sprintf("ws://%v", srv.listenAddr())
htUrl := fmt.Sprintf("http://%v", srv.listenAddr())
expOk := []func() string{
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
},
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + 4}))
},
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - 4}))
},
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
"iat": time.Now().Unix(),
"exp": time.Now().Unix() + 2,
}))
},
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
"iat": time.Now().Unix(),
"bar": "baz",
}))
},
}
for i, tokenFn := range expOk {
token := tokenFn()
if err := wsRequest(t, wsUrl, "Authorization", token); err != nil {
t.Errorf("test %d-ws, token '%v': expected ok, got %v", i, token, err)
}
token = tokenFn()
if resp := rpcRequest(t, htUrl, testMethod, "Authorization", token); resp.StatusCode != 200 {
t.Errorf("test %d-http, token '%v': expected ok, got %v", i, token, resp.StatusCode)
}
}
expFail := []func() string{
// future
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + int64(jwtExpiryTimeout.Seconds()) + 1}))
},
// stale
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - int64(jwtExpiryTimeout.Seconds()) - 1}))
},
// wrong algo
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, jwt.SigningMethodHS512, testClaim{"iat": time.Now().Unix() + 4}))
},
// expired
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix(), "exp": time.Now().Unix()}))
},
// missing mandatory iat
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{}))
},
// wrong secret
func() string {
return fmt.Sprintf("Bearer %v", issueToken([]byte("wrong"), nil, testClaim{"iat": time.Now().Unix()}))
},
func() string {
return fmt.Sprintf("Bearer %v", issueToken([]byte{}, nil, testClaim{"iat": time.Now().Unix()}))
},
func() string {
return fmt.Sprintf("Bearer %v", issueToken(nil, nil, testClaim{"iat": time.Now().Unix()}))
},
// Various malformed syntax
func() string {
return fmt.Sprintf("%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
},
func() string {
return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
},
func() string {
return fmt.Sprintf("bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
},
func() string {
return fmt.Sprintf("Bearer: %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
},
func() string {
return fmt.Sprintf("Bearer:%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
},
func() string {
return fmt.Sprintf("Bearer\t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
},
func() string {
return fmt.Sprintf("Bearer \t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
},
}
for i, tokenFn := range expFail {
token := tokenFn()
if err := wsRequest(t, wsUrl, "Authorization", token); err == nil {
t.Errorf("tc %d-ws, token '%v': expected not to allow, got ok", i, token)
}
token = tokenFn()
resp := rpcRequest(t, htUrl, testMethod, "Authorization", token)
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("tc %d-http, token '%v': expected not to allow, got %v", i, token, resp.StatusCode)
}
}
srv.stop()
}
func TestGzipHandler(t *testing.T) {
type gzipTest struct {
name string
handler http.HandlerFunc
status int
isGzip bool
header map[string]string
}
tests := []gzipTest{
{
name: "Write",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("response"))
},
isGzip: true,
status: 200,
},
{
name: "WriteHeader",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("x-foo", "bar")
w.WriteHeader(205)
w.Write([]byte("response"))
},
isGzip: true,
status: 205,
header: map[string]string{"x-foo": "bar"},
},
{
name: "WriteContentLength",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-length", "8")
w.Write([]byte("response"))
},
isGzip: true,
status: 200,
},
{
name: "Flush",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("res"))
w.(http.Flusher).Flush()
w.Write([]byte("ponse"))
},
isGzip: true,
status: 200,
},
{
name: "disable",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("transfer-encoding", "identity")
w.Header().Set("x-foo", "bar")
w.Write([]byte("response"))
},
isGzip: false,
status: 200,
header: map[string]string{"x-foo": "bar"},
},
{
name: "disable-WriteHeader",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("transfer-encoding", "identity")
w.Header().Set("x-foo", "bar")
w.WriteHeader(205)
w.Write([]byte("response"))
},
isGzip: false,
status: 205,
header: map[string]string{"x-foo": "bar"},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
srv := httptest.NewServer(newGzipHandler(test.handler))
defer srv.Close()
resp, err := http.Get(srv.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
content, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
wasGzip := resp.Uncompressed
if string(content) != "response" {
t.Fatalf("wrong response content %q", content)
}
if wasGzip != test.isGzip {
t.Fatalf("response gzipped == %t, want %t", wasGzip, test.isGzip)
}
if resp.StatusCode != test.status {
t.Fatalf("response status == %d, want %d", resp.StatusCode, test.status)
}
for name, expectedValue := range test.header {
if v := resp.Header.Get(name); v != expectedValue {
t.Fatalf("response header %s == %s, want %s", name, v, expectedValue)
}
}
})
}
}
func TestHTTPWriteTimeout(t *testing.T) {
const (
timeoutRes = `{"jsonrpc":"2.0","id":1,"error":{"code":-32002,"message":"request timed out"}}`
greetRes = `{"jsonrpc":"2.0","id":1,"result":"Hello"}`
)
// Set-up server
timeouts := rpc.DefaultHTTPTimeouts
timeouts.WriteTimeout = time.Second
srv := createAndStartServer(t, &httpConfig{Modules: []string{"test"}}, false, &wsConfig{}, &timeouts)
url := fmt.Sprintf("http://%v", srv.listenAddr())
// Send normal request
t.Run("message", func(t *testing.T) {
resp := rpcRequest(t, url, "test_sleep")
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(body) != timeoutRes {
t.Errorf("wrong response. have %s, want %s", string(body), timeoutRes)
}
})
// Batch request
t.Run("batch", func(t *testing.T) {
want := fmt.Sprintf("[%s,%s,%s]", greetRes, timeoutRes, timeoutRes)
resp := batchRpcRequest(t, url, []string{"test_greet", "test_sleep", "test_greet"})
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(body) != want {
t.Errorf("wrong response. have %s, want %s", string(body), want)
}
})
}
func apis() []rpc.API {
return []rpc.API{
{
Namespace: "test",
Service: &testService{},
},
}
}
type testService struct{}
func (s *testService) Greet() string {
return "Hello"
}
func (s *testService) Sleep() {
time.Sleep(1500 * time.Millisecond)
}

106
.github/workflows/utils_test.go vendored Normal file
View File

@ -0,0 +1,106 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// Contains a batch of utility type declarations used by the tests. As the node
// operates on unique types, a lot of them are needed to check various features.
package node
import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// NoopLifecycle is a trivial implementation of the Service interface.
type NoopLifecycle struct{}
func (s *NoopLifecycle) Start() error { return nil }
func (s *NoopLifecycle) Stop() error { return nil }
func NewNoop() *Noop {
noop := new(Noop)
return noop
}
// Set of services all wrapping the base NoopLifecycle resulting in the same method
// signatures but different outer types.
type Noop struct{ NoopLifecycle }
// InstrumentedService is an implementation of Lifecycle for which all interface
// methods can be instrumented both return value as well as event hook wise.
type InstrumentedService struct {
start error
stop error
startHook func()
stopHook func()
}
func (s *InstrumentedService) Start() error {
if s.startHook != nil {
s.startHook()
}
return s.start
}
func (s *InstrumentedService) Stop() error {
if s.stopHook != nil {
s.stopHook()
}
return s.stop
}
type FullService struct{}
func NewFullService(stack *Node) (*FullService, error) {
fs := new(FullService)
stack.RegisterProtocols(fs.Protocols())
stack.RegisterAPIs(fs.APIs())
stack.RegisterLifecycle(fs)
return fs, nil
}
func (f *FullService) Start() error { return nil }
func (f *FullService) Stop() error { return nil }
func (f *FullService) Protocols() []p2p.Protocol {
return []p2p.Protocol{
{
Name: "test1",
Version: uint(1),
},
{
Name: "test2",
Version: uint(2),
},
}
}
func (f *FullService) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "admin",
},
{
Namespace: "debug",
},
{
Namespace: "net",
},
}
}