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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ nodecore is **API/protocol-agnostic**: it is not tied to a single RPC shape. It
### Supported chains, methods & interfaces

- **Interfaces** — `json-rpc` (over HTTP), `websocket`, and `rest` upstream connectors. The set available for a given chain is declared by that chain's [method spec](docs/nodecore/11-method-specs.md).
- **Chains** — every chain defined in [`chains.yaml`](https://github.com/drpcorg/public/blob/main/chains.yaml). Current chain families are EVM (Ethereum, Polygon, Optimism, Arbitrum, Base, BSC, and many others), Solana, Algorand, Aztec, Aptos, and the Ethereum/Gnosis Beacon Chain (consensus layer, REST-only).
- **Chains** — every chain defined in [`chains.yaml`](https://github.com/drpcorg/public/blob/main/chains.yaml). Current chain families are EVM (Ethereum, Polygon, Optimism, Arbitrum, Base, BSC, and many others), Solana, Algorand, Aztec, Aptos, NEAR, and the Ethereum/Gnosis Beacon Chain (consensus layer, REST-only).
- **Methods** — per-chain RPC behavior is data-driven via [method specs](docs/nodecore/11-method-specs.md), covering standard EVM/Solana methods, subscriptions, and EVM filter methods. Methods unsupported by an upstream are automatically banned for it to avoid wasted requests.

## Key features

- **Intelligent routing** — dynamically selects the most suitable upstream based on real-time performance metrics (latency, error rate, availability) for optimal speed, reliability, and fault-tolerance. See [Upstream config](docs/nodecore/05-upstream-config.md).
- **API/protocol-agnostic** — JSON-RPC, WebSocket, and REST interfaces across EVM, Solana, Algorand, Aztec, Aptos, and the Ethereum/Gnosis Beacon Chain, all driven by data-defined [method specs](docs/nodecore/11-method-specs.md). EVM filter methods (`eth_newFilter`, `eth_getFilterLogs`, etc.) are routed only to the upstream where the filter was created.
- **API/protocol-agnostic** — JSON-RPC, WebSocket, and REST interfaces across EVM, Solana, Algorand, Aztec, Aptos, NEAR, and the Ethereum/Gnosis Beacon Chain, all driven by data-defined [method specs](docs/nodecore/11-method-specs.md). EVM filter methods (`eth_newFilter`, `eth_getFilterLogs`, etc.) are routed only to the upstream where the filter was created.
- **Subscriptions** — WebSocket subscriptions are aggregated so many identical client subscriptions share one upstream stream, with optional local synthesis of EVM topics (`newHeads`, `logs`, pending transactions). See [Subscriptions](docs/nodecore/13-subscriptions.md).
- **Caching** — minimizes redundant traffic by caching frequent requests across in-memory/Redis/Postgres backends with configurable policies. See [Cache](docs/nodecore/04-cache.md).
- **Failsafe mechanisms** — request hedging (duplicate slow requests to multiple upstreams) and configurable automatic retries. See [Upstream config](docs/nodecore/05-upstream-config.md).
Expand Down
8 changes: 6 additions & 2 deletions docs/nodecore/05-upstream-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ The `chain-defaults` section defines per-chain baseline settings. `<chain>.optio
* `disable-health-validation` - Disables only the health validators (per chain family). **_Default_**: `false`
* `disable-lower-bounds-detection` - Disables the earliest-available-block detector. Mode-dependent default: `true` in `default` mode, `false` in `strict` mode
* `disable-labels-detection` - Disables the EVM label detectors (client/version, archive, gas, flashblock, etc.). Mode-dependent default: `true` in `default` mode, `false` in `strict` mode
* `validate-syncing` - For EVM chains, calls `eth_syncing` periodically and marks the upstream unavailable when it is syncing. For beacon-chain upstreams it probes `GET /eth/v1/node/syncing` instead (marking the upstream `Syncing`, or `Unavailable` when its execution layer is offline). Mode-dependent default: `false` in `default` mode, `true` in `strict` mode
* `validate-peers` - For EVM chains, calls `net_peerCount` periodically and pairs with `min-peers`. For beacon-chain upstreams it probes `GET /eth/v1/node/peer_count`. Mode-dependent default: `false` in `default` mode, `true` in `strict` mode
* `validate-syncing` - For EVM chains, calls `eth_syncing` periodically and marks the upstream unavailable when it is syncing. For beacon-chain upstreams it probes `GET /eth/v1/node/syncing` instead (marking the upstream `Syncing`, or `Unavailable` when its execution layer is offline). For NEAR upstreams the `status` probe's `sync_info.syncing` and a stale-head guard on `latest_block_time` drive the same signal (always on as part of health validation). Mode-dependent default: `false` in `default` mode, `true` in `strict` mode
* `validate-peers` - For EVM chains, calls `net_peerCount` periodically and pairs with `min-peers`. For beacon-chain upstreams it probes `GET /eth/v1/node/peer_count`. NEAR upstreams probe `network_info` `num_active_peers`. Mode-dependent default: `false` in `default` mode, `true` in `strict` mode
* `min-peers` - Minimum acceptable peer count when `validate-peers` is on. **_Default_**: `1`
* `validate-call-limit` - For EVM chains, periodically probes the upstream's `eth_call` return-data limit and marks the upstream unhealthy when its observed limit is below `call-limit-size`. Mode-dependent default: `false` in `default` mode, `true` in `strict` mode
* `call-limit-size` - Threshold (in bytes) of the smallest acceptable `eth_call` return-data limit. **_Default_**: `1000000` (1 MB)
Expand Down Expand Up @@ -637,6 +637,7 @@ Validators and label detectors run periodically (every `validation-interval`) ag
| Chain id / `net_version` | EVM | `disable-chain-validation`, `disable-settings-validation`, `disable-validation` | Confirms the upstream is actually serving the configured chain. Fails at startup remove the upstream from the pool; runtime drift triggers re-removal |
| Aztec chain validator | Aztec | `disable-chain-validation`, `disable-settings-validation`, `disable-validation` | Equivalent of chain-id check, using the Aztec node's chain-id endpoint |
| Aptos chain validator | Aptos | `disable-chain-validation`, `disable-settings-validation`, `disable-validation` | Equivalent of chain-id check, using the `chain_id` from the Aptos ledger-info endpoint (`GET /v1`) |
| NEAR chain validator | NEAR | `disable-chain-validation`, `disable-settings-validation`, `disable-validation` | Equivalent of chain-id check, comparing the `status` probe's `chain_id` against the configured chain |
| `eth_syncing` validator | EVM | `validate-syncing` (set to `false`) or `disable-settings-validation` | Marks the upstream as syncing/unavailable when the node reports it is not fully synced |
| `net_peerCount` validator | EVM | `validate-peers` / `min-peers` or `disable-settings-validation` | Marks the upstream as unhealthy when peer count drops below `min-peers` |
| `eth_call` return-data limit | EVM | `validate-call-limit` or `disable-settings-validation` | Probes the upstream's maximum `eth_call` return-data size and marks it unhealthy if it is below `call-limit-size` |
Expand All @@ -648,10 +649,13 @@ Validators and label detectors run periodically (every `validation-interval`) ag
| Health validator (Beacon) | Beacon Chain | `disable-health-validation` | Calls `GET /eth/v1/node/health` and marks the upstream available only on a 2xx response |
| Syncing validator (Beacon) | Beacon Chain | `validate-syncing` (set to `false`) | Probes `GET /eth/v1/node/syncing`; marks the upstream `Syncing` when `is_syncing` is true and `Unavailable` when `el_offline` is true |
| Peers validator (Beacon) | Beacon Chain | `validate-peers` / `min-peers` | Probes `GET /eth/v1/node/peer_count` and marks the upstream immature while connected peers are below `min-peers` |
| Health validator (NEAR) | NEAR | `disable-health-validation` | Calls the NEAR `status` RPC; marks the upstream `Syncing` when `sync_info.syncing` is true or `latest_block_time` is stale (stale-head guard). With `validate-peers` on, also probes `network_info` and marks the upstream `Unavailable` at zero `num_active_peers` |
| Lower-bound detector | Solana, Algorand, Aztec, Aptos | `disable-lower-bounds-detection` | Determines the earliest available block / slot on the upstream so that queries against pruned ranges can be routed away |
| Lower-bound detector (Beacon) | Beacon Chain | `disable-lower-bounds-detection` | Binary-searches the earliest retained block, state, epoch (attestation rewards), and blob-sidecar slots so requests against pruned ranges are routed away |
| Lower-bound detector (NEAR) | NEAR | `disable-lower-bounds-detection` | Reads `sync_info.earliest_block_height` from `status` and publishes it as the state and block lower bounds (a sliding GC window on non-archival nodes) |
| Label detectors (EVM) | EVM | `disable-labels-detection` | Populates upstream labels - client name & version, archive vs. full, gas limit, flashblock support, high-latency-tx capability. Labels are exposed via the [gRPC API](12-grpc-server.md) so external consumers can target upstreams with specific capabilities |
| Label detectors (Aptos) | Aptos | `disable-labels-detection` | Populates client name & version labels from the ledger-info endpoint (`GET /v1`) |
| Client label detector (Beacon) | Beacon Chain | `disable-labels-detection` | Reads `GET /eth/v1/node/version` and publishes the consensus-client type and version labels (Lighthouse, Prysm, Teku, Nimbus, etc.) |
| Client label detector (NEAR) | NEAR | `disable-labels-detection` | Reads `version.version` from `status` and publishes the client (`neard`) and version labels |

`disable-validation` is the master switch and overrides every per-validator flag.
3 changes: 2 additions & 1 deletion docs/nodecore/11-method-specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ nodecore embeds the specs below (see [`pkg/methods/specs/`](../../pkg/methods/sp
| `klaytn` | `klaytn-json-rpc`, `klaytn-websocket` |
| `hyperliquid` | `hyperliquid-eth`, `hyperliquid-rest-additional` |
| `tron` | `tron-json-rpc`, `tron-rest`, `tron-rest-solidity` |
| `near` | `near-json-rpc` |

### Plain specs

Expand All @@ -179,7 +180,7 @@ Grouped by the transports they declare:
| `api-connectors` | Specs |
| --- | --- |
| `json-rpc`, `websocket` | `arbitrum`, `cronos_zkevm`, `eth-json-rpc`, `fantom`, `filecoin`, `harmony_0`, `harmony_1`, `hyperliquid-eth`, `klaytn-json-rpc`, `linea`, `mantle`, `optimism`, `polygon`, `polygon_zkevm`, `rootstock`, `scroll`, `sei`, `solana-json-rpc`, `viction`, `zk` |
| `json-rpc` | `algorand`, `aztec`, `tron-json-rpc` |
| `json-rpc` | `algorand`, `aztec`, `near-json-rpc`, `tron-json-rpc` |
| `websocket` | `eth-websocket`, `klaytn-websocket`, `solana-websocket` |
| `rest` | `aptos`, `eth-beacon-chain`, `tron-rest` |
| `rest-additional` | `hyperliquid-rest-additional`, `tron-rest-solidity` |
Expand Down
176 changes: 176 additions & 0 deletions docs/superpowers/specs/2026-07-17-near-support-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# NEAR family support — design

Date: 2026-07-17
Status: approved (pre-implementation)

## Goal

Add first-class support for the **NEAR** blockchain family to nodecore
(`BlockchainType = "near"`), covering near mainnet and testnet (betanet is
registered in `chains.yaml` and gets family support for free), with full gRPC
(emerald) API support, so these chains can be served by nodecore instead of
dshackle.

## Scope

- **In scope:** neard JSON-RPC 2.0 upstreams (plain HTTP, no auth, no WS —
nearcore has no subscriptions at all); the 14-method surface currently
served by dshackle (see method table); optimistic head tracking; chain-id
validation; syncing/peers health; client version labels; dynamic GC-window
lower bounds.
- **Out of scope (YAGNI):** stable aliases nearcore added later
(`changes`, `genesis_config`, `block_effects`, `maintenance_windows`,
`light_client_proof`) and `broadcast_tx_async`/`broadcast_tx_commit`/
`health`/`client_config`/light-client methods — none are served by dshackle
today, so no client depends on them through us; per-method cache tag
parsers (v1 ships everything `cacheable: false`, parity with dshackle which
does not cache NEAR); a separately tracked finalized head (see Head
tracking); archival-aware routing (we run no archival NEAR nodes).

## Approach

NEAR is modelled as a JSON-RPC blockchain family with a poll-only head and
a data-driven method spec: a `near_specific` chain-specific package plus
`pkg/methods/specs/near.json`. It is strictly simpler than bitcoin — a single
`status` call powers health, chain validation, labels and lower bounds; there
is no esplora-style secondary connector and no method aliases to translate.

### Verified live API shapes (from our production nodes, nearcore 2.13.1)

`status` (truncated): `{"chain_id":"mainnet","protocol_version":84,
"latest_protocol_version":86,"version":{"version":"2.13.1","build":"unknown",
"rustc_version":"1.93.0"},"validator_account_id":null,"sync_info":{
"latest_block_height":207365026,"latest_block_hash":"...",
"latest_block_time":"2026-07-17T...","earliest_block_height":207157933,
"syncing":false},...}` — testnet identical shape with `"chain_id":"testnet"`.

`block {"finality":"optimistic"}` → `{"author":...,"header":{"height":...,
"hash":"<base58>","prev_hash":"<base58>","timestamp":1784299563227484539,
"timestamp_nanosec":"1784299563227484539",...},"chunks":[...]}`. The
optimistic head runs ~3 blocks ahead of `"final"`. `timestamp` is integer
nanoseconds (int64-boundary risk in some parsers; `timestamp_nanosec` is the
string twin).

`gas_price [null]` → `{"gas_price":"100000000"}` (string).

Errors carry a structured `name`/`cause` pair alongside the standard fields:

```json
{"error":{"name":"HANDLER_ERROR","cause":{"name":"UNKNOWN_BLOCK","info":{}},
"code":-32000,"message":"Server error","data":"DB Not Found Error: ..."}}
```

- Unknown method → `REQUEST_VALIDATION_ERROR`/`METHOD_NOT_FOUND`, `-32601`.
- Garbage-collected block (`block`/`chunk`) → `UNKNOWN_BLOCK` (`query`
returns the more explicit `GARBAGE_COLLECTED_BLOCK`).
- **Unknown transaction**: the node holds the request ~10s (server-side
poll loop, 10s default timeout) and returns HTTP **408** with
`cause.name: "TIMEOUT_ERROR"` — this is not a node-health signal and must
not be blindly retried in a tight loop.
- Syncing node → `NOT_SYNCED_YET` / `NO_SYNCED_BLOCKS` (retryable on another
upstream); user errors (`INVALID_TRANSACTION`, `PARSE_ERROR`, ...) are
deterministic.

## Architecture

### Factory and type plumbing

- `upstream_factory.go`: new `case chains.Near` returning
`near_specific.NewNearChainSpecificObject(...)`.
- `chains.go`: `getMethodSpecName` gains `case Near: return "near"`
(currently resolves to `""`). `chains.Near` and the NEAR/NEAR_TESTNET/
NEAR_BETANET constants, `chains.yaml` entries (`chain-id: mainnet|testnet|
betanet`, lags 40/20, 1s block time) and gRPC ChainRefs (1050/10064/10065)
all already exist — no registry work.

### Head tracking (`near_specific`)

Poll-based `RpcHead` (no subscriptions), dshackle parity:

1. `GetLatestBlock`: `block {"finality":"optimistic"}` → height,
hash/prev_hash (base58 strings fed to `NewHashIdFromString` as-is).
2. `GetFinalizedBlock`: `block {"finality":"final"}` — implemented for
correctness, but `BlockProcessor()` returns nil in v1, so no separate
finalized-head poll loop runs (same simplification as every non-EVM
family; the optimistic/final gap is ~3 blocks / ~3s). Revisit only if
consumers need a distinct final head.

### Chain validation

`NearChainValidator` calls `status` and compares `chain_id` (case-insensitive)
against the configured `chain-id` (`mainnet`/`testnet`/`betanet` — NEAR
chain-ids in `chains.yaml` are network-name strings, not hex). Mismatch →
`FatalSettingError`; fetch error → `SettingsError`. This is exactly
dshackle's check; no genesis call needed (chain_id is unique per network,
unlike bitcoin where two forks both report `main`).

### Health validation

`NearHealthValidator` uses the same `status` payload:
- `sync_info.syncing == true` → Syncing;
- stale head guard: `latest_block_time` older than the chain's syncing-lag
worth of block time → Syncing (catches the frozen-head case `syncing:false`
does not);
- peers (when `validate-peers`): `network_info` → `num_active_peers == 0` →
Unavailable.

### Labels and lower bounds

- `client_version` label from `status.version.version` (e.g. `2.13.1`),
client type `neard` (the only production implementation).
- `NearLowerBoundDetector`: `status.sync_info.earliest_block_height` →
emitted as STATE and BLOCK bounds (dshackle parity). Our nodes are all
non-archival with a sliding ~5-epoch (~2.5 day) GC window, so the bound is
re-polled on a short period (3 min); no binary search needed — the node
reports its own boundary.

### Method spec (`pkg/methods/specs/near.json`)

The 14 methods dshackle serves, all `cacheable: false` in v1:

| group | methods | notes |
|---|---|---|
| default | block, chunk, tx, EXPERIMENTAL_tx_status, EXPERIMENTAL_receipt, query, EXPERIMENTAL_changes, EXPERIMENTAL_changes_in_block, EXPERIMENTAL_protocol_config, gas_price, validators | object params (`{"finality":...}` / `{"block_id":...}`) |
| passthrough | status, network_info | node-local, never cache |
| broadcast | send_tx | broadcast to all upstreams (dshackle used BroadcastQuorum) |

No translations, no bans: every upstream is a full neard and serves the whole
surface. Follow-up (not v1): cache tag-parsers for `block`/`chunk`/`tx` by
hash and `query` with explicit `block_id` via the existing jq `tag-parser`
mechanism.

## Testing

1. **Unit tests** (bitcoin-family parity): optimistic/final head parsing
(incl. the nanosecond timestamp), chain-id validation mainnet-vs-testnet
cross-check, syncing/stale-head/peers health states, version label,
earliest-block-height bounds, method-spec loading.
2. **Live comparison harness (laptop, before any infra change).** nodecore
runs locally against our production near mainnet + testnet nodes (reached
directly over VPN, plain HTTP, no auth). Corpus: every spec method against
node-direct vs nodecore — blocks by finality/height/hash, deep GC'd
heights (UNKNOWN_BLOCK passthrough), chunk by ids, a real tx and an
unknown tx (408/TIMEOUT_ERROR passthrough within tolerable latency),
query view_account/call_function, changes, protocol/genesis config,
gas_price, validators, unknown method, invalid params. `send_tx` only
with a deterministically invalid transaction on both networks (expected
error passthrough, nothing broadcast).
**Executed 2026-07-17 — passed.** 42/42 checks identical across mainnet
and testnet, byte-identical payloads for all deterministic cases including
GC'd-block UNKNOWN_BLOCK, PARSE_ERROR and the ~10s unknown-tx
TIMEOUT_ERROR passthrough. Expected differences only: nodecore's own
-32601 text for unknown/absent-from-spec methods (local allowlist,
standard for all families), and HTTP transport status on the timeout case
(node replies 408, nodecore returns the identical JSON-RPC error with
200 — nodecore-wide behavior). Lower bounds verified live:
STATE=BLOCK=the node's earliest_block_height.

3. **Staged rollout** is deployment-side work, out of scope for this repo.

## Out of scope / follow-ups

- Cache tag-parsers and per-method cacheability for finalized data.
- Distinct finalized-head tracking (optimistic vs final) if a consumer needs
it.
- ripple, starknet, ton follow as separate family designs reusing this
template.
Loading
Loading