Skip to content

Commit ba25f5d

Browse files
prestwichclaude
andauthored
feat(rpc): structured JSON-RPC error codes for all endpoints (#119)
* feat(rpc): structured JSON-RPC error codes and ajj 0.7.0 (ENG-1899) Replace string-based RPC errors with structured error types implementing ajj's IntoErrorPayload trait. Each namespace (eth, debug, signet) gets a dedicated error enum with spec-compliant error codes. - Add EthError, DebugError, SignetError with thiserror + IntoErrorPayload - Replace response_tri! macro and string errors with typed error propagation - Add resolve_block_id/resolve_header helpers with ResolveError - Update ajj dependency from git branch to published 0.7.0 - Preserve hot storage reader consistency from #118 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(rpc): address review feedback on error types and test coverage - Use DebugError::Resolve(e) instead of BlockNotFound for resolve_header failures in trace_transaction - Add Internal variant to DebugError and SignetError for task panic fallbacks, replacing misuse of EvmHalt and Timeout - Change SignetError::Resolve to wrap ResolveError instead of String, with proper error code/message delegation - Add From<EthError> for SignetError to handle resolve_evm_block errors - Expand error code tests to cover all variants of EthError, DebugError, and SignetError Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 24bd74b commit ba25f5d

11 files changed

Lines changed: 625 additions & 368 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
.DS_Store
33
Cargo.lock
44
.idea/
5-
docs/superpowers/
5+
docs/

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ signet-cold-mdbx = "0.6.5"
6464
signet-storage-types = "0.6.5"
6565

6666
# ajj
67-
ajj = { version = "0.6.0" }
67+
ajj = "0.7.0"
6868

6969
# trevm
7070
trevm = { version = "0.34.0", features = ["full_env_cfg"] }

crates/rpc/src/debug/endpoints.rs

Lines changed: 45 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ use crate::{
66
DebugError,
77
types::{TraceBlockParams, TraceTransactionParams},
88
},
9-
eth::helpers::{CfgFiller, await_handler, response_tri},
9+
eth::helpers::{CfgFiller, await_handler},
1010
};
11-
use ajj::{HandlerCtx, ResponsePayload};
11+
use ajj::HandlerCtx;
1212
use alloy::{
1313
consensus::BlockHeader,
1414
eips::BlockId,
1515
rpc::types::trace::geth::{GethTrace, TraceResult},
1616
};
1717
use itertools::Itertools;
18-
use signet_evm::EvmErrored;
1918
use signet_hot::{HotKv, model::HotKvRead};
2019
use signet_types::MagicSig;
2120
use tracing::Instrument;
@@ -26,13 +25,13 @@ pub(super) async fn trace_block<T, H>(
2625
hctx: HandlerCtx,
2726
TraceBlockParams(id, opts): TraceBlockParams<T>,
2827
ctx: StorageRpcCtx<H>,
29-
) -> ResponsePayload<Vec<TraceResult>, DebugError>
28+
) -> Result<Vec<TraceResult>, DebugError>
3029
where
3130
T: Into<BlockId>,
3231
H: HotKv + Send + Sync + 'static,
3332
<H::RoTx as HotKvRead>::Error: DBErrorMarker,
3433
{
35-
let opts = response_tri!(opts.ok_or(DebugError::InvalidTracerConfig));
34+
let opts = opts.ok_or(DebugError::InvalidTracerConfig)?;
3635

3736
// Acquire a tracing semaphore permit to limit concurrent debug
3837
// requests. The permit is held for the entire handler lifetime and
@@ -44,41 +43,37 @@ where
4443

4544
let fut = async move {
4645
let cold = ctx.cold();
47-
let block_num = response_tri!(ctx.resolve_block_id(id).map_err(|e| {
46+
let block_num = ctx.resolve_block_id(id).map_err(|e| {
4847
tracing::warn!(error = %e, ?id, "block resolution failed");
49-
DebugError::BlockNotFound(id)
50-
}));
48+
DebugError::Resolve(e)
49+
})?;
5150

52-
let sealed =
53-
response_tri!(ctx.resolve_header(BlockId::Number(block_num.into())).map_err(|e| {
54-
tracing::warn!(error = %e, block_num, "header resolution failed");
55-
DebugError::BlockNotFound(id)
56-
}));
51+
let sealed = ctx.resolve_header(BlockId::Number(block_num.into())).map_err(|e| {
52+
tracing::warn!(error = %e, block_num, "header resolution failed");
53+
DebugError::Resolve(e)
54+
})?;
5755

5856
let Some(sealed) = sealed else {
59-
return ResponsePayload::internal_error_message(
60-
format!("block not found: {id}").into(),
61-
);
57+
return Err(DebugError::BlockNotFound(id));
6258
};
6359

6460
let block_hash = sealed.hash();
6561
let header = sealed.into_inner();
6662

67-
let txs = response_tri!(cold.get_transactions_in_block(block_num).await.map_err(|e| {
63+
let txs = cold.get_transactions_in_block(block_num).await.map_err(|e| {
6864
tracing::warn!(error = %e, block_num, "cold storage read failed");
6965
DebugError::from(e)
70-
}));
66+
})?;
7167

7268
tracing::debug!(number = header.number, "Loaded block");
7369

7470
let mut frames = Vec::with_capacity(txs.len());
7571

7672
// State BEFORE this block.
77-
let db =
78-
response_tri!(ctx.revm_state_at_height(header.number.saturating_sub(1)).map_err(|e| {
79-
tracing::warn!(error = %e, block_num, "hot storage read failed");
80-
DebugError::from(e)
81-
}));
73+
let db = ctx.revm_state_at_height(header.number.saturating_sub(1)).map_err(|e| {
74+
tracing::warn!(error = %e, block_num, "hot storage read failed");
75+
DebugError::from(e)
76+
})?;
8277

8378
let spec_id = ctx.spec_id_for_header(&header);
8479
let mut evm = signet_evm::signet_evm(db, ctx.constants().clone());
@@ -100,30 +95,30 @@ where
10095

10196
let t = trevm.fill_tx(tx);
10297
let frame;
103-
(frame, trevm) = response_tri!(crate::debug::tracer::trace(t, &opts, tx_info));
98+
(frame, trevm) = crate::debug::tracer::trace(t, &opts, tx_info)?;
10499
frames.push(TraceResult::Success { result: frame, tx_hash: Some(*tx.tx_hash()) });
105100

106101
tracing::debug!(tx_index = idx, tx_hash = ?tx.tx_hash(), "Traced transaction");
107102
}
108103

109-
ResponsePayload(Ok(frames))
104+
Ok(frames)
110105
}
111106
.instrument(span);
112107

113-
await_handler!(@response_option hctx.spawn(fut))
108+
await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into()))
114109
}
115110

116111
/// `debug_traceTransaction` handler.
117112
pub(super) async fn trace_transaction<H>(
118113
hctx: HandlerCtx,
119114
TraceTransactionParams(tx_hash, opts): TraceTransactionParams,
120115
ctx: StorageRpcCtx<H>,
121-
) -> ResponsePayload<GethTrace, DebugError>
116+
) -> Result<GethTrace, DebugError>
122117
where
123118
H: HotKv + Send + Sync + 'static,
124119
<H::RoTx as HotKvRead>::Error: DBErrorMarker,
125120
{
126-
let opts = response_tri!(opts.ok_or(DebugError::InvalidTracerConfig));
121+
let opts = opts.ok_or(DebugError::InvalidTracerConfig)?;
127122

128123
// Held for the handler duration; dropped when the async block completes.
129124
let _permit = ctx.acquire_tracing_permit().await;
@@ -134,37 +129,36 @@ where
134129
let cold = ctx.cold();
135130

136131
// Look up the transaction and its containing block.
137-
let confirmed = response_tri!(cold.get_tx_by_hash(tx_hash).await.map_err(|e| {
132+
let confirmed = cold.get_tx_by_hash(tx_hash).await.map_err(|e| {
138133
tracing::warn!(error = %e, %tx_hash, "cold storage read failed");
139134
DebugError::from(e)
140-
}));
135+
})?;
141136

142-
let confirmed = response_tri!(confirmed.ok_or(DebugError::TransactionNotFound));
137+
let confirmed = confirmed.ok_or(DebugError::TransactionNotFound(tx_hash))?;
143138
let (_tx, meta) = confirmed.into_parts();
144139

145140
let block_num = meta.block_number();
146141
let block_hash = meta.block_hash();
147142

148143
let block_id = BlockId::Number(block_num.into());
149-
let sealed = response_tri!(ctx.resolve_header(block_id).map_err(|e| {
144+
let sealed = ctx.resolve_header(block_id).map_err(|e| {
150145
tracing::warn!(error = %e, block_num, "header resolution failed");
151-
DebugError::BlockNotFound(block_id)
152-
}));
153-
let header = response_tri!(sealed.ok_or(DebugError::BlockNotFound(block_id))).into_inner();
146+
DebugError::Resolve(e)
147+
})?;
148+
let header = sealed.ok_or(DebugError::BlockNotFound(block_id))?.into_inner();
154149

155-
let txs = response_tri!(cold.get_transactions_in_block(block_num).await.map_err(|e| {
150+
let txs = cold.get_transactions_in_block(block_num).await.map_err(|e| {
156151
tracing::warn!(error = %e, block_num, "cold storage read failed");
157152
DebugError::from(e)
158-
}));
153+
})?;
159154

160155
tracing::debug!(number = block_num, "Loaded containing block");
161156

162157
// State BEFORE this block.
163-
let db =
164-
response_tri!(ctx.revm_state_at_height(block_num.saturating_sub(1)).map_err(|e| {
165-
tracing::warn!(error = %e, block_num, "hot storage read failed");
166-
DebugError::from(e)
167-
}));
158+
let db = ctx.revm_state_at_height(block_num.saturating_sub(1)).map_err(|e| {
159+
tracing::warn!(error = %e, block_num, "hot storage read failed");
160+
DebugError::from(e)
161+
})?;
168162

169163
let spec_id = ctx.spec_id_for_header(&header);
170164
let mut evm = signet_evm::signet_evm(db, ctx.constants().clone());
@@ -175,15 +169,16 @@ where
175169
let mut txns = txs.iter().enumerate().peekable();
176170
for (_idx, tx) in txns.by_ref().peeking_take_while(|(_, t)| t.tx_hash() != &tx_hash) {
177171
if MagicSig::try_from_signature(tx.signature()).is_some() {
178-
return ResponsePayload::internal_error_message(
179-
DebugError::TransactionNotFound.to_string().into(),
180-
);
172+
return Err(DebugError::TransactionNotFound(tx_hash));
181173
}
182174

183-
trevm = response_tri!(trevm.run_tx(tx).map_err(EvmErrored::into_error)).accept_state();
175+
trevm = trevm
176+
.run_tx(tx)
177+
.map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?
178+
.accept_state();
184179
}
185180

186-
let (index, tx) = response_tri!(txns.next().ok_or(DebugError::TransactionNotFound));
181+
let (index, tx) = txns.next().ok_or(DebugError::TransactionNotFound(tx_hash))?;
187182

188183
let trevm = trevm.fill_tx(tx);
189184

@@ -195,11 +190,11 @@ where
195190
base_fee: header.base_fee_per_gas(),
196191
};
197192

198-
let res = response_tri!(crate::debug::tracer::trace(trevm, &opts, tx_info)).0;
193+
let res = crate::debug::tracer::trace(trevm, &opts, tx_info)?.0;
199194

200-
ResponsePayload(Ok(res))
195+
Ok(res)
201196
}
202197
.instrument(span);
203198

204-
await_handler!(@response_option hctx.spawn(fut))
199+
await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into()))
205200
}

crates/rpc/src/debug/error.rs

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
//! Error types for the debug namespace.
22
3-
use alloy::eips::BlockId;
3+
use alloy::{eips::BlockId, primitives::B256};
4+
use std::borrow::Cow;
45

56
/// Errors that can occur in the `debug` namespace.
6-
///
7-
/// The [`serde::Serialize`] impl emits sanitized messages suitable for
8-
/// API responses — internal storage details are not exposed to callers.
9-
/// Use [`tracing`] to log the full error chain before constructing the
10-
/// variant.
117
#[derive(Debug, thiserror::Error)]
128
pub enum DebugError {
139
/// Cold storage error.
@@ -16,28 +12,122 @@ pub enum DebugError {
1612
/// Hot storage error.
1713
#[error("hot storage error")]
1814
Hot(#[from] signet_storage::StorageError),
15+
/// Block resolution error.
16+
#[error("resolve: {0}")]
17+
Resolve(crate::config::resolve::ResolveError),
1918
/// Invalid tracer configuration.
2019
#[error("invalid tracer config")]
2120
InvalidTracerConfig,
2221
/// Unsupported tracer type.
2322
#[error("unsupported: {0}")]
2423
Unsupported(&'static str),
25-
/// EVM execution error.
26-
#[error("evm execution error")]
27-
Evm(String),
24+
/// EVM execution halted.
25+
#[error("execution halted: {reason}")]
26+
EvmHalt {
27+
/// Debug-formatted halt reason.
28+
reason: String,
29+
},
2830
/// Block not found.
2931
#[error("block not found: {0}")]
3032
BlockNotFound(BlockId),
3133
/// Transaction not found.
32-
#[error("transaction not found")]
33-
TransactionNotFound,
34+
#[error("transaction not found: {0}")]
35+
TransactionNotFound(B256),
36+
/// Internal server error.
37+
#[error("{0}")]
38+
Internal(String),
3439
}
3540

36-
impl serde::Serialize for DebugError {
37-
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
38-
where
39-
S: serde::Serializer,
40-
{
41-
serializer.serialize_str(&self.to_string())
41+
impl ajj::IntoErrorPayload for DebugError {
42+
type ErrData = ();
43+
44+
fn error_code(&self) -> i64 {
45+
match self {
46+
Self::Cold(_) | Self::Hot(_) | Self::EvmHalt { .. } | Self::Internal(_) => -32000,
47+
Self::Resolve(r) => crate::eth::error::resolve_error_code(r),
48+
Self::InvalidTracerConfig => -32602,
49+
Self::Unsupported(_) => -32601,
50+
Self::BlockNotFound(_) | Self::TransactionNotFound(_) => -32001,
51+
}
52+
}
53+
54+
fn error_message(&self) -> Cow<'static, str> {
55+
match self {
56+
Self::Cold(_) | Self::Hot(_) => "server error".into(),
57+
Self::Internal(msg) => Cow::Owned(msg.clone()),
58+
Self::Resolve(r) => crate::eth::error::resolve_error_message(r),
59+
Self::InvalidTracerConfig => "invalid tracer config".into(),
60+
Self::Unsupported(msg) => format!("unsupported: {msg}").into(),
61+
Self::EvmHalt { reason } => format!("execution halted: {reason}").into(),
62+
Self::BlockNotFound(id) => format!("block not found: {id}").into(),
63+
Self::TransactionNotFound(h) => format!("transaction not found: {h}").into(),
64+
}
65+
}
66+
67+
fn error_data(self) -> Option<Self::ErrData> {
68+
None
69+
}
70+
}
71+
72+
#[cfg(test)]
73+
mod tests {
74+
use super::*;
75+
use crate::config::resolve::ResolveError;
76+
use ajj::IntoErrorPayload;
77+
78+
#[test]
79+
fn cold_storage_error() {
80+
let err = DebugError::Cold(signet_cold::ColdStorageError::NotFound("test".into()));
81+
assert_eq!(err.error_code(), -32000);
82+
assert_eq!(err.error_message(), "server error");
83+
}
84+
85+
#[test]
86+
fn resolve_hash_not_found() {
87+
let err = DebugError::Resolve(ResolveError::HashNotFound(B256::ZERO));
88+
assert_eq!(err.error_code(), -32001);
89+
assert!(err.error_message().contains("block hash not found"));
90+
}
91+
92+
#[test]
93+
fn invalid_tracer_config() {
94+
let err = DebugError::InvalidTracerConfig;
95+
assert_eq!(err.error_code(), -32602);
96+
assert_eq!(err.error_message(), "invalid tracer config");
97+
}
98+
99+
#[test]
100+
fn unsupported() {
101+
let err = DebugError::Unsupported("flatCallTracer");
102+
assert_eq!(err.error_code(), -32601);
103+
assert!(err.error_message().contains("flatCallTracer"));
104+
}
105+
106+
#[test]
107+
fn evm_halt() {
108+
let err = DebugError::EvmHalt { reason: "OutOfGas".into() };
109+
assert_eq!(err.error_code(), -32000);
110+
assert!(err.error_message().contains("OutOfGas"));
111+
}
112+
113+
#[test]
114+
fn block_not_found() {
115+
let err = DebugError::BlockNotFound(BlockId::latest());
116+
assert_eq!(err.error_code(), -32001);
117+
assert!(err.error_message().contains("block not found"));
118+
}
119+
120+
#[test]
121+
fn transaction_not_found() {
122+
let err = DebugError::TransactionNotFound(B256::ZERO);
123+
assert_eq!(err.error_code(), -32001);
124+
assert!(err.error_message().contains("transaction not found"));
125+
}
126+
127+
#[test]
128+
fn internal() {
129+
let err = DebugError::Internal("task panicked or cancelled".into());
130+
assert_eq!(err.error_code(), -32000);
131+
assert!(err.error_message().contains("task panicked"));
42132
}
43133
}

0 commit comments

Comments
 (0)