Node protocol spec

Every detail of how MeterCall L4 nodes register, serve, prove, and claim — in one place. Reference implementation: /node/index.js.

1. Coordinator handshake

  1. Node boots. Generates node_id = sha256(stake_address || name || started_at)[:32].
  2. Node POSTs /api/node/register with { node_id, operator_address, metadata_uri, stake_pcp, regions, chains_supported }.
  3. Coordinator verifies on-chain that operator_address has ≥ 10,000 PCP staked in NodeStaking. If not, the node is accepted but ineligible for rewards until stake lands.
  4. Coordinator returns { ok, registered_at, coordinator_signature }. Node keeps the signature as proof of acceptance.

2. Receipt format

Every served request carries a client signature in header x-request-id (or is auto-generated). The node returns { result, receipt } where receipt is:

{
  "request_id":  "hex",            // unique per call
  "node_id":     "0x...",          // the serving node
  "chain_id":    "ethereum",       // or real-world API identifier
  "result_hash": "0x...sha256...", // hash of the JSON result
  "timestamp":   1714512000000,
  "signature":   "0x..."           // HMAC-SHA256(NODE_KEY, canonical json)
}

EIP-712 type hash (for 0.2.0 when secp256k1 lands):

Receipt(
  bytes32 request_id,
  bytes32 node_id,
  string  chain_id,
  bytes32 result_hash,
  uint256 timestamp
)

3. Proof-of-serve Merkle tree (24h epoch)

4. Sampling & verification

The coordinator is not trusted blindly — its job is to catch cheaters, not approve rewards by fiat.

  1. Per epoch, coordinator uniformly samples 1% of each node's reported receipts (minimum 10 per node).
  2. For each sampled receipt, coordinator re-runs the same request against a canonical upstream (Alchemy for chains it can't independently verify; deterministic real-world API sources; on-chain inclusion for bridge attestations).
  3. If sha256(coordinator_result) == receipt.result_hash → honest.
  4. If mismatch → the receipt is flagged. One mismatch = 1% slash. Three or more mismatches in an epoch escalate to human review + possible 10%–100% slash.
  5. Anyone (not just the coordinator) can POST a challenge to NodeRegistry.challengeReceipt() on-chain with a Merkle proof; a successful challenge also triggers slashing.

5. Slashing state machine

[HEALTHY] │ │ 1 bad receipt sampled ──► [WARNED] (1% stake burned) │ │ │ │ ≥3 bad receipts / epoch │ ▼ │ [ON-NOTICE] (10% stake burned) │ │ │ downtime > 1hr ──► [ON-NOTICE]│ │ │ provable fraud (double-sign) │ ▼ │ [BANNED] (100% + active=false) ▼ [UNBONDING] ── 14d ──► [EXITED]

Transitions are one-way except HEALTHY ↔ WARNED which decays back to healthy after 30 days of clean operation. BANNED is terminal — a new node_id + fresh stake is required to re-enter.

6. Reward claim flow

  1. Epoch ends. Node posts merkle_root to coordinator.
  2. Coordinator samples, verifies. Credits rewards via NodeStaking.creditRewards(operator, amount).
  3. Coordinator publishes the aggregate epoch root of the whole network via NodeRegistry.publishEpochRoot(epoch, merkleRoot, receiptCount) — public transparency.
  4. Operator (or delegator) calls NodeStaking.claim() whenever they want to harvest. No time limit; rewards accrue.

A fully trustless variant (claim-via-Merkle-proof without coordinator credit) is specified for 0.3.0; it requires on-chain secp256k1 receipt verification which is too gas-heavy at 0.1.0 scale.

Appendix — reference implementation