From 3e2a01b63c4d1c07ab28f6b1e43abde74a19abc1 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 16 Apr 2026 13:46:35 +0530 Subject: [PATCH 01/10] deny unknown fields --- Cargo.lock | 11 +++ Cargo.toml | 1 + docs/docs/users/reference/env_variables.md | 2 +- src/rpc/json_validator.rs | 85 +++++++++++++++++++++- src/rpc/reflect/parser.rs | 6 +- 5 files changed, 98 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b20de0adc32a..b1872f0b84d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3433,6 +3433,7 @@ dependencies = [ "scopeguard", "semver", "serde", + "serde_ignored", "serde_ipld_dagcbor", "serde_json", "serde_with", @@ -8652,6 +8653,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_ignored" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115dffd5f3853e06e746965a20dcbae6ee747ae30b543d91b0e089668bb07798" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_ipld_dagcbor" version = "0.6.4" diff --git a/Cargo.toml b/Cargo.toml index 4214a98acd3b..60c802894a4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -198,6 +198,7 @@ schemars = { version = "1", features = ["chrono04", "uuid1"] } scopeguard = "1" semver = "1" serde = { workspace = true } +serde_ignored = "0.1" serde_ipld_dagcbor = "0.6" serde_json = { version = "1", features = ["raw_value"] } serde_with = { version = "3", features = ["chrono_0_4"] } diff --git a/docs/docs/users/reference/env_variables.md b/docs/docs/users/reference/env_variables.md index e30f46ffb4eb..926f33d4c38a 100644 --- a/docs/docs/users/reference/env_variables.md +++ b/docs/docs/users/reference/env_variables.md @@ -57,7 +57,7 @@ process. | `FOREST_JWT_DISABLE_EXP_VALIDATION` | 1 or true | empty | 1 | Whether or not to disable JWT expiration validation | | `FOREST_ETH_BLOCK_CACHE_SIZE` | positive integer | 500 | 1 | The size of Eth block cache | | `FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK` | 1 or true | false | 1 | Whether or not to backfill full tipsets from the p2p network | -| `FOREST_STRICT_JSON` | 1 or true | false | 1 | Enable strict JSON validation to detect duplicate keys in RPC requests | +| `FOREST_STRICT_JSON` | 1 or true | false | 1 | Enable strict JSON validation to detect duplicate keys and reject unknown fields in RPC requests | | `FOREST_AUTO_DOWNLOAD_SNAPSHOT_PATH` | URL or file path | empty | `/var/tmp/forest_snapshot_calibnet.forest.car.zst` | Override snapshot path for `--auto-download-snapshot` | | `FOREST_DOWNLOAD_CONNECTIONS` | positive integer | 5 | 10 | Number of parallel HTTP connections for downloading snapshots | | `FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION` | 1 or true | empty | 1 | Whether or not to disable F3 finality resolution in Eth `v1` RPC methods | diff --git a/src/rpc/json_validator.rs b/src/rpc/json_validator.rs index 6d63356c921e..14383290db66 100644 --- a/src/rpc/json_validator.rs +++ b/src/rpc/json_validator.rs @@ -1,12 +1,18 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -//! JSON validation utilities for detecting duplicate keys before serde_json processing. +//! JSON validation utilities for RPC request processing. //! -//! serde_json automatically deduplicates keys at parse time using a "last-write-wins" strategy -//! This means JSON like `{"/":"cid1", "/":"cid2"}` will keep only the last value, which can lead to unexpected behavior in RPC calls. +//! - **Duplicate key detection**: `serde_json` automatically deduplicates keys at parse time +//! using a "last-write-wins" strategy. This means JSON like `{"/":"cid1", "/":"cid2"}` will +//! keep only the last value, which can lead to unexpected behavior in RPC calls. +//! - **Unknown field detection**: `serde_json` silently ignores unknown fields by default. +//! In strict mode, [`from_value_rejecting_unknown_fields`] rejects them in RPC calls. +//! +//! Both checks are gated behind the `FOREST_STRICT_JSON` environment variable. use ahash::HashSet; +use serde::de::DeserializeOwned; pub const STRICT_JSON_ENV: &str = "FOREST_STRICT_JSON"; @@ -50,9 +56,32 @@ pub fn validate_json_for_duplicates(json_str: &str) -> Result<(), String> { check_value(&value) } +/// De-serializes a [`serde_json::Value`] into `T`, rejecting unknown fields when strict mode is +/// enabled. When strict mode is off, this is equivalent to [`serde_json::from_value`]. +pub fn from_value_rejecting_unknown_fields( + value: serde_json::Value, +) -> Result { + if !is_strict_mode() { + return serde_json::from_value(value); + } + let mut unknown = Vec::new(); + let result: T = serde_ignored::deserialize(value, |path| { + unknown.push(path.to_string()); + })?; + if !unknown.is_empty() { + return Err(serde::de::Error::custom(format!( + "unknown field(s): {}. Set {STRICT_JSON_ENV}=0 to disable this check", + unknown.join(", ") + ))); + } + Ok(result) +} + #[cfg(test)] mod tests { use super::*; + use serde::Deserialize; + use serde_json::json; use serial_test::serial; fn with_strict_mode(enabled: bool, f: F) @@ -131,4 +160,54 @@ mod tests { assert!(result.unwrap_err().contains("duplicate key '/'")); }); } + + #[derive(Debug, Deserialize, PartialEq)] + struct RpcTestReq { + name: String, + value: i32, + } + + #[test] + #[serial] + fn test_unknown_fields_known_only() { + with_strict_mode(true, || { + let val = json!({"name": "alice", "value": 42}); + let result = from_value_rejecting_unknown_fields::(val); + assert_eq!( + result.unwrap(), + RpcTestReq { + name: "alice".into(), + value: 42 + } + ); + }); + } + + #[test] + #[serial] + fn test_unknown_fields_detected() { + with_strict_mode(true, || { + let val = json!({"name": "alice", "value": 42, "extra": true}); + let err = from_value_rejecting_unknown_fields::(val) + .expect_err("expected Err when unknown JSON field is present under strict mode"); + let msg = err.to_string(); + assert!( + msg.contains("unknown field(s)") && msg.contains("extra"), + "got: {msg}" + ); + }); + } + + #[test] + #[serial] + fn test_unknown_fields_strict_mode_off() { + with_strict_mode(false, || { + let val = json!({"name": "alice", "value": 42, "extra": true}); + let result = from_value_rejecting_unknown_fields::(val); + assert!( + result.is_ok(), + "unknown fields should be allowed when strict mode is off" + ); + }); + } } diff --git a/src/rpc/reflect/parser.rs b/src/rpc/reflect/parser.rs index cd745e93e98d..4c9f2bf22a23 100644 --- a/src/rpc/reflect/parser.rs +++ b/src/rpc/reflect/parser.rs @@ -7,7 +7,7 @@ use serde::Deserialize; use serde_json::{Value, json}; use super::{jsonrpc_types::RequestParameters, util::Optional as _}; -use crate::rpc::error::ServerError; +use crate::rpc::{error::ServerError, json_validator}; /// Parser for JSON-RPC parameters. /// Abstracts calling convention, checks for unexpected params etc, so that @@ -142,7 +142,7 @@ impl<'a> Parser<'a> { false => self.error(missing_parameter)?, }, Some(ParserInner::ByName(it)) => match it.remove(name) { - Some(it) => match serde_json::from_value::(it) { + Some(it) => match json_validator::from_value_rejecting_unknown_fields::(it) { Ok(it) => it, Err(e) => self.error(deserialize_error(e))?, }, @@ -152,7 +152,7 @@ impl<'a> Parser<'a> { }, }, Some(ParserInner::ByPosition(it)) => match it.pop_front() { - Some(it) => match serde_json::from_value::(it) { + Some(it) => match json_validator::from_value_rejecting_unknown_fields::(it) { Ok(it) => it, Err(e) => self.error(deserialize_error(e))?, }, From 284a827a7d11f755c5fc07a5033213ae0ed7fdaa Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 16 Apr 2026 14:17:43 +0530 Subject: [PATCH 02/10] deny unknown fields resp --- docs/docs/users/reference/env_variables.md | 2 +- src/rpc/json_validator.rs | 7 ++++--- src/rpc/reflect/mod.rs | 9 ++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/docs/users/reference/env_variables.md b/docs/docs/users/reference/env_variables.md index 926f33d4c38a..a1121d49f15c 100644 --- a/docs/docs/users/reference/env_variables.md +++ b/docs/docs/users/reference/env_variables.md @@ -57,7 +57,7 @@ process. | `FOREST_JWT_DISABLE_EXP_VALIDATION` | 1 or true | empty | 1 | Whether or not to disable JWT expiration validation | | `FOREST_ETH_BLOCK_CACHE_SIZE` | positive integer | 500 | 1 | The size of Eth block cache | | `FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK` | 1 or true | false | 1 | Whether or not to backfill full tipsets from the p2p network | -| `FOREST_STRICT_JSON` | 1 or true | false | 1 | Enable strict JSON validation to detect duplicate keys and reject unknown fields in RPC requests | +| `FOREST_STRICT_JSON` | 1 or true | false | 1 | Enable strict JSON validation to detect duplicate keys and reject unknown fields in RPC requests and responses | | `FOREST_AUTO_DOWNLOAD_SNAPSHOT_PATH` | URL or file path | empty | `/var/tmp/forest_snapshot_calibnet.forest.car.zst` | Override snapshot path for `--auto-download-snapshot` | | `FOREST_DOWNLOAD_CONNECTIONS` | positive integer | 5 | 10 | Number of parallel HTTP connections for downloading snapshots | | `FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION` | 1 or true | empty | 1 | Whether or not to disable F3 finality resolution in Eth `v1` RPC methods | diff --git a/src/rpc/json_validator.rs b/src/rpc/json_validator.rs index 14383290db66..c2d182f8fb2a 100644 --- a/src/rpc/json_validator.rs +++ b/src/rpc/json_validator.rs @@ -1,15 +1,16 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -//! JSON validation utilities for RPC request processing. +//! JSON validation utilities for RPC requests and responses processing. //! //! - **Duplicate key detection**: `serde_json` automatically deduplicates keys at parse time //! using a "last-write-wins" strategy. This means JSON like `{"/":"cid1", "/":"cid2"}` will //! keep only the last value, which can lead to unexpected behavior in RPC calls. //! - **Unknown field detection**: `serde_json` silently ignores unknown fields by default. -//! In strict mode, [`from_value_rejecting_unknown_fields`] rejects them in RPC calls. +//! In strict mode, [`from_value_rejecting_unknown_fields`] applies to rpc request and +//! responses. //! -//! Both checks are gated behind the `FOREST_STRICT_JSON` environment variable. +//! All of this is gated behind the `FOREST_STRICT_JSON` environment variable. use ahash::HashSet; use serde::de::DeserializeOwned; diff --git a/src/rpc/reflect/mod.rs b/src/rpc/reflect/mod.rs index 2aab1b6bdf5a..f4e75b1e971c 100644 --- a/src/rpc/reflect/mod.rs +++ b/src/rpc/reflect/mod.rs @@ -275,7 +275,14 @@ pub trait RpcMethodExt: RpcMethod { let params = Self::parse_params(params.as_str(), calling_convention) .map_err(|e| Error::invalid_params(e, None))?; let ok = Self::handle(ctx, params, &extensions).await?; - Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(ok.into_lotus_json()) + let result = ok.into_lotus_json(); + if crate::rpc::json_validator::is_strict_mode() { + let v = serde_json::to_value(&result).map_err(Error::from)?; + let _: ::LotusJson = + crate::rpc::json_validator::from_value_rejecting_unknown_fields(v) + .map_err(Error::from)?; + } + Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(result) }, )?; if let Some(alias) = Self::NAME_ALIAS { From b48816d8a6da5142062d3c630a494ed5af1c4f69 Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 16 Apr 2026 14:43:03 +0530 Subject: [PATCH 03/10] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 145ac01a96a2..3cdda462f2a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ ### Added +- [#6926](https://github.com/ChainSafe/forest/pull/6926): Added strict JSON validation to deny unknown fields in RPC request parameters and response results when `FOREST_STRICT_JSON` is enabled. + ### Changed ### Removed From 1397e48127fe085ea114afbae579f1865041b58f Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 16 Apr 2026 15:03:52 +0530 Subject: [PATCH 04/10] fix spellcheck --- src/rpc/json_validator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/json_validator.rs b/src/rpc/json_validator.rs index c2d182f8fb2a..7f23f70bcc90 100644 --- a/src/rpc/json_validator.rs +++ b/src/rpc/json_validator.rs @@ -7,7 +7,7 @@ //! using a "last-write-wins" strategy. This means JSON like `{"/":"cid1", "/":"cid2"}` will //! keep only the last value, which can lead to unexpected behavior in RPC calls. //! - **Unknown field detection**: `serde_json` silently ignores unknown fields by default. -//! In strict mode, [`from_value_rejecting_unknown_fields`] applies to rpc request and +//! In strict mode, [`from_value_rejecting_unknown_fields`] applies to RPC request and //! responses. //! //! All of this is gated behind the `FOREST_STRICT_JSON` environment variable. From c90dc856c78827b410d15993c9d8d4d149ce3bac Mon Sep 17 00:00:00 2001 From: Shashank Date: Mon, 20 Apr 2026 13:17:46 +0530 Subject: [PATCH 05/10] fix in api compare --- scripts/tests/api_compare/docker-compose.yml | 3 ++ src/rpc/reflect/mod.rs | 8 +---- .../subcommands/api_cmd/api_compare_tests.rs | 32 ++++++++++++------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/scripts/tests/api_compare/docker-compose.yml b/scripts/tests/api_compare/docker-compose.yml index 0d4dc88029c1..38a8df93106c 100644 --- a/scripts/tests/api_compare/docker-compose.yml +++ b/scripts/tests/api_compare/docker-compose.yml @@ -266,6 +266,7 @@ services: - RUST_LOG=info,forest::tool::subcommands=debug - FOREST_RPC_DEFAULT_TIMEOUT=120 - FIL_PROOFS_PARAMETER_CACHE=${FIL_PROOFS_PARAMETER_CACHE} + - FOREST_STRICT_JSON=1 entrypoint: ["/bin/bash", "-c"] user: 0:0 command: @@ -310,6 +311,7 @@ services: - RUST_LOG=info,forest::tool::subcommands=debug - FOREST_RPC_DEFAULT_TIMEOUT=120 - FIL_PROOFS_PARAMETER_CACHE=${FIL_PROOFS_PARAMETER_CACHE} + - FOREST_STRICT_JSON=1 entrypoint: ["/bin/bash", "-c"] user: 0:0 command: @@ -379,6 +381,7 @@ services: - RUST_LOG=info,forest::tool::subcommands=debug - FOREST_RPC_DEFAULT_TIMEOUT=120 - FIL_PROOFS_PARAMETER_CACHE=${FIL_PROOFS_PARAMETER_CACHE} + - FOREST_STRICT_JSON=1 entrypoint: ["/bin/bash", "-c"] user: 0:0 command: diff --git a/src/rpc/reflect/mod.rs b/src/rpc/reflect/mod.rs index f4e75b1e971c..5b7e04d50c9e 100644 --- a/src/rpc/reflect/mod.rs +++ b/src/rpc/reflect/mod.rs @@ -276,12 +276,6 @@ pub trait RpcMethodExt: RpcMethod { .map_err(|e| Error::invalid_params(e, None))?; let ok = Self::handle(ctx, params, &extensions).await?; let result = ok.into_lotus_json(); - if crate::rpc::json_validator::is_strict_mode() { - let v = serde_json::to_value(&result).map_err(Error::from)?; - let _: ::LotusJson = - crate::rpc::json_validator::from_value_rejecting_unknown_fields(v) - .map_err(Error::from)?; - } Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(result) }, )?; @@ -357,7 +351,7 @@ pub trait RpcMethodExt: RpcMethod { // Client::call has an inappropriate HasLotusJson // bound, work around it for now. let json = client.call(Self::request(params)?.map_ty()).await?; - Ok(serde_json::from_value(json)?) + Ok(crate::rpc::json_validator::from_value_rejecting_unknown_fields(json)?) } } fn call( diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index da915ccecd65..6d5bad680bcc 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -315,11 +315,13 @@ impl RpcTest { fn basic_raw(request: rpc::Request) -> Self { Self { request: request.map_ty(), - check_syntax: Box::new(|it| match serde_json::from_value::(it) { - Ok(_) => true, - Err(e) => { - debug!(?e); - false + check_syntax: Box::new(|it| { + match crate::rpc::json_validator::from_value_rejecting_unknown_fields::(it) { + Ok(_) => true, + Err(e) => { + debug!(?e); + false + } } }), check_semantics: Box::new(|_, _| true), @@ -345,17 +347,23 @@ impl RpcTest { ) -> Self { Self { request: request.map_ty(), - check_syntax: Box::new(|value| match serde_json::from_value::(value) { - Ok(_) => true, - Err(e) => { - debug!("{e}"); - false + check_syntax: Box::new(|value| { + match crate::rpc::json_validator::from_value_rejecting_unknown_fields::(value) { + Ok(_) => true, + Err(e) => { + debug!("{e}"); + false + } } }), check_semantics: Box::new(move |forest_json, lotus_json| { match ( - serde_json::from_value::(forest_json), - serde_json::from_value::(lotus_json), + crate::rpc::json_validator::from_value_rejecting_unknown_fields::( + forest_json, + ), + crate::rpc::json_validator::from_value_rejecting_unknown_fields::( + lotus_json, + ), ) { (Ok(forest), Ok(lotus)) => validate(forest, lotus), (forest, lotus) => { From 928ac735993bc759764e234ebbe4b26e259a69f7 Mon Sep 17 00:00:00 2001 From: Shashank Date: Mon, 20 Apr 2026 13:52:50 +0530 Subject: [PATCH 06/10] fmt --- src/rpc/reflect/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rpc/reflect/mod.rs b/src/rpc/reflect/mod.rs index 5b7e04d50c9e..b847e083111f 100644 --- a/src/rpc/reflect/mod.rs +++ b/src/rpc/reflect/mod.rs @@ -275,8 +275,7 @@ pub trait RpcMethodExt: RpcMethod { let params = Self::parse_params(params.as_str(), calling_convention) .map_err(|e| Error::invalid_params(e, None))?; let ok = Self::handle(ctx, params, &extensions).await?; - let result = ok.into_lotus_json(); - Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(result) + Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(ok.into_lotus_json()) }, )?; if let Some(alias) = Self::NAME_ALIAS { From 54bd0b57166a2ba07ad498748fafb1abec3cf111 Mon Sep 17 00:00:00 2001 From: Shashank Date: Mon, 20 Apr 2026 15:25:59 +0530 Subject: [PATCH 07/10] disable CI strict json check --- scripts/tests/api_compare/docker-compose.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/tests/api_compare/docker-compose.yml b/scripts/tests/api_compare/docker-compose.yml index 38a8df93106c..0d4dc88029c1 100644 --- a/scripts/tests/api_compare/docker-compose.yml +++ b/scripts/tests/api_compare/docker-compose.yml @@ -266,7 +266,6 @@ services: - RUST_LOG=info,forest::tool::subcommands=debug - FOREST_RPC_DEFAULT_TIMEOUT=120 - FIL_PROOFS_PARAMETER_CACHE=${FIL_PROOFS_PARAMETER_CACHE} - - FOREST_STRICT_JSON=1 entrypoint: ["/bin/bash", "-c"] user: 0:0 command: @@ -311,7 +310,6 @@ services: - RUST_LOG=info,forest::tool::subcommands=debug - FOREST_RPC_DEFAULT_TIMEOUT=120 - FIL_PROOFS_PARAMETER_CACHE=${FIL_PROOFS_PARAMETER_CACHE} - - FOREST_STRICT_JSON=1 entrypoint: ["/bin/bash", "-c"] user: 0:0 command: @@ -381,7 +379,6 @@ services: - RUST_LOG=info,forest::tool::subcommands=debug - FOREST_RPC_DEFAULT_TIMEOUT=120 - FIL_PROOFS_PARAMETER_CACHE=${FIL_PROOFS_PARAMETER_CACHE} - - FOREST_STRICT_JSON=1 entrypoint: ["/bin/bash", "-c"] user: 0:0 command: From b84f84d2ff2ab88bdd0e155d1218a616bc001cab Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 21 Apr 2026 15:08:15 +0530 Subject: [PATCH 08/10] Enable strict json check --- scripts/tests/api_compare/docker-compose.yml | 3 + src/lotus_json/message.rs | 17 ++++- src/lotus_json/signed_message.rs | 5 +- src/rpc/methods/chain.rs | 21 +----- src/rpc/methods/common.rs | 3 + src/rpc/methods/eth.rs | 2 + src/rpc/methods/gas.rs | 7 +- src/rpc/methods/state.rs | 5 +- src/rpc/methods/state/types.rs | 4 ++ .../forest__rpc__tests__rpc__v0.snap | 70 +++++++------------ .../forest__rpc__tests__rpc__v1.snap | 70 +++++++------------ src/state_manager/utils.rs | 2 + .../subcommands/api_cmd/api_compare_tests.rs | 4 +- .../subcommands/api_cmd/stateful_tests.rs | 4 +- src/wallet/subcommands/wallet_cmd.rs | 3 +- 15 files changed, 95 insertions(+), 125 deletions(-) diff --git a/scripts/tests/api_compare/docker-compose.yml b/scripts/tests/api_compare/docker-compose.yml index 0d4dc88029c1..38a8df93106c 100644 --- a/scripts/tests/api_compare/docker-compose.yml +++ b/scripts/tests/api_compare/docker-compose.yml @@ -266,6 +266,7 @@ services: - RUST_LOG=info,forest::tool::subcommands=debug - FOREST_RPC_DEFAULT_TIMEOUT=120 - FIL_PROOFS_PARAMETER_CACHE=${FIL_PROOFS_PARAMETER_CACHE} + - FOREST_STRICT_JSON=1 entrypoint: ["/bin/bash", "-c"] user: 0:0 command: @@ -310,6 +311,7 @@ services: - RUST_LOG=info,forest::tool::subcommands=debug - FOREST_RPC_DEFAULT_TIMEOUT=120 - FIL_PROOFS_PARAMETER_CACHE=${FIL_PROOFS_PARAMETER_CACHE} + - FOREST_STRICT_JSON=1 entrypoint: ["/bin/bash", "-c"] user: 0:0 command: @@ -379,6 +381,7 @@ services: - RUST_LOG=info,forest::tool::subcommands=debug - FOREST_RPC_DEFAULT_TIMEOUT=120 - FIL_PROOFS_PARAMETER_CACHE=${FIL_PROOFS_PARAMETER_CACHE} + - FOREST_STRICT_JSON=1 entrypoint: ["/bin/bash", "-c"] user: 0:0 command: diff --git a/src/lotus_json/message.rs b/src/lotus_json/message.rs index 306803b95821..86bd487eeb49 100644 --- a/src/lotus_json/message.rs +++ b/src/lotus_json/message.rs @@ -5,6 +5,7 @@ use super::*; use crate::shim::{address::Address, econ::TokenAmount, message::Message}; use fvm_ipld_encoding::RawBytes; +use ::cid::Cid; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "PascalCase")] @@ -40,6 +41,14 @@ pub struct MessageLotusJson { default )] params: Option, + #[schemars(with = "LotusJson>")] + #[serde( + with = "crate::lotus_json", + rename = "CID", + default, + skip_serializing_if = "Option::is_none" + )] + cid: Option, } impl HasLotusJson for Message { @@ -47,6 +56,8 @@ impl HasLotusJson for Message { #[cfg(test)] fn snapshots() -> Vec<(serde_json::Value, Self)> { + let msg = Message::default(); + let cid = msg.cid(); vec![( json!({ "From": "f00", @@ -59,12 +70,14 @@ impl HasLotusJson for Message { "To": "f00", "Value": "0", "Version": 0, + "CID": { "/": cid.to_string() }, }), - Message::default(), + msg, )] } fn into_lotus_json(self) -> Self::LotusJson { + let cid = self.cid(); let Self { version, from, @@ -88,6 +101,7 @@ impl HasLotusJson for Message { gas_premium, method: method_num, params: Some(params), + cid: Some(cid), } } @@ -103,6 +117,7 @@ impl HasLotusJson for Message { gas_premium, method, params, + cid: _, } = lotus_json; Self { version, diff --git a/src/lotus_json/signed_message.rs b/src/lotus_json/signed_message.rs index 08d029c22693..4ed7e46693e9 100644 --- a/src/lotus_json/signed_message.rs +++ b/src/lotus_json/signed_message.rs @@ -39,6 +39,8 @@ impl HasLotusJson for SignedMessage { #[cfg(test)] fn snapshots() -> Vec<(serde_json::Value, Self)> { + let msg = Message::default(); + let msg_cid = msg.cid(); vec![( json!({ "Message": { @@ -52,6 +54,7 @@ impl HasLotusJson for SignedMessage { "To": "f00", "Value": "0", "Version": 0, + "CID": { "/": msg_cid.to_string() }, }, "Signature": {"Type": 2, "Data": "aGVsbG8gd29ybGQh"}, "CID": { @@ -59,7 +62,7 @@ impl HasLotusJson for SignedMessage { }, }), SignedMessage { - message: Message::default(), + message: msg, signature: Signature { sig_type: crate::shim::crypto::SignatureType::Bls, bytes: Vec::from_iter(*b"hello world!"), diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 1fe0979ab57c..ab7190d56421 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -182,7 +182,7 @@ impl RpcMethod<1> for ChainGetMessage { const DESCRIPTION: Option<&'static str> = Some("Returns the message with the specified CID."); type Params = (Cid,); - type Ok = FlattenedApiMessage; + type Ok = Message; async fn handle( ctx: Ctx, @@ -193,13 +193,10 @@ impl RpcMethod<1> for ChainGetMessage { .store() .get_cbor(&message_cid)? .with_context(|| format!("can't find message with cid {message_cid}"))?; - let message = match chain_message { + Ok(match chain_message { ChainMessage::Signed(m) => Arc::unwrap_or_clone(m).into_message(), ChainMessage::Unsigned(m) => Arc::unwrap_or_clone(m), - }; - - let cid = message.cid(); - Ok(FlattenedApiMessage { message, cid }) + }) } } @@ -1506,18 +1503,6 @@ pub struct ApiMessage { lotus_json_with_self!(ApiMessage); -#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)] -pub struct FlattenedApiMessage { - #[serde(flatten, with = "crate::lotus_json")] - #[schemars(with = "LotusJson")] - pub message: Message, - #[serde(rename = "CID", with = "crate::lotus_json")] - #[schemars(with = "LotusJson")] - pub cid: Cid, -} - -lotus_json_with_self!(FlattenedApiMessage); - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct ForestChainExportParams { pub version: FilecoinSnapshotVersion, diff --git a/src/rpc/methods/common.rs b/src/rpc/methods/common.rs index 75653a7fca99..0a78661d4bf4 100644 --- a/src/rpc/methods/common.rs +++ b/src/rpc/methods/common.rs @@ -55,6 +55,7 @@ impl RpcMethod<0> for Version { // For the API v0, we don't support it but it should be `1.5.0`. api_version: ShiftingVersion::new(2, 3, 0), block_delay: ctx.chain_config().block_delay_secs, + agent: None, }) } } @@ -106,6 +107,8 @@ pub struct PublicVersion { #[serde(rename = "APIVersion")] pub api_version: ShiftingVersion, pub block_delay: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent: Option, } lotus_json_with_self!(PublicVersion); diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 3966c8e5b7b7..8275b7f5887a 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -4492,6 +4492,8 @@ mod test { invoked_actor: None, gas_charges: vec![], subcalls: vec![], + logs: vec![], + ipld_ops: vec![], } } diff --git a/src/rpc/methods/gas.rs b/src/rpc/methods/gas.rs index 167269dd92b7..3b2e9f252ec9 100644 --- a/src/rpc/methods/gas.rs +++ b/src/rpc/methods/gas.rs @@ -5,7 +5,6 @@ use super::state::InvocResult; use crate::blocks::Tipset; use crate::chain::{BASE_FEE_MAX_CHANGE_DENOM, BLOCK_GAS_TARGET}; use crate::message::{ChainMessage, MessageRead as _, MessageReadWrite as _, SignedMessage}; -use crate::rpc::chain::FlattenedApiMessage; use crate::rpc::{ApiPaths, Ctx, Permission, RpcMethod, error::ServerError, types::*}; use crate::shim::executor::ApplyRet; use crate::shim::{ @@ -305,16 +304,14 @@ impl RpcMethod<3> for GasEstimateMessageGas { Some("Returns the estimated gas for the given parameters."); type Params = (Message, Option, ApiTipsetKey); - type Ok = FlattenedApiMessage; + type Ok = Message; async fn handle( ctx: Ctx, (msg, spec, tsk): Self::Params, _: &http::Extensions, ) -> Result { - let message = estimate_message_gas(&ctx, msg, spec, tsk).await?; - let cid = message.cid(); - Ok(FlattenedApiMessage { message, cid }) + estimate_message_gas(&ctx, msg, spec, tsk).await } } diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 55feef3a7862..09149fe0553d 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -3172,6 +3172,7 @@ impl RpcMethod<0> for StateGetNetworkParams { fork_upgrade_params: ForkUpgradeParams::try_from(config) .context("Failed to get fork upgrade params")?, eip155_chain_id: config.eth_chain_id, + genesis_timestamp: ctx.chain_store().genesis_block_header().timestamp, }; Ok(params) @@ -3190,6 +3191,7 @@ pub struct NetworkParams { fork_upgrade_params: ForkUpgradeParams, #[serde(rename = "Eip155ChainID")] eip155_chain_id: EthChainId, + genesis_timestamp: u64, } lotus_json_with_self!(NetworkParams); @@ -3229,7 +3231,7 @@ pub struct ForkUpgradeParams { upgrade_teep_height: ChainEpoch, upgrade_tock_height: ChainEpoch, upgrade_golden_week_height: ChainEpoch, - //upgrade_xxx_height: ChainEpoch, + upgrade_xx_height: ChainEpoch, } impl TryFrom<&ChainConfig> for ForkUpgradeParams { @@ -3279,6 +3281,7 @@ impl TryFrom<&ChainConfig> for ForkUpgradeParams { upgrade_tock_height: get_height(Tock)?, upgrade_golden_week_height: get_height(GoldenWeek)?, //upgrade_firehorse_height: get_height(FireHorse)?, + upgrade_xx_height: 999_999_999_999_999, }) } } diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index 4edd6a6e3575..08ca4b8e0cb5 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -149,6 +149,10 @@ pub struct ExecutionTrace { #[serde(with = "crate::lotus_json")] #[schemars(with = "LotusJson>")] pub subcalls: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub logs: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ipld_ops: Vec, } impl ExecutionTrace { diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap index 2545c1694b5a..df96ceaec9ed 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap @@ -147,7 +147,7 @@ methods: name: Filecoin.ChainGetMessage.Result required: true schema: - $ref: "#/components/schemas/FlattenedApiMessage" + $ref: "#/components/schemas/Message" paramStructure: by-position - name: Filecoin.ChainGetMessagesInTipset params: @@ -1846,7 +1846,7 @@ methods: name: Filecoin.GasEstimateMessageGas.Result required: true schema: - $ref: "#/components/schemas/FlattenedApiMessage" + $ref: "#/components/schemas/Message" paramStructure: by-position - name: Filecoin.MarketAddBalance params: @@ -5977,6 +5977,13 @@ components: anyOf: - $ref: "#/components/schemas/ActorTrace" - type: "null" + IpldOps: + type: array + items: true + Logs: + type: array + items: + type: string Msg: $ref: "#/components/schemas/MessageTrace" MsgRct: @@ -6179,50 +6186,6 @@ components: - SupplementalData - Signers - Signature - FlattenedApiMessage: - type: object - properties: - CID: - $ref: "#/components/schemas/Cid" - From: - $ref: "#/components/schemas/Address" - GasFeeCap: - $ref: "#/components/schemas/TokenAmount" - default: "0" - GasLimit: - type: integer - format: uint64 - default: 0 - minimum: 0 - GasPremium: - $ref: "#/components/schemas/TokenAmount" - default: "0" - Method: - type: integer - format: uint64 - default: 0 - minimum: 0 - Nonce: - type: integer - format: uint64 - default: 0 - minimum: 0 - Params: - $ref: "#/components/schemas/Nullable_Base64String" - To: - $ref: "#/components/schemas/Address" - Value: - $ref: "#/components/schemas/TokenAmount" - default: "0" - Version: - type: integer - format: uint64 - default: 0 - minimum: 0 - required: - - To - - From - - CID ForestChainExportDiffParams: type: object properties: @@ -6449,6 +6412,9 @@ components: UpgradeWatermelonHeight: type: integer format: int64 + UpgradeXxHeight: + type: integer + format: int64 required: - UpgradeSmokeHeight - UpgradeBreezeHeight @@ -6482,6 +6448,7 @@ components: - UpgradeTeepHeight - UpgradeTockHeight - UpgradeGoldenWeekHeight + - UpgradeXxHeight GasTrace: type: object properties: @@ -6595,6 +6562,8 @@ components: Message: type: object properties: + CID: + $ref: "#/components/schemas/Nullable_Cid" From: $ref: "#/components/schemas/Address" GasFeeCap: @@ -6952,6 +6921,10 @@ components: minimum: 0 ForkUpgradeParams: $ref: "#/components/schemas/ForkUpgradeParams" + GenesisTimestamp: + type: integer + format: uint64 + minimum: 0 NetworkName: type: string PreCommitChallengeDelay: @@ -6964,6 +6937,7 @@ components: - PreCommitChallengeDelay - ForkUpgradeParams - Eip155ChainID + - GenesisTimestamp NetworkVersion: description: "Specifies the network version\n\n# Examples\n```\n# use forest::doctest_private::NetworkVersion;\nlet v0 = NetworkVersion::V0;\n\n// dereference to convert to FVM4\nassert_eq!(fvm_shared4::version::NetworkVersion::V0, *v0);\n\n// use `.into()` when FVM3 has to be specified.\nassert_eq!(fvm_shared3::version::NetworkVersion::V0, v0.into());\n\n// use `.into()` when FVM2 has to be specified.\nassert_eq!(fvm_shared2::version::NetworkVersion::V0, v0.into());\n```" type: integer @@ -7308,6 +7282,10 @@ components: properties: APIVersion: $ref: "#/components/schemas/ShiftingVersion" + Agent: + type: + - string + - "null" BlockDelay: type: integer format: uint32 diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap index 7f2ae0edf690..6883a7a04e29 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap @@ -143,7 +143,7 @@ methods: name: Filecoin.ChainGetMessage.Result required: true schema: - $ref: "#/components/schemas/FlattenedApiMessage" + $ref: "#/components/schemas/Message" paramStructure: by-position - name: Filecoin.ChainGetMessagesInTipset params: @@ -1926,7 +1926,7 @@ methods: name: Filecoin.GasEstimateMessageGas.Result required: true schema: - $ref: "#/components/schemas/FlattenedApiMessage" + $ref: "#/components/schemas/Message" paramStructure: by-position - name: Filecoin.MarketAddBalance params: @@ -6096,6 +6096,13 @@ components: anyOf: - $ref: "#/components/schemas/ActorTrace" - type: "null" + IpldOps: + type: array + items: true + Logs: + type: array + items: + type: string Msg: $ref: "#/components/schemas/MessageTrace" MsgRct: @@ -6298,50 +6305,6 @@ components: - SupplementalData - Signers - Signature - FlattenedApiMessage: - type: object - properties: - CID: - $ref: "#/components/schemas/Cid" - From: - $ref: "#/components/schemas/Address" - GasFeeCap: - $ref: "#/components/schemas/TokenAmount" - default: "0" - GasLimit: - type: integer - format: uint64 - default: 0 - minimum: 0 - GasPremium: - $ref: "#/components/schemas/TokenAmount" - default: "0" - Method: - type: integer - format: uint64 - default: 0 - minimum: 0 - Nonce: - type: integer - format: uint64 - default: 0 - minimum: 0 - Params: - $ref: "#/components/schemas/Nullable_Base64String" - To: - $ref: "#/components/schemas/Address" - Value: - $ref: "#/components/schemas/TokenAmount" - default: "0" - Version: - type: integer - format: uint64 - default: 0 - minimum: 0 - required: - - To - - From - - CID ForestChainExportDiffParams: type: object properties: @@ -6568,6 +6531,9 @@ components: UpgradeWatermelonHeight: type: integer format: int64 + UpgradeXxHeight: + type: integer + format: int64 required: - UpgradeSmokeHeight - UpgradeBreezeHeight @@ -6601,6 +6567,7 @@ components: - UpgradeTeepHeight - UpgradeTockHeight - UpgradeGoldenWeekHeight + - UpgradeXxHeight GasTrace: type: object properties: @@ -6811,6 +6778,8 @@ components: Message: type: object properties: + CID: + $ref: "#/components/schemas/Nullable_Cid" From: $ref: "#/components/schemas/Address" GasFeeCap: @@ -7168,6 +7137,10 @@ components: minimum: 0 ForkUpgradeParams: $ref: "#/components/schemas/ForkUpgradeParams" + GenesisTimestamp: + type: integer + format: uint64 + minimum: 0 NetworkName: type: string PreCommitChallengeDelay: @@ -7180,6 +7153,7 @@ components: - PreCommitChallengeDelay - ForkUpgradeParams - Eip155ChainID + - GenesisTimestamp NetworkVersion: description: "Specifies the network version\n\n# Examples\n```\n# use forest::doctest_private::NetworkVersion;\nlet v0 = NetworkVersion::V0;\n\n// dereference to convert to FVM4\nassert_eq!(fvm_shared4::version::NetworkVersion::V0, *v0);\n\n// use `.into()` when FVM3 has to be specified.\nassert_eq!(fvm_shared3::version::NetworkVersion::V0, v0.into());\n\n// use `.into()` when FVM2 has to be specified.\nassert_eq!(fvm_shared2::version::NetworkVersion::V0, v0.into());\n```" type: integer @@ -7544,6 +7518,10 @@ components: properties: APIVersion: $ref: "#/components/schemas/ShiftingVersion" + Agent: + type: + - string + - "null" BlockDelay: type: integer format: uint32 diff --git a/src/state_manager/utils.rs b/src/state_manager/utils.rs index b8fbda9f7de0..53e3fcd865eb 100644 --- a/src/state_manager/utils.rs +++ b/src/state_manager/utils.rs @@ -669,6 +669,8 @@ pub mod structured { gas_charges, subcalls, invoked_actor: actor_trace, + logs: vec![], + ipld_ops: vec![], }); } } diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 6d5bad680bcc..1319fafd73ab 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -2375,9 +2375,7 @@ fn gas_tests_with_tipset(shared_tipset: &Tipset) -> Vec { shared_tipset.key().into(), )) .unwrap(), - |forest_api_msg, lotus_api_msg| { - let forest_msg = forest_api_msg.message; - let lotus_msg = lotus_api_msg.message; + |forest_msg, lotus_msg| { // Validate that the gas limit is identical (must be deterministic) if forest_msg.gas_limit != lotus_msg.gas_limit { return false; diff --git a/src/tool/subcommands/api_cmd/stateful_tests.rs b/src/tool/subcommands/api_cmd/stateful_tests.rs index 53658b54717f..7feed17c22e2 100644 --- a/src/tool/subcommands/api_cmd/stateful_tests.rs +++ b/src/tool/subcommands/api_cmd/stateful_tests.rs @@ -344,14 +344,14 @@ async fn invoke_contract(client: &rpc::Client, tx: &TestTransaction) -> anyhow:: let eth_tx_args = crate::eth::EthEip1559TxArgsBuilder::default() .chain_id(ETH_CHAIN_ID) - .unsigned_message(&unsigned_msg.message)? + .unsigned_message(&unsigned_msg)? .build() .map_err(|e| anyhow::anyhow!("Failed to build EIP-1559 transaction: {}", e))?; let eth_tx = crate::eth::EthTx::from(eth_tx_args); let data = eth_tx.rlp_unsigned_message(ETH_CHAIN_ID)?; let sig = client.call(WalletSign::request((tx.from, data))?).await?; - let smsg = SignedMessage::new_unchecked(unsigned_msg.message, sig); + let smsg = SignedMessage::new_unchecked(unsigned_msg, sig); let cid = smsg.cid(); client.call(MpoolPush::request((smsg,))?).await?; diff --git a/src/wallet/subcommands/wallet_cmd.rs b/src/wallet/subcommands/wallet_cmd.rs index c5aa7940f07e..f771a3ebcea5 100644 --- a/src/wallet/subcommands/wallet_cmd.rs +++ b/src/wallet/subcommands/wallet_cmd.rs @@ -545,8 +545,7 @@ impl WalletCommands { &backend.remote, (message, spec, ApiTipsetKey(None)), ) - .await? - .message; + .await?; if message.gas_premium > message.gas_fee_cap { anyhow::bail!("After estimation, gas premium is greater than gas fee cap") From c825b00d37c83888f364b5ab3562254527b5af2d Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 21 Apr 2026 18:08:50 +0530 Subject: [PATCH 09/10] fix --- src/rpc/methods/state/types.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index 08ca4b8e0cb5..70bf5c219ab9 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -139,7 +139,7 @@ impl MessageGasCost { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "PascalCase")] pub struct ExecutionTrace { pub msg: MessageTrace, @@ -155,6 +155,17 @@ pub struct ExecutionTrace { pub ipld_ops: Vec, } +impl PartialEq for ExecutionTrace { + /// Ignore [`Self::logs`] and [`Self::ipld_ops`] as they are implementation-dependent + fn eq(&self, other: &Self) -> bool { + self.msg == other.msg + && self.msg_rct == other.msg_rct + && self.invoked_actor == other.invoked_actor + && self.gas_charges == other.gas_charges + && self.subcalls == other.subcalls + } +} + impl ExecutionTrace { pub fn sum_gas(&self) -> GasTrace { let mut out: GasTrace = GasTrace::default(); From f6ceac28881b2b64b908e7c1a62cc9d3e950841b Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 22 Apr 2026 15:29:59 +0530 Subject: [PATCH 10/10] use TraceIpld type --- src/rpc/methods/state/types.rs | 50 ++++++++++++++++++- .../forest__rpc__tests__rpc__v0.snap | 25 +++++++++- .../forest__rpc__tests__rpc__v1.snap | 25 +++++++++- 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index 70bf5c219ab9..8c5609ebcaf9 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -17,8 +17,9 @@ use crate::shim::{ use cid::Cid; use fvm_ipld_encoding::RawBytes; use num::Zero as _; -use schemars::JsonSchema; +use schemars::{JsonSchema, Schema, SchemaGenerator}; use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)] #[serde(rename_all = "PascalCase")] @@ -110,6 +111,51 @@ pub struct MessageGasCost { lotus_json_with_self!(MessageGasCost); +/// IPLD operation kind for [`TraceIpld`]. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + SerializeDisplay, + DeserializeFromStr, + strum::Display, + strum::EnumString, +)] +#[strum(serialize_all = "PascalCase")] +pub enum TraceIpldOp { + Get, + Put, + #[strum(to_string = "Unknown", default)] + Unknown(String), +} + +impl JsonSchema for TraceIpldOp { + fn schema_name() -> std::borrow::Cow<'static, str> { + "TraceIpldOp".into() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + schemars::json_schema!({ + "type": "string", + "enum": ["Get", "Put", "Unknown"], + }) + } +} + +/// IPLD operation details attached to an [`ExecutionTrace`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "PascalCase")] +pub struct TraceIpld { + pub op: TraceIpldOp, + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson")] + pub cid: Cid, + pub size: u64, +} + +lotus_json_with_self!(TraceIpld); + impl MessageGasCost { fn is_zero_cost(&self) -> bool { self.base_fee_burn.is_zero() @@ -152,7 +198,7 @@ pub struct ExecutionTrace { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub logs: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub ipld_ops: Vec, + pub ipld_ops: Vec, } impl PartialEq for ExecutionTrace { diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap index df96ceaec9ed..a1272b09aeaf 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap @@ -5979,7 +5979,8 @@ components: - type: "null" IpldOps: type: array - items: true + items: + $ref: "#/components/schemas/TraceIpld" Logs: type: array items: @@ -7643,6 +7644,28 @@ components: anyOf: - $ref: "#/components/schemas/EthCallTraceAction" - $ref: "#/components/schemas/EthCreateTraceAction" + TraceIpld: + description: "IPLD operation details attached to an [`ExecutionTrace`]." + type: object + properties: + Cid: + $ref: "#/components/schemas/Cid" + Op: + $ref: "#/components/schemas/TraceIpldOp" + Size: + type: integer + format: uint64 + minimum: 0 + required: + - Op + - Cid + - Size + TraceIpldOp: + type: string + enum: + - Get + - Put + - Unknown TraceResult: anyOf: - $ref: "#/components/schemas/EthCallTraceResult" diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap index 6883a7a04e29..aec7b5a39135 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap @@ -6098,7 +6098,8 @@ components: - type: "null" IpldOps: type: array - items: true + items: + $ref: "#/components/schemas/TraceIpld" Logs: type: array items: @@ -7879,6 +7880,28 @@ components: anyOf: - $ref: "#/components/schemas/EthCallTraceAction" - $ref: "#/components/schemas/EthCreateTraceAction" + TraceIpld: + description: "IPLD operation details attached to an [`ExecutionTrace`]." + type: object + properties: + Cid: + $ref: "#/components/schemas/Cid" + Op: + $ref: "#/components/schemas/TraceIpldOp" + Size: + type: integer + format: uint64 + minimum: 0 + required: + - Op + - Cid + - Size + TraceIpldOp: + type: string + enum: + - Get + - Put + - Unknown TraceResult: anyOf: - $ref: "#/components/schemas/EthCallTraceResult"