Skip to content

fix(antd): accept empty tx_hashes for all-already-stored wave-batch finalize - #239

Merged
Nic-dorman merged 2 commits into
mainfrom
bug/grpc-empty-tx-hashes-finalize
Aug 20, 2026
Merged

fix(antd): accept empty tx_hashes for all-already-stored wave-batch finalize#239
Nic-dorman merged 2 commits into
mainfrom
bug/grpc-empty-tx-hashes-finalize

Conversation

@Nic-dorman

Copy link
Copy Markdown
Member

Summary

Fixes #233 (reported by @JimCollinson): the gRPC UploadService.FinalizeUpload handler rejected an empty tx_hashes map with invalid_argument, even for the valid wave-batch case where prepare reported zero payments because every chunk is already stored. ant-core and REST both accept that no-payment finalize, so a repeated/content-deduplicated public upload could prepare over gRPC but never finalize and return its DataMap.

The second half of #233 — the handler consuming upload_id before validating — was already fixed on main by the 0.12.0 multi-batch rework (peek-before-consume, mirroring REST); this PR completes the remaining half and hardens the same failure mode one step further.

Change

Extracted the wave-batch validation into a shared resolve_wave_tx_hashes in rest/upload.rs (mirroring resolve_winner_pool_hashes), now used by both transports. The shape peek captures the prepare's expected quote hashes, and the resolver checks the request against them before the pending upload is consumed:

  • Empty map is valid exactly when prepare reported no payments (the all-already-stored case). When payments were expected, the error names the expected count.
  • Incomplete maps are caught up front: previously a map missing a receipt for a reported quote passed daemon validation and only failed inside ant-core's build_paid_chunksafter the upload_id had been consumed, stranding the prepared upload exactly like antd gRPC FinalizeUpload rejects empty tx_hashes for all-already-stored uploads #233's empty-map case. Now it's a pre-consume invalid_argument/400 naming the missing quote.
  • Unknown extra entries stay tolerated (ant-core ignores them), and zero-amount quotes need no receipt (they never enter payment_intent.payments).

Also de-duplicates the previously identical ~35-line parse block that existed in both handlers.

Wire compatibility

None — proto3 cannot distinguish an absent map from an empty one, so this is purely daemon-side validation. No proto shape change, no client regen; all typed gRPC bindings inherit the fix. upload.proto / openapi.yaml / docs/external-signer-flow.md comment-only updates document the empty-map contract. REST behavior change is strictly early-error: a present-but-empty tx_hashes with payments expected now 400s before consuming the upload instead of failing in ant-core after consuming it (absent tx_hashes remains a 400 as before).

Tests

New deterministic unit tests over the pure resolver (same convention as the resolve_winner_pool_hashes suite — there is no network-backed gRPC harness in this crate):

  • empty_map_valid_when_no_payments_expected — the antd gRPC FinalizeUpload rejects empty tx_hashes for all-already-stored uploads #233 repro: zero payments → empty map → Ok
  • empty_map_rejected_when_payments_expected — protective rejection kept, pre-consume
  • missing_receipt_rejected_before_consuming_upload — partial map caught before the upload_id is consumed
  • full_coverage_resolves_with_extra_entries_tolerated — 0x-prefixed + bare hex, extras pass
  • bad_hex_rejected — parse errors preserved

cargo test (50 passed), cargo clippy --all-targets -- -D warnings, cargo fmt --check all green.

Validation ordering (validate → only then consume) is structural in both handlers, so every rejection above leaves the prepared upload present and retryable.

Linear

V2-1026

🤖 Generated with Claude Code

…inalize

A wave-batch prepare where every chunk is already stored reports zero
payments, and ant-core + REST both accept the resulting no-payment
finalize — but the gRPC FinalizeUpload handler rejected an empty
tx_hashes map outright, so a repeated/deduplicated upload could prepare
over gRPC but never get its DataMap (ant-sdk#233).

Extract the wave-batch validation into a shared resolve_wave_tx_hashes
(mirroring resolve_winner_pool_hashes) used by both transports:

- an empty map is valid exactly when prepare reported no payments;
  otherwise it errors naming the expected payment count
- every reported quote must have a receipt: ant-core only rejects a
  missing one after the pending upload has been consumed, so catch the
  incomplete map up front while the upload_id is still retryable
- unknown extra entries stay tolerated (ant-core ignores them)

Validation keeps running before the pending upload is consumed on both
transports, so all rejections preserve retry state. Wire-neutral:
proto3 cannot distinguish absent from empty maps, so no proto shape
change and no client regen — proto/OpenAPI/flow-doc comments updated
only.

Fixes #233
Linear: V2-1026

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dirvine

dirvine commented Aug 20, 2026

Copy link
Copy Markdown
Member

Hermes review

Verdict: no material findings; merge looks safe.

Reviewed exact head 53889882c22b6aa9bf07ee92f356ee440e2aeb9e.

  • Empty tx_hashes is accepted only when the stored prepared upload has no expected paid quote hashes.
  • Missing or malformed receipts are rejected before the pending upload is consumed, so the request remains retryable.
  • REST and gRPC use the same resolver; extra unknown receipts remain tolerated, matching ant-core behaviour.
  • The pinned ant-core explicitly creates an empty WaveBatch only when all chunks are already stored (ant-core/src/data/client/file.rs:1653-1660); finalising that path with an empty map does not bypass payment for new storage.

Verification on this head:

  • antd: 50 tests passed
  • antd-rust: 82 unit tests and 2 doc tests passed
  • formatting and Clippy passed for both crates
  • GitHub CI: 3/3 checks green

Review panel: five seats recommended merge; one objected that an unexpectedly empty payment intent could permit unpaid storage. Direct inspection of the pinned ant-core lifecycle disproved that scenario for this path.

— Hermes

@Nic-dorman
Nic-dorman merged commit a4accf1 into main Aug 20, 2026
3 checks passed
@Nic-dorman
Nic-dorman deleted the bug/grpc-empty-tx-hashes-finalize branch August 20, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

antd gRPC FinalizeUpload rejects empty tx_hashes for all-already-stored uploads

2 participants