dig-rpc
Axum-based JSON-RPC server for the DIG Network fullnode / validator / future wallet. Couples dig-service lifecycle with the dig-rpc-types wire contract.
- mTLS transport (rustls) with server certs on a private CA (internal admin port) or a public CA (read-only public port).
- Cert-CN / SAN →
Rolemapping viaRoleMap. - Per-method metadata (
MethodMeta) governingmin_role, rate-limit bucket, and public-port exposure. - Per-(peer, bucket) token-bucket rate limiting.
- Graceful shutdown integrated with
dig_service::ShutdownToken.
See docs/resources/SPEC.md for the design doc.
Table of contents
- Install
- Architecture
- Quick reference
RpcServer<R>RpcServerModeTlsConfigRole/RoleMap/CertMatcherMethodRegistry/MethodMeta- Rate limiting
- HTTP endpoints
dispatch_envelope- Errors
- Feature flags
- v0.1 scope
- License
Install
[]
= "0.1"
Pulls in dig-service (lifecycle) + dig-rpc-types (wire contract) + axum + rustls.
Architecture
HTTP request
│
▼
┌──────────────────────────────────────────────────────┐
│ tower::Service<Request> (Axum router) │
│ ↓ RequestIdLayer │
│ ↓ PanicCatchLayer │
│ ↓ AuthLayer — TLS peer → Role │
│ ↓ RateLimitLayer — (peer_key, method) bucket │
│ ↓ AllowListLayer — role ≥ method.min_role? │
│ ↓ Body parse — JsonRpcRequest<serde_json::Value>│
│ ↓ RpcApi::dispatch (from dig-service) │
│ ↓ Envelope response │
│ ↓ AuditLayer │
└──────────────────────────────────────────────────────┘
Quick reference
use Arc;
use async_trait;
use ;
use Role;
use JsonRpcError;
use ;
;
async
RpcServer<R>
| Method | Signature | Purpose |
|---|---|---|
new |
Arc<R>, MethodRegistry, RpcServerMode -> Self |
Build a server with default rate-limit config |
with_rate_limit_state |
Self, RateLimitState -> Self |
Override the rate-limit state |
bind_addr |
&self -> SocketAddr |
The server's bind address |
serve |
self, ShutdownToken -> Result<(), RpcServerError> |
Run the server; exit on shutdown |
serve returns when the shutdown token fires or the listener dies unexpectedly. In the plaintext mode it uses axum::serve with graceful shutdown; in TLS modes it uses axum_server::bind_rustls.
RpcServerMode
| Mode | TLS | Use |
|---|---|---|
Internal { bind, tls, role_map } |
mTLS (private CA) | Admin / operator RPC on 127.0.0.1 |
Public { bind, tls } |
HTTPS server-auth (public CA) | Read-only explorer RPC on 0.0.0.0 |
PlainText { bind } |
none | Dev / test only — convenience ctor public_plaintext(addr) |
| Method | Signature | Purpose |
|---|---|---|
public_plaintext |
SocketAddr -> Self |
Convenience ctor for dev plaintext mode |
bind |
&self -> SocketAddr |
The bind address regardless of mode |
TlsConfig
Wraps a rustls::ServerConfig. Load helpers accept on-disk PEM paths:
| Method | Input | Loads |
|---|---|---|
TlsConfig::load_internal |
&InternalCertPaths |
Server cert + key + client-CA bundle (mTLS) |
TlsConfig::load_public |
&PublicCertPaths |
Server cert + key (server-auth only) |
Path structs
Private-key format accepted: PKCS#8 or SEC1 (parsed by rustls-pemfile).
Role / RoleMap / CertMatcher
Role
Ordered Admin > PairedFullnode > Validator > Explorer. Methods declare min_role; access is granted iff peer_role >= min_role.
| Method | Signature | Purpose |
|---|---|---|
as_str |
self -> &'static str |
"admin" / "paired_fullnode" / "validator" / "explorer" |
CertMatcher
| Method | Signature | Purpose |
|---|---|---|
matches |
&self, &PeerCertInfo -> bool |
Test against a peer cert |
PeerCertInfo
Populated from the TLS handshake and attached to each request's extension map.
RoleMap
| Method | Signature | Purpose |
|---|---|---|
new |
Role -> Self |
Build a map with the given default role (for peers not matching any rule) |
push |
&self, RoleMapEntry |
Append a rule |
reload |
&self, Vec<RoleMapEntry> |
Atomically replace the full rule set (live reload) |
resolve |
&self, &PeerCertInfo -> Role |
First-match-wins; falls through to default |
len / is_empty |
— | Rule count |
MethodRegistry / MethodMeta
Const builders
| Method | Behaviour |
|---|---|
MethodMeta::read(name, min_role, bucket) |
class = Read; public_exposed = (min_role == Explorer) |
MethodMeta::write(name, min_role, bucket) |
class = Write; public_exposed = false always |
MethodMeta::admin(name) |
class = Admin; min_role = Admin; rate_bucket = AdminOnly; public_exposed = false |
MethodRegistry
| Method | Signature | Purpose |
|---|---|---|
new |
() -> Self |
Empty registry |
register |
&self, MethodMeta |
Insert / overwrite |
register_all |
&self, impl IntoIterator<Item = MethodMeta> |
Bulk register |
get |
&self, &str -> Option<MethodMeta> |
Look up by method name |
len / is_empty |
— | Registered method count |
Rate limiting
RateLimitConfig / BucketSpec
RateLimitConfig::defaults() ships:
| Bucket | fill/sec | capacity |
|---|---|---|
ReadLight |
50 | 100 |
ReadHeavy |
5 | 10 |
WriteLight |
10 | 20 |
WriteHeavy |
1 | 5 |
AdminOnly |
1 | 3 |
RateLimitState
pub type PeerKey = ;
| Method | Signature | Purpose |
|---|---|---|
new |
RateLimitConfig -> Self |
Fresh state |
check |
&self, &PeerKey, RateBucket -> RateLimitOutcome |
Attempt to debit one token |
Fail-open on unconfigured buckets (logs a tracing::warn!). This prevents a single missed bucket from bricking the whole server at startup.
HTTP endpoints
| Route | Method | Behaviour |
|---|---|---|
POST / |
JSON-RPC dispatch → RpcApi::dispatch |
Response is JsonRpcResponse<serde_json::Value> |
GET /healthz |
Liveness | 200 OK iff RpcApi::healthz() returns Ok; 503 otherwise |
dispatch_envelope
Pure function used internally by RpcServer and exposed for embedding.
pub async Sized> ;
;
| Scenario | Returns |
|---|---|
| Method not in registry | JsonRpcResponseBody::Error { code: MethodNotFound, ... } |
api.dispatch returns Ok(v) |
JsonRpcResponseBody::Success { result: v } |
api.dispatch returns Err(e) |
JsonRpcResponseBody::Error { error: e } (propagated unchanged) |
Errors
Per-request errors come from dig_rpc_types::envelope::JsonRpcError with ErrorCode from dig_rpc_types::errors.
Server-level (startup / fatal) errors:
Feature flags
| Flag | Default | Effect |
|---|---|---|
metrics |
on | Prometheus counters (hooks reserved — not yet wired in v0.1) |
testing |
off | LoopbackServer helper for dependent crates' tests |
v0.1 scope
Included:
- JSON-RPC 2.0 dispatch pipeline (
POST /,/healthz). - Method registry + metadata (role, rate bucket, public-exposed).
- Rate-limit state with per-(peer, bucket) token buckets.
Role/RoleMap/CertMatcher— ordered rule chain with livereload.- TLS config loading for internal (mTLS) + public modes via
rustls. - Plaintext dev mode for loopback / tests.
- Graceful shutdown via
ShutdownToken.
Deferred to v0.2:
- Full Tower integration of rate-limit + allow-list middleware as proper layers (v0.1 exposes state; servers call
.check()inline). - mTLS client-cert extraction pipeline wired end-to-end from rustls to the per-request
Role. - Prometheus metrics registration.
- NDJSON streaming responses for bulk reads.
License
Licensed under either of Apache-2.0 or MIT at your option.