Skip to content

access-list, call, header, filter, and transaction-signing endpoints diverge from the documented JSON-RPC schema, defaults, and field semantics #35546

Description

@BenWhite713

1. eth_createAccessList: The access-list orchestration excludes recipients or created contracts and EIP-7702 authorities in addition to the documented sender/precompile exceptions

  • Statement: The access-list orchestration excludes recipients or created contracts and EIP-7702 authorities in addition to the documented sender/precompile exceptions.
  • URL: https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth
  • Code location:
    • internal/ethapi/api.go:1328-1345
      func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrHash, args TransactionArgs, stateOverrides *override.StateOverride) (acl types.AccessList, gasUsed uint64, vmErr error, err error) {
      // Retrieve the execution context
      db, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
      if db == nil || err != nil {
      return nil, 0, nil, err
      }
      // Apply state overrides immediately after StateAndHeaderByNumberOrHash.
      // If not applied here, there could be cases where user-specified overrides (e.g., nonce)
      // may conflict with default values from the database, leading to inconsistencies.
      if stateOverrides != nil {
      if err := stateOverrides.Apply(db, nil); err != nil {
      return nil, 0, nil, err
      }
      }
      // Ensure any missing fields are filled, extract the recipient and input data
      if err = args.setFeeDefaults(ctx, b, header); err != nil {
    • eth/tracers/logger/access_list_tracer.go:112-127
      func NewAccessListTracer(acl types.AccessList, addressesToExclude map[common.Address]struct{}) *AccessListTracer {
      list := newAccessList()
      for _, al := range acl {
      if _, ok := addressesToExclude[al.Address]; ok {
      continue
      }
      list.addAddress(al.Address)
      for _, slot := range al.StorageKeys {
      list.addSlot(al.Address, slot)
      }
      }
      return &AccessListTracer{
      excl: addressesToExclude,
      list: list,
      }
      }
  • Description: Root cause — NewAccessListTracer is constructed with an addressesToExclude set, and its AccessList()-building loop skips any address present in that set before adding it to the result; AccessList (the orchestration function backing the endpoint) populates that exclusion set with more than just the sender and precompiles — it also adds the transaction's recipient/created-contract address and any EIP-7702 authorization authorities. The exclusion mechanism therefore has a broader scope by design than the documented "sender account and precompiles" exception list.
  • Method: eth_createAccessList

2. eth_createAccessList: eth_createAccessList accepts BlockNumberOrHash selector encodings beyond an Object-only parameter schema

  • Statement: eth_createAccessList accepts BlockNumberOrHash selector encodings beyond an Object-only parameter schema.
  • URL: https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth
  • Code location:
    • internal/ethapi/api.go:1328-1345
      func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrHash, args TransactionArgs, stateOverrides *override.StateOverride) (acl types.AccessList, gasUsed uint64, vmErr error, err error) {
      // Retrieve the execution context
      db, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
      if db == nil || err != nil {
      return nil, 0, nil, err
      }
      // Apply state overrides immediately after StateAndHeaderByNumberOrHash.
      // If not applied here, there could be cases where user-specified overrides (e.g., nonce)
      // may conflict with default values from the database, leading to inconsistencies.
      if stateOverrides != nil {
      if err := stateOverrides.Apply(db, nil); err != nil {
      return nil, 0, nil, err
      }
      }
      // Ensure any missing fields are filled, extract the recipient and input data
      if err = args.setFeeDefaults(ctx, b, header); err != nil {
    • internal/ethapi/api.go:1244-1258
      func (api *BlockChainAPI) CreateAccessList(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, stateOverrides *override.StateOverride) (*accessListResult, error) {
      bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
      if blockNrOrHash != nil {
      bNrOrHash = *blockNrOrHash
      }
      acl, gasUsed, vmerr, err := AccessList(ctx, api.b, bNrOrHash, args, stateOverrides)
      if err != nil {
      return nil, err
      }
      result := &accessListResult{Accesslist: &acl, GasUsed: hexutil.Uint64(gasUsed)}
      if vmerr != nil {
      result.Error = vmerr.Error()
      }
      return result, nil
      }
  • Description: Root cause — BlockChainAPI.CreateAccessList declares its optional third parameter as *rpc.BlockNumberOrHash, a union type whose JSON unmarshaler accepts either an object ({"blockNumber": ...} / {"blockHash": ...}) or a bare quantity/tag string, and defaults to rpc.LatestBlockNumber when the parameter is omitted entirely. Because the decoder is the shared BlockNumberOrHash type used across the API rather than an object-only schema, string/tag encodings are accepted in addition to the documented object form.
  • Method: eth_createAccessList

3. eth_call: eth_call accepts an omitted block selector and defaults it to latest rather than requiring the parameter

  • Statement: eth_call accepts an omitted block selector and defaults it to latest rather than requiring the parameter.
  • URL: https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth
  • Code location:
    • internal/ethapi/api.go:854-867
      func (api *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, overrides *override.StateOverride, blockOverrides *override.BlockOverrides) (hexutil.Bytes, error) {
      if blockNrOrHash == nil {
      latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
      blockNrOrHash = &latest
      }
      result, err := DoCall(ctx, api.b, args, *blockNrOrHash, overrides, blockOverrides, api.b.RPCEVMTimeout(), api.b.RPCGasCap())
      if err != nil {
      return nil, err
      }
      if errors.Is(result.Err, vm.ErrExecutionReverted) {
      return nil, newRevertError(result.Revert())
      }
      return result.Return(), result.Err
      }
    • internal/ethapi/api.go:838-846
      func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *override.StateOverride, blockOverrides *override.BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
      defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())
      state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
      if state == nil || err != nil {
      return nil, err
      }
      return doCall(ctx, b, args, state, header, overrides, blockOverrides, timeout, globalGasCap)
      }
  • Description: Root cause — BlockChainAPI.Call explicitly guards if blockNrOrHash == nil { latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber); blockNrOrHash = &latest } before delegating to DoCall; the parameter is typed as a pointer specifically so the handler can detect an omitted argument and substitute the latest block, rather than the JSON-RPC dispatcher rejecting the call for a missing required parameter.
  • Method: eth_call

4. eth_getHeaderByNumber: eth_getHeaderByNumber uses rpc.BlockNumber and accepts named block tags in addition to a Quantity-only schema

  • Statement: eth_getHeaderByNumber uses rpc.BlockNumber and accepts named block tags in addition to a Quantity-only schema.
  • URL: https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth
  • Code location:
    • internal/ethapi/api.go:494-507
      func (api *BlockChainAPI) GetHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (map[string]interface{}, error) {
      header, err := api.b.HeaderByNumber(ctx, number)
      if header != nil && err == nil {
      response := RPCMarshalHeader(header)
      if number == rpc.PendingBlockNumber {
      // Pending header need to nil out a few fields
      for _, field := range []string{"hash", "nonce", "miner"} {
      response[field] = nil
      }
      }
      return response, err
      }
      return nil, err
      }
    • rpc/types.go:80-97

      go-ethereum/rpc/types.go

      Lines 80 to 97 in 81ab8b5

      func (bn *BlockNumber) UnmarshalJSON(data []byte) error {
      input := strings.TrimSpace(string(data))
      if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
      input = input[1 : len(input)-1]
      }
      switch input {
      case "earliest":
      *bn = EarliestBlockNumber
      return nil
      case "latest":
      *bn = LatestBlockNumber
      return nil
      case "pending":
      *bn = PendingBlockNumber
      return nil
      case "finalized":
      *bn = FinalizedBlockNumber
  • Description: Root cause — BlockChainAPI.GetHeaderByNumber(ctx, number rpc.BlockNumber) decodes its parameter with BlockNumber.UnmarshalJSON, which explicitly recognizes the string tags "earliest", "latest", "pending", and "finalized" (in addition to numeric quantities) and maps each to a sentinel value before any lookup happens. The parameter's real schema is therefore the tag-or-quantity union used throughout the RPC layer, not the Quantity-only schema the endpoint documentation states.
  • Method: eth_getHeaderByNumber

5. eth_getHeaderByNumber: eth_getHeaderByNumber documentation includes a size field that RPCMarshalHeader does not emit

  • Statement: eth_getHeaderByNumber documentation includes a size field that RPCMarshalHeader does not emit.
  • URL: https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth
  • Code location:
    • internal/ethapi/api.go:494-507
      func (api *BlockChainAPI) GetHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (map[string]interface{}, error) {
      header, err := api.b.HeaderByNumber(ctx, number)
      if header != nil && err == nil {
      response := RPCMarshalHeader(header)
      if number == rpc.PendingBlockNumber {
      // Pending header need to nil out a few fields
      for _, field := range []string{"hash", "nonce", "miner"} {
      response[field] = nil
      }
      }
      return response, err
      }
      return nil, err
      }
    • internal/ethapi/api.go:984-1001
      func RPCMarshalHeader(head *types.Header) map[string]interface{} {
      result := map[string]interface{}{
      "number": (*hexutil.Big)(head.Number),
      "hash": head.Hash(),
      "parentHash": head.ParentHash,
      "nonce": head.Nonce,
      "mixHash": head.MixDigest,
      "sha3Uncles": head.UncleHash,
      "logsBloom": head.Bloom,
      "stateRoot": head.Root,
      "miner": head.Coinbase,
      "difficulty": (*hexutil.Big)(head.Difficulty),
      "extraData": hexutil.Bytes(head.Extra),
      "gasLimit": hexutil.Uint64(head.GasLimit),
      "gasUsed": hexutil.Uint64(head.GasUsed),
      "timestamp": hexutil.Uint64(head.Time),
      "transactionsRoot": head.TxHash,
      "receiptsRoot": head.ReceiptHash,
  • Description: Root cause — GetHeaderByNumber returns exactly the map built by RPCMarshalHeader, and that function's field list (number, hash, parentHash, nonce, mixHash, sha3Uncles, logsBloom, stateRoot, miner, difficulty, extraData, gasLimit, gasUsed, timestamp, transactionsRoot, receiptsRoot, …) never assigns a size key — block/header size is a serialization-time property of a full RLP-encoded block, not of a header struct in isolation, so no code path computes it here. The documented size field describes block-level output, not what this header-only marshaler produces.
  • Method: eth_getHeaderByNumber

6. eth_getLogs / eth_newFilter: For log ranges, earliest resolves to a historical/pruning-cutoff block rather than transactions not yet in a block

  • Statement: For log ranges, earliest resolves to a historical/pruning-cutoff block rather than transactions not yet in a block.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • eth/filters/filter_system.go:325-342
      return nil, errPendingLogsUnsupported
      }
      if from == rpc.EarliestBlockNumber {
      from = rpc.BlockNumber(es.backend.HistoryPruningCutoff())
      }
      // Queries beyond the pruning cutoff are not supported.
      if uint64(from) < es.backend.HistoryPruningCutoff() {
      return nil, &history.PrunedHistoryError{}
      }
      // only interested in new mined logs
      if from == rpc.LatestBlockNumber && to == rpc.LatestBlockNumber {
      return es.subscribeLogs(crit, logs), nil
      }
      // only interested in mined logs within a specific block range
      if from >= 0 && to >= 0 && to >= from {
      return es.subscribeLogs(crit, logs), nil
    • eth/filters/api.go:480-497
      // Block numbers below 0 are special cases.
      if begin > 0 && end > 0 && begin > end {
      return nil, errInvalidBlockRange
      }
      if begin >= 0 && begin < int64(api.events.backend.HistoryPruningCutoff()) {
      return nil, &history.PrunedHistoryError{}
      }
      // Construct the range filter
      filter = api.sys.NewRangeFilter(begin, end, crit.Addresses, crit.Topics, api.rangeLimit)
      }
      // Run the filter and return all the logs
      logs, err := filter.Logs(ctx)
      if err != nil {
      return nil, err
      }
      return returnLogs(logs), err
      }
  • Description: Root cause — both EventSystem.SubscribeLogs and FilterAPI.GetLogs translate a from selector equal to rpc.EarliestBlockNumber into es.backend.HistoryPruningCutoff() (the oldest block number the node still retains history for), then reject the query outright with a PrunedHistoryError if the resolved range still falls below that cutoff. earliest is thus wired to mean "the oldest historically available mined block," the opposite semantic from "transactions not yet included in a block," which the shared JSON-RPC spec's fromBlock/toBlock wording assigns to pending/earliest in some tag descriptions.
  • Method: eth_getLogs, eth_newFilter

7. eth_getLogs / eth_newFilter: Pending log-range endpoints are rejected rather than returning the documented unmined-transaction range semantics

  • Statement: Pending log-range endpoints are rejected rather than returning the documented unmined-transaction range semantics.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • eth/filters/filter_system.go:301-318
      if es.sys.cfg.LogQueryLimit != 0 {
      if len(crit.Addresses) > es.sys.cfg.LogQueryLimit {
      return nil, errExceedLogQueryLimit
      }
      for _, topics := range crit.Topics {
      if len(topics) > es.sys.cfg.LogQueryLimit {
      return nil, errExceedLogQueryLimit
      }
      }
      }
      var from, to rpc.BlockNumber
      if crit.FromBlock == nil {
      from = rpc.LatestBlockNumber
      } else {
      from = rpc.BlockNumber(crit.FromBlock.Int64())
      }
      if crit.ToBlock == nil {
      to = rpc.LatestBlockNumber
    • accounts/abi/bind/v2/base.go:485-502
      */
      buff, err := c.filterer.FilterLogs(ensureContext(opts.Context), config)
      if err != nil {
      return nil, nil, err
      }
      sub := event.NewSubscription(func(quit <-chan struct{}) error {
      for _, log := range buff {
      select {
      case logs <- log:
      case <-quit:
      return nil
      }
      }
      return nil
      })
      return logs, sub, nil
      }
  • Description: Root cause — EventSystem.SubscribeLogs resolves fromBlock/toBlock into concrete rpc.BlockNumber values and only proceeds along the "mined logs" subscription path (from == rpc.LatestBlockNumber && to == rpc.LatestBlockNumber, or an explicit non-negative range); a pending selector on either end of the range falls outside both accepted branches and returns errPendingLogsUnsupported before any log matching runs. No code path assembles a result set out of unmined/pending transactions for a range query, so the documented "pending range returns unmined-transaction logs" behavior has no implementation to back it.
  • Method: eth_getLogs, eth_newFilter

8. eth_newFilter: eth_newFilter does not support safe/finalized range sentinels despite the documented selector availability and semantics

  • Statement: eth_newFilter does not support safe/finalized range sentinels despite the documented selector availability and semantics.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • eth/filters/api.go:465-482
      return nil, errBlockHashWithRange
      }
      // Block filter requested, construct a single-shot filter
      filter = api.sys.NewBlockFilter(*crit.BlockHash, crit.Addresses, crit.Topics)
      } else {
      // Convert the RPC block numbers into internal representations
      begin := rpc.LatestBlockNumber.Int64()
      if crit.FromBlock != nil {
      begin = crit.FromBlock.Int64()
      }
      end := rpc.LatestBlockNumber.Int64()
      if crit.ToBlock != nil {
      end = crit.ToBlock.Int64()
      }
      // Block numbers below 0 are special cases.
      if begin > 0 && end > 0 && begin > end {
      return nil, errInvalidBlockRange
    • accounts/abi/bind/v2/base.go:461-478
      func (c *BoundContract) FilterLogs(opts *FilterOpts, name string, query ...[]any) (chan types.Log, event.Subscription, error) {
      // Don't crash on a lazy user
      if opts == nil {
      opts = new(FilterOpts)
      }
      // Append the event selector to the query parameters and construct the topic set
      query = append([][]any{{c.abi.Events[name].ID}}, query...)
      topics, err := abi.MakeTopics(query...)
      if err != nil {
      return nil, nil, err
      }
      // Start the background filtering
      logs := make(chan types.Log, 128)
      config := ethereum.FilterQuery{
      Addresses: []common.Address{c.address},
      Topics: topics,
      FromBlock: new(big.Int).SetUint64(opts.Start),
  • Description: Root cause — FilterAPI.GetLogs's range-construction branch derives begin/end from rpc.LatestBlockNumber.Int64() unless the caller supplied a FromBlock/ToBlock, and the only special-cased values checked afterward are negative sentinels tied to latest/pending/earliest; there is no branch that resolves a safe or finalized tag to the chain's current safe/finalized head. The filter-construction code simply has no lookup for those two tags in the range path, so a request using them either falls through to an unintended interpretation or is rejected, contrary to the documented tag support.
  • Method: eth_newFilter

9. eth_newPendingTransactionFilter: eth_newPendingTransactionFilter has an optional fullTx parameter despite documentation saying it has none

  • Statement: eth_newPendingTransactionFilter has an optional fullTx parameter despite documentation saying it has none.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • eth/filters/api.go:149-166
      func (api *FilterAPI) NewPendingTransactionFilter(fullTx *bool) rpc.ID {
      var (
      pendingTxs = make(chan []*types.Transaction)
      pendingTxSub = api.events.SubscribePendingTxs(pendingTxs)
      )
      api.filtersMu.Lock()
      api.filters[pendingTxSub.ID] = &filter{typ: PendingTransactionsSubscription, fullTx: fullTx != nil && *fullTx, deadline: time.NewTimer(api.timeout), txs: make([]*types.Transaction, 0), s: pendingTxSub}
      api.filtersMu.Unlock()
      go func() {
      defer pendingTxSub.Unsubscribe()
      for {
      select {
      case pTx := <-pendingTxs:
      api.filtersMu.Lock()
      if f, found := api.filters[pendingTxSub.ID]; found {
      f.txs = append(f.txs, pTx...)
    • eth/filters/filter_system.go:388-401
      func (es *EventSystem) SubscribePendingTxs(txs chan []*types.Transaction) *Subscription {
      sub := &subscription{
      id: rpc.NewID(),
      typ: PendingTransactionsSubscription,
      created: time.Now(),
      logs: make(chan []*types.Log),
      txs: txs,
      headers: make(chan *types.Header),
      receipts: make(chan []*ReceiptWithTx),
      installed: make(chan struct{}),
      err: make(chan error),
      }
      return es.subscribe(sub)
      }
  • Description: Root cause — FilterAPI.NewPendingTransactionFilter(fullTx *bool) rpc.ID accepts an optional boolean pointer and stores fullTx: fullTx != nil && *fullTx on the created filter, which later controls whether subsequent getFilterChanges polls return full transaction objects or bare hashes for that filter. The parameter is real and functional in the handler's signature; the documentation describing the method as taking no arguments does not reflect this optional flag.
  • Method: eth_newPendingTransactionFilter

10. eth_getBlockByHash / eth_getBlockByNumber / eth_getHeaderByNumber: Current RPC header/block marshaling omits totalDifficulty, although the documented block/header schemas and examples still require it

  • Statement: Current RPC header/block marshaling omits totalDifficulty, although the documented block/header schemas and examples still require it.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • internal/ethapi/api.go:983-1028
      // RPCMarshalHeader converts the given header to the RPC output .
      func RPCMarshalHeader(head *types.Header) map[string]interface{} {
      result := map[string]interface{}{
      "number": (*hexutil.Big)(head.Number),
      "hash": head.Hash(),
      "parentHash": head.ParentHash,
      "nonce": head.Nonce,
      "mixHash": head.MixDigest,
      "sha3Uncles": head.UncleHash,
      "logsBloom": head.Bloom,
      "stateRoot": head.Root,
      "miner": head.Coinbase,
      "difficulty": (*hexutil.Big)(head.Difficulty),
      "extraData": hexutil.Bytes(head.Extra),
      "gasLimit": hexutil.Uint64(head.GasLimit),
      "gasUsed": hexutil.Uint64(head.GasUsed),
      "timestamp": hexutil.Uint64(head.Time),
      "transactionsRoot": head.TxHash,
      "receiptsRoot": head.ReceiptHash,
      }
      if head.BaseFee != nil {
      result["baseFeePerGas"] = (*hexutil.Big)(head.BaseFee)
      }
      if head.WithdrawalsHash != nil {
      result["withdrawalsRoot"] = head.WithdrawalsHash
      }
      if head.BlobGasUsed != nil {
      result["blobGasUsed"] = hexutil.Uint64(*head.BlobGasUsed)
      }
      if head.ExcessBlobGas != nil {
      result["excessBlobGas"] = hexutil.Uint64(*head.ExcessBlobGas)
      }
      if head.ParentBeaconRoot != nil {
      result["parentBeaconBlockRoot"] = head.ParentBeaconRoot
      }
      if head.RequestsHash != nil {
      result["requestsHash"] = head.RequestsHash
      }
      if head.BlockAccessListHash != nil {
      result["blockAccessListHash"] = head.BlockAccessListHash
      }
      if head.SlotNumber != nil {
      result["slotNumber"] = hexutil.Uint64(*head.SlotNumber)
      }
      return result
      }
    • internal/ethapi/api.go:1030-1036
      // RPCMarshalBlock converts the given block to the RPC output which depends on fullTx. If inclTx is true transactions are
      // returned. When fullTx is true the returned block contains full transaction details, otherwise it will only contain
      // transaction hashes.
      func RPCMarshalBlock(block *types.Block, inclTx bool, fullTx bool, config *params.ChainConfig) map[string]interface{} {
      fields := RPCMarshalHeader(block.Header())
      fields["size"] = hexutil.Uint64(block.Size())
  • Description: Root cause — RPCMarshalHeader, the single function that builds the JSON map for eth_getHeaderByNumber and (via RPCMarshalBlock, which calls it and layers on block-level fields like size) for eth_getBlockByHash/eth_getBlockByNumber, populates a fixed field list — number through receiptsRoot, plus conditional post-merge/EIP fields such as baseFeePerGas, withdrawalsRoot, blobGasUsed, excessBlobGas, parentBeaconBlockRoot, requestsHash, blockAccessListHash, and slotNumber — that never queries or attaches a chain-cumulative total-difficulty value, which would require an extra database lookup outside the header struct itself. Post-merge chains have a constant, uninformative total difficulty, so the field's computation and inclusion were dropped from both marshalers, leaving the documented schema and examples (written for the pre-merge, difficulty-bearing chain) stale across all three endpoints.
  • Method: eth_getBlockByHash, eth_getBlockByNumber, eth_getHeaderByNumber

11. eth_sendTransaction: eth_sendTransaction selects contract creation from a nil to address, not solely from data containing code

  • Statement: eth_sendTransaction selects contract creation from a nil to address, not solely from data containing code.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • core/state_transition.go:666-683
      func (st *stateTransition) execute() (*ExecutionResult, error) {
      var (
      msg = st.msg
      rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time)
      contractCreation = msg.To == nil
      floorDataGas uint64
      )
      // Validate the message and pre-pay gas.
      if err := st.preCheck(rules); err != nil {
      return nil, err
      }
      // Calculate the intrinsic gas of this transaction and make sure the gas limit
      // is sufficient to cover that.
      intrinsicGas, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules)
      if err != nil {
      return nil, err
      }
      if msg.GasLimit < intrinsicGas {
    • internal/ethapi/api.go:1674-1691
      func (api *TransactionAPI) SendTransaction(ctx context.Context, args TransactionArgs) (common.Hash, error) {
      // Look up the wallet containing the requested signer
      account := accounts.Account{Address: args.from()}
      wallet, err := api.b.AccountManager().Find(account)
      if err != nil {
      return common.Hash{}, err
      }
      if args.Nonce == nil {
      // Hold the mutex around signing to prevent concurrent assignment of
      // the same nonce to multiple accounts.
      api.nonceLock.LockAddr(args.from())
      defer api.nonceLock.UnlockAddr(args.from())
      }
      if args.IsEIP4844() {
      return common.Hash{}, errBlobTxNotSupported
      }
  • Description: Root cause — stateTransition.execute computes contractCreation = msg.To == nil as the sole condition that routes execution down the contract-creation path; the presence or shape of msg.Data/init code plays no role in that branch decision. TransactionAPI.SendTransaction passes the caller's args.To straight through unchanged, so whether a submitted transaction is treated as a creation is determined entirely by omitting/nulling the to field, not by inspecting whether the supplied data looks like contract bytecode.
  • Method: eth_sendTransaction

12. eth_sendTransaction: eth_sendTransaction dynamically estimates omitted gas rather than applying the documented fixed 90000 default

  • Statement: eth_sendTransaction dynamically estimates omitted gas rather than applying the documented fixed 90000 default.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • internal/ethapi/transaction_args.go:103-120
      func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, config sidecarConfig) error {
      if err := args.setBlobTxSidecar(ctx, config); err != nil {
      return err
      }
      if err := args.setFeeDefaults(ctx, b, b.CurrentHeader()); err != nil {
      return err
      }
      if args.Value == nil {
      args.Value = new(hexutil.Big)
      }
      if args.Nonce == nil {
      nonce, err := b.GetPoolNonce(ctx, args.from())
      if err != nil {
      return err
      }
      args.Nonce = (*hexutil.Uint64)(&nonce)
      }
    • internal/ethapi/api.go:1674-1691
      func (api *TransactionAPI) SendTransaction(ctx context.Context, args TransactionArgs) (common.Hash, error) {
      // Look up the wallet containing the requested signer
      account := accounts.Account{Address: args.from()}
      wallet, err := api.b.AccountManager().Find(account)
      if err != nil {
      return common.Hash{}, err
      }
      if args.Nonce == nil {
      // Hold the mutex around signing to prevent concurrent assignment of
      // the same nonce to multiple accounts.
      api.nonceLock.LockAddr(args.from())
      defer api.nonceLock.UnlockAddr(args.from())
      }
      if args.IsEIP4844() {
      return common.Hash{}, errBlobTxNotSupported
      }
  • Description: Root cause — TransactionArgs.setDefaults, invoked on the path from SendTransaction, fills in Value and Nonce when absent but leaves Gas to a separate gas-estimation routine (DoEstimateGas-style simulation against current state) rather than assigning a hardcoded constant; no literal 90000 value appears anywhere in the default-filling code. The documented fixed default describes an old, static-default behavior that the current implementation replaced with a dynamic, per-call gas estimate.
  • Method: eth_sendTransaction

13. eth_sendTransaction: eth_sendTransaction returns the submitted transaction hash on success and pairs a zero hash only with an error, not as a successful "unavailable" sentinel

  • Statement: eth_sendTransaction returns the submitted transaction hash on success and pairs a zero hash only with an error, not as a successful "unavailable" sentinel.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • internal/ethapi/api.go:1640-1657
      func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) {
      // If the transaction fee cap is already specified, ensure the
      // fee of the given transaction is _reasonable_.
      if err := checkTxFee(tx.GasPrice(), tx.Gas(), b.RPCTxFeeCap()); err != nil {
      return common.Hash{}, err
      }
      if !b.UnprotectedAllowed() && !tx.Protected() {
      // Ensure only eip155 signed transactions are submitted if EIP155Required is set.
      return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC")
      }
      if err := b.SendTx(ctx, tx); err != nil {
      return common.Hash{}, err
      }
      // Print a log with full tx details for manual investigations and interventions
      head := b.CurrentBlock()
      signer := types.MakeSigner(b.ChainConfig(), head.Number, head.Time)
      from, err := types.Sender(signer, tx)
      if err != nil {
    • internal/ethapi/api.go:1674-1691
      func (api *TransactionAPI) SendTransaction(ctx context.Context, args TransactionArgs) (common.Hash, error) {
      // Look up the wallet containing the requested signer
      account := accounts.Account{Address: args.from()}
      wallet, err := api.b.AccountManager().Find(account)
      if err != nil {
      return common.Hash{}, err
      }
      if args.Nonce == nil {
      // Hold the mutex around signing to prevent concurrent assignment of
      // the same nonce to multiple accounts.
      api.nonceLock.LockAddr(args.from())
      defer api.nonceLock.UnlockAddr(args.from())
      }
      if args.IsEIP4844() {
      return common.Hash{}, errBlobTxNotSupported
      }
  • Description: Root cause — every early-return in SubmitTransaction that yields common.Hash{} (the zero hash) is paired with a non-nil error value (e.g. fee-cap rejection, unprotected-transaction rejection, SendTx failure), and the only path that reaches the final success return computes and returns tx.Hash(), the real submitted transaction's hash. There is no branch in SubmitTransaction or TransactionAPI.SendTransaction that returns a zero hash together with a nil error, so a zero hash can never be read as a successful "hash unavailable" sentinel — it always accompanies a request failure.
  • Method: eth_sendTransaction

14. eth_signTransaction: eth_signTransaction requires gas and does not supply the documented 90000 default

  • Statement: eth_signTransaction requires gas and does not supply the documented 90000 default.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • internal/ethapi/api.go:944-961
      State: state,
      BlobBaseFee: blobBaseFee,
      ErrorRatio: estimateGasErrorRatio,
      }
      // Set any required transaction default, but make sure the gas cap itself is not messed with
      // if it was not specified in the original argument list.
      if args.Gas == nil {
      args.Gas = new(hexutil.Uint64)
      }
      if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID); err != nil {
      return 0, err
      }
      call := args.ToMessage(header.BaseFee, true)
      // Run the gas estimation and wrap any revertals into a custom return
      estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap)
      if err != nil {
      if errors.Is(err, vm.ErrExecutionReverted) {
    • internal/ethapi/api.go:1890-1907
      func (api *TransactionAPI) SignTransaction(ctx context.Context, args TransactionArgs) (*SignTransactionResult, error) {
      if args.Gas == nil {
      return nil, errors.New("gas not specified")
      }
      if args.GasPrice == nil && (args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil) {
      return nil, errors.New("missing gasPrice or maxFeePerGas/maxPriorityFeePerGas")
      }
      if args.Nonce == nil {
      return nil, errors.New("nonce not specified")
      }
      sidecarVersion := types.BlobSidecarVersion0
      if len(args.Blobs) > 0 {
      h := api.b.CurrentHeader()
      if api.b.ChainConfig().IsOsaka(h.Number, h.Time) {
      sidecarVersion = types.BlobSidecarVersion1
      }
      }
  • Description: Root cause — TransactionAPI.SignTransaction opens with if args.Gas == nil { return nil, errors.New("gas not specified") }, an unconditional early error rather than a default assignment; this is distinct from DoEstimateGas's helper path (used by other endpoints), which does allocate a zero-valued args.Gas before estimating. Because SignTransaction never reaches any gas-filling logic, a caller omitting gas is rejected outright instead of receiving the documented fixed 90000 fallback.
  • Method: eth_signTransaction

15. eth_signTransaction: eth_signTransaction rejects a missing nonce, contrary to documentation that marks nonce optional

  • Statement: eth_signTransaction rejects a missing nonce, contrary to documentation that marks nonce optional.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • internal/ethapi/api.go:1890-1907
      func (api *TransactionAPI) SignTransaction(ctx context.Context, args TransactionArgs) (*SignTransactionResult, error) {
      if args.Gas == nil {
      return nil, errors.New("gas not specified")
      }
      if args.GasPrice == nil && (args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil) {
      return nil, errors.New("missing gasPrice or maxFeePerGas/maxPriorityFeePerGas")
      }
      if args.Nonce == nil {
      return nil, errors.New("nonce not specified")
      }
      sidecarVersion := types.BlobSidecarVersion0
      if len(args.Blobs) > 0 {
      h := api.b.CurrentHeader()
      if api.b.ChainConfig().IsOsaka(h.Number, h.Time) {
      sidecarVersion = types.BlobSidecarVersion1
      }
      }
    • internal/ethapi/transaction_args.go:132-149
      // create check
      if args.To == nil {
      if args.BlobHashes != nil {
      return errors.New(`missing "to" in blob transaction`)
      }
      if len(args.data()) == 0 {
      return errors.New(`contract creation without any data provided`)
      }
      if len(args.AuthorizationList) > 0 {
      return errors.New(`authorizationList provided for contract creation, but "to" field is missing`)
      }
      }
      if args.Gas == nil {
      // These fields are immutable during the estimation, safe to
      // pass the pointer directly.
      data := args.data()
  • Description: Root cause — TransactionAPI.SignTransaction contains an explicit if args.Nonce == nil { return nil, errors.New("nonce not specified") } guard before any signing occurs; unlike SendTransaction, which auto-fills a missing nonce from the pending pool state under a per-address lock, SignTransaction has no equivalent nonce-defaulting branch. The endpoint therefore treats nonce as a hard requirement rather than the optional, auto-assigned field the documentation describes.
  • Method: eth_signTransaction

16. eth_signTransaction: eth_signTransaction returns a structured SignTransactionResult; RLP bytes are only its Raw field, not a top-level DATA/RLP result

  • Statement: eth_signTransaction returns a structured SignTransactionResult; RLP bytes are only its Raw field, not a top-level DATA/RLP result.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • core/types/transaction.go:133-140
      func (tx *Transaction) MarshalBinary() ([]byte, error) {
      if tx.Type() == LegacyTxType {
      return rlp.EncodeToBytes(tx.inner)
      }
      var buf bytes.Buffer
      err := tx.encodeTyped(&buf)
      return buf.Bytes(), err
      }
    • internal/ethapi/api.go:1918-1935
      return nil, err
      }
      signed, err := api.sign(args.from(), tx)
      if err != nil {
      return nil, err
      }
      // If the transaction-to-sign was a blob transaction, then the signed one
      // no longer retains the blobs, only the blob hashes. In this step, we need
      // to put back the blob(s).
      if args.IsEIP4844() {
      signed = signed.WithBlobTxSidecar(types.NewBlobTxSidecar(sidecarVersion, args.Blobs, args.Commitments, args.Proofs))
      }
      data, err := signed.MarshalBinary()
      if err != nil {
      return nil, err
      }
      return &SignTransactionResult{data, signed}, nil
      }
  • Description: Root cause — SignTransaction obtains the encoded bytes via signed.MarshalBinary() (which RLP/typed-encodes the transaction) and then returns &SignTransactionResult{data, signed}, nil — a two-field struct pairing the raw bytes with the structured, signed transaction object — rather than returning data alone. The JSON-RPC response is therefore always the SignTransactionResult object, with the RLP bytes reachable only under its raw/Raw key, not as a bare top-level DATA value as some documentation phrasing implies.
  • Method: eth_signTransaction

17. eth_signTransaction: eth_signTransaction does not consume a transaction-type input field; transaction type is inferred from other fields

  • Statement: eth_signTransaction does not consume a transaction-type input field; transaction type is inferred from other fields.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • internal/ethapi/api.go:1890-1907
      func (api *TransactionAPI) SignTransaction(ctx context.Context, args TransactionArgs) (*SignTransactionResult, error) {
      if args.Gas == nil {
      return nil, errors.New("gas not specified")
      }
      if args.GasPrice == nil && (args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil) {
      return nil, errors.New("missing gasPrice or maxFeePerGas/maxPriorityFeePerGas")
      }
      if args.Nonce == nil {
      return nil, errors.New("nonce not specified")
      }
      sidecarVersion := types.BlobSidecarVersion0
      if len(args.Blobs) > 0 {
      h := api.b.CurrentHeader()
      if api.b.ChainConfig().IsOsaka(h.Number, h.Time) {
      sidecarVersion = types.BlobSidecarVersion1
      }
      }
    • internal/ethapi/transaction_args.go:118-135
      }
      args.Nonce = (*hexutil.Uint64)(&nonce)
      }
      if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
      return errors.New(`both "data" and "input" are set and not equal. Please use "input" to pass transaction call data`)
      }
      // BlobTx fields
      if args.BlobHashes != nil && len(args.BlobHashes) == 0 {
      return errors.New("need at least 1 blob for a blob transaction")
      }
      if args.BlobHashes != nil && len(args.BlobHashes) > params.BlobTxMaxBlobs {
      return fmt.Errorf("too many blobs in transaction (have=%d, max=%d)", len(args.BlobHashes), params.BlobTxMaxBlobs)
      }
      // create check
      if args.To == nil {
      if args.BlobHashes != nil {
  • Description: Root cause — TransactionArgs.setDefaults derives the effective transaction shape from which optional fields are populated (e.g. presence of BlobHashes selects a blob transaction, presence of fee-market fields vs. GasPrice selects EIP-1559 vs. legacy, presence of AuthorizationList selects EIP-7702), with no dedicated type input read or required anywhere in the path feeding TransactionAPI.SignTransaction. The resulting transaction's type is therefore computed from which combination of other fields is set, not decoded from a caller-supplied type parameter.
  • Method: eth_signTransaction

18. eth_simulateV1: eth_simulateV1 advances an unspecified block time by a 12-second increment, not by one

  • Statement: eth_simulateV1 advances an unspecified block time by a 12-second increment, not by one.
  • URL: https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth
  • Code location:
    • internal/ethapi/simulate.go:559-576
      if overrides.BeaconRoot != nil {
      parentBeaconRoot = overrides.BeaconRoot
      }
      }
      // Set difficulty to zero if the given block is post-merge. Without this, all post-merge hardforks would remain inactive.
      // For example, calling eth_simulateV1(..., blockParameter: 0x0) on hoodi network will cause all blocks to have a difficulty of 1 and be treated as pre-merge.
      difficulty := header.Difficulty
      if sim.chainConfig.IsPostMerge(number.Uint64(), timestamp) {
      difficulty = big.NewInt(0)
      }
      header = overrides.MakeHeader(&types.Header{
      UncleHash: types.EmptyUncleHash,
      ReceiptHash: types.EmptyReceiptsHash,
      TxHash: types.EmptyTxsHash,
      Coinbase: header.Coinbase,
      Difficulty: difficulty,
      GasLimit: header.GasLimit,
      WithdrawalsHash: withdrawalsHash,
    • internal/ethapi/simulate.go:96-112
      func (r *simBlockResult) MarshalJSON() ([]byte, error) {
      blockData := RPCMarshalBlock(r.Block, true, r.fullTx, r.chainConfig)
      blockData["calls"] = r.Calls
      // Set tx sender if user requested full tx objects.
      if r.fullTx {
      if raw, ok := blockData["transactions"].([]any); ok {
      for _, tx := range raw {
      if tx, ok := tx.(*RPCTransaction); ok {
      tx.From = r.senders[tx.Hash]
      } else {
      return nil, errors.New("simulated transaction result has invalid type")
      }
      }
      }
      }
      return json.Marshal(blockData)
      }
  • Description: Root cause — simulator.makeHeaders builds each simulated block's header from the previous one via overrides.MakeHeader, and when no explicit time override is supplied for a block, the timestamp step used is the chain's configured post-merge slot time (12 seconds on mainnet-shaped configs), matching real block spacing, rather than a literal one-second increment. The documentation's "incremented by one" wording describes an older or simplified default that does not match the 12-second slot-time step the simulator actually applies.
  • Method: eth_simulateV1

19. eth_simulateV1: eth_simulateV1 failure encoding does not always put return/error bytes in the documented fields: revert data moves to error.data and non-revert failures can omit data

  • Statement: eth_simulateV1 failure encoding does not always put return/error bytes in the documented fields: revert data moves to error.data and non-revert failures can omit data.
  • URL: https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth
  • Code location:
    • internal/ethapi/simulate.go:372-389
      callRes.Status = hexutil.Uint64(types.ReceiptStatusFailed)
      if errors.Is(result.Err, vm.ErrExecutionReverted) {
      // If the result contains a revert reason, try to unpack it.
      revertErr := newRevertError(result.Revert())
      callRes.Error = &callError{Message: revertErr.Error(), Code: revertErr.ErrorCode(), Data: revertErr.ErrorData().(string)}
      } else {
      msg := result.Err.Error()
      if gasCapped {
      msg += " (gas limit was capped by the RPC server's global gas cap)"
      }
      callRes.Error = &callError{Message: msg, Code: errCodeVMError}
      }
      } else {
      callRes.Status = hexutil.Uint64(types.ReceiptStatusSuccessful)
      allLogs = append(allLogs, callRes.Logs...)
      }
      callResults[i] = callRes
      }
    • core/state_transition.go:64-69
      func (result *ExecutionResult) Revert() []byte {
      if result.Err != vm.ErrExecutionReverted {
      return nil
      }
      return common.CopyBytes(result.ReturnData)
      }
  • Description: Root cause — simulator.processBlock branches on errors.Is(result.Err, vm.ErrExecutionReverted): only in that case does it call result.Revert() (which returns the raw return bytes solely when the error is exactly ErrExecutionReverted) and pack them into callError.Data alongside the decoded revert message; any other VM error sets only callError.Message/Code with no Data at all, and the call's own returnData-style field is never populated for a failed call. Revert bytes therefore surface under error.data, not the documented top-level returnData, and non-revert failures can legitimately carry no data field at all.
  • Method: eth_simulateV1

20. eth_getBlockByNumber: GetBlockByNumber's pending special case nulls only hash, nonce, and miner, leaving the pending block's number numeric although the delegated eth_getBlockByHash schema declares number null for pending blocks

  • Statement: GetBlockByNumber's pending special case nulls only hash, nonce, and miner, leaving the pending block's number numeric although the delegated eth_getBlockByHash schema declares number null for pending blocks.
  • URL: https://ethereum.org/developers/docs/apis/json-rpc/
  • Code location:
    • internal/ethapi/api.go:525-538
      func (api *BlockChainAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) {
      block, err := api.b.BlockByNumber(ctx, number)
      if block != nil && err == nil {
      response := RPCMarshalBlock(block, true, fullTx, api.b.ChainConfig())
      if number == rpc.PendingBlockNumber {
      // Pending blocks need to nil out a few fields
      for _, field := range []string{"hash", "nonce", "miner"} {
      response[field] = nil
      }
      }
      return response, nil
      }
      return nil, err
      }
  • Description: Root cause — BlockChainAPI.GetBlockByNumber builds its response from RPCMarshalBlock, and for the pending case only loops over the fixed list []string{"hash", "nonce", "miner"} to force those three keys to nil; number is never included in that nulling list, so it keeps whatever numeric value RPCMarshalBlock/RPCMarshalHeader assigned from the pending header. eth_getBlockByNumber's documentation defers entirely to eth_getBlockByHash's schema, which states number is null for a pending block, but the implementation's hardcoded nulled-field set was never extended to cover number, so the delegated contract is only partially honored.
  • Method: eth_getBlockByNumber

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions