feat(sync): 009 - SubtreePrefetch Sync Strategy#1929
Conversation
Add SubtreePrefetch sync protocol types for deep trees with clustered changes. This protocol is optimized for scenarios where: - Tree depth > 3 levels - Divergence < 20% - Changes are clustered in subtrees Trade-off: O(1) round trips per subtree vs HashComparison's O(depth), but may over-fetch data compared to HashComparison's minimal transfer. Types added: - SubtreePrefetchRequest: Request subtrees by root hash with depth limit - SubtreePrefetchResponse: Contains fetched subtrees and not-found roots - SubtreeData: Single subtree with entities for CRDT merge - should_use_subtree_prefetch(): Heuristic for protocol selection Security: - MAX_SUBTREE_DEPTH (64): Prevents deep traversal attacks - MAX_SUBTREES_PER_REQUEST (100): Limits request size - MAX_ENTITIES_PER_SUBTREE (10,000): Bounds per-subtree data - MAX_TOTAL_ENTITIES (100,000): Caps total response size - is_valid() methods on all types for post-deserialization validation - Saturating arithmetic in total_entity_count() to prevent overflow Includes 39 unit tests covering edge cases and exploit prevention.
There was a problem hiding this comment.
🤖 AI Code Reviewer
Reviewed by 1 agents | Quality score: 33% | Review time: 99.8s
🟡 1 warnings, 💡 1 suggestions, 📝 1 nitpicks. See inline comments.
🤖 Generated by AI Code Reviewer | Review ID: review-cb995cc9
This comment has been minimized.
This comment has been minimized.
Add LevelWise sync protocol types optimized for wide, shallow trees (depth <= 2 with many children per level). Implements level-by-level breadth-first synchronization for efficient sync of wide tree structures. This ports the LevelWise protocol from PR #1873 to current master, following the same modular pattern as SubtreePrefetch (PR #1929). ## Changes - Add `levelwise.rs` module with: - `LevelWiseRequest`: Request nodes at a specific tree level - `LevelWiseResponse`: Response containing level nodes with metadata - `LevelNode`: Individual node with optional leaf data - `LevelCompareResult`: Categorized comparison results - `compare_level_nodes()`: Compare local vs remote level nodes - `should_use_levelwise()`: Heuristic for protocol selection - Security hardening with DoS prevention: - `MAX_LEVELWISE_DEPTH = 64`: Prevent depth exhaustion attacks - `MAX_PARENTS_PER_REQUEST = 1000`: Limit request size - `MAX_NODES_PER_LEVEL = 10_000`: Prevent memory exhaustion - `is_valid()` methods on all wire protocol types - Comprehensive test suite (56 tests) covering: - Basic functionality and serialization roundtrips - Boundary conditions and edge cases - Security/exploit prevention scenarios - Cross-validation consistency
Fixes issues raised in PR review: 1. **depth() now always returns bounded value (High Severity)** - Changed return type from `Option<usize>` to `usize` - Returns `MAX_SUBTREE_DEPTH` when `max_depth` is `None` - Consumers always get a safe, bounded depth value 2. **is_valid() now validates max_depth (Medium Severity)** - Added check: `max_depth <= MAX_SUBTREE_DEPTH` when `Some` - Catches invalid values from untrusted deserialization 3. **max_depth field is now private (Medium Severity)** - Matches encapsulation pattern from hash_comparison module - Added `is_unlimited()` accessor to check if unlimited was requested - Prevents bypassing `depth()` accessor 4. **Extracted heuristic magic numbers to constants (Nitpick)** - `DEEP_TREE_THRESHOLD = 3` - `MAX_DIVERGENCE_RATIO = 0.20` - `MAX_CLUSTERED_SUBTREES = 5` Tests updated: - Added test_subtree_request_max_depth_validation - Added test_heuristic_constants_are_sensible - Updated existing tests to use new API (41 tests total)
There was a problem hiding this comment.
🤖 AI Code Reviewer
Reviewed by 1 agents | Quality score: 33% | Review time: 110.2s
🟡 1 warnings, 💡 1 suggestions, 📝 1 nitpicks. See inline comments.
🤖 Generated by AI Code Reviewer | Review ID: review-c5e2be46
|
Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.
Or push these changes by commenting: Preview (174dba2cc8)diff --git a/crates/node/primitives/src/sync/protocol.rs b/crates/node/primitives/src/sync/protocol.rs
--- a/crates/node/primitives/src/sync/protocol.rs
+++ b/crates/node/primitives/src/sync/protocol.rs
@@ -5,6 +5,7 @@
use borsh::{BorshDeserialize, BorshSerialize};
use super::handshake::{SyncCapabilities, SyncHandshake};
+use super::subtree::{DEEP_TREE_THRESHOLD, MAX_DIVERGENCE_RATIO};
// =============================================================================
// Protocol Kind (Discriminant-only)
@@ -236,7 +237,11 @@
}
// Rule 4: Deep tree with localized changes
- if remote.max_depth > 3 && divergence < 0.2 {
+ #[expect(
+ clippy::cast_possible_truncation,
+ reason = "DEEP_TREE_THRESHOLD is always small (currently 3)"
+ )]
+ if remote.max_depth > DEEP_TREE_THRESHOLD as u32 && divergence < MAX_DIVERGENCE_RATIO {
return ProtocolSelection {
protocol: SyncProtocol::SubtreePrefetch {
subtree_roots: vec![], // Will be populated during sync |
There was a problem hiding this comment.
🤖 AI Code Reviewer
Reviewed by 1 agents | Quality score: 33% | Review time: 133.2s
🟡 1 warnings, 📝 1 nitpicks. See inline comments.
🤖 Generated by AI Code Reviewer | Review ID: review-e7c1cabf
| } | ||
|
|
||
| #[test] | ||
| fn test_should_use_subtree_prefetch_edge_cases() { |
There was a problem hiding this comment.
🟡 Test doesn't actually verify total entity limit
The test creates 101 subtrees (100,000/1,000 + 1), which exceeds MAX_SUBTREES_PER_REQUEST (100), so is_valid() fails on the subtree count check before ever reaching the total entity count check.
Suggested fix:
Use fewer subtrees (e.g., 20) with more entities each (e.g., 5,001) so total exceeds 100,000 while each subtree stays under 10,000 and subtree count stays under 100.
| truncated: false, | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
📝 Nit: Consider documenting NaN/Inf behavior for divergence_ratio
The heuristic function silently rejects NaN values (returns false) since NaN < anything is false; this is probably correct but undocumented.
Suggested fix:
Add a note in the doc comment that NaN/Inf values will cause the function to return false.
Add LevelWise sync protocol types optimized for wide, shallow trees (depth <= 2 with many children per level). Implements level-by-level breadth-first synchronization for efficient sync of wide tree structures. This ports the LevelWise protocol from PR #1873 to current master, following the same modular pattern as SubtreePrefetch (PR #1929). - Add `levelwise.rs` module with: - `LevelWiseRequest`: Request nodes at a specific tree level - `LevelWiseResponse`: Response containing level nodes with metadata - `LevelNode`: Individual node with optional leaf data - `LevelCompareResult`: Categorized comparison results - `compare_level_nodes()`: Compare local vs remote level nodes - `should_use_levelwise()`: Heuristic for protocol selection - Security hardening with DoS prevention: - `MAX_LEVELWISE_DEPTH = 64`: Prevent depth exhaustion attacks - `MAX_PARENTS_PER_REQUEST = 1000`: Limit request size - `MAX_NODES_PER_LEVEL = 10_000`: Prevent memory exhaustion - `is_valid()` methods on all wire protocol types - Comprehensive test suite (56 tests) covering: - Basic functionality and serialization roundtrips - Boundary conditions and edge cases - Security/exploit prevention scenarios - Cross-validation consistency
|
|
||
| let response = SubtreePrefetchResponse::complete(subtrees); | ||
| assert!(!response.is_valid()); // Should be invalid due to total entity count | ||
| } |
There was a problem hiding this comment.
Test passes for wrong reason, total entity limit untested
Medium Severity
The test_subtree_response_validation_total_entity_limit test doesn't actually exercise the MAX_TOTAL_ENTITIES check. With entities_per_subtree = 1000, num_subtrees computes to (100_000 / 1000) + 1 = 101, which exceeds MAX_SUBTREES_PER_REQUEST (100). So is_valid() returns false at the subtree count check (line 287) before ever reaching the total entity count check (line 295). The assertion passes for the wrong reason, leaving the MAX_TOTAL_ENTITIES validation effectively untested — if that check were removed, no test would catch it.



Summary
Types Added
SubtreePrefetchRequest: Request subtrees by root hash with depth limitSubtreePrefetchResponse: Contains fetched subtrees and not-found rootsSubtreeData: Single subtree with entities for CRDT mergeshould_use_subtree_prefetch(): Heuristic for protocol selectionSecurity
MAX_SUBTREE_DEPTH(64): Prevents deep traversal attacksMAX_SUBTREES_PER_REQUEST(100): Limits request sizeMAX_ENTITIES_PER_SUBTREE(10,000): Bounds per-subtree dataMAX_TOTAL_ENTITIES(100,000): Caps total response sizeis_valid()methods on all types for post-deserialization validationtotal_entity_count()to prevent overflowTest Plan
Note
Low Risk
Mostly additive sync wire/type definitions and tests with explicit size/depth validation; minimal impact on existing behavior aside from exposing new protocol types/constants.
Overview
Adds a new
SubtreePrefetchsync protocol primitives module (sync/subtree.rs) with Borsh-serializable request/response/data types and a heuristic (should_use_subtree_prefetch) for choosing this strategy on deep, low-divergence, clustered changes.Updates
sync.rsto register the new submodule and re-export its types/constants, including explicitis_valid()bounds checks (depth, subtree counts, per-subtree and total entity limits) plus extensive unit tests covering serialization roundtrips and resource-exhaustion edge cases.Written by Cursor Bugbot for commit abdf8ee. This will update automatically on new commits. Configure here.