Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:

- name: Make Test
id: unit-test
run: make test
run: make clean test

test_coverage:
runs-on: ubuntu-latest
Expand Down
12 changes: 10 additions & 2 deletions api/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"net/http"
"runtime/debug"
"strings"
"time"

"github.com/vechain/thor/v2/api/doc"
Expand Down Expand Up @@ -60,10 +61,17 @@ func HandleAPITimeout(timeout time.Duration) func(http.Handler) http.Handler {
}
}

// middleware to limit request body size.
func HandleRequestBodyLimit(maxBodySize int64) func(next http.Handler) http.Handler {
// HandleRequestBodyLimit limits request body size.
// Paths whose prefix matches any entry in exceptions bypass the limit.
func HandleRequestBodyLimit(maxBodySize int64, exceptions ...string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, exc := range exceptions {
if strings.HasPrefix(r.URL.Path, exc) {
next.ServeHTTP(w, r)
return
}
}
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
next.ServeHTTP(w, r)
})
Expand Down
29 changes: 29 additions & 0 deletions api/middleware/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,35 @@ func TestHandleRequestBodyLimitExceeded(t *testing.T) {
assert.Contains(t, rr.Body.String(), "http: request body too large")
}

func TestHandleRequestBodyLimitException(t *testing.T) {
largeBody := strings.Repeat("x", 200)

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusRequestEntityTooLarge)
return
}
w.WriteHeader(http.StatusOK)
})

mw := HandleRequestBodyLimit(10, "/rpc")

t.Run("exception_path_bypasses_limit", func(t *testing.T) {
req := httptest.NewRequest("POST", "/rpc", strings.NewReader(largeBody))
rr := httptest.NewRecorder()
mw(handler).ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})

t.Run("non_exception_path_is_limited", func(t *testing.T) {
req := httptest.NewRequest("POST", "/accounts", strings.NewReader(largeBody))
rr := httptest.NewRecorder()
mw(handler).ServeHTTP(rr, req)
assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code)
})
}

func TestBodyLimitWithRequestLogger(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := io.ReadAll(r.Body)
Expand Down
2 changes: 1 addition & 1 deletion api/middleware/request_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func TestRequestLoggerHandler(t *testing.T) {
w.Write([]byte("OK"))
},
enabled: false,
slowQueriesThreshold: 20 * time.Millisecond,
slowQueriesThreshold: time.Second,
log5xxErrors: false,
expectedStatusCode: http.StatusOK,
shouldLog: false,
Expand Down
26 changes: 24 additions & 2 deletions cmd/thor/httpserver/api_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,18 @@ import (
"github.com/vechain/thor/v2/chain"
"github.com/vechain/thor/v2/log"
"github.com/vechain/thor/v2/logdb"
"github.com/vechain/thor/v2/rpc"
"github.com/vechain/thor/v2/state"
"github.com/vechain/thor/v2/thor"
"github.com/vechain/thor/v2/txpool"

rpcaccounts "github.com/vechain/thor/v2/rpc/accounts"
rpcblocks "github.com/vechain/thor/v2/rpc/blocks"
rpcchain "github.com/vechain/thor/v2/rpc/chain"
rpcfees "github.com/vechain/thor/v2/rpc/fees"
rpclogs "github.com/vechain/thor/v2/rpc/logs"
rpcsimulation "github.com/vechain/thor/v2/rpc/simulation"
rpctransactions "github.com/vechain/thor/v2/rpc/transactions"
)

var logger = log.WithContext("pkg", "api")
Expand All @@ -52,6 +61,7 @@ type APIConfig struct {
BacktraceLimit uint32
CallGasLimit uint64
BatchDataMaxSize uint64
ClientVersion string
PprofOn bool
SkipLogs bool
AllowCustomTracer bool
Expand Down Expand Up @@ -131,6 +141,18 @@ func StartAPIServer(
subs := subscriptions.New(repo, origins, config.BacktraceLimit, txPool, config.EnableDeprecated)
subs.Mount(router, "/subscriptions")

// Ethereum JSON-RPC at /rpc — body limit enforced internally by rpc.Server (2 MB via MaxBytesReader)
chainID := repo.ChainID()
rpcSrv := rpc.NewServer()
rpcchain.New(repo, chainID, config.ClientVersion).Mount(rpcSrv)
rpcblocks.New(repo, chainID).Mount(rpcSrv)
rpctransactions.New(repo, chainID, txPool).Mount(rpcSrv)
rpcaccounts.New(repo, stater).Mount(rpcSrv)
rpclogs.New(repo, logDB, config.BacktraceLimit, config.LogsLimit).Mount(rpcSrv)
rpcfees.New(repo, config.BacktraceLimit).Mount(rpcSrv)
rpcsimulation.New(repo, stater, forkConfig, config.CallGasLimit).Mount(rpcSrv)
router.PathPrefix("/rpc").Handler(rpcSrv)

if config.PprofOn {
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
Expand All @@ -140,8 +162,8 @@ func StartAPIServer(
}

// middlewares
// body limit and timeout
router.Use(middleware.HandleRequestBodyLimit(defaultRequestBodyLimit))
// /rpc owns its body limit inside rpc.Server; skip the REST 200 KB cap for that path.
router.Use(middleware.HandleRequestBodyLimit(defaultRequestBodyLimit, "/rpc"))
if config.Timeout > 0 {
router.Use(middleware.HandleAPITimeout(time.Duration(config.Timeout) * time.Millisecond))
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/thor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,14 +322,14 @@ func defaultAction(_ context.Context, ctx *cli.Command) error {
bftEngine,
p2pCommunicator.Communicator(),
forkConfig,
makeAPIConfig(ctx, logAPIRequests, false),
makeAPIConfig(ctx, logAPIRequests, false, version),
)
if err != nil {
return err
}
defer func() { log.Info("stopping API server..."); srvCloser() }()

printStartupMessage2(gene, apiURL, p2pCommunicator.Enode(), metricsURL, adminURL, false)
printStartupMessage2(apiURL, apiURL+"rpc", p2pCommunicator.Enode(), metricsURL, adminURL, false)

if err := p2pCommunicator.Start(); err != nil {
return err
Expand Down Expand Up @@ -518,14 +518,14 @@ func soloAction(_ context.Context, ctx *cli.Command) error {
bftEngine,
&solo.Communicator{},
forkConfig,
makeAPIConfig(ctx, logAPIRequests, true),
makeAPIConfig(ctx, logAPIRequests, true, version),
)
if err != nil {
return err
}
defer func() { log.Info("stopping API server..."); srvCloser() }()

printStartupMessage2(gene, apiURL, "", metricsURL, adminURL, isDevnet)
printStartupMessage2(apiURL, apiURL+"rpc", "", metricsURL, adminURL, isDevnet)

if !ctx.Bool(disablePrunerFlag.Name) {
pruner := pruner.New(mainDB, repo, bftEngine, *forkConfig)
Expand Down
9 changes: 6 additions & 3 deletions cmd/thor/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,13 @@ func parseGenesisFile(uri string) (*genesis.Genesis, *thor.ForkConfig, error) {
return customGen, &forkConfig, nil
}

func makeAPIConfig(ctx *cli.Command, logAPIRequests *atomic.Bool, soloMode bool) httpserver.APIConfig {
func makeAPIConfig(ctx *cli.Command, logAPIRequests *atomic.Bool, soloMode bool, clientVersion string) httpserver.APIConfig {
return httpserver.APIConfig{
AllowedOrigins: ctx.String(apiCorsFlag.Name),
BacktraceLimit: uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)),
CallGasLimit: ctx.Uint64(apiCallGasLimitFlag.Name),
BatchDataMaxSize: ctx.Uint64(apiBatchDataMaxSizeFlag.Name),
ClientVersion: clientVersion,
PprofOn: ctx.Bool(pprofFlag.Name),
SkipLogs: ctx.Bool(skipLogsFlag.Name),
APIBacktraceLimit: int(ctx.Uint64(apiBacktraceLimitFlag.Name)),
Expand Down Expand Up @@ -593,14 +594,15 @@ func printStartupMessage1(
}

func printStartupMessage2(
gene *genesis.Genesis,
apiURL string,
ethRPCURL string,
nodeID string,
metricsURL string,
adminURL string,
isDevnet bool,
) {
message := fmt.Sprintf(`%v API portal [ %v ]%v%v%v`,
message := fmt.Sprintf(`%v API portal [ %v ]
Ethereum RPC [ %v ]%v%v%v`,
func() string { // node ID
if nodeID == "" {
return ""
Expand All @@ -611,6 +613,7 @@ func printStartupMessage2(
}
}(),
apiURL,
ethRPCURL,
func() string { // metrics URL
if metricsURL == "" {
return ""
Expand Down
15 changes: 10 additions & 5 deletions cmd/thor/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,23 @@ func TestPrintStartupMessage1(t *testing.T) {
}

func TestPrintStartupMessage2(t *testing.T) {
gene, _ := genesis.NewDevnet()

t.Run("all fields", func(t *testing.T) {
printStartupMessage2(gene, "http://localhost:8669", "enode://abc@127.0.0.1:11235", "http://localhost:2112", "http://localhost:2113", false)
printStartupMessage2(
"http://localhost:8669/",
"http://localhost:8669/rpc",
"enode://abc@127.0.0.1:11235",
"http://localhost:2112",
"http://localhost:2113",
false,
)
})

t.Run("minimal fields", func(t *testing.T) {
printStartupMessage2(gene, "http://localhost:8669", "", "", "", false)
printStartupMessage2("http://localhost:8669/", "http://localhost:8669/rpc", "", "", "", false)
})

t.Run("devnet", func(t *testing.T) {
printStartupMessage2(gene, "http://localhost:8669", "", "", "", true)
printStartupMessage2("http://localhost:8669/", "http://localhost:8669/rpc", "", "", "", true)
})
}

Expand Down
13 changes: 13 additions & 0 deletions consensus/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,19 @@ func (c *Consensus) validateBlockBody(blk *block.Block) error {
return consensusError(fmt.Sprintf("tx Ethereum chain ID %v does not match network chain ID %d",
cid, c.repo.ChainID()))
}
// Field-range validation (maxFeePerGas > 0, maxPriority ≤ maxFee, etc.) is
// intentionally absent here, consistent with TypeDynamicFee. Both types rely
// on execution-time checks in ResolveTransaction and BuyGas — a block whose
// tx has invalid fields will fail verifyBlock when BuyGas returns an error.
// validateBlockBody is a fast-path structural check; semantic validation is
// deferred to execution.
//
// TODO: non-empty access lists are rejected by the pool (ParseEthTransaction /
// validateEth1559Fields) but are not checked here or in eth1559TxData.decode().
// A block built outside the pool can carry ETH txs with access lists — the
// entries are stored but not used for EIP-2929 warm/cold gas accounting (not
// yet implemented). Add an explicit consensus-level rejection once access list
// support is complete, or reject them here in the interim.
}

if err := tr.TestFeatures(header.TxsFeatures()); err != nil {
Expand Down
8 changes: 8 additions & 0 deletions packer/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ func (f *Flow) hasTx(txid thor.Bytes32, txBlockRef uint32) (bool, error) {
}

func (f *Flow) txFitsBlockSize(t *tx.Transaction) bool {
// TODO: f.blockSize is the sum of individual tx sizes; blk.Size() in consensus
// measures the full RLP-encoded block (header + tx list framing). The 2 KB buffer
// covers expected header overhead, but the two accounting methods can diverge under
// RLP framing edge cases. Consider a property test that feeds blk.Size() back from
// Pack() to verify the packer never produces a block that fails the consensus check.
return f.blockSize+uint64(t.Size()) < thor.MaxRLPBlockSize-blockSizeBufferZone
}

Expand Down Expand Up @@ -174,6 +179,9 @@ func (f *Flow) Adopt(t *tx.Transaction) error {
return errGasLimitReached
}

// Block size enforcement is introduced with INTERSTELLAR; pre-INTERSTELLAR blocks
// have no packer-side size cap — only the gas limit bounds block size indirectly.
// This preserves pre-existing behaviour for all blocks before the fork.
if thor.IsForked(f.Number(), f.packer.forkConfig.INTERSTELLAR) && !f.txFitsBlockSize(t) {
return errBlockSizeLimitReached
}
Expand Down
Loading
Loading