From 65eece90a907c04b05581e4a9f922511978e1bd5 Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Thu, 9 Jul 2026 17:10:22 -0400 Subject: [PATCH 1/4] feat(signer): charge BYOC live payments from CapabilitiesPrices Resolve per-capability BYOC fees from the orchestrator's advertised CapabilitiesPrices, keyed on Capability_BYOC constraints already present in the request capabilities blob. No RemotePaymentRequest format change. Gated behind default-OFF -byocPerCapPricing. When OFF or no usable cap price matches, behavior stays on the base-price / lv2v pixel path. --- cmd/livepeer/starter/flags.go | 1 + cmd/livepeer/starter/starter.go | 6 + core/livepeernode.go | 1 + server/remote_signer.go | 104 ++++++++++-- server/remote_signer_test.go | 275 ++++++++++++++++++++++++++++++++ 5 files changed, 371 insertions(+), 16 deletions(-) diff --git a/cmd/livepeer/starter/flags.go b/cmd/livepeer/starter/flags.go index af2d7eafd7..37fedb65ce 100644 --- a/cmd/livepeer/starter/flags.go +++ b/cmd/livepeer/starter/flags.go @@ -149,6 +149,7 @@ func NewLivepeerConfig(fs *flag.FlagSet) LivepeerConfig { cfg.RemoteSignerWebhookHeaders = fs.String("remoteSignerWebhookHeaders", *cfg.RemoteSignerWebhookHeaders, "Map of headers to use for remote signer webhook requests. e.g. 'header:val,header2:val2'") cfg.RemoteSignerAllowNoAuth = fs.Bool("remoteSignerAllowNoAuth", *cfg.RemoteSignerAllowNoAuth, "Allow an unauthenticated remote signer on a public -httpAddr (no webhook). UNSAFE: signs payments from this node's deposit for any reachable caller; restrict access externally (proxy/private network).") cfg.RemoteDiscovery = fs.Bool("remoteDiscovery", *cfg.RemoteDiscovery, "Enable orchestrator discovery on remote signers") + cfg.ByocPerCapPricing = fs.Bool("byocPerCapPricing", *cfg.ByocPerCapPricing, "Remote signer: resolve BYOC live-payment fee from the orchestrator's per-capability CapabilitiesPrices (keyed on Capability_BYOC constraints in the request) instead of the flat base price. Default OFF (byte-identical to base-price behavior).") // Gateway metrics cfg.KafkaBootstrapServers = fs.String("kafkaBootstrapServers", *cfg.KafkaBootstrapServers, "URL of Kafka Bootstrap Servers") diff --git a/cmd/livepeer/starter/starter.go b/cmd/livepeer/starter/starter.go index 6c7741e7c3..2a00ad7d35 100755 --- a/cmd/livepeer/starter/starter.go +++ b/cmd/livepeer/starter/starter.go @@ -180,6 +180,7 @@ type LivepeerConfig struct { RemoteSignerWebhookHeaders *string RemoteSignerAllowNoAuth *bool RemoteDiscovery *bool + ByocPerCapPricing *bool AIRunnerImage *string AIRunnerImageOverrides *string AIVerboseLogs *bool @@ -326,6 +327,7 @@ func DefaultLivepeerConfig() LivepeerConfig { defaultRemoteSignerWebhookHeaders := "" defaultRemoteSignerAllowNoAuth := false defaultRemoteDiscovery := false + defaultByocPerCapPricing := false // Gateway logs defaultKafkaBootstrapServers := "" @@ -458,6 +460,7 @@ func DefaultLivepeerConfig() LivepeerConfig { RemoteSignerWebhookHeaders: &defaultRemoteSignerWebhookHeaders, RemoteSignerAllowNoAuth: &defaultRemoteSignerAllowNoAuth, RemoteDiscovery: &defaultRemoteDiscovery, + ByocPerCapPricing: &defaultByocPerCapPricing, // Gateway logs KafkaBootstrapServers: &defaultKafkaBootstrapServers, @@ -1881,6 +1884,9 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { if cfg.RemoteDiscovery != nil { n.RemoteDiscovery = *cfg.RemoteDiscovery } + if cfg.ByocPerCapPricing != nil { + n.ByocPerCapPricing = *cfg.ByocPerCapPricing + } if cfg.LiveAIHeartbeatHeaders != nil { n.LiveAIHeartbeatHeaders = parseHeaderMap(*cfg.LiveAIHeartbeatHeaders) } diff --git a/core/livepeernode.go b/core/livepeernode.go index 31cc4d108a..0ace42dd55 100644 --- a/core/livepeernode.go +++ b/core/livepeernode.go @@ -158,6 +158,7 @@ type LivepeerNode struct { RemoteEthAddr ethcommon.Address // eth address of the remote signer InfoSig []byte // sig over eth address for the OrchestratorInfo request RemoteDiscovery bool // expose remote discovery endpoint when enabled + ByocPerCapPricing bool // resolve BYOC fee from per-capability CapabilitiesPrices instead of base price (remote signer; default OFF) // Thread safety for config fields mu sync.RWMutex diff --git a/server/remote_signer.go b/server/remote_signer.go index f34cdead52..8dc12640b3 100644 --- a/server/remote_signer.go +++ b/server/remote_signer.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "math" "math/big" "net/http" "net/url" @@ -355,6 +356,46 @@ func (ls *LivepeerServer) authLivePayment(r *http.Request, state *RemotePaymentS return *webhookResp.Status, &webhookResp, errors.New(webhookResp.Reason) } +// resolveByocPrice resolves the per-capability BYOC price advertised in the +// orchestrator's OrchestratorInfo.CapabilitiesPrices, keyed on the BYOC model +// constraint already present in the request capabilities +// (caps.ModelIDForCapability(Capability_BYOC)). The orchestrator appends +// external BYOC capability prices to CapabilitiesPrices as +// {Capability: Capability_BYOC, Constraint: } (see +// core/orchestrator.go GetCapabilitiesPrices). +// +// Returns nil when there is no usable per-capability price (no BYOC constraint, +// or no matching entry with a positive rate). A matching entry with a +// non-positive rate is skipped rather than aborting the scan, so a later valid +// duplicate entry for the same constraint (e.g. due to misconfiguration) is +// still honored. Callers fall back to the base oInfo.PriceInfo when nil is +// returned. +func resolveByocPrice(caps *core.Capabilities, oInfo *net.OrchestratorInfo) *net.PriceInfo { + if caps == nil || oInfo == nil { + return nil + } + constraint := caps.ModelIDForCapability(core.Capability_BYOC) + if constraint == "" { + return nil + } + for _, p := range oInfo.CapabilitiesPrices { + if p == nil { + continue + } + if p.Capability != uint32(core.Capability_BYOC) || p.Constraint != constraint { + continue + } + // Only honor a valid positive rate. Skip invalid/zero entries and keep + // scanning so a later valid duplicate for the same constraint isn't + // shadowed; if none is found we fall back to base (never zeroing the fee). + if p.PricePerUnit <= 0 || p.PixelsPerUnit <= 0 { + continue + } + return &net.PriceInfo{PricePerUnit: p.PricePerUnit, PixelsPerUnit: p.PixelsPerUnit} + } + return nil +} + // GenerateLivePayment handles remote generation of a payment for live streams. func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Request) { requestID := string(core.RandomManifestID()) @@ -389,14 +430,43 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req respondJsonError(ctx, w, err, http.StatusBadRequest) return } - priceInfo := oInfo.PriceInfo - if priceInfo == nil || priceInfo.PricePerUnit == 0 || priceInfo.PixelsPerUnit == 0 { - err := fmt.Errorf("missing or zero priceInfo") + if oInfo.TicketParams == nil { + err := fmt.Errorf("missing ticketParams in OrchestratorInfo") respondJsonError(ctx, w, err, http.StatusBadRequest) return } - if oInfo.TicketParams == nil { - err := fmt.Errorf("missing ticketParams in OrchestratorInfo") + + var reqCaps *core.Capabilities + if len(req.Capabilities) > 0 { + var caps net.Capabilities + if err := proto.Unmarshal(req.Capabilities, &caps); err != nil { + clog.Errorf(ctx, "Failed to unmarshal capabilities err=%q", err) + respondJsonError(ctx, w, err, http.StatusBadRequest) + return + } + reqCaps = core.CapabilitiesFromNetCapabilities(&caps) + } + + // BYOC per-capability pricing (flag-gated, default OFF). When enabled for an + // lv2v request that carries Capability_BYOC constraints, resolve the + // per-capability price from the orchestrator's advertised CapabilitiesPrices + // (keyed on the BYOC model constraint) and use it instead of the flat base + // price. The resolved price is written back to oInfo.PriceInfo so it is the + // single source for: state init, initialPrice, the max-price ceiling, the + // payment's ExpectedPrice, and the price-doubling guard in validatePrice. + // When the flag is OFF or no usable cap price matches, behavior is + // byte-identical to the base-price path. + priceInfo := oInfo.PriceInfo + useByocPricing := false + if ls.LivepeerNode.ByocPerCapPricing && req.Type == RemoteType_LiveVideoToVideo { + if capPrice := resolveByocPrice(reqCaps, &oInfo); capPrice != nil { + priceInfo = capPrice + oInfo.PriceInfo = capPrice + useByocPricing = true + } + } + if priceInfo == nil || priceInfo.PricePerUnit == 0 || priceInfo.PixelsPerUnit == 0 { + err := fmt.Errorf("missing or zero priceInfo") respondJsonError(ctx, w, err, http.StatusBadRequest) return } @@ -454,16 +524,8 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req streamParams := &core.StreamParameters{ // Embedded within genSegCreds, may be used by orch for payment accounting - ManifestID: core.ManifestID(manifestID), - } - if len(req.Capabilities) > 0 { - var caps net.Capabilities - if err := proto.Unmarshal(req.Capabilities, &caps); err != nil { - clog.Errorf(ctx, "Failed to unmarshal capabilities err=%q", err) - respondJsonError(ctx, w, err, http.StatusBadRequest) - return - } - streamParams.Capabilities = core.CapabilitiesFromNetCapabilities(&caps) + ManifestID: core.ManifestID(manifestID), + Capabilities: reqCaps, } pmParams := pmTicketParams(oInfo.TicketParams) @@ -528,7 +590,17 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req lastUpdate = now } billableSecs := now.Sub(lastUpdate).Seconds() - if req.Type == RemoteType_LiveVideoToVideo { + if useByocPricing { + // BYOC per-capability tariff is denominated per compute-second (wei/sec), + // so charge on seconds directly rather than synthesizing lv2v pixels. With + // pixels = ceil(billableSecs) and the resolved per-cap price kept as its + // reduced wei/sec rational, calculateFee yields capPriceWeiPerSec * seconds. + if billableSecs <= 0 { + // preload with 60 seconds, mirroring the lv2v first-call behavior + billableSecs = (60 * time.Second).Seconds() + } + pixels = int64(math.Ceil(billableSecs)) + } else if req.Type == RemoteType_LiveVideoToVideo { info := defaultSegInfo if billableSecs <= 0 { // preload with 60 seconds of data for LV2V diff --git a/server/remote_signer_test.go b/server/remote_signer_test.go index 9e2d7a154a..f02ea161f6 100644 --- a/server/remote_signer_test.go +++ b/server/remote_signer_test.go @@ -1692,3 +1692,278 @@ func TestGetOrchInfoSig_SendsConfiguredHeaders(t *testing.T) { require.Equal([]byte{0x12, 0x34}, []byte(resp.Address)) require.Equal([]byte{0xab, 0xcd}, []byte(resp.Signature)) } + +func byocCaps(t *testing.T, modelID string) *core.Capabilities { + t.Helper() + if modelID == "" { + return nil + } + caps := core.NewCapabilities([]core.Capability{core.Capability_BYOC}, nil) + caps.SetPerCapabilityConstraints(core.PerCapabilityConstraints{ + core.Capability_BYOC: &core.CapabilityConstraints{ + Models: map[string]*core.ModelConstraint{ + modelID: {}, + }, + }, + }) + return caps +} + +func byocCapsBlob(t *testing.T, modelID string) []byte { + t.Helper() + caps := byocCaps(t, modelID) + if caps == nil { + return nil + } + blob, err := proto.Marshal(caps.ToNetCapabilities()) + require.NoError(t, err) + return blob +} + +// TestResolveByocPrice covers the pure per-capability BYOC price resolver: +// it matches a Capability_BYOC entry whose Constraint equals the BYOC model +// constraint from request capabilities, ignores non-BYOC entries, and returns +// nil (→ caller falls back to base) for no-match / empty constraint / +// non-positive rate. +func TestResolveByocPrice(t *testing.T) { + require := require.New(t) + + byocCap := uint32(core.Capability_BYOC) + oInfo := &net.OrchestratorInfo{ + PriceInfo: &net.PriceInfo{PricePerUnit: 100, PixelsPerUnit: 1}, // base (unused by resolver) + CapabilitiesPrices: []*net.PriceInfo{ + {Capability: byocCap, Constraint: "nano-banana", PricePerUnit: 10, PixelsPerUnit: 1}, + {Capability: byocCap, Constraint: "recraft-v4", PricePerUnit: 20, PixelsPerUnit: 1}, + // decoy: a non-BYOC capability sharing the constraint name must NOT match + {Capability: uint32(core.Capability_LiveVideoToVideo), Constraint: "nano-banana", PricePerUnit: 999, PixelsPerUnit: 1}, + // decoy: zero/invalid BYOC rate must be treated as no-match (fallback) + {Capability: byocCap, Constraint: "free-cap", PricePerUnit: 0, PixelsPerUnit: 1}, + // misconfig: an early invalid duplicate must NOT shadow a later valid + // entry for the same constraint (scan continues past invalid rates). + {Capability: byocCap, Constraint: "dup-cap", PricePerUnit: 0, PixelsPerUnit: 1}, + {Capability: byocCap, Constraint: "dup-cap", PricePerUnit: 30, PixelsPerUnit: 1}, + }, + } + + tests := []struct { + name string + capability string + oInfo *net.OrchestratorInfo + want *net.PriceInfo + }{ + { + name: "resolves per capability", + capability: "recraft-v4", + oInfo: oInfo, + want: &net.PriceInfo{PricePerUnit: 20, PixelsPerUnit: 1}, + }, + { + name: "resolves the other capability", + capability: "nano-banana", + oInfo: oInfo, + want: &net.PriceInfo{PricePerUnit: 10, PixelsPerUnit: 1}, + }, + { + name: "unknown capability falls back (nil)", + capability: "does-not-exist", + oInfo: oInfo, + want: nil, + }, + { + name: "empty capability falls back (nil)", + capability: "", + oInfo: oInfo, + want: nil, + }, + { + name: "zero/invalid matched rate falls back (nil)", + capability: "free-cap", + oInfo: oInfo, + want: nil, + }, + { + name: "skips invalid duplicate, honors later valid entry", + capability: "dup-cap", + oInfo: oInfo, + want: &net.PriceInfo{PricePerUnit: 30, PixelsPerUnit: 1}, + }, + { + name: "no capabilities prices falls back (nil)", + capability: "nano-banana", + oInfo: &net.OrchestratorInfo{PriceInfo: &net.PriceInfo{PricePerUnit: 100, PixelsPerUnit: 1}}, + want: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := resolveByocPrice(byocCaps(t, tc.capability), tc.oInfo) + if tc.want == nil { + require.Nil(got) + return + } + require.NotNil(got) + require.Equal(tc.want.PricePerUnit, got.PricePerUnit, "PricePerUnit") + require.Equal(tc.want.PixelsPerUnit, got.PixelsPerUnit, "PixelsPerUnit") + }) + } +} + +// TestGenerateLivePayment_ByocPerCapPricing drives the full GenerateLivePayment +// handler (no CGO needed) and proves, via the resulting signed balance/state: +// - flag OFF: a matching cap is ignored; the fee is the byte-identical lv2v +// synthetic-pixel base fee (zero-regression); +// - flag ON: the fee is the resolved per-capability rate * compute-seconds; +// - flag ON: two caps in a 2:1 tariff produce a 2:1 fee ratio; +// - flag ON, unknown cap: falls back to the base lv2v fee; +// - flag ON with base >> 2x cap returns 200 (no false "price more than +// doubled"), proving the oInfo.PriceInfo = capPrice correction. +func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { + require := require.New(t) + + ethClient := newTestEthClient(t) + node, _ := core.NewLivepeerNode(ethClient, "", nil) + node.Balances = core.NewAddressBalances(1 * time.Minute) + + // Large per-ticket EV keeps the lv2v fee within the handler's 100-ticket cap + // (lv2v fee = base * 1280*720*30*60 is large), while the small per-second + // BYOC fees resolve to a single ticket. + ev := big.NewRat(10_000_000_000, 1) + var totalTickets uint32 + sender := newMockSender(mockSenderConfig{ + ev: ev, + createTicketBatchFn: func(args mock.Arguments, batch *pm.TicketBatch) { + size := args.Int(1) + *batch = *defaultTicketBatch() + var baseSig []byte + if len(batch.SenderParams) > 0 && batch.SenderParams[0] != nil { + baseSig = batch.SenderParams[0].Sig + } + batch.SenderParams = make([]*pm.TicketSenderParams, size) + for i := 0; i < size; i++ { + totalTickets++ + batch.SenderParams[i] = &pm.TicketSenderParams{SenderNonce: totalTickets, Sig: baseSig} + } + }, + }) + node.Sender = sender + ls := &LivepeerServer{LivepeerNode: node} + + // Generous global max price so neither base nor cap prices trip the ceiling. + autoPrice, err := core.NewAutoConvertedPrice("", big.NewRat(1_000, 1), nil) + require.NoError(err) + BroadcastCfg.SetMaxPrice(autoPrice) + defer BroadcastCfg.SetMaxPrice(nil) + + // base is intentionally >> 2x the cap rates so a regression that left + // oInfo.PriceInfo at base (while locking the cap price into state) would trip + // the >2x doubling guard. With the correction it must NOT trip. + const basePPU, capNanoPPU, capRecraftPPU = 100, 10, 20 + oInfo := &net.OrchestratorInfo{ + Address: ethClient.addr.Bytes(), + PriceInfo: &net.PriceInfo{PricePerUnit: basePPU, PixelsPerUnit: 1}, + TicketParams: &net.TicketParams{ + Recipient: pm.RandAddress().Bytes(), + }, + AuthToken: stubAuthToken, + CapabilitiesPrices: []*net.PriceInfo{ + {Capability: uint32(core.Capability_BYOC), Constraint: "nano-banana", PricePerUnit: capNanoPPU, PixelsPerUnit: 1}, + {Capability: uint32(core.Capability_BYOC), Constraint: "recraft-v4", PricePerUnit: capRecraftPPU, PixelsPerUnit: 1}, + }, + } + orchBlob, err := proto.Marshal(oInfo) + require.NoError(err) + + // Fresh-session preload bills 60 seconds (lv2v) of data. + const preloadSecs = 60 + lv2vPixels := int64(defaultSegInfo.Height) * int64(defaultSegInfo.Width) * int64(defaultSegInfo.FPS) * preloadSecs + + doPayment := func(capability string) RemotePaymentState { + reqBody, err := json.Marshal(RemotePaymentRequest{ + Orchestrator: orchBlob, + Type: RemoteType_LiveVideoToVideo, + Capabilities: byocCapsBlob(t, capability), + }) + require.NoError(err) + req := httptest.NewRequest(http.MethodPost, "/generate-live-payment", bytes.NewReader(reqBody)) + rr := httptest.NewRecorder() + ls.GenerateLivePayment(rr, req) + require.Equal(http.StatusOK, rr.Code, "body=%s", rr.Body.String()) + var resp RemotePaymentResponse + require.NoError(json.NewDecoder(rr.Body).Decode(&resp)) + var state RemotePaymentState + require.NoError(json.Unmarshal(resp.State.State, &state)) + return state + } + + // fee = numTickets*ev - balance for a fresh session; recover it from the + // signed state's balance and the locked initial price (also returned). + feeFromState := func(state RemotePaymentState) *big.Rat { + bal := new(big.Rat) + _, ok := bal.SetString(state.Balance) + require.True(ok, "parse balance %q", state.Balance) + // derive fee from initialPrice * pixels directly using the locked state. + price := big.NewRat(state.InitialPricePerUnit, state.InitialPixelsPerUnit) + var pixels int64 + if state.InitialPricePerUnit == basePPU && state.InitialPixelsPerUnit == 1 { + pixels = lv2vPixels // base path: lv2v synthetic pixels + } else { + pixels = preloadSecs // byoc path: compute-seconds + } + return new(big.Rat).Mul(price, big.NewRat(pixels, 1)) + } + + assertBalanceConsistent := func(state RemotePaymentState, fee *big.Rat) { + // Independent check: balance must equal numTickets*ev - fee with + // numTickets = ceil(max(fee,ev)/ev), validating the fee end-to-end. + smc := fee + if ev.Cmp(smc) > 0 { + smc = ev + } + q := new(big.Rat).Quo(smc, ev) + nt := new(big.Int).Quo(q.Num(), q.Denom()) + if new(big.Int).Rem(q.Num(), q.Denom()).Sign() != 0 { + nt.Add(nt, big.NewInt(1)) + } + wantBal := new(big.Rat).Sub(new(big.Rat).Mul(new(big.Rat).SetInt(nt), ev), fee) + gotBal := new(big.Rat) + _, ok := gotBal.SetString(state.Balance) + require.True(ok) + require.Zero(gotBal.Cmp(wantBal), "balance got=%s want=%s fee=%s", gotBal.RatString(), wantBal.RatString(), fee.RatString()) + } + + // --- flag OFF: cap present but ignored → base lv2v synthetic-pixel fee --- + ls.LivepeerNode.ByocPerCapPricing = false + offState := doPayment("nano-banana") + require.EqualValues(basePPU, offState.InitialPricePerUnit, "flag OFF must lock the base price") + require.EqualValues(1, offState.InitialPixelsPerUnit) + offFee := new(big.Rat).Mul(big.NewRat(basePPU, 1), big.NewRat(lv2vPixels, 1)) + require.Zero(feeFromState(offState).Cmp(offFee)) + assertBalanceConsistent(offState, offFee) + + // --- flag ON --- + ls.LivepeerNode.ByocPerCapPricing = true + + // nano-banana: fee = capNanoPPU * 60 (seconds basis); base>>2x cap must NOT + // trip the doubling guard (proves oInfo.PriceInfo = capPrice). + nanoState := doPayment("nano-banana") + require.EqualValues(capNanoPPU, nanoState.InitialPricePerUnit, "flag ON must lock the resolved cap price") + require.EqualValues(1, nanoState.InitialPixelsPerUnit) + nanoFee := big.NewRat(capNanoPPU*preloadSecs, 1) + require.Zero(feeFromState(nanoState).Cmp(nanoFee), "nano fee") + assertBalanceConsistent(nanoState, nanoFee) + + // recraft-v4 priced 2x nano → fee ratio is exactly 2:1. + recraftState := doPayment("recraft-v4") + require.EqualValues(capRecraftPPU, recraftState.InitialPricePerUnit) + recraftFee := big.NewRat(capRecraftPPU*preloadSecs, 1) + require.Zero(feeFromState(recraftState).Cmp(recraftFee), "recraft fee") + assertBalanceConsistent(recraftState, recraftFee) + require.Zero(recraftFee.Cmp(new(big.Rat).Mul(nanoFee, big.NewRat(2, 1))), "recraft fee must be 2x nano fee") + + // unknown cap with flag ON → fall back to base lv2v fee (zero-regression). + unknownState := doPayment("totally-unknown-cap") + require.EqualValues(basePPU, unknownState.InitialPricePerUnit, "unknown cap must fall back to base") + require.Zero(feeFromState(unknownState).Cmp(offFee)) + assertBalanceConsistent(unknownState, offFee) +} From 48ffcdf7cb3180a915d3811d5fe9ee5f160c6bc9 Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Thu, 9 Jul 2026 17:17:35 -0400 Subject: [PATCH 2/4] refactor(signer): drive BYOC pricing by capability, share price lookup Select the pricing basis from the request capability type rather than req.Type: a BYOC capability is charged per-capability over compute-seconds, while lv2v (and everything else) keeps its existing pricing. Since BYOC jobs always send type:"lv2v", the capability in the request is the real signal; resolveByocPrice returns nil for non-BYOC caps so lv2v is unchanged. Extract the per-capability price lookup into a shared findCapPriceInfo helper reused by resolveByocPrice and remote_discovery capabilityPrice. The requirePositiveRate flag preserves each caller's semantics (signer skips non-positive rates and keeps scanning; discovery allows a zero/free rate on first match). --- server/remote_discovery.go | 13 +++----- server/remote_signer.go | 64 ++++++++++++++++++++++---------------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/server/remote_discovery.go b/server/remote_discovery.go index bd08efd665..2c3453bc5a 100644 --- a/server/remote_discovery.go +++ b/server/remote_discovery.go @@ -391,15 +391,10 @@ func capabilityPrice(info *common.OrchNetworkCapabilities, capability core.Capab if info == nil { return nil } - // Check per-capability price if it exists - for _, capPrice := range info.CapabilitiesPrices { - if capPrice == nil || capPrice.PixelsPerUnit <= 0 || core.Capability(capPrice.Capability) != capability { - continue - } - price := new(big.Rat).SetFrac64(capPrice.PricePerUnit, capPrice.PixelsPerUnit) - if capPrice.Constraint == modelID { - return price - } + // Check per-capability price if it exists (a zero rate is allowed here, e.g. + // a free capability). + if capPrice := findCapPriceInfo(info.CapabilitiesPrices, capability, modelID, false); capPrice != nil { + return new(big.Rat).SetFrac64(capPrice.PricePerUnit, capPrice.PixelsPerUnit) } // Global fallback if no per-capability price is available. if info.PriceInfo == nil || info.PriceInfo.PixelsPerUnit <= 0 { diff --git a/server/remote_signer.go b/server/remote_signer.go index 8dc12640b3..ac13c96391 100644 --- a/server/remote_signer.go +++ b/server/remote_signer.go @@ -356,6 +356,30 @@ func (ls *LivepeerServer) authLivePayment(r *http.Request, state *RemotePaymentS return *webhookResp.Status, &webhookResp, errors.New(webhookResp.Reason) } +// findCapPriceInfo scans advertised per-capability prices for an entry whose +// capability and model constraint match, returning a copy of its rate (or nil +// if none matches). Entries with a non-positive PixelsPerUnit are always +// skipped. When requirePositiveRate is set, entries with a non-positive +// PricePerUnit are also skipped and the scan continues, so a later valid +// duplicate for the same constraint (e.g. a misconfiguration) is still honored; +// otherwise the first constraint match is returned (a zero rate is allowed, +// e.g. a free capability). Pure match-only lookup with no base-price fallback. +func findCapPriceInfo(prices []*net.PriceInfo, capability core.Capability, modelID string, requirePositiveRate bool) *net.PriceInfo { + for _, p := range prices { + if p == nil || core.Capability(p.Capability) != capability || p.Constraint != modelID { + continue + } + if p.PixelsPerUnit <= 0 { + continue + } + if requirePositiveRate && p.PricePerUnit <= 0 { + continue + } + return &net.PriceInfo{PricePerUnit: p.PricePerUnit, PixelsPerUnit: p.PixelsPerUnit} + } + return nil +} + // resolveByocPrice resolves the per-capability BYOC price advertised in the // orchestrator's OrchestratorInfo.CapabilitiesPrices, keyed on the BYOC model // constraint already present in the request capabilities @@ -369,7 +393,7 @@ func (ls *LivepeerServer) authLivePayment(r *http.Request, state *RemotePaymentS // non-positive rate is skipped rather than aborting the scan, so a later valid // duplicate entry for the same constraint (e.g. due to misconfiguration) is // still honored. Callers fall back to the base oInfo.PriceInfo when nil is -// returned. +// returned (never zeroing the fee). func resolveByocPrice(caps *core.Capabilities, oInfo *net.OrchestratorInfo) *net.PriceInfo { if caps == nil || oInfo == nil { return nil @@ -378,22 +402,7 @@ func resolveByocPrice(caps *core.Capabilities, oInfo *net.OrchestratorInfo) *net if constraint == "" { return nil } - for _, p := range oInfo.CapabilitiesPrices { - if p == nil { - continue - } - if p.Capability != uint32(core.Capability_BYOC) || p.Constraint != constraint { - continue - } - // Only honor a valid positive rate. Skip invalid/zero entries and keep - // scanning so a later valid duplicate for the same constraint isn't - // shadowed; if none is found we fall back to base (never zeroing the fee). - if p.PricePerUnit <= 0 || p.PixelsPerUnit <= 0 { - continue - } - return &net.PriceInfo{PricePerUnit: p.PricePerUnit, PixelsPerUnit: p.PixelsPerUnit} - } - return nil + return findCapPriceInfo(oInfo.CapabilitiesPrices, core.Capability_BYOC, constraint, true) } // GenerateLivePayment handles remote generation of a payment for live streams. @@ -447,18 +456,19 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req reqCaps = core.CapabilitiesFromNetCapabilities(&caps) } - // BYOC per-capability pricing (flag-gated, default OFF). When enabled for an - // lv2v request that carries Capability_BYOC constraints, resolve the - // per-capability price from the orchestrator's advertised CapabilitiesPrices - // (keyed on the BYOC model constraint) and use it instead of the flat base - // price. The resolved price is written back to oInfo.PriceInfo so it is the - // single source for: state init, initialPrice, the max-price ceiling, the - // payment's ExpectedPrice, and the price-doubling guard in validatePrice. - // When the flag is OFF or no usable cap price matches, behavior is - // byte-identical to the base-price path. + // Pricing basis follows the request capability type: a BYOC capability is + // charged at its advertised per-capability rate over compute-seconds, while + // lv2v (and everything else) keeps the pricing it already used. resolveByocPrice + // returns nil for non-BYOC caps, so lv2v naturally falls through to the base + // price unchanged. The resolved BYOC price is written back to oInfo.PriceInfo + // so it is the single source for: state init, initialPrice, the max-price + // ceiling, the payment's ExpectedPrice, and the price-doubling guard in + // validatePrice. Flag-gated (default OFF) so BYOC pricing is opt-in; when the + // flag is OFF or no usable cap price matches, behavior is byte-identical to + // the base-price path. priceInfo := oInfo.PriceInfo useByocPricing := false - if ls.LivepeerNode.ByocPerCapPricing && req.Type == RemoteType_LiveVideoToVideo { + if ls.LivepeerNode.ByocPerCapPricing { if capPrice := resolveByocPrice(reqCaps, &oInfo); capPrice != nil { priceInfo = capPrice oInfo.PriceInfo = capPrice From bc4fa5ebfd233f25f429b996d5d0e0ed18fd6543 Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Thu, 9 Jul 2026 17:22:47 -0400 Subject: [PATCH 3/4] chore(signer): trim verbose comments on BYOC pricing path --- cmd/livepeer/starter/flags.go | 2 +- core/livepeernode.go | 2 +- server/remote_discovery.go | 2 -- server/remote_signer.go | 46 +++++++---------------------------- server/remote_signer_test.go | 45 ++++------------------------------ 5 files changed, 16 insertions(+), 81 deletions(-) diff --git a/cmd/livepeer/starter/flags.go b/cmd/livepeer/starter/flags.go index 37fedb65ce..cb536697fc 100644 --- a/cmd/livepeer/starter/flags.go +++ b/cmd/livepeer/starter/flags.go @@ -149,7 +149,7 @@ func NewLivepeerConfig(fs *flag.FlagSet) LivepeerConfig { cfg.RemoteSignerWebhookHeaders = fs.String("remoteSignerWebhookHeaders", *cfg.RemoteSignerWebhookHeaders, "Map of headers to use for remote signer webhook requests. e.g. 'header:val,header2:val2'") cfg.RemoteSignerAllowNoAuth = fs.Bool("remoteSignerAllowNoAuth", *cfg.RemoteSignerAllowNoAuth, "Allow an unauthenticated remote signer on a public -httpAddr (no webhook). UNSAFE: signs payments from this node's deposit for any reachable caller; restrict access externally (proxy/private network).") cfg.RemoteDiscovery = fs.Bool("remoteDiscovery", *cfg.RemoteDiscovery, "Enable orchestrator discovery on remote signers") - cfg.ByocPerCapPricing = fs.Bool("byocPerCapPricing", *cfg.ByocPerCapPricing, "Remote signer: resolve BYOC live-payment fee from the orchestrator's per-capability CapabilitiesPrices (keyed on Capability_BYOC constraints in the request) instead of the flat base price. Default OFF (byte-identical to base-price behavior).") + cfg.ByocPerCapPricing = fs.Bool("byocPerCapPricing", *cfg.ByocPerCapPricing, "Remote signer: charge BYOC live payments from CapabilitiesPrices instead of the base price (default OFF)") // Gateway metrics cfg.KafkaBootstrapServers = fs.String("kafkaBootstrapServers", *cfg.KafkaBootstrapServers, "URL of Kafka Bootstrap Servers") diff --git a/core/livepeernode.go b/core/livepeernode.go index 0ace42dd55..501ae0975c 100644 --- a/core/livepeernode.go +++ b/core/livepeernode.go @@ -158,7 +158,7 @@ type LivepeerNode struct { RemoteEthAddr ethcommon.Address // eth address of the remote signer InfoSig []byte // sig over eth address for the OrchestratorInfo request RemoteDiscovery bool // expose remote discovery endpoint when enabled - ByocPerCapPricing bool // resolve BYOC fee from per-capability CapabilitiesPrices instead of base price (remote signer; default OFF) + ByocPerCapPricing bool // remote signer: use CapabilitiesPrices for BYOC (default OFF) // Thread safety for config fields mu sync.RWMutex diff --git a/server/remote_discovery.go b/server/remote_discovery.go index 2c3453bc5a..dbc4589e8f 100644 --- a/server/remote_discovery.go +++ b/server/remote_discovery.go @@ -391,8 +391,6 @@ func capabilityPrice(info *common.OrchNetworkCapabilities, capability core.Capab if info == nil { return nil } - // Check per-capability price if it exists (a zero rate is allowed here, e.g. - // a free capability). if capPrice := findCapPriceInfo(info.CapabilitiesPrices, capability, modelID, false); capPrice != nil { return new(big.Rat).SetFrac64(capPrice.PricePerUnit, capPrice.PixelsPerUnit) } diff --git a/server/remote_signer.go b/server/remote_signer.go index ac13c96391..248091ef89 100644 --- a/server/remote_signer.go +++ b/server/remote_signer.go @@ -356,14 +356,9 @@ func (ls *LivepeerServer) authLivePayment(r *http.Request, state *RemotePaymentS return *webhookResp.Status, &webhookResp, errors.New(webhookResp.Reason) } -// findCapPriceInfo scans advertised per-capability prices for an entry whose -// capability and model constraint match, returning a copy of its rate (or nil -// if none matches). Entries with a non-positive PixelsPerUnit are always -// skipped. When requirePositiveRate is set, entries with a non-positive -// PricePerUnit are also skipped and the scan continues, so a later valid -// duplicate for the same constraint (e.g. a misconfiguration) is still honored; -// otherwise the first constraint match is returned (a zero rate is allowed, -// e.g. a free capability). Pure match-only lookup with no base-price fallback. +// findCapPriceInfo returns the first CapabilitiesPrices entry matching +// capability+modelID. requirePositiveRate skips non-positive PricePerUnit and +// keeps scanning; otherwise a zero rate is allowed. func findCapPriceInfo(prices []*net.PriceInfo, capability core.Capability, modelID string, requirePositiveRate bool) *net.PriceInfo { for _, p := range prices { if p == nil || core.Capability(p.Capability) != capability || p.Constraint != modelID { @@ -380,20 +375,8 @@ func findCapPriceInfo(prices []*net.PriceInfo, capability core.Capability, model return nil } -// resolveByocPrice resolves the per-capability BYOC price advertised in the -// orchestrator's OrchestratorInfo.CapabilitiesPrices, keyed on the BYOC model -// constraint already present in the request capabilities -// (caps.ModelIDForCapability(Capability_BYOC)). The orchestrator appends -// external BYOC capability prices to CapabilitiesPrices as -// {Capability: Capability_BYOC, Constraint: } (see -// core/orchestrator.go GetCapabilitiesPrices). -// -// Returns nil when there is no usable per-capability price (no BYOC constraint, -// or no matching entry with a positive rate). A matching entry with a -// non-positive rate is skipped rather than aborting the scan, so a later valid -// duplicate entry for the same constraint (e.g. due to misconfiguration) is -// still honored. Callers fall back to the base oInfo.PriceInfo when nil is -// returned (never zeroing the fee). +// resolveByocPrice looks up the BYOC model constraint from caps in +// oInfo.CapabilitiesPrices. Returns nil when no usable price matches. func resolveByocPrice(caps *core.Capabilities, oInfo *net.OrchestratorInfo) *net.PriceInfo { if caps == nil || oInfo == nil { return nil @@ -456,16 +439,9 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req reqCaps = core.CapabilitiesFromNetCapabilities(&caps) } - // Pricing basis follows the request capability type: a BYOC capability is - // charged at its advertised per-capability rate over compute-seconds, while - // lv2v (and everything else) keeps the pricing it already used. resolveByocPrice - // returns nil for non-BYOC caps, so lv2v naturally falls through to the base - // price unchanged. The resolved BYOC price is written back to oInfo.PriceInfo - // so it is the single source for: state init, initialPrice, the max-price - // ceiling, the payment's ExpectedPrice, and the price-doubling guard in - // validatePrice. Flag-gated (default OFF) so BYOC pricing is opt-in; when the - // flag is OFF or no usable cap price matches, behavior is byte-identical to - // the base-price path. + // BYOC caps: use per-capability price (and bill compute-seconds below). + // Otherwise keep base PriceInfo / lv2v pixel pricing. Write back so state, + // ExpectedPrice, and validatePrice all see the same rate. priceInfo := oInfo.PriceInfo useByocPricing := false if ls.LivepeerNode.ByocPerCapPricing { @@ -601,12 +577,8 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req } billableSecs := now.Sub(lastUpdate).Seconds() if useByocPricing { - // BYOC per-capability tariff is denominated per compute-second (wei/sec), - // so charge on seconds directly rather than synthesizing lv2v pixels. With - // pixels = ceil(billableSecs) and the resolved per-cap price kept as its - // reduced wei/sec rational, calculateFee yields capPriceWeiPerSec * seconds. + // BYOC prices are per compute-second; bill seconds instead of lv2v pixels. if billableSecs <= 0 { - // preload with 60 seconds, mirroring the lv2v first-call behavior billableSecs = (60 * time.Second).Seconds() } pixels = int64(math.Ceil(billableSecs)) diff --git a/server/remote_signer_test.go b/server/remote_signer_test.go index f02ea161f6..3ff83c6a5f 100644 --- a/server/remote_signer_test.go +++ b/server/remote_signer_test.go @@ -1720,26 +1720,17 @@ func byocCapsBlob(t *testing.T, modelID string) []byte { return blob } -// TestResolveByocPrice covers the pure per-capability BYOC price resolver: -// it matches a Capability_BYOC entry whose Constraint equals the BYOC model -// constraint from request capabilities, ignores non-BYOC entries, and returns -// nil (→ caller falls back to base) for no-match / empty constraint / -// non-positive rate. func TestResolveByocPrice(t *testing.T) { require := require.New(t) byocCap := uint32(core.Capability_BYOC) oInfo := &net.OrchestratorInfo{ - PriceInfo: &net.PriceInfo{PricePerUnit: 100, PixelsPerUnit: 1}, // base (unused by resolver) + PriceInfo: &net.PriceInfo{PricePerUnit: 100, PixelsPerUnit: 1}, CapabilitiesPrices: []*net.PriceInfo{ {Capability: byocCap, Constraint: "nano-banana", PricePerUnit: 10, PixelsPerUnit: 1}, {Capability: byocCap, Constraint: "recraft-v4", PricePerUnit: 20, PixelsPerUnit: 1}, - // decoy: a non-BYOC capability sharing the constraint name must NOT match {Capability: uint32(core.Capability_LiveVideoToVideo), Constraint: "nano-banana", PricePerUnit: 999, PixelsPerUnit: 1}, - // decoy: zero/invalid BYOC rate must be treated as no-match (fallback) {Capability: byocCap, Constraint: "free-cap", PricePerUnit: 0, PixelsPerUnit: 1}, - // misconfig: an early invalid duplicate must NOT shadow a later valid - // entry for the same constraint (scan continues past invalid rates). {Capability: byocCap, Constraint: "dup-cap", PricePerUnit: 0, PixelsPerUnit: 1}, {Capability: byocCap, Constraint: "dup-cap", PricePerUnit: 30, PixelsPerUnit: 1}, }, @@ -1809,15 +1800,6 @@ func TestResolveByocPrice(t *testing.T) { } } -// TestGenerateLivePayment_ByocPerCapPricing drives the full GenerateLivePayment -// handler (no CGO needed) and proves, via the resulting signed balance/state: -// - flag OFF: a matching cap is ignored; the fee is the byte-identical lv2v -// synthetic-pixel base fee (zero-regression); -// - flag ON: the fee is the resolved per-capability rate * compute-seconds; -// - flag ON: two caps in a 2:1 tariff produce a 2:1 fee ratio; -// - flag ON, unknown cap: falls back to the base lv2v fee; -// - flag ON with base >> 2x cap returns 200 (no false "price more than -// doubled"), proving the oInfo.PriceInfo = capPrice correction. func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { require := require.New(t) @@ -1825,9 +1807,7 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { node, _ := core.NewLivepeerNode(ethClient, "", nil) node.Balances = core.NewAddressBalances(1 * time.Minute) - // Large per-ticket EV keeps the lv2v fee within the handler's 100-ticket cap - // (lv2v fee = base * 1280*720*30*60 is large), while the small per-second - // BYOC fees resolve to a single ticket. + // Large EV keeps the lv2v fee under the 100-ticket cap. ev := big.NewRat(10_000_000_000, 1) var totalTickets uint32 sender := newMockSender(mockSenderConfig{ @@ -1849,15 +1829,12 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { node.Sender = sender ls := &LivepeerServer{LivepeerNode: node} - // Generous global max price so neither base nor cap prices trip the ceiling. autoPrice, err := core.NewAutoConvertedPrice("", big.NewRat(1_000, 1), nil) require.NoError(err) BroadcastCfg.SetMaxPrice(autoPrice) defer BroadcastCfg.SetMaxPrice(nil) - // base is intentionally >> 2x the cap rates so a regression that left - // oInfo.PriceInfo at base (while locking the cap price into state) would trip - // the >2x doubling guard. With the correction it must NOT trip. + // base >> 2x caps so leaving oInfo.PriceInfo at base would trip the doubling guard. const basePPU, capNanoPPU, capRecraftPPU = 100, 10, 20 oInfo := &net.OrchestratorInfo{ Address: ethClient.addr.Bytes(), @@ -1874,7 +1851,6 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { orchBlob, err := proto.Marshal(oInfo) require.NoError(err) - // Fresh-session preload bills 60 seconds (lv2v) of data. const preloadSecs = 60 lv2vPixels := int64(defaultSegInfo.Height) * int64(defaultSegInfo.Width) * int64(defaultSegInfo.FPS) * preloadSecs @@ -1896,26 +1872,21 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { return state } - // fee = numTickets*ev - balance for a fresh session; recover it from the - // signed state's balance and the locked initial price (also returned). feeFromState := func(state RemotePaymentState) *big.Rat { bal := new(big.Rat) _, ok := bal.SetString(state.Balance) require.True(ok, "parse balance %q", state.Balance) - // derive fee from initialPrice * pixels directly using the locked state. price := big.NewRat(state.InitialPricePerUnit, state.InitialPixelsPerUnit) var pixels int64 if state.InitialPricePerUnit == basePPU && state.InitialPixelsPerUnit == 1 { - pixels = lv2vPixels // base path: lv2v synthetic pixels + pixels = lv2vPixels } else { - pixels = preloadSecs // byoc path: compute-seconds + pixels = preloadSecs } return new(big.Rat).Mul(price, big.NewRat(pixels, 1)) } assertBalanceConsistent := func(state RemotePaymentState, fee *big.Rat) { - // Independent check: balance must equal numTickets*ev - fee with - // numTickets = ceil(max(fee,ev)/ev), validating the fee end-to-end. smc := fee if ev.Cmp(smc) > 0 { smc = ev @@ -1932,7 +1903,6 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { require.Zero(gotBal.Cmp(wantBal), "balance got=%s want=%s fee=%s", gotBal.RatString(), wantBal.RatString(), fee.RatString()) } - // --- flag OFF: cap present but ignored → base lv2v synthetic-pixel fee --- ls.LivepeerNode.ByocPerCapPricing = false offState := doPayment("nano-banana") require.EqualValues(basePPU, offState.InitialPricePerUnit, "flag OFF must lock the base price") @@ -1941,11 +1911,8 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { require.Zero(feeFromState(offState).Cmp(offFee)) assertBalanceConsistent(offState, offFee) - // --- flag ON --- ls.LivepeerNode.ByocPerCapPricing = true - // nano-banana: fee = capNanoPPU * 60 (seconds basis); base>>2x cap must NOT - // trip the doubling guard (proves oInfo.PriceInfo = capPrice). nanoState := doPayment("nano-banana") require.EqualValues(capNanoPPU, nanoState.InitialPricePerUnit, "flag ON must lock the resolved cap price") require.EqualValues(1, nanoState.InitialPixelsPerUnit) @@ -1953,7 +1920,6 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { require.Zero(feeFromState(nanoState).Cmp(nanoFee), "nano fee") assertBalanceConsistent(nanoState, nanoFee) - // recraft-v4 priced 2x nano → fee ratio is exactly 2:1. recraftState := doPayment("recraft-v4") require.EqualValues(capRecraftPPU, recraftState.InitialPricePerUnit) recraftFee := big.NewRat(capRecraftPPU*preloadSecs, 1) @@ -1961,7 +1927,6 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { assertBalanceConsistent(recraftState, recraftFee) require.Zero(recraftFee.Cmp(new(big.Rat).Mul(nanoFee, big.NewRat(2, 1))), "recraft fee must be 2x nano fee") - // unknown cap with flag ON → fall back to base lv2v fee (zero-regression). unknownState := doPayment("totally-unknown-cap") require.EqualValues(basePPU, unknownState.InitialPricePerUnit, "unknown cap must fall back to base") require.Zero(feeFromState(unknownState).Cmp(offFee)) From 1ce12e91a4618d52527ff7e454ad1909a852cffb Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Thu, 9 Jul 2026 20:38:32 -0400 Subject: [PATCH 4/4] refactor(config): remove ByocPerCapPricing flag and related logic Eliminate the ByocPerCapPricing configuration option from the LivepeerConfig structure and its associated logic throughout the codebase. Update related tests to reflect the change in naming and functionality, ensuring consistency in pricing behavior for live payments. --- cmd/livepeer/starter/flags.go | 1 - cmd/livepeer/starter/starter.go | 6 ------ core/livepeernode.go | 1 - server/remote_signer.go | 17 ++++++++--------- server/remote_signer_test.go | 19 +++++-------------- 5 files changed, 13 insertions(+), 31 deletions(-) diff --git a/cmd/livepeer/starter/flags.go b/cmd/livepeer/starter/flags.go index cb536697fc..af2d7eafd7 100644 --- a/cmd/livepeer/starter/flags.go +++ b/cmd/livepeer/starter/flags.go @@ -149,7 +149,6 @@ func NewLivepeerConfig(fs *flag.FlagSet) LivepeerConfig { cfg.RemoteSignerWebhookHeaders = fs.String("remoteSignerWebhookHeaders", *cfg.RemoteSignerWebhookHeaders, "Map of headers to use for remote signer webhook requests. e.g. 'header:val,header2:val2'") cfg.RemoteSignerAllowNoAuth = fs.Bool("remoteSignerAllowNoAuth", *cfg.RemoteSignerAllowNoAuth, "Allow an unauthenticated remote signer on a public -httpAddr (no webhook). UNSAFE: signs payments from this node's deposit for any reachable caller; restrict access externally (proxy/private network).") cfg.RemoteDiscovery = fs.Bool("remoteDiscovery", *cfg.RemoteDiscovery, "Enable orchestrator discovery on remote signers") - cfg.ByocPerCapPricing = fs.Bool("byocPerCapPricing", *cfg.ByocPerCapPricing, "Remote signer: charge BYOC live payments from CapabilitiesPrices instead of the base price (default OFF)") // Gateway metrics cfg.KafkaBootstrapServers = fs.String("kafkaBootstrapServers", *cfg.KafkaBootstrapServers, "URL of Kafka Bootstrap Servers") diff --git a/cmd/livepeer/starter/starter.go b/cmd/livepeer/starter/starter.go index 2a00ad7d35..6c7741e7c3 100755 --- a/cmd/livepeer/starter/starter.go +++ b/cmd/livepeer/starter/starter.go @@ -180,7 +180,6 @@ type LivepeerConfig struct { RemoteSignerWebhookHeaders *string RemoteSignerAllowNoAuth *bool RemoteDiscovery *bool - ByocPerCapPricing *bool AIRunnerImage *string AIRunnerImageOverrides *string AIVerboseLogs *bool @@ -327,7 +326,6 @@ func DefaultLivepeerConfig() LivepeerConfig { defaultRemoteSignerWebhookHeaders := "" defaultRemoteSignerAllowNoAuth := false defaultRemoteDiscovery := false - defaultByocPerCapPricing := false // Gateway logs defaultKafkaBootstrapServers := "" @@ -460,7 +458,6 @@ func DefaultLivepeerConfig() LivepeerConfig { RemoteSignerWebhookHeaders: &defaultRemoteSignerWebhookHeaders, RemoteSignerAllowNoAuth: &defaultRemoteSignerAllowNoAuth, RemoteDiscovery: &defaultRemoteDiscovery, - ByocPerCapPricing: &defaultByocPerCapPricing, // Gateway logs KafkaBootstrapServers: &defaultKafkaBootstrapServers, @@ -1884,9 +1881,6 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { if cfg.RemoteDiscovery != nil { n.RemoteDiscovery = *cfg.RemoteDiscovery } - if cfg.ByocPerCapPricing != nil { - n.ByocPerCapPricing = *cfg.ByocPerCapPricing - } if cfg.LiveAIHeartbeatHeaders != nil { n.LiveAIHeartbeatHeaders = parseHeaderMap(*cfg.LiveAIHeartbeatHeaders) } diff --git a/core/livepeernode.go b/core/livepeernode.go index 501ae0975c..31cc4d108a 100644 --- a/core/livepeernode.go +++ b/core/livepeernode.go @@ -158,7 +158,6 @@ type LivepeerNode struct { RemoteEthAddr ethcommon.Address // eth address of the remote signer InfoSig []byte // sig over eth address for the OrchestratorInfo request RemoteDiscovery bool // expose remote discovery endpoint when enabled - ByocPerCapPricing bool // remote signer: use CapabilitiesPrices for BYOC (default OFF) // Thread safety for config fields mu sync.RWMutex diff --git a/server/remote_signer.go b/server/remote_signer.go index 248091ef89..e32214964b 100644 --- a/server/remote_signer.go +++ b/server/remote_signer.go @@ -444,12 +444,10 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req // ExpectedPrice, and validatePrice all see the same rate. priceInfo := oInfo.PriceInfo useByocPricing := false - if ls.LivepeerNode.ByocPerCapPricing { - if capPrice := resolveByocPrice(reqCaps, &oInfo); capPrice != nil { - priceInfo = capPrice - oInfo.PriceInfo = capPrice - useByocPricing = true - } + if capPrice := resolveByocPrice(reqCaps, &oInfo); capPrice != nil { + priceInfo = capPrice + oInfo.PriceInfo = capPrice + useByocPricing = true } if priceInfo == nil || priceInfo.PricePerUnit == 0 || priceInfo.PixelsPerUnit == 0 { err := fmt.Errorf("missing or zero priceInfo") @@ -576,13 +574,14 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req lastUpdate = now } billableSecs := now.Sub(lastUpdate).Seconds() - if useByocPricing { + switch { + case useByocPricing: // BYOC prices are per compute-second; bill seconds instead of lv2v pixels. if billableSecs <= 0 { billableSecs = (60 * time.Second).Seconds() } pixels = int64(math.Ceil(billableSecs)) - } else if req.Type == RemoteType_LiveVideoToVideo { + case req.Type == RemoteType_LiveVideoToVideo: info := defaultSegInfo if billableSecs <= 0 { // preload with 60 seconds of data for LV2V @@ -590,7 +589,7 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req } pixelsPerSec := float64(info.Height) * float64(info.Width) * float64(info.FPS) pixels = int64(pixelsPerSec * billableSecs) // pixels to charge for - } else if req.Type != "" { + case req.Type != "": err = errors.New("invalid job type") respondJsonError(ctx, w, err, http.StatusBadRequest) return diff --git a/server/remote_signer_test.go b/server/remote_signer_test.go index 3ff83c6a5f..46cf6d7857 100644 --- a/server/remote_signer_test.go +++ b/server/remote_signer_test.go @@ -1800,7 +1800,7 @@ func TestResolveByocPrice(t *testing.T) { } } -func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { +func TestGenerateLivePayment_ByocCapabilityPricing(t *testing.T) { require := require.New(t) ethClient := newTestEthClient(t) @@ -1903,18 +1903,8 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { require.Zero(gotBal.Cmp(wantBal), "balance got=%s want=%s fee=%s", gotBal.RatString(), wantBal.RatString(), fee.RatString()) } - ls.LivepeerNode.ByocPerCapPricing = false - offState := doPayment("nano-banana") - require.EqualValues(basePPU, offState.InitialPricePerUnit, "flag OFF must lock the base price") - require.EqualValues(1, offState.InitialPixelsPerUnit) - offFee := new(big.Rat).Mul(big.NewRat(basePPU, 1), big.NewRat(lv2vPixels, 1)) - require.Zero(feeFromState(offState).Cmp(offFee)) - assertBalanceConsistent(offState, offFee) - - ls.LivepeerNode.ByocPerCapPricing = true - nanoState := doPayment("nano-banana") - require.EqualValues(capNanoPPU, nanoState.InitialPricePerUnit, "flag ON must lock the resolved cap price") + require.EqualValues(capNanoPPU, nanoState.InitialPricePerUnit, "must lock the resolved cap price") require.EqualValues(1, nanoState.InitialPixelsPerUnit) nanoFee := big.NewRat(capNanoPPU*preloadSecs, 1) require.Zero(feeFromState(nanoState).Cmp(nanoFee), "nano fee") @@ -1929,6 +1919,7 @@ func TestGenerateLivePayment_ByocPerCapPricing(t *testing.T) { unknownState := doPayment("totally-unknown-cap") require.EqualValues(basePPU, unknownState.InitialPricePerUnit, "unknown cap must fall back to base") - require.Zero(feeFromState(unknownState).Cmp(offFee)) - assertBalanceConsistent(unknownState, offFee) + fallbackFee := new(big.Rat).Mul(big.NewRat(basePPU, 1), big.NewRat(lv2vPixels, 1)) + require.Zero(feeFromState(unknownState).Cmp(fallbackFee)) + assertBalanceConsistent(unknownState, fallbackFee) }