From da07de5fcff319a14ba4fe10a6b85325d5902192 Mon Sep 17 00:00:00 2001 From: idky137 Date: Tue, 10 Feb 2026 13:00:33 +0000 Subject: [PATCH 1/6] updated BlockIndex serde --- .../src/chain_index/types/db/legacy.rs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/zaino-state/src/chain_index/types/db/legacy.rs b/zaino-state/src/chain_index/types/db/legacy.rs index 64dedbc15..4ac81d44b 100644 --- a/zaino-state/src/chain_index/types/db/legacy.rs +++ b/zaino-state/src/chain_index/types/db/legacy.rs @@ -676,7 +676,7 @@ impl BlockIndex { } impl ZainoVersionedSerde for BlockIndex { - const VERSION: u8 = version::V1; + const VERSION: u8 = version::V2; fn encode_body(&self, w: &mut W) -> io::Result<()> { let mut w = w; @@ -684,12 +684,11 @@ impl ZainoVersionedSerde for BlockIndex { self.hash.serialize(&mut w)?; self.parent_hash.serialize(&mut w)?; self.chainwork.serialize(&mut w)?; - - write_option(&mut w, &Some(self.height), |w, h| h.serialize(w)) + self.height.serialize(&mut *w) } fn decode_latest(r: &mut R) -> io::Result { - Self::decode_v1(r) + Self::decode_v2(r) } fn decode_v1(r: &mut R) -> io::Result { @@ -697,14 +696,19 @@ impl ZainoVersionedSerde for BlockIndex { let hash = BlockHash::deserialize(&mut r)?; let parent_hash = BlockHash::deserialize(&mut r)?; let chainwork = ChainWork::deserialize(&mut r)?; - let height = read_option(&mut r, |r| Height::deserialize(r))?; + let height = + read_option(&mut r, |r| Height::deserialize(r))?.expect("blocks always have height"); - Ok(BlockIndex::new( - hash, - parent_hash, - chainwork, - height.expect("blocks always have height"), - )) + Ok(BlockIndex::new(hash, parent_hash, chainwork, height)) + } + + fn decode_v2(r: &mut R) -> io::Result { + let mut r = r; + let hash = BlockHash::deserialize(&mut r)?; + let parent_hash = BlockHash::deserialize(&mut r)?; + let chainwork = ChainWork::deserialize(&mut r)?; + let height = Height::deserialize(&mut r)?; + Ok(BlockIndex::new(hash, parent_hash, chainwork, height)) } } From c88ac37f55e9cecbedb3239e0970b3df7dd285b2 Mon Sep 17 00:00:00 2001 From: idky137 Date: Tue, 10 Feb 2026 13:56:34 +0000 Subject: [PATCH 2/6] updated schema, migration and db changelog --- .../chain_index/finalised_state/CHANGELOG.md | 14 ++++++++--- .../{db_schema_v1_0.txt => db_schema_v1.txt} | 25 +++++++++++++------ .../src/chain_index/finalised_state/db/v1.rs | 15 ++++++----- .../chain_index/finalised_state/migrations.rs | 16 +++++++++--- 4 files changed, 48 insertions(+), 22 deletions(-) rename zaino-state/src/chain_index/finalised_state/db/{db_schema_v1_0.txt => db_schema_v1.txt} (86%) diff --git a/zaino-state/src/chain_index/finalised_state/CHANGELOG.md b/zaino-state/src/chain_index/finalised_state/CHANGELOG.md index 0b5659203..f76ea261b 100644 --- a/zaino-state/src/chain_index/finalised_state/CHANGELOG.md +++ b/zaino-state/src/chain_index/finalised_state/CHANGELOG.md @@ -103,19 +103,24 @@ Date: 2026-01-27 -------------------------------------------------------------------------------- Summary -- Minor version bump to reflect updated compact block API contract (streaming + pool filtering semantics). -- No schema or encoding changes; metadata-only migration updates persisted DB version marker. +- BlockIndex v2 introduced; because relevant tables (notably `headers` / `BlockHeaderData`) use + variable-length encodings and `BlockIndex` is nested/versioned, existing tables are updated + in-place: DB values may contain either v1 or v2 `BlockIndex` entries. +- Recorded on-disk schema text was clarified; migration refreshes persisted `DbMetadata.schema_hash` + so the metadata matches the repository's schema contract. +- Updated compact block API contract (streaming + pool filtering semantics). On-disk schema - Layout: - - No changes. + - Updated [`BlockHeaderData`] table by introducing [`BlockIndex::V2`], this table may now hold either V1 or V2 + [`BlockIndex`] structs, with serde handles internally. - Tables: - Added: None. - Removed: None. - Renamed: None. - Encoding: - Keys: No changes. - - Values: No changes. + - Values: Introduced `[BlockIndex::V2]`. - Checksums / validation: No changes. - Invariants: - No changes. @@ -142,6 +147,7 @@ Migration - Backfill: None. - Completion criteria: - DbMetadata.version updated from 1.0.0 to 1.1.0. + - DbMetadata.schema_hash updated to match repository `DB_SCHEMA_V1_HASH`. - DbMetadata.migration_status reset to Empty. - Failure handling: - Idempotent: re-running re-writes the same metadata; no partial state beyond metadata. diff --git a/zaino-state/src/chain_index/finalised_state/db/db_schema_v1_0.txt b/zaino-state/src/chain_index/finalised_state/db/db_schema_v1.txt similarity index 86% rename from zaino-state/src/chain_index/finalised_state/db/db_schema_v1_0.txt rename to zaino-state/src/chain_index/finalised_state/db/db_schema_v1.txt index ca8b539df..0d116db80 100644 --- a/zaino-state/src/chain_index/finalised_state/db/db_schema_v1_0.txt +++ b/zaino-state/src/chain_index/finalised_state/db/db_schema_v1.txt @@ -33,8 +33,11 @@ # Val : 0x01 + BlockHeaderData # BlockHeaderData V1 body = # BlockIndex + BlockData -# BlockIndex = B hash B parent_hash U256 chain_work -# Option height +# +# BlockIndex: +# - BlockIndex V2 body = B hash B parent_hash U256 chain_work H height +# - BlockIndex V1 body = B hash B parent_hash U256 chain_work Option height +# # BlockData = LE(u32) version # LE(i64) unix_time # B merkle_root @@ -42,6 +45,11 @@ # LE(u32) bits # [32] nonce # +# Note: BlockIndex is itself encoded with ZainoVersionedSerde, so the BlockIndex bytes +# begin with a u8 version_tag (0x01 = v1, 0x02 = v2). BlockHeaderData V1's +# body therefore contains a nested BlockIndex whose tag indicates which BlockIndex +# layout follows (v1 = Option, v2 = raw H). +# # 2. txids ― H -> StoredEntryVar # Val : 0x01 + CS count + count × B txid # @@ -91,11 +99,14 @@ # Val : 0x01 + CS len + raw event bytes (multiple entries may share the key) # # 10. metadata ― "metadata" (ASCII) -> StoredEntryFixed (singleton) -# DbMetadata V1 body = -# [32] schema_hash -# LE(i64) created_unix_ts -# LE(u32) pruned_tip -# LE(u32) network (Zcash main = 0, test = 1, regtest = 2) +# Val : 0x01 + DbMetadata V1 body + [32] checksum +# DbMetadata V1 body = +# DbVersion version +# 32-byte schema_hash +# MigrationStatus migration_status +# +# DbVersion = LE(u32) major LE(u32) minor LE(u32) patch +# MigrationStatus = u8 enum (0 = Empty, 1 = InProgress, 2 = Completed, 3 = Failed) # # ─────────────────────────── Environment settings ───────────────────────────── # LMDB page-size: platform default diff --git a/zaino-state/src/chain_index/finalised_state/db/v1.rs b/zaino-state/src/chain_index/finalised_state/db/v1.rs index c951cdfb6..58bec9d49 100644 --- a/zaino-state/src/chain_index/finalised_state/db/v1.rs +++ b/zaino-state/src/chain_index/finalised_state/db/v1.rs @@ -76,14 +76,14 @@ use tracing::{error, info, warn}; /// compile-time. The path is relative to this source file. /// /// 1. Bring the *exact* ASCII description of the on-disk layout into the binary at compile-time. -pub(crate) const DB_SCHEMA_V1_TEXT: &str = include_str!("db_schema_v1_0.txt"); +pub(crate) const DB_SCHEMA_V1_TEXT: &str = include_str!("db_schema_v1.txt"); /* 2. Compute the checksum once, outside the code: $ cd zaino-state/src/chain_index/finalised_state/db - $ b2sum -l 256 db_schema_v1_0.txt - bc135247b46bb46a4a971e4c2707826f8095e662b6919d28872c71b6bd676593 db_schema_v1_0.txt + $ b2sum -l 256 db_schema_v1.txt + => [HASH] db_schema_v1.txt Optional helper if you don’t have `b2sum`: @@ -95,8 +95,7 @@ pub(crate) const DB_SCHEMA_V1_TEXT: &str = include_str!("db_schema_v1_0.txt"); 3. Turn those 64 hex digits into a Rust `[u8; 32]` literal: - echo bc135247b46bb46a4a971e4c2707826f8095e662b6919d28872c71b6bd676593 \ - | sed 's/../0x&, /g' | fold -s -w48 + $ echo [HASH] | sed 's/../0x&, /g' | fold -s -w48 */ @@ -105,14 +104,14 @@ pub(crate) const DB_SCHEMA_V1_TEXT: &str = include_str!("db_schema_v1_0.txt"); /// This value is compared against the schema hash stored in the metadata record to detect schema /// drift without a corresponding version bump. pub(crate) const DB_SCHEMA_V1_HASH: [u8; 32] = [ - 0xbc, 0x13, 0x52, 0x47, 0xb4, 0x6b, 0xb4, 0x6a, 0x4a, 0x97, 0x1e, 0x4c, 0x27, 0x07, 0x82, 0x6f, - 0x80, 0x95, 0xe6, 0x62, 0xb6, 0x91, 0x9d, 0x28, 0x87, 0x2c, 0x71, 0xb6, 0xbd, 0x67, 0x65, 0x93, + 0xbf, 0xa6, 0xcc, 0x70, 0x62, 0xa3, 0x44, 0xe7, 0xbc, 0xa2, 0x34, 0x73, 0x6a, 0xfa, 0x0e, 0x9a, + 0xc2, 0xa5, 0x29, 0x18, 0xf9, 0x07, 0xc3, 0x4d, 0x52, 0x7f, 0xb9, 0x2d, 0x27, 0x28, 0x0c, 0xc7, ]; /// *Current* database V1 version. pub(crate) const DB_VERSION_V1: DbVersion = DbVersion { major: 1, - minor: 0, + minor: 1, patch: 0, }; diff --git a/zaino-state/src/chain_index/finalised_state/migrations.rs b/zaino-state/src/chain_index/finalised_state/migrations.rs index d648b5674..dc03171a1 100644 --- a/zaino-state/src/chain_index/finalised_state/migrations.rs +++ b/zaino-state/src/chain_index/finalised_state/migrations.rs @@ -79,6 +79,12 @@ //! This release also introduces [`MigrationStep`], the enum-based migration dispatcher used by //! [`MigrationManager`], to allow selecting between multiple concrete migration implementations. //! +//! Important note: `BlockIndex` now has a V2 wire layout. Because `BlockHeaderData` is stored +//! as a `StoredEntryVar` and `BlockIndex` is itself versioned, the `headers` table can hold +//! either `BlockIndex` v1 or v2 entries without a full table rewrite (in-place update). This +//! migration is metadata-only: it advances the `DbMetadata::version` and refreshes the recorded +//! on-disk schema checksum so persisted metadata matches the repository's updated schema text. +//! //! # Development: adding a new migration step //! //! 1. Introduce a new `struct MigrationX_Y_ZToA_B_C;` and implement `Migration`. @@ -538,7 +544,8 @@ impl Migration for Migration0_0_0To1_0_0 { /// [`MigrationManager`], to allow selecting between multiple concrete migration implementations. /// /// Because the persisted schema contract is unchanged, this migration only updates the stored -/// [`DbMetadata::version`] from `1.0.0` to `1.1.0`. +/// [`DbMetadata`]. Updating [`DbMetadata::version`] from `1.0.0` to `1.1.0` and +/// [`DbMetadata::schema_hash`] to the new on disk schema layout. /// /// Safety and resumability: /// - Idempotent: if run more than once, it will re-write the same metadata. @@ -570,9 +577,12 @@ impl Migration for Migration1_0_0To1_1_0 { let mut metadata: DbMetadata = router.get_metadata().await?; - // Preserve the schema hash because there are no schema changes in v1.1.0. - // Only advance the version marker to reflect the new API contract. + // Advance the version marker to reflect the new API contract (v1.1.0), and refresh the + // persisted schema hash to match the repository's recorded schema contract. + // There are no on-disk layout changes; BlockIndex V2 is supported in-place because the + // headers table stores a variable-length BlockHeaderData which nests a versioned BlockIndex. metadata.version = >::TO_VERSION; + metadata.schema_hash = crate::chain_index::finalised_state::db::v1::DB_SCHEMA_V1_HASH; // Outside of migrations this should be `Empty`. This step performs no build phases, so we // ensure we do not leave a stale in-progress status behind. From 8cc71d70ebd51277167edc134e97d9ebbdc82f53 Mon Sep 17 00:00:00 2001 From: idky137 Date: Tue, 10 Feb 2026 14:42:38 +0000 Subject: [PATCH 3/6] added new tests --- .../src/chain_index/finalised_state.rs | 13 ++- zaino-state/src/chain_index/tests.rs | 1 + .../tests/finalised_state/migrations.rs | 87 ++++++++++++++++++- zaino-state/src/chain_index/tests/types.rs | 35 ++++++++ 4 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 zaino-state/src/chain_index/tests/types.rs diff --git a/zaino-state/src/chain_index/finalised_state.rs b/zaino-state/src/chain_index/finalised_state.rs index c1aa3d297..3a54a7bb1 100644 --- a/zaino-state/src/chain_index/finalised_state.rs +++ b/zaino-state/src/chain_index/finalised_state.rs @@ -186,7 +186,10 @@ use tracing::info; use zebra_chain::parameters::NetworkKind; use crate::{ - chain_index::{source::BlockchainSourceError, types::GENESIS_HEIGHT}, + chain_index::{ + finalised_state::db::v1::DB_VERSION_V1, source::BlockchainSourceError, + types::GENESIS_HEIGHT, + }, config::BlockCacheConfig, error::FinalisedStateError, BlockHash, BlockMetadata, BlockWithMetadata, ChainWork, Height, IndexedBlock, StatusType, @@ -250,7 +253,7 @@ impl ZainoDB { /// /// ## Version selection rules /// - `cfg.db_version == 0` targets `DbVersion { 0, 0, 0 }` (legacy layout). - /// - `cfg.db_version == 1` targets `DbVersion { 1, 0, 0 }` (current layout). + /// - `cfg.db_version == 1` targets the latest v1 DB version (`DB_VERSION_V1`). /// - Any other value returns an error. /// /// ## Migrations @@ -281,11 +284,7 @@ impl ZainoDB { minor: 0, patch: 0, }, - 1 => DbVersion { - major: 1, - minor: 0, - patch: 0, - }, + 1 => DB_VERSION_V1, x => { return Err(FinalisedStateError::Custom(format!( "unsupported database version: DbV{x}" diff --git a/zaino-state/src/chain_index/tests.rs b/zaino-state/src/chain_index/tests.rs index 4c6215888..07a9fae92 100644 --- a/zaino-state/src/chain_index/tests.rs +++ b/zaino-state/src/chain_index/tests.rs @@ -3,6 +3,7 @@ pub(crate) mod finalised_state; pub(crate) mod mempool; mod proptest_blockgen; +pub(crate) mod types; pub(crate) mod vectors; pub(crate) fn init_tracing() { diff --git a/zaino-state/src/chain_index/tests/finalised_state/migrations.rs b/zaino-state/src/chain_index/tests/finalised_state/migrations.rs index 376c96845..903363cc4 100644 --- a/zaino-state/src/chain_index/tests/finalised_state/migrations.rs +++ b/zaino-state/src/chain_index/tests/finalised_state/migrations.rs @@ -5,7 +5,10 @@ use tempfile::TempDir; use zaino_common::network::ActivationHeights; use zaino_common::{DatabaseConfig, Network, StorageConfig}; -use crate::chain_index::finalised_state::capability::DbCore as _; +use crate::chain_index::finalised_state::capability::{ + DbCore as _, DbVersion, DbWrite as _, MigrationStatus, +}; +use crate::chain_index::finalised_state::db::v1::DB_SCHEMA_V1_HASH; use crate::chain_index::finalised_state::db::DbBackend; use crate::chain_index::finalised_state::ZainoDB; use crate::chain_index::tests::init_tracing; @@ -207,3 +210,85 @@ async fn v0_to_v1_partial() { assert_eq!(db_height.0, 200); dbg!(zaino_db_2.shutdown().await.unwrap()); } + +#[tokio::test(flavor = "multi_thread")] +async fn v1_0_to_v1_1_metadata_migration() { + init_tracing(); + + // Prepare test blocks/source (we won't rely on heavy rebuild in this metadata-only migration) + let TestVectorData { blocks, .. } = load_test_vectors().unwrap(); + + let temp_dir: TempDir = tempfile::tempdir().unwrap(); + let db_path: PathBuf = temp_dir.path().to_path_buf(); + + // BlockCacheConfig: use v1 target like other tests + let v1_config = BlockCacheConfig { + storage: StorageConfig { + database: DatabaseConfig { + path: db_path.clone(), + ..Default::default() + }, + ..Default::default() + }, + db_version: 1, + network: Network::Regtest(ActivationHeights::default()), + }; + + let source = build_mockchain_source(blocks.clone()); + + // Build v1 database. + let zaino_db = ZainoDB::spawn(v1_config.clone(), source.clone()) + .await + .unwrap(); + crate::chain_index::tests::vectors::sync_db_with_blockdata( + zaino_db.router(), + blocks.clone(), + None, + ) + .await; + + zaino_db.wait_until_ready().await; + dbg!(zaino_db.status()); + dbg!(zaino_db.db_height().await.unwrap()); + + // 2) Coerce the metadata to look like an older 1.0.0 DB with a stale schema hash and an + // in-progress migration status so the migration has work to do. + let mut metadata = zaino_db.get_metadata().await.unwrap(); + metadata.version = DbVersion { + major: 1, + minor: 0, + patch: 0, + }; + // An obviously different schema hash (any non-matching 32 bytes will do) + metadata.schema_hash = [0u8; 32]; + // Set a non-empty migration status to ensure migration clears it + metadata.migration_status = MigrationStatus::PartialBuidInProgress; + + zaino_db.router().update_metadata(metadata).await.unwrap(); + + // shutdown this backend so ZainoDB::spawn will open the same DB and perform migration + dbg!(zaino_db.shutdown().await.unwrap()); + + // Let the filesystem settle (tests elsewhere do a brief sleep) + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + // 3) Spawn ZainoDB which should detect current_version == 1.0.0 < target 1.1.0 and run the metadata + // migration. We await until ready so migration completes. + let zaino_db = ZainoDB::spawn(v1_config, source).await.unwrap(); + zaino_db.wait_until_ready().await; + + // 4) Read persisted metadata and assert migration effects. + let post_meta = zaino_db.get_metadata().await.unwrap(); + assert_eq!( + post_meta.version, + DbVersion { + major: 1, + minor: 1, + patch: 0 + } + ); + assert_eq!(post_meta.migration_status, MigrationStatus::Empty); + assert_eq!(post_meta.schema_hash, DB_SCHEMA_V1_HASH); + + zaino_db.shutdown().await.unwrap(); +} diff --git a/zaino-state/src/chain_index/tests/types.rs b/zaino-state/src/chain_index/tests/types.rs new file mode 100644 index 000000000..6882263a0 --- /dev/null +++ b/zaino-state/src/chain_index/tests/types.rs @@ -0,0 +1,35 @@ +//! Zaino ChainIndex::types unit tests. + +use crate::{ + chain_index::tests::init_tracing, version, write_option, BlockIndex, ZainoVersionedSerde as _, +}; + +#[tokio::test(flavor = "multi_thread")] +async fn blockindex_v1_v2_serde() { + init_tracing(); + + // Build canonical components + let hash = crate::BlockHash::from([1u8; 32]); + let parent_hash = crate::BlockHash::from([2u8; 32]); + let chainwork = crate::ChainWork::from_u256(0.into()); + let height = crate::Height(42); + + // Construct a v1-encoded BlockIndex bytes (tag 0x01 + body with Option) + let mut v1_bytes: Vec = Vec::new(); + v1_bytes.push(version::V1); // leading tag for BlockIndex v1 + hash.serialize(&mut v1_bytes).unwrap(); + parent_hash.serialize(&mut v1_bytes).unwrap(); + chainwork.serialize(&mut v1_bytes).unwrap(); + // v1 used Option + write_option(&mut v1_bytes, &Some(height), |w, h| h.serialize(w)).unwrap(); + + // Parse v1 bytes using the new BlockIndex deserialiser — should succeed and produce same height. + let parsed_v1 = BlockIndex::from_bytes(&v1_bytes).expect("decode v1 BlockIndex"); + assert_eq!(parsed_v1.height(), height); + + // Now round-trip a v2 BlockIndex (current writer). BlockIndex::to_bytes() writes V2. + let bidx = BlockIndex::new(hash, parent_hash, chainwork, height); + let v2_bytes = bidx.to_bytes().expect("v2 to_bytes"); + let parsed_v2 = BlockIndex::from_bytes(&v2_bytes).expect("decode v2 BlockIndex"); + assert_eq!(parsed_v2, bidx); +} From 34d5d0347944453fdb3e83bc6275724999e974b0 Mon Sep 17 00:00:00 2001 From: idky137 Date: Thu, 12 Feb 2026 16:40:39 +0000 Subject: [PATCH 4/6] updated ZainoVersionedSerialise and StoredEntry verify behaviour --- zaino-state/src/chain_index/encoding.rs | 590 ++++++++++++++++-- .../chain_index/finalised_state/capability.rs | 38 +- .../src/chain_index/finalised_state/entry.rs | 290 ++++++++- .../src/chain_index/types/db/commitment.rs | 36 +- .../src/chain_index/types/db/legacy.rs | 390 ++++++++---- 5 files changed, 1137 insertions(+), 207 deletions(-) diff --git a/zaino-state/src/chain_index/encoding.rs b/zaino-state/src/chain_index/encoding.rs index 621b8947d..a010861d9 100644 --- a/zaino-state/src/chain_index/encoding.rs +++ b/zaino-state/src/chain_index/encoding.rs @@ -1,4 +1,60 @@ -//! Holds traits and primitive functions for Zaino's Serialisation schema. +/* ────────────────────────── Zaino Serialiser Traits ─────────────────────────── */ + +/*! +Provides **backward-compatible** encoding and decoding for versioned structs that implement +`ZainoVersionedSerde`. + +Design goals +- Allow code to **read** and **write** older on-disk versions. +- Enable implementers to supply compact, explicit encoders/decoders for historical versions + without duplicating decode logic in multiple places. +- Provide a small set of *version-agnostic* APIs that consumers can call without caring + about which concrete wire-format was used on-disk. + +Wire-format overview +- Every record begins with a single **version tag byte** followed by a version-specific body. +- `Self::VERSION` is the newest implemented version this build **writes**; on read we dispatch + using the tag. +- Implementations *must* expose encoders/decoders for the versions they support so that + callers can reliably regenerate exact historical bytes (necessary for checksums, + backward-compatible verification and database version migration logic). + +Developer guidance (how to implement a versioned type) +1. When introducing a type, select the initial wire version: + - `const VERSION = version::V1;` for first release. + - Implement `encode_v1` and `decode_v1` (body-only helpers), and make `encode_latest` + / `decode_latest` wrap v1 behaviour. +2. When bumping to a new wire-format (V2, V3, …): + - Introduce `encode_vN` and `decode_vN` for the new layout. + - Update `const VERSION` to the new tag (this build will now write the new tag by default). + - Make `encode_latest` / `decode_latest` delegate to the new vN helpers. + - Preserve `decode_v(M)` helpers for earlier M so older on-disk values remain readable. +3. For types that *contain* inner fields that themselves implement `ZainoVersionedSerde` + (for example `BlockHeaderData` contains `BlockIndex`), **explicitly** control the inner + field’s encoded version when producing historical top-level encodings: + - Use `serialize_with_version` (or `to_bytes_with_version`) on the inner field to request + the exact nested version you need. This guarantees that `to_bytes_with_version(Some(v))` + reproduces exact bytes that historical writers produced (critical for checksum equality). + - Do *not* rely on the inner field’s current `serialize()` when producing historical + top-level encodings — doing so will change nested bytes and break checksum verification. +4. Keep encode helpers narrow and faithful: + - Implement only the `encode_vN` helpers that are required to reproduce historical + bytes; default implementations return an error. This keeps implementations explicit + and easy to review. + +Consumer (version-agnostic) APIs +- `serialize()` / `to_bytes()` — writes the current version tag + body. +- `serialize_with_version(&mut w, version)` / `to_bytes_with_version(version)` — write a + chosen version tag and body (useful when reproducing historical bytes). +- `deserialize()` / `from_bytes()` / `decode_body()` — read and dispatch by version tag. + +Safety note +- `StoredEntry*` wrappers compute and verify checksums over the exact bytes written to disk: + `blake2b256(encoded_key || encoded_item_bytes)`. For verification to succeed against older + on-disk rows, an implementation MUST be able to reproduce the exact historical `encoded_item_bytes` + (including nested fields’ tags/bodies). Use `serialize_with_version` for nested fields to + guarantee this behaviour. +*/ #![allow(dead_code)] use core::iter::FromIterator; @@ -16,7 +72,7 @@ pub mod version { // pub const V3: u8 = 3; } -/* ────────────────────────── Zaino Serialiser Traits ─────────────────────────── */ +/* ────────────────────────── ZainoVersionedSerde ─────────────────────────── */ /// # Zaino wire-format: one-byte version tag /// /// ## Quick summary @@ -25,56 +81,68 @@ pub mod version { /// │ version │ (little-endian by default) │ /// └──────────┴──────────────────────────────────────────────────┘ /// -/// * `Self::VERSION` = the tag **this build *writes***. +/// * `Self::VERSION` is the highest (newest) version implemented in this build. /// * On **read**, we peek at the tag: /// * if it equals `Self::VERSION` call `decode_latest`; /// * otherwise fall back to the relevant `decode_vN` helper /// (defaults to “unsupported” unless overwritten). /// -/// ## Update guide. +/// ## Developer behaviour (implementer guidance) +/// +/// When you implement a new versioned type: +/// - Provide *both* encoders and decoders for concrete versions you need to support: +/// - `encode_vN` and `decode_vN` are the *body-only* helpers for version `N`. +/// - `encode_latest` should emit the body for the current `Self::VERSION`. +/// - `decode_latest` must parse the body for the current `Self::VERSION`. +/// - On a version bump (v1 → v2): +/// 1. Implement `encode_v2` and `decode_v2` for the v2 layout. +/// 2. Update `const VERSION = version::V2;`. +/// 3. Make `encode_latest` forward to `encode_v2` and `decode_latest` forward to `decode_v2`. +/// 4. Keep `decode_v1` and `encode_v1`so existing on-disk v1 values remain readable and +/// constructable. +/// - On version bumps >v2: +/// 1. Create `pub const Vn: u8 = n` struct in version mod for new version. +/// 2. Add optional `encode_vn` and `decode_vn` methods to `ZainoVersionedSerde`. +/// 3. Implement new `encode_vn` and `decode_vn` for the new vn layout. +/// 2. Update `const VERSION = version::Vn;`. +/// 3. Make `encode_latest` forward to `encode_vn` and `decode_latest` forward to `decode_vn`. +/// 4. Keep existing encodes / decodes so existing on-disk v1 values remain readable and +/// constructable. /// -/// ### Initial release (`VERSION = 1`) -/// 1. `pub struct TxV1 { … }` // layout frozen forever -/// 2. `impl ZainoVersionedSerde for TxV1` -/// * `const VERSION = 1` -/// * `encode_body` – **v1** layout -/// * `decode_v1` – parses **v1** bytes -/// * `decode_latest` - wrapper for `Self::decode_v1` +/// Important: **nested versioned fields.** +/// - If your type contains fields that also implement `ZainoVersionedSerde` (for example +/// `BlockHeaderData` contains `BlockIndex`), you **must** control the concrete nested +/// version when producing historical top-level encodings. +/// - Use `serialize_with_version(..., version)` or `to_bytes_with_version(version)` on the +/// nested field inside `encode_vN` to force the inner field to be encoded with a specific +/// tag/body. This guarantees that `to_bytes_with_version(Some(v))` reproduces *exactly* the +/// bytes historical writers produced (critical for checksum equality and verification). /// -/// ### Bump to v2 -/// 1. `pub struct TxV2 { … }` // new “current” layout -/// 2. `impl From for TxV2` // loss-less upgrade path -/// 3. `impl ZainoVersionedSerde for TxV2` -/// * `const VERSION = 2` -/// * `encode_body` – **v2** layout -/// * `decode_v1` – `TxV1::decode_latest(r).map(Self::from)` -/// * `decode_v2` – parses **v2** bytes -/// * `decode_latest` - wrapper for `Self::decode_v2` +/// ## Version-agnostic consumer helpers /// -/// ### Next bumps (v3, v4, …, vN) -/// * Create struct for new version. -/// * Set `const VERSION = N`. -/// * Add the `decode_vN` trait method and extend the `match` table inside **this trait** when a brand-new tag first appears. -/// * Implement `decode_vN` for N’s layout. -/// * Update `decode_latest` to wrap `decode_vN`. -/// * Implement `decode_v(N-1)`, `decode_v(N-2)`, ..., `decode_v(N-K)` for all previous versions. +/// Implementers expose the low-level `encode_vN`/`decode_vN` helpers; consumers should use +/// the version-agnostic APIs below: +/// - `serialize()` / `to_bytes()` — write the current version (latest) tag + body. +/// - `serialize_with_version(&mut w, version)` / `to_bytes_with_version(version)` — write the +/// chosen version tag and body (use when reproducing historical bytes). +/// - `deserialize()` / `from_bytes()` — read version tag and dispatch to the correct decode. /// /// ## Mandatory items per implementation /// * `const VERSION` -/// * `encode_body` -/// * `decode_vN` — **must** parse bytes for version N, where N = [`Self::VERSION`]. -/// * `decode_latest` — **must** parse `Self::VERSION` bytes. -/// -/// Historical helpers (`decode_v1`, `decode_v2`, …) must be implemented -/// for compatibility with historical versions +/// * `encode_latest` +/// * `encode_vN` +/// * `decode_latest` +/// * `decode_vN` pub trait ZainoVersionedSerde: Sized { /// Tag this build writes. const VERSION: u8; /*──────────── encoding ────────────*/ - /// Encode **only** the body (no tag). - fn encode_body(&self, w: &mut W) -> io::Result<()>; + /// Endodes a body whose tag equals `Self::VERSION`. + /// + /// The trait implementation must wrap `decode_vN` where N = [`Self::VERSION`] + fn encode_latest(&self, w: &mut W) -> io::Result<()>; /*──────────── mandatory decoder for *this* version ────────────*/ @@ -83,24 +151,65 @@ pub trait ZainoVersionedSerde: Sized { /// The trait implementation must wrap `decode_vN` where N = [`Self::VERSION`] fn decode_latest(r: &mut R) -> io::Result; - /*──────────── version decoders ────────────*/ + /*──────────── version encoders / decoders ────────────*/ // Add more versions here when required. + /// Encode the body in the *v1* layout (tag-less body only). + #[inline(always)] + #[allow(unused)] + fn encode_v1(&self, _w: &mut W) -> io::Result<()> { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "v1 encode unsupported", + )) + } + + /// Encode the body in the *v2* layout (tag-less body only). + #[inline(always)] + #[allow(unused)] + fn encode_v2(&self, _w: &mut W) -> io::Result<()> { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "v2 encode unsupported", + )) + } + #[inline(always)] #[allow(unused)] - /// Decode an older v1 version + /// Decode the body in the *v1* layout (tag-less body only). fn decode_v1(r: &mut R) -> io::Result { Err(io::Error::new(io::ErrorKind::InvalidData, "v1 unsupported")) } #[inline(always)] #[allow(unused)] - /// Decode an older v2 version + /// Decode the body in the *v2* layout (tag-less body only). fn decode_v2(r: &mut R) -> io::Result { Err(io::Error::new(io::ErrorKind::InvalidData, "v2 unsupported")) } /*──────────── router ────────────*/ + /// Encode a body for the requested `version_tag`. + /// + /// This mirrors `decode_body` but for encoding. Types that only support latest + /// may rely on the default behaviour where attempts to encode an older version + /// return an error. + #[inline] + fn encode_body(&self, w: &mut W, version_tag: u8) -> io::Result<()> { + if version_tag == Self::VERSION { + self.encode_latest(w) + } else { + match version_tag { + version::V1 => self.encode_v1(w), + version::V2 => self.encode_v2(w), + _ => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("unsupported encode version {version_tag}"), + )), + } + } + } + #[inline] /// Decode the body, dispatcing to the appropriate decode_vx function fn decode_body(r: &mut R, version_tag: u8) -> io::Result { @@ -123,8 +232,15 @@ pub trait ZainoVersionedSerde: Sized { #[inline] /// The expected start point. Read the version tag, then decode the rest fn serialize(&self, mut w: W) -> io::Result<()> { - w.write_all(&[Self::VERSION])?; - self.encode_body(&mut w) + self.serialize_with_version(&mut w, Self::VERSION) + } + + /// Serialise specifying a `version` (None -> latest). This writes the + /// version tag byte and then the body encoded *for that version*. + #[inline] + fn serialize_with_version(&self, mut w: W, version: u8) -> io::Result<()> { + w.write_all(&[version])?; + self.encode_body(&mut w, version) } #[inline] @@ -138,8 +254,14 @@ pub trait ZainoVersionedSerde: Sized { /// Serialize into a `Vec` (tag + body). #[inline] fn to_bytes(&self) -> io::Result> { + self.to_bytes_with_version(Self::VERSION) + } + + /// to_bytes with explicit version selection (None -> latest). + #[inline] + fn to_bytes_with_version(&self, version: u8) -> io::Result> { let mut buf = Vec::new(); - self.serialize(&mut buf)?; + self.serialize_with_version(&mut buf, version)?; Ok(buf) } @@ -499,3 +621,389 @@ where let len = CompactSize::read(&mut r)? as usize; (0..len).map(|_| f(&mut r)).collect() } + +#[cfg(test)] +mod tests { + use super::*; + use core2::io::Cursor; + + #[test] + fn compactsize_roundtrip_various() { + let values: &[usize] = &[ + 0usize, + 1, + 10, + 252, + 253, + 254, + 1024, + 0xFFFFusize, + 0x1_0000usize, + MAX_COMPACT_SIZE as usize, + ]; + + for &v in values { + let mut buf = Vec::new(); + CompactSize::write(&mut buf, v).expect("write compactsize"); + let mut cur = Cursor::new(&buf); + let r = CompactSize::read(&mut cur).expect("read compactsize"); + assert_eq!(r as usize, v, "compactsize roundtrip mismatch for {}", v); + + // serialized_size should match produced length + assert_eq!(CompactSize::serialized_size(v), buf.len()); + } + } + + #[test] + fn compactsize_too_large_errors() { + let too_big = (MAX_COMPACT_SIZE as usize) + 1; + let mut buf = Vec::new(); + CompactSize::write(&mut buf, too_big).expect("write oversized"); + // Reading should return an error because the value exceeds MAX_COMPACT_SIZE. + assert!( + CompactSize::read(Cursor::new(&buf)).is_err(), + "reading compactsize > MAX_COMPAC T_SIZE should error" + ); + } + + #[test] + fn compactsize_read_t_roundtrip() { + let mut buf = Vec::new(); + CompactSize::write(&mut buf, 1000).expect("write 1000"); + let mut cur = Cursor::new(&buf); + let v: u32 = CompactSize::read_t(&mut cur).expect("read_t to u32"); + assert_eq!(v, 1000u32); + } + + #[test] + fn u8_roundtrip() { + let mut buf = Vec::new(); + write_u8(&mut buf, 0xAB).expect("write_u8"); + let v = read_u8(Cursor::new(&buf)).expect("read_u8"); + assert_eq!(v, 0xAB); + } + + #[test] + fn u16_le_roundtrip() { + let mut buf = Vec::new(); + write_u16_le(&mut buf, 0x1234).expect("write_u16_le"); + let v = read_u16_le(Cursor::new(&buf)).expect("read_u16_le"); + assert_eq!(v, 0x1234); + } + + #[test] + fn u16_be_roundtrip() { + let mut buf = Vec::new(); + write_u16_be(&mut buf, 0x1234).expect("write_u16_be"); + let v = read_u16_be(Cursor::new(&buf)).expect("read_u16_be"); + assert_eq!(v, 0x1234); + } + + #[test] + fn u32_le_roundtrip() { + let mut buf = Vec::new(); + write_u32_le(&mut buf, 0x1122_3344).expect("write_u32_le"); + let v = read_u32_le(Cursor::new(&buf)).expect("read_u32_le"); + assert_eq!(v, 0x1122_3344); + } + + #[test] + fn u32_be_roundtrip() { + let mut buf = Vec::new(); + write_u32_be(&mut buf, 0x1122_3344).expect("write_u32_be"); + let v = read_u32_be(Cursor::new(&buf)).expect("read_u32_be"); + assert_eq!(v, 0x1122_3344); + } + + #[test] + fn u64_le_roundtrip() { + let mut buf = Vec::new(); + write_u64_le(&mut buf, 0x0102_0304_0506_0708).expect("write_u64_le"); + let v = read_u64_le(Cursor::new(&buf)).expect("read_u64_le"); + assert_eq!(v, 0x0102_0304_0506_0708u64); + } + + #[test] + fn u64_be_roundtrip() { + let mut buf = Vec::new(); + write_u64_be(&mut buf, 0x0102_0304_0506_0708).expect("write_u64_be"); + let v = read_u64_be(Cursor::new(&buf)).expect("read_u64_be"); + assert_eq!(v, 0x0102_0304_0506_0708u64); + } + + #[test] + fn i64_le_roundtrip() { + let mut buf = Vec::new(); + let val: i64 = -9_001_234_567_890i64; + write_i64_le(&mut buf, val).expect("write_i64_le"); + let r = read_i64_le(Cursor::new(&buf)).expect("read_i64_le"); + assert_eq!(r, val); + } + + #[test] + fn i64_be_roundtrip() { + let mut buf = Vec::new(); + let val: i64 = -9_001_234_567_890i64; + write_i64_be(&mut buf, val).expect("write_i64_be"); + let r = read_i64_be(Cursor::new(&buf)).expect("read_i64_be"); + assert_eq!(r, val); + } + + #[test] + fn fixed_le_roundtrip() { + let arr: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; + let mut buf = Vec::new(); + write_fixed_le::<8, _>(&mut buf, &arr).expect("write_fixed_le"); + let got: [u8; 8] = read_fixed_le::<8, _>(Cursor::new(&buf)).expect("read_fixed_le"); + assert_eq!(got, arr); + } + + #[test] + fn fixed_be_roundtrip() { + let arr: [u8; 8] = [10, 11, 12, 13, 14, 15, 16, 17]; + let mut buf = Vec::new(); + write_fixed_be::<8, _>(&mut buf, &arr).expect("write_fixed_be"); + let got: [u8; 8] = read_fixed_be::<8, _>(Cursor::new(&buf)).expect("read_fixed_be"); + // read_fixed_be reverses the wire bytes back into internal order, so we expect equality + assert_eq!(got, arr); + } + + #[test] + fn option_none_roundtrip() { + let mut buf = Vec::new(); + write_option(&mut buf, &None::, |_w, _v| Ok(())).expect("write_option none"); + let mut cur = Cursor::new(&buf); + let r: Option = read_option(&mut cur, |_r| unreachable!()).expect("read_option none"); + assert!(r.is_none()); + } + + #[test] + fn option_some_roundtrip() { + let mut buf = Vec::new(); + write_option(&mut buf, &Some(0xDEADBEEFu32), |w, v| write_u32_le(w, *v)) + .expect("write_option some"); + let mut cur = Cursor::new(&buf); + let r: Option = read_option(&mut cur, |r| read_u32_le(r)).expect("read_option some"); + assert_eq!(r, Some(0xDEADBEEF)); + } + + #[test] + fn write_vec_read_vec_roundtrip() { + let items = vec![1u16, 2u16, 3u16, 0xABCDu16]; + let mut buf = Vec::new(); + write_vec(&mut buf, &items, |w, v| write_u16_le(w, *v)).expect("write_vec"); + let mut cur = Cursor::new(&buf); + let r: Vec = read_vec(&mut cur, |r| read_u16_le(r)).expect("read_vec"); + assert_eq!(r, items); + } + + #[test] + fn read_vec_into_roundtrip() { + let items = vec![10u32, 11u32, 12u32]; + let mut buf = Vec::new(); + write_vec(&mut buf, &items, |w, v| write_u32_le(w, *v)).expect("write_vec u32"); + let mut cur = Cursor::new(&buf); + let out: Vec = read_vec_into(&mut cur, |r| read_u32_le(r)).expect("read_vec_into"); + assert_eq!(out, items); + } + + #[derive(Debug, Clone, PartialEq, Eq)] + struct Inner { + pub x: u32, + } + + // Inner: versioned: v1 writes little-endian, v2 writes big-endian for demonstration. + impl ZainoVersionedSerde for Inner { + // current build writes v2 + const VERSION: u8 = version::V2; + + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + // latest = v2 + self.encode_v2(w) + } + + fn decode_latest(r: &mut R) -> io::Result { + Self::decode_v2(r) + } + + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + // v1 body: x as little-endian + write_u32_le(w, self.x) + } + + fn encode_v2(&self, w: &mut W) -> io::Result<()> { + // v2 body: x as big-endian + write_u32_be(w, self.x) + } + + fn decode_v1(r: &mut R) -> io::Result { + let x = read_u32_le(r)?; + Ok(Inner { x }) + } + + fn decode_v2(r: &mut R) -> io::Result { + let x = read_u32_be(r)?; + Ok(Inner { x }) + } + } + + #[derive(Debug, Clone, PartialEq, Eq)] + struct Outer { + pub inner: Inner, + pub y: u8, + } + + // Outer: top-level is versioned. Top-level v1 must include nested Inner encoded as v1. + impl ZainoVersionedSerde for Outer { + // this build writes v2 by default + const VERSION: u8 = version::V2; + + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + self.encode_v2(w) + } + + fn decode_latest(r: &mut R) -> io::Result { + Self::decode_v2(r) + } + + // Top-level v1: explicitly request inner as v1 (writes inner tag+body for v1) + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + // write nested Inner as a versioned record with tag+body + self.inner.serialize_with_version(&mut *w, version::V1)?; + // then write y + w.write_all(&[self.y])?; + Ok(()) + } + + // Top-level v2: explicitly request inner as v2 + fn encode_v2(&self, w: &mut W) -> io::Result<()> { + self.inner.serialize_with_version(&mut *w, version::V2)?; + w.write_all(&[self.y])?; + Ok(()) + } + + fn decode_v1(r: &mut R) -> io::Result { + // inner is stored with its own tag, so use Inner::deserialize + let inner = Inner::deserialize(&mut *r)?; + let mut buf = [0u8; 1]; + r.read_exact(&mut buf)?; + Ok(Outer { inner, y: buf[0] }) + } + + fn decode_v2(r: &mut R) -> io::Result { + // same decoding behavior: nested inner may include its tag + let inner = Inner::deserialize(&mut *r)?; + let mut buf = [0u8; 1]; + r.read_exact(&mut buf)?; + Ok(Outer { inner, y: buf[0] }) + } + } + + #[test] + fn inner_v1_v2_roundtrip_and_difference() { + let i = Inner { x: 0x1122_3344 }; + + // v1 and v2 bytes should differ + let b_v1 = i.to_bytes_with_version(version::V1).expect("v1 bytes"); + let b_v2 = i.to_bytes_with_version(version::V2).expect("v2 bytes"); + assert_ne!(b_v1, b_v2, "v1 and v2 encodings must differ for the test"); + + // decoding roundtrip for both + let decoded_v1 = Inner::from_bytes(&b_v1).expect("decode v1"); + let decoded_v2 = Inner::from_bytes(&b_v2).expect("decode v2"); + assert_eq!(decoded_v1, i); + assert_eq!(decoded_v2, i); + + // check tags: first byte is version tag + assert_eq!(b_v1[0], version::V1); + assert_eq!(b_v2[0], version::V2); + + // Inspect body bytes to ensure endianness was used differently + // body starts at index 1 (after tag) + let body_v1 = &b_v1[1..]; + let body_v2 = &b_v2[1..]; + // v1 is little-endian -> first body byte should be low-order byte 0x44 + assert_eq!(body_v1[0], 0x44); + // v2 is big-endian -> first body byte should be high-order byte 0x11 + assert_eq!(body_v2[0], 0x11); + } + + #[test] + fn outer_nested_v1_v2_roundtrip_and_nested_tag_behavior() { + let o = Outer { + inner: Inner { x: 0xAABBCCDD }, + y: 0x7f, + }; + + // produce top-level v1 and v2 bytes + let top_v1 = o.to_bytes_with_version(version::V1).expect("outer v1"); + let top_v2 = o.to_bytes_with_version(version::V2).expect("outer v2"); + + // top-level tags: + assert_eq!(top_v1[0], version::V1); + assert_eq!(top_v2[0], version::V2); + + // nested inner tag should appear immediately after top-level tag + // (we wrote inner with serialize_with_version in encode_vN) + assert!(top_v1.len() >= 2 && top_v2.len() >= 2); + let nested_tag_v1 = top_v1[1]; + let nested_tag_v2 = top_v2[1]; + + // For top-level v1 we explicitly asked inner to be v1 + assert_eq!(nested_tag_v1, version::V1); + // For top-level v2 we explicitly asked inner to be v2 + assert_eq!(nested_tag_v2, version::V2); + + // decode both and ensure roundtrip equality + let decoded_v1 = Outer::from_bytes(&top_v1).expect("decode outer v1"); + let decoded_v2 = Outer::from_bytes(&top_v2).expect("decode outer v2"); + assert_eq!(decoded_v1, o); + assert_eq!(decoded_v2, o); + } + + #[test] + fn serialize_and_deserialize_helpers_consistency() { + let i = Inner { x: 0x0102_0304 }; + + // serialize() should use latest = v2 + let latest_bytes = i.to_bytes().expect("latest bytes"); + assert_eq!(latest_bytes[0], version::V2); + + // serialize_with_version should produce requested tag + let v1_bytes = i.to_bytes_with_version(version::V1).expect("v1 bytes"); + let v2_bytes = i.to_bytes_with_version(version::V2).expect("v2 bytes"); + assert_eq!(v1_bytes[0], version::V1); + assert_eq!(v2_bytes[0], version::V2); + + // to_bytes/from_bytes roundtrip across versions + assert_eq!(Inner::from_bytes(&v1_bytes).expect("from v1"), i); + assert_eq!(Inner::from_bytes(&v2_bytes).expect("from v2"), i); + // latest roundtrip + assert_eq!(Inner::from_bytes(&latest_bytes).expect("from latest"), i); + } + + // Additional test: ensure nested explicit encoding is necessary + #[test] + fn nested_encoding_must_use_serialize_with_version() { + let o = Outer { + inner: Inner { x: 0xDEAD_BEEF }, + y: 0x42, + }; + + // If we had implemented encode_v1 for Outer *without* calling inner.serialize_with_version(..., V1) + // the nested tag would be the inner's current tag (V2), and roundtrip of top-level v1 + // produced by such a broken implementation would not match historical bytes. + // + // The test below asserts that our encode_v1 produces nested tag == V1. + let top_v1 = o.to_bytes_with_version(version::V1).expect("outer v1"); + assert_eq!( + top_v1[1], + version::V1, + "nested inner tag must be v1 for top-level v1" + ); + + // Confirm the outer decoding still works + let decoded = Outer::from_bytes(&top_v1).expect("decode outer v1"); + assert_eq!(decoded, o); + } +} diff --git a/zaino-state/src/chain_index/finalised_state/capability.rs b/zaino-state/src/chain_index/finalised_state/capability.rs index 22e542d74..64da66171 100644 --- a/zaino-state/src/chain_index/finalised_state/capability.rs +++ b/zaino-state/src/chain_index/finalised_state/capability.rs @@ -336,16 +336,20 @@ impl DbMetadata { impl ZainoVersionedSerde for DbMetadata { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - self.version.serialize(&mut *w)?; - write_fixed_le::<32, _>(&mut *w, &self.schema_hash)?; - self.migration_status.serialize(&mut *w) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + self.version.serialize_with_version(&mut *w, 1)?; + write_fixed_le::<32, _>(&mut *w, &self.schema_hash)?; + self.migration_status.serialize_with_version(&mut *w, 1) + } + fn decode_v1(r: &mut R) -> io::Result { let version = DbVersion::deserialize(&mut *r)?; let schema_hash = read_fixed_le::<32, _>(&mut *r)?; @@ -476,16 +480,20 @@ impl DbVersion { impl ZainoVersionedSerde for DbVersion { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_u32_le(&mut *w, self.major)?; - write_u32_le(&mut *w, self.minor)?; - write_u32_le(&mut *w, self.patch) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_u32_le(&mut *w, self.major)?; + write_u32_le(&mut *w, self.minor)?; + write_u32_le(&mut *w, self.patch) + } + fn decode_v1(r: &mut R) -> io::Result { let major = read_u32_le(&mut *r)?; let minor = read_u32_le(&mut *r)?; @@ -565,7 +573,15 @@ impl fmt::Display for MigrationStatus { impl ZainoVersionedSerde for MigrationStatus { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) + } + + fn decode_latest(r: &mut R) -> io::Result { + Self::decode_v1(r) + } + + fn encode_v1(&self, w: &mut W) -> io::Result<()> { let tag = match self { MigrationStatus::Empty => 0, MigrationStatus::PartialBuidInProgress => 1, @@ -576,10 +592,6 @@ impl ZainoVersionedSerde for MigrationStatus { write_u8(w, tag) } - fn decode_latest(r: &mut R) -> io::Result { - Self::decode_v1(r) - } - fn decode_v1(r: &mut R) -> io::Result { match read_u8(r)? { 0 => Ok(MigrationStatus::Empty), diff --git a/zaino-state/src/chain_index/finalised_state/entry.rs b/zaino-state/src/chain_index/finalised_state/entry.rs index 73183f376..83d728949 100644 --- a/zaino-state/src/chain_index/finalised_state/entry.rs +++ b/zaino-state/src/chain_index/finalised_state/entry.rs @@ -138,13 +138,30 @@ impl StoredEntryFixed { /// Callers should treat a checksum mismatch as a corruption or incompatibility signal and /// return a hard error (or trigger a rebuild path), depending on context. pub(crate) fn verify>(&self, key: K) -> bool { - let body = { - let mut v = Vec::with_capacity(T::VERSIONED_LEN); - self.item.serialize(&mut v).unwrap(); - v - }; - let candidate = Self::blake2b256(&[key.as_ref(), &body].concat()); - candidate == self.checksum + // Iterate from latest (T::VERSION) down to 1 (inclusive). + let mut v = T::VERSION; + loop { + // Try to obtain the encoded bytes for this candidate version (tag + body). + match self.item.to_bytes_with_version(v) { + Ok(item_bytes) => { + // Compute the candidate checksum over (encoded_key || item_bytes). + let candidate = Self::blake2b256(&[key.as_ref(), &item_bytes].concat()); + if candidate == self.checksum { + return true; + } + } + Err(_) => { + // This version not supported by the type's encoder; try older version. + } + } + + if v == 1 { + break; + } + v = v.saturating_sub(1); + } + + false } /// Returns a reference to the inner record. @@ -176,15 +193,19 @@ impl StoredEntryFixed { impl ZainoVersionedSerde for StoredEntryFixed { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - self.item.serialize(&mut *w)?; - write_fixed_le::<32, _>(&mut *w, &self.checksum) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + self.item.serialize(&mut *w)?; + write_fixed_le::<32, _>(&mut *w, &self.checksum) + } + fn decode_v1(r: &mut R) -> io::Result { let mut body = vec![0u8; T::VERSIONED_LEN]; r.read_exact(&mut body)?; @@ -253,10 +274,30 @@ impl StoredEntryVar { /// # Key requirements /// `key` must be the exact byte encoding used as the LMDB key for this record. pub(crate) fn verify>(&self, key: K) -> bool { - let mut body = Vec::new(); - self.item.serialize(&mut body).unwrap(); - let candidate = Self::blake2b256(&[key.as_ref(), &body].concat()); - candidate == self.checksum + // Iterate from latest (T::VERSION) down to 1 (inclusive). + let mut v = T::VERSION; + loop { + // Try to obtain the encoded bytes for this candidate version (tag + body). + match self.item.to_bytes_with_version(v) { + Ok(item_bytes) => { + // Compute the candidate checksum over (encoded_key || item_bytes). + let candidate = Self::blake2b256(&[key.as_ref(), &item_bytes].concat()); + if candidate == self.checksum { + return true; + } + } + Err(_) => { + // This version not supported by the type's encoder; try older version. + } + } + + if v == 1 { + break; + } + v = v.saturating_sub(1); + } + + false } /// Returns a reference to the inner record. @@ -288,7 +329,15 @@ impl StoredEntryVar { impl ZainoVersionedSerde for StoredEntryVar { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) + } + + fn decode_latest(r: &mut R) -> io::Result { + Self::decode_v1(r) + } + + fn encode_v1(&self, w: &mut W) -> io::Result<()> { let mut body = Vec::new(); self.item.serialize(&mut body)?; @@ -297,10 +346,6 @@ impl ZainoVersionedSerde for StoredEntryVar { write_fixed_le::<32, _>(&mut *w, &self.checksum) } - fn decode_latest(r: &mut R) -> io::Result { - Self::decode_v1(r) - } - fn decode_v1(r: &mut R) -> io::Result { let len = CompactSize::read(&mut *r)? as usize; @@ -312,3 +357,210 @@ impl ZainoVersionedSerde for StoredEntryVar { Ok(Self { item, checksum }) } } + +#[cfg(test)] +mod tests { + use crate::{read_u32_be, read_u32_le, write_u32_be, write_u32_le}; + + use super::*; + + #[derive(Clone, Debug, PartialEq, Eq)] + struct TestInner { + pub x: u32, + } + + // TestInner: versioned type with two encodings: + // - v1: x as little-endian (body only) + // - v2: x as big-endian (body only) and v2 is the current version + impl ZainoVersionedSerde for TestInner { + const VERSION: u8 = version::V2; + + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + self.encode_v2(w) + } + fn decode_latest(r: &mut R) -> io::Result { + Self::decode_v2(r) + } + + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_u32_le(w, self.x) + } + fn encode_v2(&self, w: &mut W) -> io::Result<()> { + write_u32_be(w, self.x) + } + + fn decode_v1(r: &mut R) -> io::Result { + let x = read_u32_le(r)?; + Ok(TestInner { x }) + } + fn decode_v2(r: &mut R) -> io::Result { + let x = read_u32_be(r)?; + Ok(TestInner { x }) + } + } + + // Make TestInner fixed-length for StoredEntryFixed tests. + impl FixedEncodedLen for TestInner { + // body length (no version tag): 4 bytes (u32) + const ENCODED_LEN: usize = 4; + } + + // Helper: simple key bytes for tests + fn key_bytes() -> Vec { + b"test-key".to_vec() + } + + // StoredEntryFixed: latest roundtrip (new -> to_bytes -> from_bytes -> verify) + #[test] + fn stored_entry_fixed_roundtrip_latest() { + let inner = TestInner { x: 0x1122_3344 }; + let key = key_bytes(); + + // Construct wrapper using current serializer (StoredEntryFixed::new uses latest encoding) + let wrapper = StoredEntryFixed::new(&key, inner.clone()); + + // Verify should succeed for the same key + assert!( + wrapper.verify(&key), + "wrapper verify (latest) should succeed" + ); + + // Encode wrapper to bytes and decode back + let bytes = wrapper.to_bytes().expect("wrapper to_bytes"); + let parsed = StoredEntryFixed::::from_bytes(&bytes).expect("from_bytes"); + assert_eq!(parsed.item, inner); + assert_eq!(parsed.checksum, wrapper.checksum); + + // parsed wrapper should verify with same key + assert!(parsed.verify(&key)); + } + + #[test] + // StoredEntryFixed: historical v1 body present on disk -> from_bytes + verify must succeed + fn stored_entry_fixed_verify_old_v1() { + let inner = TestInner { x: 0xAABB_CCDD }; + let key = key_bytes(); + + // Produce item bytes according to historical v1 (tag + body) + let item_bytes_v1 = inner + .to_bytes_with_version(version::V1) + .expect("inner v1 bytes"); + + // Compute checksum over (key || item_bytes_v1) + let mut digest_input = Vec::with_capacity(key.len() + item_bytes_v1.len()); + digest_input.extend_from_slice(&key); + digest_input.extend_from_slice(&item_bytes_v1); + let checksum = StoredEntryFixed::::blake2b256(&digest_input); + + // Manually build on-disk raw bytes for StoredEntryFixed: + // [StoredEntryFixed::VERSION] + item_bytes_v1 (should have length T::VERSIONED_LEN) + checksum + let mut raw = Vec::new(); + raw.push(StoredEntryFixed::::VERSION); + raw.extend_from_slice(&item_bytes_v1); + raw.extend_from_slice(&checksum); + + // Parse using from_bytes (which will call decode_v1 and reconstruct wrapper) + let parsed = StoredEntryFixed::::from_bytes(&raw).expect("from_bytes v1"); + // parsed.item should equal the decoded inner + assert_eq!(parsed.item, inner); + // verify should succeed using the same key (it will try v2 then v1 candidate encodings) + assert!(parsed.verify(&key)); + } + + // StoredEntryFixed: verify fails on tampered checksum or key + #[test] + fn stored_entry_fixed_verify_tamper() { + let inner = TestInner { x: 0x0102_0304 }; + let key = key_bytes(); + + let mut wrapper = StoredEntryFixed::new(&key, inner.clone()); + assert!(wrapper.verify(&key)); + + // Tamper checksum (flip a byte) and ensure verify fails + wrapper.checksum[0] ^= 0xff; + assert!( + !wrapper.verify(&key), + "verify should fail with tampered checksum" + ); + + // Restore checksum and verify ok, then check wrong key fails + wrapper = StoredEntryFixed::new(&key, inner.clone()); + assert!(wrapper.verify(&key)); + let wrong_key = b"other-key".to_vec(); + assert!( + !wrapper.verify(&wrong_key), + "verify should fail with wrong key" + ); + } + + // -------------------- StoredEntryVar tests -------------------- + + #[test] + fn stored_entry_var_roundtrip_latest() { + let inner = TestInner { x: 0x5566_7788 }; + let key = key_bytes(); + + let wrapper = StoredEntryVar::new(&key, inner.clone()); + assert!( + wrapper.verify(&key), + "var wrapper verify (latest) should succeed" + ); + + // Encode wrapper to bytes and decode back via From/To bytes + let bytes = wrapper.to_bytes().expect("var to_bytes"); + let parsed = StoredEntryVar::::from_bytes(&bytes).expect("var from_bytes"); + assert_eq!(parsed.item, inner); + assert_eq!(parsed.checksum, wrapper.checksum); + assert!(parsed.verify(&key)); + } + + #[test] + fn stored_entry_var_verify_old_v1() { + let inner = TestInner { x: 0xDEAD_BEEF }; + let key = key_bytes(); + + // item serialized as v1 (tag + body) + let item_bytes_v1 = inner + .to_bytes_with_version(version::V1) + .expect("inner v1 bytes"); + + // checksum computed over (key || item_bytes_v1) + let mut digest_input = Vec::with_capacity(key.len() + item_bytes_v1.len()); + digest_input.extend_from_slice(&key); + digest_input.extend_from_slice(&item_bytes_v1); + let checksum = StoredEntryVar::::blake2b256(&digest_input); + + // Build raw stored value for StoredEntryVar: + // [StoredEntryVar::VERSION] + CompactSize(len) + item_bytes_v1 + checksum + let mut raw = Vec::new(); + raw.push(StoredEntryVar::::VERSION); + CompactSize::write(&mut raw, item_bytes_v1.len()).expect("write compactsize"); + raw.extend_from_slice(&item_bytes_v1); + // write checksum as fixed 32 bytes + write_fixed_le::<32, _>(&mut raw, &checksum).expect("write checksum"); + + // from_bytes should parse the body and return wrapper + let parsed = StoredEntryVar::::from_bytes(&raw).expect("var from_bytes v1"); + assert_eq!(parsed.item, inner); + // verify must succeed using same key (it will try v2 then v1) + assert!(parsed.verify(&key)); + } + + #[test] + fn stored_entry_var_verify_tamper() { + let inner = TestInner { x: 0xCAFEBABE }; + let key = key_bytes(); + + let mut wrapper = StoredEntryVar::new(&key, inner); + assert!(wrapper.verify(&key)); + + // tamper checksum + wrapper.checksum[31] ^= 0xff; + assert!(!wrapper.verify(&key)); + + // restore and test wrong key fails + let wrapper = StoredEntryVar::new(&key, TestInner { x: 0xCAFEBABE }); + let wrong_key = b"bad-key".to_vec(); + assert!(!wrapper.verify(&wrong_key)); + } +} diff --git a/zaino-state/src/chain_index/types/db/commitment.rs b/zaino-state/src/chain_index/types/db/commitment.rs index 9bb0e9730..409b0eab5 100644 --- a/zaino-state/src/chain_index/types/db/commitment.rs +++ b/zaino-state/src/chain_index/types/db/commitment.rs @@ -41,16 +41,20 @@ impl CommitmentTreeData { impl ZainoVersionedSerde for CommitmentTreeData { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - self.roots.serialize(&mut w)?; // carries its own tag - self.sizes.serialize(&mut w) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + self.roots.serialize(&mut w)?; // carries its own tag + self.sizes.serialize(&mut w) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let roots = CommitmentTreeRoots::deserialize(&mut r)?; @@ -97,16 +101,20 @@ impl CommitmentTreeRoots { impl ZainoVersionedSerde for CommitmentTreeRoots { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - write_fixed_le::<32, _>(&mut w, &self.sapling)?; - write_fixed_le::<32, _>(&mut w, &self.orchard) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + write_fixed_le::<32, _>(&mut w, &self.sapling)?; + write_fixed_le::<32, _>(&mut w, &self.orchard) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let sapling = read_fixed_le::<32, _>(&mut r)?; @@ -151,16 +159,20 @@ impl CommitmentTreeSizes { impl ZainoVersionedSerde for CommitmentTreeSizes { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - write_u32_le(&mut w, self.sapling)?; - write_u32_le(&mut w, self.orchard) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + write_u32_le(&mut w, self.sapling)?; + write_u32_le(&mut w, self.orchard) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let sapling = read_u32_le(&mut r)?; diff --git a/zaino-state/src/chain_index/types/db/legacy.rs b/zaino-state/src/chain_index/types/db/legacy.rs index 4ac81d44b..262e9b687 100644 --- a/zaino-state/src/chain_index/types/db/legacy.rs +++ b/zaino-state/src/chain_index/types/db/legacy.rs @@ -152,14 +152,18 @@ impl From for BlockHash { impl ZainoVersionedSerde for BlockHash { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_fixed_le::<32, _>(w, &self.0) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_fixed_le::<32, _>(w, &self.0) + } + fn decode_v1(r: &mut R) -> io::Result { let bytes = read_fixed_le::<32, _>(r)?; Ok(BlockHash(bytes)) @@ -269,14 +273,18 @@ impl From for TransactionHash { impl ZainoVersionedSerde for TransactionHash { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_fixed_le::<32, _>(w, &self.0) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_fixed_le::<32, _>(w, &self.0) + } + fn decode_v1(r: &mut R) -> io::Result { let bytes = read_fixed_le::<32, _>(r)?; Ok(TransactionHash(bytes)) @@ -381,15 +389,19 @@ impl TryFrom for Height { impl ZainoVersionedSerde for Height { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - // Height must sort lexicographically - write **big-endian** - write_u32_be(w, self.0) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + // Height must sort lexicographically - write **big-endian** + write_u32_be(w, self.0) + } + fn decode_v1(r: &mut R) -> io::Result { let raw = read_u32_be(r)?; Height::try_from(raw).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) @@ -413,15 +425,19 @@ pub struct ShardIndex(pub u32); impl ZainoVersionedSerde for ShardIndex { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - // Index must sort lexicographically - write **big-endian** - write_u32_be(w, self.0) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + // Index must sort lexicographically - write **big-endian** + write_u32_be(w, self.0) + } + fn decode_v1(r: &mut R) -> io::Result { let raw = read_u32_be(r)?; Ok(ShardIndex(raw)) @@ -533,15 +549,19 @@ impl From for [u8; 21] { impl ZainoVersionedSerde for AddrScript { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_fixed_le::<20, _>(&mut *w, &self.hash)?; - w.write_all(&[self.script_type]) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_fixed_le::<20, _>(&mut *w, &self.hash)?; + w.write_all(&[self.script_type]) + } + fn decode_v1(r: &mut R) -> io::Result { let hash = read_fixed_le::<20, _>(&mut *r)?; let mut buf = [0u8; 1]; @@ -598,16 +618,20 @@ impl Outpoint { impl ZainoVersionedSerde for Outpoint { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - write_fixed_le::<32, _>(&mut w, &self.prev_txid)?; - write_u32_le(&mut w, self.prev_index) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + write_fixed_le::<32, _>(&mut w, &self.prev_txid)?; + write_u32_le(&mut w, self.prev_index) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let txid = read_fixed_le::<32, _>(&mut r)?; @@ -678,19 +702,34 @@ impl BlockIndex { impl ZainoVersionedSerde for BlockIndex { const VERSION: u8 = version::V2; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - - self.hash.serialize(&mut w)?; - self.parent_hash.serialize(&mut w)?; - self.chainwork.serialize(&mut w)?; - self.height.serialize(&mut *w) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v2(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v2(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + + self.hash.serialize_with_version(&mut w, 1)?; + self.parent_hash.serialize_with_version(&mut w, 1)?; + self.chainwork.serialize_with_version(&mut w, 1)?; + write_option(&mut w, &Some(self.height), |w, h| { + h.serialize_with_version(w, 1) + }) + } + + fn encode_v2(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + + self.hash.serialize_with_version(&mut w, 1)?; + self.parent_hash.serialize_with_version(&mut w, 1)?; + self.chainwork.serialize_with_version(&mut w, 1)?; + self.height.serialize_with_version(&mut w, 1) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let hash = BlockHash::deserialize(&mut r)?; @@ -767,14 +806,18 @@ impl fmt::Display for ChainWork { impl ZainoVersionedSerde for ChainWork { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_fixed_le::<32, _>(w, &self.0) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_fixed_le::<32, _>(w, &self.0) + } + fn decode_v1(r: &mut R) -> io::Result { let bytes = read_fixed_le::<32, _>(r)?; Ok(ChainWork(bytes)) @@ -933,7 +976,15 @@ impl BlockData { impl ZainoVersionedSerde for BlockData { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) + } + + fn decode_latest(r: &mut R) -> io::Result { + Self::decode_v1(r) + } + + fn encode_v1(&self, w: &mut W) -> io::Result<()> { let mut w = w; // re-borrow write_u32_le(&mut w, self.version)?; @@ -945,11 +996,7 @@ impl ZainoVersionedSerde for BlockData { write_u32_le(&mut w, self.bits)?; write_fixed_le::<32, _>(&mut w, &self.nonce)?; - self.solution.serialize(&mut w) - } - - fn decode_latest(r: &mut R) -> io::Result { - Self::decode_v1(r) + self.solution.serialize_with_version(&mut w, 1) } fn decode_v1(r: &mut R) -> io::Result { @@ -1042,7 +1089,15 @@ impl<'a> TryFrom<&'a [u8]> for EquihashSolution { impl ZainoVersionedSerde for EquihashSolution { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) + } + + fn decode_latest(r: &mut R) -> io::Result { + Self::decode_v1(r) + } + + fn encode_v1(&self, w: &mut W) -> io::Result<()> { let mut w = w; match self { @@ -1057,10 +1112,6 @@ impl ZainoVersionedSerde for EquihashSolution { } } - fn decode_latest(r: &mut R) -> io::Result { - Self::decode_v1(r) - } - fn decode_v1(r: &mut R) -> io::Result { let mut r = r; @@ -1197,17 +1248,23 @@ impl IndexedBlock { impl ZainoVersionedSerde for IndexedBlock { const VERSION: u8 = version::V1; - fn encode_body(&self, mut w: &mut W) -> io::Result<()> { - self.index.serialize(&mut w)?; - self.data.serialize(&mut w)?; - write_vec(&mut w, &self.transactions, |w, tx| tx.serialize(w))?; - self.commitment_tree_data.serialize(&mut w) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, mut w: &mut W) -> io::Result<()> { + self.index.serialize_with_version(&mut w, 1)?; + self.data.serialize_with_version(&mut w, 1)?; + write_vec(&mut w, &self.transactions, |w, tx| { + tx.serialize_with_version(w, 1) + })?; + self.commitment_tree_data.serialize_with_version(&mut w, 1) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let index = BlockIndex::deserialize(&mut r)?; @@ -1604,19 +1661,23 @@ impl TryFrom<(u64, zaino_fetch::chain::transaction::FullTransaction)> for Compac impl ZainoVersionedSerde for CompactTxData { const VERSION: u8 = version::V1; - fn encode_body(&self, mut w: &mut W) -> io::Result<()> { - write_u64_le(&mut w, self.index)?; - - self.txid.serialize(&mut w)?; - self.transparent.serialize(&mut w)?; - self.sapling.serialize(&mut w)?; - self.orchard.serialize(&mut w) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, mut w: &mut W) -> io::Result<()> { + write_u64_le(&mut w, self.index)?; + + self.txid.serialize_with_version(&mut w, 1)?; + self.transparent.serialize_with_version(&mut w, 1)?; + self.sapling.serialize_with_version(&mut w, 1)?; + self.orchard.serialize_with_version(&mut w, 1) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let index = read_u64_le(&mut r)?; @@ -1649,17 +1710,25 @@ pub struct TransparentCompactTx { impl ZainoVersionedSerde for TransparentCompactTx { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - - write_vec(&mut w, &self.vin, |w, txin| txin.serialize(w))?; - write_vec(&mut w, &self.vout, |w, txout| txout.serialize(w)) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + + write_vec(&mut w, &self.vin, |w, txin| { + txin.serialize_with_version(w, 1) + })?; + write_vec(&mut w, &self.vout, |w, txout| { + txout.serialize_with_version(w, 1) + }) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; @@ -1759,16 +1828,20 @@ impl TxInCompact { impl ZainoVersionedSerde for TxInCompact { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - write_fixed_le::<32, _>(&mut w, &self.prevout_txid)?; - write_u32_le(&mut w, self.prevout_index) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + write_fixed_le::<32, _>(&mut w, &self.prevout_txid)?; + write_u32_le(&mut w, self.prevout_index) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let txid = read_fixed_le::<32, _>(&mut r)?; @@ -1822,14 +1895,18 @@ impl ScriptType { impl ZainoVersionedSerde for ScriptType { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - w.write_all(&[*self as u8]) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + w.write_all(&[*self as u8]) + } + fn decode_v1(r: &mut R) -> io::Result { let mut b = [0u8; 1]; r.read_exact(&mut b)?; @@ -1989,17 +2066,21 @@ impl> TryFrom<(u64, T)> for TxOutCompact { impl ZainoVersionedSerde for TxOutCompact { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - write_u64_le(&mut w, self.value)?; - write_fixed_le::<20, _>(&mut w, &self.script_hash)?; - w.write_all(&[self.script_type]) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + write_u64_le(&mut w, self.value)?; + write_fixed_le::<20, _>(&mut w, &self.script_hash)?; + w.write_all(&[self.script_type]) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let value = read_u64_le(&mut r)?; @@ -2063,18 +2144,22 @@ impl SaplingCompactTx { impl ZainoVersionedSerde for SaplingCompactTx { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - - write_option(&mut w, &self.value, |w, v| write_i64_le(w, *v))?; - write_vec(&mut w, &self.spends, |w, s| s.serialize(w))?; - write_vec(&mut w, &self.outputs, |w, o| o.serialize(w)) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + + write_option(&mut w, &self.value, |w, v| write_i64_le(w, *v))?; + write_vec(&mut w, &self.spends, |w, s| s.serialize_with_version(w, 1))?; + write_vec(&mut w, &self.outputs, |w, o| o.serialize_with_version(w, 1)) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; @@ -2116,14 +2201,18 @@ impl CompactSaplingSpend { impl ZainoVersionedSerde for CompactSaplingSpend { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_fixed_le::<32, _>(w, &self.nf) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_fixed_le::<32, _>(w, &self.nf) + } + fn decode_v1(r: &mut R) -> io::Result { Ok(CompactSaplingSpend::new(read_fixed_le::<32, _>(r)?)) } @@ -2186,17 +2275,21 @@ impl CompactSaplingOutput { impl ZainoVersionedSerde for CompactSaplingOutput { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - write_fixed_le::<32, _>(&mut w, &self.cmu)?; - write_fixed_le::<32, _>(&mut w, &self.ephemeral_key)?; - write_fixed_le::<52, _>(&mut w, &self.ciphertext) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + write_fixed_le::<32, _>(&mut w, &self.cmu)?; + write_fixed_le::<32, _>(&mut w, &self.ephemeral_key)?; + write_fixed_le::<52, _>(&mut w, &self.ciphertext) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let cmu = read_fixed_le::<32, _>(&mut r)?; @@ -2242,17 +2335,21 @@ impl OrchardCompactTx { impl ZainoVersionedSerde for OrchardCompactTx { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - - write_option(&mut w, &self.value, |w, v| write_i64_le(w, *v))?; - write_vec(&mut w, &self.actions, |w, a| a.serialize(w)) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + + write_option(&mut w, &self.value, |w, v| write_i64_le(w, *v))?; + write_vec(&mut w, &self.actions, |w, a| a.serialize_with_version(w, 1)) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; @@ -2328,7 +2425,15 @@ impl CompactOrchardAction { impl ZainoVersionedSerde for CompactOrchardAction { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) + } + + fn decode_latest(r: &mut R) -> io::Result { + Self::decode_v1(r) + } + + fn encode_v1(&self, w: &mut W) -> io::Result<()> { let mut w = w; write_fixed_le::<32, _>(&mut w, &self.nullifier)?; write_fixed_le::<32, _>(&mut w, &self.cmx)?; @@ -2336,10 +2441,6 @@ impl ZainoVersionedSerde for CompactOrchardAction { write_fixed_le::<52, _>(&mut w, &self.ciphertext) } - fn decode_latest(r: &mut R) -> io::Result { - Self::decode_v1(r) - } - fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let nf = read_fixed_le::<32, _>(&mut r)?; @@ -2389,15 +2490,19 @@ impl TxLocation { impl ZainoVersionedSerde for TxLocation { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_u32_be(&mut *w, self.block_height)?; - write_u16_be(&mut *w, self.tx_index) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_u32_be(&mut *w, self.block_height)?; + write_u16_be(&mut *w, self.tx_index) + } + fn decode_v1(r: &mut R) -> io::Result { let block_height = read_u32_be(&mut *r)?; let tx_index = read_u16_be(&mut *r)?; @@ -2483,17 +2588,21 @@ impl AddrHistRecord { impl ZainoVersionedSerde for AddrHistRecord { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - self.tx_location.serialize(&mut *w)?; - write_u16_be(&mut *w, self.out_index)?; - write_u64_le(&mut *w, self.value)?; - w.write_all(&[self.flags]) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + self.tx_location.serialize_with_version(&mut *w, 1)?; + write_u16_be(&mut *w, self.out_index)?; + write_u64_le(&mut *w, self.value)?; + w.write_all(&[self.flags]) + } + fn decode_v1(r: &mut R) -> io::Result { let tx_location = TxLocation::deserialize(&mut *r)?; let out_index = read_u16_be(&mut *r)?; @@ -2585,14 +2694,18 @@ impl AddrEventBytes { impl ZainoVersionedSerde for AddrEventBytes { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_fixed_le::<17, _>(w, &self.0) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_fixed_le::<17, _>(w, &self.0) + } + fn decode_v1(r: &mut R) -> io::Result { Ok(AddrEventBytes(read_fixed_le::<17, _>(r)?)) } @@ -2654,17 +2767,21 @@ impl ShardRoot { impl ZainoVersionedSerde for ShardRoot { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - write_fixed_le::<32, _>(&mut w, &self.hash)?; - write_fixed_le::<32, _>(&mut w, &self.final_block_hash)?; - write_u32_le(&mut w, self.final_block_height) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + write_fixed_le::<32, _>(&mut w, &self.hash)?; + write_fixed_le::<32, _>(&mut w, &self.final_block_hash)?; + write_u32_le(&mut w, self.final_block_height) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let hash = read_fixed_le::<32, _>(&mut r)?; @@ -2711,15 +2828,24 @@ impl BlockHeaderData { } impl ZainoVersionedSerde for BlockHeaderData { - const VERSION: u8 = version::V1; + const VERSION: u8 = version::V2; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - self.index.serialize(&mut *w)?; - self.data.serialize(w) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v2(self, w) } fn decode_latest(r: &mut R) -> io::Result { - Self::decode_v1(r) + Self::decode_v2(r) + } + + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + self.index.serialize_with_version(&mut *w, 1)?; + self.data.serialize_with_version(w, 1) + } + + fn encode_v2(&self, w: &mut W) -> io::Result<()> { + self.index.serialize_with_version(&mut *w, 2)?; + self.data.serialize_with_version(w, 1) } fn decode_v1(r: &mut R) -> io::Result { @@ -2727,6 +2853,10 @@ impl ZainoVersionedSerde for BlockHeaderData { let data = BlockData::deserialize(r)?; Ok(BlockHeaderData::new(index, data)) } + + fn decode_v2(r: &mut R) -> io::Result { + Self::decode_v1(r) + } } /// Database wrapper for `Vec`. @@ -2752,14 +2882,18 @@ impl TxidList { impl ZainoVersionedSerde for TxidList { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_vec(w, &self.txids, |w, h| h.serialize(w)) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_vec(w, &self.txids, |w, h| h.serialize_with_version(w, 1)) + } + fn decode_v1(r: &mut R) -> io::Result { let tx = read_vec(r, |r| TransactionHash::deserialize(r))?; Ok(TxidList::new(tx)) @@ -2823,16 +2957,20 @@ impl TransparentTxList { impl ZainoVersionedSerde for TransparentTxList { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_vec(w, &self.tx, |w, opt| { - write_option(w, opt, |w, t| t.serialize(w)) - }) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_vec(w, &self.tx, |w, opt| { + write_option(w, opt, |w, t| t.serialize_with_version(w, 1)) + }) + } + fn decode_v1(r: &mut R) -> io::Result { let tx = read_vec(r, |r| { read_option(r, |r| TransparentCompactTx::deserialize(r)) @@ -2902,16 +3040,20 @@ impl SaplingTxList { impl ZainoVersionedSerde for SaplingTxList { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_vec(w, &self.tx, |w, opt| { - write_option(w, opt, |w, t| t.serialize(w)) - }) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_vec(w, &self.tx, |w, opt| { + write_option(w, opt, |w, t| t.serialize_with_version(w, 1)) + }) + } + fn decode_v1(r: &mut R) -> io::Result { let tx = read_vec(r, |r| read_option(r, |r| SaplingCompactTx::deserialize(r)))?; Ok(SaplingTxList::new(tx)) @@ -2973,16 +3115,20 @@ impl OrchardTxList { impl ZainoVersionedSerde for OrchardTxList { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - write_vec(w, &self.tx, |w, opt| { - write_option(w, opt, |w, t| t.serialize(w)) - }) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + write_vec(w, &self.tx, |w, opt| { + write_option(w, opt, |w, t| t.serialize_with_version(w, 1)) + }) + } + fn decode_v1(r: &mut R) -> io::Result { let tx = read_vec(r, |r| read_option(r, |r| OrchardCompactTx::deserialize(r)))?; Ok(OrchardTxList::new(tx)) From 9aca7940a423a3b3de9ba36432bda41575d30942 Mon Sep 17 00:00:00 2001 From: idky137 Date: Thu, 12 Feb 2026 17:31:20 +0000 Subject: [PATCH 5/6] docs --- .../chain_index/finalised_state/CHANGELOG.md | 11 ++++---- .../finalised_state/db/db_schema_v1.txt | 17 ++++++------- .../src/chain_index/finalised_state/db/v1.rs | 4 +-- .../chain_index/finalised_state/migrations.rs | 11 ++++---- zaino-state/src/chain_index/tests/types.rs | 25 +++++++++---------- 5 files changed, 32 insertions(+), 36 deletions(-) diff --git a/zaino-state/src/chain_index/finalised_state/CHANGELOG.md b/zaino-state/src/chain_index/finalised_state/CHANGELOG.md index f76ea261b..8a966147e 100644 --- a/zaino-state/src/chain_index/finalised_state/CHANGELOG.md +++ b/zaino-state/src/chain_index/finalised_state/CHANGELOG.md @@ -103,24 +103,23 @@ Date: 2026-01-27 -------------------------------------------------------------------------------- Summary -- BlockIndex v2 introduced; because relevant tables (notably `headers` / `BlockHeaderData`) use - variable-length encodings and `BlockIndex` is nested/versioned, existing tables are updated - in-place: DB values may contain either v1 or v2 `BlockIndex` entries. +- BlockHeaderData v2 introduced (internally using new BlockIndex::V2 format); because relevant tables (notably `headers` / `BlockHeaderData`) use + variable-length encodings existing tables are updated in-place: DB values may contain either v1 or v2 `BlockHeaderData` entries. - Recorded on-disk schema text was clarified; migration refreshes persisted `DbMetadata.schema_hash` so the metadata matches the repository's schema contract. - Updated compact block API contract (streaming + pool filtering semantics). On-disk schema - Layout: - - Updated [`BlockHeaderData`] table by introducing [`BlockIndex::V2`], this table may now hold either V1 or V2 - [`BlockIndex`] structs, with serde handles internally. + - Updated [`BlockHeaderData`] table by introducing [`BlockHeaderData::V2`] (and internally [`BlockIndex::V2`]), this table may now hold either V1 or V2 + [`BlockHeaderData`] structs, with serde handled internally. - Tables: - Added: None. - Removed: None. - Renamed: None. - Encoding: - Keys: No changes. - - Values: Introduced `[BlockIndex::V2]`. + - Values: Introduced `[BlockHeaderData::V2]`. - Checksums / validation: No changes. - Invariants: - No changes. diff --git a/zaino-state/src/chain_index/finalised_state/db/db_schema_v1.txt b/zaino-state/src/chain_index/finalised_state/db/db_schema_v1.txt index 0d116db80..893c22618 100644 --- a/zaino-state/src/chain_index/finalised_state/db/db_schema_v1.txt +++ b/zaino-state/src/chain_index/finalised_state/db/db_schema_v1.txt @@ -30,13 +30,15 @@ # # 1. headers ― H -> StoredEntryVar # Key : BE height -# Val : 0x01 + BlockHeaderData +# Val : 0x01/0x02 + BlockHeaderData # BlockHeaderData V1 body = -# BlockIndex + BlockData +# BlockIndex V1 + BlockData # -# BlockIndex: -# - BlockIndex V2 body = B hash B parent_hash U256 chain_work H height -# - BlockIndex V1 body = B hash B parent_hash U256 chain_work Option height +# BlockHeaderData V2 body = +# BlockIndex V2 + BlockData +# +# BlockIndex V2 body = B hash B parent_hash U256 chain_work H height +# BlockIndex V1 body = B hash B parent_hash U256 chain_work Option height # # BlockData = LE(u32) version # LE(i64) unix_time @@ -45,11 +47,6 @@ # LE(u32) bits # [32] nonce # -# Note: BlockIndex is itself encoded with ZainoVersionedSerde, so the BlockIndex bytes -# begin with a u8 version_tag (0x01 = v1, 0x02 = v2). BlockHeaderData V1's -# body therefore contains a nested BlockIndex whose tag indicates which BlockIndex -# layout follows (v1 = Option, v2 = raw H). -# # 2. txids ― H -> StoredEntryVar # Val : 0x01 + CS count + count × B txid # diff --git a/zaino-state/src/chain_index/finalised_state/db/v1.rs b/zaino-state/src/chain_index/finalised_state/db/v1.rs index 58bec9d49..3b310d8ca 100644 --- a/zaino-state/src/chain_index/finalised_state/db/v1.rs +++ b/zaino-state/src/chain_index/finalised_state/db/v1.rs @@ -104,8 +104,8 @@ pub(crate) const DB_SCHEMA_V1_TEXT: &str = include_str!("db_schema_v1.txt"); /// This value is compared against the schema hash stored in the metadata record to detect schema /// drift without a corresponding version bump. pub(crate) const DB_SCHEMA_V1_HASH: [u8; 32] = [ - 0xbf, 0xa6, 0xcc, 0x70, 0x62, 0xa3, 0x44, 0xe7, 0xbc, 0xa2, 0x34, 0x73, 0x6a, 0xfa, 0x0e, 0x9a, - 0xc2, 0xa5, 0x29, 0x18, 0xf9, 0x07, 0xc3, 0x4d, 0x52, 0x7f, 0xb9, 0x2d, 0x27, 0x28, 0x0c, 0xc7, + 0x8f, 0x21, 0x66, 0xbe, 0xfd, 0x8b, 0x98, 0xc9, 0x41, 0x47, 0x20, 0x36, 0x66, 0x3b, 0xda, 0xc9, + 0x63, 0x8d, 0x60, 0x17, 0x0a, 0xc7, 0x89, 0x41, 0xef, 0x4e, 0x46, 0x40, 0xb2, 0x6c, 0x22, 0xc2, ]; /// *Current* database V1 version. diff --git a/zaino-state/src/chain_index/finalised_state/migrations.rs b/zaino-state/src/chain_index/finalised_state/migrations.rs index dc03171a1..78ab9cf24 100644 --- a/zaino-state/src/chain_index/finalised_state/migrations.rs +++ b/zaino-state/src/chain_index/finalised_state/migrations.rs @@ -79,11 +79,12 @@ //! This release also introduces [`MigrationStep`], the enum-based migration dispatcher used by //! [`MigrationManager`], to allow selecting between multiple concrete migration implementations. //! -//! Important note: `BlockIndex` now has a V2 wire layout. Because `BlockHeaderData` is stored -//! as a `StoredEntryVar` and `BlockIndex` is itself versioned, the `headers` table can hold -//! either `BlockIndex` v1 or v2 entries without a full table rewrite (in-place update). This -//! migration is metadata-only: it advances the `DbMetadata::version` and refreshes the recorded -//! on-disk schema checksum so persisted metadata matches the repository's updated schema text. +//! Important note: `BlockHeaderData` now has a V2 on-disk layout which uses the V2 +//! `BlockIndex` wire format. Because the `headers` table stores `BlockHeaderData` as a +//! `StoredEntryVar` (no fixed-length optimisations), the table may contain both V1 and V2 +//! `BlockHeaderData` records concurrently. This migration is metadata-only: it advances +//! `DbMetadata::version` and refreshes the recorded schema checksum so persisted metadata +//! matches the repository's updated schema text. //! //! # Development: adding a new migration step //! diff --git a/zaino-state/src/chain_index/tests/types.rs b/zaino-state/src/chain_index/tests/types.rs index 6882263a0..ac5396bc0 100644 --- a/zaino-state/src/chain_index/tests/types.rs +++ b/zaino-state/src/chain_index/tests/types.rs @@ -1,8 +1,6 @@ //! Zaino ChainIndex::types unit tests. -use crate::{ - chain_index::tests::init_tracing, version, write_option, BlockIndex, ZainoVersionedSerde as _, -}; +use crate::{chain_index::tests::init_tracing, version, BlockIndex, ZainoVersionedSerde as _}; #[tokio::test(flavor = "multi_thread")] async fn blockindex_v1_v2_serde() { @@ -14,22 +12,23 @@ async fn blockindex_v1_v2_serde() { let chainwork = crate::ChainWork::from_u256(0.into()); let height = crate::Height(42); - // Construct a v1-encoded BlockIndex bytes (tag 0x01 + body with Option) - let mut v1_bytes: Vec = Vec::new(); - v1_bytes.push(version::V1); // leading tag for BlockIndex v1 - hash.serialize(&mut v1_bytes).unwrap(); - parent_hash.serialize(&mut v1_bytes).unwrap(); - chainwork.serialize(&mut v1_bytes).unwrap(); - // v1 used Option - write_option(&mut v1_bytes, &Some(height), |w, h| h.serialize(w)).unwrap(); + // Create a BlockIndex value + let bidx = BlockIndex::new(hash, parent_hash, chainwork, height); + + // Produce v1 bytes using the new versioned encode API (tag + body) + let v1_bytes = bidx + .to_bytes_with_version(version::V1) + .expect("v1 to_bytes_with_version"); // Parse v1 bytes using the new BlockIndex deserialiser — should succeed and produce same height. let parsed_v1 = BlockIndex::from_bytes(&v1_bytes).expect("decode v1 BlockIndex"); - assert_eq!(parsed_v1.height(), height); + assert_eq!(parsed_v1, bidx); // Now round-trip a v2 BlockIndex (current writer). BlockIndex::to_bytes() writes V2. - let bidx = BlockIndex::new(hash, parent_hash, chainwork, height); let v2_bytes = bidx.to_bytes().expect("v2 to_bytes"); let parsed_v2 = BlockIndex::from_bytes(&v2_bytes).expect("decode v2 BlockIndex"); assert_eq!(parsed_v2, bidx); + + // sanity: v1 and v2 encodings must differ + assert_ne!(v1_bytes, v2_bytes, "v1 and v2 encodings should differ"); } From 3613858d5e22fd8ae06b73f9634886a51909c0b1 Mon Sep 17 00:00:00 2001 From: idky137 Date: Fri, 20 Feb 2026 14:04:29 +0000 Subject: [PATCH 6/6] updated ZainoVersionedSerde for MempoolInfo --- zaino-state/src/chain_index/types/db/metadata.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/zaino-state/src/chain_index/types/db/metadata.rs b/zaino-state/src/chain_index/types/db/metadata.rs index e58ee26f7..366d1c7c5 100644 --- a/zaino-state/src/chain_index/types/db/metadata.rs +++ b/zaino-state/src/chain_index/types/db/metadata.rs @@ -18,17 +18,21 @@ pub struct MempoolInfo { impl ZainoVersionedSerde for MempoolInfo { const VERSION: u8 = version::V1; - fn encode_body(&self, w: &mut W) -> io::Result<()> { - let mut w = w; - write_u64_le(&mut w, self.size)?; - write_u64_le(&mut w, self.bytes)?; - write_u64_le(&mut w, self.usage) + fn encode_latest(&self, w: &mut W) -> io::Result<()> { + Self::encode_v1(self, w) } fn decode_latest(r: &mut R) -> io::Result { Self::decode_v1(r) } + fn encode_v1(&self, w: &mut W) -> io::Result<()> { + let mut w = w; + write_u64_le(&mut w, self.size)?; + write_u64_le(&mut w, self.bytes)?; + write_u64_le(&mut w, self.usage) + } + fn decode_v1(r: &mut R) -> io::Result { let mut r = r; let size = read_u64_le(&mut r)?;