From 68f5e5805a0c5e2539de882b445092352e0e5cde Mon Sep 17 00:00:00 2001 From: marcello33 Date: Thu, 23 Apr 2026 15:31:41 +0200 Subject: [PATCH 1/7] implement full grpc comms with heimdall --- cmd/keeper/go.mod | 2 +- cmd/keeper/go.sum | 1 + consensus/bor/bor.go | 14 +- consensus/bor/bor_test.go | 14 ++ eth/tracers/data.csv | 20 +- go.mod | 2 +- go.sum | 4 +- internal/cli/server/api_service.go | 162 ++++++++++++++- internal/cli/server/api_service_test.go | 258 ++++++++++++++++++++++++ internal/cli/server/config.go | 2 + internal/cli/server/flags.go | 6 + internal/cli/server/grpc_auth_test.go | 187 +++++++++++++++++ internal/cli/server/server.go | 74 ++++++- 13 files changed, 713 insertions(+), 33 deletions(-) create mode 100644 internal/cli/server/api_service_test.go create mode 100644 internal/cli/server/grpc_auth_test.go diff --git a/cmd/keeper/go.mod b/cmd/keeper/go.mod index db168eb7c1..e15b006dbe 100644 --- a/cmd/keeper/go.mod +++ b/cmd/keeper/go.mod @@ -50,7 +50,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/time v0.12.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/cmd/keeper/go.sum b/cmd/keeper/go.sum index e67813369a..9d3f2f1a22 100644 --- a/cmd/keeper/go.sum +++ b/cmd/keeper/go.sum @@ -168,6 +168,7 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 2b0ca4fcb7..b126786fb0 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -278,6 +278,10 @@ type Bor struct { // ctx is cancelled when Close() is called, allowing in-flight operations to abort promptly. ctx context.Context ctxCancel context.CancelFunc + + // api is the bor engine API instance reused across all callers (JSON-RPC and gRPC). + api *API + apiOnce sync.Once } type signer struct { @@ -1520,11 +1524,19 @@ func (c *Bor) SealHash(header *types.Header) common.Hash { // APIs implements consensus.Engine, returning the user facing RPC API to allow // controlling the signer voting. +// +// The returned *API is cached on the first call so that per-API state (e.g., +// rootHashCache) persists across calls. JSON-RPC only invokes APIs() once at +// node startup, but the gRPC backend fetches it on every handler call — without +// the cache those calls would each start from an empty state. func (c *Bor) APIs(chain consensus.ChainHeaderReader) []rpc.API { + c.apiOnce.Do(func() { + c.api = &API{chain: chain, bor: c} + }) return []rpc.API{{ Namespace: "bor", Version: "1.0", - Service: &API{chain: chain, bor: c}, + Service: c.api, Public: false, }} } diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index 4db081af2c..d32a319817 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -2039,6 +2039,20 @@ func TestAPIs_ReturnsBorNamespace(t *testing.T) { require.Equal(t, "1.0", apis[0].Version) } +// TestAPIs_ReturnsSameInstanceAcrossCalls verifies that repeated calls to APIs() must return the same *API +// so per-API state such as rootHashCache persists across calls. This matters for the gRPC backend +// which fetches APIs() on every handler invocation; returning a fresh *API each call defeats caching. +func TestAPIs_ReturnsSameInstanceAcrossCalls(t *testing.T) { + t.Parallel() + sp := &fakeSpanner{vals: []*valset.Validator{{Address: common.HexToAddress("0x1"), VotingPower: 1}}} + borCfg := defaultBorConfig() + chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, uint64(time.Now().Unix())) + + first := b.APIs(chain.HeaderChain()) + second := b.APIs(chain.HeaderChain()) + require.Same(t, first[0].Service, second[0].Service, "APIs must return the cached *API on repeated calls") +} + func TestClose_Idempotent(t *testing.T) { t.Parallel() sp := &fakeSpanner{vals: []*valset.Validator{{Address: common.HexToAddress("0x1"), VotingPower: 1}}} diff --git a/eth/tracers/data.csv b/eth/tracers/data.csv index 008be17eda..7d9606fa4d 100644 --- a/eth/tracers/data.csv +++ b/eth/tracers/data.csv @@ -1,40 +1,40 @@ TransactionIndex, Incarnation, VersionTxIdx, VersionInc, Path, Operation -0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read -0 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write +0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 0 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 0 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 0 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write -1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read +0 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read -1 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write -1 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write +1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 1 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 1 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write -2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read -2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read +1 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write +1 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read +2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read +2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read +2 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 2 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 2 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 2 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write -2 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write +3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read -3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 3 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 3 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 3 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 3 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write -4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read +4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 4 , 0, 4 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 4 , 0, 4 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 4 , 0, 4 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write diff --git a/go.mod b/go.mod index 2983eeb461..4d4bc94d99 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.26.2 require ( github.com/0xPolygon/crand v1.0.3 github.com/0xPolygon/heimdall-v2 v0.6.0 - github.com/0xPolygon/polyproto v0.0.7 + github.com/0xPolygon/polyproto v0.0.8-0.20260423132317-7d955b45ef8a github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 github.com/BurntSushi/toml v1.4.0 github.com/JekaMas/go-grpc-net-conn v0.0.0-20220708155319-6aff21f2d13d diff --git a/go.sum b/go.sum index 802074e40b..92e591e1af 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ github.com/0xPolygon/crand v1.0.3 h1:BYYflmgLhmGPEgqtopG4muq6wV6DOkwD8uPymNz5WeQ github.com/0xPolygon/crand v1.0.3/go.mod h1:km4366oC7EVFl1xNUCwzxUXNM10swZqd8LZ0E5SgbAE= github.com/0xPolygon/heimdall-v2 v0.6.0 h1:rA8RISMnns1w08PxTLvDBS5WiaTOFHJGSrhDWDJLtHc= github.com/0xPolygon/heimdall-v2 v0.6.0/go.mod h1:fVkGiODG6cGLaDyrE3qxIrvz1rbUr4Zdrr3dOm2SPgg= -github.com/0xPolygon/polyproto v0.0.7 h1:Ody+kFyCRK4QXRPXbsP5pdxKrDgwAAXtFB8NPgaIxRs= -github.com/0xPolygon/polyproto v0.0.7/go.mod h1:2Iw93k2LismvckKKeXQITuhJH9vLbqOa212AMskH6no= +github.com/0xPolygon/polyproto v0.0.8-0.20260423132317-7d955b45ef8a h1:vVtSjO29FcFBZbNVsVGy6z3lv3RRr/sI1vPR7IzbZZE= +github.com/0xPolygon/polyproto v0.0.8-0.20260423132317-7d955b45ef8a/go.mod h1:2Iw93k2LismvckKKeXQITuhJH9vLbqOa212AMskH6no= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= diff --git a/internal/cli/server/api_service.go b/internal/cli/server/api_service.go index 9e37ff1248..a6dd57c3d3 100644 --- a/internal/cli/server/api_service.go +++ b/internal/cli/server/api_service.go @@ -5,6 +5,7 @@ import ( "errors" "math" + "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/rpc" @@ -13,6 +14,10 @@ import ( protoutil "github.com/0xPolygon/polyproto/utils" ) +// maxBlockInfoBatchSize caps the per-call range to prevent abuse of the batch endpoint. +// Must be >= heimdall's MaxMilestonePropositionLength. +const maxBlockInfoBatchSize = 256 + func (s *Server) GetRootHash(ctx context.Context, req *protobor.GetRootHashRequest) (*protobor.GetRootHashResponse, error) { rootHash, err := s.backend.APIBackend.GetRootHash(ctx, req.StartBlockNumber, req.EndBlockNumber) if err != nil { @@ -31,12 +36,43 @@ func (s *Server) GetVoteOnHash(ctx context.Context, req *protobor.GetVoteOnHashR return &protobor.GetVoteOnHashResponse{Response: vote}, nil } -func headerToProtoborHeader(h *types.Header) *protobor.Header { - return &protobor.Header{ - Number: h.Number.Uint64(), - ParentHash: protoutil.ConvertHashToH256(h.ParentHash), - Time: h.Time, +func headerToProtoBorHeader(h *types.Header) *protobor.Header { + out := &protobor.Header{ + Number: h.Number.Uint64(), + ParentHash: protoutil.ConvertHashToH256(h.ParentHash), + Time: h.Time, + UncleHash: protoutil.ConvertHashToH256(h.UncleHash), + Coinbase: protoutil.ConvertAddressToH160(h.Coinbase), + StateRoot: protoutil.ConvertHashToH256(h.Root), + TxRoot: protoutil.ConvertHashToH256(h.TxHash), + ReceiptRoot: protoutil.ConvertHashToH256(h.ReceiptHash), + Bloom: h.Bloom.Bytes(), + GasLimit: h.GasLimit, + GasUsed: h.GasUsed, + ExtraData: append([]byte(nil), h.Extra...), + MixDigest: protoutil.ConvertHashToH256(h.MixDigest), + Nonce: h.Nonce[:], + } + if h.Difficulty != nil { + out.Difficulty = h.Difficulty.Bytes() + } + if h.BaseFee != nil { + out.BaseFee = h.BaseFee.Bytes() + } + if h.WithdrawalsHash != nil { + out.WithdrawalsHash = protoutil.ConvertHashToH256(*h.WithdrawalsHash) } + // BlobGasUsed and ExcessBlobGas are proto3 optional + // using *uint64 as a direct pointer, so the copy preserves nil vs.zero. + out.BlobGasUsed = h.BlobGasUsed + out.ExcessBlobGas = h.ExcessBlobGas + if h.ParentBeaconRoot != nil { + out.ParentBeaconBlockRoot = protoutil.ConvertHashToH256(*h.ParentBeaconRoot) + } + if h.RequestsHash != nil { + out.RequestsHash = protoutil.ConvertHashToH256(*h.RequestsHash) + } + return out } func (s *Server) HeaderByNumber(ctx context.Context, req *protobor.GetHeaderByNumberRequest) (*protobor.GetHeaderByNumberResponse, error) { @@ -53,7 +89,7 @@ func (s *Server) HeaderByNumber(ctx context.Context, req *protobor.GetHeaderByNu return nil, errors.New("header not found") } - return &protobor.GetHeaderByNumberResponse{Header: headerToProtoborHeader(header)}, nil + return &protobor.GetHeaderByNumberResponse{Header: headerToProtoBorHeader(header)}, nil } func (s *Server) BlockByNumber(ctx context.Context, req *protobor.GetBlockByNumberRequest) (*protobor.GetBlockByNumberResponse, error) { @@ -75,7 +111,7 @@ func (s *Server) BlockByNumber(ctx context.Context, req *protobor.GetBlockByNumb func blockToProtoBlock(h *types.Block) *protobor.Block { return &protobor.Block{ - Header: headerToProtoborHeader(h.Header()), + Header: headerToProtoBorHeader(h.Header()), } } @@ -107,6 +143,118 @@ func (s *Server) BorBlockReceipt(ctx context.Context, req *protobor.ReceiptReque return &protobor.ReceiptResponse{Receipt: ConvertReceiptToProtoReceipt(receipt)}, nil } +func (s *Server) GetAuthor(ctx context.Context, req *protobor.GetAuthorRequest) (*protobor.GetAuthorResponse, error) { + bN, err := getRpcBlockNumberFromString(req.Number) + if err != nil { + return nil, err + } + + header, err := s.backend.APIBackend.HeaderByNumber(ctx, bN) + if err != nil { + return nil, err + } + if header == nil { + return nil, errors.New("header not found") + } + + author, err := s.backend.Engine().Author(header) + if err != nil { + return nil, err + } + + return &protobor.GetAuthorResponse{Author: protoutil.ConvertAddressToH160(author)}, nil +} + +func (s *Server) GetTdByHash(ctx context.Context, req *protobor.GetTdByHashRequest) (*protobor.GetTdResponse, error) { + hashBytes := protoutil.ConvertH256ToHash(req.Hash) + hash := common.BytesToHash(hashBytes[:]) + + td := s.backend.APIBackend.GetTd(ctx, hash) + if td == nil { + return nil, errors.New("total difficulty not found") + } + if !td.IsUint64() { + return nil, errors.New("total difficulty overflows uint64") + } + return &protobor.GetTdResponse{TotalDifficulty: td.Uint64()}, nil +} + +func (s *Server) GetTdByNumber(ctx context.Context, req *protobor.GetTdByNumberRequest) (*protobor.GetTdResponse, error) { + bN, err := getRpcBlockNumberFromString(req.Number) + if err != nil { + return nil, err + } + td := s.backend.APIBackend.GetTdByNumber(ctx, bN) + if td == nil { + return nil, errors.New("total difficulty not found") + } + if !td.IsUint64() { + return nil, errors.New("total difficulty overflows uint64") + } + return &protobor.GetTdResponse{TotalDifficulty: td.Uint64()}, nil +} + +func (s *Server) GetBlockInfoInBatch(ctx context.Context, req *protobor.GetBlockInfoInBatchRequest) (*protobor.GetBlockInfoInBatchResponse, error) { + if req.EndBlockNumber < req.StartBlockNumber { + return nil, errors.New("invalid range: end < start") + } + if req.EndBlockNumber-req.StartBlockNumber >= uint64(maxBlockInfoBatchSize) { + return nil, errors.New("invalid range: exceeds max batch size") + } + + size := int(req.EndBlockNumber-req.StartBlockNumber) + 1 + out := &protobor.GetBlockInfoInBatchResponse{ + Blocks: make([]*protobor.BlockInfo, 0, size), + } + + // the i++ -> i-- requires an integration test with a multi-block batch + // mutator-disable-next-line loop-step + for i := req.StartBlockNumber; i <= req.EndBlockNumber; i++ { + info, ok := s.fetchBlockInfo(ctx, i) + // this requires APIBackend mock returning a missing block mid-range + // mutator-disable-next-line gap-stop semantics + if !ok { + // Match HTTP batch semantics: stop at the first gap, return what we have. + break + } + out.Blocks = append(out.Blocks, info) + } + + return out, nil +} + +// fetchBlockInfo loads header, total difficulty, and author for blockNum. +// Returns (nil, false) if any piece is missing — the caller should stop the loop. +// Author is left zero-valued for genesis (matching bor_getAuthor behavior). +func (s *Server) fetchBlockInfo(ctx context.Context, blockNum uint64) (*protobor.BlockInfo, bool) { + header, err := s.backend.APIBackend.HeaderByNumber(ctx, rpc.BlockNumber(blockNum)) + // the negate_conditional requires mocking both err!=nil and nil-header paths + // mutator-disable-next-line defensive APIBackend guard + if err != nil || header == nil { + return nil, false + } + + td := s.backend.APIBackend.GetTd(ctx, header.Hash()) + if td == nil || !td.IsUint64() { + return nil, false + } + + info := &protobor.BlockInfo{ + Header: headerToProtoBorHeader(header), + TotalDifficulty: td.Uint64(), + } + + if blockNum > 0 { + author, err := s.backend.Engine().Author(header) + if err != nil { + return nil, false + } + info.Author = protoutil.ConvertAddressToH160(author) + } + + return info, true +} + func getRpcBlockNumberFromString(blockNumber string) (rpc.BlockNumber, error) { switch blockNumber { case "latest": diff --git a/internal/cli/server/api_service_test.go b/internal/cli/server/api_service_test.go new file mode 100644 index 0000000000..6c1a323dbc --- /dev/null +++ b/internal/cli/server/api_service_test.go @@ -0,0 +1,258 @@ +package server + +import ( + "context" + "math" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + protobor "github.com/0xPolygon/polyproto/bor" + commonproto "github.com/0xPolygon/polyproto/common" + protoutil "github.com/0xPolygon/polyproto/utils" +) + +// Compile-time check that Server implements the proto interface. +var _ protobor.BorApiServer = (*Server)(nil) + +func TestGetAuthor_InvalidBlockNumber(t *testing.T) { + srv := &Server{} + _, err := srv.GetAuthor(context.Background(), &protobor.GetAuthorRequest{Number: "not-a-number"}) + if err == nil { + t.Fatalf("expected error on invalid block number, got nil") + } +} + +func TestHeaderToProtoBorHeader_RoundTrip_Cancun(t *testing.T) { + src := &types.Header{ + ParentHash: common.HexToHash("0x3333333333333333333333333333333333333333333333333333333333333333"), + UncleHash: types.EmptyUncleHash, + Coinbase: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + Root: common.HexToHash("0x4444444444444444444444444444444444444444444444444444444444444444"), + TxHash: common.HexToHash("0x5555555555555555555555555555555555555555555555555555555555555555"), + ReceiptHash: common.HexToHash("0x6666666666666666666666666666666666666666666666666666666666666666"), + Bloom: types.Bloom{0x01, 0x02, 0x03}, + Difficulty: big.NewInt(17), + Number: big.NewInt(1234567), + GasLimit: 30_000_000, + GasUsed: 21_000, + Time: 1_700_000_000, + Extra: []byte{0xde, 0xad, 0xbe, 0xef}, + MixDigest: common.HexToHash("0x7777777777777777777777777777777777777777777777777777777777777777"), + Nonce: types.BlockNonce{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8}, + BaseFee: big.NewInt(1_000_000_000), + WithdrawalsHash: new(common.HexToHash("0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899")), + BlobGasUsed: new(uint64(131072)), + ExcessBlobGas: new(uint64(262144)), + ParentBeaconRoot: new(common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111")), + RequestsHash: new(common.HexToHash("0x2222222222222222222222222222222222222222222222222222222222222222")), + } + + pb := headerToProtoBorHeader(src) + got := protoHeaderToEthHeaderLocal(t, pb) + if got.Hash() != src.Hash() { + t.Fatalf("hash mismatch: got %x want %x", got.Hash(), src.Hash()) + } +} + +// TestHeaderToProtoBorHeader_RoundTrip_CancunZeroBlobGas guards against the nil vs. zero trap on blobGasUsed / excessBlobGas. +// A header with BlobGasUsed=&0 must round-trip to BlobGasUsed=&0 (not nil), +// otherwise Hash() changes and milestone propositions break. +func TestHeaderToProtoBorHeader_RoundTrip_CancunZeroBlobGas(t *testing.T) { + zeroHash := common.Hash{} + + src := &types.Header{ + ParentHash: common.HexToHash("0x01"), + UncleHash: types.EmptyUncleHash, + Coinbase: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + Root: common.HexToHash("0x02"), + TxHash: common.HexToHash("0x03"), + ReceiptHash: common.HexToHash("0x04"), + Difficulty: big.NewInt(1), + Number: big.NewInt(100), + GasLimit: 30_000_000, + Time: 1_700_000_000, + BaseFee: big.NewInt(1_000_000_000), + BlobGasUsed: new(uint64(0)), + ExcessBlobGas: new(uint64(0)), + ParentBeaconRoot: &zeroHash, + } + + pb := headerToProtoBorHeader(src) + got := protoHeaderToEthHeaderLocal(t, pb) + if got.Hash() != src.Hash() { + t.Fatalf("hash mismatch (zero-blob-gas): got %x want %x", got.Hash(), src.Hash()) + } + if got.BlobGasUsed == nil { + t.Fatalf("BlobGasUsed must round-trip to &0, not nil") + } + if got.ExcessBlobGas == nil { + t.Fatalf("ExcessBlobGas must round-trip to &0, not nil") + } +} + +func TestHeaderToProtoBorHeader_RoundTrip_PreShanghai(t *testing.T) { + src := &types.Header{ + ParentHash: common.HexToHash("0x01"), + UncleHash: types.EmptyUncleHash, + Coinbase: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + Root: common.HexToHash("0x02"), + TxHash: common.HexToHash("0x03"), + ReceiptHash: common.HexToHash("0x04"), + Difficulty: big.NewInt(1), + Number: big.NewInt(100), + GasLimit: 30_000_000, + GasUsed: 0, + Time: 1_600_000_000, + Extra: []byte{}, + MixDigest: common.Hash{}, + Nonce: types.BlockNonce{}, + } + pb := headerToProtoBorHeader(src) + got := protoHeaderToEthHeaderLocal(t, pb) + if got.Hash() != src.Hash() { + t.Fatalf("hash mismatch pre-shanghai: got %x want %x", got.Hash(), src.Hash()) + } +} + +func TestGetTdByNumber_InvalidNumber(t *testing.T) { + srv := &Server{} + _, err := srv.GetTdByNumber(context.Background(), &protobor.GetTdByNumberRequest{Number: "not-a-number"}) + if err == nil { + t.Fatalf("expected error on invalid block number") + } +} + +func TestGetBlockInfoInBatch_RangeBound(t *testing.T) { + srv := &Server{} + _, err := srv.GetBlockInfoInBatch(context.Background(), &protobor.GetBlockInfoInBatchRequest{ + StartBlockNumber: 0, EndBlockNumber: 2_000, // exceeds maxBlockInfoBatchSize + }) + if err == nil { + t.Fatalf("expected error when batch range exceeds limit") + } +} + +func TestGetBlockInfoInBatch_StartGreaterThanEnd(t *testing.T) { + srv := &Server{} + _, err := srv.GetBlockInfoInBatch(context.Background(), &protobor.GetBlockInfoInBatchRequest{ + StartBlockNumber: 10, EndBlockNumber: 5, + }) + if err == nil { + t.Fatalf("expected error when start > end") + } +} + +// TestGetBlockInfoInBatch_RangeOverflow guards against the uint64 overflow that allowed `end - start + 1` to wrap +// past the batch-size limit and drive the server into a non-terminating loop. +func TestGetBlockInfoInBatch_RangeOverflow(t *testing.T) { + srv := &Server{} + _, err := srv.GetBlockInfoInBatch(context.Background(), &protobor.GetBlockInfoInBatchRequest{ + StartBlockNumber: 0, EndBlockNumber: math.MaxUint64, + }) + if err == nil { + t.Fatalf("expected error on overflowing range, got nil (would non-terminate)") + } +} + +// TestGetBlockInfoInBatch_SizeGateBoundary tests the boundaries. +// A range of exactly maxBlockInfoBatchSize must pass the size gate, +// and a range of maxBlockInfoBatchSize+1 must fail it with the specific error. +// We distinguish the size gate from downstream failures by checking the error +// message. A panic from the nil backend is fine for the at-limit case since +// we only care that the gate itself didn't reject. +func TestGetBlockInfoInBatch_SizeGateBoundary(t *testing.T) { + t.Run("at limit passes the size gate", func(t *testing.T) { + srv := &Server{} + // size = end - start + 1 = 256 = maxBlockInfoBatchSize (allowed) + var err error + func() { + defer func() { + // Backend is nil; handler panics calling APIBackend.HeaderByNumber. + // A panic here means we *passed* the gate, which is what we want. + _ = recover() + }() + _, err = srv.GetBlockInfoInBatch(context.Background(), &protobor.GetBlockInfoInBatchRequest{ + StartBlockNumber: 0, EndBlockNumber: uint64(maxBlockInfoBatchSize - 1), + }) + }() + if err != nil && strings.Contains(err.Error(), "exceeds max batch size") { + t.Fatalf("size gate rejected a size-of-%d request; should accept: %v", maxBlockInfoBatchSize, err) + } + }) + + t.Run("just over limit fails the size gate", func(t *testing.T) { + srv := &Server{} + // size = end - start + 1 = 257 > maxBlockInfoBatchSize + _, err := srv.GetBlockInfoInBatch(context.Background(), &protobor.GetBlockInfoInBatchRequest{ + StartBlockNumber: 0, EndBlockNumber: uint64(maxBlockInfoBatchSize), + }) + if err == nil { + t.Fatalf("expected size-gate error for range size %d (>%d), got nil", maxBlockInfoBatchSize+1, maxBlockInfoBatchSize) + } + if !strings.Contains(err.Error(), "exceeds max batch size") { + t.Fatalf("expected size-gate error message, got: %v", err) + } + }) +} + +// protoHeaderToEthHeaderLocal is the test-side inverse of headerToProtoBorHeader. +// It mirrors the decoder that heimdall's x/bor/grpc package will ship. +func protoHeaderToEthHeaderLocal(t *testing.T, p *protobor.Header) *types.Header { + t.Helper() + if p == nil { + return nil + } + convH := func(h *commonproto.H256) common.Hash { + if h == nil { + return common.Hash{} + } + b := protoutil.ConvertH256ToHash(h) + return common.BytesToHash(b[:]) + } + convA := func(a *commonproto.H160) common.Address { + if a == nil { + return common.Address{} + } + arr := protoutil.ConvertH160toAddress(a) + return common.BytesToAddress(arr[:]) + } + + h := &types.Header{ + ParentHash: convH(p.ParentHash), + UncleHash: convH(p.UncleHash), + Coinbase: convA(p.Coinbase), + Root: convH(p.StateRoot), + TxHash: convH(p.TxRoot), + ReceiptHash: convH(p.ReceiptRoot), + Difficulty: new(big.Int).SetBytes(p.Difficulty), + Number: new(big.Int).SetUint64(p.Number), + GasLimit: p.GasLimit, + GasUsed: p.GasUsed, + Time: p.Time, + Extra: append([]byte(nil), p.ExtraData...), + MixDigest: convH(p.MixDigest), + } + h.Bloom.SetBytes(p.Bloom) + copy(h.Nonce[:], p.Nonce) + + if len(p.BaseFee) > 0 { + h.BaseFee = new(big.Int).SetBytes(p.BaseFee) + } + if p.WithdrawalsHash != nil { + h.WithdrawalsHash = new(convH(p.WithdrawalsHash)) + } + // BlobGasUsed / ExcessBlobGas are proto3 `optional` on the wire → *uint64 on the Go side. + h.BlobGasUsed = p.BlobGasUsed + h.ExcessBlobGas = p.ExcessBlobGas + if p.ParentBeaconBlockRoot != nil { + h.ParentBeaconRoot = new(convH(p.ParentBeaconBlockRoot)) + } + if p.RequestsHash != nil { + h.RequestsHash = new(convH(p.RequestsHash)) + } + return h +} diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index dd55aa98bb..8ab5442cc3 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -526,6 +526,8 @@ type AUTHConfig struct { type GRPCConfig struct { // Addr is the bind address for the grpc rpc server Addr string `hcl:"addr,optional" toml:"addr,optional"` + // Token is the bearer token required for incoming gRPC calls; empty disables auth + Token string `hcl:"token,optional" toml:"token,optional"` } type APIConfig struct { diff --git a/internal/cli/server/flags.go b/internal/cli/server/flags.go index 270d9f6bde..acea7bf9ba 100644 --- a/internal/cli/server/flags.go +++ b/internal/cli/server/flags.go @@ -1224,6 +1224,12 @@ func (c *Command) Flags(config *Config) *flagset.Flagset { Value: &c.cliConfig.GRPC.Addr, Default: c.cliConfig.GRPC.Addr, }) + f.StringFlag(&flagset.StringFlag{ + Name: "grpc.token", + Usage: "Bearer token required for incoming gRPC calls (empty disables auth)", + Value: &c.cliConfig.GRPC.Token, + Default: c.cliConfig.GRPC.Token, + }) // developer f.BoolFlag(&flagset.BoolFlag{ diff --git a/internal/cli/server/grpc_auth_test.go b/internal/cli/server/grpc_auth_test.go new file mode 100644 index 0000000000..36818870cd --- /dev/null +++ b/internal/cli/server/grpc_auth_test.go @@ -0,0 +1,187 @@ +package server + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +const fakeToken = "Bearer secret" + +func TestAuthenticate_MissingMetadata(t *testing.T) { + t.Parallel() + // Plain context with no gRPC metadata attached. + err := authenticate(context.Background(), "secret") + require.Error(t, err) + s, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unauthenticated, s.Code()) + require.Contains(t, s.Message(), "missing metadata") +} + +func TestAuthenticate_MissingAuthorizationHeader(t *testing.T) { + t.Parallel() + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("other-header", "value")) + err := authenticate(ctx, "secret") + require.Error(t, err) + s, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unauthenticated, s.Code()) + require.Contains(t, s.Message(), "missing authorization header") +} + +func TestAuthenticate_MissingBearerPrefix(t *testing.T) { + t.Parallel() + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Basic secret")) + err := authenticate(ctx, "secret") + require.Error(t, err) + s, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unauthenticated, s.Code()) + require.Contains(t, s.Message(), "invalid authorization header") +} + +func TestAuthenticate_WrongToken(t *testing.T) { + t.Parallel() + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer wrong_token")) + err := authenticate(ctx, "secret") + require.Error(t, err) + s, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unauthenticated, s.Code()) + require.Contains(t, s.Message(), "invalid token") +} + +func TestAuthenticate_CorrectToken(t *testing.T) { + t.Parallel() + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", fakeToken)) + err := authenticate(ctx, "secret") + require.NoError(t, err) +} + +// TestAuthenticate_ConstantTimeCompare verifies that both a close-miss token and +// a completely different token both return Unauthenticated (no behavioral +// difference based on byte position — the unit test checks that both fail). +func TestAuthenticate_ConstantTimeCompare(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + token string + }{ + {"one-byte-diff", "abd"}, + {"totally-different", "xyz"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer "+tc.token)) + err := authenticate(ctx, "abc") + require.Error(t, err) + s, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unauthenticated, s.Code()) + }) + } +} + +// TestCombinedUnaryInterceptor tests the unary interceptor's behavior with various token configurations and metadata. +func TestCombinedUnaryInterceptor(t *testing.T) { + makeSrv := func(token string) *Server { + return &Server{config: &Config{GRPC: &GRPCConfig{Token: token}}} + } + + info := &grpc.UnaryServerInfo{FullMethod: "/test/Method"} + okHandler := func(ctx context.Context, req interface{}) (interface{}, error) { return "ran", nil } + + t.Run("no configured token bypasses auth and runs handler", func(t *testing.T) { + srv := makeSrv("") + resp, err := srv.combinedUnaryInterceptor()(context.Background(), nil, info, okHandler) + require.NoError(t, err) + require.Equal(t, "ran", resp) + }) + + t.Run("configured token with no client metadata rejects Unauthenticated", func(t *testing.T) { + srv := makeSrv("secret") + ran := false + handler := func(ctx context.Context, req interface{}) (interface{}, error) { ran = true; return nil, nil } + _, err := srv.combinedUnaryInterceptor()(context.Background(), nil, info, handler) + require.Error(t, err) + s, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unauthenticated, s.Code()) + require.False(t, ran, "handler must not run when auth is rejected") + }) + + t.Run("configured token with matching bearer runs handler", func(t *testing.T) { + srv := makeSrv("secret") + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", fakeToken)) + resp, err := srv.combinedUnaryInterceptor()(ctx, nil, info, okHandler) + require.NoError(t, err) + require.Equal(t, "ran", resp) + }) +} + +// TestCombinedStreamInterceptor covers the stream interceptor's behavior with various token configurations and metadata. +func TestCombinedStreamInterceptor(t *testing.T) { + makeSrv := func(token string) *Server { + return &Server{config: &Config{GRPC: &GRPCConfig{Token: token}}} + } + + info := &grpc.StreamServerInfo{FullMethod: "/test/Stream"} + + t.Run("no configured token bypasses auth and runs handler", func(t *testing.T) { + srv := makeSrv("") + ran := false + handler := func(srv interface{}, ss grpc.ServerStream) error { ran = true; return nil } + err := srv.combinedStreamInterceptor()(nil, &fakeServerStream{ctx: context.Background()}, info, handler) + require.NoError(t, err) + require.True(t, ran) + }) + + t.Run("configured token with no metadata rejects Unauthenticated and skips handler", func(t *testing.T) { + srv := makeSrv("secret") + ran := false + handler := func(srv interface{}, ss grpc.ServerStream) error { ran = true; return nil } + err := srv.combinedStreamInterceptor()(nil, &fakeServerStream{ctx: context.Background()}, info, handler) + require.Error(t, err) + s, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unauthenticated, s.Code()) + require.False(t, ran, "handler must not run when auth is rejected") + }) + + t.Run("handler error is propagated unchanged", func(t *testing.T) { + srv := makeSrv("") + sentinel := errors.New("handler failure sentinel") + handler := func(srv interface{}, ss grpc.ServerStream) error { return sentinel } + err := srv.combinedStreamInterceptor()(nil, &fakeServerStream{ctx: context.Background()}, info, handler) + require.ErrorIs(t, err, sentinel, "interceptor must propagate handler errors, not swallow them") + }) + + t.Run("configured token with matching bearer runs handler", func(t *testing.T) { + srv := makeSrv("secret") + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", fakeToken)) + ran := false + handler := func(srv interface{}, ss grpc.ServerStream) error { ran = true; return nil } + err := srv.combinedStreamInterceptor()(nil, &fakeServerStream{ctx: ctx}, info, handler) + require.NoError(t, err) + require.True(t, ran) + }) +} + +// fakeServerStream is the minimum surface of grpc.ServerStream needed by the +// auth interceptor: it only inspects ss.Context(). +type fakeServerStream struct { + grpc.ServerStream + ctx context.Context +} + +func (f *fakeServerStream) Context() context.Context { return f.ctx } diff --git a/internal/cli/server/server.go b/internal/cli/server/server.go index 8cde0e2149..dfd2bb1aa7 100644 --- a/internal/cli/server/server.go +++ b/internal/cli/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "crypto/subtle" "encoding/json" "fmt" "io" @@ -23,7 +24,10 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/reflection" + "google.golang.org/grpc/status" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -434,7 +438,10 @@ func (s *Server) gRPCServerByAddress(addr string) error { } func (s *Server) gRPCServerByListener(listener net.Listener) error { - s.grpcServer = grpc.NewServer(s.withLoggingUnaryInterceptor()) + s.grpcServer = grpc.NewServer( + grpc.UnaryInterceptor(s.combinedUnaryInterceptor()), + grpc.StreamInterceptor(s.combinedStreamInterceptor()), + ) proto.RegisterBorServer(s.grpcServer, s) protobor.RegisterBorApiServer(s.grpcServer, s) reflection.Register(s.grpcServer) @@ -450,17 +457,62 @@ func (s *Server) gRPCServerByListener(listener net.Listener) error { return nil } -func (s *Server) withLoggingUnaryInterceptor() grpc.ServerOption { - return grpc.UnaryInterceptor(s.loggingServerInterceptor) +// combinedUnaryInterceptor returns a single unary server interceptor that +// optionally enforces bearer-token authentication (when a token is configured) +// and always logs the request duration. +func (s *Server) combinedUnaryInterceptor() grpc.UnaryServerInterceptor { + token := s.config.GRPC.Token + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if token != "" { + if err := authenticate(ctx, token); err != nil { + return nil, err + } + } + start := time.Now() + h, err := handler(ctx, req) + log.Trace("Request", "method", info.FullMethod, "duration", time.Since(start), "error", err) + return h, err + } } -func (s *Server) loggingServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - start := time.Now() - h, err := handler(ctx, req) - - log.Trace("Request", "method", info.FullMethod, "duration", time.Since(start), "error", err) +// combinedStreamInterceptor mirrors combinedUnaryInterceptor for stream RPCs. +// Needed so the reflection service is gated by the same bearer-token check as unary calls. +func (s *Server) combinedStreamInterceptor() grpc.StreamServerInterceptor { + token := s.config.GRPC.Token + return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + if token != "" { + if err := authenticate(ss.Context(), token); err != nil { + return err + } + } + start := time.Now() + err := handler(srv, ss) + log.Trace("Stream", "method", info.FullMethod, "duration", time.Since(start), "error", err) + return err + } +} - return h, err +// authenticate validates the bearer token in the gRPC metadata against the +// configured token using a constant-time comparison. +func authenticate(ctx context.Context, expected string) error { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return status.Error(codes.Unauthenticated, "missing metadata") + } + headers := md.Get("authorization") + if len(headers) == 0 { + return status.Error(codes.Unauthenticated, "missing authorization header") + } + const prefix = "Bearer " + h := headers[0] + if !strings.HasPrefix(h, prefix) { + return status.Error(codes.Unauthenticated, "invalid authorization header") + } + got := h[len(prefix):] + if subtle.ConstantTimeCompare([]byte(got), []byte(expected)) != 1 { + return status.Error(codes.Unauthenticated, "invalid token") + } + return nil } func setupLogger(logLevel int, loggingInfo LoggingConfig) { @@ -620,8 +672,8 @@ func (s *Server) customHealthServiceHandler() http.Handler { healthResponse["node_info"] = s.getBorInfo() - status := s.performHealthChecks(healthResponse) - healthResponse["status"] = status + sts := s.performHealthChecks(healthResponse) + healthResponse["status"] = sts healthResponse["error"] = false healthResponse["error_message"] = "" From fe62ec8ea065e090824159fbf26578e0b88932b5 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Thu, 23 Apr 2026 18:11:16 +0200 Subject: [PATCH 2/7] address comments --- consensus/bor/bor.go | 10 +++++- internal/cli/server/api_service.go | 45 ++++++++++++++++--------- internal/cli/server/api_service_test.go | 15 +++++++++ internal/cli/server/command.go | 7 ++++ internal/cli/server/flags.go | 2 +- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index b126786fb0..409ff25c5f 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1529,9 +1529,17 @@ func (c *Bor) SealHash(header *types.Header) common.Hash { // rootHashCache) persists across calls. JSON-RPC only invokes APIs() once at // node startup, but the gRPC backend fetches it on every handler call — without // the cache those calls would each start from an empty state. +// +// rootHashCache is initialized here (inside the sync.Once) rather than lazily +// in GetRootHash so that concurrent gRPC handlers sharing the cached *API +// cannot race in initializeRootHashCache. func (c *Bor) APIs(chain consensus.ChainHeaderReader) []rpc.API { c.apiOnce.Do(func() { - c.api = &API{chain: chain, bor: c} + a := &API{chain: chain, bor: c} + if err := a.initializeRootHashCache(); err != nil { + panic(fmt.Errorf("bor: failed to initialize rootHashCache: %w", err)) + } + c.api = a }) return []rpc.API{{ Namespace: "bor", diff --git a/internal/cli/server/api_service.go b/internal/cli/server/api_service.go index a6dd57c3d3..61b75eb887 100644 --- a/internal/cli/server/api_service.go +++ b/internal/cli/server/api_service.go @@ -46,12 +46,12 @@ func headerToProtoBorHeader(h *types.Header) *protobor.Header { StateRoot: protoutil.ConvertHashToH256(h.Root), TxRoot: protoutil.ConvertHashToH256(h.TxHash), ReceiptRoot: protoutil.ConvertHashToH256(h.ReceiptHash), - Bloom: h.Bloom.Bytes(), + Bloom: append([]byte(nil), h.Bloom.Bytes()...), GasLimit: h.GasLimit, GasUsed: h.GasUsed, ExtraData: append([]byte(nil), h.Extra...), MixDigest: protoutil.ConvertHashToH256(h.MixDigest), - Nonce: h.Nonce[:], + Nonce: append([]byte(nil), h.Nonce[:]...), } if h.Difficulty != nil { out.Difficulty = h.Difficulty.Bytes() @@ -62,10 +62,18 @@ func headerToProtoBorHeader(h *types.Header) *protobor.Header { if h.WithdrawalsHash != nil { out.WithdrawalsHash = protoutil.ConvertHashToH256(*h.WithdrawalsHash) } - // BlobGasUsed and ExcessBlobGas are proto3 optional - // using *uint64 as a direct pointer, so the copy preserves nil vs.zero. - out.BlobGasUsed = h.BlobGasUsed - out.ExcessBlobGas = h.ExcessBlobGas + // BlobGasUsed and ExcessBlobGas are proto3 optional. *uint64 preserves + // nil-vs-zero; we copy through a fresh variable so the proto doesn't alias + // the source header's pointers (consistent with how ExtraData/Bloom/Nonce + // are handled above). + if h.BlobGasUsed != nil { + v := *h.BlobGasUsed + out.BlobGasUsed = &v + } + if h.ExcessBlobGas != nil { + v := *h.ExcessBlobGas + out.ExcessBlobGas = &v + } if h.ParentBeaconRoot != nil { out.ParentBeaconBlockRoot = protoutil.ConvertHashToH256(*h.ParentBeaconRoot) } @@ -166,8 +174,7 @@ func (s *Server) GetAuthor(ctx context.Context, req *protobor.GetAuthorRequest) } func (s *Server) GetTdByHash(ctx context.Context, req *protobor.GetTdByHashRequest) (*protobor.GetTdResponse, error) { - hashBytes := protoutil.ConvertH256ToHash(req.Hash) - hash := common.BytesToHash(hashBytes[:]) + hash := common.Hash(protoutil.ConvertH256ToHash(req.Hash)) td := s.backend.APIBackend.GetTd(ctx, hash) if td == nil { @@ -201,16 +208,20 @@ func (s *Server) GetBlockInfoInBatch(ctx context.Context, req *protobor.GetBlock if req.EndBlockNumber-req.StartBlockNumber >= uint64(maxBlockInfoBatchSize) { return nil, errors.New("invalid range: exceeds max batch size") } + if req.EndBlockNumber > math.MaxInt64 { + return nil, errors.New("invalid range: end exceeds max int64") + } - size := int(req.EndBlockNumber-req.StartBlockNumber) + 1 + count := req.EndBlockNumber - req.StartBlockNumber + 1 out := &protobor.GetBlockInfoInBatchResponse{ - Blocks: make([]*protobor.BlockInfo, 0, size), + Blocks: make([]*protobor.BlockInfo, 0, count), } - // the i++ -> i-- requires an integration test with a multi-block batch - // mutator-disable-next-line loop-step - for i := req.StartBlockNumber; i <= req.EndBlockNumber; i++ { - info, ok := s.fetchBlockInfo(ctx, i) + for j := uint64(0); j < count; j++ { + if err := ctx.Err(); err != nil { + return nil, err + } + info, ok := s.fetchBlockInfo(ctx, req.StartBlockNumber+j) // this requires APIBackend mock returning a missing block mid-range // mutator-disable-next-line gap-stop semantics if !ok { @@ -225,8 +236,12 @@ func (s *Server) GetBlockInfoInBatch(ctx context.Context, req *protobor.GetBlock // fetchBlockInfo loads header, total difficulty, and author for blockNum. // Returns (nil, false) if any piece is missing — the caller should stop the loop. -// Author is left zero-valued for genesis (matching bor_getAuthor behavior). +// Author is left as a nil *H160 for genesis; callers must nil-check before +// decoding. func (s *Server) fetchBlockInfo(ctx context.Context, blockNum uint64) (*protobor.BlockInfo, bool) { + if blockNum > math.MaxInt64 { + return nil, false + } header, err := s.backend.APIBackend.HeaderByNumber(ctx, rpc.BlockNumber(blockNum)) // the negate_conditional requires mocking both err!=nil and nil-header paths // mutator-disable-next-line defensive APIBackend guard diff --git a/internal/cli/server/api_service_test.go b/internal/cli/server/api_service_test.go index 6c1a323dbc..f1dd6f6ae3 100644 --- a/internal/cli/server/api_service_test.go +++ b/internal/cli/server/api_service_test.go @@ -158,6 +158,21 @@ func TestGetBlockInfoInBatch_RangeOverflow(t *testing.T) { } } +// TestGetBlockInfoInBatch_NearMaxUint64 guards against a narrow range near MaxUint64. +func TestGetBlockInfoInBatch_NearMaxUint64(t *testing.T) { + srv := &Server{} + _, err := srv.GetBlockInfoInBatch(context.Background(), &protobor.GetBlockInfoInBatchRequest{ + StartBlockNumber: math.MaxUint64 - 3, + EndBlockNumber: math.MaxUint64, + }) + if err == nil { + t.Fatalf("expected error on near-MaxUint64 range, got nil (would wrap and walk chain)") + } + if !strings.Contains(err.Error(), "exceeds max int64") { + t.Fatalf("expected int64-overflow error, got: %v", err) + } +} + // TestGetBlockInfoInBatch_SizeGateBoundary tests the boundaries. // A range of exactly maxBlockInfoBatchSize must pass the size gate, // and a range of maxBlockInfoBatchSize+1 must fail it with the specific error. diff --git a/internal/cli/server/command.go b/internal/cli/server/command.go index 2925affaea..0ca5a51486 100644 --- a/internal/cli/server/command.go +++ b/internal/cli/server/command.go @@ -134,6 +134,13 @@ func (c *Command) extractFlags(args []string) error { // Handle multiple flags for tx lookup limit c.cliConfig.Cache.TxLookupLimit = handleTxLookupLimitFlag(tomlConfig, args, c.cliConfig) + // Env-var fallback for the gRPC auth token. + if c.cliConfig.GRPC != nil && c.cliConfig.GRPC.Token == "" { + if envTok := os.Getenv("BOR_GRPC_TOKEN"); envTok != "" { + c.cliConfig.GRPC.Token = envTok + } + } + c.config = c.cliConfig return nil diff --git a/internal/cli/server/flags.go b/internal/cli/server/flags.go index acea7bf9ba..823928e6f0 100644 --- a/internal/cli/server/flags.go +++ b/internal/cli/server/flags.go @@ -1226,7 +1226,7 @@ func (c *Command) Flags(config *Config) *flagset.Flagset { }) f.StringFlag(&flagset.StringFlag{ Name: "grpc.token", - Usage: "Bearer token required for incoming gRPC calls (empty disables auth)", + Usage: "Raw token expected in the `authorization: Bearer ` header of incoming gRPC calls (empty disables auth; the `Bearer ` prefix is stripped before comparison). Prefer the BOR_GRPC_TOKEN environment variable over this flag.", Value: &c.cliConfig.GRPC.Token, Default: c.cliConfig.GRPC.Token, }) From edafc70de4d67d7d9cfe4a5c8e64bd15ea6323e2 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Thu, 23 Apr 2026 19:15:28 +0200 Subject: [PATCH 3/7] address comments --- consensus/bor/api.go | 4 +- internal/cli/server/api_service.go | 81 +++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/consensus/bor/api.go b/consensus/bor/api.go index 9176fbe7ea..82eb3103ca 100644 --- a/consensus/bor/api.go +++ b/consensus/bor/api.go @@ -277,7 +277,9 @@ func (api *API) GetCurrentValidators() ([]*valset.Validator, error) { return snap.ValidatorSet.Validators, nil } -// GetRootHash returns the merkle root of the start-to-end blocks' headers +// GetRootHash returns the merkle root of the start-to-end blocks' headers. +// rootHashCache is normally initialized eagerly inside Bor.APIs (sync.Once); +// the lazy init below is kept as fallback for direct-API paths (e.g., tests) that don't go through Bor.APIs. func (api *API) GetRootHash(start uint64, end uint64) (string, error) { if err := api.initializeRootHashCache(); err != nil { return "", err diff --git a/internal/cli/server/api_service.go b/internal/cli/server/api_service.go index 61b75eb887..316fe1e382 100644 --- a/internal/cli/server/api_service.go +++ b/internal/cli/server/api_service.go @@ -9,11 +9,23 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" protobor "github.com/0xPolygon/polyproto/bor" + commonproto "github.com/0xPolygon/polyproto/common" protoutil "github.com/0xPolygon/polyproto/utils" ) +// protoHashToCommon safely converts a proto H256 to a common.Hash. Returns +// codes.InvalidArgument if the outer pointer or any inner H128 sub-message is nil +func protoHashToCommon(h *commonproto.H256) (common.Hash, error) { + if h == nil || h.Hi == nil || h.Lo == nil { + return common.Hash{}, status.Error(codes.InvalidArgument, "hash is required with non-nil Hi/Lo") + } + return common.Hash(protoutil.ConvertH256ToHash(h)), nil +} + // maxBlockInfoBatchSize caps the per-call range to prevent abuse of the batch endpoint. // Must be >= heimdall's MaxMilestonePropositionLength. const maxBlockInfoBatchSize = 256 @@ -124,7 +136,11 @@ func blockToProtoBlock(h *types.Block) *protobor.Block { } func (s *Server) TransactionReceipt(ctx context.Context, req *protobor.ReceiptRequest) (*protobor.ReceiptResponse, error) { - _, _, blockHash, _, txnIndex := s.backend.APIBackend.GetTransaction(protoutil.ConvertH256ToHash(req.Hash)) + txHash, err := protoHashToCommon(req.Hash) + if err != nil { + return nil, err + } + _, _, blockHash, _, txnIndex := s.backend.APIBackend.GetTransaction(txHash) receipts, err := s.backend.APIBackend.GetReceipts(ctx, blockHash) if err != nil { @@ -143,7 +159,11 @@ func (s *Server) TransactionReceipt(ctx context.Context, req *protobor.ReceiptRe } func (s *Server) BorBlockReceipt(ctx context.Context, req *protobor.ReceiptRequest) (*protobor.ReceiptResponse, error) { - receipt, err := s.backend.APIBackend.GetBorBlockReceipt(ctx, protoutil.ConvertH256ToHash(req.Hash)) + txHash, err := protoHashToCommon(req.Hash) + if err != nil { + return nil, err + } + receipt, err := s.backend.APIBackend.GetBorBlockReceipt(ctx, txHash) if err != nil { return nil, err } @@ -174,7 +194,10 @@ func (s *Server) GetAuthor(ctx context.Context, req *protobor.GetAuthorRequest) } func (s *Server) GetTdByHash(ctx context.Context, req *protobor.GetTdByHashRequest) (*protobor.GetTdResponse, error) { - hash := common.Hash(protoutil.ConvertH256ToHash(req.Hash)) + hash, err := protoHashToCommon(req.Hash) + if err != nil { + return nil, err + } td := s.backend.APIBackend.GetTd(ctx, hash) if td == nil { @@ -191,7 +214,16 @@ func (s *Server) GetTdByNumber(ctx context.Context, req *protobor.GetTdByNumberR if err != nil { return nil, err } - td := s.backend.APIBackend.GetTdByNumber(ctx, bN) + // Resolve the block number (including special tags) to a concrete header + // before looking up TD by hash. + header, err := s.backend.APIBackend.HeaderByNumber(ctx, bN) + if err != nil { + return nil, err + } + if header == nil { + return nil, errors.New("header not found") + } + td := s.backend.APIBackend.GetTd(ctx, header.Hash()) if td == nil { return nil, errors.New("total difficulty not found") } @@ -221,7 +253,10 @@ func (s *Server) GetBlockInfoInBatch(ctx context.Context, req *protobor.GetBlock if err := ctx.Err(); err != nil { return nil, err } - info, ok := s.fetchBlockInfo(ctx, req.StartBlockNumber+j) + info, ok, err := s.fetchBlockInfo(ctx, req.StartBlockNumber+j) + if err != nil { + return nil, err + } // this requires APIBackend mock returning a missing block mid-range // mutator-disable-next-line gap-stop semantics if !ok { @@ -235,23 +270,31 @@ func (s *Server) GetBlockInfoInBatch(ctx context.Context, req *protobor.GetBlock } // fetchBlockInfo loads header, total difficulty, and author for blockNum. -// Returns (nil, false) if any piece is missing — the caller should stop the loop. -// Author is left as a nil *H160 for genesis; callers must nil-check before -// decoding. -func (s *Server) fetchBlockInfo(ctx context.Context, blockNum uint64) (*protobor.BlockInfo, bool) { +// Return semantics: +// - (info, true, nil): success, append to the batch +// - (nil, false, nil): legitimate gap (header not yet on chain / TD missing); caller should break the loop and return the partial prefix, matching HTTP side +// - (nil, false, err): real failure (backend error, ecrecover failure, overflow); caller should propagate the error +// +// Author is left as a nil *H160 for genesis; callers must nil-check before decoding. +func (s *Server) fetchBlockInfo(ctx context.Context, blockNum uint64) (*protobor.BlockInfo, bool, error) { if blockNum > math.MaxInt64 { - return nil, false + return nil, false, status.Error(codes.InvalidArgument, "block number exceeds max int64") } header, err := s.backend.APIBackend.HeaderByNumber(ctx, rpc.BlockNumber(blockNum)) - // the negate_conditional requires mocking both err!=nil and nil-header paths - // mutator-disable-next-line defensive APIBackend guard - if err != nil || header == nil { - return nil, false + if err != nil { + return nil, false, err + } + if header == nil { + return nil, false, nil } td := s.backend.APIBackend.GetTd(ctx, header.Hash()) - if td == nil || !td.IsUint64() { - return nil, false + if td == nil { + // TD not yet indexed — treat as a gap + return nil, false, nil + } + if !td.IsUint64() { + return nil, false, errors.New("total difficulty overflows uint64") } info := &protobor.BlockInfo{ @@ -262,12 +305,14 @@ func (s *Server) fetchBlockInfo(ctx context.Context, blockNum uint64) (*protobor if blockNum > 0 { author, err := s.backend.Engine().Author(header) if err != nil { - return nil, false + // Author() failure on a validated header indicates a corrupted + // seal — propagate the error. + return nil, false, err } info.Author = protoutil.ConvertAddressToH160(author) } - return info, true + return info, true, nil } func getRpcBlockNumberFromString(blockNumber string) (rpc.BlockNumber, error) { From 43b6efccf03636a24d89e84538286f4d0747b4d7 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Thu, 23 Apr 2026 20:27:45 +0200 Subject: [PATCH 4/7] address comments --- internal/cli/server/api_service.go | 8 +++++--- internal/cli/server/server.go | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/internal/cli/server/api_service.go b/internal/cli/server/api_service.go index 316fe1e382..aa79341cc2 100644 --- a/internal/cli/server/api_service.go +++ b/internal/cli/server/api_service.go @@ -234,14 +234,16 @@ func (s *Server) GetTdByNumber(ctx context.Context, req *protobor.GetTdByNumberR } func (s *Server) GetBlockInfoInBatch(ctx context.Context, req *protobor.GetBlockInfoInBatchRequest) (*protobor.GetBlockInfoInBatchResponse, error) { + // Input validation returns codes.InvalidArgument so clients can + // distinguish malformed requests from internal failures if req.EndBlockNumber < req.StartBlockNumber { - return nil, errors.New("invalid range: end < start") + return nil, status.Error(codes.InvalidArgument, "invalid range: end < start") } if req.EndBlockNumber-req.StartBlockNumber >= uint64(maxBlockInfoBatchSize) { - return nil, errors.New("invalid range: exceeds max batch size") + return nil, status.Error(codes.InvalidArgument, "invalid range: exceeds max batch size") } if req.EndBlockNumber > math.MaxInt64 { - return nil, errors.New("invalid range: end exceeds max int64") + return nil, status.Error(codes.InvalidArgument, "invalid range: end exceeds max int64") } count := req.EndBlockNumber - req.StartBlockNumber + 1 diff --git a/internal/cli/server/server.go b/internal/cli/server/server.go index dfd2bb1aa7..cf05319767 100644 --- a/internal/cli/server/server.go +++ b/internal/cli/server/server.go @@ -459,12 +459,15 @@ func (s *Server) gRPCServerByListener(listener net.Listener) error { // combinedUnaryInterceptor returns a single unary server interceptor that // optionally enforces bearer-token authentication (when a token is configured) -// and always logs the request duration. +// and logs the request outcome — both successful handler invocations and +// auth-rejected attempts. Rejected attempts are logged, successful calls are +// logged at Trace, rejections at Debug. func (s *Server) combinedUnaryInterceptor() grpc.UnaryServerInterceptor { token := s.config.GRPC.Token return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if token != "" { if err := authenticate(ctx, token); err != nil { + log.Debug("gRPC auth rejected", "method", info.FullMethod, "error", err) return nil, err } } @@ -476,12 +479,15 @@ func (s *Server) combinedUnaryInterceptor() grpc.UnaryServerInterceptor { } // combinedStreamInterceptor mirrors combinedUnaryInterceptor for stream RPCs. -// Needed so the reflection service is gated by the same bearer-token check as unary calls. +// Needed so the reflection service is gated by the same bearer-token check as +// unary calls. Logging behavior matches combinedUnaryInterceptor: rejected +// auth at Debug, successful stream duration at Trace. func (s *Server) combinedStreamInterceptor() grpc.StreamServerInterceptor { token := s.config.GRPC.Token return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { if token != "" { if err := authenticate(ss.Context(), token); err != nil { + log.Debug("gRPC auth rejected (stream)", "method", info.FullMethod, "error", err) return err } } @@ -493,7 +499,10 @@ func (s *Server) combinedStreamInterceptor() grpc.StreamServerInterceptor { } // authenticate validates the bearer token in the gRPC metadata against the -// configured token using a constant-time comparison. +// configured token. +// Token byte-comparison uses subtle.ConstantTimeCompare, which is constant-time +// for equal-length inputs; length-mismatched inputs short-circuit. +// Scheme matching is case-insensitive per RFC 6750 §2.1. func authenticate(ctx context.Context, expected string) error { md, ok := metadata.FromIncomingContext(ctx) if !ok { @@ -505,7 +514,7 @@ func authenticate(ctx context.Context, expected string) error { } const prefix = "Bearer " h := headers[0] - if !strings.HasPrefix(h, prefix) { + if len(h) < len(prefix) || !strings.EqualFold(h[:len(prefix)], prefix) { return status.Error(codes.Unauthenticated, "invalid authorization header") } got := h[len(prefix):] From 84b7d5c4dbaa803c3e153989a29ce3315d250cd7 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Thu, 23 Apr 2026 20:39:59 +0200 Subject: [PATCH 5/7] address comments --- internal/cli/server/api_service.go | 27 +++++++++++++-------------- internal/cli/server/grpc_auth_test.go | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/internal/cli/server/api_service.go b/internal/cli/server/api_service.go index aa79341cc2..38269707e9 100644 --- a/internal/cli/server/api_service.go +++ b/internal/cli/server/api_service.go @@ -2,7 +2,6 @@ package server import ( "context" - "errors" "math" "github.com/ethereum/go-ethereum/common" @@ -106,7 +105,7 @@ func (s *Server) HeaderByNumber(ctx context.Context, req *protobor.GetHeaderByNu } if header == nil { - return nil, errors.New("header not found") + return nil, status.Error(codes.NotFound, "header not found") } return &protobor.GetHeaderByNumberResponse{Header: headerToProtoBorHeader(header)}, nil @@ -123,7 +122,7 @@ func (s *Server) BlockByNumber(ctx context.Context, req *protobor.GetBlockByNumb } if block == nil { - return nil, errors.New("block not found") + return nil, status.Error(codes.NotFound, "block not found") } return &protobor.GetBlockByNumberResponse{Block: blockToProtoBlock(block)}, nil @@ -148,11 +147,11 @@ func (s *Server) TransactionReceipt(ctx context.Context, req *protobor.ReceiptRe } if receipts == nil { - return nil, errors.New("no receipts found") + return nil, status.Error(codes.NotFound, "no receipts found") } if len(receipts) <= int(txnIndex) { - return nil, errors.New("transaction index out of bounds") + return nil, status.Error(codes.OutOfRange, "transaction index out of bounds") } return &protobor.ReceiptResponse{Receipt: ConvertReceiptToProtoReceipt(receipts[txnIndex])}, nil @@ -182,7 +181,7 @@ func (s *Server) GetAuthor(ctx context.Context, req *protobor.GetAuthorRequest) return nil, err } if header == nil { - return nil, errors.New("header not found") + return nil, status.Error(codes.NotFound, "header not found") } author, err := s.backend.Engine().Author(header) @@ -201,10 +200,10 @@ func (s *Server) GetTdByHash(ctx context.Context, req *protobor.GetTdByHashReque td := s.backend.APIBackend.GetTd(ctx, hash) if td == nil { - return nil, errors.New("total difficulty not found") + return nil, status.Error(codes.NotFound, "total difficulty not found") } if !td.IsUint64() { - return nil, errors.New("total difficulty overflows uint64") + return nil, status.Error(codes.OutOfRange, "total difficulty overflows uint64") } return &protobor.GetTdResponse{TotalDifficulty: td.Uint64()}, nil } @@ -221,14 +220,14 @@ func (s *Server) GetTdByNumber(ctx context.Context, req *protobor.GetTdByNumberR return nil, err } if header == nil { - return nil, errors.New("header not found") + return nil, status.Error(codes.NotFound, "header not found") } td := s.backend.APIBackend.GetTd(ctx, header.Hash()) if td == nil { - return nil, errors.New("total difficulty not found") + return nil, status.Error(codes.NotFound, "total difficulty not found") } if !td.IsUint64() { - return nil, errors.New("total difficulty overflows uint64") + return nil, status.Error(codes.OutOfRange, "total difficulty overflows uint64") } return &protobor.GetTdResponse{TotalDifficulty: td.Uint64()}, nil } @@ -296,7 +295,7 @@ func (s *Server) fetchBlockInfo(ctx context.Context, blockNum uint64) (*protobor return nil, false, nil } if !td.IsUint64() { - return nil, false, errors.New("total difficulty overflows uint64") + return nil, false, status.Error(codes.OutOfRange, "total difficulty overflows uint64") } info := &protobor.BlockInfo{ @@ -332,10 +331,10 @@ func getRpcBlockNumberFromString(blockNumber string) (rpc.BlockNumber, error) { default: blckNum, err := hexutil.DecodeUint64(blockNumber) if err != nil { - return rpc.BlockNumber(0), errors.New("invalid block number") + return rpc.BlockNumber(0), status.Error(codes.InvalidArgument, "invalid block number") } if blckNum > math.MaxInt64 { - return rpc.BlockNumber(0), errors.New("block number out of range") + return rpc.BlockNumber(0), status.Error(codes.InvalidArgument, "block number out of range") } return rpc.BlockNumber(blckNum), nil } diff --git a/internal/cli/server/grpc_auth_test.go b/internal/cli/server/grpc_auth_test.go index 36818870cd..db48c15ecb 100644 --- a/internal/cli/server/grpc_auth_test.go +++ b/internal/cli/server/grpc_auth_test.go @@ -65,6 +65,28 @@ func TestAuthenticate_CorrectToken(t *testing.T) { require.NoError(t, err) } +// TestAuthenticate_CaseInsensitiveBearerPrefix verifies the auth scheme name is case-insensitive. +func TestAuthenticate_CaseInsensitiveBearerPrefix(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + hdr string + }{ + {"canonical", "Bearer secret"}, + {"lowercase", "bearer secret"}, + {"uppercase", "BEARER secret"}, + {"mixed-case", "BeArEr secret"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", tc.hdr)) + require.NoError(t, authenticate(ctx, "secret")) + }) + } +} + // TestAuthenticate_ConstantTimeCompare verifies that both a close-miss token and // a completely different token both return Unauthenticated (no behavioral // difference based on byte position — the unit test checks that both fail). From 5d681d7fd4b3d17810013b931ff2d956f3b7608f Mon Sep 17 00:00:00 2001 From: marcello33 Date: Thu, 23 Apr 2026 21:17:12 +0200 Subject: [PATCH 6/7] address comments --- consensus/bor/bor.go | 4 +++- internal/cli/server/api_service.go | 5 ++++- internal/cli/server/command.go | 11 ++++++++--- internal/cli/server/utils.go | 12 ++++++++++-- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 409ff25c5f..43cf637a0a 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1537,7 +1537,9 @@ func (c *Bor) APIs(chain consensus.ChainHeaderReader) []rpc.API { c.apiOnce.Do(func() { a := &API{chain: chain, bor: c} if err := a.initializeRootHashCache(); err != nil { - panic(fmt.Errorf("bor: failed to initialize rootHashCache: %w", err)) + // log.Crit logs at the highest severity and then exits the process; + // This is currently unreachable (size is a constant in initializeRootHashCache), + log.Crit("bor: failed to initialize rootHashCache", "err", err) } c.api = a }) diff --git a/internal/cli/server/api_service.go b/internal/cli/server/api_service.go index 38269707e9..b3a008497c 100644 --- a/internal/cli/server/api_service.go +++ b/internal/cli/server/api_service.go @@ -139,7 +139,10 @@ func (s *Server) TransactionReceipt(ctx context.Context, req *protobor.ReceiptRe if err != nil { return nil, err } - _, _, blockHash, _, txnIndex := s.backend.APIBackend.GetTransaction(txHash) + found, _, blockHash, _, txnIndex := s.backend.APIBackend.GetTransaction(txHash) + if !found { + return nil, status.Error(codes.NotFound, "transaction not found") + } receipts, err := s.backend.APIBackend.GetReceipts(ctx, blockHash) if err != nil { diff --git a/internal/cli/server/command.go b/internal/cli/server/command.go index 0ca5a51486..da149ab4f6 100644 --- a/internal/cli/server/command.go +++ b/internal/cli/server/command.go @@ -135,9 +135,14 @@ func (c *Command) extractFlags(args []string) error { c.cliConfig.Cache.TxLookupLimit = handleTxLookupLimitFlag(tomlConfig, args, c.cliConfig) // Env-var fallback for the gRPC auth token. - if c.cliConfig.GRPC != nil && c.cliConfig.GRPC.Token == "" { - if envTok := os.Getenv("BOR_GRPC_TOKEN"); envTok != "" { - c.cliConfig.GRPC.Token = envTok + if c.cliConfig.GRPC != nil { + if c.cliConfig.GRPC.Token == "" { + if envTok := os.Getenv("BOR_GRPC_TOKEN"); envTok != "" { + c.cliConfig.GRPC.Token = envTok + } + } else { + // Warn when the token was supplied via --grpc.token or flag + log.Warn("grpc.token sourced from CLI/TOML config — prefer the BOR_GRPC_TOKEN env var to avoid exposing the token") } } diff --git a/internal/cli/server/utils.go b/internal/cli/server/utils.go index 192a602100..4d2e984b60 100644 --- a/internal/cli/server/utils.go +++ b/internal/cli/server/utils.go @@ -61,6 +61,14 @@ func ConvertTopicsToProtoTopics(topics []common.Hash) []*protocommon.H256 { } func ConvertReceiptToProtoReceipt(receipt *types.Receipt) *protobor.Receipt { + var egp int64 + if receipt.EffectiveGasPrice != nil { + egp = receipt.EffectiveGasPrice.Int64() + } + var blockNum int64 + if receipt.BlockNumber != nil { + blockNum = receipt.BlockNumber.Int64() + } return &protobor.Receipt{ Type: uint64(receipt.Type), PostState: receipt.PostState, @@ -71,10 +79,10 @@ func ConvertReceiptToProtoReceipt(receipt *types.Receipt) *protobor.Receipt { TxHash: protoutil.ConvertHashToH256(receipt.TxHash), ContractAddress: protoutil.ConvertAddressToH160(receipt.ContractAddress), GasUsed: receipt.GasUsed, - EffectiveGasPrice: receipt.EffectiveGasPrice.Int64(), + EffectiveGasPrice: egp, BlobGasUsed: receipt.BlobGasUsed, BlockHash: protoutil.ConvertHashToH256(receipt.BlockHash), - BlockNumber: receipt.BlockNumber.Int64(), + BlockNumber: blockNum, TransactionIndex: uint64(receipt.TransactionIndex), } } From a7ebf678d2e70ab354227135a9f7ff263f521698 Mon Sep 17 00:00:00 2001 From: marcello33 Date: Thu, 23 Apr 2026 22:39:31 +0200 Subject: [PATCH 7/7] revert tracers/data.csv --- eth/tracers/data.csv | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/eth/tracers/data.csv b/eth/tracers/data.csv index 7d9606fa4d..008be17eda 100644 --- a/eth/tracers/data.csv +++ b/eth/tracers/data.csv @@ -1,40 +1,40 @@ TransactionIndex, Incarnation, VersionTxIdx, VersionInc, Path, Operation +0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read -0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read +0 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 0 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 0 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 0 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write -0 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write +1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read -1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read -1 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write -1 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 1 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 1 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write -2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read -2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read +1 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write +1 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read -2 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write +2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read +2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 2 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 2 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 2 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write -3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read +2 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read +3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 3 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 3 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 3 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 3 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write +4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read -4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 4 , 0, 4 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 4 , 0, 4 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 4 , 0, 4 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write