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)
+}