diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3374429686..15d9b96108fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ ### Changed +- [#6655](https://github.com/ChainSafe/forest/issues/6655): Updated garbage collector to keep message receipts and events. + - [#6522](https://github.com/ChainSafe/forest/pull/6522): `Filecoin.EthTraceFilter` filter options `from_block` and `to_block` now default to `latest` tag when omitted for v1 and v2 API. ### Removed diff --git a/src/chain/mod.rs b/src/chain/mod.rs index ff9af77272dd..5940ac5e0154 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -33,6 +33,8 @@ use tokio::io::{AsyncWrite, AsyncWriteExt, BufWriter}; #[derive(Debug, Clone, Default)] pub struct ExportOptions { pub skip_checksum: bool, + pub include_receipts: bool, + pub include_events: bool, pub seen: CidHashSet, } @@ -140,9 +142,15 @@ async fn export_to_forest_car( ) -> anyhow::Result>> { let ExportOptions { skip_checksum, + include_receipts, + include_events, seen, } = options.unwrap_or_default(); + if include_events && !include_receipts { + anyhow::bail!("message receipts must be included when events are included"); + } + let stateroot_lookup_limit = tipset.epoch() - lookup_depth; // Wrap writer in optional checksum calculator @@ -161,6 +169,8 @@ async fn export_to_forest_car( stateroot_lookup_limit, ) .with_seen(seen) + .with_message_receipts(include_receipts) + .with_events(include_events) .track_progress(true), ); diff --git a/src/cli/subcommands/snapshot_cmd.rs b/src/cli/subcommands/snapshot_cmd.rs index b361d4fa341a..399aeee62bfe 100644 --- a/src/cli/subcommands/snapshot_cmd.rs +++ b/src/cli/subcommands/snapshot_cmd.rs @@ -143,6 +143,8 @@ impl SnapshotCommands { recent_roots: depth, output_path: temp_path.to_path_buf(), tipset_keys: tipset.key().clone().into(), + include_receipts: false, + include_events: false, skip_checksum, dry_run, }; diff --git a/src/db/gc/snapshot.rs b/src/db/gc/snapshot.rs index 5c0dd60b830e..cb7144636c98 100644 --- a/src/db/gc/snapshot.rs +++ b/src/db/gc/snapshot.rs @@ -256,7 +256,9 @@ where file, Some(ExportOptions { skip_checksum: true, - ..Default::default() + include_receipts: true, + include_events: true, + seen: Default::default(), }), ) .await?; diff --git a/src/ipld/util.rs b/src/ipld/util.rs index 3085d4f34307..0f019929cf93 100644 --- a/src/ipld/util.rs +++ b/src/ipld/util.rs @@ -5,6 +5,7 @@ use crate::blocks::Tipset; use crate::cid_collections::CidHashSet; use crate::ipld::Ipld; use crate::shim::clock::ChainEpoch; +use crate::shim::executor::Receipt; use crate::utils::db::car_stream::CarBlock; use crate::utils::encoding::extract_cids; use crate::utils::multihash::prelude::*; @@ -137,7 +138,9 @@ impl Iterator for DfsIter { enum IterateType { Message(Cid), + MessageReceipts(Cid), StateRoot(Cid), + EventsRoot(Cid), } enum Task { @@ -155,6 +158,8 @@ pin_project! { seen: CidHashSet, stateroot_limit_exclusive: ChainEpoch, fail_on_dead_links: bool, + message_receipts: bool, + events: bool, track_progress: bool, } } @@ -175,6 +180,19 @@ impl ChainStream { self } + /// Enable traversal of message receipt roots during chain export. + pub fn with_message_receipts(mut self, message_receipts: bool) -> Self { + self.message_receipts = message_receipts; + self + } + + /// Enable traversal of events roots during chain export. + /// Requires message receipts to be enabled as well. + pub fn with_events(mut self, events: bool) -> Self { + self.events = events; + self + } + #[allow(dead_code)] pub fn into_seen(self) -> CidHashSet { self.seen @@ -204,6 +222,8 @@ pub fn stream_chain, ITER: Iterator seen: CidHashSet::default(), stateroot_limit_exclusive, fail_on_dead_links: true, + message_receipts: false, + events: false, track_progress: false, } } @@ -276,6 +296,20 @@ impl, ITER: Iterator + Unpin> Stream IterateType::StateRoot(c) => { format!("state root {c}") } + IterateType::MessageReceipts(c) => { + // Forgive message receipts + tracing::trace!( + "[Iterate] missing key: {cid} from message receipts {c} in block {block_cid} at epoch {epoch}" + ); + continue; + } + IterateType::EventsRoot(c) => { + // Forgive events + tracing::trace!( + "[Iterate] missing key: {cid} from events root {c} in block {block_cid} at epoch {epoch}" + ); + continue; + } }; return Poll::Ready(Some(Err(anyhow::anyhow!( "[Iterate] missing key: {cid} from {type_display} in block {block_cid} at epoch {epoch}" @@ -318,6 +352,34 @@ impl, ITER: Iterator + Unpin> Stream .filter_map(ipld_to_cid) .collect(), )); + if *this.message_receipts { + this.dfs.push_back(Iterate( + block.epoch, + *block.cid(), + IterateType::MessageReceipts(block.message_receipts), + DfsIter::from(block.message_receipts) + .filter_map(ipld_to_cid) + .collect(), + )); + } + // ignore failure as receipts are not required by a lite snapshot + if *this.events + && let Ok(receipts) = + Receipt::get_receipts(this.db, block.message_receipts) + { + for receipt in receipts { + if let Some(events_root) = receipt.events_root() { + this.dfs.push_back(Iterate( + block.epoch, + *block.cid(), + IterateType::EventsRoot(events_root), + DfsIter::from(events_root) + .filter_map(ipld_to_cid) + .collect(), + )); + } + } + } } // Visit the block if it's within required depth. And a special case for `0` diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index f7bce2dc35b0..8ff8a8c87904 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -408,7 +408,7 @@ pub enum ForestChainExport {} impl RpcMethod<1> for ForestChainExport { const NAME: &'static str = "Forest.ChainExport"; const PARAM_NAMES: [&'static str; 1] = ["params"]; - const API_PATHS: BitFlags = ApiPaths::all(); + const API_PATHS: BitFlags = ApiPaths::all_with_v2(); const PERMISSION: Permission = Permission::Read; type Params = (ForestChainExportParams,); @@ -425,6 +425,8 @@ impl RpcMethod<1> for ForestChainExport { recent_roots, output_path, tipset_keys: ApiTipsetKey(tsk), + include_receipts, + include_events, skip_checksum, dry_run, } = params; @@ -448,6 +450,8 @@ impl RpcMethod<1> for ForestChainExport { let options = Some(ExportOptions { skip_checksum, + include_receipts, + include_events, seen: Default::default(), }); let writer = if dry_run { @@ -534,7 +538,7 @@ pub enum ForestChainExportStatus {} impl RpcMethod<0> for ForestChainExportStatus { const NAME: &'static str = "Forest.ChainExportStatus"; const PARAM_NAMES: [&'static str; 0] = []; - const API_PATHS: BitFlags = ApiPaths::all(); + const API_PATHS: BitFlags = ApiPaths::all_with_v2(); const PERMISSION: Permission = Permission::Read; type Params = (); @@ -575,7 +579,7 @@ pub enum ForestChainExportCancel {} impl RpcMethod<0> for ForestChainExportCancel { const NAME: &'static str = "Forest.ChainExportCancel"; const PARAM_NAMES: [&'static str; 0] = []; - const API_PATHS: BitFlags = ApiPaths::all(); + const API_PATHS: BitFlags = ApiPaths::all_with_v2(); const PERMISSION: Permission = Permission::Read; type Params = (); @@ -599,7 +603,7 @@ pub enum ForestChainExportDiff {} impl RpcMethod<1> for ForestChainExportDiff { const NAME: &'static str = "Forest.ChainExportDiff"; const PARAM_NAMES: [&'static str; 1] = ["params"]; - const API_PATHS: BitFlags = ApiPaths::all(); + const API_PATHS: BitFlags = ApiPaths::all_with_v2(); const PERMISSION: Permission = Permission::Read; type Params = (ForestChainExportDiffParams,); @@ -682,6 +686,8 @@ impl RpcMethod<1> for ChainExport { recent_roots, output_path, tipset_keys, + include_receipts: false, + include_events: false, skip_checksum, dry_run, },), @@ -1468,6 +1474,10 @@ pub struct ForestChainExportParams { #[schemars(with = "LotusJson")] #[serde(with = "crate::lotus_json")] pub tipset_keys: ApiTipsetKey, + #[serde(default)] + pub include_receipts: bool, + #[serde(default)] + pub include_events: bool, pub skip_checksum: bool, pub dry_run: bool, } diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap index 781e68c0951c..3ee72247f483 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap @@ -6016,6 +6016,12 @@ components: epoch: type: integer format: int64 + include_events: + type: boolean + default: false + include_receipts: + type: boolean + default: false output_path: type: string recent_roots: diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap index 07240e89837d..4357389571c9 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap @@ -6254,6 +6254,12 @@ components: epoch: type: integer format: int64 + include_events: + type: boolean + default: false + include_receipts: + type: boolean + default: false output_path: type: string recent_roots: diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap index ea3cf1df44b5..709a56bd2d68 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap @@ -22,6 +22,46 @@ methods: - $ref: "#/components/schemas/Tipset" - type: "null" paramStructure: by-position + - name: Forest.ChainExport + params: + - name: params + required: true + schema: + $ref: "#/components/schemas/ForestChainExportParams" + result: + name: Forest.ChainExport.Result + required: true + schema: + $ref: "#/components/schemas/ApiExportResult" + paramStructure: by-position + - name: Forest.ChainExportDiff + params: + - name: params + required: true + schema: + $ref: "#/components/schemas/ForestChainExportDiffParams" + result: + name: Forest.ChainExportDiff.Result + required: true + schema: + type: "null" + paramStructure: by-position + - name: Forest.ChainExportStatus + params: [] + result: + name: Forest.ChainExportStatus.Result + required: true + schema: + $ref: "#/components/schemas/ApiExportStatus" + paramStructure: by-position + - name: Forest.ChainExportCancel + params: [] + result: + name: Forest.ChainExportCancel.Result + required: true + schema: + type: boolean + paramStructure: by-position - name: Filecoin.EthAccounts params: [] result: @@ -1520,6 +1560,39 @@ components: - v - r - s + ApiExportResult: + oneOf: + - type: string + enum: + - Cancelled + - type: object + properties: + Done: + type: + - string + - "null" + additionalProperties: false + required: + - Done + ApiExportStatus: + type: object + properties: + cancelled: + type: boolean + exporting: + type: boolean + progress: + type: number + format: double + start_time: + type: + - string + - "null" + format: date-time + required: + - progress + - exporting + - cancelled Base64String: type: - string @@ -2335,8 +2408,65 @@ components: - $ref: "#/components/schemas/EthHash" - $ref: "#/components/schemas/BlockNumber" - $ref: "#/components/schemas/BlockHash" + FilecoinSnapshotVersion: + type: string + enum: + - V1 + - V2 FilterID: $ref: "#/components/schemas/EthHash" + ForestChainExportDiffParams: + type: object + properties: + depth: + type: integer + format: int64 + from: + type: integer + format: int64 + output_path: + type: string + to: + type: integer + format: int64 + required: + - from + - to + - depth + - output_path + ForestChainExportParams: + type: object + properties: + dry_run: + type: boolean + epoch: + type: integer + format: int64 + include_events: + type: boolean + default: false + include_receipts: + type: boolean + default: false + output_path: + type: string + recent_roots: + type: integer + format: int64 + skip_checksum: + type: boolean + tipset_keys: + $ref: "#/components/schemas/Nullable_Array_of_Cid" + version: + $ref: "#/components/schemas/FilecoinSnapshotVersion" + required: + - version + - epoch + - recent_roots + - output_path + - tipset_keys + - skip_checksum + - dry_run NonEmpty_Array_of_BlockHeader: type: array items: @@ -2364,6 +2494,12 @@ components: - "null" items: $ref: "#/components/schemas/BeaconEntry" + Nullable_Array_of_Cid: + type: + - array + - "null" + items: + $ref: "#/components/schemas/Cid" Nullable_Array_of_PoStProof: type: - array diff --git a/src/tool/subcommands/archive_cmd.rs b/src/tool/subcommands/archive_cmd.rs index 41192ada5ac8..0660320d12b4 100644 --- a/src/tool/subcommands/archive_cmd.rs +++ b/src/tool/subcommands/archive_cmd.rs @@ -41,6 +41,7 @@ use crate::ipld::{stream_chain, stream_graph}; use crate::networks::{ChainConfig, NetworkChain, butterflynet, calibnet, mainnet}; use crate::shim::address::CurrentNetwork; use crate::shim::clock::{ChainEpoch, EPOCH_DURATION_SECONDS, EPOCHS_IN_DAY}; +use crate::shim::executor::{Receipt, StampedEvent}; use crate::shim::fvm_shared_latest::address::Network; use crate::shim::machine::GLOBAL_MULTI_ENGINE; use crate::state_manager::{NO_CALLBACK, StateOutput, apply_block_messages}; @@ -308,6 +309,8 @@ pub struct ArchiveInfo { epoch: ChainEpoch, tipsets: ChainEpoch, messages: ChainEpoch, + message_receipts: usize, + events: usize, head: Tipset, snapshot_version: FilecoinSnapshotVersion, index_size_bytes: Option, @@ -321,6 +324,8 @@ impl std::fmt::Display for ArchiveInfo { writeln!(f, "Epoch: {}", self.epoch)?; writeln!(f, "State-roots: {}", self.epoch - self.tipsets + 1)?; writeln!(f, "Messages sets: {}", self.epoch - self.messages + 1)?; + writeln!(f, "Message receipts: {}", self.message_receipts)?; + writeln!(f, "Events: {}", self.events)?; let head_tipset_key_string = self .head .cids() @@ -381,6 +386,8 @@ impl ArchiveInfo { let mut network: String = "unknown".into(); let mut lowest_stateroot_epoch = root_epoch; let mut lowest_message_epoch = root_epoch; + let mut message_receipts_count = 0; + let mut events_count = 0; let iter = if progress { itertools::Either::Left(windowed.progress_count(root_epoch as u64)) @@ -388,6 +395,16 @@ impl ArchiveInfo { itertools::Either::Right(windowed) }; + let mut update_network_name = |block_cid: &Cid| { + if block_cid == &*calibnet::GENESIS_CID { + network = calibnet::NETWORK_COMMON_NAME.into(); + } else if block_cid == &*mainnet::GENESIS_CID { + network = mainnet::NETWORK_COMMON_NAME.into(); + } else if block_cid == &*butterflynet::GENESIS_CID { + network = butterflynet::NETWORK_COMMON_NAME.into(); + } + }; + for (parent, tipset) in iter { if tipset.epoch() >= parent.epoch() && parent.epoch() != root_epoch { bail!("Broken invariant: non-sequential epochs"); @@ -409,15 +426,16 @@ impl ArchiveInfo { lowest_message_epoch = tipset.epoch(); } - let mut update_network_name = |block_cid: &Cid| { - if block_cid == &*calibnet::GENESIS_CID { - network = calibnet::NETWORK_COMMON_NAME.into(); - } else if block_cid == &*mainnet::GENESIS_CID { - network = mainnet::NETWORK_COMMON_NAME.into(); - } else if block_cid == &*butterflynet::GENESIS_CID { - network = butterflynet::NETWORK_COMMON_NAME.into(); + if let Ok(receipts) = Receipt::get_receipts(store, *tipset.parent_message_receipts()) { + message_receipts_count += 1; + for receipt in receipts { + if let Some(events_root) = receipt.events_root() + && let Ok(e) = StampedEvent::get_events(store, &events_root) + { + events_count += e.len(); + } } - }; + } if tipset.epoch() == 0 { let block_cid = tipset.min_ticket_block().cid(); @@ -442,6 +460,8 @@ impl ArchiveInfo { epoch: root_epoch, tipsets: lowest_stateroot_epoch, messages: lowest_message_epoch, + message_receipts: message_receipts_count, + events: events_count, head, snapshot_version, index_size_bytes, @@ -607,6 +627,8 @@ pub async fn do_export( writer, Some(ExportOptions { skip_checksum: true, + include_receipts: false, + include_events: false, seen, }), )