diff --git a/beacon/blsync/server.go b/beacon/blsync/server.go new file mode 100644 index 00000000000..cebb93aa7af --- /dev/null +++ b/beacon/blsync/server.go @@ -0,0 +1,25 @@ +// Copyright 2025 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 . + +package blsync + +import ( + "net/http" +) + +func (c *Client) NewAPIServer() func(mux *http.ServeMux, maxResponseSize int) { + return func(mux *http.ServeMux, maxResponseSize int) {} +} diff --git a/cmd/clef/main.go b/cmd/clef/main.go index dde4ae853ff..026cd23b2fa 100644 --- a/cmd/clef/main.go +++ b/cmd/clef/main.go @@ -737,7 +737,7 @@ func signer(c *cli.Context) error { srv := rpc.NewServer() srv.SetBatchLimits(node.DefaultConfig.BatchRequestLimit, node.DefaultConfig.BatchResponseMaxSize) - err := node.RegisterApis(rpcAPI, []string{"account"}, srv) + err := node.RegisterRpcAPIs(rpcAPI, []string{"account"}, srv) if err != nil { utils.Fatalf("Could not register API: %w", err) } diff --git a/cmd/geth/config.go b/cmd/geth/config.go index fcb315af979..a346734dc12 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -300,6 +300,9 @@ func makeFullNode(ctx *cli.Context) *node.Node { srv.RegisterName("engine", catalyst.NewConsensusAPI(eth)) blsyncer := blsync.NewClient(utils.MakeBeaconLightConfig(ctx)) blsyncer.SetEngineRPC(rpc.DialInProc(srv)) + if eth != nil { + eth.Blsync = blsyncer + } stack.RegisterLifecycle(blsyncer) } else { // Launch the engine API for interacting with external consensus client. diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index c9da08578c9..4dabc12818b 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -2017,7 +2017,7 @@ func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (*eth.EthAPIBac if err != nil { Fatalf("Failed to register the Ethereum service: %v", err) } - stack.RegisterAPIs(tracers.APIs(backend.APIBackend)) + stack.RegisterRpcAPIs(tracers.APIs(backend.APIBackend)) return backend.APIBackend, backend } @@ -2042,7 +2042,7 @@ func RegisterFilterAPI(stack *node.Node, backend ethapi.Backend, ethcfg *ethconf LogCacheSize: ethcfg.FilterLogCacheSize, LogQueryLimit: ethcfg.LogQueryLimit, }) - stack.RegisterAPIs([]rpc.API{{ + stack.RegisterRpcAPIs([]rpc.API{{ Namespace: "eth", Service: filters.NewFilterAPI(filterSystem), }}) diff --git a/eth/api_backend.go b/eth/api_backend.go index 3ae73e78af1..6b7adb26f3d 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/beacon/blsync" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" @@ -431,6 +432,10 @@ func (b *EthAPIBackend) BlobBaseFee(ctx context.Context) *big.Int { return nil } +func (b *EthAPIBackend) Blsync() *blsync.Client { + return b.eth.Blsync +} + func (b *EthAPIBackend) ChainDb() ethdb.Database { return b.eth.ChainDb() } diff --git a/eth/backend.go b/eth/backend.go index 85095618222..23a8ae439e9 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -28,6 +28,7 @@ import ( "time" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/beacon/blsync" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" @@ -50,6 +51,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/internal/restapi" "github.com/ethereum/go-ethereum/internal/shutdowncheck" "github.com/ethereum/go-ethereum/internal/version" "github.com/ethereum/go-ethereum/log" @@ -59,6 +61,7 @@ import ( "github.com/ethereum/go-ethereum/p2p/dnsdisc" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rest" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" gethversion "github.com/ethereum/go-ethereum/version" @@ -113,6 +116,8 @@ type Ethereum struct { APIBackend *EthAPIBackend + Blsync *blsync.Client + miner *miner.Miner gasPrice *big.Int @@ -346,7 +351,11 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData)) eth.miner.SetPrioAddresses(config.TxPool.Locals) - eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil} + eth.APIBackend = &EthAPIBackend{ + extRPCEnabled: stack.Config().ExtRPCEnabled(), + allowUnprotectedTxs: stack.Config().AllowUnprotectedTxs, + eth: eth, + } if eth.APIBackend.allowUnprotectedTxs { log.Info("Unprotected transactions allowed") } @@ -356,7 +365,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, networkID) // Register the backend on the node - stack.RegisterAPIs(eth.APIs()) + stack.RegisterRpcAPIs(eth.RpcAPIs()) + stack.RegisterRestAPIs(eth.RestAPIs()) stack.RegisterProtocols(eth.Protocols()) stack.RegisterLifecycle(eth) @@ -383,9 +393,9 @@ func makeExtraData(extra []byte) []byte { return extra } -// APIs return the collection of RPC services the ethereum package offers. +// RpcAPIs return the collection of RPC services the ethereum package offers. // NOTE, some of these services probably need to be moved to somewhere else. -func (s *Ethereum) APIs() []rpc.API { +func (s *Ethereum) RpcAPIs() []rpc.API { apis := ethapi.GetAPIs(s.APIBackend) // Append all the local APIs and return @@ -409,6 +419,11 @@ func (s *Ethereum) APIs() []rpc.API { }...) } +// RestAPIs return the collection of REST API services the ethereum package offers. +func (s *Ethereum) RestAPIs() []rest.API { + return restapi.GetAPIs(s.APIBackend) +} + func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) { s.blockchain.ResetWithGenesisBlock(gb) } diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 6dfe24f729b..6df6595b0e4 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -48,7 +48,7 @@ import ( // Register adds the engine API to the full node. func Register(stack *node.Node, backend *eth.Ethereum) error { log.Warn("Engine API enabled", "protocol", "eth") - stack.RegisterAPIs([]rpc.API{ + stack.RegisterRpcAPIs([]rpc.API{ { Namespace: "engine", Service: NewConsensusAPI(backend), diff --git a/eth/catalyst/simulated_beacon.go b/eth/catalyst/simulated_beacon.go index c10990c233c..3131e8a2183 100644 --- a/eth/catalyst/simulated_beacon.go +++ b/eth/catalyst/simulated_beacon.go @@ -359,7 +359,7 @@ func (c *SimulatedBeacon) AdjustTime(adjustment time.Duration) error { // stack. func RegisterSimulatedBeaconAPIs(stack *node.Node, sim *SimulatedBeacon) { api := newSimulatedBeaconAPI(sim) - stack.RegisterAPIs([]rpc.API{ + stack.RegisterRpcAPIs([]rpc.API{ { Namespace: "dev", Service: api, diff --git a/eth/syncer/syncer.go b/eth/syncer/syncer.go index 6b33ec54ba5..2be5fc89160 100644 --- a/eth/syncer/syncer.go +++ b/eth/syncer/syncer.go @@ -63,7 +63,7 @@ func Register(stack *node.Node, backend *eth.Ethereum, target common.Hash, exitW closed: make(chan struct{}), exitWhenSynced: exitWhenSynced, } - stack.RegisterAPIs(s.APIs()) + stack.RegisterRpcAPIs(s.APIs()) stack.RegisterLifecycle(s) return s, nil } diff --git a/ethclient/gethclient/gethclient_test.go b/ethclient/gethclient/gethclient_test.go index 0eed63cacfe..2c4f60fd003 100644 --- a/ethclient/gethclient/gethclient_test.go +++ b/ethclient/gethclient/gethclient_test.go @@ -66,10 +66,10 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block, []common.Hash) { if err != nil { t.Fatalf("can't create new ethereum service: %v", err) } - n.RegisterAPIs(tracers.APIs(ethservice.APIBackend)) + n.RegisterRpcAPIs(tracers.APIs(ethservice.APIBackend)) filterSystem := filters.NewFilterSystem(ethservice.APIBackend, filters.Config{}) - n.RegisterAPIs([]rpc.API{{ + n.RegisterRpcAPIs([]rpc.API{{ Namespace: "eth", Service: filters.NewFilterAPI(filterSystem), }}) diff --git a/ethclient/simulated/backend.go b/ethclient/simulated/backend.go index d573c7e7507..33cdd150e69 100644 --- a/ethclient/simulated/backend.go +++ b/ethclient/simulated/backend.go @@ -111,7 +111,7 @@ func newWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (*Backe } // Register the filter system filterSystem := filters.NewFilterSystem(backend.APIBackend, filters.Config{}) - stack.RegisterAPIs([]rpc.API{{ + stack.RegisterRpcAPIs([]rpc.API{{ Namespace: "eth", Service: filters.NewFilterAPI(filterSystem), }}) diff --git a/internal/restapi/backend.go b/internal/restapi/backend.go new file mode 100644 index 00000000000..b6905596f69 --- /dev/null +++ b/internal/restapi/backend.go @@ -0,0 +1,51 @@ +// Copyright 2025 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 . + +package restapi + +import ( + "context" + + "github.com/ethereum/go-ethereum/beacon/blsync" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rest" + "github.com/ethereum/go-ethereum/rpc" +) + +type Backend interface { + HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) + HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) + ChainConfig() *params.ChainConfig + Blsync() *blsync.Client +} + +func GetAPIs(apiBackend Backend) []rest.API { + apis := []rest.API{ + { + Namespace: "exec", + Register: NewExecutionRestAPI(apiBackend), + }, + } + if apiBackend.Blsync() != nil { + apis = append(apis, rest.API{ + Namespace: "beacon", + Register: apiBackend.Blsync().NewAPIServer(), + }) + } + return apis +} diff --git a/internal/restapi/exec_api.go b/internal/restapi/exec_api.go new file mode 100644 index 00000000000..13faa5af1b4 --- /dev/null +++ b/internal/restapi/exec_api.go @@ -0,0 +1,230 @@ +// Copyright 2025 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 . + +package restapi + +import ( + "context" + "encoding/json" + "errors" + "mime" + "net/http" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params/forks" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" +) + +var ( + urlHeaders = "/eth/v1/exec/headers/" + urlBlocks = "/eth/v1/exec/blocks" + urlBlockReceipts = "/eth/v1/exec/block_receipts" + urlTransaction = "/eth/v1/exec/transaction" + urlTxByIndex = "/eth/v1/exec/transaction_by_index" + urlReceiptByIndex = "/eth/v1/exec/receipt_by_index" + urlState = "/eth/v1/exec/state" + urlCall = "/eth/v1/exec/call" + // Requires EIP-7745 + urlHistory = "/eth/v1/exec/history" + urlTxPosition = "/eth/v1/exec/transaction_position" + urlLogs = "/eth/v1/exec/logs" +) + +type execApiServer struct { + apiBackend Backend + maxAmount, maxResponseSize int +} + +func NewExecutionRestAPI(apiBackend Backend) func(mux *http.ServeMux, maxResponseSize int) { + return func(mux *http.ServeMux, maxResponseSize int) { + s := &execApiServer{ + apiBackend: apiBackend, + maxResponseSize: maxResponseSize, + } + mux.HandleFunc(urlHeaders, s.handleHeaders) + mux.HandleFunc(urlBlocks, s.handleBlocks) + mux.HandleFunc(urlBlockReceipts, s.handleBlockReceipts) + mux.HandleFunc(urlTransaction, s.handleTransaction) + mux.HandleFunc(urlTxByIndex, s.handleTxByIndex) + mux.HandleFunc(urlReceiptByIndex, s.handleReceiptByIndex) + mux.HandleFunc(urlState, s.handleState) + mux.HandleFunc(urlCall, s.handleCall) + // Requires EIP-7745 + mux.HandleFunc(urlHistory, s.handleHistory) + mux.HandleFunc(urlTxPosition, s.handleTxPosition) + mux.HandleFunc(urlLogs, s.handleLogs) + } +} + +type blockId struct { + hash common.Hash + number uint64 +} + +func (b *blockId) isHash() bool { + return b.hash != (common.Hash{}) +} + +func decodeBlockId(id string) (blockId, bool) { + if hex, err := hexutil.Decode(id); err == nil { + if len(hex) != common.HashLength { + return blockId{}, false + } + var b blockId + copy(b.hash[:], hex) + return b, true + } + if number, err := strconv.ParseUint(id, 10, 64); err == nil { + return blockId{number: number}, true + } + return blockId{}, false +} + +// forkId returns the fork corresponding to the given header. +// Note that frontier thawing and difficulty bomb adjustments are ignored according +// to the API specification as they do not affect the interpretation of the +// returned data structures. +func (s *execApiServer) forkId(header *types.Header) forks.Fork { + c := s.apiBackend.ChainConfig() + switch { + case header.Difficulty.Sign() == 0: + return c.LatestFork(header.Time) + case c.IsLondon(header.Number): + return forks.London + case c.IsBerlin(header.Number): + return forks.Berlin + case c.IsIstanbul(header.Number): + return forks.Istanbul + case c.IsPetersburg(header.Number): + return forks.Petersburg + case c.IsConstantinople(header.Number): + return forks.Constantinople + case c.IsByzantium(header.Number): + return forks.Byzantium + case c.IsEIP155(header.Number): + return forks.SpuriousDragon + case c.IsEIP150(header.Number): + return forks.TangerineWhistle + case c.IsDAOFork(header.Number): + return forks.DAO + case c.IsHomestead(header.Number): + return forks.Homestead + default: + return forks.Frontier + } +} + +func (s *execApiServer) forkName(header *types.Header) string { + return strings.ToLower(s.forkId(header).String()) +} + +func (s *execApiServer) handleHeaders(resp http.ResponseWriter, req *http.Request) { + type headerResponse struct { + Version string `json:"version"` + Data *types.Header `json:"data"` + } + var ( + amount int + response []headerResponse + binary bool + err error + ) + + if mt, _, err := mime.ParseMediaType(req.Header.Get("accept")); err == nil { + switch mt { + case "application/json": + case "application/octet-stream": + binary = true + default: + http.Error(resp, "invalid accepted media type", http.StatusNotAcceptable) + } + } + id, ok := decodeBlockId(req.URL.Path[len(urlHeaders):]) + if !ok { + http.Error(resp, "invalid block id", http.StatusBadRequest) + return + } + if s := req.URL.Query().Get("amount"); s != "" { + amount, err = strconv.Atoi(s) + if err != nil || amount <= 0 { + http.Error(resp, "invalid amount", http.StatusBadRequest) + return + } + } else { + amount = 1 + } + + response = make([]headerResponse, amount) + for i := amount - 1; i >= 0; i-- { + if id.isHash() { + response[i].Data, err = s.apiBackend.HeaderByHash(req.Context(), id.hash) + } else { + response[i].Data, err = s.apiBackend.HeaderByNumber(req.Context(), rpc.BlockNumber(id.number)) + } + if errors.Is(err, context.Canceled) { + http.Error(resp, "request timeout", http.StatusRequestTimeout) + return + } + if response[i].Data == nil { + http.Error(resp, "not available", http.StatusNotFound) + return + } + response[i].Version = s.forkName(response[i].Data) + if response[i].Data.Number.Uint64() == 0 { + response = response[i:] + break + } + id = blockId{hash: response[i].Data.ParentHash} + } + + if binary { + respRlp, err := rlp.EncodeToBytes(response) + if err != nil { + http.Error(resp, "response encoding error", http.StatusInternalServerError) + return + } + resp.Header().Set("content-type", "application/octet-stream") + resp.Write(respRlp) + } else { + respJson, err := json.Marshal(response) + if err != nil { + http.Error(resp, "response encoding error", http.StatusInternalServerError) + return + } + resp.Header().Set("content-type", "application/json") + resp.Write(respJson) + } +} + +func (s *execApiServer) handleBlocks(resp http.ResponseWriter, req *http.Request) { panic("TODO") } +func (s *execApiServer) handleBlockReceipts(resp http.ResponseWriter, req *http.Request) { + panic("TODO") +} +func (s *execApiServer) handleTransaction(resp http.ResponseWriter, req *http.Request) { panic("TODO") } +func (s *execApiServer) handleTxByIndex(resp http.ResponseWriter, req *http.Request) { panic("TODO") } +func (s *execApiServer) handleReceiptByIndex(resp http.ResponseWriter, req *http.Request) { + panic("TODO") +} +func (s *execApiServer) handleState(resp http.ResponseWriter, req *http.Request) { panic("TODO") } +func (s *execApiServer) handleCall(resp http.ResponseWriter, req *http.Request) { panic("TODO") } +func (s *execApiServer) handleHistory(resp http.ResponseWriter, req *http.Request) { panic("TODO") } // Requires EIP-7745 +func (s *execApiServer) handleTxPosition(resp http.ResponseWriter, req *http.Request) { panic("TODO") } // Requires EIP-7745 +func (s *execApiServer) handleLogs(resp http.ResponseWriter, req *http.Request) { panic("TODO") } // Requires EIP-7745 diff --git a/node/api.go b/node/api.go index e5dda5ac4de..16991fa1592 100644 --- a/node/api.go +++ b/node/api.go @@ -178,7 +178,7 @@ func (api *adminAPI) StartHTTP(host *string, port *int, cors *string, apis *stri CorsAllowedOrigins: api.node.config.HTTPCors, Vhosts: api.node.config.HTTPVirtualHosts, Modules: api.node.config.HTTPModules, - rpcEndpointConfig: rpcEndpointConfig{ + apiEndpointConfig: apiEndpointConfig{ batchItemLimit: api.node.config.BatchRequestLimit, batchResponseSizeLimit: api.node.config.BatchResponseMaxSize, }, @@ -205,7 +205,7 @@ func (api *adminAPI) StartHTTP(host *string, port *int, cors *string, apis *stri 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 { + if err := api.node.http.enableHTTP(api.node.rpcAPIs, api.node.restAPIs, config); err != nil { return false, err } if err := api.node.http.start(); err != nil { @@ -256,7 +256,7 @@ func (api *adminAPI) StartWS(host *string, port *int, allowedOrigins *string, ap Modules: api.node.config.WSModules, Origins: api.node.config.WSOrigins, // ExposeAll: api.node.config.WSExposeAll, - rpcEndpointConfig: rpcEndpointConfig{ + apiEndpointConfig: apiEndpointConfig{ batchItemLimit: api.node.config.BatchRequestLimit, batchResponseSizeLimit: api.node.config.BatchResponseMaxSize, }, @@ -280,7 +280,7 @@ func (api *adminAPI) StartWS(host *string, port *int, allowedOrigins *string, ap return false, err } openApis, _ := api.node.getAPIs() - if err := server.enableWS(openApis, config); err != nil { + if err := server.enableWS(openApis, api.node.restAPIs, config); err != nil { return false, err } if err := server.start(); err != nil { diff --git a/node/rpcstack.go b/node/api_stack.go similarity index 79% rename from node/rpcstack.go rename to node/api_stack.go index 655f7db9e4f..56fafcee50e 100644 --- a/node/rpcstack.go +++ b/node/api_stack.go @@ -32,38 +32,40 @@ import ( "time" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rest" "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 + Modules []string + CorsAllowedOrigins []string + Vhosts []string + rpcPrefix, restPrefix string // path prefix on which to mount http handler + apiEndpointConfig } // 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 + Origins []string + Modules []string + rpcPrefix, restPrefix string // path prefix on which to mount ws handler + apiEndpointConfig } -type rpcEndpointConfig struct { +type apiEndpointConfig struct { jwtSecret []byte // optional JWT secret batchItemLimit int batchResponseSizeLimit int httpBodyLimit int } -type rpcHandler struct { - http.Handler - prefix string - server *rpc.Server +type apiHandler struct { + rpcHandler, restHandler http.Handler + rpcPrefix, restPrefix string + rpcServer *rpc.Server + restServer *rest.Server } type httpServer struct { @@ -78,11 +80,11 @@ type httpServer struct { // HTTP RPC handler things. httpConfig httpConfig - httpHandler atomic.Pointer[rpcHandler] + httpHandler atomic.Pointer[apiHandler] // WebSocket handler things. wsConfig wsConfig - wsHandler atomic.Pointer[rpcHandler] + wsHandler atomic.Pointer[apiHandler] // These are set by setListenAddr. endpoint string @@ -151,7 +153,7 @@ func (h *httpServer) start() error { 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.disableHTTP() h.disableWS() return err } @@ -160,19 +162,17 @@ func (h *httpServer) start() error { 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) + h.log.Info("WebSocket enabled", "RPC URL", url+h.wsConfig.rpcPrefix, "REST URL", url+h.wsConfig.restPrefix) } // if server is websocket only, return after logging - if !h.rpcAllowed() { + if !h.httpAllowed() { return nil } // Log http endpoint. h.log.Info("HTTP server started", "endpoint", listener.Addr(), "auth", h.httpConfig.jwtSecret != nil, - "prefix", h.httpConfig.prefix, + "RPC prefix", h.httpConfig.rpcPrefix, + "REST prefix", h.httpConfig.restPrefix, "cors", strings.Join(h.httpConfig.CorsAllowedOrigins, ","), "vhosts", strings.Join(h.httpConfig.Vhosts, ","), ) @@ -198,15 +198,19 @@ func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { // check if ws request and serve if ws enabled ws := h.wsHandler.Load() if ws != nil && isWebsocket(r) { - if checkPath(r, ws.prefix) { - ws.ServeHTTP(w, r) + if checkPath(r, ws.restPrefix) { + ws.restHandler.ServeHTTP(w, r) + return //TODO is this in the right place? + } + if checkPath(r, ws.rpcPrefix) { + ws.rpcHandler.ServeHTTP(w, r) + return //TODO is this in the right place? } - return } // if http-rpc is enabled, try to serve request - rpc := h.httpHandler.Load() - if rpc != nil { + api := h.httpHandler.Load() + if api != 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. @@ -217,8 +221,12 @@ func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if checkPath(r, rpc.prefix) { - rpc.ServeHTTP(w, r) + if checkPath(r, api.restPrefix) { + api.restHandler.ServeHTTP(w, r) + return + } + if checkPath(r, api.rpcPrefix) { + api.rpcHandler.ServeHTTP(w, r) return } } @@ -269,11 +277,13 @@ func (h *httpServer) doStop() { wsHandler := h.wsHandler.Load() if httpHandler != nil { h.httpHandler.Store(nil) - httpHandler.server.Stop() + httpHandler.rpcServer.Stop() + httpHandler.restServer.Stop() } if wsHandler != nil { h.wsHandler.Store(nil) - wsHandler.server.Stop() + wsHandler.rpcServer.Stop() + wsHandler.restServer.Stop() } ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) @@ -292,65 +302,76 @@ func (h *httpServer) doStop() { 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 { +// enableHTTP turns on JSON-RPC and REST API over HTTP on the server. +func (h *httpServer) enableHTTP(rpcApis []rpc.API, restApis []rest.API, config httpConfig) error { h.mu.Lock() defer h.mu.Unlock() - if h.rpcAllowed() { + if h.httpAllowed() { return errors.New("JSON-RPC over HTTP is already enabled") } - // Create RPC server and handler. - srv := rpc.NewServer() - srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) + // Create servers and handlers. + rpcServer := rpc.NewServer() + restServer := rest.NewServer() + rpcServer.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) if config.httpBodyLimit > 0 { - srv.SetHTTPBodyLimit(config.httpBodyLimit) + rpcServer.SetHTTPBodyLimit(config.httpBodyLimit) } - if err := RegisterApis(apis, config.Modules, srv); err != nil { + //TODO REST server limits + if err := RegisterAPIs(rpcApis, restApis, config.Modules, rpcServer, restServer); err != nil { return err } h.httpConfig = config - h.httpHandler.Store(&rpcHandler{ - Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret), - prefix: config.prefix, - server: srv, + h.httpHandler.Store(&apiHandler{ + rpcHandler: NewHTTPHandlerStack(rpcServer, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret), + restHandler: NewHTTPHandlerStack(restServer, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret), + rpcPrefix: config.rpcPrefix, + restPrefix: config.restPrefix, + rpcServer: rpcServer, + restServer: restServer, }) return nil } -// disableRPC stops the HTTP RPC handler. This is internal, the caller must hold h.mu. -func (h *httpServer) disableRPC() bool { +// disableHTTP stops the HTTP RPC and REST API handlers. This is internal, the caller must hold h.mu. +func (h *httpServer) disableHTTP() bool { handler := h.httpHandler.Load() if handler != nil { h.httpHandler.Store(nil) - handler.server.Stop() + handler.rpcServer.Stop() + handler.restServer.Stop() } return handler != nil } // enableWS turns on JSON-RPC over WebSocket on the server. -func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error { +func (h *httpServer) enableWS(rpcApis []rpc.API, restApis []rest.API, config wsConfig) error { h.mu.Lock() defer h.mu.Unlock() if h.wsAllowed() { return errors.New("JSON-RPC over WebSocket is already enabled") } - // Create RPC server and handler. - srv := rpc.NewServer() - srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) + // Create servers and handlers. + rpcServer := rpc.NewServer() + restServer := rest.NewServer() + rpcServer.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) if config.httpBodyLimit > 0 { - srv.SetHTTPBodyLimit(config.httpBodyLimit) + rpcServer.SetHTTPBodyLimit(config.httpBodyLimit) } - if err := RegisterApis(apis, config.Modules, srv); err != nil { + //TODO REST server limits + if err := RegisterAPIs(rpcApis, restApis, config.Modules, rpcServer, restServer); err != nil { return err } h.wsConfig = config - h.wsHandler.Store(&rpcHandler{ - Handler: NewWSHandlerStack(srv.WebsocketHandler(config.Origins), config.jwtSecret), - prefix: config.prefix, - server: srv, + h.wsHandler.Store(&apiHandler{ + rpcHandler: NewWSHandlerStack(rpcServer.WebsocketHandler(config.Origins), config.jwtSecret), + //TODO restHandler: NewWSHandlerStack(restServer.WebsocketHandler(config.Origins), config.jwtSecret), + rpcPrefix: config.rpcPrefix, + restPrefix: config.restPrefix, + rpcServer: rpcServer, + restServer: restServer, }) return nil } @@ -361,7 +382,7 @@ func (h *httpServer) stopWS() { defer h.mu.Unlock() if h.disableWS() { - if !h.rpcAllowed() { + if !h.httpAllowed() { h.doStop() } } @@ -372,13 +393,14 @@ func (h *httpServer) disableWS() bool { ws := h.wsHandler.Load() if ws != nil { h.wsHandler.Store(nil) - ws.server.Stop() + ws.rpcServer.Stop() + ws.restServer.Stop() } return ws != nil } // rpcAllowed returns true when JSON-RPC over HTTP is enabled. -func (h *httpServer) rpcAllowed() bool { +func (h *httpServer) httpAllowed() bool { return h.httpHandler.Load() != nil } @@ -629,10 +651,10 @@ func (is *ipcServer) stop() error { return err } -// RegisterApis checks the given modules' availability, generates an allowlist based on the allowed modules, +// RegisterRpcAPIs 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 { +func RegisterAPIs(rpcApis []rpc.API, restApis []rest.API, modules []string, rpcServer *rpc.Server, restServer *rest.Server) error { + if bad, available := checkModuleAvailability(modules, rpcApis, restApis); len(bad) > 0 { log.Error("Unavailable modules in HTTP API list", "unavailable", bad, "available", available) } // Generate the allow list based on the allowed modules @@ -641,12 +663,17 @@ func RegisterApis(apis []rpc.API, modules []string, srv *rpc.Server) error { allowList[module] = true } // Register all the APIs exposed by the services - for _, api := range apis { + for _, api := range rpcApis { if allowList[api.Namespace] || len(allowList) == 0 { - if err := srv.RegisterName(api.Namespace, api.Service); err != nil { + if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil { return err } } } + for _, api := range restApis { + if allowList[api.Namespace] || len(allowList) == 0 { + restServer.Register(api) + } + } return nil } diff --git a/node/rpcstack_test.go b/node/api_stack_test.go similarity index 98% rename from node/rpcstack_test.go rename to node/api_stack_test.go index 54e58cccb2b..865455506a7 100644 --- a/node/rpcstack_test.go +++ b/node/api_stack_test.go @@ -242,7 +242,7 @@ func createAndStartServer(t *testing.T, conf *httpConfig, ws bool, wsConf *wsCon timeouts = &rpc.DefaultHTTPTimeouts } srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), *timeouts) - assert.NoError(t, srv.enableRPC(apis(), *conf)) + assert.NoError(t, srv.enableHTTP(apis(), *conf)) if ws { assert.NoError(t, srv.enableWS(nil, *wsConf)) } @@ -339,9 +339,9 @@ func TestJWT(t *testing.T) { 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} + cfg := apiEndpointConfig{jwtSecret: []byte("secret")} + httpcfg := &httpConfig{apiEndpointConfig: cfg} + wscfg := &wsConfig{Origins: []string{"*"}, apiEndpointConfig: cfg} srv := createAndStartServer(t, httpcfg, true, wscfg, nil) wsUrl := fmt.Sprintf("ws://%v", srv.listenAddr()) htUrl := fmt.Sprintf("http://%v", srv.listenAddr()) diff --git a/node/endpoints.go b/node/endpoints.go index 14c12fd1f17..5aaec1df1ec 100644 --- a/node/endpoints.go +++ b/node/endpoints.go @@ -22,6 +22,7 @@ import ( "time" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rest" "github.com/ethereum/go-ethereum/rpc" ) @@ -52,9 +53,15 @@ func StartHTTPEndpoint(endpoint string, timeouts rpc.HTTPTimeouts, handler http. // 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) { +func checkModuleAvailability(modules []string, rpcApis []rpc.API, restApis []rest.API) (bad, available []string) { availableSet := make(map[string]struct{}) - for _, api := range apis { + for _, api := range rpcApis { + if _, ok := availableSet[api.Namespace]; !ok { + availableSet[api.Namespace] = struct{}{} + available = append(available, api.Namespace) + } + } + for _, api := range restApis { if _, ok := availableSet[api.Namespace]; !ok { availableSet[api.Namespace] = struct{}{} available = append(available, api.Namespace) diff --git a/node/node.go b/node/node.go index f9ebb243b03..8f1e9964cd3 100644 --- a/node/node.go +++ b/node/node.go @@ -38,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rest" "github.com/ethereum/go-ethereum/rpc" "github.com/gofrs/flock" ) @@ -58,7 +59,8 @@ type Node struct { 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 + rpcAPIs []rpc.API // List of RPC APIs currently provided by the node + restAPIs []rest.API // List of REST APIs currently provided by the node http *httpServer // ws *httpServer // httpAuth *httpServer // @@ -75,6 +77,8 @@ const ( closedState ) +var restApiPrefix = "/eth/" //TODO + // 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 @@ -390,7 +394,7 @@ func (n *Node) startRPC() error { openAPIs, allAPIs = n.getAPIs() ) - rpcConfig := rpcEndpointConfig{ + rpcConfig := apiEndpointConfig{ batchItemLimit: n.config.BatchRequestLimit, batchResponseSizeLimit: n.config.BatchResponseMaxSize, } @@ -399,12 +403,13 @@ func (n *Node) startRPC() error { if err := server.setListenAddr(n.config.HTTPHost, port); err != nil { return err } - if err := server.enableRPC(openAPIs, httpConfig{ + if err := server.enableHTTP(openAPIs, n.restAPIs, httpConfig{ CorsAllowedOrigins: n.config.HTTPCors, Vhosts: n.config.HTTPVirtualHosts, Modules: n.config.HTTPModules, - prefix: n.config.HTTPPathPrefix, - rpcEndpointConfig: rpcConfig, + rpcPrefix: n.config.HTTPPathPrefix, + restPrefix: restApiPrefix, + apiEndpointConfig: rpcConfig, }); err != nil { return err } @@ -417,11 +422,12 @@ func (n *Node) startRPC() error { if err := server.setListenAddr(n.config.WSHost, port); err != nil { return err } - if err := server.enableWS(openAPIs, wsConfig{ + if err := server.enableWS(openAPIs, n.restAPIs, wsConfig{ Modules: n.config.WSModules, Origins: n.config.WSOrigins, - prefix: n.config.WSPathPrefix, - rpcEndpointConfig: rpcConfig, + rpcPrefix: n.config.WSPathPrefix, + restPrefix: restApiPrefix, + apiEndpointConfig: rpcConfig, }); err != nil { return err } @@ -435,18 +441,19 @@ func (n *Node) startRPC() error { if err := server.setListenAddr(n.config.AuthAddr, port); err != nil { return err } - sharedConfig := rpcEndpointConfig{ + sharedConfig := apiEndpointConfig{ jwtSecret: secret, batchItemLimit: engineAPIBatchItemLimit, batchResponseSizeLimit: engineAPIBatchResponseSizeLimit, httpBodyLimit: engineAPIBodyLimit, } - err := server.enableRPC(allAPIs, httpConfig{ + err := server.enableHTTP(allAPIs, n.restAPIs, httpConfig{ CorsAllowedOrigins: DefaultAuthCors, Vhosts: n.config.AuthVirtualHosts, Modules: DefaultAuthModules, - prefix: DefaultAuthPrefix, - rpcEndpointConfig: sharedConfig, + rpcPrefix: DefaultAuthPrefix, + restPrefix: restApiPrefix, + apiEndpointConfig: sharedConfig, }) if err != nil { return err @@ -458,11 +465,12 @@ func (n *Node) startRPC() error { if err := server.setListenAddr(n.config.AuthAddr, port); err != nil { return err } - if err := server.enableWS(allAPIs, wsConfig{ + if err := server.enableWS(allAPIs, n.restAPIs, wsConfig{ Modules: DefaultAuthModules, Origins: DefaultAuthOrigins, - prefix: DefaultAuthPrefix, - rpcEndpointConfig: sharedConfig, + rpcPrefix: DefaultAuthPrefix, + restPrefix: restApiPrefix, + apiEndpointConfig: sharedConfig, }); err != nil { return err } @@ -568,8 +576,8 @@ func (n *Node) RegisterProtocols(protocols []p2p.Protocol) { 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) { +// RegisterRpcAPIs registers the RPC APIs a service provides on the node. +func (n *Node) RegisterRpcAPIs(apis []rpc.API) { n.lock.Lock() defer n.lock.Unlock() @@ -579,6 +587,17 @@ func (n *Node) RegisterAPIs(apis []rpc.API) { n.rpcAPIs = append(n.rpcAPIs, apis...) } +// RegisterRestAPIs registers the RPC APIs a service provides on the node. +func (n *Node) RegisterRestAPIs(apis []rest.API) { + n.lock.Lock() + defer n.lock.Unlock() + + if n.state != initializingState { + panic("can't register APIs on running/stopped node") + } + n.restAPIs = append(n.restAPIs, 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) { @@ -672,9 +691,9 @@ func (n *Node) HTTPEndpoint() string { // 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.http.listenAddr() + n.http.wsConfig.rpcPrefix //TODO ??? } - return "ws://" + n.ws.listenAddr() + n.ws.wsConfig.prefix + return "ws://" + n.ws.listenAddr() + n.ws.wsConfig.rpcPrefix } // HTTPAuthEndpoint returns the URL of the authenticated HTTP server. @@ -685,9 +704,9 @@ func (n *Node) HTTPAuthEndpoint() string { // 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.httpAuth.listenAddr() + n.httpAuth.wsConfig.rpcPrefix } - return "ws://" + n.wsAuth.listenAddr() + n.wsAuth.wsConfig.prefix + return "ws://" + n.wsAuth.listenAddr() + n.wsAuth.wsConfig.rpcPrefix } // EventMux retrieves the event multiplexer used by all the network services in diff --git a/node/node_auth_test.go b/node/node_auth_test.go index 900f53440cb..c41b087ba67 100644 --- a/node/node_auth_test.go +++ b/node/node_auth_test.go @@ -120,7 +120,7 @@ func TestAuthEndpoints(t *testing.T) { 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{ + node.RegisterRpcAPIs([]rpc.API{ { Namespace: "engine", Version: "1.0", diff --git a/node/utils_test.go b/node/utils_test.go index 681f3a8b285..5f7e5b044bf 100644 --- a/node/utils_test.go +++ b/node/utils_test.go @@ -69,7 +69,7 @@ func NewFullService(stack *Node) (*FullService, error) { fs := new(FullService) stack.RegisterProtocols(fs.Protocols()) - stack.RegisterAPIs(fs.APIs()) + stack.RegisterRpcAPIs(fs.APIs()) stack.RegisterLifecycle(fs) return fs, nil } diff --git a/rest/server.go b/rest/server.go new file mode 100644 index 00000000000..ddaf192cd3b --- /dev/null +++ b/rest/server.go @@ -0,0 +1,78 @@ +// Copyright 2025 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 . + +package rest + +import ( + "fmt" + "net/http" +) + +const ( + defaultRequestLimit = 5 * 1024 * 1024 + defaultResponseLimit = 5 * 1024 * 1024 +) + +// Server is a REST API server. +type Server struct { + itemLimit, requestLimit, responseLimit int + mux http.ServeMux +} + +// NewServer creates a new server instance with no registered handlers. +func NewServer() *Server { + return &Server{ + requestLimit: defaultRequestLimit, + responseLimit: defaultResponseLimit, + } +} + +func (s *Server) Stop() {} //TODO is this required? + +func (s *Server) Register(api API) { + api.Register(&s.mux, s.responseLimit) +} + +// SetBatchLimits sets limits applied to batch requests. There are two limits: 'itemLimit' +// is the maximum number of items in a batch. 'maxResponseSize' is the maximum number of +// response bytes across all requests in a batch. +// +// This method should be called before processing any requests via ServeCodec, ServeHTTP, +// ServeListener etc. +/*func (s *Server) SetBatchLimits(itemLimit, maxResponseSize int) { + s.batchItemLimit = itemLimit + s.batchResponseLimit = maxResponseSize +}*/ + +// SetHTTPBodyLimit sets the size limit for HTTP requests. +// +// This method should be called before processing any requests via ServeHTTP. +/*func (s *Server) SetHTTPBodyLimit(limit int) { + s.httpBodyLimit = limit +}*/ + +// ServeHTTP serves REST API requests over HTTP. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.ContentLength < 0 { + http.Error(w, "request size unknown", http.StatusRequestEntityTooLarge) + return + } + if reqLen := int64(len(r.URL.RawQuery)) + r.ContentLength; reqLen > int64(s.requestLimit) { + http.Error(w, fmt.Sprintf("request too large (%d>%d)", reqLen, s.requestLimit), http.StatusRequestEntityTooLarge) + return + } + s.mux.ServeHTTP(w, r) +} diff --git a/rest/types.go b/rest/types.go new file mode 100644 index 00000000000..15e707046d1 --- /dev/null +++ b/rest/types.go @@ -0,0 +1,27 @@ +// Copyright 2025 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 . + +package rest + +import ( + "net/http" +) + +// API describes the set of methods offered over the REST API interface +type API struct { + Namespace string // namespace under which the REST API methods of Service are exposed: /eth/v*/ + Register func(mux *http.ServeMux, maxResponseSize int) +}