From 9ae05d501d6b6b8319c02c4ffb2053bbfd495b6f Mon Sep 17 00:00:00 2001 From: Michal Kucharczyk <1728078+michalkucharczyk@users.noreply.github.com> Date: Fri, 15 May 2026 18:28:31 +0200 Subject: [PATCH 1/2] sketch --- sketch.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 sketch.rs diff --git a/sketch.rs b/sketch.rs new file mode 100644 index 000000000..e3cd0e11b --- /dev/null +++ b/sketch.rs @@ -0,0 +1,114 @@ +// crates/orchestrator/src/network/node.rs + +impl NetworkNode { + /// Tar this node's database into `out_path` (gzip). Archive layout: + /// `data/` at the top, plus `relay-data/` for cumulus collators + /// (auto-detected from the node's context). Directly consumable by + /// `with_db_snapshot` on a sibling node. + /// + /// The caller is responsible for pausing the node before calling this; + /// snapshotting a running node risks a torn RocksDB state. + /// + /// `kind` controls whether per-node identity (`keystore/`, `network/`) + /// is included — see [`SnapshotKind`]. + pub async fn snapshot_db( + &self, + out_path: impl AsRef, + kind: SnapshotKind, + ) -> Result; +} + +/// Identity-handling policy for [`NetworkNode::snapshot_db`]. +pub enum SnapshotKind { + /// Includes `keystore/` and `network/`. Safe to load back into a + /// single node. If consumed by multiple sibling nodes the shared + /// session keys cause equivocation and the libp2p identity causes + /// peer-id collisions. + Full, + /// Strips `keystore/` and `network/`. Safe to load on any number of + /// sibling nodes; zombienet re-injects per-node keys at startup via + /// `author_insertKey`. + Shareable, +} + +/// Result of [`NetworkNode::snapshot_db`]. +pub struct NodeSnapshot { + /// Absolute path to the produced `.tgz`. + pub path: PathBuf, + /// Hex-encoded SHA-256 of the archive contents. + pub sha256: String, + /// Size of the archive in bytes. + pub size: u64, + /// Name of the node this snapshot was taken from. + pub node_name: String, +} + + +// crates/orchestrator/src/network.rs + +impl Network { + /// Pause every node in the network (SIGSTOP). Issued in parallel. + pub async fn pause(&self) -> Result<(), anyhow::Error>; + + /// Resume every node in the network (SIGCONT). Issued in parallel. + pub async fn resume(&self) -> Result<(), anyhow::Error>; +} + + +// crates/sdk/src/snapshot.rs (new module, re-exported as zombienet_sdk::snapshot) + +/// Assembles a single `bundle.tar.gz` from one or more [`NodeSnapshot`]s +/// plus a JSON `user_data` blob. The bundle is the unit of upload. +/// +/// Layout inside the bundle: +/// ```text +/// .tgz +/// .tgz +/// ... +/// manifest.json // schema: SnapshotManifest +/// ``` +pub struct BundleBuilder { /* … */ } + +impl BundleBuilder { + pub fn new() -> Self; + + /// Add a per-node archive produced by [`NetworkNode::snapshot_db`]. + pub fn add(self, snap: NodeSnapshot) -> Self; + + /// Attach an arbitrary serializable blob. Stored as JSON under + /// `user_data` in the manifest. Test authors put block heights, + /// CIDs, release tags, "number of collators", etc. here. + pub fn user_data(self, data: T) -> Self; + + /// Produce `out_path` (gzipped tarball). Fails if no archives were + /// added. + pub fn build(self, out_path: impl AsRef) -> Result; +} + +/// Result of [`BundleBuilder::build`]. +pub struct Bundle { + pub path: PathBuf, + pub sha256: String, + pub size: u64, +} + +/// Schema of `manifest.json` inside the bundle. Versioned — +/// `schema_version` bumps are breaking changes. +#[derive(serde::Serialize, serde::Deserialize)] +pub struct SnapshotManifest { + pub schema_version: u32, + /// RFC 3339 timestamp at bundle-build time. + pub created_at: String, + pub archives: Vec, + /// Caller-provided payload from [`BundleBuilder::user_data`]. + pub user_data: serde_json::Value, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ArchiveEntry { + /// Basename inside the bundle (e.g. `"relaychain-db.tgz"`). + pub file: String, + pub sha256: String, + pub size: u64, + pub node_name: String, +} From 0a04d26aee7a6668fc375edb3bf141dab504a24a Mon Sep 17 00:00:00 2001 From: Michal Kucharczyk <1728078+michalkucharczyk@users.noreply.github.com> Date: Mon, 25 May 2026 14:31:04 +0200 Subject: [PATCH 2/2] improvements --- sketch.rs | 53 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/sketch.rs b/sketch.rs index e3cd0e11b..e355e1af5 100644 --- a/sketch.rs +++ b/sketch.rs @@ -19,16 +19,17 @@ impl NetworkNode { } /// Identity-handling policy for [`NetworkNode::snapshot_db`]. +#[derive(Default)] pub enum SnapshotKind { - /// Includes `keystore/` and `network/`. Safe to load back into a - /// single node. If consumed by multiple sibling nodes the shared - /// session keys cause equivocation and the libp2p identity causes - /// peer-id collisions. - Full, - /// Strips `keystore/` and `network/`. Safe to load on any number of - /// sibling nodes; zombienet re-injects per-node keys at startup via - /// `author_insertKey`. + /// Strips the node's identity, only DB. Safe to load on any number of sibling + /// nodes. + #[default] Shareable, + /// Includes the node's identity (`keystore/` and `network/`). Use + /// only when the snapshot will be loaded back into a node that plays + /// the same role as the source. Loading a `Full` snapshot on a + /// different node causes equivocation and peer-id collisions. + Full, } /// Result of [`NetworkNode::snapshot_db`]. @@ -67,22 +68,42 @@ impl Network { /// ... /// manifest.json // schema: SnapshotManifest /// ``` -pub struct BundleBuilder { /* … */ } +/// +/// Uses a typestate to make [`build`](BundleBuilder::build) unreachable +/// until at least one archive has been added. Matches the SDK's +/// `NetworkConfigBuilder` convention. +pub struct BundleBuilder { /* … */ } -impl BundleBuilder { +/// Typestate marker — no archives added yet. +pub struct Empty; +/// Typestate marker — at least one archive has been added. +pub struct NonEmpty; + +impl BundleBuilder { pub fn new() -> Self; - /// Add a per-node archive produced by [`NetworkNode::snapshot_db`]. + /// Add the first per-node archive. Transitions the builder to the + /// [`NonEmpty`] state, which is the only state that exposes + /// [`build`](BundleBuilder::build). + pub fn add(self, snap: NodeSnapshot) -> BundleBuilder; +} + +impl BundleBuilder { + /// Add a subsequent per-node archive. pub fn add(self, snap: NodeSnapshot) -> Self; + /// Produce `out_path` (gzipped tarball). Only callable once at least + /// one archive has been added — enforced at compile time via the + /// [`NonEmpty`] typestate. + pub fn build(self, out_path: impl AsRef) -> Result; +} + +impl BundleBuilder { /// Attach an arbitrary serializable blob. Stored as JSON under /// `user_data` in the manifest. Test authors put block heights, - /// CIDs, release tags, "number of collators", etc. here. + /// CIDs, release tags, "number of collators", etc. here. Can be + /// called before or after [`add`](BundleBuilder::add); last call wins. pub fn user_data(self, data: T) -> Self; - - /// Produce `out_path` (gzipped tarball). Fails if no archives were - /// added. - pub fn build(self, out_path: impl AsRef) -> Result; } /// Result of [`BundleBuilder::build`].