repro: the spend path discards the remove_mark outcome - #2730
Draft
dorianvp wants to merge 1 commit into
Draft
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Claim
pepper-sync/src/sync/spend.rs:366-368(update_spent_notes_by_protocol) callsshard_tree.remove_mark(position, Some(&spending_height)).expect("infallible")and discards the returnedbool. In shardtree 0.7.1 (src/lib.rs,ShardTree::remove_mark) that bool carries two silent outcomes:spending_heightis at or above the oldest checkpoint id but no checkpoint with exactly that id exists,update_checkpoint_withfinds nothing,remove_markreturnsOk(false), and the mark is kept. Nothing records the spend in the tree.spending_heightis below the oldest checkpoint id, the mark is cleared directly andOk(true)is returned. No checkpoint records the removal, so a rollback cannot restore it.The crate's
raw_boolean_calls_are_confined_to_this_modulelint test inpepper-sync/src/shardtree_ext.rswrapscheckpointandtruncate_to_checkpointand does not coverremove_mark.What the tests do
All three tests live in
pepper-sync/src/sync/spend.rs, moduleremove_mark_outcome_repro.library_remove_mark_without_checkpoint_at_height_is_silent_noopis a library fact. A saplingMemoryShardStoretree with a marked leaf checkpointed at height 10 and a second checkpoint at 30 receivesremove_mark(0, Some(&20)). It returnsOk(false), the leaf keepsRetentionFlags::MARKED, no checkpoint records the position, andwitness_at_checkpoint_idstill succeeds. This test passes and documents what pepper-sync silently depends on.library_remove_mark_below_oldest_checkpoint_is_not_restored_by_rollbackis a library fact. With checkpoints 6..10 and the marked leaf,remove_mark(0, Some(&2))returnsOk(true), the flag is cleared, no checkpoint records it, and afterrollback_to_checkpoint(6)the leaf cannot be witnessed. This test passes.update_spent_notes_drops_remove_mark_outcomeis the crate path. AMockWalletholds a confirmed funding transaction with a sapling note at position 0 and a known nullifier, a spending transaction confirmed at height 20, a nullifier map entry pointing at it, and a sapling tree whose checkpoints are 0, 10 and 30. Spend detection andupdate_spent_notes(.., remove_marks = true)run. The note'sspending_transactionis set, and the test then asserts the intended invariant that the mark is either cleared or recorded for removal in a checkpoint. This test fails on current code.Observed failing output
Which part is library behaviour and which part is pepper-sync
Outcomes (a) and (b) are shardtree behaviour and are documented by the two passing library tests. The pepper-sync defect is that
update_spent_notes_by_protocolcannot tell outcome (a) apart from a successful removal, so a retention leak for a spent note passes without any signal. Outcome (b) is the intended shardtree behaviour for heights older than the reorg window and is included for completeness, since pepper-sync does not record which path was taken.Reachability note for (a) in production:
SyncShardTrees::update_shard_treesadds a checkpoint at every height in the topMAX_REORG_ALLOWANCEscanned blocks, so the checkpoint set is dense there and a spend inside that window finds its checkpoint. The failing test sets up a sparse checkpoint set directly. A sparse set needs fewer thanMAX_REORG_ALLOWANCEwindow checkpoints plus block-level checkpoints surviving from an out-of-order lower scan, which this PR does not drive end to end.This PR is a reproduction and not a fix. The sync-bench A/B rule was not run because the commit adds a test only.
🤖 Generated with Claude Code