diff --git a/Cargo.lock b/Cargo.lock index 8e90a16a5..720048238 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6766,8 +6766,10 @@ dependencies = [ "cainome", "cairo-lang-starknet-classes", "cartridge", + "futures", "hex", "http 1.3.1", + "hyper 1.6.0", "indexmap 2.10.0", "jsonrpsee 0.26.0", "katana-chain-spec", diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 000000000..c90eaaef4 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,199 @@ +# Versioned Starknet RPC Implementation Plan + +## Overview + +Implement versioned Starknet RPC API support in Katana, exposing spec versions 0.9.0 and 0.10.0 via URL path prefixes (`/rpc/v0_9`, `/rpc/v0_10`). The default `/` path routes to v0.9 (current). All three API groups (Read, Write, Trace) are versioned. Non-Starknet APIs (Katana, Dev, TxPool, etc.) are available on all paths. + +## Goals + +- Expose v0.9.0 and v0.10.0 Starknet APIs at distinct URL paths +- Keep RPC handlers clean — no version branching in handler logic +- Version-specific types defined at the trait level via the `#[rpc]` macro +- Default `/` routes to v0.9 (current); `/rpc/v0_10` opts in to latest + +## Non-Goals + +- WebSocket subscription API versioning (out of scope) +- Supporting spec versions older than 0.9.0 +- Runtime-configurable version selection (compile-time trait structure) + +## Assumptions and Constraints + +- jsonrpsee 0.26 has no built-in path-based routing; we use a tower middleware layer +- v0.9 and v0.10 have identical method sets (no methods added/removed) +- Only 4 types differ between versions (BlockHeader, EmittedEvent, StateDiff, PreConfirmedStateUpdate) +- `RpcModule::merge` rejects duplicate method names, so each version needs its own `RpcModule` + +## Technical Design + +### Type Differences (v0.9 → v0.10) + +| Type | v0.9 | v0.10 | +|------|------|-------| +| `BlockHeader` | Current fields | +7 fields: `event_commitment`, `event_count`, `receipt_commitment`, `state_diff_commitment`, `state_diff_length`, `transaction_commitment`, `transaction_count` | +| `EmittedEvent` | `event_index`/`transaction_index` are `Option` (skipped if None) | Both become **required** (always serialized) | +| `StateDiff` | `migrated_compiled_classes` is `Option` (skipped if None) | `migrated_compiled_classes` is **required** (defaults to empty) | +| `PreConfirmedStateUpdate` | `old_root` required | `old_root` becomes optional | + +### Architecture + +``` +Request → Tower Middleware (path inspection) + ├─ /rpc/v0_9 → v0.9 RpcModule (starknet + shared modules) + ├─ /rpc/v0_10 → v0.10 RpcModule (starknet + shared modules) + └─ / → v0.9 RpcModule (default) +``` + +### Module Structure + +``` +crates/rpc/rpc-api/src/ +├── starknet.rs → starknet/mod.rs (shared imports, version enum) +│ starknet/v0_9.rs (#[rpc] traits with v0.9 types) +│ starknet/v0_10.rs (#[rpc] traits with v0.10 types) + +crates/rpc/rpc-types/src/ +├── block.rs (existing shared types, kept as-is for internal use) +├── event.rs (existing shared types) +├── state_update.rs (existing shared types) +├── v0_9/ +│ ├── mod.rs +│ ├── block.rs (BlockHeader WITHOUT 7 new fields) +│ ├── event.rs (EmittedEvent with Option event_index/transaction_index, skip_serializing_if) +│ └── state_update.rs (StateDiff with Option migrated_compiled_classes, skip_serializing_if) +├── v0_10/ +│ ├── mod.rs +│ ├── block.rs (BlockHeader WITH 7 new fields) +│ ├── event.rs (EmittedEvent with required event_index/transaction_index) +│ └── state_update.rs (StateDiff with required migrated_compiled_classes) + +crates/rpc/rpc-server/src/ +├── starknet/ +│ ├── mod.rs (shared StarknetApi struct + internal helpers, unchanged) +│ ├── read.rs → read/mod.rs + read/v0_9.rs + read/v0_10.rs +│ ├── write.rs → write/mod.rs + write/v0_9.rs + write/v0_10.rs +│ ├── trace.rs → trace/mod.rs + trace/v0_9.rs + trace/v0_10.rs +├── versioned.rs (VersionedRpcModule builder + tower middleware) +├── lib.rs (RpcServer updated to accept versioned modules) +``` + +--- + +## Implementation Plan + +### Serial Dependencies (Must Complete First) + +#### Phase 0: Versioned RPC Types +**Prerequisite for:** All subsequent phases + +| Task | Description | Output | +|------|-------------|--------| +| 0.1 | Create `crates/rpc/rpc-types/src/v0_9/` module with version-specific block, event, and state_update types. These wrap/re-export shared types but serialize according to v0.9 rules (skip optional new fields). | `v0_9::BlockHeader`, `v0_9::EmittedEvent`, `v0_9::StateDiff`, `v0_9::StateUpdate` and their response wrappers | +| 0.2 | Create `crates/rpc/rpc-types/src/v0_10/` module with version-specific types. BlockHeader includes the 7 new commitment/count fields. EmittedEvent has required `event_index`/`transaction_index`. StateDiff has required `migrated_compiled_classes`. | `v0_10::BlockHeader`, `v0_10::EmittedEvent`, `v0_10::StateDiff`, `v0_10::StateUpdate` and their response wrappers | +| 0.3 | Add `From` conversions from internal/shared types to each version's types. The internal helpers return shared types; the trait impls convert to version-specific types via `.into()`. | `From for v0_9::X` and `From for v0_10::X` | +| 0.4 | Add v0.10 test fixtures for blocks and events (v0.10/blocks/, v0.10/events/) alongside the existing v0.10/state-updates/. Add roundtrip serde tests for all new versioned types. | Test fixtures + passing serde tests | + +--- + +### Parallel Workstreams + +#### Workstream A: Versioned API Traits (`rpc-api`) +**Dependencies:** Phase 0 +**Can parallelize with:** Workstream B, C + +| Task | Description | Output | +|------|-------------|--------| +| A.1 | Convert `crates/rpc/rpc-api/src/starknet.rs` into a `starknet/` module directory. Create `starknet/mod.rs` with shared imports and a version constant per version. | `starknet/mod.rs` with `V0_9_SPEC_VERSION = "0.9.0"` and `V0_10_SPEC_VERSION = "0.10.0"` | +| A.2 | Create `starknet/v0_9.rs` with `#[rpc(server, namespace = "starknet")]` traits: `StarknetApi`, `StarknetWriteApi`, `StarknetTraceApi`. These use `katana_rpc_types::v0_9::*` response types for the 4 affected methods. `specVersion` returns `"0.9.0"`. Unaffected methods use shared types. | Three `*Server` traits generated by jsonrpsee | +| A.3 | Create `starknet/v0_10.rs` with identical structure but using `katana_rpc_types::v0_10::*` response types. `specVersion` returns `"0.10.0"`. | Three `*Server` traits for v0.10 | +| A.4 | Update `crates/rpc/rpc-api/src/lib.rs` to expose versioned submodules: `pub mod starknet { pub mod v0_9; pub mod v0_10; }`. Remove the old `starknet.rs`. | Updated module structure | + +#### Workstream B: Versioned Server Impls (`rpc-server`) +**Dependencies:** Phase 0, Workstream A +**Can parallelize with:** Workstream C (partially) + +| Task | Description | Output | +|------|-------------|--------| +| B.1 | Implement `v0_9::StarknetApiServer` for `StarknetApi<...>` in `read/v0_9.rs`. Each method calls the existing shared helper (e.g., `self.block_with_tx_hashes()`), then converts the result to v0.9 types via `.into()`. | v0.9 Read API impl | +| B.2 | Implement `v0_10::StarknetApiServer` for `StarknetApi<...>` in `read/v0_10.rs`. Same pattern, converting to v0.10 types. | v0.10 Read API impl | +| B.3 | Implement versioned Write API (`write/v0_9.rs`, `write/v0_10.rs`). Since Write API types are identical, these are thin wrappers delegating to shared helpers. | v0.9 + v0.10 Write API impls | +| B.4 | Implement versioned Trace API (`trace/v0_9.rs`, `trace/v0_10.rs`). Same as Write — identical types, thin delegation. | v0.9 + v0.10 Trace API impls | +| B.5 | Remove old `read.rs`, `write.rs`, `trace.rs` single-version impls. Update `mod.rs` to export versioned submodules. | Clean module structure | + +#### Workstream C: Path-Based Routing Middleware +**Dependencies:** None (can start immediately, tested with mock modules) +**Can parallelize with:** Workstreams A, B + +| Task | Description | Output | +|------|-------------|--------| +| C.1 | Create `crates/rpc/rpc-server/src/versioned.rs` with a `VersionedRpcModule` struct that holds a map of `path_prefix → Methods` and a default `Methods`. | `VersionedRpcModule` struct | +| C.2 | Implement a tower `Layer`/`Service` (`VersionedRpcRouter`) that inspects `req.uri().path()`, strips the version prefix, and forwards to the appropriate jsonrpsee `Methods` set. For unrecognized paths, fall through to default. | `VersionedRpcRouterLayer` + `VersionedRpcRouterService` | +| C.3 | Update `RpcServer` to accept versioned module configuration. Add a `.versioned_modules(VersionedRpcModule)` builder method alongside the existing `.module()`. Wire the tower layer into `start()`. | Updated `RpcServer::start()` | +| C.4 | Write integration test: start server with two versioned modules, verify that `/rpc/v0_9` returns `specVersion = "0.9.0"`, `/rpc/v0_10` returns `"0.10.0"`, and `/` returns `"0.9.0"`. | Passing integration test | + +--- + +### Merge Phase + +#### Phase N: Integration & Wiring +**Dependencies:** Workstreams A, B, C + +| Task | Description | Output | +|------|-------------|--------| +| N.1 | Update `crates/node/sequencer/src/lib.rs` module assembly: build two `RpcModule`s (v0.9, v0.10) by calling `v0_9::StarknetApiServer::into_rpc()` and `v0_10::StarknetApiServer::into_rpc()` on the same `starknet_api` instance. Merge shared modules (Katana, Dev, TxPool, etc.) into both. Pass both to `RpcServer` via `VersionedRpcModule`. | Versioned server startup | +| N.2 | Update any RPC client code or test utilities that import from `katana_rpc_api::starknet::*` to use the versioned paths (e.g., `katana_rpc_api::starknet::v0_9::*`). | All imports updated | +| N.3 | End-to-end test: start Katana, hit `/rpc/v0_9/` and `/rpc/v0_10/` with `starknet_getBlockWithTxHashes`, verify the v0.10 response includes the 7 new block header fields and v0.9 does not. | Passing E2E test | +| N.4 | Run full test suite (`cargo nextest run`), fix any regressions. Run clippy and fmt. | Green CI | + +--- + +## Testing and Validation + +- **Unit tests**: Serde roundtrip for all versioned types (v0_9 and v0_10 block, event, state_update) +- **Integration tests**: Path-based routing (C.4), spec version per path +- **E2E tests**: Full Katana startup with versioned endpoints (N.3) +- **Regression**: Full `cargo nextest run` to catch import/type breakage + +## Verification Checklist + +```bash +# Build +cargo build + +# Unit tests for versioned types +cargo nextest run -p katana-rpc-types + +# Integration tests for routing +cargo nextest run -p katana-rpc-server + +# Full test suite +cargo nextest run + +# Lint +./scripts/clippy.sh +cargo +nightly-2025-02-20 fmt --all --check +``` + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| jsonrpsee tower middleware can't intercept path before RPC dispatch | Medium | High | Prototype C.1-C.2 early; fall back to `to_service_builder()` if middleware approach doesn't work | +| Large number of trait impls creates maintenance burden | Low | Medium | Write/Trace impls are thin wrappers; only Read has real version-specific logic (4 methods) | +| Breaking existing client imports (`katana_rpc_api::starknet::*`) | High | Low | Systematic search-and-replace in N.2; compiler will catch all misses | +| Health check proxy (`/`) conflicts with default RPC routing | Medium | Medium | Ensure health check layer runs before versioned router; test GET `/` still returns health | + +## Open Questions + +- [ ] Should the v0.10 block header commitment fields be computed from actual data, or zero-filled initially? (Likely needs executor/provider changes to populate them) +- [ ] Should the version path format be `/rpc/v0_9` or `/rpc/v0.9` or `/v0_9`? (Using `/rpc/v0_9` as proposed) + +## Decision Log + +| Decision | Rationale | Alternatives Considered | +|----------|-----------|------------------------| +| Version all three API groups (Read, Write, Trace) | Consistency; future-proof for when Write/Trace diverge | Only version Read API | +| Default `/` → v0.9 | Avoid breaking existing clients; opt-in to v0.10 | Default to latest (v0.10) | +| Tower middleware for routing | Less invasive than custom accept loop; keeps `Server::start()` flow | `to_service_builder()` custom accept loop | +| Separate versioned types (not serde conditional) | Clean separation at macro level; handlers stay version-unaware | `#[serde(skip_serializing_if)]` with runtime version flag | +| Non-Starknet APIs on all paths | Clients using versioned paths shouldn't lose access to dev/katana APIs | Restrict to `/` only | diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 9c417bc6a..907e310c5 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -403,16 +403,27 @@ impl SequencerNodeArgs { port: self.server.http_port, addr: self.server.http_addr, max_connections: self.server.max_connections, - max_concurrent_estimate_fee_requests: None, max_request_body_size: None, max_response_body_size: None, timeout: self.server.timeout.map(Duration::from_secs), cors_origins, #[cfg(feature = "explorer")] explorer: self.explorer.explorer, - max_event_page_size: Some(self.server.max_event_page_size), - max_proof_keys: Some(self.server.max_proof_keys), - max_call_gas: Some(self.server.max_call_gas), + starknet: { + let mut sn = katana_node_config::rpc::StarknetApiConfig { + max_event_page_size: Some(self.server.max_event_page_size), + max_proof_keys: Some(self.server.max_proof_keys), + max_call_gas: Some(self.server.max_call_gas), + ..Default::default() + }; + if let Some(versions) = self.server.starknet_api_versions.clone() { + sn.versions = versions; + } + if let Some(version) = self.server.starknet_default_version { + sn.default_version = version; + } + sn + }, }) } diff --git a/crates/cli/src/full.rs b/crates/cli/src/full.rs index 906a9f586..1a8a17321 100644 --- a/crates/cli/src/full.rs +++ b/crates/cli/src/full.rs @@ -193,16 +193,18 @@ impl FullNodeArgs { port: self.server.http_port, addr: self.server.http_addr, max_connections: self.server.max_connections, - max_concurrent_estimate_fee_requests: None, max_request_body_size: None, max_response_body_size: None, timeout: self.server.timeout.map(Duration::from_secs), cors_origins, #[cfg(feature = "explorer")] explorer: self.explorer.explorer, - max_event_page_size: Some(self.server.max_event_page_size), - max_proof_keys: Some(self.server.max_proof_keys), - max_call_gas: Some(self.server.max_call_gas), + starknet: katana_node_config::rpc::StarknetApiConfig { + max_event_page_size: Some(self.server.max_event_page_size), + max_proof_keys: Some(self.server.max_proof_keys), + max_call_gas: Some(self.server.max_call_gas), + ..Default::default() + }, }) } diff --git a/crates/cli/src/options.rs b/crates/cli/src/options.rs index 5af1ab8c2..08fff66b7 100644 --- a/crates/cli/src/options.rs +++ b/crates/cli/src/options.rs @@ -34,7 +34,8 @@ use katana_sequencer_node::config::metrics::{DEFAULT_METRICS_ADDR, DEFAULT_METRI use katana_sequencer_node::config::rpc::{RpcModulesList, DEFAULT_RPC_MAX_PROOF_KEYS}; #[cfg(feature = "server")] use katana_sequencer_node::config::rpc::{ - DEFAULT_RPC_ADDR, DEFAULT_RPC_MAX_CALL_GAS, DEFAULT_RPC_MAX_EVENT_PAGE_SIZE, DEFAULT_RPC_PORT, + StarknetApiVersion, StarknetApiVersionsList, DEFAULT_RPC_ADDR, DEFAULT_RPC_MAX_CALL_GAS, + DEFAULT_RPC_MAX_EVENT_PAGE_SIZE, DEFAULT_RPC_PORT, }; use katana_tracing::{default_log_file_directory, gcloud, otlp, LogColor, LogFormat, TracerConfig}; use serde::{Deserialize, Serialize}; @@ -233,6 +234,18 @@ pub struct ServerOptions { #[arg(default_value_t = DEFAULT_RPC_MAX_CALL_GAS)] #[serde(default = "default_max_call_gas")] pub max_call_gas: u64, + + /// Starknet API spec versions to expose (comma-separated). + /// Available versions: v0.9, v0.10 + #[arg(long = "rpc.starknet.versions", value_name = "VERSIONS")] + #[arg(value_parser = StarknetApiVersionsList::parse)] + #[serde(default)] + pub starknet_api_versions: Option, + + /// Starknet API spec version served at the root path (/). + #[arg(long = "rpc.starknet.root-version", value_name = "VERSION")] + #[serde(default)] + pub starknet_default_version: Option, } #[cfg(feature = "server")] @@ -250,6 +263,8 @@ impl Default for ServerOptions { max_response_body_size: None, timeout: None, max_call_gas: DEFAULT_RPC_MAX_CALL_GAS, + starknet_api_versions: None, + starknet_default_version: None, } } } diff --git a/crates/node/config/src/rpc.rs b/crates/node/config/src/rpc.rs index 96ad5dd4d..89cec4c32 100644 --- a/crates/node/config/src/rpc.rs +++ b/crates/node/config/src/rpc.rs @@ -15,6 +15,39 @@ pub const DEFAULT_RPC_MAX_PROOF_KEYS: u64 = 100; /// Default maximum gas for the `starknet_call` RPC method. pub const DEFAULT_RPC_MAX_CALL_GAS: u64 = 1_000_000_000; +/// Supported Starknet JSON-RPC spec versions. +#[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + Hash, + strum_macros::EnumString, + strum_macros::Display, + Serialize, + Deserialize, +)] +pub enum StarknetApiVersion { + #[strum(serialize = "v0.9", serialize = "v0_9", serialize = "0.9")] + #[serde(rename = "v0.9")] + V0_9, + + #[strum(serialize = "v0.10", serialize = "v0_10", serialize = "0.10")] + #[serde(rename = "v0.10")] + V0_10, +} + +impl StarknetApiVersion { + /// Returns the URL path segment for this version (e.g., `/v0_9`). + pub fn path_segment(&self) -> &'static str { + match self { + Self::V0_9 => "/v0_9", + Self::V0_10 => "/v0_10", + } + } +} + /// List of RPC modules supported by Katana. #[derive( Debug, @@ -40,6 +73,107 @@ pub enum RpcModuleKind { Tee, } +#[derive(Debug, thiserror::Error)] +#[error("invalid starknet api version: {0}")] +pub struct InvalidStarknetApiVersionError(String); + +/// A set of [`StarknetApiVersion`]s to expose. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct StarknetApiVersionsList(HashSet); + +impl StarknetApiVersionsList { + /// Creates an empty list. + pub fn new() -> Self { + Self(HashSet::new()) + } + + /// Creates a list with all supported versions. + pub fn all() -> Self { + Self(HashSet::from([StarknetApiVersion::V0_9, StarknetApiVersion::V0_10])) + } + + pub fn add(&mut self, version: StarknetApiVersion) { + self.0.insert(version); + } + + pub fn contains(&self, version: &StarknetApiVersion) -> bool { + self.0.contains(version) + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Used as the value parser for `clap`. + pub fn parse(value: &str) -> Result { + if value.is_empty() { + return Ok(Self::new()); + } + + let mut versions = HashSet::new(); + for v in value.split(',') { + let trimmed = v.trim(); + if trimmed.is_empty() { + continue; + } + + let version = trimmed + .parse::() + .map_err(|_| InvalidStarknetApiVersionError(trimmed.to_string()))?; + + versions.insert(version); + } + + Ok(Self(versions)) + } +} + +impl Default for StarknetApiVersionsList { + fn default() -> Self { + Self::all() + } +} + +/// Starknet API-specific configuration within the RPC server. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StarknetApiConfig { + /// Which spec versions to expose at `/rpc/`. + pub versions: StarknetApiVersionsList, + + /// The default version served at the root path `/`. + pub default_version: StarknetApiVersion, + + /// Maximum page size for `starknet_getEvents`. + pub max_event_page_size: Option, + + /// Maximum number of keys for `starknet_getStorageProof`. + pub max_proof_keys: Option, + + /// Maximum gas for `starknet_call`. + pub max_call_gas: Option, + + /// Maximum concurrent `starknet_estimateFee` requests. + pub max_concurrent_estimate_fee_requests: Option, +} + +impl Default for StarknetApiConfig { + fn default() -> Self { + Self { + versions: StarknetApiVersionsList::default(), + default_version: StarknetApiVersion::V0_9, + max_event_page_size: Some(DEFAULT_RPC_MAX_EVENT_PAGE_SIZE), + max_proof_keys: Some(DEFAULT_RPC_MAX_PROOF_KEYS), + max_call_gas: Some(DEFAULT_RPC_MAX_CALL_GAS), + max_concurrent_estimate_fee_requests: None, + } + } +} + /// Configuration for the RPC server. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RpcConfig { @@ -50,13 +184,11 @@ pub struct RpcConfig { pub apis: RpcModulesList, pub cors_origins: Vec, pub max_connections: Option, - pub max_concurrent_estimate_fee_requests: Option, pub max_request_body_size: Option, pub max_response_body_size: Option, pub timeout: Option, - pub max_proof_keys: Option, - pub max_event_page_size: Option, - pub max_call_gas: Option, + /// Starknet API-specific configuration. + pub starknet: StarknetApiConfig, } impl RpcConfig { @@ -75,14 +207,11 @@ impl Default for RpcConfig { addr: DEFAULT_RPC_ADDR, port: DEFAULT_RPC_PORT, max_connections: None, - max_concurrent_estimate_fee_requests: None, max_request_body_size: None, max_response_body_size: None, timeout: None, apis: RpcModulesList::default(), - max_event_page_size: Some(DEFAULT_RPC_MAX_EVENT_PAGE_SIZE), - max_proof_keys: Some(DEFAULT_RPC_MAX_PROOF_KEYS), - max_call_gas: Some(DEFAULT_RPC_MAX_CALL_GAS), + starknet: StarknetApiConfig::default(), } } } diff --git a/crates/node/full/src/lib.rs b/crates/node/full/src/lib.rs index 0ae1cb0a8..887697e40 100644 --- a/crates/node/full/src/lib.rs +++ b/crates/node/full/src/lib.rs @@ -369,10 +369,13 @@ impl Node { // // --- build starknet api let starknet_api_cfg = StarknetApiConfig { - max_event_page_size: config.rpc.max_event_page_size, - max_proof_keys: config.rpc.max_proof_keys, - max_call_gas: config.rpc.max_call_gas, - max_concurrent_estimate_fee_requests: config.rpc.max_concurrent_estimate_fee_requests, + max_event_page_size: config.rpc.starknet.max_event_page_size, + max_proof_keys: config.rpc.starknet.max_proof_keys, + max_call_gas: config.rpc.starknet.max_call_gas, + max_concurrent_estimate_fee_requests: config + .rpc + .starknet + .max_concurrent_estimate_fee_requests, simulation_flags: ExecutionFlags::default(), versioned_constant_overrides: None, #[cfg(feature = "cartridge")] @@ -416,7 +419,7 @@ impl Node { #[allow(unused_mut)] let mut rpc_server = - RpcServer::new().metrics(true).health_check(true).cors(cors).module(rpc_modules)?; + RpcServer::new().metrics(true).health_check(true).cors(cors).router(rpc_modules); #[cfg(feature = "explorer")] { diff --git a/crates/node/sequencer/src/lib.rs b/crates/node/sequencer/src/lib.rs index 6061ce4fa..3ff6a1161 100755 --- a/crates/node/sequencer/src/lib.rs +++ b/crates/node/sequencer/src/lib.rs @@ -41,7 +41,15 @@ use katana_rpc_api::dev::DevApiServer; use katana_rpc_api::katana::KatanaApiServer; #[cfg(feature = "paymaster")] use katana_rpc_api::paymaster::PaymasterApiServer; -use katana_rpc_api::starknet::{StarknetApiServer, StarknetTraceApiServer, StarknetWriteApiServer}; +use katana_rpc_api::starknet::v0_10::{ + StarknetApiServer as StarknetApiServerV010, + StarknetTraceApiServer as StarknetTraceApiServerV010, + StarknetWriteApiServer as StarknetWriteApiServerV010, +}; +use katana_rpc_api::starknet::v0_9::{ + StarknetApiServer as StarknetApiServerV09, StarknetTraceApiServer as StarknetTraceApiServerV09, + StarknetWriteApiServer as StarknetWriteApiServerV09, +}; #[cfg(feature = "explorer")] use katana_rpc_api::starknet_ext::StarknetApiExtServer; #[cfg(feature = "tee")] @@ -57,7 +65,7 @@ use katana_rpc_server::starknet::CartridgePaymasterConfig; use katana_rpc_server::starknet::{RpcCache, StarknetApi, StarknetApiConfig}; #[cfg(feature = "tee")] use katana_rpc_server::tee::TeeApi; -use katana_rpc_server::{RpcServer, RpcServerHandle}; +use katana_rpc_server::{RpcRouter, RpcServer, RpcServerHandle}; use katana_rpc_types::GetBlockWithTxHashesResponse; use katana_stage::Sequencing; use katana_starknet::rpc::Client as StarknetClient; @@ -300,10 +308,13 @@ where // --- build starknet api let starknet_api_cfg = StarknetApiConfig { - max_event_page_size: config.rpc.max_event_page_size, - max_proof_keys: config.rpc.max_proof_keys, - max_call_gas: config.rpc.max_call_gas, - max_concurrent_estimate_fee_requests: config.rpc.max_concurrent_estimate_fee_requests, + max_event_page_size: config.rpc.starknet.max_event_page_size, + max_proof_keys: config.rpc.starknet.max_proof_keys, + max_call_gas: config.rpc.starknet.max_call_gas, + max_concurrent_estimate_fee_requests: config + .rpc + .starknet + .max_concurrent_estimate_fee_requests, simulation_flags: execution_flags, versioned_constant_overrides, #[cfg(feature = "cartridge")] @@ -323,32 +334,71 @@ where RpcCache::new(), ); + // --- build versioned starknet modules --- + // + // Versioned paths (/rpc/v0_9, /rpc/v0_10) only expose starknet APIs. + // Non-starknet APIs (Katana, Dev, TxPool, TEE) are only on the root (/). + + use katana_node_config::rpc::StarknetApiVersion; + + // Build a starknet module for each configured version. + let mut starknet_modules: Vec<(StarknetApiVersion, RpcModule<()>)> = Vec::new(); + if config.rpc.apis.contains(&RpcModuleKind::Starknet) { - #[cfg(feature = "explorer")] - if config.rpc.explorer { - rpc_modules.merge(StarknetApiExtServer::into_rpc(starknet_api.clone()))?; - } + for &version in config.rpc.starknet.versions.iter() { + let mut module = RpcModule::new(()); + + #[cfg(feature = "explorer")] + if config.rpc.explorer { + module.merge(StarknetApiExtServer::into_rpc(starknet_api.clone()))?; + } - rpc_modules.merge(StarknetApiServer::into_rpc(starknet_api.clone()))?; - rpc_modules.merge(StarknetWriteApiServer::into_rpc(starknet_api.clone()))?; - rpc_modules.merge(StarknetTraceApiServer::into_rpc(starknet_api.clone()))?; + match version { + StarknetApiVersion::V0_9 => { + module.merge(StarknetApiServerV09::into_rpc(starknet_api.clone()))?; + module.merge(StarknetWriteApiServerV09::into_rpc(starknet_api.clone()))?; + module.merge(StarknetTraceApiServerV09::into_rpc(starknet_api.clone()))?; + } + StarknetApiVersion::V0_10 => { + module.merge(StarknetApiServerV010::into_rpc(starknet_api.clone()))?; + module.merge(StarknetWriteApiServerV010::into_rpc(starknet_api.clone()))?; + module.merge(StarknetTraceApiServerV010::into_rpc(starknet_api.clone()))?; + } + } + + starknet_modules.push((version, module)); + } } + // --- build root module (all APIs) --- + // + // `rpc_modules` already contains paymaster/cartridge modules merged above. + // We add the default starknet version plus all non-starknet APIs. + + let default_version = config.rpc.starknet.default_version; + let default_starknet = starknet_modules + .iter() + .find(|(v, _)| *v == default_version) + .map(|(_, m)| m.clone()) + .unwrap_or_else(|| RpcModule::new(())); + + let mut root_module = rpc_modules; + root_module.merge(default_starknet)?; + if config.rpc.apis.contains(&RpcModuleKind::Starknet) { - rpc_modules.merge(KatanaApiServer::into_rpc(starknet_api.clone()))?; + root_module.merge(KatanaApiServer::into_rpc(starknet_api.clone()))?; } if config.rpc.apis.contains(&RpcModuleKind::Dev) { let api = DevApi::new(backend.clone(), block_producer.clone(), pool.clone()); - rpc_modules.merge(DevApiServer::into_rpc(api))?; + root_module.merge(DevApiServer::into_rpc(api))?; } if config.rpc.apis.contains(&RpcModuleKind::TxPool) { let api = katana_rpc_server::txpool::TxPoolApi::new(pool.clone()); - rpc_modules.merge(katana_rpc_api::txpool::TxPoolApiServer::into_rpc(api))?; + root_module.merge(katana_rpc_api::txpool::TxPoolApiServer::into_rpc(api))?; } - // --- build tee api (if configured) #[cfg(feature = "tee")] if config.rpc.apis.contains(&RpcModuleKind::Tee) { if let Some(ref tee_config) = config.tee { @@ -373,15 +423,23 @@ where }; let api = TeeApi::new(provider.clone(), tee_provider, tee_config.fork_block_number); - rpc_modules.merge(TeeApiServer::into_rpc(api))?; + root_module.merge(TeeApiServer::into_rpc(api))?; info!(target: "node", provider = ?tee_config.provider_type, "TEE API enabled"); } } + // Build RPC server with path-based routing. + let mut versioned_router = RpcRouter::new(); + for (version, module) in starknet_modules { + versioned_router = versioned_router.route(version.path_segment(), module); + } + + let router = RpcRouter::new().route("/", root_module).nest("/rpc", versioned_router); + #[allow(unused_mut)] let mut rpc_server = - RpcServer::new().metrics(true).health_check(true).cors(cors).module(rpc_modules)?; + RpcServer::new().metrics(true).health_check(true).cors(cors).router(router); #[cfg(feature = "explorer")] { diff --git a/crates/rpc/rpc-api/src/starknet/mod.rs b/crates/rpc/rpc-api/src/starknet/mod.rs new file mode 100644 index 000000000..a79d9fff5 --- /dev/null +++ b/crates/rpc/rpc-api/src/starknet/mod.rs @@ -0,0 +1,14 @@ +//! Versioned Starknet JSON-RPC API trait definitions. +//! +//! Each version module defines the same set of traits (`StarknetApi`, `StarknetWriteApi`, +//! `StarknetTraceApi`) using version-specific response types. The jsonrpsee `#[rpc]` macro +//! generates `*Server` traits (`StarknetApiServer`, etc.) that can be implemented independently +//! for each version. +//! +//! Spec: + +pub mod v0_10; +pub mod v0_9; + +// Backward-compatible re-exports: default to v0.9 (the current spec version). +pub use v0_9::*; diff --git a/crates/rpc/rpc-api/src/starknet/v0_10.rs b/crates/rpc/rpc-api/src/starknet/v0_10.rs new file mode 100644 index 000000000..197fb83dc --- /dev/null +++ b/crates/rpc/rpc-api/src/starknet/v0_10.rs @@ -0,0 +1,210 @@ +//! Starknet JSON-RPC API v0.10.0 trait definitions. + +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use katana_primitives::block::{BlockIdOrTag, ConfirmedBlockIdOrTag}; +use katana_primitives::class::ClassHash; +use katana_primitives::contract::{Nonce, StorageKey}; +use katana_primitives::transaction::TxHash; +use katana_primitives::{ContractAddress, Felt}; +use katana_rpc_types::broadcasted::{ + AddDeclareTransactionResponse, AddDeployAccountTransactionResponse, + AddInvokeTransactionResponse, BroadcastedDeclareTx, BroadcastedDeployAccountTx, + BroadcastedInvokeTx, BroadcastedTx, +}; +use katana_rpc_types::class::{CasmClass, Class}; +use katana_rpc_types::message::MsgFromL1; +use katana_rpc_types::receipt::TxReceiptWithBlockInfo; +use katana_rpc_types::trace::{ + SimulatedTransactionsResponse, TraceBlockTransactionsResponse, TxTrace, +}; +use katana_rpc_types::transaction::RpcTxWithHash; +use katana_rpc_types::trie::{ContractStorageKeys, GetStorageProofResponse}; +// v0.10-specific types +use katana_rpc_types::v0_10::block::{ + BlockHashAndNumberResponse, BlockNumberResponse, BlockTxCount, GetBlockWithReceiptsResponse, + GetBlockWithTxHashesResponse, MaybePreConfirmedBlock, +}; +use katana_rpc_types::v0_10::event::{EventFilterWithPage, GetEventsResponse}; +use katana_rpc_types::v0_10::state_update::StateUpdate; +use katana_rpc_types::{ + CallResponse, EstimateFeeSimulationFlag, FeeEstimate, FunctionCall, SimulationFlag, + SyncingResponse, TxStatus, +}; + +pub const RPC_SPEC_VERSION: &str = "0.10.0"; + +/// Read API. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] +#[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] +pub trait StarknetApi { + #[method(name = "specVersion")] + async fn spec_version(&self) -> RpcResult { + Ok(RPC_SPEC_VERSION.into()) + } + + #[method(name = "getBlockWithTxHashes")] + async fn get_block_with_tx_hashes( + &self, + block_id: BlockIdOrTag, + ) -> RpcResult; + + #[method(name = "getBlockWithTxs")] + async fn get_block_with_txs(&self, block_id: BlockIdOrTag) + -> RpcResult; + + #[method(name = "getBlockWithReceipts")] + async fn get_block_with_receipts( + &self, + block_id: BlockIdOrTag, + ) -> RpcResult; + + #[method(name = "getStateUpdate")] + async fn get_state_update(&self, block_id: BlockIdOrTag) -> RpcResult; + + #[method(name = "getStorageAt")] + async fn get_storage_at( + &self, + contract_address: ContractAddress, + key: StorageKey, + block_id: BlockIdOrTag, + ) -> RpcResult; + + #[method(name = "getTransactionStatus")] + async fn get_transaction_status(&self, transaction_hash: TxHash) -> RpcResult; + + #[method(name = "getTransactionByHash")] + async fn get_transaction_by_hash(&self, transaction_hash: TxHash) -> RpcResult; + + #[method(name = "getTransactionByBlockIdAndIndex")] + async fn get_transaction_by_block_id_and_index( + &self, + block_id: BlockIdOrTag, + index: u64, + ) -> RpcResult; + + #[method(name = "getTransactionReceipt")] + async fn get_transaction_receipt( + &self, + transaction_hash: TxHash, + ) -> RpcResult; + + #[method(name = "getClass")] + async fn get_class(&self, block_id: BlockIdOrTag, class_hash: ClassHash) -> RpcResult; + + #[method(name = "getClassHashAt")] + async fn get_class_hash_at( + &self, + block_id: BlockIdOrTag, + contract_address: ContractAddress, + ) -> RpcResult; + + #[method(name = "getClassAt")] + async fn get_class_at( + &self, + block_id: BlockIdOrTag, + contract_address: ContractAddress, + ) -> RpcResult; + + #[method(name = "getCompiledCasm")] + async fn get_compiled_casm(&self, class_hash: ClassHash) -> RpcResult; + + #[method(name = "getBlockTransactionCount")] + async fn get_block_transaction_count(&self, block_id: BlockIdOrTag) -> RpcResult; + + #[method(name = "call")] + async fn call(&self, request: FunctionCall, block_id: BlockIdOrTag) -> RpcResult; + + #[method(name = "estimateFee")] + async fn estimate_fee( + &self, + request: Vec, + simulation_flags: Vec, + block_id: BlockIdOrTag, + ) -> RpcResult>; + + #[method(name = "estimateMessageFee")] + async fn estimate_message_fee( + &self, + message: MsgFromL1, + block_id: BlockIdOrTag, + ) -> RpcResult; + + #[method(name = "blockNumber")] + async fn block_number(&self) -> RpcResult; + + #[method(name = "blockHashAndNumber")] + async fn block_hash_and_number(&self) -> RpcResult; + + #[method(name = "chainId")] + async fn chain_id(&self) -> RpcResult; + + #[method(name = "syncing")] + async fn syncing(&self) -> RpcResult { + Ok(SyncingResponse::NotSyncing) + } + + #[method(name = "getEvents")] + async fn get_events(&self, filter: EventFilterWithPage) -> RpcResult; + + #[method(name = "getNonce")] + async fn get_nonce( + &self, + block_id: BlockIdOrTag, + contract_address: ContractAddress, + ) -> RpcResult; + + #[method(name = "getStorageProof")] + async fn get_storage_proof( + &self, + block_id: BlockIdOrTag, + class_hashes: Option>, + contract_addresses: Option>, + contracts_storage_keys: Option>, + ) -> RpcResult; +} + +/// Write API. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] +#[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] +pub trait StarknetWriteApi { + #[method(name = "addInvokeTransaction")] + async fn add_invoke_transaction( + &self, + invoke_transaction: BroadcastedInvokeTx, + ) -> RpcResult; + + #[method(name = "addDeclareTransaction")] + async fn add_declare_transaction( + &self, + declare_transaction: BroadcastedDeclareTx, + ) -> RpcResult; + + #[method(name = "addDeployAccountTransaction")] + async fn add_deploy_account_transaction( + &self, + deploy_account_transaction: BroadcastedDeployAccountTx, + ) -> RpcResult; +} + +/// Trace API. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] +#[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] +pub trait StarknetTraceApi { + #[method(name = "traceTransaction")] + async fn trace_transaction(&self, transaction_hash: TxHash) -> RpcResult; + + #[method(name = "simulateTransactions")] + async fn simulate_transactions( + &self, + block_id: BlockIdOrTag, + transactions: Vec, + simulation_flags: Vec, + ) -> RpcResult; + + #[method(name = "traceBlockTransactions")] + async fn trace_block_transactions( + &self, + block_id: ConfirmedBlockIdOrTag, + ) -> RpcResult; +} diff --git a/crates/rpc/rpc-api/src/starknet.rs b/crates/rpc/rpc-api/src/starknet/v0_9.rs similarity index 72% rename from crates/rpc/rpc-api/src/starknet.rs rename to crates/rpc/rpc-api/src/starknet/v0_9.rs index c7b923c2b..eeba58ddb 100644 --- a/crates/rpc/rpc-api/src/starknet.rs +++ b/crates/rpc/rpc-api/src/starknet/v0_9.rs @@ -1,4 +1,4 @@ -//! Starknet JSON-RPC specifications: +//! Starknet JSON-RPC API v0.9.0 trait definitions. use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; @@ -7,67 +7,61 @@ use katana_primitives::class::ClassHash; use katana_primitives::contract::{Nonce, StorageKey}; use katana_primitives::transaction::TxHash; use katana_primitives::{ContractAddress, Felt}; -use katana_rpc_types::block::{ - BlockHashAndNumberResponse, BlockNumberResponse, BlockTxCount, GetBlockWithReceiptsResponse, - GetBlockWithTxHashesResponse, MaybePreConfirmedBlock, -}; use katana_rpc_types::broadcasted::{ AddDeclareTransactionResponse, AddDeployAccountTransactionResponse, AddInvokeTransactionResponse, BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, BroadcastedTx, }; use katana_rpc_types::class::{CasmClass, Class}; -use katana_rpc_types::event::{EventFilterWithPage, GetEventsResponse}; use katana_rpc_types::message::MsgFromL1; use katana_rpc_types::receipt::TxReceiptWithBlockInfo; -use katana_rpc_types::state_update::StateUpdate; use katana_rpc_types::trace::{ SimulatedTransactionsResponse, TraceBlockTransactionsResponse, TxTrace, }; use katana_rpc_types::transaction::RpcTxWithHash; use katana_rpc_types::trie::{ContractStorageKeys, GetStorageProofResponse}; +// v0.9-specific types (re-exports of the default/shared types) +use katana_rpc_types::v0_9::block::{ + BlockHashAndNumberResponse, BlockNumberResponse, BlockTxCount, GetBlockWithReceiptsResponse, + GetBlockWithTxHashesResponse, MaybePreConfirmedBlock, +}; +use katana_rpc_types::v0_9::event::{EventFilterWithPage, GetEventsResponse}; +use katana_rpc_types::v0_9::state_update::StateUpdate; use katana_rpc_types::{ CallResponse, EstimateFeeSimulationFlag, FeeEstimate, FunctionCall, SimulationFlag, SyncingResponse, TxStatus, }; -/// The currently supported version of the Starknet JSON-RPC specification. pub const RPC_SPEC_VERSION: &str = "0.9.0"; /// Read API. #[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] #[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] pub trait StarknetApi { - /// Returns the version of the Starknet JSON-RPC specification being used. #[method(name = "specVersion")] async fn spec_version(&self) -> RpcResult { Ok(RPC_SPEC_VERSION.into()) } - /// Get block information with transaction hashes given the block id. #[method(name = "getBlockWithTxHashes")] async fn get_block_with_tx_hashes( &self, block_id: BlockIdOrTag, ) -> RpcResult; - /// Get block information with full transactions given the block id. #[method(name = "getBlockWithTxs")] async fn get_block_with_txs(&self, block_id: BlockIdOrTag) -> RpcResult; - /// Get block information with full transactions and receipts given the block id. #[method(name = "getBlockWithReceipts")] async fn get_block_with_receipts( &self, block_id: BlockIdOrTag, ) -> RpcResult; - /// Get the information about the result of executing the requested block. #[method(name = "getStateUpdate")] async fn get_state_update(&self, block_id: BlockIdOrTag) -> RpcResult; - /// Get the value of the storage at the given address and key #[method(name = "getStorageAt")] async fn get_storage_at( &self, @@ -76,16 +70,12 @@ pub trait StarknetApi { block_id: BlockIdOrTag, ) -> RpcResult; - /// Gets the transaction status (possibly reflecting that the tx is still in the mempool, or - /// dropped from it). #[method(name = "getTransactionStatus")] async fn get_transaction_status(&self, transaction_hash: TxHash) -> RpcResult; - /// Get the details and status of a submitted transaction. #[method(name = "getTransactionByHash")] async fn get_transaction_by_hash(&self, transaction_hash: TxHash) -> RpcResult; - /// Get the details of a transaction by a given block id and index. #[method(name = "getTransactionByBlockIdAndIndex")] async fn get_transaction_by_block_id_and_index( &self, @@ -93,19 +83,15 @@ pub trait StarknetApi { index: u64, ) -> RpcResult; - /// Get the transaction receipt by the transaction hash. #[method(name = "getTransactionReceipt")] async fn get_transaction_receipt( &self, transaction_hash: TxHash, ) -> RpcResult; - /// Get the contract class definition in the given block associated with the given hash. #[method(name = "getClass")] async fn get_class(&self, block_id: BlockIdOrTag, class_hash: ClassHash) -> RpcResult; - /// Get the contract class hash in the given block for the contract deployed at the given - /// address. #[method(name = "getClassHashAt")] async fn get_class_hash_at( &self, @@ -113,7 +99,6 @@ pub trait StarknetApi { contract_address: ContractAddress, ) -> RpcResult; - /// Get the contract class definition in the given block at the given address. #[method(name = "getClassAt")] async fn get_class_at( &self, @@ -121,19 +106,15 @@ pub trait StarknetApi { contract_address: ContractAddress, ) -> RpcResult; - /// Get the compiled CASM code resulting from compiling a given class. #[method(name = "getCompiledCasm")] async fn get_compiled_casm(&self, class_hash: ClassHash) -> RpcResult; - /// Get the number of transactions in a block given a block id. #[method(name = "getBlockTransactionCount")] async fn get_block_transaction_count(&self, block_id: BlockIdOrTag) -> RpcResult; - /// Call a starknet function without creating a StarkNet transaction. #[method(name = "call")] async fn call(&self, request: FunctionCall, block_id: BlockIdOrTag) -> RpcResult; - /// Estimate the fee for of StarkNet transactions. #[method(name = "estimateFee")] async fn estimate_fee( &self, @@ -142,7 +123,6 @@ pub trait StarknetApi { block_id: BlockIdOrTag, ) -> RpcResult>; - /// Estimate the L2 fee of a message sent on L1. #[method(name = "estimateMessageFee")] async fn estimate_message_fee( &self, @@ -150,29 +130,23 @@ pub trait StarknetApi { block_id: BlockIdOrTag, ) -> RpcResult; - /// Get the most recent accepted block number. #[method(name = "blockNumber")] async fn block_number(&self) -> RpcResult; - /// Get the most recent accepted block hash and number. #[method(name = "blockHashAndNumber")] async fn block_hash_and_number(&self) -> RpcResult; - /// Return the currently configured StarkNet chain id. #[method(name = "chainId")] async fn chain_id(&self) -> RpcResult; - /// Returns an object about the sync status, or false if the node is not synching. #[method(name = "syncing")] async fn syncing(&self) -> RpcResult { Ok(SyncingResponse::NotSyncing) } - /// Returns all event objects matching the conditions in the provided filter. #[method(name = "getEvents")] async fn get_events(&self, filter: EventFilterWithPage) -> RpcResult; - /// Get the nonce associated with the given address in the given block. #[method(name = "getNonce")] async fn get_nonce( &self, @@ -180,9 +154,6 @@ pub trait StarknetApi { contract_address: ContractAddress, ) -> RpcResult; - /// Get merkle paths in one of the state tries: global state, classes, individual contract. A - /// single request can query for any mix of the three types of storage proofs (classes, - /// contracts, and storage). #[method(name = "getStorageProof")] async fn get_storage_proof( &self, @@ -197,21 +168,18 @@ pub trait StarknetApi { #[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] #[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] pub trait StarknetWriteApi { - /// Submit a new transaction to be added to the chain. #[method(name = "addInvokeTransaction")] async fn add_invoke_transaction( &self, invoke_transaction: BroadcastedInvokeTx, ) -> RpcResult; - /// Submit a new class declaration transaction. #[method(name = "addDeclareTransaction")] async fn add_declare_transaction( &self, declare_transaction: BroadcastedDeclareTx, ) -> RpcResult; - /// Submit a new deploy account transaction. #[method(name = "addDeployAccountTransaction")] async fn add_deploy_account_transaction( &self, @@ -223,11 +191,9 @@ pub trait StarknetWriteApi { #[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] #[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] pub trait StarknetTraceApi { - /// Returns the execution trace of the transaction designated by the input hash. #[method(name = "traceTransaction")] async fn trace_transaction(&self, transaction_hash: TxHash) -> RpcResult; - /// Simulates a list of transactions on the provided block. #[method(name = "simulateTransactions")] async fn simulate_transactions( &self, @@ -236,7 +202,6 @@ pub trait StarknetTraceApi { simulation_flags: Vec, ) -> RpcResult; - /// Returns the execution traces of all transactions included in the given block. #[method(name = "traceBlockTransactions")] async fn trace_block_transactions( &self, diff --git a/crates/rpc/rpc-server/Cargo.toml b/crates/rpc/rpc-server/Cargo.toml index f666acd50..914363c1b 100644 --- a/crates/rpc/rpc-server/Cargo.toml +++ b/crates/rpc/rpc-server/Cargo.toml @@ -26,7 +26,9 @@ katana-tracing.workspace = true quick_cache = "0.6.10" anyhow.workspace = true auto_impl.workspace = true +futures.workspace = true http.workspace = true +hyper = { version = "1", features = ["server"] } jsonrpsee = { workspace = true, features = [ "client", "server" ] } metrics.workspace = true serde_json.workspace = true diff --git a/crates/rpc/rpc-server/src/lib.rs b/crates/rpc/rpc-server/src/lib.rs index 00f5fabbd..4714c0f7c 100644 --- a/crates/rpc/rpc-server/src/lib.rs +++ b/crates/rpc/rpc-server/src/lib.rs @@ -4,16 +4,14 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] use std::net::SocketAddr; +use std::sync::Arc; use std::time::Duration; use jsonrpsee::core::middleware::RpcServiceBuilder; use jsonrpsee::core::{RegisterMethodError, TEN_MB_SIZE_BYTES}; -use jsonrpsee::server::{Server, ServerConfig, ServerHandle}; -use jsonrpsee::RpcModule; -use katana_tracing::gcloud::GoogleStackDriverMakeSpan; -use tower::ServiceBuilder; -use tower_http::trace::TraceLayer; -use tracing::info; +use jsonrpsee::server::{Server, ServerConfig, ServerHandle, StopHandle, TowerServiceBuilder}; +use jsonrpsee::{Methods, RpcModule}; +use tracing::{error, info}; #[cfg(feature = "cartridge")] pub mod cartridge; @@ -28,6 +26,7 @@ pub mod dev; pub mod health; pub mod metrics; pub mod permit; +mod router; pub mod starknet; pub mod txpool; @@ -38,6 +37,7 @@ use health::HealthCheck; pub use jsonrpsee::http_client::HttpClient; pub use katana_rpc_api as api; use metrics::RpcServerMetricsLayer; +pub use router::RpcRouter; /// The default maximum number of concurrent RPC connections. pub const DEFAULT_RPC_MAX_CONNECTIONS: u32 = 100; @@ -65,6 +65,10 @@ pub enum Error { Client(#[from] jsonrpsee::core::ClientError), } +// --------------------------------------------------------------------------- +// RpcServerHandle +// --------------------------------------------------------------------------- + /// The RPC server handle. #[derive(Debug, Clone)] pub struct RpcServerHandle { @@ -98,14 +102,36 @@ impl RpcServerHandle { } } +// --------------------------------------------------------------------------- +// RpcServer +// --------------------------------------------------------------------------- + +/// JSON-RPC server with path-based module routing. +/// +/// Accepts an [`RpcRouter`] that maps URL paths to modules, and handles +/// server configuration (CORS, timeouts, metrics, etc.). +/// +/// ```rust,ignore +/// let router = RpcRouter::new() +/// .route("/", v09_module.clone()) +/// .route("/rpc/v0_9", v09_module) +/// .route("/rpc/v0_10", v010_module); +/// +/// let handle = RpcServer::new(router) +/// .cors(cors) +/// .health_check(true) +/// .metrics(true) +/// .start(addr) +/// .await?; +/// ``` #[derive(Debug)] pub struct RpcServer { + router: RpcRouter, + metrics: bool, cors: Option, health_check: bool, explorer: bool, - - module: RpcModule<()>, max_connections: u32, max_request_body_size: u32, max_response_body_size: u32, @@ -115,11 +141,11 @@ pub struct RpcServer { impl RpcServer { pub fn new() -> Self { Self { + router: RpcRouter::default(), cors: None, metrics: false, explorer: false, health_check: false, - module: RpcModule::new(()), max_connections: 100, max_request_body_size: TEN_MB_SIZE_BYTES, max_response_body_size: TEN_MB_SIZE_BYTES, @@ -152,20 +178,18 @@ impl RpcServer { } /// Collect metrics about the RPC server. - /// - /// See top level module of [`crate::metrics`] to see what metrics are collected. pub fn metrics(mut self, enable: bool) -> Self { self.metrics = enable; self } - /// Enables health checking endpoint via HTTP `GET /health` + /// Enables health checking endpoint via HTTP `GET /health`. pub fn health_check(mut self, enable: bool) -> Self { self.health_check = enable; self } - /// Enables explorer. + /// Enables the embedded explorer UI. pub fn explorer(mut self, enable: bool) -> Self { self.explorer = enable; self @@ -176,40 +200,63 @@ impl RpcServer { self } - /// Adds a new RPC module to the server. - /// - /// This can be chained with other calls to `module` to add multiple modules. - /// - /// # Example - /// - /// ```rust - /// let server = RpcServer::new().module(module_a()).unwrap().module(module_b()).unwrap(); - /// ``` - pub fn module(mut self, module: RpcModule<()>) -> Result { - self.module.merge(module)?; - Ok(self) + pub fn router(mut self, router: impl Into) -> Self { + self.router = router.into(); + self } pub async fn start(&self, addr: SocketAddr) -> Result { - let mut modules = self.module.clone(); - - let health_check_proxy = if self.health_check { - modules.merge(HealthCheck)?; - Some(HealthCheck::proxy()) + use futures::FutureExt; + use jsonrpsee::server::{serve_with_graceful_shutdown, stop_channel}; + use katana_tracing::gcloud::GoogleStackDriverMakeSpan; + use tokio::net::TcpListener; + use tower::ServiceBuilder; + use tower_http::trace::TraceLayer; + + // Prepare health check module + let health_module: Option = if self.health_check { + let mut m = RpcModule::new(()); + m.merge(HealthCheck)?; + Some(m.into()) } else { None }; - #[cfg(feature = "explorer")] - let explorer_layer = if self.explorer { - let layer = katana_explorer::ExplorerLayer::builder().embedded().build().unwrap(); - Some(layer) - } else { - None - }; - - let rpc_metrics = self.metrics.then(|| RpcServerMetricsLayer::new(&modules)); + // Convert router to Methods, merging health check and building per-route + // metrics. Versioned routes get a `version` label on their metrics (e.g., + // "v0_9"), the root route uses unlabelled metrics. + let routes: Vec<(String, Methods, Option)> = self + .router + .routes + .iter() + .map(|(path, module)| { + let mut m = module.clone(); + if let Some(ref hc) = health_module { + let _ = m.merge(hc.clone()); + } + + let metrics = if self.metrics { + // Extract a version label from the path. For paths like + // "/rpc/v0_9" we use "v0_9"; for "/" we use no version label. + let version = path.rsplit('/').find(|s| !s.is_empty()); + + match version { + Some(v) if path != "/" => { + Some(RpcServerMetricsLayer::new_with_labels(module, &[("version", v)])) + } + _ => Some(RpcServerMetricsLayer::new(module)), + } + } else { + None + }; + + (path.clone(), m.into(), metrics) + }) + .collect(); + + // HTTP middleware let http_tracer = TraceLayer::new_for_http().make_span_with(GoogleStackDriverMakeSpan); + let health_check_proxy = self.health_check.then(HealthCheck::proxy); let http_middleware = ServiceBuilder::new() .layer(http_tracer) @@ -218,41 +265,101 @@ impl RpcServer { .timeout(self.timeout); #[cfg(feature = "explorer")] - let http_middleware = http_middleware.option_layer(explorer_layer); - - let rpc_middleware = - RpcServiceBuilder::new().option_layer(rpc_metrics).layer(logger::RpcLoggerLayer::new()); + let http_middleware = { + let explorer_layer = if self.explorer { + Some(katana_explorer::ExplorerLayer::builder().embedded().build().unwrap()) + } else { + None + }; + http_middleware.option_layer(explorer_layer) + }; + // Server config let cfg = ServerConfig::builder() .max_connections(self.max_connections) .max_request_body_size(self.max_request_body_size) .max_response_body_size(self.max_response_body_size) .build(); - let server = Server::builder() + let svc_builder = Server::builder() .set_http_middleware(http_middleware) - .set_rpc_middleware(rpc_middleware) .set_config(cfg) - .build(addr) - .await?; - - let actual_addr = server.local_addr()?; - let handle = server.start(modules); - - let handle = RpcServerHandle { handle, addr: actual_addr }; - - // The socket address that we log out must be from the RPC handle, in the case that the - // `addr` passed to this method has port number 0. As the 0 port will be resolved to - // a free port during the call to `ServerBuilder::build(addr)`. + .to_service_builder(); + + let listener = TcpListener::bind(addr).await?; + let actual_addr = listener.local_addr()?; + let (stop_hdl, server_handle) = stop_channel(); + + // Per-connection state. + #[derive(Clone)] + struct PerConnection { + routes: Arc)>>, + stop_handle: StopHandle, + svc_builder: TowerServiceBuilder, + } - info!(target: "rpc", addr = %handle.addr, "RPC server started."); + let per_conn = + PerConnection { svc_builder, stop_handle: stop_hdl.clone(), routes: Arc::new(routes) }; + + tokio::spawn(async move { + loop { + let stream = tokio::select! { + res = listener.accept() => { + match res { + Ok((stream, _)) => stream, + Err(e) => { + error!(target: "rpc", "failed to accept connection: {e:?}"); + continue; + } + } + } + + _ = per_conn.stop_handle.clone().shutdown() => break, + }; + + let per_conn = per_conn.clone(); + let stop_handle = per_conn.stop_handle.clone(); + + let svc = tower::service_fn(move |req: hyper::Request| { + let PerConnection { routes, stop_handle, svc_builder } = per_conn.clone(); + + // Route: first prefix match wins. Each route carries its own + // metrics layer so that method calls are labelled with the path. + let path = req.uri().path(); + let (methods, rpc_metrics) = routes + .iter() + .find(|(prefix, _, _)| path.starts_with(prefix.as_str())) + .map(|(_, m, metrics)| (m.clone(), metrics.clone())) + .unwrap_or_default(); + + let rpc_middleware = RpcServiceBuilder::new() + .option_layer(rpc_metrics) + .layer(logger::RpcLoggerLayer::new()); + + let mut svc = + svc_builder.set_rpc_middleware(rpc_middleware).build(methods, stop_handle); + + async move { tower::Service::call(&mut svc, req).await }.boxed() + }); + + tokio::spawn(serve_with_graceful_shutdown(stream, svc, stop_handle.shutdown())); + } + }); + + info!(target: "rpc", addr = %actual_addr, "RPC server started."); + + for (path, _) in &self.router.routes { + if path != "/" { + info!(target: "rpc", path = %path, "RPC module mounted."); + } + } if self.explorer { - let addr = format!("{}/explorer", handle.addr); + let addr = format!("{actual_addr}/explorer"); info!(target: "explorer", %addr, "Explorer started."); } - Ok(handle) + Ok(RpcServerHandle { handle: server_handle, addr: actual_addr }) } } @@ -269,23 +376,22 @@ mod tests { use jsonrpsee::{rpc_params, RpcModule}; - use crate::RpcServer; + use crate::{RpcRouter, RpcServer}; #[tokio::test] async fn test_rpc_server_timeout() { use jsonrpsee::core::client::ClientT; - // Create a method that never returns to simulate a long running request let mut module = RpcModule::new(()); module.register_async_method("test_timeout", |_, _, _| pending::<()>()).unwrap(); - let server = RpcServer::new().timeout(Duration::from_millis(200)).module(module).unwrap(); + let router = RpcRouter::new().route("/", module); + let server = RpcServer::new().timeout(Duration::from_millis(200)).router(router); - // Start the server let addr = "127.0.0.1:0".parse().unwrap(); - let server_handle = server.start(addr).await.unwrap(); + let handle = server.start(addr).await.unwrap(); - let client = server_handle.http_client().unwrap(); + let client = handle.http_client().unwrap(); let result = client.request::("test_timeout", rpc_params![]).await; assert!(result.is_err(), "the request failed due to timeout"); diff --git a/crates/rpc/rpc-server/src/metrics.rs b/crates/rpc/rpc-server/src/metrics.rs index 99c08f019..78e8dfae9 100644 --- a/crates/rpc/rpc-server/src/metrics.rs +++ b/crates/rpc/rpc-server/src/metrics.rs @@ -41,7 +41,6 @@ pub struct RpcServerMetrics { impl RpcServerMetrics { /// Creates a new instance of `RpcServerMetrics` for the given `RpcModule`. - /// This will create metrics for each method in the module. pub fn new(module: &RpcModule<()>) -> Self { let call_metrics = HashMap::from_iter(module.method_names().map(|method| { let metrics = RpcServerCallMetrics::new_with_labels(&[("method", method)]); @@ -55,6 +54,39 @@ impl RpcServerMetrics { }), } } + + /// Creates a new instance of `RpcServerMetrics` with additional labels + /// on each method's metrics. + /// + /// ```rust,ignore + /// RpcServerMetrics::new_with_labels(module, &[("version", "v0_9")]); + /// ``` + pub fn new_with_labels(module: &RpcModule<()>, extra_labels: &[(&str, &str)]) -> Self { + // Leak label strings to get 'static lifetimes, required by the metrics + // API. This is fine because labels are registered once at startup. + let extra: Vec<(&'static str, &'static str)> = extra_labels + .iter() + .map(|(k, v)| { + let k: &'static str = Box::leak(k.to_string().into_boxed_str()); + let v: &'static str = Box::leak(v.to_string().into_boxed_str()); + (k, v) + }) + .collect(); + + let call_metrics = HashMap::from_iter(module.method_names().map(|method| { + let mut labels = vec![("method", method)]; + labels.extend_from_slice(&extra); + let metrics = RpcServerCallMetrics::new_with_labels(&labels); + (method, metrics) + })); + + Self { + inner: Arc::new(RpcServerMetricsInner { + call_metrics, + connection_metrics: RpcServerConnectionMetrics::default(), + }), + } + } } #[derive(Default, Clone)] @@ -106,6 +138,10 @@ impl RpcServerMetricsLayer { pub fn new(module: &RpcModule<()>) -> Self { Self { metrics: RpcServerMetrics::new(module) } } + + pub fn new_with_labels(module: &RpcModule<()>, labels: &[(&str, &str)]) -> Self { + Self { metrics: RpcServerMetrics::new_with_labels(module, labels) } + } } impl Layer for RpcServerMetricsLayer { diff --git a/crates/rpc/rpc-server/src/router.rs b/crates/rpc/rpc-server/src/router.rs new file mode 100644 index 000000000..ab8bb905a --- /dev/null +++ b/crates/rpc/rpc-server/src/router.rs @@ -0,0 +1,66 @@ +//! Path-based JSON-RPC module router. + +use jsonrpsee::RpcModule; + +/// Maps URL path prefixes to JSON-RPC modules. +/// +/// Routes are matched by prefix in registration order (first match wins). +/// Use [`nest`](Self::nest) to group routes under a common prefix. +/// +/// ```rust,ignore +/// use jsonrpsee::RpcModule; +/// +/// let router = RpcRouter::new() +/// .route("/", v09_module.clone()) +/// .nest("/rpc", RpcRouter::new() +/// .route("/v0_9", v09_module) +/// .route("/v0_10", v010_module) +/// ); +/// // Equivalent to: "/", "/rpc/v0_9", "/rpc/v0_10" +/// ``` +#[derive(Debug, Default, Clone)] +pub struct RpcRouter { + pub(crate) routes: Vec<(String, RpcModule<()>)>, +} + +impl RpcRouter { + pub fn new() -> Self { + Self { routes: Vec::new() } + } + + /// Register a module at the given path prefix. + pub fn route(mut self, path: impl Into, module: RpcModule<()>) -> Self { + self.routes.push((path.into(), module)); + self + } + + /// Nest another router under a path prefix. + /// + /// All routes in `router` are prepended with `prefix`: + /// + /// ```rust,ignore + /// // These two are equivalent: + /// RpcRouter::new().nest("/rpc", RpcRouter::new().route("/v0_9", m)); + /// RpcRouter::new().route("/rpc/v0_9", m); + /// ``` + pub fn nest(mut self, prefix: impl Into, router: RpcRouter) -> Self { + let prefix = prefix.into(); + for (path, module) in router.routes { + self.routes.push((format!("{prefix}{path}"), module)); + } + self + } + + /// Merge another router's routes into this one (no prefix prepended). + pub fn merge(mut self, other: RpcRouter) -> Self { + self.routes.extend(other.routes); + self + } +} + +/// Allow constructing from a single module (mounts at `/`). +impl From> for RpcRouter { + fn from(module: RpcModule<()>) -> Self { + Self::new().route("/", module) + } +} diff --git a/crates/rpc/rpc-server/src/starknet/mod.rs b/crates/rpc/rpc-server/src/starknet/mod.rs index e9737859d..6bb118caa 100644 --- a/crates/rpc/rpc-server/src/starknet/mod.rs +++ b/crates/rpc/rpc-server/src/starknet/mod.rs @@ -66,6 +66,9 @@ mod list; mod pending; mod read; mod trace; +mod v0_10_read; +mod v0_10_trace; +mod v0_10_write; mod write; pub use cache::RpcCache; diff --git a/crates/rpc/rpc-server/src/starknet/v0_10_read.rs b/crates/rpc/rpc-server/src/starknet/v0_10_read.rs new file mode 100644 index 000000000..a6638af55 --- /dev/null +++ b/crates/rpc/rpc-server/src/starknet/v0_10_read.rs @@ -0,0 +1,321 @@ +//! v0.10 Read API implementation. +//! +//! Delegates to the same shared helpers as v0.9, converting results to v0.10 types. + +#[cfg(feature = "cartridge")] +use std::sync::Arc; + +use jsonrpsee::core::{async_trait, RpcResult}; +use jsonrpsee::types::ErrorObjectOwned; +use katana_pool::TransactionPool; +use katana_primitives::block::BlockIdOrTag; +use katana_primitives::class::ClassHash; +use katana_primitives::contract::{Nonce, StorageKey, StorageValue}; +use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; +use katana_primitives::{ContractAddress, Felt}; +#[cfg(feature = "cartridge")] +use katana_provider::api::state::StateFactoryProvider; +use katana_provider::{ProviderFactory, ProviderRO}; +use katana_rpc_api::error::starknet::StarknetApiError; +use katana_rpc_api::starknet::v0_10::StarknetApiServer; +use katana_rpc_types::block::{BlockHashAndNumberResponse, BlockNumberResponse}; +use katana_rpc_types::broadcasted::BroadcastedTx; +use katana_rpc_types::message::MsgFromL1; +use katana_rpc_types::receipt::TxReceiptWithBlockInfo; +use katana_rpc_types::transaction::RpcTxWithHash; +use katana_rpc_types::trie::{ContractStorageKeys, GetStorageProofResponse}; +use katana_rpc_types::v0_10::block::{ + GetBlockWithReceiptsResponse, GetBlockWithTxHashesResponse, MaybePreConfirmedBlock, +}; +use katana_rpc_types::v0_10::event::GetEventsResponse; +use katana_rpc_types::v0_10::state_update::StateUpdate; +use katana_rpc_types::{ + BroadcastedTxWithChainId, CallResponse, CasmClass, Class, EstimateFeeSimulationFlag, + FeeEstimate, FunctionCall, TxStatus, +}; + +use super::StarknetApi; +#[cfg(feature = "cartridge")] +use crate::cartridge; +use crate::starknet::pending::PendingBlockProvider; + +#[async_trait] +impl StarknetApiServer for StarknetApi +where + Pool: TransactionPool + Send + Sync + 'static, + PoolTx: From, + Pending: PendingBlockProvider, + PF: ProviderFactory, + ::Provider: ProviderRO, +{ + async fn chain_id(&self) -> RpcResult { + Ok(self.inner.chain_spec.id().id()) + } + + async fn get_nonce( + &self, + block_id: BlockIdOrTag, + contract_address: ContractAddress, + ) -> RpcResult { + Ok(self.nonce_at(block_id, contract_address).await?) + } + + async fn block_number(&self) -> RpcResult { + Ok(self.latest_block_number().await?) + } + + async fn get_transaction_by_hash(&self, transaction_hash: TxHash) -> RpcResult { + Ok(self.transaction(transaction_hash).await?) + } + + async fn get_block_transaction_count(&self, block_id: BlockIdOrTag) -> RpcResult { + Ok(self.block_tx_count(block_id).await?) + } + + async fn get_class_at( + &self, + block_id: BlockIdOrTag, + contract_address: ContractAddress, + ) -> RpcResult { + Ok(self.class_at_address(block_id, contract_address).await?) + } + + async fn block_hash_and_number(&self) -> RpcResult { + Ok(self.block_hash_and_number().await?) + } + + async fn get_block_with_tx_hashes( + &self, + block_id: BlockIdOrTag, + ) -> RpcResult { + Ok(self.block_with_tx_hashes(block_id).await?.into()) + } + + async fn get_transaction_by_block_id_and_index( + &self, + block_id: BlockIdOrTag, + index: u64, + ) -> RpcResult { + Ok(self.transaction_by_block_id_and_index(block_id, index).await?) + } + + async fn get_block_with_txs( + &self, + block_id: BlockIdOrTag, + ) -> RpcResult { + Ok(self.block_with_txs(block_id).await?.into()) + } + + async fn get_block_with_receipts( + &self, + block_id: BlockIdOrTag, + ) -> RpcResult { + Ok(self.block_with_receipts(block_id).await?.into()) + } + + async fn get_state_update(&self, block_id: BlockIdOrTag) -> RpcResult { + let state_update = self.state_update(block_id).await?; + Ok(state_update.into()) + } + + async fn get_transaction_receipt( + &self, + transaction_hash: TxHash, + ) -> RpcResult { + Ok(self.receipt(transaction_hash).await?) + } + + async fn get_class_hash_at( + &self, + block_id: BlockIdOrTag, + contract_address: ContractAddress, + ) -> RpcResult { + Ok(self.class_hash_at_address(block_id, contract_address).await?) + } + + async fn get_class(&self, block_id: BlockIdOrTag, class_hash: ClassHash) -> RpcResult { + Ok(self.class_at_hash(block_id, class_hash).await?) + } + + async fn get_compiled_casm(&self, class_hash: ClassHash) -> RpcResult { + Ok(self.compiled_class_at_hash(class_hash).await?) + } + + async fn get_events( + &self, + filter: katana_rpc_types::v0_10::event::EventFilterWithPage, + ) -> RpcResult { + // v0.10 EventFilterWithPage re-exports the same type, so we can pass it directly + // to the shared helper, then convert the response. + let shared_filter = katana_rpc_types::event::EventFilterWithPage { + event_filter: filter.event_filter, + result_page_request: filter.result_page_request, + }; + Ok(self.events(shared_filter).await?.into()) + } + + async fn call(&self, request: FunctionCall, block_id: BlockIdOrTag) -> RpcResult { + Ok(self.call_contract(request, block_id).await?) + } + + async fn get_storage_at( + &self, + contract_address: ContractAddress, + key: StorageKey, + block_id: BlockIdOrTag, + ) -> RpcResult { + Ok(self.storage_at(contract_address, key, block_id).await?) + } + + async fn estimate_fee( + &self, + request: Vec, + simulation_flags: Vec, + block_id: BlockIdOrTag, + ) -> RpcResult> { + let chain = self.inner.chain_spec.id(); + + let transactions = request + .into_iter() + .map(|tx| { + let is_query = tx.is_query(); + let tx = ExecutableTx::from(BroadcastedTxWithChainId { tx, chain }); + ExecutableTxWithHash::new_query(tx, is_query) + }) + .collect::>(); + + let skip_validate = simulation_flags.contains(&EstimateFeeSimulationFlag::SkipValidate); + + let should_validate = + !skip_validate && self.inner.config.simulation_flags.account_validation(); + + let flags = katana_executor::ExecutionFlags::new() + .with_account_validation(should_validate) + .with_nonce_check(false); + + #[cfg(feature = "cartridge")] + let transactions = if let Some(paymaster) = &self.inner.config.paymaster { + let paymaster_address = paymaster.paymaster_address; + let paymaster_private_key = paymaster.paymaster_private_key; + + let state = + self.storage().provider().latest().map(Arc::new).map_err(StarknetApiError::from)?; + + let mut ctrl_deploy_txs = Vec::new(); + + let paymaster_nonce = match self.nonce_at(block_id, paymaster_address).await { + Ok(nonce) => nonce, + Err(err) => match err { + StarknetApiError::ContractNotFound => { + return Err(StarknetApiError::unexpected( + "Cartridge paymaster account doesn't exist", + ) + .into()); + } + _ => return Err(ErrorObjectOwned::from(err)), + }, + }; + + for tx in &transactions { + let api = ::cartridge::Client::new(paymaster.cartridge_api_url.clone()); + + let deploy_controller_tx = + cartridge::get_controller_deploy_tx_if_controller_address( + paymaster_address, + paymaster_private_key, + paymaster_nonce, + tx, + self.inner.chain_spec.id(), + state.clone(), + &api, + ) + .await + .map_err(StarknetApiError::from)?; + + if let Some(tx) = deploy_controller_tx { + ctrl_deploy_txs.push(tx); + } + } + + if !ctrl_deploy_txs.is_empty() { + ctrl_deploy_txs.extend(transactions); + ctrl_deploy_txs + } else { + transactions + } + } else { + transactions + }; + + let permit = + self.inner.estimate_fee_permit.acquire().await.map_err(|e| { + StarknetApiError::unexpected(format!("Failed to acquire permit: {e}")) + })?; + + self.on_cpu_blocking_task(move |this| async move { + let _permit = permit; + let results = this.estimate_fee_with(transactions, block_id, flags)?; + Ok(results) + }) + .await? + } + + async fn estimate_message_fee( + &self, + message: MsgFromL1, + block_id: BlockIdOrTag, + ) -> RpcResult { + self.on_cpu_blocking_task(move |this| async move { + let chain_id = this.inner.chain_spec.id(); + + let tx = message.into_tx_with_chain_id(chain_id); + let hash = tx.calculate_hash(); + + let result = this.estimate_fee_with( + vec![ExecutableTxWithHash { hash, transaction: tx.into() }], + block_id, + Default::default(), + ); + + match result { + Ok(mut res) => { + if let Some(fee) = res.pop() { + Ok(FeeEstimate { + overall_fee: fee.overall_fee, + l2_gas_price: fee.l2_gas_price, + l1_gas_price: fee.l1_gas_price, + l2_gas_consumed: fee.l2_gas_consumed, + l1_gas_consumed: fee.l1_gas_consumed, + l1_data_gas_price: fee.l1_data_gas_price, + l1_data_gas_consumed: fee.l1_data_gas_consumed, + }) + } else { + Err(ErrorObjectOwned::from(StarknetApiError::unexpected( + "Fee estimation result should exist", + ))) + } + } + + Err(err) => Err(ErrorObjectOwned::from(err)), + } + }) + .await? + } + + async fn get_transaction_status(&self, transaction_hash: TxHash) -> RpcResult { + Ok(self.transaction_status(transaction_hash).await?) + } + + async fn get_storage_proof( + &self, + block_id: BlockIdOrTag, + class_hashes: Option>, + contract_addresses: Option>, + contracts_storage_keys: Option>, + ) -> RpcResult { + let proofs = self + .get_proofs(block_id, class_hashes, contract_addresses, contracts_storage_keys) + .await?; + Ok(proofs) + } +} diff --git a/crates/rpc/rpc-server/src/starknet/v0_10_trace.rs b/crates/rpc/rpc-server/src/starknet/v0_10_trace.rs new file mode 100644 index 000000000..8fbf54762 --- /dev/null +++ b/crates/rpc/rpc-server/src/starknet/v0_10_trace.rs @@ -0,0 +1,50 @@ +//! v0.10 Trace API implementation. +//! +//! Trace API types are identical between v0.9 and v0.10 — thin delegation. + +use jsonrpsee::core::{async_trait, RpcResult}; +use katana_pool::TransactionPool; +use katana_primitives::block::{BlockIdOrTag, ConfirmedBlockIdOrTag}; +use katana_primitives::transaction::TxHash; +use katana_provider::{ProviderFactory, ProviderRO}; +use katana_rpc_api::starknet::v0_10::StarknetTraceApiServer; +use katana_rpc_types::broadcasted::BroadcastedTx; +use katana_rpc_types::trace::{ + SimulatedTransactionsResponse, TraceBlockTransactionsResponse, TxTrace, +}; +use katana_rpc_types::{BroadcastedTxWithChainId, SimulationFlag}; + +use super::StarknetApi; +use crate::starknet::pending::PendingBlockProvider; + +#[async_trait] +impl StarknetTraceApiServer for StarknetApi +where + Pool: TransactionPool + Send + Sync + 'static, + PoolTx: From, + Pending: PendingBlockProvider, + PF: ProviderFactory, + ::Provider: ProviderRO, +{ + async fn trace_transaction(&self, transaction_hash: TxHash) -> RpcResult { + Ok(self.trace(transaction_hash).await?) + } + + async fn simulate_transactions( + &self, + block_id: BlockIdOrTag, + transactions: Vec, + simulation_flags: Vec, + ) -> RpcResult { + let transactions = self.simulate_txs(block_id, transactions, simulation_flags).await?; + Ok(SimulatedTransactionsResponse { transactions }) + } + + async fn trace_block_transactions( + &self, + block_id: ConfirmedBlockIdOrTag, + ) -> RpcResult { + let traces = self.block_traces(block_id).await?; + Ok(TraceBlockTransactionsResponse { traces }) + } +} diff --git a/crates/rpc/rpc-server/src/starknet/v0_10_write.rs b/crates/rpc/rpc-server/src/starknet/v0_10_write.rs new file mode 100644 index 000000000..05ab60485 --- /dev/null +++ b/crates/rpc/rpc-server/src/starknet/v0_10_write.rs @@ -0,0 +1,47 @@ +//! v0.10 Write API implementation. +//! +//! Write API types are identical between v0.9 and v0.10 — thin delegation. + +use jsonrpsee::core::{async_trait, RpcResult}; +use katana_pool::TransactionPool; +use katana_provider::ProviderFactory; +use katana_rpc_api::starknet::v0_10::StarknetWriteApiServer; +use katana_rpc_types::broadcasted::{ + AddDeclareTransactionResponse, AddDeployAccountTransactionResponse, + AddInvokeTransactionResponse, BroadcastedDeclareTx, BroadcastedDeployAccountTx, + BroadcastedInvokeTx, +}; +use katana_rpc_types::BroadcastedTxWithChainId; + +use super::StarknetApi; +use crate::starknet::pending::PendingBlockProvider; + +#[async_trait] +impl StarknetWriteApiServer for StarknetApi +where + Pool: TransactionPool + Send + Sync + 'static, + PoolTx: From, + Pending: PendingBlockProvider, + PF: ProviderFactory, +{ + async fn add_invoke_transaction( + &self, + invoke_transaction: BroadcastedInvokeTx, + ) -> RpcResult { + Ok(self.add_invoke_tx(invoke_transaction).await?) + } + + async fn add_declare_transaction( + &self, + declare_transaction: BroadcastedDeclareTx, + ) -> RpcResult { + Ok(self.add_declare_tx(declare_transaction).await?) + } + + async fn add_deploy_account_transaction( + &self, + deploy_account_transaction: BroadcastedDeployAccountTx, + ) -> RpcResult { + Ok(self.add_deploy_account_tx(deploy_account_transaction).await?) + } +} diff --git a/crates/rpc/rpc-server/tests/estimate_fee_rate_limit.rs b/crates/rpc/rpc-server/tests/estimate_fee_rate_limit.rs index a347e3301..bad284009 100644 --- a/crates/rpc/rpc-server/tests/estimate_fee_rate_limit.rs +++ b/crates/rpc/rpc-server/tests/estimate_fee_rate_limit.rs @@ -17,7 +17,8 @@ async fn test_estimate_fee_rate_limiting() -> Result<()> { let mut config = katana_utils::node::test_config(); let max_concurrent_estimate_fee_requests = 2; - config.rpc.max_concurrent_estimate_fee_requests = Some(max_concurrent_estimate_fee_requests); + config.rpc.starknet.max_concurrent_estimate_fee_requests = + Some(max_concurrent_estimate_fee_requests); let sequencer = TestNode::new_with_config(config).await; diff --git a/crates/rpc/rpc-types/src/block.rs b/crates/rpc/rpc-types/src/block.rs index 21dafe9fd..fd3946f65 100644 --- a/crates/rpc/rpc-types/src/block.rs +++ b/crates/rpc/rpc-types/src/block.rs @@ -15,6 +15,7 @@ pub type BlockTxCount = u64; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] +#[allow(clippy::large_enum_variant)] pub enum MaybePreConfirmedBlock { Confirmed(BlockWithTxs), PreConfirmed(PreConfirmedBlockWithTxs), @@ -35,6 +36,21 @@ pub struct BlockWithTxs { pub l1_da_mode: L1DataAvailabilityMode, pub starknet_version: String, pub transactions: Vec, + // Commitment fields — populated from Header but not serialized in v0.9. + #[serde(skip)] + pub event_commitment: Felt, + #[serde(skip)] + pub event_count: u32, + #[serde(skip)] + pub receipt_commitment: Felt, + #[serde(skip)] + pub state_diff_commitment: Felt, + #[serde(skip)] + pub state_diff_length: u32, + #[serde(skip)] + pub transaction_commitment: Felt, + #[serde(skip)] + pub transaction_count: u32, } impl BlockWithTxs { @@ -70,6 +86,13 @@ impl BlockWithTxs { status: finality_status, l1_da_mode: block.header.l1_da_mode, l1_data_gas_price, + event_commitment: block.header.events_commitment, + event_count: block.header.events_count, + receipt_commitment: block.header.receipts_commitment, + state_diff_commitment: block.header.state_diff_commitment, + state_diff_length: block.header.state_diff_length, + transaction_commitment: block.header.transactions_commitment, + transaction_count: block.header.transaction_count, } } } @@ -122,6 +145,7 @@ impl PreConfirmedBlockWithTxs { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(untagged)] +#[allow(clippy::large_enum_variant)] pub enum GetBlockWithTxHashesResponse { Block(BlockWithTxHashes), PreConfirmed(PreConfirmedBlockWithTxHashes), @@ -142,6 +166,20 @@ pub struct BlockWithTxHashes { pub l1_da_mode: L1DataAvailabilityMode, pub starknet_version: String, pub transactions: Vec, + #[serde(skip)] + pub event_commitment: Felt, + #[serde(skip)] + pub event_count: u32, + #[serde(skip)] + pub receipt_commitment: Felt, + #[serde(skip)] + pub state_diff_commitment: Felt, + #[serde(skip)] + pub state_diff_length: u32, + #[serde(skip)] + pub transaction_commitment: Felt, + #[serde(skip)] + pub transaction_count: u32, } impl BlockWithTxHashes { @@ -179,6 +217,13 @@ impl BlockWithTxHashes { status: finality_status, l1_da_mode: block.header.l1_da_mode, l1_data_gas_price, + event_commitment: block.header.events_commitment, + event_count: block.header.events_count, + receipt_commitment: block.header.receipts_commitment, + state_diff_commitment: block.header.state_diff_commitment, + state_diff_length: block.header.state_diff_length, + transaction_commitment: block.header.transactions_commitment, + transaction_count: block.header.transaction_count, } } } @@ -267,6 +312,7 @@ impl BlockHashAndNumberResponse { #[derive(Debug, Clone, Serialize)] #[serde(untagged)] +#[allow(clippy::large_enum_variant)] pub enum GetBlockWithReceiptsResponse { Block(BlockWithReceipts), PreConfirmed(PreConfirmedBlockWithReceipts), @@ -326,6 +372,13 @@ impl<'de> Deserialize<'de> for GetBlockWithReceiptsResponse { l1_da_mode, starknet_version, transactions, + event_commitment: Felt::ZERO, + event_count: 0, + receipt_commitment: Felt::ZERO, + state_diff_commitment: Felt::ZERO, + state_diff_length: 0, + transaction_commitment: Felt::ZERO, + transaction_count: 0, })) } else { Ok(GetBlockWithReceiptsResponse::PreConfirmed(PreConfirmedBlockWithReceipts { @@ -358,6 +411,20 @@ pub struct BlockWithReceipts { pub l1_da_mode: L1DataAvailabilityMode, pub starknet_version: String, pub transactions: Vec, + #[serde(skip)] + pub event_commitment: Felt, + #[serde(skip)] + pub event_count: u32, + #[serde(skip)] + pub receipt_commitment: Felt, + #[serde(skip)] + pub state_diff_commitment: Felt, + #[serde(skip)] + pub state_diff_length: u32, + #[serde(skip)] + pub transaction_commitment: Felt, + #[serde(skip)] + pub transaction_count: u32, } impl BlockWithReceipts { @@ -404,6 +471,13 @@ impl BlockWithReceipts { l1_da_mode: L1DataAvailabilityMode::Calldata, starknet_version: header.starknet_version.to_string(), transactions, + event_commitment: header.events_commitment, + event_count: header.events_count, + receipt_commitment: header.receipts_commitment, + state_diff_commitment: header.state_diff_commitment, + state_diff_length: header.state_diff_length, + transaction_commitment: header.transactions_commitment, + transaction_count: header.transaction_count, } } } diff --git a/crates/rpc/rpc-types/src/lib.rs b/crates/rpc/rpc-types/src/lib.rs index 80e9a6334..9d5a0cf95 100644 --- a/crates/rpc/rpc-types/src/lib.rs +++ b/crates/rpc/rpc-types/src/lib.rs @@ -23,6 +23,11 @@ pub mod transaction; pub mod trie; pub mod txpool; +/// Versioned types for Starknet JSON-RPC spec v0.10.0. +pub mod v0_10; +/// Versioned types for Starknet JSON-RPC spec v0.9.0. +pub mod v0_9; + pub use block::*; pub use broadcasted::*; pub use cartridge::*; diff --git a/crates/rpc/rpc-types/src/v0_10/block.rs b/crates/rpc/rpc-types/src/v0_10/block.rs new file mode 100644 index 000000000..bb07719cc --- /dev/null +++ b/crates/rpc/rpc-types/src/v0_10/block.rs @@ -0,0 +1,264 @@ +//! Block types for Starknet spec v0.10.0. +//! +//! Adds 7 new fields to confirmed block headers: +//! `event_commitment`, `event_count`, `receipt_commitment`, `state_diff_commitment`, +//! `state_diff_length`, `transaction_commitment`, `transaction_count`. +//! +//! These types are constructed via `From` conversions from the shared block types, +//! which carry the commitment data as `#[serde(skip)]` fields. + +use katana_primitives::block::{BlockHash, BlockNumber, FinalityStatus}; +use katana_primitives::da::L1DataAvailabilityMode; +use katana_primitives::transaction::TxHash; +use katana_primitives::{ContractAddress, Felt}; +use serde::{Deserialize, Serialize}; +use starknet::core::types::ResourcePrice; + +// Re-export types that are identical to v0.9. +pub use crate::block::{ + BlockHashAndNumberResponse, BlockNumberResponse, BlockTxCount, PreConfirmedBlockWithReceipts, + PreConfirmedBlockWithTxHashes, PreConfirmedBlockWithTxs, RpcTxWithReceipt, +}; +use crate::transaction::RpcTxWithHash; + +// ---------- BlockWithTxs ---------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum MaybePreConfirmedBlock { + Confirmed(BlockWithTxs), + PreConfirmed(PreConfirmedBlockWithTxs), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BlockWithTxs { + pub status: FinalityStatus, + pub block_hash: BlockHash, + pub parent_hash: BlockHash, + pub block_number: BlockNumber, + pub new_root: Felt, + pub timestamp: u64, + pub sequencer_address: ContractAddress, + pub l1_gas_price: ResourcePrice, + pub l2_gas_price: ResourcePrice, + pub l1_data_gas_price: ResourcePrice, + pub l1_da_mode: L1DataAvailabilityMode, + pub starknet_version: String, + pub event_commitment: Felt, + pub event_count: u32, + pub receipt_commitment: Felt, + pub state_diff_commitment: Felt, + pub state_diff_length: u32, + pub transaction_commitment: Felt, + pub transaction_count: u32, + pub transactions: Vec, +} + +impl From for BlockWithTxs { + fn from(b: crate::block::BlockWithTxs) -> Self { + Self { + status: b.status, + block_hash: b.block_hash, + parent_hash: b.parent_hash, + block_number: b.block_number, + new_root: b.new_root, + timestamp: b.timestamp, + sequencer_address: b.sequencer_address, + l1_gas_price: b.l1_gas_price, + l2_gas_price: b.l2_gas_price, + l1_data_gas_price: b.l1_data_gas_price, + l1_da_mode: b.l1_da_mode, + starknet_version: b.starknet_version, + event_commitment: b.event_commitment, + event_count: b.event_count, + receipt_commitment: b.receipt_commitment, + state_diff_commitment: b.state_diff_commitment, + state_diff_length: b.state_diff_length, + transaction_commitment: b.transaction_commitment, + transaction_count: b.transaction_count, + transactions: b.transactions, + } + } +} + +impl From for MaybePreConfirmedBlock { + fn from(b: crate::block::MaybePreConfirmedBlock) -> Self { + match b { + crate::block::MaybePreConfirmedBlock::Confirmed(b) => { + MaybePreConfirmedBlock::Confirmed(b.into()) + } + crate::block::MaybePreConfirmedBlock::PreConfirmed(b) => { + MaybePreConfirmedBlock::PreConfirmed(b) + } + } + } +} + +// ---------- BlockWithTxHashes ---------- + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum GetBlockWithTxHashesResponse { + Block(BlockWithTxHashes), + PreConfirmed(PreConfirmedBlockWithTxHashes), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BlockWithTxHashes { + pub status: FinalityStatus, + pub block_hash: BlockHash, + pub parent_hash: BlockHash, + pub block_number: BlockNumber, + pub new_root: Felt, + pub timestamp: u64, + pub sequencer_address: ContractAddress, + pub l1_gas_price: ResourcePrice, + pub l2_gas_price: ResourcePrice, + pub l1_data_gas_price: ResourcePrice, + pub l1_da_mode: L1DataAvailabilityMode, + pub starknet_version: String, + pub event_commitment: Felt, + pub event_count: u32, + pub receipt_commitment: Felt, + pub state_diff_commitment: Felt, + pub state_diff_length: u32, + pub transaction_commitment: Felt, + pub transaction_count: u32, + pub transactions: Vec, +} + +impl From for BlockWithTxHashes { + fn from(b: crate::block::BlockWithTxHashes) -> Self { + Self { + status: b.status, + block_hash: b.block_hash, + parent_hash: b.parent_hash, + block_number: b.block_number, + new_root: b.new_root, + timestamp: b.timestamp, + sequencer_address: b.sequencer_address, + l1_gas_price: b.l1_gas_price, + l2_gas_price: b.l2_gas_price, + l1_data_gas_price: b.l1_data_gas_price, + l1_da_mode: b.l1_da_mode, + starknet_version: b.starknet_version, + event_commitment: b.event_commitment, + event_count: b.event_count, + receipt_commitment: b.receipt_commitment, + state_diff_commitment: b.state_diff_commitment, + state_diff_length: b.state_diff_length, + transaction_commitment: b.transaction_commitment, + transaction_count: b.transaction_count, + transactions: b.transactions, + } + } +} + +impl From for GetBlockWithTxHashesResponse { + fn from(r: crate::block::GetBlockWithTxHashesResponse) -> Self { + match r { + crate::block::GetBlockWithTxHashesResponse::Block(b) => { + GetBlockWithTxHashesResponse::Block(b.into()) + } + crate::block::GetBlockWithTxHashesResponse::PreConfirmed(b) => { + GetBlockWithTxHashesResponse::PreConfirmed(b) + } + } + } +} + +// ---------- BlockWithReceipts ---------- + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum GetBlockWithReceiptsResponse { + Block(BlockWithReceipts), + PreConfirmed(PreConfirmedBlockWithReceipts), +} + +impl<'de> Deserialize<'de> for GetBlockWithReceiptsResponse { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = + serde_json::Value::deserialize(deserializer).map_err(serde::de::Error::custom)?; + + if value.get("block_hash").is_some() { + let block = serde_json::from_value::(value) + .map_err(serde::de::Error::custom)?; + Ok(GetBlockWithReceiptsResponse::Block(block)) + } else { + let block = serde_json::from_value::(value) + .map_err(serde::de::Error::custom)?; + Ok(GetBlockWithReceiptsResponse::PreConfirmed(block)) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BlockWithReceipts { + pub status: FinalityStatus, + pub block_hash: BlockHash, + pub parent_hash: BlockHash, + pub block_number: BlockNumber, + pub new_root: Felt, + pub timestamp: u64, + pub sequencer_address: ContractAddress, + pub l1_gas_price: ResourcePrice, + pub l2_gas_price: ResourcePrice, + pub l1_data_gas_price: ResourcePrice, + pub l1_da_mode: L1DataAvailabilityMode, + pub starknet_version: String, + pub event_commitment: Felt, + pub event_count: u32, + pub receipt_commitment: Felt, + pub state_diff_commitment: Felt, + pub state_diff_length: u32, + pub transaction_commitment: Felt, + pub transaction_count: u32, + pub transactions: Vec, +} + +impl From for BlockWithReceipts { + fn from(b: crate::block::BlockWithReceipts) -> Self { + Self { + status: b.status, + block_hash: b.block_hash, + parent_hash: b.parent_hash, + block_number: b.block_number, + new_root: b.new_root, + timestamp: b.timestamp, + sequencer_address: b.sequencer_address, + l1_gas_price: b.l1_gas_price, + l2_gas_price: b.l2_gas_price, + l1_data_gas_price: b.l1_data_gas_price, + l1_da_mode: b.l1_da_mode, + starknet_version: b.starknet_version, + event_commitment: b.event_commitment, + event_count: b.event_count, + receipt_commitment: b.receipt_commitment, + state_diff_commitment: b.state_diff_commitment, + state_diff_length: b.state_diff_length, + transaction_commitment: b.transaction_commitment, + transaction_count: b.transaction_count, + transactions: b.transactions, + } + } +} + +impl From for GetBlockWithReceiptsResponse { + fn from(r: crate::block::GetBlockWithReceiptsResponse) -> Self { + match r { + crate::block::GetBlockWithReceiptsResponse::Block(b) => { + GetBlockWithReceiptsResponse::Block(b.into()) + } + crate::block::GetBlockWithReceiptsResponse::PreConfirmed(b) => { + GetBlockWithReceiptsResponse::PreConfirmed(b) + } + } + } +} diff --git a/crates/rpc/rpc-types/src/v0_10/event.rs b/crates/rpc/rpc-types/src/v0_10/event.rs new file mode 100644 index 000000000..1c31c32a8 --- /dev/null +++ b/crates/rpc/rpc-types/src/v0_10/event.rs @@ -0,0 +1,67 @@ +//! Event types for Starknet spec v0.10.0. +//! +//! In v0.10, `event_index` and `transaction_index` are required fields (not optional). + +use katana_primitives::block::{BlockHash, BlockNumber}; +use katana_primitives::transaction::TxHash; +use katana_primitives::{ContractAddress, Felt}; +use serde::{Deserialize, Serialize}; + +// Re-export unchanged types. +pub use crate::event::{EventFilter, EventFilterWithPage, ResultPageRequest}; + +/// A "page" of events in a cursor-based pagination system. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GetEventsResponse { + /// Matching events + pub events: Vec, + + /// A pointer to the last element of the delivered page. + #[serde(skip_serializing_if = "Option::is_none")] + pub continuation_token: Option, +} + +/// Emitted event for v0.10 — `event_index` and `transaction_index` are required. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EmittedEvent { + /// The hash of the block in which the event was emitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub block_hash: Option, + /// The number of the block in which the event was emitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub block_number: Option, + /// The hash of the transaction where the event was emitted. + pub transaction_hash: TxHash, + /// The index of the transaction in the block (required in v0.10). + pub transaction_index: u64, + /// The index of the event within the transaction (required in v0.10). + pub event_index: u64, + /// The address of the contract that emitted the event. + pub from_address: ContractAddress, + pub keys: Vec, + pub data: Vec, +} + +impl From for EmittedEvent { + fn from(e: crate::event::EmittedEvent) -> Self { + Self { + block_hash: e.block_hash, + block_number: e.block_number, + transaction_hash: e.transaction_hash, + transaction_index: e.transaction_index.unwrap_or(0), + event_index: e.event_index.unwrap_or(0), + from_address: e.from_address, + keys: e.keys, + data: e.data, + } + } +} + +impl From for GetEventsResponse { + fn from(r: crate::event::GetEventsResponse) -> Self { + Self { + events: r.events.into_iter().map(Into::into).collect(), + continuation_token: r.continuation_token, + } + } +} diff --git a/crates/rpc/rpc-types/src/v0_10/mod.rs b/crates/rpc/rpc-types/src/v0_10/mod.rs new file mode 100644 index 000000000..a6b996445 --- /dev/null +++ b/crates/rpc/rpc-types/src/v0_10/mod.rs @@ -0,0 +1,12 @@ +//! Starknet JSON-RPC types for spec version 0.10.0. +//! +//! This module defines types that differ from the v0.9 spec. The key changes are: +//! +//! - **BlockHeader**: Adds 7 commitment/count fields. +//! - **EmittedEvent**: `event_index` and `transaction_index` are required (not optional). +//! - **StateDiff**: `migrated_compiled_classes` is required (always serialized). +//! - **PreConfirmedStateUpdate**: `old_root` is optional. + +pub mod block; +pub mod event; +pub mod state_update; diff --git a/crates/rpc/rpc-types/src/v0_10/state_update.rs b/crates/rpc/rpc-types/src/v0_10/state_update.rs new file mode 100644 index 000000000..8e911f176 --- /dev/null +++ b/crates/rpc/rpc-types/src/v0_10/state_update.rs @@ -0,0 +1,103 @@ +//! State update types for Starknet spec v0.10.0. +//! +//! In v0.10: +//! - `StateDiff::migrated_compiled_classes` is required (always serialized, defaults to empty). +//! - `PreConfirmedStateUpdate::old_root` is optional. + +use std::collections::BTreeMap; + +use katana_primitives::block::BlockHash; +use katana_primitives::Felt; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum StateUpdate { + Confirmed(ConfirmedStateUpdate), + PreConfirmed(PreConfirmedStateUpdate), +} + +/// State update of a confirmed block (same structure as v0.9). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConfirmedStateUpdate { + pub block_hash: BlockHash, + pub new_root: Felt, + pub old_root: Felt, + pub state_diff: StateDiff, +} + +/// State update of a pre-confirmed block. +/// +/// In v0.10, `old_root` is optional (removed from required fields in the spec). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PreConfirmedStateUpdate { + #[serde(skip_serializing_if = "Option::is_none")] + pub old_root: Option, + pub state_diff: StateDiff, +} + +/// v0.10 StateDiff — `migrated_compiled_classes` is always serialized (required). +/// +/// We wrap the shared `StateDiff` and override serialization to always include the field. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StateDiff(pub crate::state_update::StateDiff); + +impl serde::Serialize for StateDiff { + fn serialize(&self, serializer: S) -> Result { + // Ensure migrated_compiled_classes is always present (empty if None). + let mut inner = self.0.clone(); + if inner.migrated_compiled_classes.is_none() { + inner.migrated_compiled_classes = Some(BTreeMap::new()); + } + + // Delegate to the inner type's serialization. + // Since the inner Serialize always includes migrated_compiled_classes when Some, + // this will always include the field. + inner.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for StateDiff { + fn deserialize>(deserializer: D) -> Result { + let mut inner = crate::state_update::StateDiff::deserialize(deserializer)?; + // In v0.10, migrated_compiled_classes is required — default to empty if not present. + if inner.migrated_compiled_classes.is_none() { + inner.migrated_compiled_classes = Some(BTreeMap::new()); + } + Ok(StateDiff(inner)) + } +} + +impl From for StateDiff { + fn from(inner: crate::state_update::StateDiff) -> Self { + StateDiff(inner) + } +} + +impl From for StateDiff { + fn from(value: katana_primitives::state::StateUpdates) -> Self { + let inner: crate::state_update::StateDiff = value.into(); + StateDiff(inner) + } +} + +impl From for StateUpdate { + fn from(su: crate::state_update::StateUpdate) -> Self { + match su { + crate::state_update::StateUpdate::Confirmed(c) => { + StateUpdate::Confirmed(ConfirmedStateUpdate { + block_hash: c.block_hash, + new_root: c.new_root, + old_root: c.old_root, + state_diff: StateDiff(c.state_diff), + }) + } + crate::state_update::StateUpdate::PreConfirmed(p) => { + StateUpdate::PreConfirmed(PreConfirmedStateUpdate { + old_root: p.old_root, + state_diff: StateDiff(p.state_diff), + }) + } + } + } +} diff --git a/crates/rpc/rpc-types/src/v0_9/mod.rs b/crates/rpc/rpc-types/src/v0_9/mod.rs new file mode 100644 index 000000000..18971c324 --- /dev/null +++ b/crates/rpc/rpc-types/src/v0_9/mod.rs @@ -0,0 +1,22 @@ +//! Starknet JSON-RPC types for spec version 0.9.0. +//! +//! This module re-exports the existing types which are already v0.9-compatible. + +pub mod block { + pub use crate::block::{ + BlockHashAndNumberResponse, BlockNumberResponse, BlockTxCount, BlockWithReceipts, + BlockWithTxHashes, BlockWithTxs, GetBlockWithReceiptsResponse, + GetBlockWithTxHashesResponse, MaybePreConfirmedBlock, PreConfirmedBlockWithReceipts, + PreConfirmedBlockWithTxHashes, PreConfirmedBlockWithTxs, RpcTxWithReceipt, + }; +} + +pub mod event { + pub use crate::event::{EmittedEvent, EventFilterWithPage, GetEventsResponse}; +} + +pub mod state_update { + pub use crate::state_update::{ + ConfirmedStateUpdate, PreConfirmedStateUpdate, StateDiff, StateUpdate, + }; +} diff --git a/crates/storage/fork/src/lib.rs b/crates/storage/fork/src/lib.rs index bbb158a2c..0f3a67df2 100644 --- a/crates/storage/fork/src/lib.rs +++ b/crates/storage/fork/src/lib.rs @@ -391,6 +391,7 @@ impl Backend { /// RPC request is made to the remote provider. When that request completes, the same response /// needs to be distributed to all waiting senders, which requires cloning the response for each /// sender in the deduplication vector. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] enum BackendResponse { Receipt(BackendResult), diff --git a/crates/sync/stage/src/blocks/downloader.rs b/crates/sync/stage/src/blocks/downloader.rs index aaf95e8a7..7f569f550 100644 --- a/crates/sync/stage/src/blocks/downloader.rs +++ b/crates/sync/stage/src/blocks/downloader.rs @@ -436,6 +436,7 @@ pub mod json_rpc { l1_da_mode, starknet_version, transactions: tx_with_receipts, + .. } = match block_resp { GetBlockWithReceiptsResponse::Block(b) => b, GetBlockWithReceiptsResponse::PreConfirmed(_) => { diff --git a/crates/utils/src/node.rs b/crates/utils/src/node.rs index 17ef61f1f..b0369230f 100644 --- a/crates/utils/src/node.rs +++ b/crates/utils/src/node.rs @@ -15,7 +15,9 @@ use katana_rpc_server::HttpClient; use katana_sequencer_node::config::db::DbConfig; use katana_sequencer_node::config::dev::DevConfig; use katana_sequencer_node::config::grpc::{GrpcConfig, DEFAULT_GRPC_ADDR}; -use katana_sequencer_node::config::rpc::{RpcConfig, RpcModulesList, DEFAULT_RPC_ADDR}; +use katana_sequencer_node::config::rpc::{ + RpcConfig, RpcModulesList, StarknetApiConfig, DEFAULT_RPC_ADDR, +}; use katana_sequencer_node::config::sequencing::SequencingConfig; use katana_sequencer_node::config::Config; use katana_sequencer_node::{LaunchedNode, Node}; @@ -353,9 +355,11 @@ pub fn test_config() -> Config { explorer: true, addr: DEFAULT_RPC_ADDR, apis: RpcModulesList::all(), - max_proof_keys: Some(100), - max_event_page_size: Some(100), - max_concurrent_estimate_fee_requests: None, + starknet: StarknetApiConfig { + max_proof_keys: Some(100), + max_event_page_size: Some(100), + ..Default::default() + }, ..Default::default() };