Skip to content

feat(evm): Surface on-chain revert data in failed transaction status#802

Open
dylankilkenny wants to merge 2 commits into
mainfrom
004-evm-revert-data
Open

feat(evm): Surface on-chain revert data in failed transaction status#802
dylankilkenny wants to merge 2 commits into
mainfrom
004-evm-revert-data

Conversation

@dylankilkenny

@dylankilkenny dylankilkenny commented Jun 16, 2026

Copy link
Copy Markdown
Member

What & why

Closes #781.

Today a failed EVM transaction records a generic status_reason:
Transaction reverted on-chain (receipt status: failed) — enough to know that it reverted, not why. Every downstream consumer (dashboards, alerting) has to re-derive the revert reason client-side via debug_traceTransaction/eth_call, which only works on capable RPCs and adds a round-trip per failure.

This change surfaces the on-chain revert payload (4-byte selector + ABI-encoded args) directly on the failed transaction:

Transaction reverted on-chain (revert_data: 0x5592f1b2000000...)

How

On the transition into Failed (only once per tx), recovery runs best-effort:

  1. Preferreddebug_traceTransaction(hash, {tracer: callTracer}), reading the top-level output.
  2. Fallback — re-run the tx as an eth_call at the receipt's execution block; the revert bytes come from the JSON-RPC error data.
  3. Degrade — keep the existing generic string (preserved byte-for-byte) when neither yields data.

A new mockable EvmProviderTrait::get_call_revert_data extracts the revert bytes from RpcError::ErrorResp before the lossy From<RpcError> conversion (which drops the data field).

All recovery is gated by a new per-relayer EVM policy include_revert_data (default enabled), plumbed through config → domain model → API response → OpenAPI, mirroring gas_limit_estimation. Set it to false to skip the extra RPC calls on cost-sensitive deployments.

Recovery errors are always contained (logged, never propagated), so a recovery failure never changes the Failed outcome.

Tests

  • Domain (MockEvmProvider): trace-hit, eth_call fallback, both-fail→generic, already-Failed→skip, disabled→zero recovery RPCs, enabled (None/Some(true))→attempted.
  • Provider trait + config (de)serialization round-trip.
  • cargo test --lib, cargo fmt --check, cargo clippy --lib all pass.

Caveats / follow-ups

  • Provider impl is mock-tested onlyget_call_revert_data's ErrorRespas_revert_data extraction and the callTracer JSON shape are not verified against a live node. A mockito/anvil test on the real EvmProvider would close this gap.
  • Recovery is awaited before the Failed write is persisted, so a slow RPC can delay (not change) the Failed transition. Accepted for v1; could move to persist-first-then-patch if it matters in practice.
  • callTracer output semantics are based on Geth docs; client quirks are why the eth_call fallback exists.

Summary by CodeRabbit

Release Notes

  • New Features

    • Failed EVM transactions now display enriched status reasons recovered from on-chain revert payloads
    • New include_revert_data per-relayer policy option (enabled by default) to disable revert data recovery
  • Documentation

    • Updated configuration and EVM integration documentation with new policy settings
  • API Updates

    • Version bumped to 1.5.0 with new EVM policy field additions

When an EVM transaction transitions into Failed from a failed on-chain
receipt, enrich status_reason with the revert payload as hex:
"Transaction reverted on-chain (revert_data: 0x...)". Recovery runs only
on the transition into Failed and is best-effort: prefer
debug_traceTransaction (callTracer), fall back to eth_call at the
execution block, and degrade to the existing generic string (preserved
byte-for-byte) when neither yields data.

Add a mockable EvmProviderTrait::get_call_revert_data that extracts the
revert bytes from the JSON-RPC error before the lossy From<RpcError>
conversion drops them. Gate all recovery on a new per-relayer EVM policy
include_revert_data (default enabled), plumbed through config, domain
model, API response, and OpenAPI.

Signed-off-by: Dylan Kilkenny <dylankilkenny95@gmail.com>
@dylankilkenny dylankilkenny requested a review from a team as a code owner June 16, 2026 11:09
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6fdc7836-b9ab-45c6-b3b8-2efc7a5018f5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds best-effort on-chain revert payload recovery into status_reason for failed EVM transactions. Introduces include_revert_data: Option<bool> to RelayerEvmPolicy and ConfigFileRelayerEvmPolicy, a new get_call_revert_data provider method, and recovery logic in the EVM status handler that tries debug_traceTransaction then eth_call fallback. Updates OpenAPI, docs, constants, response models, and propagates the new field across test fixtures.

Changes

EVM Revert Data Recovery Feature

Layer / File(s) Summary
Policy field definition and config propagation
src/constants/relayer.rs, src/models/relayer/mod.rs, src/models/relayer/config.rs, src/api/controllers/relayer.rs, src/domain/relayer/evm/..., src/domain/transaction/evm/evm_transaction.rs, src/jobs/handlers/..., src/repositories/relayer/..., src/utils/mocks.rs
Adds DEFAULT_EVM_INCLUDE_REVERT_DATA = true constant, include_revert_data: Option<bool> field to RelayerEvmPolicy and ConfigFileRelayerEvmPolicy, wires config-to-domain conversion, and propagates include_revert_data: None across all test fixtures.
API response model and OpenAPI schema
src/models/relayer/response.rs, openapi.json
Adds include_revert_data: bool to EvmPolicyResponse with serde default via default_evm_include_revert_data(), extends empty-policy detection, maps domain field to response, and bumps OpenAPI to 1.5.0 with the new field in both EVM policy schemas.
EvmProviderTrait::get_call_revert_data
src/services/provider/evm/mod.rs
Extends the provider trait and EvmProvider with get_call_revert_data that executes an eth_call at a given block, returns None on success, and extracts revert payload bytes from RpcError::ErrorResp. Adds mock-based unit tests.
Revert recovery logic in status handler
src/domain/transaction/evm/status.rs
Adds build_revert_call_request, extract_trace_output, build_failed_status_reason, and recover_revert_data helpers. On Failed transition, attempts debug_traceTransaction first, falls back to eth_call; all failures fall through to the legacy generic reason. Adds revert_data_recovery_tests suite.
Docs and changelog
CHANGELOG.md, docs/configuration/index.mdx, docs/evm.mdx
Documents revert-data recovery and include_revert_data policy toggle in changelog, configuration reference table, and EVM relayer policies section.

Sequence Diagram(s)

sequenceDiagram
  participant Poller as Transaction Poller
  participant StatusHandler as handle_status_impl
  participant RevertRecovery as recover_revert_data
  participant Provider as EvmProvider
  participant RPC as EVM RPC Node

  Poller->>StatusHandler: receipt.status == 0 (Failed)
  StatusHandler->>RevertRecovery: include_revert_data=true, tx_hash, receipt_block
  RevertRecovery->>Provider: debug_traceTransaction(hash, callTracer)
  Provider->>RPC: debug_traceTransaction
  alt trace returns revert output
    RPC-->>Provider: trace JSON with output bytes
    Provider-->>RevertRecovery: Some(revert_bytes)
    RevertRecovery-->>StatusHandler: status_reason = "reverted (revert_data: 0x...)"
  else trace unavailable or empty output
    RPC-->>Provider: error or empty
    Provider-->>RevertRecovery: None
    RevertRecovery->>Provider: get_call_revert_data(eth_call, block)
    Provider->>RPC: eth_call at receipt block
    alt eth_call reverts with data
      RPC-->>Provider: RpcError::ErrorResp(data)
      Provider-->>RevertRecovery: Some(Bytes)
      RevertRecovery-->>StatusHandler: status_reason = "reverted (revert_data: 0x...)"
    else no revert data
      RPC-->>Provider: Ok or no data
      Provider-->>RevertRecovery: None
      RevertRecovery-->>StatusHandler: status_reason = "Transaction reverted on-chain (receipt status: failed)"
    end
  end
  StatusHandler-->>Poller: updated transaction
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

cla: allowlist

Suggested reviewers

  • zeljkoX

🐇 A transaction failed, but why did it revert?
I hopped through trace calls to find the clue!
debug_traceTransaction tried first, you see,
then eth_call as fallback, for completeness too. 🔍
Now status_reason holds the revert bytes plain —
no client-side detective work again! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(evm): Surface on-chain revert data in failed transaction status' clearly and concisely describes the main feature addition: surfacing revert data in failed EVM transaction status.
Description check ✅ Passed The PR description is comprehensive, covering motivation, implementation approach, testing, and known caveats. It includes a reference to the related issue (#781) and confirms unit tests were added.
Linked Issues check ✅ Passed The PR addresses all core requirements from #781: surfaces on-chain revert data in status_reason [evm_transaction.rs, status.rs], implements tiered recovery (debug_traceTransaction preferred, eth_call fallback) [status.rs, evm/mod.rs], and includes configuration flag [config.rs, response.rs].
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the revert data feature: domain/API model additions for include_revert_data field, status enrichment logic, provider trait extension, config plumbing, and test updates. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 004-evm-revert-data

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.04878% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.55%. Comparing base (fddc35c) to head (9aee9db).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
src/domain/transaction/evm/status.rs 91.87% 26 Missing ⚠️
src/services/provider/evm/mod.rs 58.69% 19 Missing ⚠️
src/models/relayer/response.rs 75.00% 3 Missing ⚠️
src/models/relayer/repository.rs 83.33% 1 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
ai 0.00% <0.00%> (ø)
dev 89.55% <88.04%> (-0.72%) ⬇️
properties 0.01% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

@@            Coverage Diff             @@
##             main     #802      +/-   ##
==========================================
- Coverage   90.26%   89.55%   -0.72%     
==========================================
  Files         292      301       +9     
  Lines      125079   128106    +3027     
==========================================
+ Hits       112897   114719    +1822     
- Misses      12182    13387    +1205     
Files with missing lines Coverage Δ
src/api/controllers/relayer.rs 52.69% <ø> (ø)
src/domain/relayer/evm/evm_relayer.rs 99.21% <100.00%> (+<0.01%) ⬆️
src/domain/relayer/evm/validations.rs 100.00% <100.00%> (ø)
src/domain/transaction/evm/evm_transaction.rs 95.43% <100.00%> (+<0.01%) ⬆️
src/jobs/handlers/notification_handler.rs 78.63% <100.00%> (+0.18%) ⬆️
src/jobs/handlers/relayer_health_check_handler.rs 81.47% <100.00%> (+0.04%) ⬆️
src/models/relayer/config.rs 91.86% <100.00%> (+0.14%) ⬆️
src/models/relayer/mod.rs 91.28% <100.00%> (+<0.01%) ⬆️
src/models/relayer/request.rs 96.07% <100.00%> (+<0.01%) ⬆️
src/repositories/relayer/mod.rs 81.08% <100.00%> (+0.17%) ⬆️
... and 6 more

... and 33 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/models/relayer/repository.rs (1)

365-373: ⚡ Quick win

Add explicit assertions for include_revert_data propagation in these conversion tests.

You now set include_revert_data in fixtures, but the tests don’t assert it after conversion/round-trip. That can let regressions slip through silently.

Suggested patch
@@
         if let RelayerNetworkPolicy::Evm(evm_policy) = repo_model.policies {
+            assert_eq!(evm_policy.include_revert_data, None);
             assert_eq!(evm_policy.gas_price_cap, Some(100_000_000_000));
             assert_eq!(evm_policy.eip1559_pricing, Some(true));
         } else {
             panic!("Expected EVM policy");
         }
@@
         let repo_evm = RelayerRepoModel::from(original_evm.clone());
         let recovered_evm = Relayer::from(repo_evm);
@@
         assert_eq!(original_evm.id, recovered_evm.id);
         assert_eq!(original_evm.network_type, recovered_evm.network_type);
         assert_eq!(original_evm.notification_id, recovered_evm.notification_id);
+        if let Some(RelayerNetworkPolicy::Evm(policy)) = recovered_evm.policies {
+            assert_eq!(policy.include_revert_data, None);
+        } else {
+            panic!("Expected EVM policy");
+        }

Also applies to: 896-904

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/models/relayer/repository.rs` around lines 365 - 373, The conversion
tests for RelayerEvmPolicy fixtures do not include explicit assertions to verify
that the include_revert_data field is properly propagated through round-trip
conversions. Modify the test fixtures to set include_revert_data to a specific
non-None value (e.g., Some(true) or Some(false)) instead of None, and then add
explicit assertions after each conversion/round-trip operation in the test to
verify that the include_revert_data field in the resulting RelayerEvmPolicy
matches the expected value. Apply this change at both the primary location
(lines 365-373) and the secondary location mentioned at lines 896-904.
src/services/provider/evm/mod.rs (1)

1212-1248: 🏗️ Heavy lift

Add a test that exercises the concrete provider implementation.

These tests only stub MockEvmProviderTrait, so they would pass even if EvmProvider::get_call_revert_data mishandled RpcError::ErrorResp, block selection, or revert-data extraction. Add an implementation-level test with a mocked JSON-RPC response, or extract the error-response handling into a small helper and test that directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/provider/evm/mod.rs` around lines 1212 - 1248, The existing
tests test_get_call_revert_data_returns_payload and
test_get_call_revert_data_no_revert_returns_none only mock the EvmProviderTrait
and would not catch bugs in the actual EvmProvider::get_call_revert_data
implementation's handling of RpcError::ErrorResp, block selection, or
revert-data extraction. Add an implementation-level test that mocks the JSON-RPC
responses directly to test the concrete EvmProvider behavior, or extract the
error-response and revert-data handling logic into a small helper function and
add unit tests for that helper directly.
src/domain/transaction/evm/status.rs (1)

1099-1105: Replace hard-coded true with DEFAULT_EVM_INCLUDE_REVERT_DATA constant.

The .unwrap_or(true) at lines 1099-1105 should use the shared constant to maintain consistency with the relayer policy defaults. If DEFAULT_EVM_INCLUDE_REVERT_DATA changes in the future, this code path would still use the old default.

♻️ Proposed fix
use crate::constants::{
    get_evm_min_age_for_hash_recovery, get_evm_pending_recovery_trigger_timeout,
    get_evm_prepare_timeout, get_evm_resend_timeout, ARBITRUM_TIME_TO_RESUBMIT,
-    EVM_MIN_HASHES_FOR_RECOVERY, MAX_GAP_SCAN_RANGE,
+    DEFAULT_EVM_INCLUDE_REVERT_DATA, EVM_MIN_HASHES_FOR_RECOVERY, MAX_GAP_SCAN_RANGE,
 };
             .get_evm_policy()
             .include_revert_data
-            .unwrap_or(true)
+            .unwrap_or(DEFAULT_EVM_INCLUDE_REVERT_DATA)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/domain/transaction/evm/status.rs` around lines 1099 - 1105, Replace the
hard-coded `true` value in the `.unwrap_or(true)` call on the
`.include_revert_data` property access chain with the
`DEFAULT_EVM_INCLUDE_REVERT_DATA` constant. This ensures consistency with the
relayer policy defaults across the codebase, so that if the default value is
updated in the future, all code paths automatically use the new default value
rather than maintaining a separate hard-coded value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/domain/transaction/evm/status.rs`:
- Around line 1113-1115: The revert data recovered from
`recover_revert_data(tx).await` can be arbitrarily large (controlled by
RPC/contract), and when included in the format string for `status_reason`, it
can cause database or notification write failures. Bound the recovered hex
string to a reasonable maximum size before including it in the formatted error
message. When the hex exceeds the limit, truncate it and optionally append an
indicator (like "...") to show it was truncated. Apply this bounding logic in
the Some branch of the match expression before the format! call.
- Around line 67-73: The eth_call fallback request in the TransactionRequest
construction is dropping gas-price and EIP-1559 fee fields (gas_price,
max_fee_per_gas, max_priority_fee_per_gas), which causes contracts that branch
on tx.gasprice to return different revert reasons. Add these missing fee fields
to the TransactionRequest being constructed by extracting them from the evm_data
object (similar to how value and gas_limit are already being populated),
ensuring the fallback preserves complete transaction context. Additionally,
extend the fallback test to assert that the reconstructed request includes these
fee fields when no tracing is available.

In `@src/services/provider/evm/mod.rs`:
- Around line 558-560: The `Err(RpcError::ErrorResp(payload))` match arm
currently treats all error responses that lack revert data as `Ok(None)`,
bypassing the retry mechanism. Instead, check whether the error response payload
contains a retriable error code (such as -32002, -32005, -32603); if it does and
has no revert data, return `Err(ProviderError)` to allow retry logic and
provider failover to handle it. Only return `Ok(payload.as_revert_data())` when
revert bytes are present or when the error is non-retriable, ensuring retriable
errors are propagated as `Err` results rather than swallowed as successful
`None` responses.

---

Nitpick comments:
In `@src/domain/transaction/evm/status.rs`:
- Around line 1099-1105: Replace the hard-coded `true` value in the
`.unwrap_or(true)` call on the `.include_revert_data` property access chain with
the `DEFAULT_EVM_INCLUDE_REVERT_DATA` constant. This ensures consistency with
the relayer policy defaults across the codebase, so that if the default value is
updated in the future, all code paths automatically use the new default value
rather than maintaining a separate hard-coded value.

In `@src/models/relayer/repository.rs`:
- Around line 365-373: The conversion tests for RelayerEvmPolicy fixtures do not
include explicit assertions to verify that the include_revert_data field is
properly propagated through round-trip conversions. Modify the test fixtures to
set include_revert_data to a specific non-None value (e.g., Some(true) or
Some(false)) instead of None, and then add explicit assertions after each
conversion/round-trip operation in the test to verify that the
include_revert_data field in the resulting RelayerEvmPolicy matches the expected
value. Apply this change at both the primary location (lines 365-373) and the
secondary location mentioned at lines 896-904.

In `@src/services/provider/evm/mod.rs`:
- Around line 1212-1248: The existing tests
test_get_call_revert_data_returns_payload and
test_get_call_revert_data_no_revert_returns_none only mock the EvmProviderTrait
and would not catch bugs in the actual EvmProvider::get_call_revert_data
implementation's handling of RpcError::ErrorResp, block selection, or
revert-data extraction. Add an implementation-level test that mocks the JSON-RPC
responses directly to test the concrete EvmProvider behavior, or extract the
error-response and revert-data handling logic into a small helper function and
add unit tests for that helper directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5dce9d8e-bcff-4d70-8134-10afea0aebdc

📥 Commits

Reviewing files that changed from the base of the PR and between ef5d08b and 385727a.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • docs/configuration/index.mdx
  • docs/evm.mdx
  • openapi.json
  • src/api/controllers/relayer.rs
  • src/constants/relayer.rs
  • src/domain/relayer/evm/evm_relayer.rs
  • src/domain/relayer/evm/validations.rs
  • src/domain/transaction/evm/evm_transaction.rs
  • src/domain/transaction/evm/status.rs
  • src/jobs/handlers/notification_handler.rs
  • src/jobs/handlers/relayer_health_check_handler.rs
  • src/models/relayer/config.rs
  • src/models/relayer/mod.rs
  • src/models/relayer/repository.rs
  • src/models/relayer/request.rs
  • src/models/relayer/response.rs
  • src/repositories/relayer/mod.rs
  • src/repositories/relayer/relayer_in_memory.rs
  • src/repositories/relayer/relayer_redis.rs
  • src/services/provider/evm/mod.rs
  • src/utils/mocks.rs

Comment thread src/domain/transaction/evm/status.rs
Comment thread src/domain/transaction/evm/status.rs
Comment thread src/services/provider/evm/mod.rs Outdated
- get_call_revert_data: forward non-revert ErrorResp as Err so retriable
  JSON-RPC codes (rate limit, internal error) hit retry/failover instead of
  being silently reported as "no revert data"
- bound recovered revert hex embedded in status_reason to avoid oversized
  DB/notification writes from RPC/contract-controlled payloads
- preserve fee context (gas price / EIP-1559 caps) in the eth_call fallback
  request so contracts branching on tx.gasprice revert as they did on-chain
- use DEFAULT_EVM_INCLUDE_REVERT_DATA instead of hard-coded true
- assert include_revert_data propagation in conversion round-trip tests

Signed-off-by: Dylan Kilkenny <dylankilkenny95@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Include on-chain revert data in status_reason for failed EVM transactions

1 participant