Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a1fac53
feat(remote_signer, orchestrator) Add external capabilities support t…
eliteprox Feb 3, 2026
ec501ab
Refactor ExternalCapabilityInfo: Remove PriceInfo field and related m…
eliteprox Feb 4, 2026
4a889ef
Implement capabilities endpoint and refactor ExternalCapabilityInfo
eliteprox Feb 4, 2026
85268d8
revert changes to gRPC
eliteprox Feb 4, 2026
df9275f
feat(capabilities): Add BYOC external capability and integrate pricin…
eliteprox Feb 11, 2026
18472ac
refactor(capabilities): cleanup unnecessary capability changes
eliteprox Feb 11, 2026
4d47903
Merge branch 'master' into feat/remote-signer-byoc
eliteprox Feb 11, 2026
deae93b
Merge branch 'master' into feat/remote-signer-byoc
eliteprox Feb 11, 2026
c500fc5
Merge branch 'master' into feat/remote-signer-byoc
eliteprox Mar 2, 2026
7287b04
apply gofmt
eliteprox Mar 2, 2026
cf9ca75
Enhance BYOC capability validation in remote signer
eliteprox Mar 3, 2026
9884e79
Refactor PriceInfo methods to simplify node checks and enhance BYOC p…
eliteprox Mar 3, 2026
a78e876
Merge branch 'master' into feat/remote-signer-byoc
eliteprox Mar 27, 2026
947825a
Enhance BYOC job signing and payment processing
eliteprox Apr 3, 2026
5f3d6be
Refactor job credential verification in BYOC orchestrator
eliteprox Apr 3, 2026
b08d680
Merge branch 'master' into feat/remote-signer-byoc
eliteprox Apr 3, 2026
ec6bd32
revert erroneous changes to pricing logic in orchestrator.go, fix tests
eliteprox Apr 3, 2026
55ecbbc
revert removed comments
eliteprox Apr 3, 2026
a6b94a8
Update test for GetOrchestrator to assert empty capabilities prices i…
eliteprox Apr 3, 2026
a19eb98
Refactor pricing logic in orchestrator.go to remove unnecessary recip…
eliteprox Apr 3, 2026
5342094
revert changes to byoc/stream_orchestrator.go
eliteprox Apr 3, 2026
08bed0a
Refactor BYOC payment processing logic in remote_signer.go
eliteprox Apr 24, 2026
e90af90
Update JSON tag for ManifestID in RemotePaymentRequest struct in remo…
eliteprox Apr 24, 2026
b14f6ed
Refactor BYOC payment request validation and pricing resolution
eliteprox Apr 24, 2026
60e9588
Update JSON tag for ManifestID in RemotePaymentRequest struct to lowe…
eliteprox Apr 24, 2026
cf89209
Remove unnecessary recipient check in priceInfo function of orchestra…
eliteprox Apr 24, 2026
ee93532
Remove error handling for capability prices retrieval in orchestrator…
eliteprox Apr 24, 2026
8e0c947
Enhance BYOC payment handling and error reporting
eliteprox Apr 24, 2026
a5ba467
fix(tests): TestGetOrchestrator_CapabilitiesPricesError to allow Empt…
eliteprox Apr 24, 2026
cacec0d
refactor(remote_signer, byoc): Use Type param for capability instead …
eliteprox Apr 30, 2026
61b0363
Enhance BYOC request validation and pricing resolution
eliteprox Apr 30, 2026
20e763c
refactor(remote_signer): restore stream parameters to ensure proper e…
eliteprox Apr 30, 2026
b969c06
refactor(remote_signer): improve BYOC pricing resolution and test cases
eliteprox Apr 30, 2026
07e30b7
refactor(remote_signer): optimize BYOC pre-funding logic for payment …
eliteprox May 1, 2026
0222b67
refactor(remote_signer): consolidate remote type constants for clarity
eliteprox May 1, 2026
8e5d724
refactor(remote_signer): enhance BYOC pricing resolution and testing
eliteprox May 1, 2026
037a23c
refactor(remote_signer): streamline manifest ID handling in payment g…
eliteprox May 1, 2026
9561497
refactor(remote_signer): improve BYOC pricing resolution and error ha…
eliteprox May 1, 2026
a62177b
refactor(remote_signer): remove unused payment type validation function
eliteprox May 1, 2026
30d9ee0
refactor(orchestrator): revert changes to priceInfo function
eliteprox May 1, 2026
3f13060
refactor(remote_signer): simplify billable seconds calculation in pay…
eliteprox May 1, 2026
0a1919e
refactor(remote_signer): remove deprecated BYOC pricing resolution logic
eliteprox May 1, 2026
e7e924d
Merge branch 'master' into feat/remote-signer-byoc
eliteprox May 14, 2026
97debb0
Merge branch 'master' into feat/remote-signer-byoc
cursoragent Jul 8, 2026
8826686
fix(byoc): reject timeout_seconds values that exceed uint32 wire limit
cursoragent Jul 8, 2026
a00d26b
Merge branch 'master' into feat/remote-signer-byoc
eliteprox Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Breaking Changes 🚨🚨

- BYOC orchestrator job requests now hard-reject invalid non-empty `Livepeer-Payment` headers with `HTTP 400 Bad Request` (`Could not parse payment`) instead of logging and continuing until balance depletion. Gateways/SDKs must omit stale payment headers or send a valid fresh payment.

### Features ⚒

#### General
Expand Down
43 changes: 26 additions & 17 deletions byoc/job_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,10 @@ func (bso *BYOCOrchestratorServer) confirmPayment(ctx context.Context, sender et
orchBal, pmtErr := bso.processPayment(ctx, sender, capability, paymentHdr)
if pmtErr != nil {
//log if there are payment errors but continue, balance will runout and clean up
if paymentHdr != "" {
clog.Errorf(ctx, "rejecting request: payment header present but invalid: %v", pmtErr)
return errPaymentError
}
clog.Infof(ctx, "job payment error: %v", pmtErr)
}

Expand All @@ -502,25 +506,22 @@ func (bso *BYOCOrchestratorServer) confirmPayment(ctx context.Context, sender et
return nil
}

// process payment and return balance
// processPayment decodes and applies the payment header if present.
// Always returns a non-nil balance so callers can safely compare.
func (bso *BYOCOrchestratorServer) processPayment(ctx context.Context, sender ethcommon.Address, capability string, paymentHdr string) (*big.Rat, error) {
if paymentHdr != "" {
payment, err := getPayment(paymentHdr)
if err != nil {
clog.Errorf(ctx, "job payment invalid: %v", err)
return nil, errPaymentError
return bso.getPaymentBalance(sender, capability), errPaymentError
}

if err := bso.orch.ProcessPayment(ctx, payment, core.ManifestID(capability)); err != nil {
bso.orch.FreeExternalCapabilityCapacity(capability)
clog.Errorf(ctx, "Error processing payment: %v", err)
return nil, errPaymentError
return bso.getPaymentBalance(sender, capability), errPaymentError
}
}
orchBal := bso.getPaymentBalance(sender, capability)

return orchBal, nil

return bso.getPaymentBalance(sender, capability), nil
}

func (bso *BYOCOrchestratorServer) chargeForCompute(start time.Time, price *net.PriceInfo, sender ethcommon.Address, jobId string) {
Expand Down Expand Up @@ -566,18 +567,26 @@ func (bso *BYOCOrchestratorServer) verifyJobCreds(ctx context.Context, jobCreds
return nil, errSegSig
}

if !bso.orch.VerifySig(ethcommon.HexToAddress(jobData.Sender), jobData.Request+jobData.Parameters, sigByte) {
clog.Errorf(ctx, "Sig check failed sender=%v", jobData.Sender)
return nil, errSegSig
}
sender := ethcommon.HexToAddress(jobData.Sender)

if reserveCapacity && bso.orch.ReserveExternalCapabilityCapacity(jobData.Capability) != nil {
return nil, errZeroCapacity
// Verify V1 structured binary format
v1Payload := FlattenBYOCJob(&BYOCJobSigningInput{
ID: jobData.ID,
Capability: jobData.Capability,
Request: jobData.Request,
Parameters: jobData.Parameters,
TimeoutSeconds: jobData.Timeout,
})
if bso.orch.VerifySig(sender, string(v1Payload), sigByte) {
if reserveCapacity && bso.orch.ReserveExternalCapabilityCapacity(jobData.Capability) != nil {
return nil, errZeroCapacity
}
jobData.CapabilityUrl = bso.orch.GetUrlForCapability(jobData.Capability)
return jobData, nil
}

jobData.CapabilityUrl = bso.orch.GetUrlForCapability(jobData.Capability)

return jobData, nil
clog.Errorf(ctx, "Sig check failed sender=%v", jobData.Sender)
return nil, errSegSig
}

func (bso *BYOCOrchestratorServer) verifyTokenCreds(ctx context.Context, tokenCreds string) (*JobSender, error) {
Expand Down
81 changes: 81 additions & 0 deletions byoc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package byoc
import (
"context"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"math"
"math/big"
gonet "net"
"net/http"
Expand Down Expand Up @@ -279,3 +282,81 @@ type byocLiveRequestParams struct {
// when the write for the last segment started
lastSegmentTime time.Time
}

// BYOCJobSigV1Prefix is the 16-byte domain separator for BYOC job signatures (V1).
// Prevents cross-protocol signature replay. Changing this prefix is wire-breaking.
const BYOCJobSigV1Prefix = "LP_BYOC_JOB_V1\x00\x00"

// MaxBYOCJobTimeoutSeconds is the largest timeout_seconds value accepted on the wire.
// The V1 format encodes timeout as a big-endian uint32.
const MaxBYOCJobTimeoutSeconds = math.MaxUint32

// ValidateBYOCJobTimeoutSeconds rejects values that cannot be encoded in the V1 wire format.
func ValidateBYOCJobTimeoutSeconds(seconds int) error {
if seconds <= 0 {
return errors.New("timeout_seconds must be positive")
}
if seconds > MaxBYOCJobTimeoutSeconds {
return fmt.Errorf("timeout_seconds exceeds maximum %d", MaxBYOCJobTimeoutSeconds)
}
return nil
}

// BYOCJobSigningInput holds the fields that are bound into a BYOC job signature.
type BYOCJobSigningInput struct {
ID string
Capability string
Request string
Parameters string
TimeoutSeconds int
}

// FlattenBYOCJob produces a deterministic binary representation of a BYOC job
// for signing, similar to SegTranscodingMetadata.Flatten() used by LV2V.
//
// Wire format:
//
// version(16) || timeout(4,BE uint32) || len(id)(4,BE) || id || len(cap)(4,BE) || cap
// || len(req)(4,BE) || req || len(params)(4,BE) || params
func FlattenBYOCJob(job *BYOCJobSigningInput) []byte {
idBytes := []byte(job.ID)
capBytes := []byte(job.Capability)
reqBytes := []byte(job.Request)
paramsBytes := []byte(job.Parameters)

size := 16 + 4 +
4 + len(idBytes) +
4 + len(capBytes) +
4 + len(reqBytes) +
4 + len(paramsBytes)

buf := make([]byte, size)
offset := 0

copy(buf[offset:], []byte(BYOCJobSigV1Prefix))
offset += 16

binary.BigEndian.PutUint32(buf[offset:], uint32(job.TimeoutSeconds))
offset += 4

binary.BigEndian.PutUint32(buf[offset:], uint32(len(idBytes)))
offset += 4
copy(buf[offset:], idBytes)
offset += len(idBytes)

binary.BigEndian.PutUint32(buf[offset:], uint32(len(capBytes)))
offset += 4
copy(buf[offset:], capBytes)
offset += len(capBytes)

binary.BigEndian.PutUint32(buf[offset:], uint32(len(reqBytes)))
offset += 4
copy(buf[offset:], reqBytes)
offset += len(reqBytes)

binary.BigEndian.PutUint32(buf[offset:], uint32(len(paramsBytes)))
offset += 4
copy(buf[offset:], paramsBytes)

return buf
}
Comment on lines +321 to +362

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether Timeout is validated anywhere before gatewayJob.sign()
rg -nP '\bTimeout\b' byoc/*.go -C2

Repository: livepeer/go-livepeer

Length of output: 13322


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== byoc/job_gateway.go (around sign path) =="
sed -n '300,360p' byoc/job_gateway.go

echo
echo "== byoc/job_orchestrator.go (around remote-signer verify path) =="
sed -n '540,595p' byoc/job_orchestrator.go

echo
echo "== byoc/types.go (BYOC job signing input) =="
sed -n '280,350p' byoc/types.go

Repository: livepeer/go-livepeer

Length of output: 5959


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

files = ["byoc/job_gateway.go", "byoc/job_orchestrator.go", "byoc/types.go"]
for f in files:
    text = Path(f).read_text()
    print(f"\n== {f} ==")
    for m in re.finditer(r'(?m)^(.*Timeout.*)$', text):
        line = text[:m.start()].count("\n") + 1
        if 1 <= line <= 700:
            print(f"{line}: {m.group(1)}")
PY

Repository: livepeer/go-livepeer

Length of output: 4161


Reject non-positive timeouts before flattening. The BYOC request validators only guard Timeout == 0, so negative values can reach this encoder and wrap through uint32(job.TimeoutSeconds). <= 0 is the safer check.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 321-321: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(job.TimeoutSeconds)
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)


[warning] 324-324: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(len(idBytes))
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)


[warning] 329-329: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(len(capBytes))
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)


[warning] 334-334: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(len(reqBytes))
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)


[warning] 339-339: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(len(paramsBytes))
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@byoc/types.go` around lines 304 - 345, Reject non-positive timeout values
before flattening the BYOC job so negative values can’t be cast through uint32
in FlattenBYOCJob. Update the existing validation for BYOCJobSigningInput (where
TimeoutSeconds/Timeout is checked) to treat any value <= 0 as invalid instead of
only zero, and keep the encoder unchanged. This should be applied in the request
validation path that feeds FlattenBYOCJob.

Source: Linters/SAST tools

26 changes: 26 additions & 0 deletions byoc/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package byoc

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestValidateBYOCJobTimeoutSeconds(t *testing.T) {
require := require.New(t)

require.NoError(ValidateBYOCJobTimeoutSeconds(1))
require.NoError(ValidateBYOCJobTimeoutSeconds(MaxBYOCJobTimeoutSeconds))

err := ValidateBYOCJobTimeoutSeconds(0)
require.Error(err)
require.Contains(err.Error(), "positive")

err = ValidateBYOCJobTimeoutSeconds(-1)
require.Error(err)
require.Contains(err.Error(), "positive")

err = ValidateBYOCJobTimeoutSeconds(MaxBYOCJobTimeoutSeconds + 1)
require.Error(err)
require.Contains(err.Error(), "maximum")
}
18 changes: 17 additions & 1 deletion byoc/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,20 @@ var sendJobReqWithTimeout = sendReqWithTimeout
func (g *gatewayJob) sign() error {
//sign the request
gateway := g.node.OrchestratorPool.Broadcaster()
sig, err := gateway.Sign([]byte(g.Job.Req.Request + g.Job.Req.Parameters))

if err := ValidateBYOCJobTimeoutSeconds(g.Job.Req.Timeout); err != nil {
return err
}

sigPayload := FlattenBYOCJob(&BYOCJobSigningInput{
ID: g.Job.Req.ID,
Capability: g.Job.Req.Capability,
Request: g.Job.Req.Request,
Parameters: g.Job.Req.Parameters,
TimeoutSeconds: g.Job.Req.Timeout,
})

sig, err := gateway.Sign(sigPayload)
if err != nil {
return errors.New(fmt.Sprintf("Unable to sign request err=%v", err))
}
Expand Down Expand Up @@ -67,6 +80,9 @@ func parseJobRequest(jobReq string) (*JobRequest, error) {
if jobData.Timeout == 0 {
return nil, errNoTimeoutSet
}
if err := ValidateBYOCJobTimeoutSeconds(jobData.Timeout); err != nil {
return nil, err
}

return &jobData, nil
}
Expand Down
15 changes: 12 additions & 3 deletions core/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,18 @@ func (orch *orchestrator) priceInfo(sender ethcommon.Address, manifestID Manifes
continue
}
for modelID := range constraints.Models {
price := orch.node.GetBasePriceForCap(sender.String(), Capability(cap), modelID)
if price == nil {
price = orch.node.GetBasePriceForCap("default", Capability(cap), modelID)
var price *big.Rat
if Capability(cap) == Capability_BYOC {
// BYOC prices are stored in jobPriceInfo, keyed by capability name
price = orch.node.GetPriceForJob(sender.String(), modelID)
if price == nil || price.Sign() == 0 {
price = orch.node.GetPriceForJob("default", modelID)
}
} else {
price = orch.node.GetBasePriceForCap(sender.String(), Capability(cap), modelID)
if price == nil {
price = orch.node.GetBasePriceForCap("default", Capability(cap), modelID)
}
}

if price != nil {
Expand Down
Loading
Loading