Skip to content

Commit f028edc

Browse files
committed
spec
1 parent b0dbce2 commit f028edc

23 files changed

Lines changed: 3419 additions & 0 deletions
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Piece 1: NodeKernelAccess types and cardano-rpc plumbing
2+
3+
## Problem
4+
5+
cardano-rpc currently threads `LocalNodeConnectInfo` through its environment and `MonadRpc` constraint.
6+
Every RPC method grabs the connection info and opens a fresh N2C socket connection per request.
7+
To support direct ledger state access (ADR-019), we need a new abstraction that replaces this pattern with an `IORef (Maybe NodeKernelAccess)` passed in by cardano-node at startup.
8+
9+
## Why
10+
11+
This piece creates the `NodeKernelAccess` abstraction and wires it through the cardano-rpc infrastructure.
12+
After this piece, cardano-rpc compiles with the new types threaded through, but no new RPC methods exist yet (piece 2) and no node-side implementation exists yet (piece 3).
13+
Separating the plumbing from the method rewrites and node-side implementation keeps each piece small and reviewable.
14+
15+
## User value
16+
17+
As a cardano-rpc developer, I want the `NodeKernelAccess` abstraction and environment wiring in place so that I can rewrite individual RPC methods to use node kernel access in subsequent pieces.
18+
19+
## Acceptance criteria
20+
21+
1. **AC1: NodeKernelAccess module** - A new module `Cardano.Rpc.Server.Internal.NodeKernelAccess` exists at `src/Cardano/Rpc/Server/Internal/NodeKernelAccess.hs`, exporting `NodeKernelAccess(..)`, `LedgerSnapshot(..)`, and `withNodeKernelAccess`.
22+
`NodeKernelAccess` is a record with three fields: `nkaWithSnapshot :: forall a. (LedgerSnapshot -> IO a) -> IO a`, `nkaSubmitTx :: TxInMode -> IO (SubmitResult TxValidationErrorInCardanoMode)`, and `nkaFetchBlock :: SlotNo -> ByteString -> IO (Maybe ByteString)`.
23+
`LedgerSnapshot` is a newtype wrapping `runQuery :: forall result. QueryInMode result -> IO result`.
24+
The module is listed in `exposed-modules` in `cardano-rpc.cabal`.
25+
- Test: unit - compiles and is importable from the test suite
26+
27+
2. **AC2: withNodeKernelAccess unavailable behaviour** - `withNodeKernelAccess` reads the `IORef (Maybe NodeKernelAccess)`; when the value is `Nothing`, it throws a `GrpcException` with `grpcError = GrpcUnavailable` and message containing "not yet initialised".
28+
When the value is `Just na`, it passes `na` to the callback and returns the callback's result.
29+
- Test: unit - `H.propertyOnce`: create `IORef Nothing`, call `withNodeKernelAccess`, assert `GrpcException` with `GrpcUnavailable` is thrown; create `IORef (Just mockNodeKernelAccess)`, call `withNodeKernelAccess`, assert callback receives the value and its return value is propagated
30+
31+
3. **AC3: Server.hs signature change** - `runRpcServer` signature changes from `Tracer IO TraceRpc -> (RpcConfig, NetworkMagic) -> IO ()` to `Tracer IO TraceRpc -> RpcConfig -> NetworkMagic -> IORef (Maybe NodeKernelAccess) -> IO ()`.
32+
The module re-exports `NodeKernelAccess(..)` and `LedgerSnapshot(..)`.
33+
`RpcEnv` construction is updated to include both `rpcNodeKernelAccess` (from the new parameter) and `rpcLocalNodeConnectInfo` (preserved temporarily).
34+
Note: `methodsSyncRpc` is NOT registered in this piece - that happens in piece 2 when the SyncService proto and handler exist.
35+
- Test: unit - compiles (API change verified by build)
36+
37+
4. **AC4: Environment and MonadRpc wiring** - `RpcEnv` in `Env.hs` gains a new field `rpcNodeKernelAccess :: !(IORef (Maybe NodeKernelAccess))`.
38+
A `Has (IORef (Maybe NodeKernelAccess)) RpcEnv` instance is added to `Monad.hs`.
39+
`MonadRpc` constraint includes `Has (IORef (Maybe NodeKernelAccess)) e`.
40+
The old `rpcLocalNodeConnectInfo` field and `Has LocalNodeConnectInfo RpcEnv` instance are kept temporarily so that method files compile unchanged.
41+
- Test: unit - compiles; the new constraint is exercised by `withNodeKernelAccess` usage in the test from AC2
42+
43+
5. **AC5: Tracing for new trace types** - `Tracing.hs` gains a `TraceRpcSync` sum type with constructors: `TraceRpcFetchBlockSpan TraceSpanEvent` (span begin/end), `TraceRpcFetchBlockNotFound SlotNo` (block not on chain).
44+
`TraceRpc` gains a `TraceRpcSync TraceRpcSync` constructor.
45+
`Pretty` instances render the span events as "Started fetch block method" / "Finished fetch block method" and the not-found as "Block not found at slot <n>".
46+
An `Inject TraceRpcSync TraceRpc` instance is provided.
47+
`TraceRpcSubmitN2cConnectionError SomeException` is replaced by `TraceRpcNodeKernelAccessUnavailable` (no payload) and `TraceRpcForkerError String`.
48+
`Pretty TraceRpcSubmit` renders them as `"Ledger access unavailable (node kernel not yet initialised)"` and `"Forker error: <msg>"` respectively.
49+
The corresponding one-line update in `Submit.hs` (replacing `Left $ TraceRpcSubmitN2cConnectionError e` with `Left $ TraceRpcNodeKernelAccessUnavailable`) is included so that the build stays clean.
50+
- Test: unit - `H.propertyOnce` asserting the `Pretty` output of each new constructor contains the expected substrings
51+
52+
## Out of scope
53+
54+
- Populating the `cardano` oneof field in `AnyChainBlock` (requires protobuf block type mapping, a separate piece of work).
55+
- Streaming RPCs from the sync proto (`FollowTip`, `DumpHistory`).
56+
- Rewriting existing RPC methods (Query, Submit, Eval, Node) to use `NodeKernelAccess` (pieces 4-7).
57+
- Removing `rpcLocalNodeConnectInfo` and `Has LocalNodeConnectInfo` from `RpcEnv` / `MonadRpc` (happens when the last N2C method is rewritten in pieces 4-7).
58+
- Removing `mkLocalNodeConnectInfo` (removed alongside `rpcLocalNodeConnectInfo`).
59+
- Removing `nodeSocketPath` from `RpcConfig` (still needed for `nodeSocketPathToRpcSocketPath`).
60+
- Proto definitions and codegen (piece 2).
61+
- FetchBlock handler (piece 2).
62+
- `mkNodeKernelAccess` in cardano-node (piece 3).
63+
- Node startup wiring (piece 3).
64+
- Adding new E2E tests (no runtime behaviour changes in this piece).
65+
66+
## Definition of done
67+
68+
- [ ] All AC tests written (compile, fail on stubs)
69+
- [ ] Implementation complete (all tests pass via `cabal test`)
70+
- [ ] `cabal build cardano-rpc` succeeds from `/work` with no warnings
71+
- [ ] Nix CI checks pass
72+
- [ ] haskell-reviewer agent finds no critical or style issues
73+
- [ ] fourmolu clean (`scripts/devshell/prettify` run on changed files)
74+
- [ ] No build warnings
75+
76+
## Notes
77+
78+
### Design decision: keep both fields temporarily
79+
80+
This piece adds `rpcNodeKernelAccess :: IORef (Maybe NodeKernelAccess)` to `RpcEnv` alongside the existing `rpcLocalNodeConnectInfo :: LocalNodeConnectInfo`.
81+
Removing `rpcLocalNodeConnectInfo` would break every existing method file (`Node.hs`, `Query.hs`, `Submit.hs`, `Eval.hs`) because they all use `nodeConnInfo <- grab` to obtain a `LocalNodeConnectInfo`.
82+
Rewriting those method bodies is the work of pieces 4-7.
83+
84+
Both fields coexist in `RpcEnv` and both `Has` instances exist in `MonadRpc`.
85+
This means:
86+
- Existing method files compile without any changes.
87+
- Runtime behaviour of existing methods is unchanged (they still use N2C).
88+
- Pieces 4-7 each rewrite one method's N2C usage; the last piece to land removes the old field and instance.
89+
90+
### Files affected
91+
92+
| File | Change |
93+
|---|---|
94+
| `src/Cardano/Rpc/Server/Internal/NodeKernelAccess.hs` | **New.** `NodeKernelAccess`, `LedgerSnapshot`, `withNodeKernelAccess`. |
95+
| `src/Cardano/Rpc/Server/Internal/Env.hs` | Add `rpcNodeKernelAccess` field alongside existing `rpcLocalNodeConnectInfo`. |
96+
| `src/Cardano/Rpc/Server/Internal/Monad.hs` | Add `Has (IORef (Maybe NodeKernelAccess)) RpcEnv` instance. Add constraint to `MonadRpc`. |
97+
| `src/Cardano/Rpc/Server/Internal/Tracing.hs` | Add `TraceRpcSync` type and constructors. Replace `TraceRpcSubmitN2cConnectionError` with `TraceRpcNodeKernelAccessUnavailable` and `TraceRpcForkerError`. |
98+
| `src/Cardano/Rpc/Server/Internal/UtxoRpc/Submit.hs` | One-line trace constructor update. |
99+
| `src/Cardano/Rpc/Server.hs` | New signature, re-exports, updated `RpcEnv` construction. |
100+
| `cardano-rpc.cabal` | Add `NodeKernelAccess` module to `exposed-modules`. |
101+
102+
**cardano-node** (must update in lockstep to keep `-Werror` clean):
103+
104+
| File | Change |
105+
|---|---|
106+
| `src/Cardano/Node/Tracing/Tracers/Rpc.hs` | Handle renamed `TraceRpcNodeKernelAccessUnavailable`/`TraceRpcForkerError` and new `TraceRpcSync` constructors in `forMachine`, `asMetrics`, `namespaceFor`, `severityFor`, `documentFor`, `allNamespaces`. |
107+
| `src/Cardano/Node/Run.hs` | Create `nodeKernelAccessRef <- newIORef Nothing`, pass through `rpcServerLoop` to `runRpcServer`. Update `rpcServerLoop` signature. |
108+
109+
### Gotchas for the implementer
110+
111+
- **Import narrowing in `Monad.hs`**: when adding the new `Has` instance, ensure `Inject` (used by `putTrace`) is still available.
112+
Currently it comes from `import Cardano.Api`; if imports are narrowed, import it explicitly from `Cardano.Api.Era`.
113+
114+
- **`RankNTypes` extension.** Both `NodeKernelAccess` and `LedgerSnapshot` use higher-rank fields, requiring the `RankNTypes` extension in `NodeKernelAccess.hs`.
115+
116+
- **`runRpcServer` keeps `NetworkMagic`.** The old `rpcLocalNodeConnectInfo` is still used by existing methods, so `mkLocalNodeConnectInfo` still needs `NetworkMagic`.
117+
It is dropped only when `rpcLocalNodeConnectInfo` is finally removed in a later piece.
118+
119+
- **`Submit.hs` trace constructor.** `Submit.hs` currently references `TraceRpcSubmitN2cConnectionError` in its `submitTx` helper.
120+
The trace constructor rename requires a corresponding one-line update in `Submit.hs`: replace `Left $ TraceRpcSubmitN2cConnectionError e` with `Left $ TraceRpcNodeKernelAccessUnavailable` (dropping the exception payload, since the new constructor carries no payload).
121+
122+
- **`SomeException` import**: `Control.Exception` is still needed in `Tracing.hs` because `TraceRpcError` and `TraceRpcFatalError` use `SomeException`.
123+
124+
- **`GrpcException` import**: `withNodeKernelAccess` throws `GrpcException` from `Network.GRPC.Spec`.
125+
`grpc-spec` is already a dependency of `cardano-rpc`.
126+
127+
- **`RpcConfig.nodeSocketPath` stays**: ADR-019 explicitly notes this.
128+
The config field remains for deriving `rpcSocketPath` via `nodeSocketPathToRpcSocketPath`.
129+
130+
### Dependencies
131+
132+
- **Upstream:** none (this is the first piece).
133+
- **Downstream:** all pieces 2-8 depend on this (for the `NodeKernelAccess` record, environment wiring, and tracing).
134+
135+
### Testing approach
136+
137+
This piece is primarily a wiring/structural change.
138+
Two ACs have genuine Hedgehog property tests:
139+
- AC2 (`withNodeKernelAccess` behaviour): `H.propertyOnce` covering the `Nothing` and `Just` branches.
140+
- AC5 (tracing pretty-print): `H.propertyOnce` asserting rendered output of the new constructors.
141+
142+
AC1, AC3, AC4 are verified by successful compilation.
143+
144+
## Reference docs
145+
146+
- [Consensus protocol and snapshots](analysis-consensus-protocol.md) - snapshot consistency rationale
147+
- [API signatures](prereqs-api-signatures.md) - `NodeKernelAccess` type design context
148+
- [Implementation details](prereqs-implementation-details.md) - subtle gotchas for the interface
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Piece 2: FetchBlock proto and handler
2+
3+
## Problem
4+
5+
cardano-rpc has no `FetchBlock` implementation and no direct ChainDB access path.
6+
Before rewriting existing N2C-based RPCs, we need a clean end-to-end proof that direct node access works, using a new method with no legacy code to break.
7+
8+
## Why
9+
10+
This piece adds the SyncService proto definition and the `fetchBlockMethod` handler.
11+
After this piece, the proto bindings exist, the handler compiles, but it cannot run end-to-end yet (needs piece 3 for node-side implementation).
12+
Starting with a new RPC method de-risks the node kernel access architecture without touching any existing N2C code.
13+
14+
## User value
15+
16+
As a dApp developer, I want to fetch raw block bytes by slot and hash via gRPC so that I can decode blocks client-side without running a separate chain indexer.
17+
18+
## Acceptance criteria
19+
20+
1. **AC1: sync.proto and codegen** - A new proto file `cardano-rpc/proto/utxorpc/v1beta/sync/sync.proto` exists, containing the `SyncService` with only the `FetchBlock` RPC and its message types (`BlockRef`, `FetchBlockRequest`, `FetchBlockResponse`, `AnyChainBlock`).
21+
Running `buf generate proto` in the nix dev shell produces proto-lens bindings under `gen/` (`Proto.Utxorpc.V1beta.Sync.Sync` and `Proto.Utxorpc.V1beta.Sync.Sync_Fields`).
22+
Both generated modules are listed in `cardano-rpc.cabal` under `library gen`.
23+
- Test: unit - `cabal build cardano-rpc` compiles the generated modules
24+
25+
2. **AC2: Proto API wrapper for SyncService** - A new module `Cardano.Rpc.Proto.Api.UtxoRpc.Sync` exists, following the same pattern as `Query.hs` and `Submit.hs` (re-exports generated proto modules, declares `RequestMetadata`, `ResponseInitialMetadata`, `ResponseTrailingMetadata` type instances for `Protobuf SyncService`).
26+
The module is listed in `exposed-modules` in `cardano-rpc.cabal`.
27+
- Test: unit - compiles
28+
29+
3. **AC3: fetchBlockMethod implementation** - A new module `Cardano.Rpc.Server.Internal.UtxoRpc.Sync` exists with `fetchBlockMethod :: FetchBlockRequest -> RpcHandler FetchBlockResponse`.
30+
For each `BlockRef` in the request, the method extracts `slot` and `hash` fields.
31+
It calls `nkaFetchBlock slotNo hashBytes` via `withNodeKernelAccess`.
32+
If the result is `Just rawBytes`, the block is wrapped in an `AnyChainBlock` with `native_bytes` set to `rawBytes`.
33+
If the result is `Nothing`, the block is omitted from the response (not-found blocks are skipped silently) and a `TraceRpcFetchBlockNotFound` trace is emitted with the slot number.
34+
Only the `native_bytes` field is populated; the `cardano` oneof field is left empty.
35+
- Test: E2E - `hprop_rpc_fetch_block` verifies a produced block can be fetched and its raw bytes are non-empty
36+
37+
4. **AC4: Missing slot returns INVALID_ARGUMENT** - When a `BlockRef` has `slot == 0` (the proto default, meaning unset) but a non-empty `hash`, `fetchBlockMethod` returns a gRPC `INVALID_ARGUMENT` error with a message containing "slot is required".
38+
A `RealPoint` cannot be constructed without both slot and hash; the method validates this before calling `nkaFetchBlock`.
39+
- Test: unit - `H.propertyOnce`: construct a `FetchBlockRequest` with a `BlockRef` whose slot is 0 and hash is non-empty, call `fetchBlockMethod`, assert `GrpcException` with `GrpcInvalidArgument` is thrown
40+
41+
5. **AC5: Empty hash returns INVALID_ARGUMENT** - When a `BlockRef` has a non-zero `slot` but an empty `hash`, `fetchBlockMethod` returns a gRPC `INVALID_ARGUMENT` error with a message containing "hash is required".
42+
- Test: unit - `H.propertyOnce`: construct a `FetchBlockRequest` with a `BlockRef` whose slot is non-zero and hash is empty, call `fetchBlockMethod`, assert `GrpcException` with `GrpcInvalidArgument` is thrown
43+
44+
6. **AC6: Server.hs registers SyncService** - `Server.hs` registers `methodsSyncRpc` alongside `methodsNodeRpc`, `methodsUtxoRpc`, and `methodsUtxoRpcSubmit`.
45+
- Test: unit - compiles (verified by build)
46+
47+
## Out of scope
48+
49+
- Populating the `cardano` oneof field in `AnyChainBlock` (requires protobuf block type mapping, a separate piece of work).
50+
- Streaming RPCs from the sync proto (`FollowTip`, `DumpHistory`).
51+
- `mkNodeKernelAccess` in cardano-node (piece 3).
52+
- Node startup wiring in `Run.hs` (piece 3).
53+
- E2E test infrastructure (piece 3 provides the node-side implementation needed to run FetchBlock end-to-end).
54+
- `field_mask` support on `FetchBlockRequest` (parse-into-proto would need the `cardano` field populated first).
55+
- Handling of `BlockRef.height` and `BlockRef.timestamp` fields (not needed for `RealPoint` construction; reserved for future use).
56+
57+
## Definition of done
58+
59+
- [ ] All AC tests written (compile, fail on stubs)
60+
- [ ] Implementation complete (all tests pass via `cabal test`)
61+
- [ ] `cabal build cardano-rpc` succeeds from `/work` with no warnings
62+
- [ ] Nix CI checks pass
63+
- [ ] haskell-reviewer agent finds no critical or style issues
64+
- [ ] fourmolu clean (`scripts/devshell/prettify` run on changed files)
65+
- [ ] No build warnings
66+
67+
## Notes
68+
69+
### Design decision: skip not-found blocks (not NOT_FOUND status)
70+
71+
The proto `FetchBlockResponse` has `repeated AnyChainBlock block` with no per-element status field.
72+
A missing block can only be expressed by omission from the list or by failing the entire request with a gRPC NOT_FOUND status.
73+
Failing the entire batch because one block is missing would be surprising and unhelpful for clients requesting multiple blocks.
74+
Therefore, blocks not found in ChainDB are silently omitted from the response.
75+
Clients can compare the count of returned blocks against the count of requested `BlockRef` entries to detect missing blocks.
76+
77+
### Files affected
78+
79+
| File | Change |
80+
|---|---|
81+
| `proto/utxorpc/v1beta/sync/sync.proto` | **New.** FetchBlock RPC and message types. |
82+
| `gen/Proto/Utxorpc/V1beta/Sync/Sync.hs` | **Generated.** Proto-lens bindings (do not edit manually). |
83+
| `gen/Proto/Utxorpc/V1beta/Sync/Sync_Fields.hs` | **Generated.** Proto-lens field accessors (do not edit manually). |
84+
| `src/Cardano/Rpc/Proto/Api/UtxoRpc/Sync.hs` | **New.** Proto API wrapper for SyncService. |
85+
| `src/Cardano/Rpc/Server/Internal/UtxoRpc/Sync.hs` | **New.** `fetchBlockMethod`. |
86+
| `src/Cardano/Rpc/Server.hs` | Register `methodsSyncRpc`. |
87+
| `cardano-rpc.cabal` | Add new modules to `exposed-modules` and `library gen`. |
88+
89+
### Gotchas for the implementer
90+
91+
- **Proto codegen requires nix dev shell.** `buf` is not available outside it.
92+
Run `nix develop --command bash -c "cd cardano-rpc && buf generate proto"`.
93+
94+
- **`AnyChainBlock` has both `native_bytes` and `cardano`.** For this piece, only populate `native_bytes`.
95+
The parsed `cardano` block field is a separate, complex piece of work.
96+
97+
- **`GetRawBlock` returns `Lazy.ByteString`.** The proto `native_bytes` field is strict `ByteString`.
98+
Convert via `Data.ByteString.Lazy.toStrict`.
99+
100+
- **Hash bytes conversion.** The proto hash is raw bytes (`ByteString`).
101+
`HeaderHash (CardanoBlock StandardCrypto)` is `OneEraHash` wrapping `ShortByteString`.
102+
Convert via `OneEraHash . SBS.toShort . BS.toStrict` (if the input is lazy) or `OneEraHash . SBS.toShort` (if strict).
103+
No CBOR wrapping is needed; it is raw hash bytes.
104+
105+
- **`RealPoint` requires both slot and hash.** If the proto `BlockRef` has `slot == 0` (proto default for unset) but a non-empty hash, or a non-zero slot but empty hash, we cannot construct a `RealPoint`.
106+
Validate both fields and return `INVALID_ARGUMENT` if either is missing.
107+
108+
### Dependencies
109+
110+
- **Upstream:** piece 1 (for `NodeKernelAccess`, `withNodeKernelAccess`, environment wiring, and tracing types).
111+
- **Downstream:** piece 3 (provides the node-side `mkNodeKernelAccess` implementation needed for E2E).
112+
113+
### Testing approach
114+
115+
| AC | Type | What it tests |
116+
|---|---|---|
117+
| AC1 | unit | Proto codegen compiles |
118+
| AC2 | unit | Proto API wrapper compiles |
119+
| AC3 | E2E | `fetchBlockMethod` end-to-end happy path (requires piece 3) |
120+
| AC4 | unit | Missing slot returns `INVALID_ARGUMENT` |
121+
| AC5 | unit | Empty hash returns `INVALID_ARGUMENT` |
122+
| AC6 | unit | Server registration compiles |
123+
124+
### Open questions
125+
126+
- Should `nkaFetchBlock` also support fetching by hash alone (without slot)?
127+
`ChainDB` has `getBlockComponent` which takes a `RealPoint` (requiring both), so hash-only lookup would need a different API (`iteratorNext` or similar).
128+
For now, both slot and hash are required.
129+
Hash-only lookup can be a follow-up story if needed.
130+
131+
## Reference docs
132+
133+
- [Architecture and current state](analysis-architecture.md) - cardano-rpc overview and spec coverage
134+
- [Build and conventions](prereqs-build-and-conventions.md) - proto codegen instructions, nix build commands

0 commit comments

Comments
 (0)