Skip to content

Normalise rpc debug trace format #839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

AmosAidoo
Copy link

Which issue does this PR close?

Changes

  • Moved normalisation that Hardhat does on debug trace into EDR.
  • Added new types RpcDebugTraceResult and RpcDebugTraceLogItem that conform to expected structure.
  • Added tests to validate trace structure against expected normalised structure.

How was this tested?

  • Compared trace manually with output from Geth.
  • Wrote test to validate normalised structure.
  • Ran tests and verified all tests pass including old ones.

Copy link

changeset-bot bot commented Mar 23, 2025

🦋 Changeset detected

Latest commit: 090729a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@nomicfoundation/edr Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@AmosAidoo
Copy link
Author

@nebasuke This is my PR for the issue.
I am available anytime to make the necessary changes.

@nebasuke nebasuke requested a review from a team March 24, 2025 12:23
@nebasuke
Copy link
Member

Great, thank you @AmosAidoo, we appreciate the effort!

It will probably be @Wodann or @agostbiro that can take a look.

@AmosAidoo
Copy link
Author

cd hardhat-tests && pnpm test:ci
Noticed I missed these tests. I'm taking a look.

@nebasuke
Copy link
Member

Noticed I missed these tests. I'm taking a look.

Great, thanks! I'll re-run CI when needed.

nebasuke and others added 3 commits March 25, 2025 11:29
additionally added pass and gasUsed fields and leading 0x for storage and memory
as the hardhat tests still expects them
@AmosAidoo
Copy link
Author

the tests still expect the pass, gasUsed and opName fields so I added them in addition to the new format. Also, the tests still assume the memory and storage strings will have the leading 0x string.
So I guess this PR will need a follow-up issue after this is integrated into Hardhat. The hardhat tests will then have to be updated afterwards.

@AmosAidoo
Copy link
Author

Great, thanks! I'll re-run CI when needed.

You can re-run the CI

@AmosAidoo
Copy link
Author

Can you kindly re-run

Copy link
Member

@Wodann Wodann left a comment

Choose a reason for hiding this comment

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

Thank you for taking a stab at this. I left some requests to try and move the changes into edr_provider as the current approach goes through multiple steps of serialization and deserialization.

Comment on lines 41 to 48
pub struct RpcDebugTraceResult {
pub failed: bool,
pub gas: BigInt,
pub pass: bool,
pub gas_used: BigInt,
pub return_value: String,
pub struct_logs: Vec<RpcDebugTraceLogItem>,
}
Copy link
Member

Choose a reason for hiding this comment

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

Could you please add documentation for the struct and its pub member fields?

Copy link
Author

Choose a reason for hiding this comment

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

Done

@@ -102,6 +105,9 @@ impl Provider {
#[doc = "Handles a JSON-RPC request and returns a JSON-RPC response."]
#[napi]
pub async fn handle_request(&self, json_request: String) -> napi::Result<Response> {
let method_name = serde_json::from_str::<JsonRpcMethodExtractor>(&json_request)
.map_or_else(|_| "".to_string(), |m| m.method);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
.map_or_else(|_| "".to_string(), |m| m.method);
.map_or_else(|_| String::new(), |m| m.method);

Copy link
Author

Choose a reason for hiding this comment

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

Removed this as well

Comment on lines 223 to 245
let parsed_data: Result<DebugTraceResult, _> = match &data {
Either::A(json_string) => {
serde_json::from_str::<JsonRpcResponse<DebugTraceResult>>(json_string)
.map(|r| r.result)
}
Either::B(json_value) => serde_json::from_value::<
JsonRpcResponse<DebugTraceResult>,
>(json_value.clone())
.map(|r| r.result),
};

match parsed_data {
Ok(trace_result) => {
let transformed = normalise_rpc_debug_trace(trace_result)
.map_err(|e| napi::Error::new(Status::GenericFailure, e))?;

let transformed = JsonRpcResponse {
result: transformed,
};
Either::B(serde_json::to_value(transformed)?)
}
Err(_) => data,
}
Copy link
Member

Choose a reason for hiding this comment

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

Could you please split this into a function with a descriptive name?

Copy link
Author

@AmosAidoo AmosAidoo Mar 25, 2025

Choose a reason for hiding this comment

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

No need for these lines now after moving normalisation to edr_provider.

Comment on lines 223 to 232
let parsed_data: Result<DebugTraceResult, _> = match &data {
Either::A(json_string) => {
serde_json::from_str::<JsonRpcResponse<DebugTraceResult>>(json_string)
.map(|r| r.result)
}
Either::B(json_value) => serde_json::from_value::<
JsonRpcResponse<DebugTraceResult>,
>(json_value.clone())
.map(|r| r.result),
};
Copy link
Member

Choose a reason for hiding this comment

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

It is incredibly inefficient to:

  1. serialize the result to JSON (string or object)
  2. deserialize the result from JSON (string or object)
  3. normalize result
  4. serialize the result

Could you please investigate making this change one level up, before we first serialize the result? That would be in edr_provider

Copy link
Author

Choose a reason for hiding this comment

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

I have moved the normalisation into edr_provider

@AmosAidoo
Copy link
Author

Thank you for taking a stab at this. I left some requests to try and move the changes into edr_provider as the current approach goes through multiple steps of serialization and deserialization.

Perfect, thank you for the review. I'll make the changes.

@AmosAidoo
Copy link
Author

I made the necessary changes. Tests pass when I run them locally. Could you kindly re-trigger the ci.

@AmosAidoo
Copy link
Author

Seems to be looking good so far regarding the tests. Looks like the remaining failures are because of environmental variables not set as I am an external contributor.

@nebasuke nebasuke requested a review from Wodann March 27, 2025 09:38
Copy link
Member

@Wodann Wodann left a comment

Choose a reason for hiding this comment

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

This is going in the right direction, but quite a few things are still unclear to me about the chosen solution. I'd appreciate if you could clarify.

/// together because Hardhat still depends on them but `pass` and `gas_used`
/// should likely to be removed after a while.
#[napi(object)]
pub struct RpcDebugTraceResult {
Copy link
Member

Choose a reason for hiding this comment

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

As this type is no longer part of the API, do we really need to expose it?

I assume Hardhat has its own definition, given that it's a standard.

cc: @fvictorio

Copy link
Author

Choose a reason for hiding this comment

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

You are right, Hardhat has its own definition

/// Debug logs after normalising the EIP-3155 debug logs.
/// This is the format Hardhat expects
#[napi(object)]
pub struct RpcDebugTraceLogItem {
Copy link
Member

Choose a reason for hiding this comment

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

As this type is no longer part of the API, do we really need to expose it?

I assume Hardhat has its own definition, given that it's a standard.

cc: @fvictorio

@@ -415,3 +515,48 @@ function assertEqualMemory(stepMemory: Buffer | undefined, expected: Buffer) {

assert.isTrue(stepMemory.equals(expected));
}

function assertJsonRpcFormatNormalised(trace: RpcDebugTraceResult) {
Copy link
Member

Choose a reason for hiding this comment

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

I think these assertions can happen without knowing the type RpcDebugTraceResult, but am not 100%. If so, we can just remove the type from EDR's N-API, reducing our API surface.

Comment on lines +553 to +554
// assert.isFalse(key.startsWith("0x"));
// assert.isFalse(value.startsWith("0x"));
Copy link
Member

Choose a reason for hiding this comment

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

Why are these two disabled?

Copy link
Author

Choose a reason for hiding this comment

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

I disabled these two because at this moment, the normalisation is still being done in Hardhat so it is still correct to have 0x at the start of the strings.
Here is the reference in Hardhat: https://github.com/NomicFoundation/hardhat/blob/024d72b09c6edefb00c012e9514a0948c255d0ab/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts#L197-L203

The Hardhat tests also operate on this assumption so I believe these two asserts should be disabled until the normalisation is completely removed from Hardhat.

Copy link
Member

Choose a reason for hiding this comment

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

There is a process for incorporating changes to Hardhat into your PR. It's explained here. That's what would be required in order to test your changes in Hardhat and make sure everything is working as expected.

Copy link
Author

Choose a reason for hiding this comment

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

Ah perfect. Thank you

@@ -380,10 +382,12 @@ impl<LoggerErrorT: Debug + Send + Sync + 'static, TimerT: Clone + TimeSinceEpoch
// debug_* methods
MethodInvocation::DebugTraceTransaction(transaction_hash, config) => {
debug::handle_debug_trace_transaction(data, transaction_hash, config)
.and_then(normalise_rpc_debug_trace)
Copy link
Member

Choose a reason for hiding this comment

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

Please move this call inside the handle_debug_trace_transaction call, following the same pattern as all other functions. The handle_* functions determine the result type, not the provider.


// Rust port of https://github.com/NomicFoundation/hardhat/blob/024d72b09c6edefb00c012e9514a0948c255d0ab/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts#L176
/// This normalization is done because this is the format Hardhat expects
fn normalise_rpc_debug_trace<LoggerErrorT: Debug>(
Copy link
Member

Choose a reason for hiding this comment

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

Please move this to the same file as the handle_ function I mentioned in my other comment.

Comment on lines 541 to 542
// Removing this trim temporarily as the Hardhat test assumes 0x is there
// .map(|value| value.trim_start_matches("0x").to_string())
Copy link
Member

Choose a reason for hiding this comment

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

If Hardhat requires the 0x, then why do you believe we should trim the 0x?

Copy link
Author

Choose a reason for hiding this comment

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

storage: log.storage.map(|storage| {
storage
.into_iter()
// Removing this trim temporarily as the Hardhat test assumes 0x is there
Copy link
Member

Choose a reason for hiding this comment

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

If Hardhat requires the 0x, then why do you believe we should trim the 0x?

Copy link
Author

Choose a reason for hiding this comment

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

Same as the reasons stated for the two related comments above. Hardhat doesn't require 0x but assumes it is there and so it get removed during normalisation.

@@ -120,3 +121,48 @@ impl From<DebugTraceConfig> for edr_evm::DebugTraceConfig {
}
}
}

/// This is the JSON-RPC Debug trace format
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
Copy link
Member

Choose a reason for hiding this comment

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

Do we need both Deserialize and Serialize? Based on the code, it seems that serde::Serialize is sufficient.

Copy link
Author

Choose a reason for hiding this comment

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

Yes you are right, thanks for pointing it out. I'll remove it.

#[serde(rename_all = "camelCase")]
pub struct RpcDebugTraceResult {
pub failed: bool,
pub gas: u64,
Copy link
Member

Choose a reason for hiding this comment

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

Did the old format require a 0x... representation? What made you decide to go with normal u64 serialization?

Do you have a reference to support this choice?

If we should support a hex representation of a quantity, you should use alloy_serde::quantity.

This applies to all u64 usages here.

Copy link
Author

Choose a reason for hiding this comment

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

Yes so I used u64 because gas_used in DebugTraceResult is also u64:
https://github.com/NomicFoundation/edr/blob/main/crates/edr_evm/src/debug_trace.rs#L220

Copy link
Author

Choose a reason for hiding this comment

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

Noted the point on alloy_serde::quantity for hex quantities.

Remove unused serde::Deserialize from rpc debug structs and
move normalization into debug.rs, following the pattern of
other functions in the provider
@AmosAidoo
Copy link
Author

I have made some changes and replied to the questions

@Wodann
Copy link
Member

Wodann commented Apr 10, 2025

I added some information to the original issue on how to create test vectors for your code. You can essentially use geth to create some expected output formats and do string comparison between your generated output and the expected output.

Most of the info about debug_traceCall is here, but note that they use a js lib, not the raw rpc, in the snippets.
To be able to port it with confidence, we should create a few test cases using geth, and making sure we match their format.

@AmosAidoo
Copy link
Author

Noted. Thank you

@AmosAidoo
Copy link
Author

I added some information to the original issue on how to create test vectors for your code. You can essentially use geth to create some expected output formats and do string comparison between your generated output and the expected output.

Most of the info about debug_traceCall is here, but note that they use a js lib, not the raw rpc, in the snippets.
To be able to port it with confidence, we should create a few test cases using geth, and making sure we match their format.

@Wodann is it sufficient to use geth in dev mode to generate the test cases?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Normalise JSON-RPC format for rpcDebugTrace
3 participants