Skip to content

fix(remote_signer): bind live payment manifest id to auth token session id - #4006

Closed
seanhanca wants to merge 2 commits into
livepeer:feat/remote-signer-byoc-v2from
seanhanca:fix/live-payment-manifest-session-id
Closed

fix(remote_signer): bind live payment manifest id to auth token session id#4006
seanhanca wants to merge 2 commits into
livepeer:feat/remote-signer-byoc-v2from
seanhanca:fix/live-payment-manifest-session-id

Conversation

@seanhanca

Copy link
Copy Markdown

Summary

The remote signer's POST /generate-live-payment handler
(server/remote_signer.goGenerateLivePayment) set the segment
ManifestID from the request (req.ManifestID), falling back to a fresh
core.RandomManifestID() when empty. The orchestrator's live-runner (v0.9.0)
enforces SegData.ManifestID == AuthToken.SessionId for fixed live
payments
and rejects any mismatch with 403 mismatched manifest and auth token.

Result: native live generation failed at the final hop even though auth,
per-cap pricing, payment mint, the 402 challenge, and reservation all
succeeded.

Fix

For non-BYOC (live) payments, bind the manifest id to the challenge's
oInfo.AuthToken.SessionId (the session id echoed back from the 402 challenge
OrchestratorInfo). genSegCreds then emits a matching (manifestID, sessionId) pair, so the orchestrator's check passes.

  • Only applies when the orchestrator info carries a session id
    (AuthToken.GetSessionId() != ""); otherwise behavior is unchanged
    (nil-safe getter).
  • BYOC is deliberately excluded — it keeps its capability-name manifest id
    for shared balance tracking across streams.
  • The session id is stable for the life of a live session, so stateful
    (continuing) requests stay tied to the same id — the reason the previous
    "missing manifestID with state" guard existed. That guard now only fires when
    there is no session id to bind to.

Bug location (exact)

server/remote_signer.go, GenerateLivePayment: manifestID := req.ManifestID
(~L401) then manifestID = string(core.RandomManifestID()) (~L419) — the
manifest id was never tied to oInfo.AuthToken.SessionId, which
genSegCreds also emits into SegData.AuthToken (server/segment_rpc.go
L693/L700).

Scope

Single, self-contained change in GenerateLivePayment. The manifest id is not
threaded through additional call sites; streamParams.ManifestID already flows
into genSegCreds, so setting the variable is sufficient.

Test plan

  • Adds TestGenerateLivePayment_LV2V_ManifestBoundToSessionId: decodes the
    returned segCreds and asserts SegData.ManifestID == AuthToken.SessionId
    both when the request supplies a different manifest id and when it omits
    one entirely.
  • Updates TestGenerateLivePayment_StateValidationErrors "missing manifest
    id with state" case to use an empty AuthToken so the guard is still
    exercised (it now only applies when no session id is available).
  • go test ./server/ -run TestGenerateLivePayment — runs in CI. (Could not
    run locally: this environment lacks go-livepeer's patched ffmpeg toolchain
    (~/compiled), so the lpms cgo build fails independently of this change.
    gofmt -e confirms both files parse and are formatted.)

Deploy (for John / pymthouse)

Not deployed here. The pymthouse signer image
(docker/signer-dmz/Dockerfile.signer) is built FROM livepeer/go-livepeer:sha-….
After this merges, rebuild the signer image from the updated
feat/remote-signer-byoc-v2 (or cherry-pick this commit) and repoint
pymthouse-signer-test to the new tag. No orchestrator/infra change required.

Made with Cursor

…on id

The orchestrator's live-runner enforces SegData.ManifestID ==
AuthToken.SessionId for fixed live payments and rejects a mismatch with
"mismatched manifest and auth token". GenerateLivePayment derived the
manifest id from the request (or a fresh RandomManifestID when empty),
so v0.9.0 live generation failed with 403 even though auth, pricing,
the 402 challenge, and reservation all succeeded.

Bind the live manifest id to the challenge's oInfo.AuthToken.SessionId
for non-BYOC payments so genSegCreds emits a matching (manifestID,
sessionId) pair. BYOC keeps its capability-name manifest id for shared
balance tracking across streams.

Adds TestGenerateLivePayment_LV2V_ManifestBoundToSessionId proving the
emitted SegData.ManifestID equals the challenge session id whether the
request supplies a mismatched manifest id or omits it entirely, and
updates the state-validation case so the "missing manifestID" guard is
exercised only when no session id is available to bind to.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d25afcc-7f2a-4e5a-9ea8-0e75b3a1c7da

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the go Pull requests that update Go code label Jul 29, 2026
The native (non-BYOC) live payment path types fixed-price single-shot
live-runner generations as "lv2v" and, when no explicit unit count is
supplied, always recomputes pixels as a continuous 720p30 stream estimate
(720*1280*30*60 = 1,658,880,000 pixels). Multiplying the orchestrator's
per-request fixed price (advertised with PixelsPerUnit == 1) by that
volume inflates the fee ~1.66e9x, so newBalanceUpdate/StageUpdate needs
billions of tickets and GenerateLivePayment fails the max-100 guard
(observed e2e: HTTP 400 "numTickets 2721947758 exceeds maximum of 100").

The v0.9.0 live-runner orchestrator debits exactly one unit for a fixed
generation (server/ai_http.go reservePaidLiveRunnerSession -> AccountPayment
units:1, requiring ExpectedPrice.PixelsPerUnit == 1). Honor an explicit
req.InPixels on the lv2v path instead of unconditionally overriding it with
the 720p estimate, so the gateway can charge exactly one unit (inPixels:1)
and the fee equals the fixed per-request price (numTickets ~1). The
continuous live-video path sends no InPixels and is unchanged.

Sibling of the manifest-id fix on the same GenerateLivePayment path.

Note: this is inert until the gateway sends inPixels:1 for fixed-unit
(price_info.unit == "fixed") live runners.
@seanhanca

Copy link
Copy Markdown
Author

Stacked a second fix on the same GenerateLivePayment path (commit cd99507).

Second bug: numTickets explodes for fixed-price live-runner generations

Driving the native v0.9.0 live-runner path (type:"lv2v"), /generate-live-payment returned HTTP 400 numTickets 2721947758 exceeds maximum of 100.

Root cause. For type:"lv2v" with no explicit unit count, the signer recomputes pixels as a continuous 720p30 stream estimate:

pixels = 720 * 1280 * 30 * 60 = 1,658,880,000   // server/remote_signer.go
fee    = (PricePerUnit / PixelsPerUnit) * pixels // server/live_payment.go calculateFee
numTickets = ceil(fee / ev)                      // core/accounting.go StageUpdate

A fixed-price single-shot fal generation (e.g. flux-schnell, PriceInfo = 1648852084881/1, $0.00315) is priced per request (PixelsPerUnit == 1), not per video pixel. Multiplying by ~1.66e9 pixels inflates the fee ~1.66e9x, so numTickets blows past the max-100 guard.

Why this is correct (not just raising the cap). The v0.9.0 orchestrator debits exactly one unit for a fixed generation — server/ai_http.go reservePaidLiveRunnerSessionAccountPayment{units: 1, priceInfo: ExpectedPrice}, and it rejects the payment unless ExpectedPrice.PixelsPerUnit == 1. So the correct fee is PricePerUnit * 1 (numTickets ~1), matching the orch's units:1 accounting.

The fix. Honor an explicit req.InPixels on the lv2v path instead of unconditionally overriding it with the 720p estimate. The continuous live-video path sends no InPixels and is byte-identical.

The signer cannot self-identify "fixed" — the gateway must pair

The 402 challenge OrchestratorInfo carries no pricing-unit signal: runnerOrchInfo (server/ai_http.go) hardcodes PixelsPerUnit: 1 and sets an AuthToken.SessionId for both fixed and 720p live-runner units. So the signer genuinely can't distinguish a fixed single-shot from a continuous 720p stream from the request alone.

Required paired change (owner: gateway). For price_info.unit == "fixed" runners, the gateway must send inPixels: 1 to /generate-live-payment (currently livepeer_gateway/live_runner.py::_get_runner_payment sends type:"lv2v" and no inPixels). Until then this commit is inert (same shape as #4006 / the model_id pairing).

Test

TestGenerateLivePayment_LV2V_FixedPriceHonorsInPixels: at the representative flux-schnell fixed price, inPixels:1 → HTTP 200 with exactly 1 ticket; and a regression guard asserting the no-inPixels auto path still trips the max-100 guard (continuous path left unchanged).

Local test note

Full go test ./server/ can't run on this box — the lpms cgo build needs go-livepeer's patched ffmpeg (~/compiled), so go vet ./server/ fails in lpms/ffmpeg, not in this change. gofmt clean; relying on CI.

@seanhanca
seanhanca requested review from eliteprox and j0sh July 29, 2026 01:38
@seanhanca

Copy link
Copy Markdown
Author

Closing as superseded — this PR is built on the wrong lineage.

Its base is feat/remote-signer-byoc-v2 (BYOC), not ja/live-runner. Investigation (LR-AUTHOR-INPUTS-INVESTIGATION.md, Points 1 & 3) confirms the native fixed-price single-shot path already exists on ja/live-runner + v0.9.0 (commit #3999 "runner: Add fixed pricing", 2183b675):

  • The v0.9.0 signer's RemoteType_Fixed branch sets billableUnits = 1numTickets ~1 (no 720p30 lv2v recompute), so the numTickets 2721947758 / 400 is a symptom of the stale lv2v-only signer, not a bug to patch.
  • The v0.9.0 orch 402 challenge already binds manifest_id = AuthToken.SessionId and signs it verbatim, so the signer-side manifest-binding here is redundant — the 403 "mismatched manifest" cannot occur on the native path.

Both effects this PR re-implements as lv2v hacks ship natively in v0.9.0. The real signer fix is a deployment: put a v0.9.0 (≥#3999) remote-signer on signer-staging-1/2 (currently c0e79ccb, 2026-06-10, lv2v-only). This is a deployment/lineage fix, not code.

Do not reopen or rebase (rebasing onto ja/live-runner would re-implement shipped functionality and conflict). Branch left intact. See LR-AUTHOR-INPUTS-INVESTIGATION.md for full evidence.

@seanhanca seanhanca closed this Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go Pull requests that update Go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant