Normalise rpc debug trace format - #839
Conversation
Geth is used to generate the desired JSON trace format and these tests validates the results against them.
🦋 Changeset detectedLatest commit: 090729a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
@nebasuke This is my PR for the issue. |
|
Great, thank you @AmosAidoo, we appreciate the effort! It will probably be @Wodann or @agostbiro that can take a look. |
|
|
Great, thanks! I'll re-run CI when needed. |
additionally added pass and gasUsed fields and leading 0x for storage and memory as the hardhat tests still expects them
|
the tests still expect the |
You can re-run the CI |
|
Can you kindly re-run |
Wodann
left a comment
There was a problem hiding this comment.
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.
| 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>, | ||
| } |
There was a problem hiding this comment.
Could you please add documentation for the struct and its pub member fields?
| #[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); |
There was a problem hiding this comment.
| .map_or_else(|_| "".to_string(), |m| m.method); | |
| .map_or_else(|_| String::new(), |m| m.method); |
| 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, | ||
| } |
There was a problem hiding this comment.
Could you please split this into a function with a descriptive name?
There was a problem hiding this comment.
No need for these lines now after moving normalisation to edr_provider.
| 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), | ||
| }; |
There was a problem hiding this comment.
It is incredibly inefficient to:
- serialize the result to JSON (string or object)
- deserialize the result from JSON (string or object)
- normalize result
- 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
There was a problem hiding this comment.
I have moved the normalisation into edr_provider
Perfect, thank you for the review. I'll make the changes. |
|
I made the necessary changes. Tests pass when I run them locally. Could you kindly re-trigger the ci. |
|
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. |
Wodann
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
| assert.isTrue(stepMemory.equals(expected)); | ||
| } | ||
|
|
||
| function assertJsonRpcFormatNormalised(trace: RpcDebugTraceResult) { |
There was a problem hiding this comment.
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.
| // assert.isFalse(key.startsWith("0x")); | ||
| // assert.isFalse(value.startsWith("0x")); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| // debug_* methods | ||
| MethodInvocation::DebugTraceTransaction(transaction_hash, config) => { | ||
| debug::handle_debug_trace_transaction(data, transaction_hash, config) | ||
| .and_then(normalise_rpc_debug_trace) |
There was a problem hiding this comment.
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>( |
There was a problem hiding this comment.
Please move this to the same file as the handle_ function I mentioned in my other comment.
| // Removing this trim temporarily as the Hardhat test assumes 0x is there | ||
| // .map(|value| value.trim_start_matches("0x").to_string()) |
There was a problem hiding this comment.
If Hardhat requires the 0x, then why do you believe we should trim the 0x?
There was a problem hiding this comment.
Hardhat doesn't require the 0x but assumes they are there. Hardhat later on trims the 0x when normalising. Here is the relevant code:
https://github.com/NomicFoundation/hardhat/blob/024d72b09c6edefb00c012e9514a0948c255d0ab/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts#L192-L195
When running the hardhart-tests for example, I confirmed this expected format from the trace generated from Geth which the tests depend on:
| storage: log.storage.map(|storage| { | ||
| storage | ||
| .into_iter() | ||
| // Removing this trim temporarily as the Hardhat test assumes 0x is there |
There was a problem hiding this comment.
If Hardhat requires the 0x, then why do you believe we should trim the 0x?
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| /// This is the JSON-RPC Debug trace format | ||
| #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] |
There was a problem hiding this comment.
Do we need both Deserialize and Serialize? Based on the code, it seems that serde::Serialize is sufficient.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
|
I have made some changes and replied to the questions |
|
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.
|
|
Noted. Thank you |
@Wodann is it sufficient to use geth in dev mode to generate the test cases? |
I don't see why not; as long as the generated output conforms to the JSON-RPC spec. |
Which issue does this PR close?
Changes
RpcDebugTraceResultandRpcDebugTraceLogItemthat conform to expected structure.How was this tested?