fix: correct midpoint level so odd-depth uploads don't revert#9
Merged
mickvandijke merged 2 commits intomainfrom Apr 27, 2026
Merged
fix: correct midpoint level so odd-depth uploads don't revert#9mickvandijke merged 2 commits intomainfrom
mickvandijke merged 2 commits intomainfrom
Conversation
3 tasks
mickvandijke
approved these changes
Apr 27, 2026
MerklePaymentLib.expectedRewardPools(depth) = 2^ceil(depth/2), but the client was computing midpoints at level ceil(depth/2) from the leaves. ant_merkle::get_nodes_at_level numbers levels from the leaves up (L=0 leaves, L=depth root) so a tree of 2^d leaves has 2^(d-L) nodes at level L. With L=ceil(d/2) the client produced 2^floor(d/2) pools, half the count for odd depths, and the on-chain payForMerkleTree call reverted with WrongPoolCount(expected, actual). Setting the midpoint level to floor(depth/2) yields 2^ceil(depth/2) midpoints, matching the contract for both even and odd depths. Observed in production as WrongPoolCount(16, 8): any file producing 65-128 chunks (depth 7) failed at payment regardless of network state. Files with chunk counts in 5-8 (depth 3) and 17-32 (depth 5) hit the same bug. Adds five regression tests: - reward_candidate_count_matches_contract_for_all_depths (every leaf count up to MAX_LEAVES) - depth_seven_produces_sixteen_pools_not_eight (the production failure) - depth_one_produces_two_pools_not_one (smallest odd-depth case) - every_midpoint_proof_verifies_for_all_depths (proofs still verify post-fix) - verify_merkle_proof_accepts_all_depths (full production verifier path)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ed36ae0 to
53fcb89
Compare
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.
Summary
WrongPoolCountreverts at the on-chainpayForMerkleTreecall for files producing odd-depth merkle trees (chunk counts in the bands 5-8, 17-32, 65-128). Production observed it asWrongPoolCount(16, 8)for files in the 65-128 chunk band.MerklePaymentLib.expectedRewardPools(depth) = 2^ceil(depth/2)(Solidity), but the client computed midpoints at levelceil(depth/2)from the leaves.ant_merkle::get_nodes_at_levelnumbers levels from the leaves up (L=0 leaves, L=depth root), so pickingL = ceil(d/2)returns2^floor(d/2)nodes — half the count for odd depths.midpoint_level = floor(depth/2), which yields2^(d - floor(d/2)) = 2^ceil(d/2)nodes — matches the contract for both even and odd depths.expected_reward_poolsis unchanged (already2^ceil(d/2)since fix: payment hardening -- pricing, panics, external signer #5).Why the previous attempt missed this
PR #5's commit
f4cdd45("align pool count formula with Solidity contract (floor->ceil)") flipped bothexpected_reward_poolsandmidpoint_levelto ceil in lockstep. That fixesexpected_reward_pools(correct) but leaves the bug inmidpoint_levelbecauseant_merklelevels are counted from the leaves — flipping floor to ceil there moves the cut closer to the root and halves the pool count for odd depths. The doc comment now spells this out so the next reader doesn't repeat the mistake.Test plan
cargo test --lib— 16 passed (5 new merkle regression tests + 11 pre-existing)cargo clippy --all-targets -- -D warningscleancargo fmtcleanRisk
depth.div_ceil(2)->depth / 2) in a non-public function. No wire-format change, no contract change.floor(d/2) == ceil(d/2)for even d.midpoint_proof_depthis unchanged and remainsceil(d/2)— that is the path length from levelfloor(d/2)to the root, which isd - floor(d/2) = ceil(d/2). Theevery_midpoint_proof_verifies_for_all_depthsandverify_merkle_proof_accepts_all_depthstests confirm the proof shape stays consistent post-fix.