Skip to content

feat(solana): poll media jobs with a wallet ownership proof, not a payment - #83

Merged
VickyXAI merged 5 commits into
mainfrom
feat/solana-poll-ownership
Oct 11, 2026
Merged

VickyXAI merged 5 commits into
mainfrom
feat/solana-poll-ownership

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

Implements the sol.blockrun.ai poll-ownership contract (live since rev 00561-ghx; server source blockrun-sol/src/lib/poll-ownership.ts).

What changes (Solana wallet rail only)

  • Media polls send x-poll-wallet / x-poll-timestamp / x-poll-signature: an ed25519 signature by the submitting wallet over blockrun-poll:v1:<video|audio|image>:<job id>:<unix seconds>, signed fresh per poll. The job id is the URL-decoded last segment of poll_url. Uses the solders Keypair behind the existing x402 signer, so there's no new dependency.
  • Video: pending polls carry only the proof and never call the payment signer. The finished-but-unpaid poll answers 402 with the challenge and status:"completed". The client signs one payment from that challenge (terms pinned to the submit's) and re-sends the GET. If the paid poll gets a 402 (concurrent claim or stale blockhash), the client goes back to proof polling, which returns already_settled or a fresh challenge. Payments are capped at 1 + max_resigns.
  • Images and music: these were paid at POST, so their polls never pay. A batch-billed image 402 falls back to the payment path.
  • Fallback: if the gateway refuses the proof (video 400 Invalid job id, audio/image 404 Job not found), the client falls back once, for that job, to the existing payment-signature polling. This is logged as a warning on blockrun_llm.solana_client.
  • Fix: slow music() tracks on the Solana wallet rail are now polled. Before this, the 202 stub was returned as the result and failed MusicResponse validation after the track had been paid for.
  • Unchanged: the account rail (API key), Base clients (jobs.py, music.py, video.py, image.py), and any signer without a keypair.

The poll loop moved from _request_image_with_payment into _poll_media_job (sync and async) so music can share it. The legacy branch is the old code, kept verbatim.

Tests

  • tests/unit/test_solana_poll_ownership.py (34 tests). Signatures are checked with an independent pure-Python RFC 8032 verifier and base58 decoder. The fake gateways enforce the contract.
  • They cover: exact message bytes; kind/job id parsing; pending video polls with no signer calls; completion 402 leading to exactly one payment and then a 200; in-flight settlement not paying twice; capped completion payments; a repriced completion challenge being refused; image and music polls never paying; batch 402 fallback; refusal fallback (including another wallet's proof); account rail and Base music never sending a proof. Each case runs sync and async.
  • Planted defects all went red, then were reverted: v1→v2 message (11 failed), completion signing the submit challenge (sync 3 / async 1), proof polls also carrying the payment header (11), fallback disabled (5), job id not URL-decoded (7), uncapped completion payments (1).
  • black --check ., ruff check . and pytest tests/unit all pass (1152 passed). No new mypy errors in the touched files. No version bump, no publish, and the paid API was not called.

1bcMax added 5 commits October 10, 2026 19:06
…yment

A Solana payment dies with its blockhash in ~60-90s, so polling a
multi-minute video meant signing a fresh payment on almost every poll.
sol.blockrun.ai (rev 00561-ghx) accepts an ed25519 proof instead:
x-poll-wallet / x-poll-timestamp / x-poll-signature over
"blockrun-poll:v1:<video|audio|image>:<job id>:<unix seconds>".

- SolanaLLMClient / AsyncSolanaLLMClient sign that proof fresh per poll with
  the wallet key they already hold (solders Keypair; no new dependency).
- Video: pending polls carry only the proof. The finished-but-unpaid poll
  answers 402 + challenge + status "completed"; one payment is signed from
  THAT challenge (terms pinned to the submit's) and the GET re-sent. A paid
  poll that 402s (concurrent claim, stale blockhash) goes back to proof
  polling, which answers already_settled or a fresh challenge, bounded by
  1 + max_resigns payments.
- Images and music were paid at POST: their polls never sign a payment. A
  batch-billed image 402 falls back to the payment path.
- A "no such job" refusal of the proof falls back once, per job, to the
  existing payment-signature polling, logged on blockrun_llm.solana_client.
- Slow music() tracks on the wallet rail are now polled at all: the 202
  stub used to be returned and failed MusicResponse validation after the
  track was paid for.
- Account rail and Base unchanged; a signer without a keypair keeps the
  payment path.
…ideo

- Spend limits: image/image_edit/video/video_from_content check
  max_cost_per_call and max_session_cost before signing the submit payment
  (from the challenge's Solana exact accepts; the signed amount before
  sending when the challenge has none), and before signing a video's
  completion payment.
- A paid completion poll lost in transit resumes ownership polling (the
  settlement record answers) instead of abandoning the clip; Retry-After on a
  paid 402 is honoured; exhaustion, timeouts and errors after the payment
  went out say it may still settle and are tagged settled.
- Under ownership polling only a 200 delivers: the completion 402's
  status "completed" no longer turns a 5xx/HTML answer into the result.
  503 on an ownership/paid poll is retried.
- A clip settled by an earlier poll/call is logged and cached at zero cost.
- A completion payment signed at the deadline is still sent; none is signed
  after it.
- _last_raw_payment is per thread / asyncio task and reset before the cache
  check, so concurrent music calls never poll or pay against another call's
  challenge.
- Any exception escaping a paid-at-POST job's poll is tagged settled
  (covers PaymentError on the music fallback).
…58 package

_create_signer decoded a 32-byte seed with `import base58`, which is not a
dependency of blockrun-llm[solana]; on a clean install (CI 3.11) every
seed-only key was refused as invalid. Decode it with solders (a 32-byte base58
string parses as a Pubkey) instead. Caught by
test_real_wallet_key_gives_the_payer_keypair, red on CI before this.
Also de-flakes test_budget_runs_out_after_the_completion_payment (a 0.3s
budget raced the first completion sign under async).
@VickyXAI
VickyXAI merged commit 8c0bdaf into main Oct 11, 2026
6 checks passed
@VickyXAI VickyXAI mentioned this pull request Oct 11, 2026
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.

1 participant