Files
zeekayandHanzo Dev fa5ce6fdd9 Hold every block with all of its transactions
The index could hold a block without its transactions. Before v1.5.9 a null
receipt became an empty transaction, stored under the empty hash, and every
later miss merged into that one row. The blocks stayed and counted their
transactions, and nothing read them again, so Zoo mainnet holds 800 blocks and
2 of 811 transactions.

- A transaction without a hash is refused with its block.
- The block row is written last, after its transactions, logs and transfers.
  Rows at that height under another block hash are deleted.
- indexBlock can be repeated. Transactions are replaced, and address counts and
  token balances move only for rows not already stored.
- The audit deletes hashless rows, then reads again every block whose
  transaction rows do not match its tx_count or its hash. Totals are compared
  first, so a consistent index costs two range reads. A repair runs 256
  heights per pass, with a pass after every head poll.
- A node that answers eth_getBlockReceipts with MethodNotFound (zood) is read
  per transaction with eth_getTransactionReceipt, and never asked for block
  receipts again. A null receipt is still ErrNoBlock. Any other receipt error
  is retried instead of storing transactions without their results.
- The transports return JSON-RPC errors as transport.RPCError with their code.
- An empty index starts at StartBlock. An index holding only block 0 resumes
  at 1 instead of re-reading block 0 on every poll.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 23:03:34 -07:00

169 lines
4.1 KiB
Go

// Copyright (c) 2025 Lux Industries Inc
// SPDX-License-Identifier: MIT
package transport
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"sync/atomic"
"time"
)
// HTTP implements Transport over standard JSON-RPC.
type HTTP struct {
endpoint string
client *http.Client
reqID atomic.Uint64
}
// NewHTTP creates an HTTP JSON-RPC transport.
func NewHTTP(endpoint string) *HTTP {
return &HTTP{
endpoint: endpoint,
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// Call makes a single JSON-RPC call.
func (h *HTTP) Call(ctx context.Context, method string, params ...any) (json.RawMessage, error) {
if len(params) == 0 {
params = []any{}
}
id := h.reqID.Add(1)
reqBody, err := json.Marshal(map[string]any{
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": id,
})
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
return h.doRequest(ctx, reqBody)
}
// BatchCall makes multiple JSON-RPC calls in a single HTTP request.
func (h *HTTP) BatchCall(ctx context.Context, calls []RPCCall) ([]json.RawMessage, error) {
if len(calls) == 0 {
return nil, nil
}
batch := make([]map[string]any, len(calls))
for i, c := range calls {
params := c.Params
if len(params) == 0 {
params = []any{}
}
batch[i] = map[string]any{
"jsonrpc": "2.0",
"method": c.Method,
"params": params,
"id": h.reqID.Add(1),
}
}
reqBody, err := json.Marshal(batch)
if err != nil {
return nil, fmt.Errorf("marshal batch request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", h.endpoint, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := h.client.Do(req)
if err != nil {
return nil, fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http status %d from %s", resp.StatusCode, h.endpoint)
}
var results []struct {
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
ID uint64 `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
return nil, fmt.Errorf("decode batch response: %w", err)
}
out := make([]json.RawMessage, len(results))
for i, r := range results {
if r.Error != nil {
return nil, fmt.Errorf("batch[%d]: %w", i, r.Error)
}
out[i] = r.Result
}
return out, nil
}
// Subscribe is not supported over HTTP. Returns an error.
func (h *HTTP) Subscribe(_ context.Context, _ string, _ ...any) (<-chan json.RawMessage, error) {
return nil, fmt.Errorf("subscribe not supported over HTTP transport")
}
// Close is a no-op for HTTP (connections are pooled by http.Client).
func (h *HTTP) Close() error {
return nil
}
func (h *HTTP) doRequest(ctx context.Context, body []byte) (json.RawMessage, error) {
req, err := http.NewRequestWithContext(ctx, "POST", h.endpoint, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := h.client.Do(req)
if err != nil {
return nil, fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http status %d from %s", resp.StatusCode, h.endpoint)
}
var result struct {
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
if result.Error != nil {
return nil, result.Error
}
return result.Result, nil
}
// RPCError is the error object a JSON-RPC server answers with. Its code is
// what tells "this node has no such method" (MethodNotFound) from a failure.
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// MethodNotFound is the JSON-RPC code for a method the server does not serve.
const MethodNotFound = -32601
func (e *RPCError) Error() string { return fmt.Sprintf("rpc error %d: %s", e.Code, e.Message) }