Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3
1319 words
7 minutes
Geth(13) QA

Q1: What is the complete path of a JSON-RPC request from client to response?#

Overall flow#

Using eth_getBalance("0xAlice", "latest") as an example:

Client sends HTTP POST:
{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xAlice","latest"],"id":1}
Layer 1: Transport layer (HTTP / WebSocket / IPC)
HTTP server receives request, reads JSON body
Layer 2: Server decoding
codec.readBatch() → parse into jsonrpcMessage struct
Create handler, call handleMsg()
Layer 3: Method dispatch
handleCallMsg() → handleCall()
├─ "eth_getBalance" split on "_" → service="eth", method="getBalance"
├─ serviceRegistry lookup → find BlockChainAPI.GetBalance callback
├─ parsePositionalArguments() → decode JSON params to Go types
│ params[0] "0xAlice" → common.Address
│ params[1] "latest" → rpc.BlockNumberOrHash
└─ callb.call() → invoke Go method via reflection
Layer 4: API struct execution
BlockChainAPI.GetBalance(ctx, 0xAlice, "latest")
├─ backend.StateAndHeaderByNumberOrHash("latest")
│ → "latest" resolves to BlockNumber(-2)
│ → blockchain.CurrentBlock() gets latest block header
│ → state.New(header.Root) opens StateDB
└─ statedb.GetBalance(0xAlice)
→ reads alice's balance from state trie (Chapter 4)
Layer 5: Return
Balance → hexutil.Big → JSON serialization
{"jsonrpc":"2.0","id":1,"result":"0x1234..."}
→ HTTP response sent back to client

Layer 1: Three transport types#

TransportProtocolSubscription supportTypical use
HTTPRequest/responseNoWallets, scripts, remote access
WebSocketFull-duplexYesDApps needing eth_subscribe
IPCUnix socketYesLocal tools (geth attach)

HTTP is the most common. Each request creates a new handler, disposed after processing — no persistent state:

func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
h := newHandler(ctx, codec, s.idgen, &s.services, ...)
h.allowSubscribe = false // HTTP can't push notifications
defer h.close(io.EOF, nil)
reqs, batch, err := codec.readBatch()
if batch {
h.handleBatch(reqs)
} else {
h.handleMsg(reqs[0])
}
}

WebSocket and IPC are persistent connections that support subscriptions — the server can proactively push notifications to the client (e.g., new block arrived).

Anti-abuse limits: HTTP body max 5MB, configurable batch limits control request count and total response size.

Layer 2: Service registry (reflection magic)#

At startup, each subsystem registers its API methods with the server:

server.RegisterName("eth", blockChainAPI) // eth namespace
server.RegisterName("eth", transactionAPI) // same namespace, multiple structs
server.RegisterName("txpool", txPoolAPI) // txpool namespace
server.RegisterName("debug", debugAPI) // debug namespace

RegisterName() uses reflection to scan all exported methods:

func suitableCallbacks(receiver reflect.Value) map[string]*callback {
typ := receiver.Type()
for m := 0; m < typ.NumMethod(); m++ {
method := typ.Method(m)
if method.PkgPath != "" {
continue // unexported method, skip
}
cb := newCallback(receiver, method.Func)
name := formatName(method.Name) // GetBalance → getBalance
callbacks[name] = cb
}
}

Requirements for a Go method to become an RPC callback:

  1. Must be exported (uppercase first letter)
  2. Optionally takes context.Context as first parameter
  3. Returns at most two values: optional result + optional error

Name transformation: GetBalancegetBalance. Combined with namespace: eth + _ + getBalance = eth_getBalance.

Multiple structs can share the same namespace:

Methods under "eth" namespace come from multiple structs:
EthereumAPI → eth_gasPrice, eth_syncing
BlockChainAPI → eth_getBalance, eth_call, eth_blockNumber
TransactionAPI → eth_sendRawTransaction, eth_getTransactionByHash

Layer 3: Method dispatch#

When a request reaches the handler:

func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
// 1. Subscription request?
if msg.isSubscribe() {
return h.handleSubscribe(cp, msg)
}
// 2. Unsubscribe?
if msg.isUnsubscribe() {
callb = h.unsubscribeCb
} else {
// 3. Normal call: look up callback
callb = h.reg.callback(msg.Method)
// "eth_getBalance" → split on "_"
// → service "eth", method "getBalance"
// → look up in registry
}
// 4. Parse arguments
args, err := parsePositionalArguments(msg.Params, callb.argTypes)
// JSON params array → Go type list
// 5. Invoke via reflection
answer := h.runMethod(cp.ctx, msg, callb, args)
// build arg list (receiver + context + args)
// reflect.Value.Call()
}

Block number sentinel values#

Many RPC methods accept a block identifier. Special strings map to negative sentinel values:

"earliest"BlockNumber(-5) → earliest available block
"safe"BlockNumber(-4) → safe block (CL confirmed)
"finalized"BlockNumber(-3) → finalized block (CL finalized)
"latest"BlockNumber(-2) → latest block
"pending"BlockNumber(-1) → pending state

Resolved to actual headers in EthAPIBackend:

func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
switch number {
case rpc.PendingBlockNumber: return b.eth.miner.Pending()
case rpc.LatestBlockNumber: return b.eth.blockchain.CurrentBlock()
case rpc.FinalizedBlockNumber: return b.eth.blockchain.CurrentFinalBlock()
case rpc.SafeBlockNumber: return b.eth.blockchain.CurrentSafeBlock()
case rpc.EarliestBlockNumber: return b.eth.blockchain.GetHeaderByNumber(cutoff)
default: return b.eth.blockchain.GetHeaderByNumber(uint64(number))
}
}

Backend interface: bridge between API and core#

All API structs delegate to the Backend interface rather than accessing blockchain, txpool, etc. directly:

API structs → Backend interface → Core subsystems
├─ HeaderByNumber() → blockchain
├─ StateAndHeaderBy...() → blockchain + state
├─ SendTx() → txpool
├─ GetPoolTransaction() → txpool
├─ GetEVM() → vm
└─ ChainConfig() → params

Why this abstraction layer? Because the same API code can serve different backends — full nodes use EthAPIBackend, light clients use LESAPIBackend.


Q2: How does eth_call work, and why is it one of the most important RPC methods?#

Why it matters#

Almost all DApp read operations go through eth_call:

Check token balance → eth_call(balanceOf(address))
Get DEX quote → eth_call(getAmountsOut(amount, path))
Simulate transaction → eth_call(transfer(to, amount))
Check contract state → eth_call(any view function)

Its core promise: execute a transaction against specified block state, return the result, but never modify the chain.

Complete execution path#

eth_call(args, blockNrOrHash, stateOverride, blockOverrides)
Step 1: Resolve block, open StateDB
blockNrOrHash = "latest" → current chain head
backend.StateAndHeaderByNumberOrHash()
→ state.New(header.Root) → StateDB at that block
Step 2: Apply block overrides (optional)
Modify EVM block context: block number, timestamp, base fee, etc.
→ In-memory only, nothing written to disk
Step 3: Apply state overrides (optional)
Modify account state: balance, nonce, code, storage
→ In-memory only, nothing written to disk
Step 4: Set up gas pool and timeout
Gas pool = RPCGasCap (default 50 million) or unlimited
Timeout = RPCEVMTimeout (default 5 seconds)
Step 5: Execute
TransactionArgs → core.Message
Create EVM instance
core.ApplyMessage() ← same execution path as real transactions (Chapter 6)
A goroutine monitors timeout, cancels EVM if exceeded
Step 6: Return
├─ Success → return EVM output bytes (hex encoded)
├─ REVERT → return revert reason (decoded error message)
└─ Other error → return error

Override mechanism: temporarily modifying world state#

This is eth_call’s most powerful and least intuitive feature.

State Override — temporarily replace any account’s state:

type OverrideAccount struct {
Nonce *hexutil.Uint64
Code *hexutil.Bytes // replace contract code
Balance *hexutil.Big // replace balance
State map[common.Hash]common.Hash // completely replace storage
StateDiff map[common.Hash]common.Hash // incremental storage diff
MovePrecompileTo *common.Address // move precompile contract
}

Practical use cases:

// "If alice had 100 ETH, would this transaction succeed?"
{
"0xAlice": {
"balance": "0x56BC75E2D63100000"
}
}
// "If this contract's code were the new version, what would happen?"
{
"0xContract": {
"code": "0x6080604052..."
}
}
// "If this storage slot's value changed, what would happen?"
{
"0xContract": {
"stateDiff": {
"0x0000...0001": "0x0000...00FF"
}
}
}

Block Override — temporarily modify block context:

type BlockOverrides struct {
Number *hexutil.Big // custom block number
Time *hexutil.Uint64 // custom timestamp
GasLimit *hexutil.Uint64 // custom gas limit
BaseFeePerGas *hexutil.Big // custom base fee
// ...
}

Use case: simulate “if it were tomorrow, could this timelock contract unlock?”

All overrides take effect in memory only, discarded after execution, never written to disk.

Gas and timeout control#

// Gas limit
if globalGasCap == 0 {
gp.AddGas(math.MaxUint64) // unlimited gas
} else {
gp.AddGas(globalGasCap) // default 50,000,000
}
// Timeout control
if timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, timeout) // default 5 seconds
}
// A goroutine cancels EVM execution on timeout

Why the limits? Because eth_call costs no real money — callers can pass arbitrarily large gas limits. Without limits, malicious callers could craft extremely long-running calls to DoS the node.

Comparison with eth_sendRawTransaction#

eth_call eth_sendRawTransaction
Purpose Read/simulate Submit real transaction
Modifies chain No Yes (tx enters txpool → gets mined)
Requires sig No Yes (tx must be signed)
Costs gas No (free) Yes (deducts real ETH)
Overrides Supported Not supported
Execution Synchronous, returns result Async, returns tx hash

Concrete example#

Query alice’s balance in the USDC contract:

1. Client constructs call data:
balanceOf(address) selector = 0x70a08231
Argument = alice's address (left-padded to 32 bytes)
data = 0x70a08231000000000000000000000000AliceAddress...
2. Send eth_call:
{to: "0xUSDC", data: "0x70a08231..."}
3. Geth executes:
Open "latest" block's StateDB
→ Create EVM
→ CALL to USDC contract address
→ EVM executes balanceOf bytecode
→ SLOAD reads alice's balance storage slot
→ RETURN returns balance value
4. Return result:
"0x0000000000000000000000000000000000000000000000000000000005F5E100"
= 100,000,000 (100 USDC, since USDC has 6 decimals)

No state was modified, no gas was spent, no transaction was created.

Geth(13) QA
https://kehaozheng.vercel.app/posts/chainethgeth/13_qa/
Author
Kehao Zheng
Published at
2026-04-22
License
CC BY-NC-SA 4.0

Some information may be outdated