> ## Documentation Index
> Fetch the complete documentation index at: https://docs.leafage.chaintable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# nodex-proxy

> JSON-RPC 网关：服务发现、区块上下文路由、负载均衡与流量治理

nodex-proxy 把一个 leafage-evm 集群收敛成单一 RPC 端点。它按请求的区块上下文决定该发往 State、Archive 还是 Native 节点，并承担限流、镜像、可观测性等流量治理职责。

| 项    | 值                                                                                  |
| ---- | ---------------------------------------------------------------------------------- |
| 仓库   | [Chaintable/nodex-proxy](https://github.com/Chaintable/nodex-proxy)                |
| 语言   | Go 1.22+                                                                           |
| 许可证  | Apache-2.0                                                                         |
| 核心依赖 | [Hertz](https://github.com/cloudwego/hertz)、etcd client v3、OpenTelemetry、zap、sonic |

## 端口与路径

| 端口     | 用途                                            |
| ------ | --------------------------------------------- |
| `8663` | JSON-RPC（`POST /:chainId`）与管理接口，共用同一 listener |
| `8664` | Prometheus 指标                                 |

一个进程同时服务多条链，链由路径里的 `chainId` 区分。十六进制 ID 会被规范化为十进制字符串。

<Warning>
  管理接口和 JSON-RPC 共用端口，且能改变路由状态。这个端口不应暴露到内网之外，或需要放在外部认证层之后。
</Warning>

## 请求生命周期

```mermaid theme={null}
flowchart LR
    C["客户端"] --> H["Hertz :8663"]
    H --> PRE["预处理"]
    PRE --> SEL["节点选择"]
    SEL --> UP["反向代理到上游"]
    UP --> RETRY{"错误码<br/>需要重试?"}
    RETRY -->|"是"| SEL
    RETRY -->|"否"| POST["后处理"]
    POST --> C
```

预处理按固定顺序执行：

```text theme={null}
记录请求 → 方法拒绝列表 → 方法名校验 → 指标 → 方法级限流 → 方法特定改写 → 请求镜像
```

后处理：

```text theme={null}
解析响应 → 记录响应 → 方法特定后处理 → 慢请求/错误日志 → 指标
```

## 节点选择

先按区块上下文决定节点池，再在池内按策略选具体节点。

| 请求上下文                | 节点池     |
| -------------------- | ------- |
| `latest` / `pending` | State   |
| 明确高度，距已知链头 64 块以内    | State   |
| 明确高度，落后链头超过 64 块     | Archive |
| `Contains` 类型上下文     | State   |
| 显式 archive 标记（请求头）   | Archive |
| Native 重试            | Native  |

链头高度来自 etcd 的 `{chainId}/lastBlockNumber`，由 consistency-checker 维护。链高度未知时保守选择 Archive 节点。

两个池会互相兜底：State 池为空时用 Archive 池，反之亦然。每个池内优先使用 `Available` 的节点。

### 负载均衡策略

<Tabs>
  <Tab title="random（默认）">
    按权重概率选择。非 batch 请求会先按方法路由规则过滤候选节点，再按权重选。没有显式权重的节点默认 `100`。
  </Tab>

  <Tab title="round_robin">
    在候选池中轮询。不应用 gateway 权重和方法路由。
  </Tab>
</Tabs>

### 自动重试

| 错误码                           | 条件                | 重试目标                  |
| ----------------------------- | ----------------- | --------------------- |
| `-39006` `StateBlockNotFound` | 首次请求不是 archive 请求 | Archive 节点            |
| `-39008` `CosmosPrecompile`   | —                 | Native 节点，上游路径改写为 `/` |

## 服务发现

监听 etcd 前缀（`etcd_prefix` 可配置），下列键后缀会被解释为运行时数据：

| 键后缀                                   | 作用                                     |
| ------------------------------------- | -------------------------------------- |
| `{chainId}/nodes/{nodeKey}`           | State / Archive 节点                     |
| `{chainId}/nativeNodes/{nodeKey}`     | Native 回退节点                            |
| `{chainId}/lastBlockNumber`           | 当前链头高度                                 |
| `{chainId}/gateway`                   | 节点权重与方法路由                              |
| `{chainId}/mirror/{addrKey}`          | 镜像目标及可选限流                              |
| `{chainId}/version`                   | 把基础链路由覆盖到版本化链 ID                       |
| `{chainId}/{version}/nodes/{nodeKey}` | 版本化节点，内部 ID 规范化为 `{chainId}-{version}` |

代理通过 watch `PUT` / `DELETE` 事件实时增删节点、更新配置，不需要重启。

### 健康检查

etcd 中出现新节点时不会立刻加入节点池：

<Steps>
  <Step title="探测">
    向节点发送 RPC 调用验证可达性。State / Archive 节点用 `getLatestBlock`，Native 节点用 `eth_blockNumber`。
  </Step>

  <Step title="重试">
    失败则每 5 秒重试一次，直到 `node_health_check_max_wait`（默认 300 秒）。
  </Step>

  <Step title="加入">
    通过检查后才进入负载均衡池。
  </Step>
</Steps>

### 版本路由

请求使用基础链 ID 且没有显式版本后缀时，代理读取 etcd 的 `{chainId}/version`，把请求改写到 `{chainId}-{version}` 的节点池。这让版本切换对客户端透明。

## 流量治理

| 能力     | 配置项                                | 说明                         |
| ------ | ---------------------------------- | -------------------------- |
| 方法拒绝   | `method_denied`                    | 直接拒绝的方法列表，默认含 `txpool_*` 等 |
| 方法名校验  | `method_name_checker`              | 正则校验方法名格式                  |
| 方法级限流  | `rate_limiter.rpc_methods`         | 令牌桶，按方法配置 RPS              |
| 区块范围限制 | `block_range_query_limit`          | 限制区块范围查询，可改写为 `latest`     |
| 请求镜像   | `request_mirror`                   | 异步复制到影子后端，不影响主请求           |
| 慢请求日志  | `observability_log.slow_threshold` | 按方法配置阈值                    |

## 用量上报

配置 `usage` 后，RPC 耗时按 `client-id` 请求头在本地聚合，达到 10000 个聚合键或 `report_interval`（默认 5s）时写入 Kafka。

```json theme={null}
{
  "id": "3c9d1b7e-52aa-4f0e-8d21-77b4e0c9a1f2",
  "client_id": "instance:019f45e26c307c86bd45ab350bb52ca8",
  "service": "leafage",
  "resource_type": "read",
  "usage": 123,
  "timestamp": 1783568373000
}
```

`usage` 是聚合耗时的毫秒数，最小为 1；缺失的 `client-id` 记为 `unknown`。发送是 best-effort：优雅退出时发送最后一批，进程崩溃或 Kafka 异常时允许丢失。当前聚合键数量由 `jrpcx_usage_aggregation_keys` 反映。

## 管理接口

| 接口组                                                                          | 用途                          |
| ---------------------------------------------------------------------------- | --------------------------- |
| `/getChains`、`/:chainId/getAllNodes`、`/:chainId/debug_chooseOneNode`         | 查看链、节点和选择行为                 |
| `/:chainId/addNode`、`/updateNode/:nodeKey`、`/deleteNode/:nodeKey`            | 持久化到 etcd 的节点变更             |
| `/:chainId/addLocalNode`、`/deleteLocalNode/:nodeKey`                         | 只改内存，不落 etcd                |
| `/:chainId/setWeight`、`/getWeight`、`/deleteWeight`                           | 权重管理                        |
| `/:chainId/addMethodRoute`、`/removeMethodRoute`、`/deleteMethodRoute/:method` | 方法级 include / exclude 路由    |
| `/:chainId/addMirror`、`/deleteMirror`、`/deleteAllMirrors`                    | 镜像目标（etcd 持久化）              |
| `/:chainId/writers`、`/writers/leader`、`/writers/switchLeader`                | 查看写节点、查看和切换 pipeline Leader |

`debug_chooseOneNode` 在排查路由问题时很有用：它返回当前请求条件下会被选中的节点，不实际转发。

## 指标

暴露在 `:8664/metrics`，通用标签为 `host`、`target`、`chain_id`、`chain_version`。

| 指标                                                           | 类型                  |
| ------------------------------------------------------------ | ------------------- |
| `jrpcx_rpc_calls_started` / `finished` / `failed`            | Counter             |
| `jrpcx_rpc_calls_time`                                       | Histogram           |
| `jrpcx_rpc_batch_calls_finished` / `_time`                   | Counter / Histogram |
| `jrpcx_rpc_request_payload_sizes` / `response_payload_sizes` | Histogram           |
| `jrpcx_rpc_http_status_code`                                 | Counter             |
| `jrpcx_rpc_calls_cache_hits`                                 | Counter             |

按方法统计的指标附加 `method` 标签；`jrpcx_rpc_calls_started` 附加 `sourcedapp`；失败指标附加 `status_code`、`upstream_related`、`reason`。

## 运行

```bash theme={null}
go build -o node-proxy cmd/proxy/main.go
./node-proxy -config config/config.example.yaml -listen 8663
```

多个实例可以连接同一个 etcd 集群水平扩展，各自维护本地 selector 状态。

## 开发

```bash theme={null}
make build
make test
make lint
make ci
```

## 相关文档

* [`docs/architecture_cn.md`](https://github.com/Chaintable/nodex-proxy/blob/main/docs/architecture_cn.md) — 组件边界与运行架构
* [`docs/deployment_cn.md`](https://github.com/Chaintable/nodex-proxy/blob/main/docs/deployment_cn.md) — Docker Compose、Kubernetes、systemd 与生产调优
