Skip to content

feat(rpc): versioned Starknet RPC API#509

Open
kariy wants to merge 16 commits intomainfrom
feat/versioned-rpc
Open

feat(rpc): versioned Starknet RPC API#509
kariy wants to merge 16 commits intomainfrom
feat/versioned-rpc

Conversation

@kariy
Copy link
Copy Markdown
Member

@kariy kariy commented Mar 26, 2026

Overview

Adds support for exposing multiple Starknet JSON-RPC spec versions simultaneously via URL path prefixes. Each version is served at /rpc/v0_<minor> with the root path / serving a configurable default version.

Routing:

Path Spec Version
/ v0.9 (default, configurable)
/rpc/v0_9 v0.9.0
/rpc/v0_10 v0.10.0

CLI flags:

--rpc.starknet.versions v0.9,v0.10     # which versions to expose (default: all)
--rpc.starknet.root-version v0.9       # version served at / (default: v0.9)

Versioned paths only expose starknet_* methods. Non-starknet APIs (Katana, Dev, TxPool, TEE) are only available at the root path.

RPC Server Router

Introduces RpcRouter for path-based JSON-RPC module routing, with .route() and .nest() APIs:

let router = RpcRouter::new()
    .route("/", root_module)
    .nest("/rpc", RpcRouter::new()
        .route("/v0_9", v09_starknet)
        .route("/v0_10", v010_starknet)
    );

let server = RpcServer::new()
    .router(router)
    .cors(cors)
    .health_check(true)
    .start(addr).await?;

RPC metrics are collected per-route with a path label, so metrics for the same method served at different paths are distinguishable.

Starknet Spec v0.10.0 Changes

v0.9.0 and v0.10.0 have identical method sets (25 read + 3 write + 3 trace). The differences are in response types only:

Block Header

v0.10 adds 7 new fields to confirmed block headers:

Field Type
event_commitment FELT
event_count u32
receipt_commitment FELT
state_diff_commitment FELT
state_diff_length u32
transaction_commitment FELT
transaction_count u32

These are present in v0.10 responses from getBlockWithTxHashes, getBlockWithTxs, and getBlockWithReceipts. They are omitted in v0.9 responses.

Emitted Events

event_index and transaction_index change from optional to required in v0.10 getEvents responses.

State Diff

migrated_compiled_classes changes from optional to required (serialized as empty array when absent) in v0.10 getStateUpdate responses.

PreConfirmed State Update

old_root changes from required to optional in v0.10.

kariy and others added 15 commits March 25, 2026 15:17
Expose different Starknet JSON-RPC spec versions via URL path prefixes:
- `/` and `/rpc/v0_9` → spec v0.9.0 (default)
- `/rpc/v0_10` → spec v0.10.0

The RPC server uses a single unified accept loop with per-request
path-based routing. Each request's URL path selects which Methods set
(v0.9 or v0.10) to dispatch to, with the full middleware stack (CORS,
tracing, health check, metrics, explorer) applied to all versions.

Key changes:

**rpc-types**: Added v0_9/ and v0_10/ versioned type modules. Block
header types carry commitment fields as #[serde(skip)] for v0.9
compatibility; v0.10 types expose them via From conversions. v0.10
EmittedEvent has required event_index/transaction_index; v0.10 StateDiff
always serializes migrated_compiled_classes.

**rpc-api**: Split starknet.rs into starknet/{mod,v0_9,v0_10}.rs. Each
version defines StarknetApi, StarknetWriteApi, StarknetTraceApi traits
with version-specific response types. Backward-compatible re-exports
from mod.rs (pub use v0_9::*) keep existing imports working.

**rpc-server**: v0.10 trait impls delegate to shared helpers with
.into() conversion. RpcServer uses jsonrpsee's to_service_builder() +
serve_with_graceful_shutdown pattern for a clean per-connection routing
service. VersionedRpcModules makes versioning a first-class concept.

**sequencer**: Builds two RpcModules (v0.9, v0.10) from the same
StarknetApi instance. Non-Starknet modules (Katana, Dev, TxPool, etc.)
are merged into both.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dule mounting

Replace the versioning-specific `VersionedRpcModules` abstraction with
a generic `module_at(path, module)` API on `RpcServer`. Path-based
module mounting is now a first-class server capability rather than a
versioning-specific add-on.

- Remove `versioned.rs` and `VersionedRpcModules` struct
- Add `RpcServer::module_at(prefix, module)` for mounting at any path
- `RpcServer::module()` still merges into the default (/) module
- Sequencer uses `.module(v09)?.module_at("/rpc/v0_9", v09)?.module_at("/rpc/v0_10", v010)?`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove redundant health check merge into route modules
- Use imported `Server`, `error!` instead of full paths
- Flatten handle construction and logging
- Minor formatting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the implicit default-module + module_at() API with an explicit
RpcRouter that maps URL path prefixes to RPC modules, inspired by
axum's Router design.

- Add `RpcRouter` with `.route(path, module)` and `.merge(other)`
- `RpcServer::new(router)` takes a router (or a bare RpcModule via From)
- Remove `module()` and `module_at()` from RpcServer
- All module registration goes through the router
- Routing logic unchanged: first prefix match wins

Usage:
```rust
let router = RpcRouter::new()
    .route("/", v09_module.clone())
    .route("/rpc/v0_9", v09_module)
    .route("/rpc/v0_10", v010_module);

let server = RpcServer::new(router)
    .cors(cors)
    .health_check(true)
    .start(addr).await?;
```

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each route in the RpcRouter now gets its own RpcServerMetricsLayer,
with method call metrics labelled by both `method` and `path`. This
makes it possible to distinguish metrics for the same RPC method
served at different paths (e.g., /rpc/v0_9 vs /rpc/v0_10).

- Add `RpcServerMetrics::new_with_path` and `RpcServerMetricsLayer::new_with_path`
- Build per-route metrics in `RpcServer::start()` instead of a single global layer
- Each route's metrics layer is selected at request time based on path match

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `.nest(prefix, router)` to RpcRouter, which prepends the prefix to
all routes in the nested router. This avoids repeating common prefixes.

```rust
// Before:
RpcRouter::new()
    .route("/rpc/v0_9", v09)
    .route("/rpc/v0_10", v010)

// After:
RpcRouter::new()
    .nest("/rpc", RpcRouter::new()
        .route("/v0_9", v09)
        .route("/v0_10", v010)
    )
```

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Non-starknet APIs (Katana, Dev, TxPool, TEE, paymaster) are now only
available on the root path (/). Versioned paths (/rpc/v0_9, /rpc/v0_10)
only expose starknet-specific methods.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract RpcRouter into `router.rs` and re-export from `lib.rs`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add configuration for which Starknet spec versions to expose and which
version is the default at the root path.

- Add `StarknetApiVersion` enum (V0_9, V0_10) with path segment helper
- Add `StarknetApiVersionsList` (same pattern as `RpcModulesList`) with
  `all()`, `parse()`, `contains()`, etc.
- Add `starknet_api_versions` and `default_starknet_api_version` fields
  to `RpcConfig` (defaults: all versions exposed, V0_9 as default)
- Sequencer builds versioned modules dynamically from config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move starknet-specific RPC fields (max_event_page_size, max_proof_keys,
max_call_gas, max_concurrent_estimate_fee_requests, versions,
default_version) into a dedicated StarknetApiConfig struct within
RpcConfig.

Access changes: `config.rpc.max_event_page_size` → `config.rpc.starknet.max_event_page_size`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add CLI flags for controlling which Starknet spec versions are exposed:

  --rpc.starknet-versions v0.9,v0.10   # versions to expose (default: all)
  --rpc.starknet-default-version v0.9  # version at root path (default: v0.9)

Both are optional — when omitted, the defaults from StarknetApiConfig
apply (all versions exposed, v0.9 as default).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- --rpc.starknet-versions    -> --rpc.starknet.versions
- --rpc.starknet-default-version -> --rpc.starknet.root-version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change metrics labelling from path-based to version-based:
- `{method="starknet_getBlock", path="/rpc/v0_9"}` → `{method="starknet_getBlock", version="v0_9"}`
- Root path `/` uses unlabelled metrics (no version dimension)

Make `RpcServerMetrics::new_with_labels` generic over arbitrary
key-value label pairs instead of hardcoding a specific label.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Allow large_enum_variant on block response enums (added by commitment fields)
- Apply rustfmt formatting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Details
Benchmark suite Current: de63ce0 Previous: 48d22e9 Ratio
CompiledClass(fixture)/compress 2666068 ns/iter (± 18324) 2644615 ns/iter (± 8591) 1.01
CompiledClass(fixture)/decompress 2933674 ns/iter (± 92726) 2915458 ns/iter (± 12657) 1.01
ExecutionCheckpoint/compress 34 ns/iter (± 7) 32 ns/iter (± 6) 1.06
ExecutionCheckpoint/decompress 26 ns/iter (± 9) 26 ns/iter (± 10) 1
PruningCheckpoint/compress 34 ns/iter (± 4) 32 ns/iter (± 6) 1.06
PruningCheckpoint/decompress 26 ns/iter (± 1) 27 ns/iter (± 5) 0.96
VersionedHeader/compress 668 ns/iter (± 9) 670 ns/iter (± 7) 1.00
VersionedHeader/decompress 824 ns/iter (± 10) 817 ns/iter (± 9) 1.01
StoredBlockBodyIndices/compress 76 ns/iter (± 1) 78 ns/iter (± 2) 0.97
StoredBlockBodyIndices/decompress 36 ns/iter (± 6) 37 ns/iter (± 4) 0.97
StorageEntry/compress 148 ns/iter (± 2) 153 ns/iter (± 2) 0.97
StorageEntry/decompress 138 ns/iter (± 2) 141 ns/iter (± 3) 0.98
ContractNonceChange/compress 146 ns/iter (± 5) 151 ns/iter (± 2) 0.97
ContractNonceChange/decompress 237 ns/iter (± 4) 232 ns/iter (± 3) 1.02
ContractClassChange/compress 222 ns/iter (± 2) 202 ns/iter (± 3) 1.10
ContractClassChange/decompress 253 ns/iter (± 15) 251 ns/iter (± 4) 1.01
ContractStorageEntry/compress 163 ns/iter (± 3) 164 ns/iter (± 2) 0.99
ContractStorageEntry/decompress 309 ns/iter (± 3) 311 ns/iter (± 4) 0.99
GenericContractInfo/compress 143 ns/iter (± 5) 144 ns/iter (± 1) 0.99
GenericContractInfo/decompress 105 ns/iter (± 3) 109 ns/iter (± 5) 0.96
Felt/compress 81 ns/iter (± 4) 81 ns/iter (± 7) 1
Felt/decompress 57 ns/iter (± 9) 55 ns/iter (± 4) 1.04
BlockHash/compress 82 ns/iter (± 12) 81 ns/iter (± 4) 1.01
BlockHash/decompress 57 ns/iter (± 6) 55 ns/iter (± 5) 1.04
TxHash/compress 81 ns/iter (± 1) 82 ns/iter (± 3) 0.99
TxHash/decompress 57 ns/iter (± 10) 55 ns/iter (± 3) 1.04
ClassHash/compress 81 ns/iter (± 1) 81 ns/iter (± 5) 1
ClassHash/decompress 56 ns/iter (± 14) 55 ns/iter (± 6) 1.02
CompiledClassHash/compress 81 ns/iter (± 1) 81 ns/iter (± 5) 1
CompiledClassHash/decompress 56 ns/iter (± 6) 55 ns/iter (± 5) 1.02
BlockNumber/compress 47 ns/iter (± 2) 49 ns/iter (± 2) 0.96
BlockNumber/decompress 26 ns/iter (± 2) 26 ns/iter (± 2) 1
TxNumber/compress 47 ns/iter (± 2) 49 ns/iter (± 2) 0.96
TxNumber/decompress 27 ns/iter (± 1) 27 ns/iter (± 2) 1
FinalityStatus/compress 1 ns/iter (± 0) 1 ns/iter (± 0) 1
FinalityStatus/decompress 12 ns/iter (± 0) 12 ns/iter (± 0) 1
TypedTransactionExecutionInfo/compress 16032 ns/iter (± 109) 18618 ns/iter (± 107) 0.86
TypedTransactionExecutionInfo/decompress 3530 ns/iter (± 64) 3546 ns/iter (± 105) 1.00
VersionedContractClass/compress 368 ns/iter (± 12) 366 ns/iter (± 3) 1.01
VersionedContractClass/decompress 773 ns/iter (± 6) 780 ns/iter (± 54) 0.99
MigratedCompiledClassHash/compress 150 ns/iter (± 2) 149 ns/iter (± 3) 1.01
MigratedCompiledClassHash/decompress 141 ns/iter (± 6) 148 ns/iter (± 4) 0.95
ContractInfoChangeList/compress 1590 ns/iter (± 64) 1517 ns/iter (± 42) 1.05
ContractInfoChangeList/decompress 2208 ns/iter (± 381) 2233 ns/iter (± 377) 0.99
BlockChangeList/compress 717 ns/iter (± 25) 657 ns/iter (± 17) 1.09
BlockChangeList/decompress 895 ns/iter (± 155) 888 ns/iter (± 147) 1.01
ReceiptEnvelope/compress 28274 ns/iter (± 1576) 28353 ns/iter (± 689) 1.00
ReceiptEnvelope/decompress 5964 ns/iter (± 226) 5924 ns/iter (± 218) 1.01
TrieDatabaseValue/compress 168 ns/iter (± 2) 174 ns/iter (± 1) 0.97
TrieDatabaseValue/decompress 249 ns/iter (± 3) 246 ns/iter (± 3) 1.01
TrieHistoryEntry/compress 296 ns/iter (± 2) 293 ns/iter (± 3) 1.01
TrieHistoryEntry/decompress 270 ns/iter (± 11) 271 ns/iter (± 10) 1.00

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 26, 2026

Runner: AMD EPYC 7763 64-Core Processor (4 cores) · 15Gi RAM

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 35.24721% with 406 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.63%. Comparing base (9bde0ae) to head (de63ce0).
⚠️ Report is 335 commits behind head on main.

Files with missing lines Patch % Lines
crates/rpc/rpc-server/src/starknet/v0_10_read.rs 0.00% 162 Missing ⚠️
crates/rpc/rpc-types/src/v0_10/block.rs 0.00% 106 Missing ⚠️
crates/rpc/rpc-types/src/v0_10/state_update.rs 0.00% 36 Missing ⚠️
crates/node/config/src/rpc.rs 47.16% 28 Missing ⚠️
crates/rpc/rpc-types/src/v0_10/event.rs 0.00% 18 Missing ⚠️
crates/rpc/rpc-server/src/starknet/v0_10_trace.rs 0.00% 11 Missing ⚠️
crates/rpc/rpc-server/src/starknet/v0_10_write.rs 0.00% 9 Missing ⚠️
crates/node/full/src/lib.rs 0.00% 8 Missing ⚠️
crates/rpc/rpc-server/src/router.rs 66.66% 7 Missing ⚠️
crates/cli/src/full.rs 0.00% 6 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #509      +/-   ##
==========================================
- Coverage   73.32%   66.63%   -6.69%     
==========================================
  Files         209      302      +93     
  Lines       23132    38378   +15246     
==========================================
+ Hits        16961    25573    +8612     
- Misses       6171    12805    +6634     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant