Skip to content

Commit 112c417

Browse files
prestwichclaude
andcommitted
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>
1 parent 6ab1327 commit 112c417

11 files changed

Lines changed: 469 additions & 364 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: 50 additions & 49 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,33 @@ 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!(
109+
hctx.spawn(fut),
110+
DebugError::EvmHalt { reason: "task panicked or cancelled".into() }
111+
)
114112
}
115113

116114
/// `debug_traceTransaction` handler.
117115
pub(super) async fn trace_transaction<H>(
118116
hctx: HandlerCtx,
119117
TraceTransactionParams(tx_hash, opts): TraceTransactionParams,
120118
ctx: StorageRpcCtx<H>,
121-
) -> ResponsePayload<GethTrace, DebugError>
119+
) -> Result<GethTrace, DebugError>
122120
where
123121
H: HotKv + Send + Sync + 'static,
124122
<H::RoTx as HotKvRead>::Error: DBErrorMarker,
125123
{
126-
let opts = response_tri!(opts.ok_or(DebugError::InvalidTracerConfig));
124+
let opts = opts.ok_or(DebugError::InvalidTracerConfig)?;
127125

128126
// Held for the handler duration; dropped when the async block completes.
129127
let _permit = ctx.acquire_tracing_permit().await;
@@ -134,37 +132,36 @@ where
134132
let cold = ctx.cold();
135133

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

142-
let confirmed = response_tri!(confirmed.ok_or(DebugError::TransactionNotFound));
140+
let confirmed = confirmed.ok_or(DebugError::TransactionNotFound(tx_hash))?;
143141
let (_tx, meta) = confirmed.into_parts();
144142

145143
let block_num = meta.block_number();
146144
let block_hash = meta.block_hash();
147145

148146
let block_id = BlockId::Number(block_num.into());
149-
let sealed = response_tri!(ctx.resolve_header(block_id).map_err(|e| {
147+
let sealed = ctx.resolve_header(block_id).map_err(|e| {
150148
tracing::warn!(error = %e, block_num, "header resolution failed");
151149
DebugError::BlockNotFound(block_id)
152-
}));
153-
let header = response_tri!(sealed.ok_or(DebugError::BlockNotFound(block_id))).into_inner();
150+
})?;
151+
let header = sealed.ok_or(DebugError::BlockNotFound(block_id))?.into_inner();
154152

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

160158
tracing::debug!(number = block_num, "Loaded containing block");
161159

162160
// 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-
}));
161+
let db = ctx.revm_state_at_height(block_num.saturating_sub(1)).map_err(|e| {
162+
tracing::warn!(error = %e, block_num, "hot storage read failed");
163+
DebugError::from(e)
164+
})?;
168165

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

183-
trevm = response_tri!(trevm.run_tx(tx).map_err(EvmErrored::into_error)).accept_state();
178+
trevm = trevm
179+
.run_tx(tx)
180+
.map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?
181+
.accept_state();
184182
}
185183

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

188186
let trevm = trevm.fill_tx(tx);
189187

@@ -195,11 +193,14 @@ where
195193
base_fee: header.base_fee_per_gas(),
196194
};
197195

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

200-
ResponsePayload(Ok(res))
198+
Ok(res)
201199
}
202200
.instrument(span);
203201

204-
await_handler!(@response_option hctx.spawn(fut))
202+
await_handler!(
203+
hctx.spawn(fut),
204+
DebugError::EvmHalt { reason: "task panicked or cancelled".into() }
205+
)
205206
}

crates/rpc/src/debug/error.rs

Lines changed: 40 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,55 @@ 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),
3436
}
3537

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())
38+
impl ajj::IntoErrorPayload for DebugError {
39+
type ErrData = ();
40+
41+
fn error_code(&self) -> i64 {
42+
match self {
43+
Self::Cold(_) | Self::Hot(_) | Self::EvmHalt { .. } => -32000,
44+
Self::Resolve(r) => crate::eth::error::resolve_error_code(r),
45+
Self::InvalidTracerConfig => -32602,
46+
Self::Unsupported(_) => -32601,
47+
Self::BlockNotFound(_) | Self::TransactionNotFound(_) => -32001,
48+
}
49+
}
50+
51+
fn error_message(&self) -> Cow<'static, str> {
52+
match self {
53+
Self::Cold(_) | Self::Hot(_) => "server error".into(),
54+
Self::Resolve(r) => crate::eth::error::resolve_error_message(r),
55+
Self::InvalidTracerConfig => "invalid tracer config".into(),
56+
Self::Unsupported(msg) => format!("unsupported: {msg}").into(),
57+
Self::EvmHalt { reason } => format!("execution halted: {reason}").into(),
58+
Self::BlockNotFound(id) => format!("block not found: {id}").into(),
59+
Self::TransactionNotFound(h) => format!("transaction not found: {h}").into(),
60+
}
61+
}
62+
63+
fn error_data(self) -> Option<Self::ErrData> {
64+
None
4265
}
4366
}

crates/rpc/src/debug/tracer.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ where
5353
NoopFrame::default().into(),
5454
trevm
5555
.run()
56-
.map_err(|err| DebugError::Evm(err.into_error().to_string()))?
56+
.map_err(|err| DebugError::EvmHalt { reason: err.into_error().to_string() })?
5757
.accept_state(),
5858
)),
5959
GethDebugBuiltInTracerType::MuxTracer => trace_mux(&config.tracer_config, trevm, tx_info),
@@ -70,7 +70,7 @@ where
7070
let mut four_byte = FourByteInspector::default();
7171
let trevm = trevm
7272
.try_with_inspector(&mut four_byte, |trevm| trevm.run())
73-
.map_err(|e| DebugError::Evm(e.into_error().to_string()))?;
73+
.map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?;
7474
Ok((FourByteFrame::from(four_byte).into(), trevm.accept_state()))
7575
}
7676

@@ -90,7 +90,7 @@ where
9090

9191
let trevm = trevm
9292
.try_with_inspector(&mut inspector, |trevm| trevm.run())
93-
.map_err(|e| DebugError::Evm(e.into_error().to_string()))?;
93+
.map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?;
9494

9595
let frame = inspector
9696
.with_transaction_gas_limit(trevm.gas_limit())
@@ -118,7 +118,7 @@ where
118118

119119
let trevm = trevm
120120
.try_with_inspector(&mut inspector, |trevm| trevm.run())
121-
.map_err(|e| DebugError::Evm(e.into_error().to_string()))?;
121+
.map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?;
122122
let gas_limit = trevm.gas_limit();
123123

124124
// NB: state must be UNCOMMITTED for prestate diff computation.
@@ -128,7 +128,7 @@ where
128128
.with_transaction_gas_limit(gas_limit)
129129
.into_geth_builder()
130130
.geth_prestate_traces(&result, &prestate_config, trevm.inner_mut_unchecked().db_mut())
131-
.map_err(|err| DebugError::Evm(err.to_string()))?;
131+
.map_err(|err| DebugError::EvmHalt { reason: err.to_string() })?;
132132

133133
// Equivalent to `trevm.accept_state()`.
134134
trevm.inner_mut_unchecked().db_mut().commit(result.state);
@@ -155,7 +155,7 @@ where
155155

156156
let trevm = trevm
157157
.try_with_inspector(&mut inspector, |trevm| trevm.run())
158-
.map_err(|e| DebugError::Evm(e.into_error().to_string()))?;
158+
.map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?;
159159

160160
let frame = inspector
161161
.with_transaction_gas_limit(trevm.gas_limit())
@@ -178,18 +178,18 @@ where
178178
tracer_config.clone().into_mux_config().map_err(|_| DebugError::InvalidTracerConfig)?;
179179

180180
let mut inspector = MuxInspector::try_from_config(mux_config)
181-
.map_err(|err| DebugError::Evm(err.to_string()))?;
181+
.map_err(|err| DebugError::EvmHalt { reason: err.to_string() })?;
182182

183183
let trevm = trevm
184184
.try_with_inspector(&mut inspector, |trevm| trevm.run())
185-
.map_err(|e| DebugError::Evm(e.into_error().to_string()))?;
185+
.map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?;
186186

187187
// NB: state must be UNCOMMITTED for prestate diff computation.
188188
let (result, mut trevm) = trevm.take_result_and_state();
189189

190190
let frame = inspector
191191
.try_into_mux_frame(&result, trevm.inner_mut_unchecked().db_mut(), tx_info)
192-
.map_err(|err| DebugError::Evm(err.to_string()))?;
192+
.map_err(|err| DebugError::EvmHalt { reason: err.to_string() })?;
193193

194194
// Equivalent to `trevm.accept_state()`.
195195
trevm.inner_mut_unchecked().db_mut().commit(result.state);

0 commit comments

Comments
 (0)