Summary
Re-verified on v4.1-dev at 259ae1d105 after the #4004/#4005/#4008 reconcile/watermark rework — the finding's original "watermark never advances" shape is partially fixed, but a regression-to-zero defect remains live:
- Fixed half:
new_sync_height now advances on an empty delta (rs-sdk sets it from the response metadata tip), so the height watermark no longer stalls.
- Live half: rs-sdk's
incremental_catch_up sets result.last_known_recent_block only from actually returned recent-tree entries — on a quiet pass (zero entries) it stays 0 by design (the boundary cursor must be a real tree key). PlatformPaymentAddressProvider::update_sync_state then copies all three scalars verbatim, so a previously stored non-zero boundary watermark is wiped back to 0 on every empty delta (BLAST cadence ≈ 15 s).
Consequences:
PlatformAddressWallet::sync_watermark() maps 0 → None, so the watermark surface reports "no watermark" after any quiet pass — the exact pa_007 symptom from the e2e run (all sync_watermark() reads return None).
- The in-memory value now disagrees with the persisted one:
PlatformAddressChangeSet only carries last_known_recent_block when > 0 and merges monotonically, so disk keeps the old boundary while memory holds 0 — after restart the value reappears, then vanishes again on the next quiet pass.
- With the boundary lost, the next pass falls back from exclusive
RangeAfter(boundary) to inclusive RangeFrom(sync_height); an AddToCredits entry sitting exactly at the boundary block can be re-applied — the same double-count family as ADDR-09, reintroduced through the fallback.
Affected crate + location
Root cause
Contract mismatch between rs-sdk and rs-platform-wallet: rs-sdk uses 0 as "no boundary observed this pass", while the provider treats the copied value as "the boundary watermark" (and its own persistence layer already assumes monotonicity via the > 0 gate and .max() merge). Nothing keeps the previous non-zero value when the new result carries the 0 sentinel.
Reproduction
Crate-level unit test (no network):
- Crate-level repro:
found_032_empty_delta_must_not_regress_recent_block_watermark
(in packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs, branch repro/pr3549-platform-a)
- Command:
cargo test -p platform-wallet found_032_empty_delta_must_not_regress_recent_block_watermark
- RED by design: fails today with
stored 42 before the pass, got 0 after; passes once the provider (or the result contract) preserves the boundary across empty deltas.
Expected vs Actual
- Expected: the recent-block boundary watermark is monotone across successful passes — an empty delta leaves it unchanged (matching the persistence layer's own
> 0/.max() semantics); sync_watermark() keeps reporting the last known boundary.
- Actual: any quiet pass zeroes it in memory;
sync_watermark() flips to None; memory and disk diverge until restart.
Severity
- risk: 0.75 (every quiet 15-second BLAST pass triggers it; quiet passes are the common case)
- impact: 0.5 (watermark surface unusable for progress/diagnostics; memory/disk divergence; boundary-block delta re-application window reintroduces an ADDR-09-shaped double-count)
- scope: 0.4 (platform-address sync subsystem, all wallets)
- overall = (0.75+0.5+0.4)/3 = 0.55 → MEDIUM (3)
Suggested fix direction
Smallest fix: in update_sync_state, apply the same monotone gate the persistence layer uses —
if result.last_known_recent_block > 0 {
self.last_known_recent_block = result.last_known_recent_block;
}
(Height/timestamp stay verbatim — those do advance on empty deltas.) Alternatively, change the rs-sdk contract so AddressSyncResult echoes the input boundary when no new entry supersedes it — but that widens the change into every sync_address_balances consumer; the provider-side gate is self-contained. Keeping a stale boundary is safe by design: the next pass's RangeAfter proof detects a compacted-away boundary and runs the compacted phase.
Cross-references
Note on the sh_012 half of the original finding
The e2e also observed ShieldedInsufficientBalance available=0 (local balance map not refreshed on empty delta). On the current base the incremental-only branch seeds result.found from current_balances() and the full-scan branch rebuilds it from the tree, so that half appears addressed by the #4004/#4005 rework; only the boundary-watermark regression above was still reproducible from source. If sh_012 still reds on a live run after this fix, it should be re-triaged separately.
🤖 Co-authored by Claudius the Magnificent AI Agent
Summary
Re-verified on
v4.1-devat259ae1d105after the #4004/#4005/#4008 reconcile/watermark rework — the finding's original "watermark never advances" shape is partially fixed, but a regression-to-zero defect remains live:new_sync_heightnow advances on an empty delta (rs-sdk sets it from the response metadata tip), so the height watermark no longer stalls.incremental_catch_upsetsresult.last_known_recent_blockonly from actually returned recent-tree entries — on a quiet pass (zero entries) it stays0by design (the boundary cursor must be a real tree key).PlatformPaymentAddressProvider::update_sync_statethen copies all three scalars verbatim, so a previously stored non-zero boundary watermark is wiped back to 0 on every empty delta (BLAST cadence ≈ 15 s).Consequences:
PlatformAddressWallet::sync_watermark()maps0 → None, so the watermark surface reports "no watermark" after any quiet pass — the exactpa_007symptom from the e2e run (allsync_watermark()reads returnNone).PlatformAddressChangeSetonly carrieslast_known_recent_blockwhen> 0and merges monotonically, so disk keeps the old boundary while memory holds 0 — after restart the value reappears, then vanishes again on the next quiet pass.RangeAfter(boundary)to inclusiveRangeFrom(sync_height); anAddToCreditsentry sitting exactly at the boundary block can be re-applied — the same double-count family as ADDR-09, reintroduced through the fallback.Affected crate + location
packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs:493-500(update_sync_state— verbatim copy)packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs:685-689(sync_watermarkmaps 0 →None)packages/rs-sdk/src/platform/address_sync/mod.rs:774-781(last_known_recent_block = highest_recent_block, 0 on an empty delta — documented as intentional on the rs-sdk side)Root cause
Contract mismatch between rs-sdk and rs-platform-wallet: rs-sdk uses
0as "no boundary observed this pass", while the provider treats the copied value as "the boundary watermark" (and its own persistence layer already assumes monotonicity via the> 0gate and.max()merge). Nothing keeps the previous non-zero value when the new result carries the0sentinel.Reproduction
Crate-level unit test (no network):
found_032_empty_delta_must_not_regress_recent_block_watermark(in
packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs, branchrepro/pr3549-platform-a)stored 42 before the pass, got 0 after; passes once the provider (or the result contract) preserves the boundary across empty deltas.Expected vs Actual
> 0/.max()semantics);sync_watermark()keeps reporting the last known boundary.sync_watermark()flips toNone; memory and disk diverge until restart.Severity
Suggested fix direction
Smallest fix: in
update_sync_state, apply the same monotone gate the persistence layer uses —(Height/timestamp stay verbatim — those do advance on empty deltas.) Alternatively, change the rs-sdk contract so
AddressSyncResultechoes the input boundary when no new entry supersedes it — but that widens the change into everysync_address_balancesconsumer; the provider-side gate is self-contained. Keeping a stale boundary is safe by design: the next pass'sRangeAfterproof detects a compacted-away boundary and runs the compacted phase.Cross-references
pa_007_sync_watermark.rs,sh_012_sync_watermark_idempotency.rs).Note on the sh_012 half of the original finding
The e2e also observed
ShieldedInsufficientBalance available=0(local balance map not refreshed on empty delta). On the current base the incremental-only branch seedsresult.foundfromcurrent_balances()and the full-scan branch rebuilds it from the tree, so that half appears addressed by the #4004/#4005 rework; only the boundary-watermark regression above was still reproducible from source. If sh_012 still reds on a live run after this fix, it should be re-triaged separately.🤖 Co-authored by Claudius the Magnificent AI Agent