fix(payment): widen receiver storage admission to uploader over-query window#148
Closed
jacderida wants to merge 1 commit into
Closed
fix(payment): widen receiver storage admission to uploader over-query window#148jacderida wants to merge 1 commit into
jacderida wants to merge 1 commit into
Conversation
… window Direct client PUT receiver admission (`validate_store_membership`) rejected honest uploads on NAT-simulated testnets: the storer decided it was "not among this node's local 9 closest peers" for a chunk it is XOR-close to. Root cause: on a network with unreachable (NAT'd, relay-only) peers the client cannot quote the genuinely-closest nodes, so it legitimately pays a peer that ranks just past `close_group_size` in a storer's full local routing-table view (which still contains those closer, client-unreachable peers). The admission window was `close_group_size + STORAGE_ADMISSION_MARGIN` (= 9), narrower than the divergence, so the receiver rejected the paid PUT. On the single-node payment path — which fails the whole file on one un-stored chunk — this produced multiplicative per-file upload failures. v0.12.0-rc.1 (which had no admission gate) achieved 100% uploads on the same fault-injection environment; the gate was introduced in #136 and is the regression. Fix: size the storage-admission window to the uploader's quote over-query window (`close_group_size * STORAGE_ADMISSION_OVERQUERY_MULTIPLIER`, mirroring ant-client `CLOSE_GROUP_SIZE * FAULT_TOLERANT_QUOTE_QUERY_MULTIPLIER`), so a storer admits a chunk it is XOR-close to within the same window the uploader selected from. The check stays XOR-only (#140). Admission and stored-record retention stay coupled (a node that accepts a chunk also retains it); audit, quorum, and paid-list target sets are unchanged at `close_group_size`. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Problem
On NAT-simulated testnets (STG-01,
rc-2026.6.2), ~3–6% of single-node uploads fail. Each failed upload reportsN-1/N stored, 1 failed, and the storer-side rejection is:Because the single-node payment path fails the whole file on one un-stored chunk, the per-chunk rejection rate compounds into per-file failures.
Regression
The previous staging run on the same fault-injection environment (30% NAT-simulated nodes, ±2h clock-skewed clients, +3h-skewed node) running
v0.12.0-rc.1achieved 1,724/1,724 uploads (100%) with 2 retried chunks of 69,403.v0.12.0-rc.1had no single-node receiver/issuer close-group admission gate at all.The gate was introduced after that run by #136 (
feat(payment): accept flexible single-node proof bundles) —validate_store_membership(receiver) and the paid-quote-issuer check. It has since been tuned by #137 (margin), #140 (XOR-only — correct, kept), and #147 (issuer width →K_BUCKET_SIZE). #140 fixed the reachability re-rank; the receiver width was left atclose_group_size + STORAGE_ADMISSION_MARGIN(= 9), which is the remaining binding constraint.Cause
On a network with unreachable (NAT'd, relay-only) peers, the client cannot quote the genuinely-closest nodes, so it legitimately pays a peer that ranks just past
close_group_sizein a storer's full local routing-table view — that view still contains the closer peers the client could not reach. Aclose_group_size + 2admission window is narrower than that divergence, so the storer rejects an honest payment for a chunk it is genuinely XOR-close to.The merkle path is largely spared because its closeness check already uses a much wider window (
2 * CANDIDATES_PER_POOL= 32, 9-of-16 majority) for exactly this reason.Fix
Size the storage-admission window to the uploader's quote over-query window:
close_group_size * STORAGE_ADMISSION_OVERQUERY_MULTIPLIER, where the multiplier (2) mirrors ant-clientget_store_quotes(CLOSE_GROUP_SIZE * FAULT_TOLERANT_QUOTE_QUERY_MULTIPLIER). A storer now admits a chunk it is XOR-close to within the same window the uploader selected its close group from. Production close group of 7 → admission window 14 (was 9).find_closest_nodes_local_by_distance_with_self, fix(payment): use XOR-only local lookup for close-group verification #140 preserved).storage_admission_widthdrives both, avoiding accept-then-prune churn; the 3-day prune hysteresis is unchanged.close_group_size(they do not usestorage_admission_width), so Sybil/payment-integrity guarantees are not loosened.K_BUCKET_SIZE) is untouched.If 14 proves insufficient under heavier NAT clustering, the multiplier is a one-line bump.
Verification
cargo check -p ant-node✅cargo test -p ant-node --lib storage_admission_width✅ (storage_admission_width_is_overquery_window)not among local 9 closestrejections disappear — the receiver gate's live behaviour needs a real routing table (the handler only exposes a boolean test-override seam), so it isn't unit-testable in isolation.🤖 Generated with Claude Code