Skip to content

refactor(state_sync)!: sync state transitions by (shard,state_version) - #1543

Merged
sdbondi merged 5 commits into
tari-project:developmentfrom
sdbondi:vn-state-sync-shard-version
Aug 20, 2025
Merged

refactor(state_sync)!: sync state transitions by (shard,state_version)#1543
sdbondi merged 5 commits into
tari-project:developmentfrom
sdbondi:vn-state-sync-shard-version

Conversation

@sdbondi

@sdbondi sdbondi commented Aug 15, 2025

Copy link
Copy Markdown
Member

Description

refactor(state_sync)!: sync state transitions by (shard,state_version)

Motivation and Context

State sync uses the (shard, state_version) tuple to begin syncing a specific shard from a particular version of state.
This will allow clients to monitor (sync) state transitions similarly.

This refactor also removes the need for shard sequence tracking in the db level.

How Has This Been Tested?

Manually, existing tests updated

What process can a PR reviewer use to test or verify this change?

Sync a node

Breaking Changes

  • None
  • Requires data directory to be deleted
  • Other - Please specify

Summary by CodeRabbit

  • New Features

    • Versioned, shard-aware state sync with streaming chunks and per-chunk has_more/state_version; GetSubstate responses now include per-substate created/destroyed state-version metadata.
    • Batched substate commits and structured creation/destruction metadata; per-shard state-version propagation and ShardStateVersions support.
  • Refactor

    • Storage and state-tree moved to a payload- and proof-backed, versioned model; epoch checkpoints now include per-shard root+version summaries.
  • Chores

    • Added bounded-vec dependency and generated TypeScript types for new models.
  • Tests

    • Fixtures and tests updated for batched, versioned, payload-based APIs.

@coderabbitai

coderabbitai Bot commented Aug 15, 2025

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@sdbondi has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 2 minutes and 5 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 86c6371 and fe3bf6f.

📒 Files selected for processing (1)
  • crates/rpc_state_sync/src/state_sync.rs (8 hunks)

Walkthrough

Refactors state-sync and storage to a versioned, proof-backed, batched model: adds SubstateUpdateBatch/StateVersionTransitions/StateTreePayload/TreeRootSummary, updates RocksDB schemas, proto/RPC messages, consensus and validator flows, and migrates many APIs and tests to shard+state_version semantics.

Changes

Cohort / File(s) Summary of Changes
Top-level deps
Cargo.toml, crates/common_types/Cargo.toml, applications/tari_validator_node/Cargo.toml
Add workspace deps: bounded-vec (with serde feature) and tari_state_tree.
Indexer — types & imports
applications/tari_indexer/src/block_data.rs, applications/tari_indexer/src/event_scanner.rs
Replace SubstateUpdate with SubstateUpdateProof; update imports, signatures, conversions; handle missing UP values with a warning; persist proof-based updates.
Validator node wiring
applications/tari_validator_node/src/bootstrap.rs, applications/tari_validator_node/src/state_bootstrap.rs
Move sidechain_id usage to consensus::spawn; simplify bootstrap_state signature; bootstrap uses versioned addressing and batch semantics.
Validator RPC & sync
applications/tari_validator_node/src/p2p/rpc/block_sync_task.rs, applications/tari_validator_node/src/p2p/rpc/service_impl.rs, applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs
Migrate to SubstateUpdateProof; rework state sync to shard+state_version streaming with chunking, validations, per-substate state-version fields, and updated StateSyncTask signatures.
Common types & shards
crates/common_types/src/{lib.rs,committee.rs,shard.rs,shard_group.rs,shard_state_versions.rs,versioned_substate_id.rs,num_preshards.rs}
Add and export shard_state_versions (ShardStateVersions); add Shard::from_u32 and relative index helper; make ShardGroup range inclusive and add shard_iter_with_global; add NumPreshards::MAX_SHARD; add VersionedSubstateIdRef::to_shard.
Consensus (HotStuff)
crates/consensus/src/hotstuff/{*.rs}, crates/consensus/src/hotstuff/commit_proofs.rs
Replace per-shard root hash map with TreeRootSummary (root_hash + state_version); use VersionedSubstateIdRef and .changes(); add HotStuffError::NeedsSync and shard-group consistency guards; compute roots via ShardedStateTree.
P2P proto & conversions
crates/p2p/proto/{consensus.proto,rpc.proto}, crates/p2p/src/conversions/{consensus.rs,rpc.rs}
Proto: add ShardStateVersions, TreeRootSummary, new Substate created/destroyed metadata; remove StateTransition/StateTransitionId. Conversions adapt to proof-based types, TreeRootSummary bridging, and drop justification fields.
RPC state sync service
crates/rpc_state_sync/src/state_sync.rs
Move to versioned, batched SubstateUpdateProof model: ordered state_version validation, extract template/tree changes, commit via SubstateUpdateBatch, validate per-shard roots.
RocksDB schema & Cf APIs
crates/state_store_rocksdb/src/{reader.rs,writer.rs,store.rs,dbs/transaction.rs,cf_api.rs,range.rs,lib.rs}, crates/state_store_rocksdb/src/codecs/{mod.rs,tuple.rs}, crates/state_store_rocksdb/src/column_families/{state_transition.rs,state_tree.rs,state_tree_shard_versions.rs,bookkeeping.rs,block_diff.rs,substate.rs}
Replace StateTransitionId keys with composite (Shard, Version); add QueryRange and query_range_iterator; change node payloads to Node; add ByShard/ByShardStateVersion queries; remove PreviousEpochStateRootCf; update codecs and read options; add range module.
State tree payload migration
crates/state_tree/src/{lib.rs,tree.rs}, tests
Add StateTreePayload = SubstateAddress; switch proofs, diffs, commit/batch APIs and tree/store trait bounds to payload-based types; update tests to expect addresses in proofs.
Storage consensus models
crates/storage/src/consensus_models/{mod.rs,block.rs,block_diff.rs,block_header.rs,epoch_checkpoint.rs,state_transition.rs,state_tree_diff.rs,substate.rs,substate_change.rs,substate_update_batch.rs}, crates/state_store/mod.rs
Introduce SubstateUpdateBatch, SubstateTransition, TreeRootSummary; rename SubstateUpdate → SubstateUpdateProof; add commit_block(commit versions) and batch commit APIs; remove EpochStateRoot and StateTransitionId; expose new versioned types and methods.
State-store RocksDB reader/writer
crates/state_store_rocksdb/src/reader.rs, crates/state_store_rocksdb/src/writer.rs
Reader: new state_transitions_get_starting_at returning StateVersionTransitions, fetch nodes as Node, add state_tree_versions_get_latest_for_shard_group. Writer: add substates_commit_batch producing StateTransitionModelDataV1, accept Node.
Tests & fixtures
crates/{consensus_tests/*,state_store_tests/*}, crates/consensus_tests/fixtures/*.json
Update tests/fixtures to use versioned, batched transitions and inclusive shard ranges; adapt many tests/helpers to new APIs and types.
State-store tests & helpers
crates/state_store_tests/*
Update builders to new SubstateCreated/SubstateUpdateBatch shapes; change build_substate_record signature; add create_substate_update_batch; use Version and VersionedSubstateIdRef.
State tree tests/support
crates/state_tree/tests/*
Migrate test harness to StateTreePayload-based MemoryTreeStore; update proof expectations to substate addresses.
Utilities / db inspector
utilities/db_inspector/src/webserver/handlers/{bookkeeping.rs,state_transitions.rs}, utilities/db_inspector/src/webserver/server.rs
Adjust bookkeeping item mappings; state-transitions listing emits one row per transition using composite (shard, state_version) keys; remove ShardSeqIndex registration.
Bindings (TS)
bindings/src/{index.ts,types/*.ts}
Add TS types/exports for ShardStateVersions and SubstateCreated; update SubstateRecord/SubstateDestroyed/SubstateCreated shapes and BlockHeader.proposed_by type.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant VN as Validator Node
  participant RPC as Validator RPC
  participant Store as State Store
  participant Tree as ShardedStateTree

  Note over VN,RPC: Versioned shard-centric state sync

  VN->>RPC: SyncStateRequest { shard, start_state_version, until_epoch }
  RPC->>Store: state_transitions_get_starting_at(shard, start_state_version, include_values)
  Store-->>RPC: StateVersionTransitions { epoch, shard, state_version, updates[] }
  RPC->>VN: SyncStateResponse { state_version, updates[], has_more, epoch }

  loop chunked pages for same state_version
    RPC-->>VN: SyncStateResponse (has_more=true/false)
  end

  VN->>Store: SubstateRecord::commit_batch(SubstateUpdateBatch{ epoch, updates })
  VN->>Tree: calculate_state_root(shard_group)
  Tree-->>VN: aggregated_root
  VN-->>VN: validate root vs checkpoint
Loading
sequenceDiagram
  autonumber
  participant Cons as Consensus Worker
  participant Block as Block model
  participant Store as State Store
  participant Tree as ShardedStateTree

  Note over Cons,Block: Commit block with per-shard versions

  Cons->>Tree: calculate_state_root(shard_group)
  Tree-->>Cons: aggregated_root

  alt no state changes / dummy
    Cons->>Block: commit_block_without_state_changes(tx, qc_id)
  else state changes present
    Cons->>Block: commit_block(tx, qc_id, version_updates{Shard->Version})
    Block->>Store: get_substate_updates(tx) -> Vec<SubstateUpdateProof>
    Block->>Store: SubstateRecord::commit_batch(SubstateUpdateBatch)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

A rabbit taps the version drum,
Shards align and updates come. 🥕
Proofs hop in tidy rows,
Roots checked where the river flows.
Batches bundle, syncs take flight—
Ledger hums through day and night.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions

github-actions Bot commented Aug 15, 2025

Copy link
Copy Markdown

Test Results (CI)

418 tests  +6   394 ✅ +6   1h 17m 56s ⏱️ + 1m 46s
 69 suites ±0     0 💤 ±0 
  2 files   ±0    24 ❌ ±0 

For more details on these failures, see this check.

Results for commit fe3bf6f. ± Comparison against base commit e943ac1.

This pull request removes 1 and adds 7 tests. Note that renamed tests count towards both.
tari_ootle_storage ‑ consensus_models::state_transition::tests::to_and_from_bytes
tari_ootle_common_types ‑ shard_state_versions::tests::it_applies_a_bitmap_to_increment_versions
tari_ootle_common_types ‑ shard_state_versions::tests::it_deserializes_if_serialized_vec_is_within_bounds
tari_ootle_common_types ‑ shard_state_versions::tests::it_errors_if_more_then_max_shards
tari_ootle_common_types ‑ shard_state_versions::tests::it_errors_if_serialized_vec_is_empty
tari_ootle_common_types ‑ shard_state_versions::tests::it_errors_if_serialized_vec_is_too_large
tari_ootle_common_types ‑ shard_state_versions::tests::it_gets_by_index
tari_ootle_common_types ‑ shard_state_versions::tests::it_gets_by_shard

♻️ This comment has been updated with latest results.

@sdbondi
sdbondi force-pushed the vn-state-sync-shard-version branch from db2aa59 to 6edb4d9 Compare August 19, 2025 09:52
@sdbondi
sdbondi marked this pull request as ready for review August 19, 2025 09:52
@sdbondi sdbondi changed the title fix(consensus)!: add shard state versions to block header refactor(state_sync)!: sync state transitions by (shard,state_version) Aug 19, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
crates/consensus/src/hotstuff/on_propose.rs (1)

486-493: Extend BlockHeader to carry per-shard state versions

Currently in on_propose.rs we call

let (state_root, _) = calculate_state_merkle_root()?;let mut header = BlockHeader::create_unsigned(, state_root,)?;

but drop the second return value (the per-shard version summary). The BlockHeader struct and its constructors (create, create_unsigned, genesis) have no field or parameter to accept that summary, so shard-state versions never make it into the header.

Required refactors:

  • crates/storage/src/consensus_models/block_header.rs
    – Add a new field (e.g. shard_tree_summary: IndexMap<Shard, TreeRootSummary> or ShardStateVersions) to BlockHeader.
    – Update create, create_unsigned, and genesis signatures to accept this summary.
    – Ensure calculate_id() includes the new field in its hash.
  • crates/consensus/src/hotstuff/on_propose.rs
    – Destructure calculate_state_merkle_root into (state_root, shard_summary) instead of (_, _).
    – Pass shard_summary into the updated create_unsigned call.
  • crates/consensus/src/hotstuff/common.rs (if you want reuse)
    – Convert the returned IndexMap<Shard, PendingShardStateTreeDiff> into a serializable summary type.

Without these changes, per-shard state versions are never committed in the block header.

utilities/db_inspector/src/webserver/handlers/state_transitions.rs (1)

47-53: Prefix filter bug: using a half-open byte range will over-include results

cf.range_iterator(ordering, key_prefix.as_slice()..) scans all keys ≥ prefix, not only those with the prefix. Use the dedicated prefix iterator to bound the scan correctly.

-    let iter = if let Some(prefix_hex) = req.query.as_ref() {
-        let key_prefix = decode_hex_prefix(prefix_hex)?;
-        cf.range_iterator(ordering, key_prefix.as_slice()..)
-    } else {
-        let empty = Vec::<u8>::new();
-        cf.range_iterator(ordering, empty.as_slice()..)
-    };
+    let iter = if let Some(prefix_hex) = req.query.as_ref() {
+        let key_prefix = decode_hex_prefix(prefix_hex)?;
+        cf.prefix_range_iterator_raw_key(ordering, key_prefix)
+    } else {
+        // Full scan
+        cf.iterator(ordering, OPERATION)
+    };
crates/p2p/proto/consensus.proto (1)

83-96: Do not renumber existing fields in BlockHeader; this is wire-incompatible. Add shard_state_versions at a new tag instead.
Moving epoch_hash from 13→12 and extra_data from 14→13 breaks backward compatibility. To add shard state versions, use an unused tag (9 is free) and leave existing tags as-is. Optionally reserve 12 to avoid future confusion.

Apply this diff:

 message BlockHeader {
   bytes parent_id = 1;
   int32 network = 2;
   uint64 height = 3;
   uint64 epoch = 4;
   uint32 shard_group = 5;
   bytes proposed_by = 6;
   bytes state_merkle_root = 7;
   uint64 total_leader_fee = 8;
+  // New: shard-state versions committed in this header. Order MUST be canonical:
+  // [global, shards in ascending order within shard_group].
+  ShardStateVersions shard_state_versions = 9;
   tari.ootle.common.Signature signature = 10;
   uint64 timestamp = 11;
-  bytes epoch_hash = 12;
-  ExtraData extra_data = 13;
+  // Keep original field numbers to preserve wire compatibility
+  bytes epoch_hash = 13;
+  ExtraData extra_data = 14;
+  // (Optional) Reserve tag 12 to prevent accidental reuse
+  reserved 12;
 }

Please also update conversions/bridges to populate this field and document the ordering for consensus verification.

crates/p2p/src/conversions/consensus.rs (1)

468-485: BlockHeader shard_state_versions support is incomplete
The proto, Rust model, and conversion logic currently omit the new shard_state_versions field, so versions will never be sent or received.

Critical changes needed:

  • crates/p2p/proto/consensus.proto:
    · In message BlockHeader { … }, add a ShardStateVersions shard_state_versions = <next_tag>;
  • crates/storage/src/consensus_models/block_header.rs:
    · Add a shard_state_versions: ShardStateVersions field to the BlockHeader struct.
    · Update pub fn create(…) and create_unsigned(…) signatures to accept a ShardStateVersions parameter.
  • crates/p2p/src/conversions/consensus.rs:
    · In impl From<&BlockHeader> for proto::consensus::BlockHeader, set
    shard_state_versions: Some(value.shard_state_versions().into()),
    · In the TryFrom<proto::consensus::BlockHeader> branch (inside try_convert_proto_block_header), pass
    value.shard_state_versions.map(TryInto::try_into).transpose()?
    into the BlockHeader::create(…) call in its correct position.

These updates are required before merging to ensure shard state versions are carried end-to-end.

crates/storage/src/consensus_models/substate.rs (1)

419-421: Compile error: incorrect pattern in is_destroy

SubstateUpdateProof::Destroy is a tuple variant. The pattern Self::Destroy { .. } is invalid and won’t compile. Use Self::Destroy(_).

Apply this diff:

-    pub fn is_destroy(&self) -> bool {
-        matches!(self, Self::Destroy { .. })
-    }
+    pub fn is_destroy(&self) -> bool {
+        matches!(self, Self::Destroy(_))
+    }
🧹 Nitpick comments (75)
crates/state_store_rocksdb/src/dbs/transaction.rs (1)

56-60: Cache bypass for multi_get: consider making it conditional or configurable

Disabling block cache globally for multi_get can hurt workloads that benefit from cache warming/reuse, especially for smaller batches and hot keys. If you have evidence it helps your specific access patterns, consider:

  • Accepting ReadOptions from the call site for fine-grained control, or
  • Making fill_cache(false) conditional on batch size.

Given the current signature, a lightweight alternative is to add a separate method (e.g., multi_get_cf_no_cache) and use it where profiling shows gains.

crates/consensus_tests/src/support/helpers.rs (1)

35-43: Random address generation ignores the end bound; use a uniform sampler over the inclusive range

Current implementation fixes the upper 16 bytes to the range start and randomizes only the lower half. This biases the distribution and ignores range.end(). Suggest sampling uniformly in [start, end] by working in U256 space and mapping a 256-bit random value into the span.

Example refactor:

-// TODO: this biases the start of the shard group
-fn random_substate_address_range(range: RangeInclusive<SubstateAddress>) -> SubstateAddress {
-    let start = range.start();
-    let mut bytes = [0u8; 16];
-    OsRng.fill_bytes(&mut bytes);
-    let mut start = start.into_array();
-    start[16..32].copy_from_slice(&bytes);
-    SubstateAddress::from_bytes(&start).unwrap()
-}
+// Sample uniformly within the inclusive address range
+fn random_substate_address_range(range: RangeInclusive<SubstateAddress>) -> SubstateAddress {
+    let start = range.start().to_u256();
+    let end = range.end().to_u256();
+    debug_assert!(end >= start, "invalid SubstateAddress range");
+    let span = end - start + 1u8.into(); // inclusive
+    // Rejection-free modulo mapping
+    let uniform = loop {
+        let mut r = [0u8; 32];
+        OsRng.fill_bytes(&mut r);
+        // Interpret as big-endian U256
+        let x = tari_u256::U256::from_be_bytes(r);
+        let m = x % span;
+        break start + m;
+    };
+    let bytes = uniform.to_be_bytes();
+    SubstateAddress::from_bytes(&bytes).expect("valid SubstateAddress")
+}

This uses the full range and removes bias while keeping membership guarantees.

crates/storage/src/consensus_models/substate_update_batch.rs (1)

11-14: Add Debug/Clone derives to ease debugging and testing

These types are passed around in tests and storage layers; deriving Debug (and Clone where appropriate) will help logging, assertions, and simple composition without impacting behavior.

-pub struct SubstateUpdateBatch {
+#[derive(Debug, Clone)]
+pub struct SubstateUpdateBatch {
     pub epoch: Epoch,
     pub updates: IndexMap<Shard, SubstateTransitionData>,
 }
@@
-pub struct SubstateTransitionData {
+#[derive(Debug, Clone)]
+pub struct SubstateTransitionData {
     pub state_version: Version,
     pub transitions: Vec<SubstateTransition>,
 }
-pub enum SubstateTransition {
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum SubstateTransition {

Also applies to: 42-55

crates/state_store_tests/src/helpers.rs (2)

128-141: Consider passing epoch into build_substate_record to avoid hardcoded Epoch::zero()

created.at_epoch is always set to zero here. If callers rely on epoch fidelity (as gen_substates does), consider adding an epoch: Epoch parameter or documenting that this helper is only for epoch-agnostic cases.

Example signature change (if you choose to generalize):

-pub fn build_substate_record(substate_id: &SubstateId, version: u32, state_version: Version) -> SubstateRecord {
+pub fn build_substate_record(
+    substate_id: &SubstateId,
+    version: u32,
+    state_version: Version,
+    epoch: Epoch,
+) -> SubstateRecord {
@@
-        created: SubstateCreated {
-            at_epoch: Epoch::zero(),
+        created: SubstateCreated {
+            at_epoch: epoch,
             in_shard: VersionedSubstateIdRef::new(substate_id, version).to_shard(TEST_NUM_PRESHARDS),
             at_state_version: state_version,
         },

165-193: Batch construction looks correct; minor note on cloning

The Up transition uses substate.clone().into_substate_value_or_hash(). In tests this is fine; if this becomes hot-path, consider a zero-copy conversion or moving the value out. As a helper in test code, LGTM as-is.

crates/p2p/proto/rpc.proto (2)

210-213: Document TreeRootSummary semantics

The meaning of state_version is non-obvious (checkpoint version? last applied update?). Add a brief comment to lock down semantics.

Apply this diff to document intent:

 message TreeRootSummary {
   bytes root_hash = 1;
-  uint64 state_version = 2;
+  // The highest committed state version included in this root
+  uint64 state_version = 2;
 }

215-219: Prefer using Epoch type and document inclusive/exclusive semantics

until_epoch switches to a raw uint64 while other messages use tari.ootle.common.Epoch. Use the same type for consistency and document whether it’s inclusive.

Apply this diff:

 message SyncStateRequest {
-  uint64 start_state_version = 1;
-  uint32 shard = 2;
-  uint64 until_epoch = 3;
+  // Start streaming from this state version (inclusive)
+  uint64 start_state_version = 1;
+  uint32 shard = 2;
+  // Stream updates up to and including this epoch; use Epoch(0)/unset to mean "unbounded"
+  tari.ootle.common.Epoch until_epoch = 3;
 }
crates/state_tree/src/lib.rs (1)

21-23: LGTM: clear alias for state-tree payload

The alias is succinct and centralizes the payload type for the state tree.

Optionally re-export SubstateAddress for ergonomics in downstream crates so they don’t have to import it separately:

// At crate root, alongside other pub uses
pub use tari_ootle_common_types::SubstateAddress;
crates/state_store_tests/src/blocks.rs (1)

110-111: Prefer deriving shard_group from zero_block to avoid mismatches

You already consolidated construction via a local shard_group. For even tighter coupling (and to mirror the pattern in missing_transactions.rs), derive it from zero_block.shard_group() so changes to the zero block’s preshards cannot drift from the test’s shard group.

Apply these diffs:

@@
-        let shard_group = ShardGroup::all_shards(NumPreshards::P64);
+        let shard_group = zero_block.shard_group();
@@
-            shard_group,
+            shard_group,
@@
-            shard_group,
+            shard_group,

And similarly in block_query_operations:

@@
-        let shard_group = ShardGroup::all_shards(NumPreshards::P64);
+        let shard_group = zero_block.shard_group();
@@
-            shard_group,
+            shard_group,
@@
-            shard_group,
+            shard_group,
@@
-            shard_group,
+            shard_group,

Also applies to: 118-118, 140-140, 241-242, 249-249, 274-274, 306-306

crates/common_types/src/num_preshards.rs (1)

30-31: Document 1-indexed semantics for MAX_SHARD

Tiny clarity win: make it explicit that shards are 1..=MAX (consistent with all_shards_iter and ShardGroup usage).

Apply this diff:

-    pub const MAX_SHARD: Shard = Shard::from_u32(Self::MAX.as_u32());
+    /// Highest valid shard identifier for the current maximum presharding (1-indexed).
+    pub const MAX_SHARD: Shard = Shard::from_u32(Self::MAX.as_u32());
applications/tari_validator_node/src/p2p/rpc/block_sync_task.rs (1)

332-333: Nit: error messages don’t match the entity (receipts/transactions vs substates)

Adjust the messages to reflect the correct count type for clarity.

Apply this diff:

-                        self.send(Err(RpcStatus::general("number of substates exceeds u32")))
+                        self.send(Err(RpcStatus::general("number of transaction receipts exceeds u32")))
                             .await?;
-                    self.send(Err(RpcStatus::general("number of substates exceeds u32")))
+                    self.send(Err(RpcStatus::general("number of transactions exceeds u32")))
                         .await?;

Also applies to: 355-356

crates/state_store_rocksdb/src/column_families/state_tree_shard_versions.rs (1)

44-50: Add a brief doc comment to ByShard marker

Minor clarity improvement for discoverability in IDEs and docs.

Apply this diff:

-pub struct ByShard;
+/// Query marker for shard-scoped lookups on `StateTreeShardVersionCf`.
+pub struct ByShard;
crates/common_types/src/substate_address.rs (1)

589-594: Duplicate test assertion – remove repetition

The plus_one(address_at(3, 64)) case appears twice with identical assertions. Please remove the duplicate to keep tests lean.

Apply this diff to remove the duplicate block:

-            let group = plus_one(address_at(3, 64)).to_shard_group(NumPreshards::P64, 32);
-            assert_eq!(group.as_range_inclusive(), Shard::from(3)..=Shard::from(4));
-
-            let group = plus_one(address_at(3, 64)).to_shard_group(NumPreshards::P64, 32);
-            assert_eq!(group.as_range_inclusive(), Shard::from(3)..=Shard::from(4));
+            let group = plus_one(address_at(3, 64)).to_shard_group(NumPreshards::P64, 32);
+            assert_eq!(group.as_range_inclusive(), Shard::from(3)..=Shard::from(4));
crates/consensus/src/hotstuff/on_receive_local_proposal.rs (2)

484-486: Nit: comment grammar

“we should to kick into sync” → “we should kick into sync.”

Apply this diff to fix the comment:

-                    // If this shard group remains the same, we can just continue. However, if shard groups change and
-                    // we now manage a shard we have not synced, we should to kick into sync.
+                    // If this shard group remains the same, we can just continue. However, if shard groups change and
+                    // we now manage a shard we have not synced, we should kick into sync.

483-495: HotStuffError::NeedsSync correctly triggers a sync in the running state machine

Verified: in crates/consensus/src/hotstuff/state_machine/running.rs (lines 43–48), errors matching HotStuffError::NeedsSync are caught and mapped to Ok(ConsensusStateEvent::NeedSync), which drives the sync logic as intended.

Optional improvement:

  • Consider emitting a dedicated metric or event alongside the log at this location to monitor shard-group transitions and sync triggers in production.
utilities/db_inspector/src/webserver/handlers/bookkeeping.rs (1)

98-103: Duplicate entry for last_sent_new_view

last_sent_new_view is added twice in this handler. Remove the duplicate to avoid confusing UI output.

Apply this diff to remove the duplicate:

-    add_item(
-        &tx,
-        "last_sent_new_view",
-        column_families::bookkeeping::LastSentNewViewCf,
-        &mut table,
-    )?;
crates/state_store_tests/src/block_diffs.rs (1)

41-41: Be explicit about state_version type to avoid accidental inference drift

Passing a bare 1 relies on type inference for state_version. To guard against future signature or alias changes, consider making the intent explicit (e.g., Version::from(1) or a named constant) if available in scope.

crates/common_types/src/committee.rs (1)

318-321: Use VersionedSubstateIdRef for shard resolution — consider a regression test

Behavior should be unchanged (version-agnostic shard), but this path is critical. Recommend adding a small unit test that asserts includes_substate_id returns identical results for multiple versions of the same SubstateId, guarding against future refactor regressions in to_shard.

I can add a focused test under this module to validate version-independence if you’d like.

crates/state_store_tests/src/state_tree.rs (1)

81-89: gen_nodes now returns Node — consider naming clarity

Implementation is sound. Minor nit: consider renaming the version parameter to state_version to disambiguate from “substate version” used elsewhere.

crates/common_types/src/shard_group.rs (2)

112-115: Add a doc comment and test for shard_iter_with_global

The iterator makes sense (global shard first, then group shards). Two suggestions:

  • Add a doc comment clarifying order and the guarantee that global (0) won’t duplicate any in-group shard.
  • Add a unit test to assert it yields global once followed by all shards in self.shard_iter() order.

Example test sketch (adjust names as needed):

let sg = ShardGroup::new(1, 3);
let shards = sg.shard_iter_with_global().collect::<Vec<_>>();
assert_eq!(shards, vec![Shard::global(), Shard::from(1), Shard::from(2), Shard::from(3)]);

I can add the test under the existing test module if you prefer.


150-152: Const fn returning RangeInclusive — confirm MSRV compatibility

Declaring this as pub const fn is nice, but creating RangeInclusive in a const context depends on compiler support. If your MSRV doesn’t support this fully, consider dropping const to avoid build issues.

Optional fallback:

-pub const fn as_range_inclusive(&self) -> RangeInclusive<Shard> {
+pub fn as_range_inclusive(&self) -> RangeInclusive<Shard> {
     self.start..=self.end_inclusive
 }

Please confirm this compiles under the repository’s MSRV and toolchain.

crates/state_store_rocksdb/src/range.rs (2)

6-14: Derive Debug/Clone and clarify exclusivity in docs

For a public enum used in APIs, deriving Debug/Clone improves ergonomics in logs/tests, and explicit rustdoc avoids ambiguity around bound inclusivity.

Apply:

-/// A subset of RangeBounds that are possible to query in RocksDB.
-pub enum QueryRange<B> {
-    // start..end
-    Exclusive { start: B, end: B },
-    // start..
-    From { start: B },
-    // ..end
-    To { end: B },
-}
+/// A subset of RangeBounds that are possible to query in RocksDB.
+/// Semantics:
+/// - `Exclusive { start, end }` is `start..end` where `start` is inclusive and `end` is exclusive.
+/// - `From { start }` is `start..` (inclusive start).
+/// - `To { end }` is `..end` (exclusive end).
+#[derive(Debug, Clone)]
+pub enum QueryRange<B> {
+    /// start (inclusive) .. end (exclusive)
+    Exclusive { start: B, end: B },
+    /// start (inclusive) ..
+    From { start: B },
+    /// .. end (exclusive)
+    To { end: B },
+}

16-35: Optional: add conversions for more RangeBounds variants if needed

Current From impls cover Range, RangeFrom, and RangeTo. If call sites require RangeInclusive or RangeFull (..), consider adding those for parity with RangeBounds. Otherwise, this is sufficient.

crates/common_types/src/versioned_substate_id.rs (1)

436-441: Add a doc comment for the ref variant’s to_shard

Minor ergonomics: mirror the owned variant’s docs for consistency and discoverability.

-    pub fn to_shard(&self, num_preshards: NumPreshards) -> Shard {
+    /// Calculates and returns the shard for this versioned substate reference.
+    /// If the underlying substate is global, returns `Shard::global()`.
+    pub fn to_shard(&self, num_preshards: NumPreshards) -> Shard {
crates/state_store_tests/src/misc.rs (1)

169-173: IndexMap uses Shard as key
The EpochCheckpoint::new constructor expects an IndexMap<Shard, TreeRootSummary>, so using shard_group.start() (a Shard) is correct. To self-document and avoid inference surprises, add an explicit type annotation:

• In crates/state_store_tests/src/misc.rs around line 169:

-    let mut shard_summary = IndexMap::new();
+    let mut shard_summary: IndexMap<Shard, TreeRootSummary> = IndexMap::new();
     shard_summary.insert(
         shard_group.start(),
         TreeRootSummary {
             root_hash: TreeHash::zero(),
             state_version: 0,
         },
     );
crates/common_types/src/shard.rs (1)

49-56: Clarify None semantics and add edge-case tests for relative_to_shard_group_start

The logic correctly returns None when the shard is not in the group and also for the global shard (via contains() or checked_sub underflow). Consider:

  • Documenting explicitly that global always returns None.
  • Adding unit tests covering:
    • shard == group.start() (returns Some(0))
    • shard == group.end_inclusive (last index)
    • shard outside the group (None)
    • global shard (None)
crates/storage/src/consensus_models/substate_change.rs (1)

102-111: into_transition mapping is correct; consider From implementation for ergonomics

The Up/Down mappings look correct and align with VersionedSubstateId usage for Down. For ergonomics, consider implementing impl From<SubstateChange> for SubstateTransition so callers can use .into() directly.

crates/consensus/src/hotstuff/on_propose.rs (1)

453-455: Shard derivation for burnt UTXOs: OK; consider naming and magic-number nit

Using VersionedSubstateIdRef to derive the shard is consistent with the broader refactor. Minor:

  • Rename id to versioned_id for clarity.
  • Replace the bare 0 with a named constant for initial version to reduce magic numbers.
crates/consensus_tests/src/consensus.rs (1)

1403-1416: Template verification across all validators: solid; minor efficiency nit

The verification loop is correct and robust. Minor nit: hash_template_code(&wasm) is recomputed for each validator; compute once outside the loop and compare to the cached value.

-    for (addr, vn) in test.validators() {
+    let expected_hash = hash_template_code(&wasm);
+    for (addr, vn) in test.validators() {
         let substate_addr = VersionedSubstateId::new(template_id, 0).to_substate_address();
         let template_substate = vn
             .state_store
             .with_read_tx(|tx| SubstateRecord::get(tx, &substate_addr))
             .unwrap_or_else(|e| panic!("Failed to get template substate from {addr}: {e}"));
         let binary_hash = template_substate
             .substate_value
             .unwrap()
             .into_template()
             .expect("Expected template substate")
             .binary_hash;
-        assert_eq!(binary_hash, hash_template_code(&wasm), "Template binary does not match");
+        assert_eq!(binary_hash, expected_hash, "Template binary does not match");
     }
crates/storage/src/consensus_models/block_diff.rs (1)

40-49: into_filtered shard_group parameter

The ShardGroup struct (two Shard fields) isn’t currently marked Copy and is moved into into_filtered(self, shard_group: ShardGroup), but at the sole call site you already construct it via local_committee_info.shard_group(), so there’s no extra clone or move of an existing instance. Since it’s small (likely 16 bytes), passing by value here is fine.

Nit (optional):

  • If you’d like to avoid a move when passing an existing ShardGroup, either
    • derive Copy, Clone on ShardGroup (its fields are already Copy), or
    • change the signature to &ShardGroup.
crates/consensus_tests/src/state_tree.rs (1)

63-82: Transition scan loop is sound; consider early guard to avoid runaway loops in case of DB anomalies

The loop properly advances next_state_version and breaks on epoch boundary. For test robustness, you could add a sanity cap to iterations to avoid a hang if the DB ever returns repeating versions.

-                while let Some(transitions) = tx
+                let mut scan_iters = 0usize;
+                while scan_iters < 10_000 && let Some(transitions) = tx
                     .state_transitions_get_after(shard, next_state_version)
                     .optional()
                     .unwrap()
                 {
+                    scan_iters += 1;
crates/state_store_rocksdb/src/column_families/state_tree.rs (1)

67-74: Query renamed to ByShardStateVersionQuery with composite key — comment nit and codec ordering

The type Key/KeyCodec changes look correct. The inline comment mentions NodeKeyCodec but the code uses NumberCodec; minor mismatch.

Apply this doc fix:

-    // Depends on NodeKeyCodec first serializing the Shard, then the Version.
+    // Depends on ShardCodec serializing the Shard first, then NumberCodec<Version> for the Version.
crates/consensus/src/hotstuff/error.rs (1)

263-275: Nit: fix user-facing error string typos (“an foreign”/“mistmatched”)

Minor grammar in error messages that bubble up in logs and APIs.

-    #[error(
-        "Foreign node submitted an foreign proposal {block_id} that did not contain any transaction evidence for this \
-         node"
-    )]
+    #[error(
+        "Foreign node submitted a foreign proposal {block_id} that did not contain any transaction evidence for this \
+         node"
+    )]
     NoTransactionsInCommittee { block_id: BlockId },
-    #[error("Foreign node submitted an foreign proposal {block_id} that did not contain a sidechain ID")]
+    #[error("Foreign node submitted a foreign proposal {block_id} that did not contain a sidechain ID")]
     MissingSidechainId { block_id: BlockId },
-    #[error("Foreign node submitted an foreign proposal {block_id} with an invalid sidechain ID: {reason}")]
+    #[error("Foreign node submitted a foreign proposal {block_id} with an invalid sidechain ID: {reason}")]
     InvalidSidechainId { block_id: BlockId, reason: String },
-    #[error(
-        "Foreign node submitted an foreign proposal {block_id} with a mistmatched sidechain ID: expected \
+    #[error(
+        "Foreign node submitted a foreign proposal {block_id} with a mismatched sidechain ID: expected \
         {expected_sidechain_id} but got {sidechain_id}"
     )]
crates/state_store_rocksdb/src/cf_api.rs (3)

166-171: Unify key encoding and error messaging in multi_get

Use the existing encode_key helper for consistency (it already provides a detailed panic message and centralizes codec use).

-        let keys = keys.map(|k| {
-            // We don't support key encoding failing here for mem allocation reasons. Generally, key encoding is
-            // infallible so we should evaluate whether to change the codec to be infallible. If key encoding on the
-            // database level ever fails a crash is reasonable.
-            let key = self.key_codec.encode(k.borrow()).expect("Failed to encode key");
-            (self.handle, key)
-        });
+        let keys = keys.map(|k| {
+            let key = self.encode_key(k.borrow());
+            (self.handle, key)
+        });

224-242: Nit: incorrect operation label in error mapping

This function is range_iterator, but errors are labeled "range_iterator_with_codecs", which can mislead diagnostics.

-            res.map_err(|e| RocksDbStorageError::RocksDbError {
-                operation: "range_iterator_with_codecs",
+            res.map_err(|e| RocksDbStorageError::RocksDbError {
+                operation: "range_iterator",
                 source: e,
             })

456-457: Doc clarity: specify half-open semantics

The term “exclusive” is ambiguous. Clarify that ranges are half-open [start, end) and that From/To map to [start, ∞)/(-∞, end).

-    /// Returns a decoded key value iterator over the range of keys (exclusive).
+    /// Returns a decoded key-value iterator over a half-open range. For Exclusive, the end is exclusive: [start, end).
+    /// From is [start, ∞) and To is (-∞, end).
utilities/db_inspector/src/webserver/handlers/state_transitions.rs (2)

55-72: Pagination/total mismatch after per-transition expansion

You paginate by KVs (skip/take on the CF iterator) but emit multiple rows per KV. This can inflate page sizes unpredictably and makes total_entries = cf.count(OPERATION) misleading (it counts KVs, not rows). Consider paginating by transitions or at least reflect total as “KV entries” in the UI.

I can propose a lightweight approach to either:

  • compute a capped transition count for the current page to honor the requested limit, or
  • adjust the UI to clarify totals and page sizes are per KV, not per transition.

63-71: Nit: build row id without intermediate string allocations

Minor readability/alloc nit.

-                "id": hex::encode(&encoded_key) + &format!("-{}", i),
+                "id": format!("{}-{}", hex::encode(&encoded_key), i),
crates/storage/src/consensus_models/block_header.rs (1)

193-206: Reuse the local shard_group to avoid drift

You compute shard_group locally, but still inline ShardGroup::all_shards(num_preshards) when constructing the QC. This duplication risks mismatches if the computation ever changes. Reuse the local variable.

Apply:

-            justify_id: ProposalCertificate::genesis(Epoch::zero(), ShardGroup::all_shards(num_preshards))
+            justify_id: ProposalCertificate::genesis(Epoch::zero(), shard_group)
                 .calculate_id(),
crates/consensus/src/hotstuff/block_change_set.rs (2)

145-157: Reasonable memory cap handling for block diff vector

Clearing and shrinking capacity behind a threshold is a sensible compromise. Consider logging the post-shrink capacity to confirm effectiveness under load, but not required.


448-451: Use accessor consistently for block_id

Elsewhere you use self.block.block_id(). For consistency and to avoid potential visibility issues (in case field visibility changes), use the accessor here too.

-        BlockDiff::insert(tx, &self.block.block_id, &self.local_substate_changes)?;
+        BlockDiff::insert(tx, self.block.block_id(), &self.local_substate_changes)?;
crates/state_store_tests/src/state_transitions.rs (2)

29-37: Minor type-style nit: avoid as cast for Version

You use 1 as Version here but rely on type inference for 2 and 3 later. Prefer consistent style. If Version is a numeric alias, inference is fine; otherwise, wrap/construct via an explicit constructor.

-    shards.insert(
-        1 as Version,
+    shards.insert(
+        1,
         (
             substates.len(),
             substates.iter().map(|s| s.shard()).collect::<HashSet<_>>(),
         ),
     );

29-71: Name shadowing: shards map vs shards set

You use shards for both the outer HashMap and the inner HashSet. Consider renaming the inner binding (e.g., shard_set) for readability.

applications/tari_indexer/src/event_scanner.rs (1)

312-346: Graceful handling of pruned UP substates

Warning on missing values is useful. Two small suggestions:

  • Include shard/version context in the warning to aid diagnosis.
  • Consider optionally skipping DB insert when value is absent, if your consumers don’t benefit from empty rows.

For richer logs (if feasible in this scope), include shard/version:

-                warn!(
-                    target: LOG_TARGET,
-                    "⚠️ Received UP substate {} without value. This indicates that the substate has been pruned. Some event data is not available.", create.substate.as_versioned_substate_id_ref(),
-                );
+                warn!(
+                    target: LOG_TARGET,
+                    "⚠️ Received UP substate {} without value (v{}). Likely pruned; some event data unavailable.",
+                    create.substate.substate_id, create.substate.version
+                );
crates/consensus/src/hotstuff/substate_store/sharded_state_tree.rs (2)

124-131: calculate_state_root: Good addition; minor pre-allocation nit.
To avoid re-allocations, pre-allocate the map since group size is known.

Apply this diff:

-        let mut shard_state_roots = HashMap::new();
+        let mut shard_state_roots = HashMap::with_capacity(shard_group.len() + 1);

183-200: Returning an unordered HashMap may lead to non-determinism; prefer a stable order for shard versions.
Given these versions are destined for the block header, ordering must be canonical. Either:

  • Return an ordered map (IndexMap or BTreeMap), or
  • Guarantee that the caller sorts by shard before serializing.

I recommend returning IndexMap for deterministic ordering by insertion (e.g., ascending shard).

Apply this diff:

-    ) -> Result<HashMap<Shard, Version>, StateTreeError> {
+    ) -> Result<IndexMap<Shard, Version>, StateTreeError> {
@@
-        let mut state_versions = HashMap::with_capacity(diffs.len());
+        let mut state_versions = IndexMap::with_capacity(diffs.len());
@@
-        Ok(state_versions)
+        Ok(state_versions)

And please confirm at call sites that the versions are serialized in a canonical shard order (e.g., global first, then ascending shards within shard_group). If not, I can help patch those.

crates/state_store_tests/src/substates.rs (1)

40-48: State-versioned test records: sanity check.
Both substate1 (v0) and substate1b (v1) are created at the same at_state_version (1). If this is intentional to model multiple substate versions in the same shard-state version, all good. Otherwise, consider setting substate1b's at_state_version to 2 for clearer causality.

If desired, I can push a small patch to make the versions strictly monotonic.

crates/common_types/src/shard_state_versions.rs (3)

18-21: Typo in documentation ("forth" -> "fourth")

Fix the minor typo in the doc comment.

Apply this diff:

-/// version for shard 1, third is shard 2, and forth is shard 3.
+/// version for shard 1, third is shard 2, and fourth is shard 3.

36-38: Misleading expect message in genesis()

The message says "Empty vec should always be valid" but the vector is non-empty by construction (len = shard_group.len() + 1).

Apply this diff:

-                .expect("Empty vec should always be valid"),
+                .expect("Non-empty version vec should always be valid"),

93-103: apply_bitmap() should not panic on length mismatch

Panicking on user-provided bitmap length makes this API footgunny. Prefer returning a Result and documenting the contract. Also, taking &[bool] avoids an unnecessary allocation and allows callers to reuse buffers.

Suggested signature and early return:

-    pub fn apply_bitmap(mut self, bitmap: Vec<bool>) -> Self {
-        if self.len() != bitmap.len() {
-            panic!("Length mismatch: expected {} but got {}", self.len(), bitmap.len());
-        }
+    pub fn apply_bitmap(mut self, bitmap: &[bool]) -> Result<Self, BoundedVecOutOfBounds> {
+        if self.len() != bitmap.len() {
+            return Err(BoundedVecOutOfBounds::LowerBoundError { expected: self.len(), received: bitmap.len() });
+        }
         let inner_mut: &mut [u64] = self.inner.as_mut();
-        for (i, _) in bitmap.into_iter().enumerate().filter(|(_, v)| *v) {
+        for (i, _) in bitmap.iter().enumerate().filter(|(_, v)| **v) {
             inner_mut[i] += 1;
         }
-        self
+        Ok(self)
     }

Note: Adjust call sites and tests accordingly.

crates/p2p/src/conversions/consensus.rs (1)

969-986: Substate.created metadata is now required (breaking on older peers)

Ok for a breaking change, but make sure all producers (internal/external) set created in proto::consensus::Substate. Otherwise, decoding will fail at runtime.

Consider a transitional period by accepting both legacy fields and new metadata if backward compatibility is needed. Do you want help adding a compatibility decoder?

crates/storage/src/consensus_models/epoch_checkpoint.rs (3)

75-81: get_shard_state_version returns 0 for missing entries

Returning a default Version (0) might be ambiguous vs. "explicitly version 0". Consider returning Option to distinguish absent entries from version zero.

If you want to keep the current API, at least document the "0 means missing" convention in the method doc.


141-149: Length validation checks capacity but not membership

You guard against "too many" entries but allow out-of-group shard keys. This is acceptable since compute_state_merkle_root only uses group/global, but you may want to warn or reject unknown shards for stricter validation.

I can add a membership check that errors on keys not equal to global or within header shard_group.


176-183: Display label is stale ("count(shard_roots)")

Update the label to reflect shard_tree_summary.

Apply this diff:

-            "EpochCheckpoint: block_id={}, epoch={}, count(shard_roots)={}",
+            "EpochCheckpoint: block_id={}, epoch={}, count(shard_tree_summary)={}",
crates/consensus/src/hotstuff/substate_store/pending_store.rs (2)

312-313: Stale panic messages after renaming diff -> changes

The expect() messages still say "diff", which is confusing after the rename to changes.

Apply this diff:

-            .map(|&pos| self.changes.get(pos).expect("diff and head are not in sync"))
+            .map(|&pos| self.changes.get(pos).expect("changes and head are not in sync"))
-            .map(|&pos| self.changes.get(pos).expect("diff and head are not in sync"))
+            .map(|&pos| self.changes.get(pos).expect("changes and head are not in sync"))
-            .map(|&pos| self.changes.get_mut(pos).expect("diff and head are not in sync"))
+            .map(|&pos| self.changes.get_mut(pos).expect("changes and head are not in sync"))
-            .map(|&pos| self.changes.get(pos).expect("pending map and diff are out of sync"))
+            .map(|&pos| self.changes.get(pos).expect("pending map and changes are out of sync"))

Also applies to: 356-357, 362-363, 700-701


856-859: API shape: return a slice instead of &Vec for changes()

Returning &[SubstateChange] is more idiomatic and flexible than &Vec<...>.

Apply this diff and adjust call sites:

-    pub fn changes(&self) -> &Vec<SubstateChange> {
-        &self.changes
-    }
+    pub fn changes(&self) -> &[SubstateChange] {
+        &self.changes
+    }
crates/rpc_state_sync/src/state_sync.rs (1)

123-126: Duplicate not-found branch in checkpoint fetch match.

There are two identical Err(RpcError::RequestFailed(err)) if err.is_not_found() arms. The second is dead code and should be removed.

         match client
             .get_checkpoint(GetCheckpointRequest {
                 epoch: prev_epoch.as_u64(),
             })
             .await
         {
             Ok(GetCheckpointResponse {
                 checkpoint: Some(checkpoint),
             }) => match EpochCheckpoint::try_from(checkpoint) {
                 Ok(checkpoint) => {
                     info!(target: LOG_TARGET, "🛜 Checkpoint: {checkpoint}");
                     self.validate_checkpoint(&checkpoint, prev_committee, prev_epoch)?;
                     self.state_store.with_write_tx(|tx| checkpoint.save(tx))?;
                     self.valid_checkpoints.insert(for_shard_group, checkpoint.clone());
                     Ok(Some(checkpoint))
                 },
                 Err(err) => Err(RpcStateSyncError::InvalidResponse(err)),
             },
-            Err(RpcError::RequestFailed(err)) if err.is_not_found() => Ok(None),
             Ok(GetCheckpointResponse { checkpoint: None }) => Ok(None),
             Err(RpcError::RequestFailed(err)) if err.is_not_found() => Ok(None),
             Err(err) => Err(err.into()),
         }
crates/p2p/src/conversions/rpc.rs (1)

146-153: Minor message wording: “shard roots” -> “shard_tree_summary”.

The DoS guard now checks shard_tree_summary, but the error string still says “shard roots”.

-        if value.shard_tree_summary.len() > 100_000 {
-            return Err(anyhow!("too many shard roots (num={})", value.shard_tree_summary.len()));
-        }
+        if value.shard_tree_summary.len() > 100_000 {
+            return Err(anyhow!(
+                "too many shard_tree_summary entries (num={})",
+                value.shard_tree_summary.len()
+            ));
+        }
crates/state_store_rocksdb/src/column_families/state_transition.rs (1)

32-37: Data model LGTM; consider whether state_version duplication is intentional

The V1 record includes state_version while the CF key is (Shard, Version). If the in-value state_version is only for redundancy/self-describing records, fine. If not strictly required, dropping it would reduce storage and avoid potential inconsistencies.

If you decide to remove duplication:

 pub struct StateTransitionModelDataV1 {
-    pub epoch: Epoch,
-    pub transitions: Vec<StateTransitionRecordData>,
-    pub state_version: Version,
+    pub epoch: Epoch,
+    pub transitions: Vec<StateTransitionRecordData>,
 }

And adjust writer/reader code that accesses this field accordingly.

crates/storage/src/consensus_models/block.rs (5)

83-87: Unused BlockError variant

InvalidShardStateVersions isn’t used in this file. Either wire it into commit path validation or remove it to avoid dead code.

-    #[error("Invalid shard state versions: {details}")]
-    InvalidShardStateVersions { details: String },
+    // #[error("Invalid shard state versions: {details}")]
+    // InvalidShardStateVersions { details: String },

Or have commit_block use this variant when shard versions are missing/mismatched.


588-595: Guard commit_block_without_state_changes against accidental diffs

This helper unconditionally calls commit_block with an empty version map. If a diff exists (unexpectedly), commit_block will error due to missing shard versions. Add a defensive check so this path is strictly “no state changes”.

 pub fn commit_block_without_state_changes<TTx>(&self, tx: &mut TTx, commit_qc_id: &QcId) -> Result<(), StorageError>
 where
     TTx: StateStoreWriteTransaction + Deref,
     TTx::Target: StateStoreReadTransaction,
 {
-    self.commit_block(tx, commit_qc_id, &HashMap::new())
+    // Early guard: ensure there are no changes to commit
+    if self.get_diff(&**tx).optional()?.map_or(false, |d| d.len() > 0) {
+        return Err(StorageError::QueryError {
+            reason: format!(
+                "commit_block_without_state_changes called but block {} has substate changes",
+                self
+            ),
+        });
+    }
+    self.commit_block(tx, commit_qc_id, &HashMap::new())
 }

Also consider renaming to commit_header_only to better convey behavior.


617-626: Optional: The None-path likely never triggers with RocksDB reader returning empty diffs

Reader.block_diffs_get returns a BlockDiff even when there are zero changes. The optional()? branch implying NotFound may never occur, making this early return unreachable. Consider simplifying to a length check instead.

-        let Some(block_diff) = self.get_diff(&**tx).optional()? else {
-            info!(target: LOG_TARGET, "🌳 COMMIT block {} with no substate change(s)", self);
-            // No diff to commit
-            return Ok(());
-        };
+        let block_diff = self.get_diff(&**tx)?;
+        if block_diff.len() == 0 {
+            info!(target: LOG_TARGET, "🌳 COMMIT block {} with no substate change(s)", self);
+            return Ok(());
+        }

637-681: Validate shard-version coverage up-front and improve error message

Currently, version lookups happen per change and produce a DataInconsistency error with “NEVER HAPPEN” in the message. Pre-validate once, and use a neutral error message.

-        let updates = changes.into_iter()
+        // Pre-validate that a state version is provided for every shard present in this block’s changes
+        {
+            use std::collections::HashSet;
+            let shards_in_block: HashSet<Shard> = changes.iter().map(|c| c.shard()).collect();
+            let missing: Vec<_> = shards_in_block
+                .iter()
+                .filter(|s| !version_updates.contains_key(*s))
+                .copied()
+                .collect();
+            if !missing.is_empty() {
+                return Err(StorageError::DataInconsistency {
+                    details: format!(
+                        "Missing shard state version(s) for shard(s) {} in block {}",
+                        missing.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(", "),
+                        self.id()
+                    ),
+                });
+            }
+        }
+
+        let updates = changes.into_iter()
             .filter(|change| {
                 if self.shard_group().contains_or_global(&change.shard()) {
                     true
                 } else {
                     // This should have been filtered out already, but just in case
                     warn!(
                     target: LOG_TARGET,
                     "❓️ Skipping substate change {} for shard {} in block {} because it is not in the shard group {}",
                     change.as_change_string(),
                     change.shard(),
                     self.id(),
                     self.shard_group()
                 );
                     false
                 }
             })
             // Group by shard
             .try_fold(IndexMap::new(), |mut acc, change| {
-                let Some(state_version) =
-                    version_updates
-                        .get(&change.shard())
-                        .copied() else {
-                    // A panic may be more appropriate here, this should never happen
-                    return Err(StorageError::DataInconsistency {
-                        details: format!(
-                            "NEVER HAPPEN: Shard state version for shard {} not found in block {}",
-                            change.shard(),
-                            self.id()
-                        ),
-                    });
-                };
+                let state_version = *version_updates
+                    .get(&change.shard())
+                    .expect("prevalidated: state version exists for shard");

Remove “NEVER HAPPEN” wording from end-user messages.


789-842: Block sync update proofs are coherent; slight readability tweak possible

The Up/Down mapping to SubstateUpdateProof looks correct. Minor readability nit: consider renaming local variable substate in the zip loop to substate_rec to avoid confusion with the concept of “substate” elsewhere.

-        for substate in substates {
+        for substate_rec in substates {
-            if substate.is_destroyed() {
+            if substate_rec.is_destroyed() {
                 ...
-                updates.push(SubstateUpdateProof::Destroy(SubstateDestroyedProof {
-                    substate_id: substate.substate_id.clone(),
-                    version: substate.version,
+                updates.push(SubstateUpdateProof::Destroy(SubstateDestroyedProof {
+                    substate_id: substate_rec.substate_id.clone(),
+                    version: substate_rec.version,
                 }));
             } else {
                 updates.push(SubstateUpdateProof::Create(SubstateCreatedProof {
-                    substate: substate.into(),
+                    substate: substate_rec.into(),
                 }));
             };
         }
crates/state_store_rocksdb/src/reader.rs (4)

1592-1601: “OPERATION” label mismatch

The operation string is “state_transitions_get_n_after” but the method is state_transitions_get_after. Align the label for consistency in logs/metrics.

-        const OPERATION: &str = "state_transitions_get_n_after";
+        const OPERATION: &str = "state_transitions_get_after";

1620-1643: Avoid shadowing and improve readability in transitions loop

The loop variable data shadows the outer data binding. Rename the inner binding to rec for clarity.

-        for (data, substate) in data.transitions.iter().zip(substates) {
-            let update = match data.transition {
+        for (rec, substate) in data.transitions.iter().zip(substates) {
+            let update = match rec.transition {
                 StateTransitionType::Up => {
                     ...
-                    SubstateUpdateProof::Create(SubstateCreatedProof {
+                    SubstateUpdateProof::Create(SubstateCreatedProof {
                         substate: SubstateData {
                             substate_id: substate.substate_id,
                             version: substate.version,
                             value,
                         },
                     })
                 },
                 StateTransitionType::Down => ...
             };
             updates.push(update);
         }

1660-1671: Convenience helper for version-scoped node reads looks good

The query and mapping to (NodeKey, Node) is clear. Consider documenting ordering guarantees (if any).


1680-1716: Avoid panics in shard-group version reads; return errors instead

There are a few expect calls that will panic on unexpected DB state. Prefer returning StorageError to avoid crashing a node on malformed data.

-        let index =
-            ShardStateVersions::shard_to_index(shard_group, sg_shard).expect("sg_shard must be in shard group");
+        let Some(index) = ShardStateVersions::shard_to_index(shard_group, sg_shard) else {
+            return Err(StorageError::DataInconsistency {
+                details: format!("Shard {sg_shard} not in shard group {shard_group}"),
+            });
+        };
...
-        let index = ShardStateVersions::shard_to_index(shard_group, shard)
-            .expect("BUG: we checked the end of the shard group, so shard must be in shard group");
+        let Some(index) = ShardStateVersions::shard_to_index(shard_group, shard) else {
+            return Err(StorageError::DataInconsistency {
+                details: format!("Shard {shard} not in shard group {shard_group}"),
+            });
+        };
...
-        let shard_tree_versions =
-            ShardStateVersions::from_vec(shard_tree_versions).expect("BUG: more shard tree versions than shards");
+        let shard_tree_versions = ShardStateVersions::from_vec(shard_tree_versions).map_err(|_| {
+            StorageError::DataInconsistency {
+                details: "More shard tree versions than shards".to_string(),
+            }
+        })?;
crates/state_store_rocksdb/src/writer.rs (1)

1197-1264: Batch commit implementation is coherent; minor cleanup and head index consistency

  • Good: per-shard batching with a single StateTransitionModelDataV1 write keyed by (Shard, Version).
  • Nit: re-fetching HeadIndex CF in the Down branch despite having head_cf already.
-                        db.cf(substate::HeadIndex)?.put(
+                        head_cf.put(
                             &substate.substate_id,
                             &SubstateHeadData {
                                 version: substate.version(),
                                 is_up: false,
                             },
                             OPERATION,
-                        )?;
+                        )?;

Also consider skipping StateTransitionCf writes if transitions is empty (defensive), although upstream grouping currently prevents that.

applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs (4)

52-66: End-epoch early stop drops the fetched batch without notifying client

When end_epoch is set and the fetched transitions.epoch > end_epoch, the stream returns Ok(()) without sending a terminal response. If clients interpret has_more == false as “end of stream,” they won’t receive that signal here and may hang, depending on their expectations.

  • If the protocol requires a terminal frame, consider sending a final response (with has_more: false) indicating the reason (end-epoch reached) before returning.
  • Alternatively, document that the server may close the stream without a terminal frame once end-of-range is reached.

64-66: Version progression logic is correct but relies on inclusive “get_after” semantics

Incrementing to transitions.state_version + 1 is correct if get_for_shard returns the first record with version >= current_state_version. From the RocksDB reader, this is true. Consider a brief comment to lock in this assumption, since changing the storage query to strictly “after” would otherwise skip versions.

Apply this diff to document the inclusive semantics:

-                    current_state_version = transitions.state_version + 1;
+                    // Note: get_for_shard returns the first record with version >= current_state_version.
+                    // Advance to the next version to avoid re-fetching the same record.
+                    current_state_version = transitions.state_version + 1;

109-131: Chunking looks good; clarify has_more semantics and remove stale commented code

  • has_more is set per chunk within a single state_version. If clients treat has_more == false as “no more items in the overall stream,” this will be misleading as further versions may still be streamed. Confirm consumer expectations. If necessary, rename to has_more_in_version in proto, or add an overall termination flag in a separate field/message.
  • The commented block (Lines 110-118) should be removed.

Apply this diff to clean up comments:

-        // let updates = transitions.updates.into_iter().map(Into::into).collect();
-        // self.send(Ok(SyncStateResponse {
-        //     state_version: transitions.state_version,
-        //     updates,
-        //     has_more: false,
-        //     epoch: Some(transitions.epoch.into()),
-        // }))
-        // .await?;

76-76: Minor log tweak for readability

Add a space after the emoji for consistency with other logs.

Apply this diff:

-                    info!(target: LOG_TARGET, "🌍sync complete ({}). {} update(s) sent.", current_state_version, counter);
+                    info!(target: LOG_TARGET, "🌍 sync complete ({}). {} update(s) sent.", current_state_version, counter);
crates/storage/src/state_store/mod.rs (1)

593-595: Typo: “Diagonstics” -> “Diagnostics”

Small spelling correction in the comment.

Apply this diff:

-    // -------------------------------- Diagnotics -------------------------------- //
+    // -------------------------------- Diagnostics -------------------------------- //
crates/storage/src/consensus_models/substate.rs (1)

464-468: Consider adding parity From<SubstateDestroyedProof> conversion

You added From<SubstateCreatedProof> for SubstateUpdateProof but not the symmetric From<SubstateDestroyedProof>. Adding it improves ergonomics.

Apply this diff:

 impl From<SubstateCreatedProof> for SubstateUpdateProof {
     fn from(value: SubstateCreatedProof) -> Self {
         Self::Create(value)
     }
 }
+
+impl From<SubstateDestroyedProof> for SubstateUpdateProof {
+    fn from(value: SubstateDestroyedProof) -> Self {
+        Self::Destroy(value)
+    }
+}

Comment thread crates/common_types/src/shard_state_versions.rs
Comment thread crates/common_types/src/versioned_substate_id.rs
Comment thread crates/consensus_tests/fixtures/block_with_dummies.json
Comment thread crates/consensus_tests/src/support/harness.rs
Comment thread crates/consensus/src/hotstuff/substate_store/pending_store.rs
Comment on lines +67 to 69
Self::HighestSeenBlock => 10,
Self::LastSentNewView => 11,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential on-disk data corruption: key re-numbering without a migration

Reassigning byte keys changes where existing values are stored (orphaning old values and reading wrong ones) for existing databases. Previously, PreviousEpochStateRoot=10, HighestSeenBlock=11, LastSentNewView=12 (per PR context). This change shifts HighestSeenBlock to 10 and LastSentNewView to 11, which will misread prior data unless you provide a migration that re-keys persisted entries or you keep ordinal stability.

Two options:

  • Keep ordinal stability (preferred) by leaving gaps and preserving the previous numbers.
  • Add an explicit DB migration step that reads old keys and writes them to the new ordinals, bumping DatabaseMigrationVersion.

If keeping ordinal stability, apply:

-            Self::HighestSeenBlock => 10,
-            Self::LastSentNewView => 11,
+            // 10 reserved for removed PreviousEpochStateRoot to preserve ordinal stability
+            Self::HighestSeenBlock => 11,
+            Self::LastSentNewView => 12,

If opting for a migration, ensure it:

  • Iterates all column family entries for keys 11 and 12 (old mapping),
  • Moves them to the new ordinals,
  • Clears old entries,
  • Bumps and checks DatabaseMigrationVersion.
🤖 Prompt for AI Agents
In crates/state_store_rocksdb/src/column_families/bookkeeping.rs around lines
67-69, the enum ordinal values were changed and that will re-number on-disk keys
and corrupt existing DBs; restore ordinal stability by preserving the previous
numeric assignments (set HighestSeenBlock back to 11 and LastSentNewView back to
12, leaving any gaps used previously) so existing data continues to map to the
same column family keys; if you cannot preserve ordinals, instead implement a DB
migration routine that (1) reads entries from the old ordinals (11 and 12), (2)
writes them to the new ordinals, (3) deletes the old keys, and (4)
increments/checks DatabaseMigrationVersion before and after migration.

Comment thread crates/state_store_rocksdb/src/dbs/transaction.rs
Comment thread crates/storage/src/consensus_models/state_transition.rs
Comment thread crates/storage/src/consensus_models/substate_update_batch.rs Outdated
Comment thread crates/storage/src/consensus_models/substate.rs
@sdbondi
sdbondi force-pushed the vn-state-sync-shard-version branch from 6edb4d9 to 3b6bde5 Compare August 19, 2025 10:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/p2p/src/conversions/consensus.rs (1)

964-986: Remove legacy created_* fields across the codebase

There are still call sites and definitions referencing the old created_* fields instead of the new created: SubstateCreatedMetadata and destroyed: SubstateDestroyedMetadata. Please update or remove these to align with the updated proto and conversion logic:

• utilities/db_inspector/src/webserver/handlers/tables.rs
– Column::new("created_by_shard", …) and Column::new("created_at_epoch", …)
• crates/p2p/src/conversions/rpc.rs:44
– commented‐out created_justify field
• crates/p2p/proto/rpc.proto:150–151
– legacy created_justify in message SubstateCreatedProof
• bindings/src/types/SubstateRecord.ts:13–16
– properties created_justify, created_block, created_by_shard, created_at_epoch
• applications/tari_validator_node/openrpc.json:2388, 2390
created_justify / destroyed_justify examples

These must be removed or migrated to use the new metadata messages.

♻️ Duplicate comments (9)
crates/state_store_rocksdb/src/column_families/bookkeeping.rs (1)

67-68: Do not renumber on-disk keys: preserve ordinals or add a migration

Changing HighestSeenBlock to 10 and LastSentNewView to 11 will misread existing databases (old 10 was PreviousEpochStateRoot), potentially causing decode errors or silent corruption. Preserve ordinal stability or ship a migration that re-keys persisted entries.

Preferred fix: keep old ordinals and leave 10 reserved.

Apply:

             Self::HighTc => 9,
-            Self::HighestSeenBlock => 10,
-            Self::LastSentNewView => 11,
+            // 10 reserved for removed PreviousEpochStateRoot to preserve ordinal stability
+            Self::HighestSeenBlock => 11,
+            Self::LastSentNewView => 12,

If you must renumber, implement a migration that:

  • Reads old keys at 11 (HighestSeenBlock) and 12 (LastSentNewView),
  • Writes them to the new ordinals (10 and 11),
  • Deletes the old keys,
  • Bumps and validates DatabaseMigrationVersion.

I can draft the migration scaffold and tests if you want.

crates/p2p/proto/consensus.proto (1)

105-107: ShardStateVersions is defined but not committed anywhere. Add it to BlockHeader.

PR objective is to commit shard state versions in the block header. Defining the message without a field on BlockHeader does not achieve this. Add a field (tag 9 is available) and document the canonical order so all nodes can deterministically reconstruct and compare.

Apply this diff to BlockHeader (tag 9) and add ordering docs:

 message ShardStateVersions {
-  repeated uint64 versions = 1;
+  // Canonical order: index 0 is the global state/version, followed by versions
+  // for each shard in ascending Shard ID order (0..=MAX_SHARD).
+  repeated uint64 versions = 1;
 }
 
 message BlockHeader {
   bytes parent_id = 1;
   int32 network = 2;
   uint64 height = 3;
   uint64 epoch = 4;
   uint32 shard_group = 5;
   bytes proposed_by = 6;
   bytes state_merkle_root = 7;
   uint64 total_leader_fee = 8;
+  // State version commitments for the global tree and all shards (see canonical order above)
+  ShardStateVersions shard_state_versions = 9;
   tari.ootle.common.Signature signature = 10;
   uint64 timestamp = 11;
   bytes epoch_hash = 13;
   ExtraData extra_data = 14;
 }

Run this script to find and update conversions/validators to handle the new field:

#!/bin/bash
set -euo pipefail
# Locate proto-to-Rust conversions that must include shard_state_versions
rg -n -C2 -e '\b(BlockHeader|from\(\s*proto::consensus::BlockHeader|\bInto<.*BlockHeader\b)' --type=rust
# Find any existing use of ShardStateVersions (should now include references in conversions and consensus)
rg -n -C2 'ShardStateVersions|shard_state_versions'
crates/common_types/src/shard_state_versions.rs (1)

59-74: Incorrect capacity check in shard_to_index (duplicate of prior review)

Comparing shard_group.end() (absolute id) to MAX_SHARDS (capacity) is wrong; it rejects valid groups with large absolute shard ids. Check the group’s length (+1 for global) instead.

Apply:

     pub fn shard_to_index(shard_group: ShardGroup, shard: Shard) -> Option<usize> {
         if shard.is_global() {
             return Some(0);
         }
 
         if !shard_group.contains_or_global(&shard) {
             return None;
         }
-        shard_group.checked_len()?;
-        if shard_group.end().as_u32() as usize > MAX_SHARDS {
-            return None;
-        }
+        // Ensure capacity is not exceeded (including the global shard at index 0)
+        shard_group.checked_len()?;
+        let group_len = shard_group.len();
+        if group_len + 1 > MAX_SHARDS {
+            return None;
+        }
         let index = shard.as_u32().checked_sub(shard_group.start().as_u32())? as usize;
         // + 1 to account for the global shard at index 0
         Some(index + 1)
     }

Recommend adding a test to prevent regression:

#[test]
fn shard_to_index_handles_large_absolute_ids() {
    // Large absolute shard ids but small group size
    let sg = ShardGroup::new(10_000u32, 10_015u32);
    // Versions length must be sg.len() + 1 (global)
    let versions = ShardStateVersions::from_vec(vec![0; sg.len() + 1]).unwrap();
    // Global is index 0
    assert_eq!(ShardStateVersions::shard_to_index(sg, Shard::global()), Some(0));
    // First non-global shard maps to index 1
    assert_eq!(
        ShardStateVersions::shard_to_index(sg, Shard::from(10_000)),
        Some(1)
    );
    // Last non-global shard maps to index sg.len()
    assert_eq!(
        ShardStateVersions::shard_to_index(sg, Shard::from(10_015)),
        Some(sg.len())
    );
}
crates/consensus/src/hotstuff/substate_store/pending_store.rs (1)

860-863: repeat_n is not in std; this won’t compile on stable

Use a stable alternative to initialize the bitmap.

Apply this diff:

-        let shard_group = self.parent_block.shard_group();
-        let mut bitmap = iter::repeat_n(false, shard_group.len() + 1).collect::<Vec<_>>();
+        let shard_group = self.parent_block.shard_group();
+        let mut bitmap = vec![false; shard_group.len() + 1];
crates/storage/src/consensus_models/state_transition.rs (1)

18-43: into_chunks has a correctness bug (infinite loop when len == size, reversed order, possible empty chunk).

As previously noted, split_off(size) leaves self.updates at length size and returns an empty Vec when len == size, causing an infinite loop. It also pushes tail-first, reversing batch order.

Fix by draining from the front until empty; assert size > 0.

Apply this diff:

 impl StateVersionTransitions {
-    pub fn into_chunks(mut self, size: usize) -> Vec<Self> {
-        let num_chunks = self.updates.len().div_ceil(size);
-        let mut chunks = Vec::with_capacity(num_chunks);
-        loop {
-            if self.updates.len() < size {
-                chunks.push(Self {
-                    epoch: self.epoch,
-                    shard: self.shard,
-                    state_version: self.state_version,
-                    updates: self.updates,
-                });
-                break;
-            }
-
-            let chunk = self.updates.split_off(size);
-
-            chunks.push(Self {
-                epoch: self.epoch,
-                shard: self.shard,
-                state_version: self.state_version,
-                updates: chunk,
-            });
-        }
-        chunks
-    }
+    pub fn into_chunks(mut self, size: usize) -> Vec<Self> {
+        assert!(size > 0, "chunk size must be greater than 0");
+        let mut chunks = Vec::with_capacity(self.updates.len().div_ceil(size));
+        let mut updates = std::mem::take(&mut self.updates);
+        while !updates.is_empty() {
+            let take = updates.len().min(size);
+            let chunk_updates: Vec<_> = updates.drain(..take).collect();
+            chunks.push(Self {
+                epoch: self.epoch,
+                shard: self.shard,
+                state_version: self.state_version,
+                updates: chunk_updates,
+            });
+        }
+        chunks
+    }
crates/p2p/proto/rpc.proto (3)

221-226: Field name should be lower_snake_case (“epoch”, not “Epoch”).

Proto field style requires lower_snake_case.

Apply this diff:

 message SyncStateResponse {
   uint64 state_version = 1;
   repeated SubstateUpdate updates = 2;
   bool has_more = 3;
-  tari.ootle.common.Epoch Epoch = 4;
+  tari.ootle.common.Epoch epoch = 4;
 }

102-104: Avoid sentinel “0 means not destroyed”; use presence semantics.

Proto3 supports optional. Using 0 conflates “not destroyed” with “destroyed at version 0”.

Apply this diff (if optional is available in your toolchain):

 message GetSubstateResponse {
   bytes address = 1;
   uint32 version = 2;
   // Encoded Substate
   bytes substate = 3;
   SubstateStatus status = 4;
-  uint64 created_at_state_version = 5;
-  // Optional (i.e. 0 if not destroyed)
-  uint64 destroyed_at_state_version = 6;
+  // State version at which this substate was created
+  uint64 created_at_state_version = 5;
+  // Only present if the substate was destroyed
+  optional uint64 destroyed_at_state_version = 6;
 }

If optional is not usable, prefer google.protobuf.UInt64Value and import wrappers.proto.


207-208: Wire-incompatible type change on tag 2; use a new field number and reserve 2.

Switching the value type on an existing map field breaks compatibility with deployed peers and stored messages.

Apply this diff to preserve backward compatibility:

 message EpochCheckpoint {
   bytes proof = 1;
-  map<uint32, TreeRootSummary> shard_tree_summary = 2;
+  // Do not reuse tag 2: previously shard_roots (map<uint32, bytes>)
+  reserved 2;
+  // New field for the enriched type
+  map<uint32, TreeRootSummary> shard_tree_summary = 3;
 }

Coordinate a protocol/version bump and dual-read (old/new) during rollout if backward compatibility must be maintained.

crates/storage/src/consensus_models/substate.rs (1)

45-47: Bindings/JSON-RPC mismatch: SubstateRecord.created and SubstateDestroyed shapes changed

Rust now exposes created: SubstateCreated and destroyed: Option (with at_epoch/in_shard/at_state_version), removing the old flattened created_* and legacy destroyed fields. TypeScript bindings and JSON-RPC schemas still reference the old fields.

Please update:

  • bindings/src/types/SubstateRecord.ts to have created: SubstateCreated; destroyed: SubstateDestroyed | null; and remove created_justify/created_block/created_by_shard/created_at_epoch.
  • bindings/src/types/SubstateDestroyed.ts to match { at_epoch: Epoch; at_state_version: number } (or equivalent).
  • Any OpenRPC/JSON schemas and consumers referencing the removed fields.

To locate stale references:

#!/bin/bash
rg -n -C2 --glob bindings --glob applications '\bcreated_(justify|block|by_shard|at_epoch)\b|SubstateDestroyed\b.*(justify|by_block|by_shard)'

Also applies to: 296-321

🧹 Nitpick comments (32)
crates/state_store_rocksdb/src/column_families/bookkeeping.rs (1)

65-66: Naming mismatch: BookKeepingKey::HighQc vs HighPc value type

Enum variant is HighQc, but the CF value type is HighPc (Line 189). This is easy to misread and causes maintenance confusion even if functionally correct.

Two low-risk options:

  • Add a clarifying comment near as_byte() for HighQc noting it stores HighPc and the ordinal must not change.
  • Or rename the variant to HighPc without changing its ordinal (8) and update references accordingly. If you pick the rename, be careful to keep the ordinal stable.

Example comment:

-            Self::HighQc => 8,
+            // Historical name: key 8 stores HighPc; do not change the ordinal
+            Self::HighQc => 8,

Also applies to: 184-191

crates/storage/src/consensus_models/state_tree_diff.rs (1)

28-30: load duplicates new; delegate to avoid duplication (or remove if unused).

Minor cleanup to keep a single initialization path.

Apply this diff:

-    pub fn load(version: Version, diff: StateHashTreeDiff<StateTreePayload>) -> Self {
-        Self { version, diff }
-    }
+    pub fn load(version: Version, diff: StateHashTreeDiff<StateTreePayload>) -> Self {
+        Self::new(version, diff)
+    }

If load is purely semantic sugar for database reads and is unused, consider removing it altogether.

utilities/db_inspector/src/webserver/handlers/state_transitions.rs (3)

60-64: Avoid repeated hex encoding and string concatenation in the inner loop

Minor allocation/CPU wins: compute the hex once per key and use format! instead of String + format!.

Apply this diff (included in the pagination refactor above, but safe to apply independently):

-        for (i, transition) in data.transitions.into_iter().enumerate() {
+        let key_hex = hex::encode(&encoded_key);
+        for (i, transition) in data.transitions.into_iter().enumerate() {
             let substate = substate_cf.get(&transition.substate_address, OPERATION).optional()?;
             table.add_row(json!({
-                "id": hex::encode(&encoded_key) + &format!("-{}", i),
+                "id": format!("{}-{}", key_hex, i),
                 "epoch": data.epoch,
                 "shard": shard,
                 "state_version": state_version,

61-61: Reduce N+1 reads: cache substate lookups within the page

Each transition triggers a substate_cf.get. For pages with many transitions, this becomes expensive. A small in-memory cache keyed by substate_address will avoid duplicate reads when transitions reference the same substate.

Apply this diff within the loop and add the following supporting code outside the selected range:

-        for (i, transition) in data.transitions.into_iter().enumerate() {
+        for (i, transition) in data.transitions.into_iter().enumerate() {
-            let substate = substate_cf.get(&transition.substate_address, OPERATION).optional()?;
+            let substate = if let Some(v) = substate_cache.get(&transition.substate_address) {
+                v.clone()
+            } else {
+                let v = substate_cf.get(&transition.substate_address, OPERATION).optional()?;
+                substate_cache.insert(transition.substate_address.clone(), v.clone());
+                v
+            };

Support changes required outside the selected range:

// At the top of the file:
use std::collections::HashMap;

// Just before iterating transitions for the current page (e.g., after computing pagination vars):
let mut substate_cache: HashMap<_, Option<_>> = HashMap::new();

If RocksDB wrapper supports multi-get, that would be even better, but the cache is a low-effort improvement.


73-75: Verify total_entries semantics: count now reflects keys, not rows (transitions)

After expanding one key into multiple rows, cf.count(OPERATION) no longer matches the number of rows emitted. If TableResponse.total_entries is used for pagination, this will mislead clients.

Options:

  • Update total_entries to reflect total transitions (requires summing transition counts or maintaining an aggregate counter during writes).
  • If computing the true total is too costly, consider leaving total_entries unset/None for this endpoint, or clearly documenting that the total reflects number of keys, not rows.
  • Alternatively, expose both totals (keys vs. transitions) if the UI needs both.

Confirm how the frontend uses total_entries and adjust accordingly. I can help draft a small aggregation helper if you maintain per-key transition counts during writes.

crates/consensus_tests/src/state_tree.rs (1)

22-22: Avoid hard-coded /tmp paths; prefer per-test tempdirs.

Using a fixed path like “/tmp/test{}” risks collisions and leftover state between runs. Use a tempdir (e.g., tempfile) or the test harness’s per-test directory helper.

Example:

-        .with_rocks_path("/tmp/test{}")
+        .with_temp_rocks_path()

If with_temp_rocks_path doesn’t exist, I can help wire it using tempfile.

applications/tari_validator_node/src/p2p/rpc/service_impl.rs (1)

380-405: sync_state now keyed by (shard, start_state_version) with range checks — LGTM.

  • Shard range validation against NumPreshards::MAX_SHARD.
  • start_state_version > 0 guard prevents ambiguous “from genesis” semantics.
  • Optional end_epoch and NonZeroUsize conversion for batch size are correct.

Consider including the peer address in the info log for easier tracing of sync sessions.

applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs (2)

48-67: Run loop semantics look correct; minor wording/log nits.

  • Good: advances by one state_version per batch and stops at end_epoch.
  • Nits:
    • “Fetched … up to v{}” reads like an inclusive range; since each batch corresponds to a single version, “for v{}” is clearer.
    • Comment still mentions “id”. Rename to “state_version”.

Apply this diff to clarify logs/comments:

-                    info!(target: LOG_TARGET, "🌍 Fetched {} state transition(s) up to v{}", transitions.updates.len(), transitions.state_version);
+                    info!(target: LOG_TARGET, "🌍 Fetched {} update(s) for v{}", transitions.updates.len(), transitions.state_version);
...
-                    //     "State transition not found with id={current_state_version}"
+                    //     "State transitions not found for state_version={current_state_version}"
-                    info!(target: LOG_TARGET, "🌍sync complete ({}). {} update(s) sent.", current_state_version, counter);
+                    info!(target: LOG_TARGET, "🌍 sync complete at v{}. {} update(s) sent.", current_state_version, counter);

110-127: Chunk materialization may be unnecessary; stream as you split.

Calling into_chunks(...).len() materializes the full Vec of chunks to compute has_more. For large batches, consider deriving has_more from the iterator (peekable) to avoid allocating all chunks up front.

Example:

-        let chunks = transitions.into_chunks(self.batch_size.get());
-        let num_chunks = chunks.len();
-        for (i, chunk) in chunks.into_iter().enumerate() {
+        let mut chunks = transitions.into_chunks(self.batch_size.get()).into_iter().peekable();
+        while let Some(chunk) = chunks.next() {
+            let has_more = chunks.peek().is_some();
             let updates = chunk.updates.into_iter().map(Into::into).collect();
             self.send(Ok(SyncStateResponse {
                 state_version: chunk.state_version,
                 updates,
-                has_more: i < num_chunks - 1,
+                has_more,
                 epoch: Some(chunk.epoch.into()),
             }))
             .await?;
         }
crates/state_tree/src/tree.rs (1)

70-80: Internal naming nit: next_version vs version.

calculate_substate_changes takes next_version, but the inner version variable is renamed. For consistency/readability, consider aligning parameter names across the file.

-        current_version: Option<Version>,
-        next_version: Version,
+        current_version: Option<Version>,
+        version: Version,

And update call sites within this file accordingly.

crates/common_types/src/lib.rs (1)

70-71: Public API surface expanded: confirm intent and scope of re-exports

Re-exporting everything from shard_state_versions makes all items public at the crate root. If the intent is to only surface the stable types (e.g., ShardStateVersions and its primary error), consider narrowing the export to avoid accidental API growth.

Two suggestions:

  1. Restrict re-exports to specific items to reduce churn in dependents:
-mod shard_state_versions;
-pub use shard_state_versions::*;
+mod shard_state_versions;
+pub use shard_state_versions::{ShardStateVersions, BoundedVecOutOfBounds};
  1. Since the PR title is marked breaking (“!”) but the PR description says “Breaking Changes: None”, please reconcile this in the PR notes/CHANGELOG if this public API change is intentional. Do you want me to draft a short CHANGELOG entry?
crates/consensus_tests/src/dummy_blocks.rs (1)

28-28: ShardGroup change: verify range/semantics and keep test invariant explicit

You switched to a shared local shard_group = ShardGroup::new(1, 127) and thread it through. Looks good for consistency, but please double-check that starting at 1 (instead of 0) matches the intended shard space slice for these tests and any JSON fixtures that assume a specific group.

To make the intent explicit and catch regressions, add an assertion right after creating genesis:

assert_eq!(genesis.shard_group(), shard_group);

If helpful, I can update the test to include this assertion.

Also applies to: 33-33, 51-51, 66-66

crates/common_types/src/committee.rs (1)

318-321: Use of VersionedSubstateIdRef avoids unnecessary allocation; clarify the “0” sentinel

Good change: computing the shard via a borrowed VersionedSubstateIdRef avoids constructing a SubstateAddress.

Small clarity nit: the literal 0 is a sentinel for “version doesn’t affect shard”. Consider a named constant or a variable name change to avoid confusion with SubstateAddress.

Apply rename for readability:

-        let addr = VersionedSubstateIdRef::new(substate_id, 0);
-        let shard = addr.to_shard(self.num_shards);
+        let vsid = VersionedSubstateIdRef::new(substate_id, 0);
+        let shard = vsid.to_shard(self.num_shards);

If there’s a domain type alias (e.g., StateVersion::GENESIS or similar), using it instead of 0 would be even clearer.

crates/consensus/src/hotstuff/worker.rs (1)

1058-1059: Compute genesis state root only if needed

You compute the shard state root even when the genesis block already exists. This adds unnecessary IO/CPU on startup.

Consider moving the calculate_state_root call after the genesis existence check, computing only when inserting the genesis block. If a pre-check requires a header instance to call exists(), add a separate existence predicate that avoids constructing a full block.

crates/consensus/src/hotstuff/common.rs (1)

263-308: Checkpoint now tracks (root_hash, state_version) per shard; watch duplicate global computation

The overall change to shard_tree_summary is correct. One thing: if shard_group includes the global shard (start == global), the loop will recompute and reinsert the global entry. IndexMap preserves insertion order so this won’t reorder, but it does extra work.

  • Skip the global shard in the loop:
-    for shard in shard_group.shard_iter() {
+    for shard in shard_group.shard_iter() {
+        if shard.is_global() {
+            continue;
+        }
         let Some(version) = tx.state_tree_versions_get_latest(shard)? else {
             // At v0 there have been no state changes
             shard_tree_summary.insert(shard, TreeRootSummary {
                 root_hash: SPARSE_MERKLE_PLACEHOLDER_HASH,
                 state_version: 0,
             });
             continue;
         };
         // ...
     }

Alternatively, after fixing ShardGroup::shard_iter_with_global to avoid duplicates, you could simplify by iterating shard_group.shard_iter_with_global() and dropping the special “global first” block (caps stays correct at len()+1).

crates/storage/src/consensus_models/block_header.rs (1)

195-206: zero_block minor refactor reads better

Local shard_group variable improves readability. The TODO about whether zero block is needed is worth tracking; consider opening an issue to evaluate removal.

I can open an issue to track the “do we need a zero block anymore?” question with pros/cons and migration notes if you’d like.

applications/tari_indexer/src/event_scanner.rs (1)

312-346: Consider widening version field and using compact JSON encoding

  • Verified that NewSubstate.version (and the corresponding version column in the substates table) is currently defined as an i32/SQLite INTEGER. If there’s any chance a substate’s version could exceed i32::MAX, you’ll need to update both the Rust model and your migrations to use i64/BIGINT.
  • By default encode_substate uses serde_json::to_string_pretty, which adds extra whitespace. To reduce storage and I/O overhead, switch to the compact form:
-    fn encode_substate(substate: &SubstateValue) -> Result<String, anyhow::Error> {
-        let pretty_json = serde_json::to_string_pretty(&substate)?;
-        Ok(pretty_json)
-    }
+    fn encode_substate(substate: &SubstateValue) -> Result<String, anyhow::Error> {
+        let json = serde_json::to_string(&substate)?;
+        Ok(json)
+    }

These changes are optional refactors—no blocker if you’re confident versions will stay within 32-bit bounds.

crates/common_types/src/shard_state_versions.rs (2)

34-39: Avoid panicking in genesis on out-of-bounds shard groups

genesis constructs a bounded vec and .expect(...)s; this will panic for oversize groups. Either assert with a clear message or return a Result to propagate the bounded-vec error.

Minimal improvement without changing the signature:

-        Self {
-            inner: BoundedVersionVec::try_from(vec![0; shard_group.len() + 1])
-                .expect("Empty vec should always be valid"),
-        }
+        let len = shard_group.len() + 1;
+        assert!(
+            len <= MAX_SHARDS,
+            "ShardStateVersions::genesis: shard group size ({}) exceeds MAX_SHARDS ({})",
+            len,
+            MAX_SHARDS
+        );
+        Self {
+            inner: BoundedVersionVec::try_from(vec![0; len])
+                .expect("INVARIANT: len checked against bounds"),
+        }

If you can change the API, prefer pub fn try_genesis(...) -> Result<Self, BoundedVecOutOfBounds>.


93-104: apply_bitmap should not panic on length mismatch

Panicking in a common types crate is brittle. Consider a non-panicking variant (e.g., try_apply_bitmap) returning Result<Self, Error>.

Example shape:

pub fn try_apply_bitmap(mut self, bitmap: &[bool]) -> Result<Self, &'static str> {
    if self.len() != bitmap.len() {
        return Err("Length mismatch");
    }
    for (i, inc) in bitmap.iter().copied().enumerate() {
        if inc {
            self.inner.as_mut()[i] += 1;
        }
    }
    Ok(self)
}
crates/storage/src/consensus_models/epoch_checkpoint.rs (1)

179-183: Nit: log label still says count(shard_roots)

Minor naming mismatch in Display; consider updating the label to reflect shard_tree_summary.

Apply this diff:

-            "EpochCheckpoint: block_id={}, epoch={}, count(shard_roots)={}",
+            "EpochCheckpoint: block_id={}, epoch={}, count(shard_tree_summary)={}",
crates/consensus/src/hotstuff/substate_store/pending_store.rs (2)

4-4: Remove unused import of iter

After replacing repeat_n, iter is unused and may trigger warnings or CI failures if unused imports are denied.

Apply this diff:

-use std::{borrow::Cow, collections::HashMap, fmt::Display, iter};
+use std::{borrow::Cow, collections::HashMap, fmt::Display};

307-317: Nit: error messages still reference “diff”

Update panic messages to reference “changes” to align with the renamed field and avoid confusion.

Apply this diff:

-            .map(|&pos| self.changes.get(pos).expect("diff and head are not in sync"))
+            .map(|&pos| self.changes.get(pos).expect("changes and head are not in sync"))
-            .map(|&pos| self.changes.get(pos).expect("diff and head are not in sync"))
+            .map(|&pos| self.changes.get(pos).expect("changes and head are not in sync"))
-            .map(|&pos| self.changes.get(pos).expect("pending map and diff are out of sync"))
+            .map(|&pos| self.changes.get(pos).expect("pending map and changes are out of sync"))

Also applies to: 353-363, 697-705

crates/p2p/src/conversions/rpc.rs (1)

151-153: Nit: error text still refers to “shard roots”.

This path now handles shard_tree_summary. Align the error message for clarity.

Apply this diff:

-        if value.shard_tree_summary.len() > 100_000 {
-            return Err(anyhow!("too many shard roots (num={})", value.shard_tree_summary.len()));
+        if value.shard_tree_summary.len() > 100_000 {
+            return Err(anyhow!(
+                "too many shard tree summary entries (num={})",
+                value.shard_tree_summary.len()
+            ));
crates/storage/src/consensus_models/state_transition.rs (1)

49-56: Clarify inclusive semantics of get_for_shard().

The RocksDB impl returns the first transition at-or-after the given state_version. The name “get_after” suggests exclusive. Consider documenting or renaming for clarity.

Apply a doc comment:

 impl StateTransition {
-    pub fn get_for_shard<TTx: StateStoreReadTransaction>(
+    /// Returns the first set of transitions at or after `state_version` for `shard`.
+    /// If there are no transitions at exactly `state_version`, the next available version is returned.
+    pub fn get_for_shard<TTx: StateStoreReadTransaction>(
crates/storage/src/state_store/mod.rs (1)

278-285: Document include_values semantics on state_transitions_get_after.

Reader currently loads full records even when include_values is false (see TODO). Clarify current behavior and intent to avoid surprises.

Proposed doc:

-    fn state_transitions_get_after(
+    /// Returns transitions at or after `state_version` for `shard`.
+    /// NOTE: For now, values may still be read from storage even if `include_values` is false (implementation detail).
+    fn state_transitions_get_after(
crates/state_store_rocksdb/src/reader.rs (4)

193-193: Nit: fix grammar in doc comment.

“in the pending chain; otherwise an empty list is returned.”

Apply this diff:

-    /// in the pending chain if not an empty list is returned.
+    /// in the pending chain; otherwise an empty list is returned.

1598-1598: Nit: OPERATION label out of date.

The function is state_transitions_get_after; update the trace/metrics label.

Apply this diff:

-        const OPERATION: &str = "state_transitions_get_n_after";
+        const OPERATION: &str = "state_transitions_get_after";

1619-1622: Perf: include_values = false still deserializes full substates.

The TODO is valid; this adds unnecessary IO/CPU under hash-only syncs. Consider a lighter path that fetches only IDs, versions, and state_hash (e.g., a lean index/CF or selective columns).

Happy to sketch a read path that joins HeadIndex with a slim hash CF to avoid decoding full values in the common case.


1689-1695: Avoid potential overflow when computing the end shard in sg_range.

While MAX_SHARDS likely prevents u32::MAX, prefer checked/saturating add to be defensive.

Apply this diff:

-        let sg_range = shard_group.start()..Shard::from(shard_group.end().as_u32() + 1);
+        let end_plus_one = shard_group
+            .end()
+            .as_u32()
+            .checked_add(1)
+            .expect("shard_group.end() + 1 must not overflow");
+        let sg_range = shard_group.start()..Shard::from(end_plus_one);
crates/state_store_rocksdb/src/writer.rs (2)

1239-1246: Minor: reuse head_cf instead of reacquiring CF handle

The Down branch reacquires HeadIndex via db.cf(...) despite head_cf being available, adding unnecessary handle lookups.

Apply:

-                        db.cf(substate::HeadIndex)?.put(
-                            &substate.substate_id,
-                            &SubstateHeadData {
-                                version: substate.version(),
-                                is_up: false,
-                            },
-                            OPERATION,
-                        )?;
+                        head_cf.put(
+                            &substate.substate_id,
+                            &SubstateHeadData {
+                                version: substate.version(),
+                                is_up: false,
+                            },
+                            OPERATION,
+                        )?;

1205-1219: Optional: assert shard consistency for created records

We rely on the provided shard when recording SubstateCreated.in_shard. Add a debug assertion that the computed substate address (or shard) derived from (id, version) matches shard to catch upstream mismatches early.

If SubstateAddress exposes shard(), consider:

  • debug_assert_eq!(address.shard(), shard);

If not, compute from id/version using the same function the rest of the system uses and assert equality.

crates/storage/src/consensus_models/substate.rs (1)

109-114: Nit: fix grammar in comment

"cant" -> "can't". Also consider clarifying: “do not trust this value from untrusted sources; validate locally by deriving the shard from (SubstateId, Version) and current NumPreshards.”

-    /// WARN: you cant trust this if this is deserialized from an untrusted source, this should be validated by locally
-    /// calculating which shard this substate falls.
+    /// WARN: don't trust this if deserialized from an untrusted source. Validate locally by deriving
+    /// the shard from (SubstateId, Version) and the current NumPreshards.

Comment thread crates/common_types/src/shard_group.rs
Comment thread crates/state_store_rocksdb/src/writer.rs
Comment thread crates/state_store_tests/src/substates.rs
Comment thread crates/storage/src/consensus_models/state_tree_diff.rs
Comment thread crates/storage/src/consensus_models/substate.rs
Comment thread utilities/db_inspector/src/webserver/handlers/state_transitions.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
utilities/db_inspector/src/webserver/handlers/state_transitions.rs (1)

47-53: Bound prefix iteration in state_transitions.rs
Using cf.range_iterator(ordering, key_prefix.as_slice()..) with a RangeFrom only sets a lower bound—your loop will continue past the desired prefix and scan the entire CF. To enforce a true prefix‐only scan (and avoid expensive full-table iteration):

  • Replace the raw range_iterator call with the prefix-aware API:
    let iter = if let Some(prefix_hex) = req.query.as_ref() {
        let key_prefix = decode_hex_prefix(prefix_hex)?;
    -   cf.range_iterator(ordering, key_prefix.as_slice()..)
    +   cf.prefix_range_iterator_raw_key(ordering, key_prefix)
    } else {
        let empty = Vec::<u8>::new();
    -   cf.range_iterator(ordering, empty.as_slice()..)
    +   cf.range_iterator(ordering, empty.as_slice()..)
    };
  • If you must stick with range_iterator, add an early break once cf.encode_key(&key).starts_with(&key_prefix) fails.

This change will ensure you only scan keys matching your prefix rather than the entire column family.

crates/storage/src/consensus_models/substate.rs (1)

216-223: Bug: removing from HashSet uses wrong key type; causes a mismatch.

substate_ids is HashSet<VersionedSubstateIdRef<'a>> but remove is called with &SubstateId. This won’t match and will leave entries in “missing”.

Fix by removing with a VersionedSubstateIdRef constructed from the found record:

-        for f in &found {
-            substate_ids.remove(f.substate_id());
-        }
+        for f in &found {
+            let key = VersionedSubstateIdRef::new(f.substate_id(), f.version());
+            substate_ids.remove(&key);
+        }
♻️ Duplicate comments (10)
crates/p2p/proto/rpc.proto (2)

102-104: Avoid sentinel 0; use presence for destroyed_at_state_version + reserve removed field name

Using 0 to mean “not destroyed” is ambiguous on the wire and in generated bindings. Prefer proto3 optional (or wrappers) so absence indicates “not destroyed”. Also reserve any previously-removed field names to prevent accidental reuse.

Apply this diff:

 message GetSubstateResponse {
   bytes address = 1;
   uint32 version = 2;
   // Encoded Substate
   bytes substate = 3;
   SubstateStatus status = 4;
-  uint64 created_at_state_version = 5;
-  // Optional (i.e. 0 if not destroyed)
-  uint64 destroyed_at_state_version = 6;
+  // State version at which this substate was created
+  uint64 created_at_state_version = 5;
+  // Optional; only present if the substate was destroyed
+  optional uint64 destroyed_at_state_version = 6;
+  // Prevent reuse of previously removed field names
+  reserved "quorum_certificates";
 }

If your toolchain doesn’t support optional in proto3, replace the field with google.protobuf.UInt64Value destroyed_at_state_version = 6; and add import "google/protobuf/wrappers.proto"; at the top.


205-208: Changing the type on tag 2 is wire-incompatible; move to a new field number and reserve 2

Switching map<uint32, bytes> to map<uint32, TreeRootSummary> on the same tag breaks compatibility with existing peers and stored messages. Use a new tag and reserve the old one.

If a coordinated network upgrade is already enforced (protocol version gating), reply here and we can downgrade this to documentation.

Apply this diff:

 message EpochCheckpoint {
   bytes proof = 1;
-  map<uint32, TreeRootSummary> shard_tree_summary = 2;
+  // Reserved to avoid wire-incompatible reuse of the old type on tag 2
+  reserved 2;
+  // New field number for the new map value type
+  map<uint32, TreeRootSummary> shard_tree_summary = 3;
 }
crates/common_types/src/versioned_substate_id.rs (1)

75-83: Doc and implementation now correctly handle global substates

Docs now state the global-substate behavior and the code short-circuits to Some(Shard::global()). This resolves the previously raised discrepancy.

bindings/src/types/SubstateDestroyed.ts (1)

4-4: BigInt shape change acknowledged; confirm shard derivation is sufficient for consumers

Switching to { at_epoch, at_state_version: bigint } matches the new model. Since in_shard is omitted here (present in SubstateCreated), confirm consumers can derive shard from substate_id consistently for destruction metadata. Also ensure BigInt handling (serialization/TS target) per the SubstateCreated comment.

crates/state_store_tests/src/substates.rs (1)

40-48: Fix: duplicate state_version for successive versions of the same substate

Both v0 and v1 of substate1 are created at state_version=1. This can produce two transitions for the same (shard, state_version) and the same logical substate across versions, which is semantically inconsistent and risks non-deterministic ordering at commit time. Make the later version occur at the next state_version.

-    let substate1b = build_substate_record(&substate1_id, 1, 1);
+    let substate1b = build_substate_record(&substate1_id, 1, 2);
crates/consensus_tests/src/support/harness.rs (1)

182-187: Good: created.in_shard now matches computed shard

Replacing the hard-coded Shard::first() with the shard derived from the versioned ID fixes metadata inconsistencies flagged earlier.

crates/rpc_state_sync/src/state_sync.rs (1)

242-301: Critical: Stop consuming stream after reaching checkpoint version

After validating and matching the checkpoint state root inside the write transaction, the code returns Ok(()) from the closure but continues the outer while loop, which risks over-sync if a peer streams additional updates. Set a flag inside the closure and break the outer loop afterward.

-    async fn start_state_sync(
+    async fn start_state_sync(
         &mut self,
         client: &mut ValidatorNodeRpcClient,
         shard: Shard,
         checkpoint: &EpochCheckpoint,
         mut maybe_persisted_state_version: Option<Version>,
     ) -> Result<(Option<Version>, Vec<TemplateChange>), RpcStateSyncError> {
         let mut template_changes = vec![];
         let checkpoint_shard_root = checkpoint.get_shard_root(shard);
         let checkpoint_state_version = checkpoint.get_shard_state_version(shard);
+        let mut reached_checkpoint = false;
@@
-            self.state_store.with_write_tx(|tx| {
+            self.state_store.with_write_tx(|tx| {
                 info!(
                     target: LOG_TARGET,
                     "🛜 Next state updates batch of size {} from v{}",
                     updates.len(),
                     state_version
                 );
@@
-                    if state_version == checkpoint_state_version {
+                    if state_version == checkpoint_state_version {
                         if local_state_root != checkpoint_shard_root {
@@
-                        info!(
+                        info!(
                             target: LOG_TARGET,
                             "🛜 ✅ State root for {shard} matches checkpoint: {local_state_root} (v{state_version})",
                         );
 
                         maybe_persisted_state_version = Some(state_version);
                         store.set_state_version(state_version)?;
-                        // Done
-                        return Ok(());
+                        // Mark to break outer loop after tx commits
+                        reached_checkpoint = true;
+                        return Ok(());
                     }
@@
                 Ok::<_, RpcStateSyncError>(())
             })?;
+            if reached_checkpoint {
+                break;
+            }
crates/storage/src/consensus_models/state_transition.rs (1)

20-36: Fix confirmed: chunking now preserves order and avoids empty batches.

This resolves the previous split_off-based reversal/empty-chunk issue by draining from the front until empty. Good use of NonZeroUsize to prevent size=0.

crates/state_store_rocksdb/src/writer.rs (1)

1260-1266: Unpruned downed-values index maintenance added — good.

Recording (epoch, shard, state_version) -> [addresses] ensures prune sees entries to clear values later. This addresses earlier leak concerns.

crates/storage/src/consensus_models/substate.rs (1)

409-421: LGTM: tuple-variant pattern fix applied for is_destroy().

matches!(self, Self::Destroy(_)) is correct for tuple variants.

🧹 Nitpick comments (25)
crates/p2p/proto/rpc.proto (3)

210-213: Add brief docs for TreeRootSummary fields

Clarify what hash and version these represent for easier interop.

 message TreeRootSummary {
-  bytes root_hash = 1;
-  uint64 state_version = 2;
+  // Hash of the shard's state commitment (e.g., tree root) at the given state_version
+  bytes root_hash = 1;
+  // State version corresponding to root_hash
+  uint64 state_version = 2;
 }

215-219: Specify request semantics (inclusive/exclusive, bounds, and defaults)

Document whether start_state_version is inclusive, how until_epoch is applied, and any sentinel values or maximums. This reduces client/server ambiguity.

 message SyncStateRequest {
-  uint64 start_state_version = 1;
-  uint32 shard = 2;
-  uint64 until_epoch = 3;
+  // Inclusive start state version to sync from for the given shard
+  uint64 start_state_version = 1;
+  // Shard identifier whose transitions are requested
+  uint32 shard = 2;
+  // Optional upper bound: fetch updates until (and including?) this epoch.
+  // Clarify whether 0 means "no upper bound".
+  uint64 until_epoch = 3;
 }

If until_epoch is optional, consider optional uint64 until_epoch = 3; (or a wrapper) to avoid sentinel values.


221-226: Clarify state_version meaning in the response and epoch semantics

It’s unclear if state_version is the last applied version in updates, or the “next cursor” the client should request from. Also confirm whether epoch is the epoch of the last update in this page or a watermark.

 message SyncStateResponse {
-  uint64 state_version = 1;
-  repeated SubstateUpdate updates = 2;
-  bool has_more = 3;
-  tari.ootle.common.Epoch epoch = 4;
+  // Cursor semantics: last state version included in updates (or next to request from). Specify explicitly.
+  uint64 state_version = 1;
+  // Batched substate updates for this shard and range
+  repeated SubstateUpdate updates = 2;
+  // True if more updates are available after this page
+  bool has_more = 3;
+  // Epoch context for these updates; clarify whether this is the epoch watermark of the batch
+  tari.ootle.common.Epoch epoch = 4;
 }
crates/state_store_rocksdb/src/column_families/substate.rs (2)

26-26: Alias Version to StateVersion for clarity and consistency with PR semantics

The PR revolves around shard + state_version transitions. Aliasing the imported Version clarifies intent and avoids ambiguity with other “version” concepts.

Apply:

- use tari_state_tree::Version;
+ use tari_state_tree::Version as StateVersion;

And update usages:

-    type Key = (Epoch, Shard, Version);
-    type KeyCodec = (EpochCodec, ShardCodec, NumberCodec<Version>);
+    type Key = (Epoch, Shard, StateVersion);
+    type KeyCodec = (EpochCodec, ShardCodec, NumberCodec<StateVersion>);

Also applies to: 77-78


77-78: Validate key order vs shard+state_version sync path; consider reordering or a secondary index

Your PR objective emphasizes syncing by (shard, state_version). With the key ordered as (Epoch, Shard, Version), prefix/range scans by (shard, version) won’t be optimal. If the hot-path requires shard+version iteration, please confirm this CF isn’t on that path. Otherwise, consider reordering or introducing a sibling CF keyed by (Shard, StateVersion[, Epoch]).

Option (illustrative reordering in this file, cascading changes expected elsewhere):

-    type Key = (Epoch, Shard, StateVersion);
-    type KeyCodec = (EpochCodec, ShardCodec, NumberCodec<StateVersion>);
+    type Key = (Shard, StateVersion, Epoch);
+    type KeyCodec = (ShardCodec, NumberCodec<StateVersion>, EpochCodec);

Additionally, to avoid accidental reuse of existing on-disk data (given schema changes), consider bumping the CF name:

-        "substates_unpruned_idx"
+        "substates_unpruned_idx_v2"
utilities/db_inspector/src/webserver/handlers/state_transitions.rs (1)

60-84: Avoid needless per-transition work after the page is filled; bulk-skip within entries

Current logic continues iterating and performing substate lookups after the page is full to compute an exact total. You can keep exact totals while significantly reducing CPU/IO by:

  • Counting transitions per key up-front.
  • Bulk-skipping transitions within a key when still below row_skip (no per-transition loop).
  • Skipping substate lookups entirely once emitted >= row_limit (but continue counting using len).

Apply this diff to make the hot path cheaper for large datasets and big page offsets:

-    for result in iter {
-        let ((shard, state_version), data) = result?;
-        let encoded_key = cf.encode_key(&(shard, state_version));
-        let key_hex = hex::encode(&encoded_key);
-        for (i, transition) in data.transitions.into_iter().enumerate() {
-            if skipped < row_skip {
-                skipped += 1;
-                continue;
-            }
-            if emitted < row_limit {
-                let substate = substate_cf.get(&transition.substate_address, OPERATION).optional()?;
-                table.add_row(json!({
-                    "id": format!("{}-{}", key_hex, i),
-                    "epoch": data.epoch,
-                    "shard": shard,
-                    "state_version": state_version,
-                    "substate_id": substate.as_ref().map(|s| s.substate_id()),
-                    "version": substate.as_ref().map(|s| s.version()),
-                    "transition": transition.transition,
-                }));
-                emitted += 1;
-            }
-
-            count += 1;
-        }
-    }
+    for result in iter {
+        let ((shard, state_version), data) = result?;
+        let trans_len = data.transitions.len();
+        count += trans_len;
+
+        // Bulk skip within this key if we still have rows to skip.
+        let mut start_i = 0usize;
+        if skipped < row_skip {
+            let to_skip = (row_skip - skipped).min(trans_len);
+            skipped += to_skip;
+            start_i = to_skip;
+        }
+
+        // If page is already full, avoid any further per-transition work on this key
+        if emitted >= row_limit {
+            continue;
+        }
+
+        let encoded_key = cf.encode_key(&(shard, state_version));
+        let key_hex = hex::encode(&encoded_key);
+        for (i, transition) in data.transitions.into_iter().enumerate().skip(start_i) {
+            if emitted >= row_limit {
+                break;
+            }
+            let substate = substate_cf.get(&transition.substate_address, OPERATION).optional()?;
+            table.add_row(json!({
+                "id": format!("{}-{}", key_hex, i),
+                "epoch": data.epoch,
+                "shard": shard,
+                "state_version": state_version,
+                "substate_id": substate.as_ref().map(|s| s.substate_id()),
+                "version": substate.as_ref().map(|s| s.version()),
+                "transition": transition.transition,
+            }));
+            emitted += 1;
+        }
+    }
bindings/src/types/ShardStateVersions.ts (2)

3-10: Fix minor JSDoc typo and tighten the example wording

Typo: “forth” -> “fourth”. Also consider making the shard indexing language a touch clearer.

Apply upstream (in the Rust doc that generates this) so it regenerates here:

- * version for shard 1, third is shard 2, and forth is shard 3.
+ * version for shard 1, the third is shard 2, and the fourth is shard 3.

11-11: uint64 state versions mapped to TS number[] risk precision loss beyond 2^53-1

Proto and Rust indicate these are 64-bit integers. TS number cannot exactly represent all uint64 values. If state_version can grow large, precision errors will silently corrupt client-side logic.

Recommendations (adjust in the Rust ts-rs annotations so this file regenerates):

  • Prefer bigint for exact integers in TS: ts(type = "{ inner: bigint[] }") (note: JSON serialization of bigint needs care).
  • Or use strings for maximal compatibility: ts(type = "{ inner: string[] }").

Illustrative TS diff (generated output), if choosing bigint:

-export type ShardStateVersions = { inner: number[] };
+export type ShardStateVersions = { inner: bigint[] };

If choosing string:

-export type ShardStateVersions = { inner: number[] };
+export type ShardStateVersions = { inner: string[] };

Would you like me to point to the exact Rust location and attribute needed for ts-rs?

crates/common_types/src/versioned_substate_id.rs (1)

437-442: New VersionedSubstateIdRef::to_shard mirrors owned variant; add a short doc comment

Functionality looks correct and consistent with the owned type. Consider adding a brief doc comment mirroring the global-substate note for discoverability.

Suggested doc (apply above the method):

+    /// Returns the shard for this versioned substate id.
+    /// If the substate is global, returns `Shard::global()` regardless of version.
+    /// Otherwise, computes the shard from the address using the given `NumPreshards`.
     pub fn to_shard(&self, num_preshards: NumPreshards) -> Shard {
applications/tari_validator_node/src/state_bootstrap.rs (2)

47-48: Prefer symbolic zero if available for consistency

You use Epoch::zero() below; if Version provides a zero() as well, prefer that to keep style consistent. If not, this is fine as-is.

Example:

-const INITIAL_STATE_VERSION: Version = 0;
+const INITIAL_STATE_VERSION: Version = Version::zero();

176-203: Each create_substate commits separately; consider batching for atomicity and fewer writes

Right now, every substate creation creates a fresh SubstateUpdateBatch and commits it. Batching all initial creations into a single batch per epoch/state_version reduces I/O, ensures atomic bootstrap, and guarantees a consistent state_version across the set.

One approach: accumulate transitions and commit once in bootstrap_state. For minimal impact, introduce a helper that pushes into a provided batch:

-fn create_substate<TTx, TId, TVal>(
+fn create_substate<TTx, TId, TVal>(
     tx: &mut TTx,
     num_preshards: NumPreshards,
     substate_id: TId,
     value: TVal,
 ) -> Result<(), StorageError>
 where
@@
-    let substate_id = substate_id.into();
-    let shard = VersionedSubstateIdRef::new(&substate_id, 0).to_shard(num_preshards);
-    let mut batch = SubstateUpdateBatch::new(Epoch::zero());
-    batch
-        .with_transition(shard, INITIAL_STATE_VERSION)
-        .push(SubstateTransition::Up {
-            id: substate_id,
-            version: 0,
-            substate_or_hash: value.into().into(),
-        });
-
-    SubstateRecord::commit_batch(tx, batch)?;
+    let substate_id = substate_id.into();
+    let shard = VersionedSubstateIdRef::new(&substate_id, 0).to_shard(num_preshards);
+    let mut batch = SubstateUpdateBatch::new(Epoch::zero());
+    batch
+        .with_transition(shard, INITIAL_STATE_VERSION)
+        .push(SubstateTransition::Up {
+            id: substate_id,
+            version: 0,
+            substate_or_hash: value.into().into(),
+        });
+    SubstateRecord::commit_batch(tx, batch)?;

Alternatively, I can provide a patch to:

  • create a single SubstateUpdateBatch in bootstrap_state,
  • change create_* helpers to push into that batch, and
  • commit once at the end.

Want me to draft that full refactor?

applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs (1)

88-97: Fetching values unconditionally may be wasteful

You always set include_values = true. If some clients only need hashes/metadata, this increases payload size and latency.

Consider:

  • adding an include_values: bool field to StateSyncTask,
  • wiring it via new(...), and
  • passing it through here instead of hard-coding true.
bindings/src/types/BlockHeader.ts (1)

41-41: Type aligns with bindings; specify encoding in docs

RistrettoPublicKeyBytes is a string alias. Consider clarifying expected encoding (hex, base64, etc.) in the upstream Rust docs so the generated TS includes it. This helps clients avoid mismatches.

Example (upstream Rust doc):

/// The public key of the proposer as hex-encoded bytes.
crates/consensus_tests/src/substate_store.rs (1)

217-226: Clarify sentinel state_version = 0 and avoid naming ambiguity

  • I confirmed that
    SubstateUpdateBatch::with_transition(&mut self, shard, state_version: Version)
    does not enforce state_version > 0, so using 0 as a test‐only sentinel is valid.
  • To improve readability and prevent confusing the batch’s state_version with the substate’s own version field:
    • Add a brief comment where you pass 0 to indicate it’s a test sentinel.
    • Optionally rename the local version parameter to substate_version.

Example refactor in crates/consensus_tests/src/substate_store.rs:

- fn add_substate(store: &TestStore, seed: u8, version: u32) -> VersionedSubstateId {
+ fn add_substate(store: &TestStore, seed: u8, substate_version: u32) -> VersionedSubstateId {
     let id = new_substate_id(seed);
     let value = new_substate_value(seed);
     let mut batch = SubstateUpdateBatch::new(Epoch::zero());
-    batch.with_transition(Shard::first(), 0).push(SubstateTransition::Up {
-        id: id.clone(),
-        version,
-        substate_or_hash: value.into(),
-    });
+    // Use 0 as a sentinel state_version in tests
+    batch
+        .with_transition(Shard::first(), 0)
+        .push(SubstateTransition::Up {
+            id: id.clone(),
+            version: substate_version,
+            substate_or_hash: value.into(),
+        });
     store
         .with_write_tx(|tx| SubstateRecord::commit_batch(tx, batch))
         .unwrap();
crates/storage/src/consensus_models/substate_update_batch.rs (2)

11-14: Expose batch structure is fine; consider deriving Debug/Clone for ergonomics

Public fields and nested map model by (shard, state_version) look good for the new design. Deriving Debug/Clone improves testability and diagnostics.

-pub struct SubstateUpdateBatch {
+#[derive(Debug, Clone)]
+pub struct SubstateUpdateBatch {
     pub epoch: Epoch,
     pub updates: IndexMap<Shard, IndexMap<Version, Vec<SubstateTransition>>>,
 }

29-38: Disambiguate “version” field to avoid confusion with state_version

This “version” is the per-substate version, not the global state_version. Adding a short doc comment avoids future misreads.

-pub enum SubstateTransition {
+#[derive(Debug, Clone)]
+pub enum SubstateTransition {
     Up {
-        id: SubstateId,
-        version: u32,
+        id: SubstateId,
+        /// Per-substate version (NOT the global state_version)
+        version: u32,
         substate_or_hash: SubstateValueOrHash,
     },
     Down {
         id: VersionedSubstateId,
     },
 }
crates/consensus_tests/src/support/harness.rs (1)

192-200: Minor: reuse substate.shard() instead of recomputing

You can avoid recomputing the shard each time by using SubstateRecord::shard(), which reads created.in_shard and guarantees consistency with record metadata.

-            for substate in &substates {
-                let shard = substate.to_versioned_substate_id().to_shard(TEST_NUM_PRESHARDS);
-                if v.shard_group.contains(&shard) {
-                    batch
-                        .with_transition(shard, substate.created().at_state_version)
-                        .push(substate.clone().into_transition());
-                }
-            }
+            for substate in &substates {
+                let shard = substate.shard();
+                if v.shard_group.contains(&shard) {
+                    batch
+                        .with_transition(shard, substate.created().at_state_version)
+                        .push(substate.clone().into_transition());
+                }
+            }
crates/state_store_tests/src/helpers.rs (1)

165-194: Use substate.shard() for consistency and readability

Slight simplification and avoids repeating shard derivation logic. This prevents future drift between computed shard and record metadata.

-            batch
-                .with_transition(
-                    substate.to_versioned_substate_id().to_shard(TEST_NUM_PRESHARDS),
-                    destroyed.at_state_version,
-                )
+            batch
+                .with_transition(substate.shard(), destroyed.at_state_version)
                 .push(tari_ootle_storage::consensus_models::SubstateTransition::Down {
                     id: VersionedSubstateId::new(substate.substate_id.clone(), substate.version),
                 });
@@
-            batch
-                .with_transition(
-                    substate.to_versioned_substate_id().to_shard(TEST_NUM_PRESHARDS),
-                    substate.created().at_state_version,
-                )
+            batch
+                .with_transition(substate.shard(), substate.created().at_state_version)
                 .push(tari_ootle_storage::consensus_models::SubstateTransition::Up {
                     id: substate.substate_id.clone(),
                     version: substate.version,
                     substate_or_hash: substate.clone().into_substate_value_or_hash(),
                 });
crates/rpc_state_sync/src/state_sync.rs (2)

122-126: Remove duplicate not_found match arm

The RequestFailed(is_not_found) case appears twice. The second is unreachable and will trigger an “unreachable pattern” warning.

-            Err(RpcError::RequestFailed(err)) if err.is_not_found() => Ok(None),
-            Ok(GetCheckpointResponse { checkpoint: None }) => Ok(None),
-            Err(RpcError::RequestFailed(err)) if err.is_not_found() => Ok(None),
+            Err(RpcError::RequestFailed(err)) if err.is_not_found() => Ok(None),
+            Ok(GetCheckpointResponse { checkpoint: None }) => Ok(None),

640-709: Template extraction logic is reasonable

Create path handles Template adds; destroy path logs deprecations (currently not possible) without side-effects. Consider auditing when/if template DOWNs are introduced.

If/when template DOWNs are allowed, ensure we gate TemplateChange::Deprecate generation behind a protocol version and add validation that the “destroy” proof indeed refers to a template.

crates/storage/src/consensus_models/state_transition.rs (2)

20-36: Optional: avoid O(n^2) memmoves from repeated drain(..take).

Draining from the front on a Vec repeatedly causes shifting of the tail each iteration. For large batches, consider VecDeque to keep this O(n).

Apply along these lines:

- use std::num::NonZeroUsize;
+ use std::{collections::VecDeque, num::NonZeroUsize};

 impl StateVersionTransitions {
     pub fn into_chunks(self, size: NonZeroUsize) -> Vec<Self> {
-        let num_chunks = self.updates.len().div_ceil(size.get());
-        let mut chunks = Vec::with_capacity(num_chunks);
-        let mut updates = self.updates;
-        while !updates.is_empty() {
-            let take = updates.len().min(size.get());
-            let chunk_updates: Vec<_> = updates.drain(..take).collect();
+        let size = size.get();
+        let mut chunks = Vec::with_capacity(self.updates.len().div_ceil(size));
+        let mut updates: VecDeque<_> = self.updates.into();
+        while !updates.is_empty() {
+            let take = updates.len().min(size);
+            let mut chunk_updates = Vec::with_capacity(take);
+            for _ in 0..take {
+                chunk_updates.push(updates.pop_front().expect("deque not empty"));
+            }
             chunks.push(Self {
                 epoch: self.epoch,
                 shard: self.shard,
                 state_version: self.state_version,
                 updates: chunk_updates,
             });
         }
         chunks
     }
 }

41-49: Clarify inclusive semantics of get_after

Tests confirm that state_transitions_get_after is actually inclusive—it returns the transition at exactly the given state_version if present. To avoid confusion, please:

• Add a doc comment above StateStoreReadTransaction::state_transitions_get_after in
crates/storage/src/state_store/mod.rs noting that it returns transitions at or after the specified version (inclusive).
• Add a matching doc comment above StateTransition::get_for_shard in
crates/storage/src/consensus_models/state_transition.rs.

Optionally, if you want to improve API discoverability (at the cost of a breaking rename), consider renaming the method to state_transitions_get_at_or_after (and the wrapper to get_for_shard_at_or_after) in a follow-up.

crates/state_store_rocksdb/src/writer.rs (2)

1282-1301: Prune routine is correct but counts index entries, not values.

  • Clears values for each downed substate address and removes the index key.
  • Log message reports number of index entries pruned (count), not number of substates. If useful, consider summing addresses.len() for more granular metrics.

Apply if desired:

-    let mut count = 0usize;
+    let mut entries = 0usize;
+    let mut values = 0usize;
     for result in iter {
         let (key, addresses) = result?;
-        // TODO(perf): consider storing the actual values in a separate column family to avoid get/set
+        // TODO(perf): consider storing the actual values in a separate column family to avoid get/set
         for substate_addr in addresses {
             let mut substate = substates_cf.get(&substate_addr, OPERATION)?;
             substate.clear_substate_value();
             substates_cf.put(&substate_addr, &substate, OPERATION)?;
+            values += 1;
         }
         unpruned_index.delete(&key, OPERATION)?;
-        count += 1;
+        entries += 1;
     }
     info!(
         target: LOG_TARGET,
-        "🗑️ Pruned {count} downed substates for epoch {epoch} from unpruned values index"
+        "🗑️ Pruned {values} downed substate values across {entries} entries for epoch {epoch}"
     );

1274-1276: Optional: guard against accidental overwrite of an existing (shard, state_version) transition.

If duplicate writes for the same key are unexpected, add an exists check or use put_if_absent semantics and log/return an error to prevent silent overwrite.

Would you like a patch to check existence and return a QueryError on duplicates?

crates/storage/src/consensus_models/substate.rs (1)

203-211: Nit: parameter name is misleading in substate_is_up.

The parameter is a SubstateAddress, not a Shard.

Consider renaming for clarity:

-    pub fn substate_is_up<TTx: StateStoreReadTransaction>(
-        tx: &TTx,
-        shard: &SubstateAddress,
+    pub fn substate_is_up<TTx: StateStoreReadTransaction>(
+        tx: &TTx,
+        address: &SubstateAddress,
     ) -> Result<bool, StorageError> {
         // TODO: consider optimising
-        let rec = tx.substates_get(shard)?;
+        let rec = tx.substates_get(address)?;
         Ok(rec.is_up())
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3b6bde5 and 5b50ef7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs (3 hunks)
  • applications/tari_validator_node/src/state_bootstrap.rs (10 hunks)
  • bindings/src/index.ts (1 hunks)
  • bindings/src/types/BlockHeader.ts (2 hunks)
  • bindings/src/types/ShardStateVersions.ts (1 hunks)
  • bindings/src/types/SubstateCreated.ts (1 hunks)
  • bindings/src/types/SubstateDestroyed.ts (1 hunks)
  • bindings/src/types/SubstateRecord.ts (2 hunks)
  • crates/common_types/src/versioned_substate_id.rs (2 hunks)
  • crates/consensus_tests/fixtures/block.json (2 hunks)
  • crates/consensus_tests/fixtures/block_with_dummies.json (2 hunks)
  • crates/consensus_tests/src/substate_store.rs (3 hunks)
  • crates/consensus_tests/src/support/harness.rs (4 hunks)
  • crates/p2p/proto/rpc.proto (2 hunks)
  • crates/rpc_state_sync/src/state_sync.rs (9 hunks)
  • crates/state_store_rocksdb/src/column_families/substate.rs (2 hunks)
  • crates/state_store_rocksdb/src/writer.rs (6 hunks)
  • crates/state_store_tests/src/helpers.rs (9 hunks)
  • crates/state_store_tests/src/substates.rs (3 hunks)
  • crates/storage/src/consensus_models/block.rs (8 hunks)
  • crates/storage/src/consensus_models/state_transition.rs (1 hunks)
  • crates/storage/src/consensus_models/substate.rs (9 hunks)
  • crates/storage/src/consensus_models/substate_update_batch.rs (1 hunks)
  • crates/storage/src/state_store/mod.rs (6 hunks)
  • utilities/db_inspector/src/webserver/handlers/state_transitions.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/consensus_tests/fixtures/block_with_dummies.json
  • crates/consensus_tests/fixtures/block.json
  • crates/storage/src/consensus_models/block.rs
🧰 Additional context used
🧬 Code Graph Analysis (19)
bindings/src/types/SubstateCreated.ts (2)
bindings/src/types/Epoch.ts (1)
  • Epoch (3-3)
bindings/src/types/Shard.ts (1)
  • Shard (3-3)
bindings/src/types/BlockHeader.ts (1)
bindings/src/types/RistrettoPublicKeyBytes.ts (1)
  • RistrettoPublicKeyBytes (6-6)
crates/state_store_rocksdb/src/column_families/substate.rs (3)
bindings/src/types/Epoch.ts (1)
  • Epoch (3-3)
bindings/src/types/Shard.ts (1)
  • Shard (3-3)
bindings/src/types/SubstateAddress.ts (1)
  • SubstateAddress (3-3)
bindings/src/types/SubstateDestroyed.ts (1)
bindings/src/types/Epoch.ts (1)
  • Epoch (3-3)
crates/storage/src/consensus_models/substate_update_batch.rs (7)
crates/storage/src/consensus_models/substate_change.rs (2)
  • substate (58-63)
  • shard (66-71)
bindings/src/types/SubstateId.ts (1)
  • SubstateId (15-24)
crates/storage/src/consensus_models/substate.rs (2)
  • shard (112-114)
  • new (50-65)
bindings/src/types/Shard.ts (1)
  • Shard (3-3)
bindings/src/types/Epoch.ts (1)
  • Epoch (3-3)
bindings/src/types/VersionedSubstateId.ts (1)
  • VersionedSubstateId (4-4)
crates/common_types/src/versioned_substate_id.rs (4)
  • new (25-30)
  • new (197-199)
  • new (314-319)
  • new (433-435)
crates/consensus_tests/src/substate_store.rs (3)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
crates/storage/src/consensus_models/substate.rs (2)
  • new (50-65)
  • commit_batch (174-180)
crates/common_types/src/shard.rs (1)
  • first (27-29)
bindings/src/types/SubstateRecord.ts (1)
bindings/src/types/SubstateCreated.ts (1)
  • SubstateCreated (5-5)
utilities/db_inspector/src/webserver/handlers/state_transitions.rs (4)
crates/storage/src/consensus_models/substate.rs (6)
  • new (50-65)
  • substate_id (79-81)
  • substate_id (390-392)
  • substate_id (423-428)
  • version (105-107)
  • version (430-435)
utilities/db_inspector/src/webserver/handlers/types.rs (2)
  • new (25-30)
  • new (55-64)
crates/state_store_rocksdb/src/reader.rs (3)
  • iter (305-308)
  • iter (696-698)
  • iter (1671-1673)
crates/state_store_rocksdb/src/codecs/tuple.rs (4)
  • encode (33-37)
  • encode (64-69)
  • encode (103-113)
  • encode (132-136)
crates/common_types/src/versioned_substate_id.rs (3)
crates/common_types/src/substate_address.rs (2)
  • to_shard (145-195)
  • to_shard (421-474)
crates/common_types/src/num_preshards.rs (1)
  • num_shards (36-38)
crates/common_types/src/shard.rs (1)
  • global (33-35)
crates/rpc_state_sync/src/state_sync.rs (4)
crates/storage/src/consensus_models/substate.rs (11)
  • from (359-361)
  • from (365-367)
  • new (50-65)
  • version (105-107)
  • version (430-435)
  • commit_batch (174-180)
  • value (329-334)
  • value (378-380)
  • substate_id (79-81)
  • substate_id (390-392)
  • substate_id (423-428)
crates/state_store_rocksdb/src/reader.rs (1)
  • new (169-175)
crates/p2p/src/conversions/rpc.rs (15)
  • try_from (29-37)
  • try_from (52-57)
  • try_from (73-79)
  • try_from (96-105)
  • try_from (132-141)
  • try_from (149-162)
  • try_from (182-187)
  • from (41-46)
  • from (61-67)
  • from (83-90)
  • from (109-115)
  • from (121-126)
  • from (166-175)
  • from (191-196)
  • value (155-159)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
crates/consensus_tests/src/support/harness.rs (4)
bindings/src/types/SubstateCreated.ts (1)
  • SubstateCreated (5-5)
bindings/src/types/SubstateRecord.ts (1)
  • SubstateRecord (7-14)
crates/storage/src/consensus_models/substate.rs (5)
  • shard (112-114)
  • new (50-65)
  • value (329-334)
  • value (378-380)
  • commit_batch (174-180)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
applications/tari_validator_node/src/state_bootstrap.rs (3)
crates/storage/src/consensus_models/substate.rs (5)
  • new (50-65)
  • substate_id (79-81)
  • substate_id (390-392)
  • substate_id (423-428)
  • commit_batch (174-180)
crates/common_types/src/versioned_substate_id.rs (8)
  • new (25-30)
  • new (197-199)
  • new (314-319)
  • new (433-435)
  • substate_id (46-48)
  • substate_id (226-228)
  • substate_id (325-327)
  • substate_id (444-446)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs (2)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
crates/storage/src/consensus_models/state_transition.rs (1)
  • get_for_shard (42-49)
crates/state_store_tests/src/substates.rs (2)
crates/state_store_tests/src/helpers.rs (4)
  • assert_eq_debug (254-257)
  • build_substate_record (128-143)
  • create_rocksdb (81-88)
  • create_substate_update_batch (165-194)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
crates/storage/src/consensus_models/state_transition.rs (2)
crates/storage/src/consensus_models/substate.rs (1)
  • shard (112-114)
crates/storage/src/consensus_models/substate_change.rs (1)
  • shard (66-71)
crates/storage/src/state_store/mod.rs (4)
bindings/src/types/ShardStateVersions.ts (1)
  • ShardStateVersions (11-11)
crates/state_store_rocksdb/src/reader.rs (5)
  • state_transitions_get_after (1592-1655)
  • state_tree_nodes_get (1657-1662)
  • state_tree_nodes_get_all_by_state_version (1664-1675)
  • state_tree_versions_get_latest (1677-1682)
  • state_tree_versions_get_latest_for_shard_group (1684-1720)
crates/storage/src/consensus_models/substate.rs (1)
  • shard (112-114)
crates/state_store_rocksdb/src/writer.rs (1)
  • substates_commit_batch (1197-1280)
crates/state_store_rocksdb/src/writer.rs (6)
bindings/src/types/SubstateCreated.ts (1)
  • SubstateCreated (5-5)
bindings/src/types/SubstateDestroyed.ts (1)
  • SubstateDestroyed (4-4)
bindings/src/types/SubstateRecord.ts (1)
  • SubstateRecord (7-14)
crates/storage/src/state_store/mod.rs (1)
  • substates_commit_batch (493-493)
crates/storage/src/consensus_models/substate.rs (4)
  • shard (112-114)
  • new (50-65)
  • version (105-107)
  • version (430-435)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
crates/state_store_tests/src/helpers.rs (5)
bindings/src/types/SubstateCreated.ts (1)
  • SubstateCreated (5-5)
bindings/src/types/SubstateRecord.ts (1)
  • SubstateRecord (7-14)
crates/common_types/src/versioned_substate_id.rs (12)
  • substate_id (46-48)
  • substate_id (226-228)
  • substate_id (325-327)
  • substate_id (444-446)
  • version (58-60)
  • version (222-224)
  • version (333-335)
  • version (448-450)
  • new (25-30)
  • new (197-199)
  • new (314-319)
  • new (433-435)
crates/state_store_tests/src/substates.rs (1)
  • substate_id (21-24)
crates/storage/src/consensus_models/substate.rs (9)
  • substate_id (79-81)
  • substate_id (390-392)
  • substate_id (423-428)
  • version (105-107)
  • version (430-435)
  • value (329-334)
  • value (378-380)
  • destroyed (120-122)
  • shard (112-114)
crates/storage/src/consensus_models/substate.rs (5)
bindings/src/types/SubstateCreated.ts (1)
  • SubstateCreated (5-5)
bindings/src/types/SubstateDestroyed.ts (1)
  • SubstateDestroyed (4-4)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
crates/storage/src/consensus_models/substate_change.rs (3)
  • version (54-56)
  • shard (66-71)
  • into_transition (102-111)
crates/state_tree/src/tree.rs (1)
  • id (212-217)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: check stable
  • GitHub Check: machete
  • GitHub Check: test
  • GitHub Check: check nightly
  • GitHub Check: clippy
🔇 Additional comments (33)
crates/state_store_rocksdb/src/column_families/substate.rs (2)

77-80: LGTM: Versioned key and batched value are aligned with the new model

  • Key moves to (Epoch, Shard, Version) and uses NumberCodec<…> — appropriate for ordered iteration.
  • Value becomes Vec with DefaultCodec — matches the new batched semantics.

No blockers from my side on this change.


79-80: Serde support confirmed; review Vec sizes for write amplification

  • SubstateAddress is annotated with #[derive(..., Serialize, Deserialize, ...)].
  • type ValueCodec = DefaultCodec<Vec<SubstateAddress>> is an alias for Bincode<Vec<SubstateAddress>>, which enforces the serde bounds.

No runtime decode errors should occur. Please validate that the typical length of the Vec<SubstateAddress> values stays within your expected size limits to avoid excessive RocksDB write amplification.

utilities/db_inspector/src/webserver/handlers/state_transitions.rs (2)

32-36: Header update matches the new (shard, state_version) model

Replacing Seq with State Version and removing Substate Address is consistent with the new composite-key and per-transition row expansion.


55-87: Row-level pagination fix is correct and addresses earlier feedback

Switching to row-level counters (row_skip/emitted) inside the per-transition loop yields deterministic at-most-limit pages and correct semantics when a key expands to multiple rows. Nice work.

applications/tari_validator_node/src/state_bootstrap.rs (2)

190-198: Shard derivation via VersionedSubstateIdRef is correct

Deriving the shard using VersionedSubstateIdRef::to_shard with version 0 aligns with global/non-global rules and the new shard+state_version model.


57-62: Signature update verified: all relevant call sites updated

  • In applications/tari_validator_node/src/bootstrap.rs:224, bootstrap_state(tx, config.network, consensus_constants.num_preshards) now correctly passes the new num_preshards parameter.
  • The bootstrap_state in crates/template_test_tooling/src/template_test.rs is an unrelated method on a test struct and isn’t affected by this change.

No remaining calls use the old signature.

applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs (1)

110-124: Chunked sending logic looks solid

has_more is computed correctly from the chunk index and you preserve state_version and epoch per chunk. Good.

bindings/src/types/BlockHeader.ts (1)

11-11: Doc rewording LGTM

“This is computed” better reflects the immutable, derived nature of the ID.

bindings/src/types/SubstateCreated.ts (2)

2-3: LGTM on type-only imports

Type-only imports keep the runtime bundle clean and match the generated patterns.


5-5: Confirm BigInt support and serialization

  • All tsconfig.json files in the repo target ES2020 or ESNext, so the native bigint type is supported at compile time (even where lib isn’t explicitly listed, the default libs for ES2020+ include BigInt definitions).
  • We didn’t find any direct JSON.stringify calls on bigint values in the codebase.
  • As a reminder, when you expose these types over JSON (e.g. in a public API), convert at_state_version: bigint to a string or supply a custom replacer/toJSON method to avoid runtime errors.
bindings/src/index.ts (1)

99-107: ✅ New type exports verified

Scripts confirm:

  • bindings/src/types/ShardStateVersions.ts exists and is exported once (line 99 in bindings/src/index.ts)
  • bindings/src/types/SubstateCreated.ts exists and is exported once (line 107 in bindings/src/index.ts)

No duplicate exports found.

crates/consensus_tests/src/substate_store.rs (2)

8-8: LGTM: import cleanup

Importing only BlockId, LeafBlock (dropping unused types) keeps tests tidy.


25-32: LGTM: test imports align with batched update path

The new imports (SubstateTransition, SubstateUpdateBatch, etc.) match the refactor towards batched transitions.

bindings/src/types/SubstateRecord.ts (2)

2-2: LGTM: switch to composed creation metadata

Importing SubstateCreated simplifies the record shape and reflects the new model.


12-12: No legacy created_* usages detected – shape change is safe

I’ve scanned all .ts/.tsx files and confirmed there are no remaining references to the old created_* fields (justify, block, by_shard, at_epoch). The new created: SubstateCreated export is present and consumers/imports update correctly. No downstream code or tests need further migration.

crates/storage/src/consensus_models/substate_update_batch.rs (1)

24-26: Method name vs. behavior: returns a transition buffer for in-place push

with_transition correctly ensures map entries and returns the buffer to mutate. No issues; just noting the "builder" style is intentional and clear from usage.

crates/state_store_tests/src/substates.rs (1)

131-141: Down transition sequencing looks correct

DOWN for substate2 at state_version=2 after creation at 1 is consistent with monotonic state_version progression per shard.

crates/state_store_tests/src/helpers.rs (3)

128-143: Builder looks correct and consistent with new model

build_substate_record correctly sets created metadata (epoch, shard, state_version) and computes state_hash from value/version.


234-250: gen_substates API/usage is coherent

Passing epoch and state_version through to SubstateCreated keeps test scaffolding aligned with (shard, state_version) semantics.


252-257: Nice touch: track_caller on assert helper

This makes panics point to the caller site, simplifying debugging.

crates/rpc_state_sync/src/state_sync.rs (3)

140-147: Guard: early-exit when already at checkpoint root

The short-circuit when local root equals checkpoint root is correct and avoids unnecessary sync.


206-238: Iterator sizing is fine

Using ExactSizeIterator::len() on the mapped Vec iterator is valid; no action needed.


323-349: Batching commit path is aligned with storage API

Building SubstateUpdateBatch per (shard, state_version) and committing via SubstateRecord::commit_batch is consistent with the storage refactor.

crates/storage/src/state_store/mod.rs (5)

193-198: LGTM: targeted accessor for last change of a substate.

Narrow read reduces unnecessary I/O for callers needing only the last change.


278-285: Confirm/document inclusive semantics for “get_after”.

Matches the RocksDB reader’s inclusive behavior at the requested state_version. Consider clarifying in the trait docstring so backends stay consistent.


288-299: LGTM: state-tree payload/type updates are coherent.

Using Node across read paths and adding “get_all_by_state_version” and shard-group latest versions aligns with the new model.


491-494: LGTM: batched substates commit API is a good abstraction boundary.

The write API matches the reader/writer separation and enables efficient batch processing.


523-529: LGTM: writer signature updated to StateTreePayload.

Consistent with reader changes and the payload-based tree.

crates/state_store_rocksdb/src/writer.rs (3)

140-145: LGTM: new StateTransition model types wired into writer.*

Imports match downstream usage in commit paths.


1197-1279: Batched substate commit path looks correct and consistent.

  • Up: writes SubstateRecord with created metadata, updates head, records transition.
  • Down: marks destroyed with epoch/state_version, updates head, records transition, buffers addresses for pruning.
  • Emits one StateTransitionModelDataV1 per (shard, state_version).

This matches the new (shard, state_version) keyed transition model.


1403-1411: LGTM: state-tree batch insert switched to StateTreePayload.

Matches the API shift; write path remains straightforward.

crates/storage/src/consensus_models/substate.rs (2)

136-151: LGTM: into_transition maps record state to transition correctly.

Uses value-or-hash for Up and constructs VersionedSubstateId for Down — consistent with the new transition model.


296-321: LGTM: SubstateCreated/Destroyed metadata and TS exports.

Nested created/destroyed metadata structures are clean and align with bindings; this unblocks cross-language consumers.

Comment thread applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs
@sdbondi
sdbondi force-pushed the vn-state-sync-shard-version branch from 01a1d35 to 86c6371 Compare August 19, 2025 12:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
crates/template_lib/src/resource/builder/stealth.rs (3)

15-16: Fix incorrect terminology in type docs: “Confidential” → “Stealth”

The struct docs say “Confidential” but this is StealthResourceBuilder. Update for accuracy.

Apply this diff:

-/// Implements the builder pattern for Confidential resources.
+/// Implements the builder pattern for Stealth resources.
 pub struct StealthResourceBuilder {

186-201: Doc examples reference the wrong builder

Examples show ResourceBuilder::confidential() in the stealth builder docs. Replace with ResourceBuilder::stealth() to avoid confusion.

Apply this diff:

-/// ResourceBuilder::confidential()
+/// ResourceBuilder::stealth()
 ///     .with_authorization_hook(CallerContext::current_component_address(), "my_hook")
 ///     .build();
@@
-/// ResourceBuilder::confidential()
+/// ResourceBuilder::stealth()
 ///     .with_authorization_hook(*alloc.address(), "my_hook")
 ///     .build();

222-231: Correct initial_supply docs: they contradict the function signature and behavior

The docs claim the method returns an address and that stealth resources do not return a bucket, but the function returns Bucket and the engine tests depend on it. Clarify that the returned bucket is a transient handle that must be used with stealth_transfer to produce depositable output(s).

Apply this diff:

-/// Sets up how many tokens are going to be minted on resource creation
-/// This builds the resource and mints the initial supply of tokens, returning the address of the resource.
-/// NOTE that stealth resources do not return the bucket of the initial supply since
-/// they are minted as individual UTXO substates and cannot be placed in vault.
+/// Sets up how many tokens are going to be minted on resource creation.
+/// This builds the resource and mints the initial supply of tokens, returning a transient `Bucket`
+/// that references the minted UTXO outputs.
+/// NOTE: The returned `Bucket` cannot be deposited directly into a vault; you must first perform a
+/// `stealth_transfer` to reveal outputs and obtain a depositable bucket.
 pub fn initial_supply(self, initial_supply: Amount) -> Bucket {
crates/rpc_state_sync/src/state_sync.rs (1)

121-124: Unreachable duplicate match arm for RequestFailed(NotFound)

There are two identical arms handling Err(RpcError::RequestFailed(err)) if err.is_not_found(). The second one is unreachable and will cause a compiler error.

Apply this diff:

-            Err(RpcError::RequestFailed(err)) if err.is_not_found() => Ok(None),
-            Ok(GetCheckpointResponse { checkpoint: None }) => Ok(None),
-            Err(RpcError::RequestFailed(err)) if err.is_not_found() => Ok(None),
+            Err(RpcError::RequestFailed(err)) if err.is_not_found() => Ok(None),
+            Ok(GetCheckpointResponse { checkpoint: None }) => Ok(None),
♻️ Duplicate comments (1)
crates/rpc_state_sync/src/state_sync.rs (1)

128-137: Stop consuming the stream after reaching the checkpoint

When the checkpoint state_version is reached and validated, the code returns Ok(()) from inside the write-tx closure but continues the outer stream loop. A malicious peer could send additional (unvalidated) updates beyond the checkpoint, leading to over-sync risk.

Apply this diff to set a function-scoped flag and break after the transaction:

@@
     async fn start_state_sync(
@@
-        let mut template_changes = vec![];
+        let mut template_changes = vec![];
+        // Break the outer stream loop once the checkpoint version is reached and validated
+        let mut reached_checkpoint = false;
@@
-                        maybe_persisted_state_version = Some(state_version);
-                        store.set_state_version(state_version)?;
-                        // Done
-                        return Ok(());
+                        maybe_persisted_state_version = Some(state_version);
+                        store.set_state_version(state_version)?;
+                        // Mark for breaking the outer loop after the transaction commits
+                        reached_checkpoint = true;
+                        return Ok(());
@@
-            })?;
+            })?;
+            if reached_checkpoint {
+                break;
+            }

Also applies to: 268-292, 299-301

🧹 Nitpick comments (8)
crates/template_lib/src/resource/builder/stealth.rs (2)

29-31: Constructor doc nit: “confidential” → “stealth”

The constructor doc still references “confidential resource builder”.

Apply this diff:

-/// Returns a new confidential resource builder
+/// Returns a new stealth resource builder
 pub(super) fn new() -> Self {

138-142: Grammar fix: “whom” → “who”

Minor readability nit in docs.

Apply this diff:

-/// Sets up whom (apart from the owner) can update the access rules of the resource.
+/// Sets up who (apart from the owner) can update the access rules of the resource.
 pub fn update_access_rules(mut self, rule: AccessRule) -> Self {
crates/rpc_state_sync/src/state_sync.rs (6)

202-204: Prefer reserve over reserve_exact for amortized growth

reserve_exact may cause more frequent reallocations across many batches. reserve is generally preferred unless you have a strong reason to match capacity exactly.

Apply this diff:

-            tree_changes.reserve_exact(msg.updates.len());
-            updates.reserve_exact(msg.updates.len());
+            tree_changes.reserve(msg.updates.len());
+            updates.reserve(msg.updates.len());

205-223: Validate update shards match the shard being synced

Before buffering, validate that each update’s substate ID belongs to shard. Otherwise a malicious peer could mix in updates from other shards. If SpreadPrefixStateTree/ShardScopedTreeStoreWriter already enforces this, consider returning a clear error when mismatched instead of relying on lower-level errors.

Follow-up: If you want, I can draft a small helper that extracts the shard from SubstateUpdateProof and checks it against shard here.


214-223: Minor: avoid calling .len() on the iterator after mapping

This is fine because Map<IntoIter<Vec<_>>> implements ExactSizeIterator, but if readability is a concern, collect to a small vector before logging/iterating. Not urgent.


268-281: Rollback path is good; consider logging the number of buffered updates discarded on mismatch

When the checkpoint root mismatches, we bail out (and the transaction should roll back). It may help troubleshooting to include the number of updates buffered for this state_version in the error context.


322-347: Batch mapping looks correct; consider duplicate-detection within a version

Mapping from SubstateUpdateProof to SubstateTransition is solid. Depending on upstream guarantees, you may want to reject duplicate updates for the same substate ID within the same (shard, state_version) to fail fast.


676-706: Template DOWN handling is logged as Deprecate but comments say “currently not possible”

Destroying a template currently “should not happen”, but the code emits a Deprecate change. This is fine as a placeholder, but please confirm that downstream consumers will treat this as a no-op or a safe deprecation. Otherwise, consider ignoring or hard-failing on template destroy updates until deprecation semantics are formalized.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5b50ef7 and a00cd82.

📒 Files selected for processing (4)
  • crates/engine/tests/templates/stealth/src/lib.rs (1 hunks)
  • crates/rpc_state_sync/src/state_sync.rs (8 hunks)
  • crates/template_lib/src/resource/builder/confidential.rs (1 hunks)
  • crates/template_lib/src/resource/builder/stealth.rs (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
crates/engine/tests/templates/stealth/src/lib.rs (1)
crates/engine_types/src/resource.rs (1)
  • view_key (130-132)
crates/template_lib/src/resource/builder/stealth.rs (3)
crates/template_lib/src/resource/builder/confidential.rs (2)
  • with_view_key (86-88)
  • with_view_key_opt (93-96)
crates/engine_types/src/resource.rs (1)
  • view_key (130-132)
bindings/src/types/RistrettoPublicKeyBytes.ts (1)
  • RistrettoPublicKeyBytes (6-6)
crates/template_lib/src/resource/builder/confidential.rs (3)
crates/template_lib/src/resource/builder/stealth.rs (2)
  • with_view_key (89-91)
  • with_view_key_opt (96-99)
crates/engine_types/src/resource.rs (1)
  • view_key (130-132)
bindings/src/types/RistrettoPublicKeyBytes.ts (1)
  • RistrettoPublicKeyBytes (6-6)
crates/rpc_state_sync/src/state_sync.rs (3)
crates/storage/src/consensus_models/substate.rs (8)
  • from (359-361)
  • from (365-367)
  • new (50-65)
  • version (105-107)
  • version (430-435)
  • commit_batch (174-180)
  • value (329-334)
  • value (378-380)
crates/p2p/src/conversions/rpc.rs (15)
  • try_from (29-37)
  • try_from (52-57)
  • try_from (73-79)
  • try_from (96-105)
  • try_from (132-141)
  • try_from (149-162)
  • try_from (182-187)
  • from (41-46)
  • from (61-67)
  • from (83-90)
  • from (109-115)
  • from (121-126)
  • from (166-175)
  • from (191-196)
  • value (155-159)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test
  • GitHub Check: check stable
  • GitHub Check: check nightly
  • GitHub Check: clippy
🔇 Additional comments (3)
crates/engine/tests/templates/stealth/src/lib.rs (1)

23-23: LGTM: cleaner optional view-key handling

Switching to with_view_key_opt(view_key) simplifies conditional chaining and aligns with the new builder API. No functional concerns.

crates/template_lib/src/resource/builder/confidential.rs (1)

85-89: LGTM: by-value with_view_key and new with_view_key_opt are sound and consistent

  • with_view_key(self, ...) -> Self delegating to with_view_key_opt keeps chaining ergonomics and avoids boilerplate.
  • with_view_key_opt correctly sets the internal Option<RistrettoPublicKeyBytes>, enabling both set and clear semantics at build time.

Also applies to: 90-96

crates/template_lib/src/resource/builder/stealth.rs (1)

89-91: LGTM: unified optional view-key API

  • By-value with_view_key(self, ...) and with_view_key_opt(mut self, Option<...>) mirror the confidential builder and make conditional application trivial.
  • Semantics are clear and consistent with the internal Option field.

Also applies to: 96-99

Comment thread crates/rpc_state_sync/src/state_sync.rs
Comment thread crates/rpc_state_sync/src/state_sync.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (10)
crates/consensus_tests/src/state_tree.rs (3)

22-22: Make RocksDB path portable and still keep the {}-placeholder behavior

Hardcoding /tmp makes this test non-portable on non-Unix platforms. If with_rocks_path preserves a literal {} for later substitution (as intended), prefer using std::env::temp_dir to build the base path while keeping the {} placeholder.

Apply this diff:

-        .with_rocks_path("/tmp/test{}")
+        .with_rocks_path(&format!("{}/tari-consensus-tests-{{}}", std::env::temp_dir().display()))

66-85: Harden the transitions scan: avoid potential infinite loops and improve error messages

Two improvements:

  • Add a monotonicity assertion to guard against a buggy store returning the same/lower state_version, which would loop forever.
  • Replace unwrap() with expect() to surface shard/version context on failure.

Apply this diff:

-                let mut all_transitions = vec![];
-                let mut next_state_version = 1;
-                while let Some(transitions) = tx
-                    .state_transitions_get_starting_at(shard, next_state_version, false)
-                    .optional()
-                    .unwrap()
-                {
+                let mut all_transitions = vec![];
+                let mut next_state_version = 1;
+                while let Some(transitions) = tx
+                    .state_transitions_get_starting_at(shard, next_state_version, false)
+                    .optional()
+                    .expect(&format!(
+                        "Failed to fetch state transitions for shard {shard} starting at state_version {next_state_version}"
+                    ))
+                {
+                    // Sanity check to prevent potential infinite loops if the store misbehaves
+                    assert!(
+                        transitions.state_version >= next_state_version,
+                        "Non-monotonic state_version returned: got {}, expected >= {} for shard {}",
+                        transitions.state_version,
+                        next_state_version,
+                        shard
+                    );
                     if transitions.epoch > checkpoint.epoch() {
                         break;
                     }
 
                     next_state_version = transitions.state_version + 1;
                     all_transitions.push(transitions);
                 }

105-109: Confirm the version argument to put_substate_changes; consider using the last observed state_version instead of a constant

The second argument is currently 1. If this parameter represents the state_version (or a versioned commit index), passing the last observed state_version will better reflect the tree state at the checkpoint.

Apply this diff:

-                let values = all_transitions
+                let values = all_transitions
                     .iter()
                     .flat_map(|t| &t.updates)
                     .map(|transition| transition.to_tree_change());
-                let root = tree.put_substate_changes(None, 1, values).unwrap();
+                let target_state_version = all_transitions
+                    .last()
+                    .map(|t| t.state_version)
+                    .unwrap_or(1);
+                let root = tree.put_substate_changes(None, target_state_version, values).unwrap();

Optional follow-up: Instead of batching all updates at once, apply each transition in sequence to validate intermediate root evolution as well:

for t in &all_transitions {
    let changes = t.updates.iter().map(|u| u.to_tree_change());
    let _ = tree.put_substate_changes(None, t.state_version, changes).unwrap();
}
// Now read the final root and compare to checkpoint
let root = tree.root();

I can draft this into a separate test if you want to validate per-transition correctness too.

crates/storage/src/state_store/mod.rs (1)

278-285: Document “starting_at” inclusive semantics and NotFound behavior

Please add a short doc comment clarifying:

  • The method returns the first available transition record for (shard, state_version) where state_version’ >= requested (inclusive).
  • NotFound is returned only if no transitions exist at or after the requested state_version for the shard.

This reduces ambiguity for integrators updating to the new API.

-    fn state_transitions_get_starting_at(
+    /// Returns the first transition record whose (shard, state_version) is equal to or greater than the requested
+    /// tuple (inclusive). If no transition exists at or after the requested state_version for the shard, a NotFound
+    /// error is returned.
+    fn state_transitions_get_starting_at(
         &self,
         shard: Shard,
         state_version: Version,
         include_values: bool,
     ) -> Result<StateVersionTransitions, StorageError>;
crates/state_store_tests/src/state_transitions.rs (2)

23-23: Prefer using the EPOCH constant consistently

You introduced EPOCH; use it in batch creation to keep test intent obvious.

-    let batch = create_substate_update_batch(Epoch::zero(), &substates);
+    let batch = create_substate_update_batch(EPOCH, &substates);

Apply similarly for subsequent batch creations in this file.


42-52: Optional: add a value-inclusion check

Consider one assertion with include_values = true to confirm Value vs Hash behavior on Up transitions (and Hash fallback when pruned).

crates/storage/src/consensus_models/state_transition.rs (1)

20-36: into_chunks preserves order but is O(n^2); switch to split_off to avoid repeated head drains

Draining from the head repeatedly causes repeated memmoves (quadratic behavior). Use split_off to take the tail once per chunk and rotate.

 impl StateVersionTransitions {
-    pub fn into_chunks(self, size: NonZeroUsize) -> Vec<Self> {
-        let num_chunks = self.updates.len().div_ceil(size.get());
-        let mut chunks = Vec::with_capacity(num_chunks);
-        let mut updates = self.updates;
-        while !updates.is_empty() {
-            let take = updates.len().min(size.get());
-            let chunk_updates: Vec<_> = updates.drain(..take).collect();
-            chunks.push(Self {
-                epoch: self.epoch,
-                shard: self.shard,
-                state_version: self.state_version,
-                updates: chunk_updates,
-            });
-        }
-        chunks
-    }
+    pub fn into_chunks(mut self, size: NonZeroUsize) -> Vec<Self> {
+        let size = size.get();
+        let mut chunks = Vec::with_capacity(self.updates.len().div_ceil(size));
+        let mut updates = std::mem::take(&mut self.updates);
+        while updates.len() > size {
+            let tail = updates.split_off(size);
+            chunks.push(Self {
+                epoch: self.epoch,
+                shard: self.shard,
+                state_version: self.state_version,
+                updates,
+            });
+            updates = tail;
+        }
+        if !updates.is_empty() {
+            chunks.push(Self {
+                epoch: self.epoch,
+                shard: self.shard,
+                state_version: self.state_version,
+                updates,
+            });
+        }
+        chunks
+    }
 }
crates/state_store_rocksdb/src/reader.rs (3)

1598-1602: Rename OPERATION label for consistency

The OPERATION name still says "state_transitions_get_n_after". Rename to "state_transitions_get_starting_at" to match the new API and ease metrics/log searches.

-        const OPERATION: &str = "state_transitions_get_n_after";
+        const OPERATION: &str = "state_transitions_get_starting_at";

1619-1622: Perf note is valid; consider storing state_hash in transition records

Since include_values=false still requires a Substate CF read to obtain the hash, consider storing the state_hash alongside the substate_address in the transition record to avoid the extra read when values are not requested.

Happy to propose a minimal schema bump strategy if desired.


1684-1720: Latest versions for shard group — LGTM; tiny range nit

Logic for global slot, range scan, and gap filling is solid. As a small readability nit, you can use an inclusive end range instead of +1 arithmetic.

-        let sg_range = shard_group.start()..Shard::from(shard_group.end().as_u32() + 1);
+        let sg_range = shard_group.start()..=shard_group.end();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a00cd82 and 86c6371.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • crates/consensus_tests/src/state_tree.rs (3 hunks)
  • crates/rpc_state_sync/Cargo.toml (0 hunks)
  • crates/state_store_rocksdb/src/reader.rs (6 hunks)
  • crates/state_store_tests/src/state_transitions.rs (2 hunks)
  • crates/storage/src/consensus_models/state_transition.rs (1 hunks)
  • crates/storage/src/state_store/mod.rs (6 hunks)
💤 Files with no reviewable changes (1)
  • crates/rpc_state_sync/Cargo.toml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-19T12:04:31.325Z
Learnt from: sdbondi
PR: tari-project/tari-ootle#1543
File: applications/tari_validator_node/src/p2p/rpc/state_sync_task.rs:64-66
Timestamp: 2025-08-19T12:04:31.325Z
Learning: The `state_transitions_get_after` function in the Tari codebase returns transitions equal to and after the given version (inclusive), not strictly after as the name might suggest.

Applied to files:

  • crates/storage/src/consensus_models/state_transition.rs
🧬 Code Graph Analysis (5)
crates/state_store_tests/src/state_transitions.rs (4)
crates/state_store_tests/src/helpers.rs (3)
  • create_rocksdb (81-88)
  • create_substate_update_batch (165-194)
  • gen_substates (234-250)
crates/storage/src/consensus_models/block.rs (3)
  • zero_block (207-219)
  • new (157-173)
  • epoch (356-358)
crates/storage/src/consensus_models/substate_update_batch.rs (1)
  • new (17-22)
crates/storage/src/consensus_models/substate.rs (2)
  • new (50-65)
  • shard (112-114)
crates/storage/src/consensus_models/state_transition.rs (2)
crates/storage/src/consensus_models/substate.rs (1)
  • shard (112-114)
crates/storage/src/consensus_models/substate_change.rs (1)
  • shard (66-71)
crates/consensus_tests/src/state_tree.rs (1)
crates/consensus_tests/src/support/harness.rs (2)
  • builder (80-82)
  • new (626-658)
crates/state_store_rocksdb/src/reader.rs (4)
crates/storage/src/state_store/mod.rs (5)
  • state_transitions_get_starting_at (279-284)
  • state_tree_nodes_get (288-288)
  • state_tree_nodes_get_all_by_state_version (289-293)
  • state_tree_versions_get_latest (294-294)
  • state_tree_versions_get_latest_for_shard_group (295-298)
crates/common_types/src/shard_group.rs (2)
  • format (246-246)
  • len (64-66)
crates/common_types/src/shard_state_versions.rs (3)
  • len (81-83)
  • shard_to_index (59-74)
  • from_vec (41-45)
crates/common_types/src/shard.rs (1)
  • global (33-35)
crates/storage/src/state_store/mod.rs (3)
bindings/src/types/ShardStateVersions.ts (1)
  • ShardStateVersions (11-11)
crates/state_store_rocksdb/src/reader.rs (5)
  • state_transitions_get_starting_at (1592-1655)
  • state_tree_nodes_get (1657-1662)
  • state_tree_nodes_get_all_by_state_version (1664-1675)
  • state_tree_versions_get_latest (1677-1682)
  • state_tree_versions_get_latest_for_shard_group (1684-1720)
crates/state_store_rocksdb/src/writer.rs (1)
  • substates_commit_batch (1197-1280)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test
  • GitHub Check: check stable
  • GitHub Check: check nightly
  • GitHub Check: clippy
🔇 Additional comments (20)
crates/consensus_tests/src/state_tree.rs (3)

8-14: Imports align with the new payload/tree APIs — LGTM

Brings in Optional, StateStore, and state_tree types cleanly and matches the refactor direction.


32-32: LGTM: Extra batch broadens transition coverage

Queuing additional transactions increases the chance of per-shard transitions and strengthens the assertions later in the test.


90-101: LGTM: Placeholder root short-circuit

Using SPARSE_MERKLE_PLACEHOLDER_HASH to assert the absence/presence of transitions per shard is appropriate and keeps the test crisp.

crates/storage/src/state_store/mod.rs (4)

34-39: Public surface updated to new versioned model — LGTM

The new imports (ShardStateVersions, StateTreePayload, StateVersionTransitions, SubstateUpdateBatch) align with the PR’s objectives and downstream usage.

Also applies to: 60-66


286-299: State-tree payload/type updates — LGTM

Returning Node and adding the by-state-version accessor matches the RocksDB reader/writer changes and the new payload model.


523-529: Batch insert signature updated — no stale Node usages

Verified that there are no remaining Node<Version> references and all calls to state_tree_nodes_batch_insert now pass Node<StateTreePayload>.


491-494: All StateStoreWriteTransaction implementations updated

The only implementor (RocksDbStateStoreWriteTransaction) now includes both new methods, so no action required:

  • crates/state_store_rocksdb/src/writer.rs:
    • fn substates_commit_batch at line 1197
    • fn substates_prune_downed_values at line 1282
  • No other StateStoreWriteTransaction impls found.
crates/state_store_tests/src/state_transitions.rs (4)

4-8: Imports and setup align with the new API — LGTM


29-40: Record-then-assert pattern is sound — LGTM

Tracking per-(state_version, shard) and asserting via the new API validates the intended “starting_at” semantics.


53-63: Batching across versions is exercised — LGTM


64-73: Assertions cover epoch, version, shard, and count — LGTM

These invariants squarely test the new API’s contract.

crates/storage/src/consensus_models/state_transition.rs (2)

13-18: StateVersionTransitions struct fits the new model — LGTM


41-49: Thin wrapper to the read transaction — LGTM

Name and behavior align with the “starting_at (inclusive)” semantics, matching past expectations.

crates/state_store_rocksdb/src/reader.rs (7)

60-62: ShardGroup/ShardStateVersions imports — LGTM


75-84: Consensus type imports updated to proof-based model — LGTM


96-97: State-tree payload types (Node) — LGTM


139-147: state_tree and shard_versions CF imports — LGTM


1657-1662: state_tree_nodes_get returning Node — LGTM


1664-1675: By-state-version node fetch — LGTM

The prefix scan and mapping to (NodeKey, Node) look correct.


1679-1681: Latest shard version — LGTM

Comment thread crates/consensus_tests/src/state_tree.rs
Comment thread crates/state_store_rocksdb/src/reader.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants