Skip to content

Commit cd20825

Browse files
authored
ai/live: Remote signer updates (#3870)
* ai/live: Special-case zero NumTickets Per the code: if the balance is already large enough to cover the required minimum credit (fee with ticket EV as the floor) then the code currently does not generate a ticket. Checking for this case prevents us from creating an empty-ticket payment. While those are not a huge problem for the orchestrators, it still leads to undesirable noise across the network. * ai/live: Check for a required ManifestID Help out SDK implementers in case they miss this, since the errors are non-obvious: the orchestrator runs out of balance within a few minutes, even if payments are being sent regularly. * ai/live: Simplify state in remote signer unit tests. Compute the signature for test cases automatically if one was not provided as part of the test. Saves a bunch of boilerplate.
1 parent d654ac5 commit cd20825

File tree

2 files changed

+74
-21
lines changed

2 files changed

+74
-21
lines changed

server/remote_signer.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
const HTTPStatusRefreshSession = 480
2828
const HTTPStatusPriceExceeded = 481
29+
const HTTPStatusNoTickets = 482
2930
const RemoteType_LiveVideoToVideo = "lv2v"
3031

3132
// SignOrchestratorInfo handles signing GetOrchestratorInfo requests for multiple orchestrators
@@ -248,7 +249,8 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req
248249
err error
249250
)
250251
reqState, reqSig := req.State.State, req.State.Sig
251-
if len(reqState) != 0 || len(reqSig) != 0 {
252+
hasState := len(reqState) != 0 || len(reqSig) != 0
253+
if hasState {
252254
if err := verifyStateSignature(ls, reqState, reqSig); err != nil {
253255
err = errors.New("invalid sig")
254256
respondJsonError(ctx, w, err, http.StatusBadRequest)
@@ -278,6 +280,12 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req
278280

279281
manifestID := req.ManifestID
280282
if manifestID == "" {
283+
if hasState {
284+
// Required for lv2v so stateful requests stay tied to the same id.
285+
err := errors.New("missing manifestID")
286+
respondJsonError(ctx, w, err, http.StatusBadRequest)
287+
return
288+
}
281289
manifestID = string(core.RandomManifestID())
282290
}
283291
ctx = clog.AddVal(ctx, "manifest_id", manifestID)
@@ -393,6 +401,15 @@ func (ls *LivepeerServer) GenerateLivePayment(w http.ResponseWriter, r *http.Req
393401
respondJsonError(ctx, w, err, http.StatusInternalServerError)
394402
return
395403
}
404+
if balUpdate.NumTickets <= 0 {
405+
// No new tickets are needed when reserved balance already covers the
406+
// required minimum credit (fee with ticket EV as the floor). Caller
407+
// should retry once balance has been run down further.
408+
err = errors.New("no tickets")
409+
clog.Errorf(ctx, "No tickets")
410+
respondJsonError(ctx, w, err, HTTPStatusNoTickets)
411+
return
412+
}
396413
if balUpdate.NumTickets > 100 {
397414
// Prevent both draining funds and perf issues
398415
ev, err := sender.EV(sess.PMSessionID)

server/remote_signer_test.go

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -347,25 +347,42 @@ func TestGenerateLivePayment_StateValidationErrors(t *testing.T) {
347347
return sig
348348
}
349349

350-
priceIncreaseStateBytes, priceIncreaseStateSig := func() ([]byte, []byte) {
350+
priceIncreaseStateBytes := func() []byte {
351351
stateBytes, err := json.Marshal(RemotePaymentState{
352352
StateID: "state",
353353
OrchestratorAddress: ethcommon.BytesToAddress(orchInfo.Address),
354354
InitialPricePerUnit: 100,
355355
InitialPixelsPerUnit: 1,
356356
})
357357
require.NoError(err)
358-
return stateBytes, sign(stateBytes)
358+
return stateBytes
359359
}()
360360

361361
tests := []struct {
362-
name string
363-
stateBytes []byte
364-
stateSig []byte
365-
orchInfo *net.OrchestratorInfo
366-
wantStatus int
367-
wantMsg string
362+
name string
363+
stateBytes []byte
364+
stateSig []byte
365+
orchInfo *net.OrchestratorInfo
366+
omitManifestID bool
367+
wantStatus int
368+
wantMsg string
368369
}{
370+
{
371+
name: "missing manifest id with state",
372+
stateBytes: func() []byte {
373+
state, err := json.Marshal(RemotePaymentState{
374+
StateID: "state",
375+
OrchestratorAddress: ethcommon.BytesToAddress(orchInfo.Address),
376+
InitialPricePerUnit: 1,
377+
InitialPixelsPerUnit: 1,
378+
})
379+
require.NoError(err)
380+
return state
381+
}(),
382+
omitManifestID: true,
383+
wantStatus: http.StatusBadRequest,
384+
wantMsg: "missing manifestID",
385+
},
369386
{
370387
name: "invalid state signature",
371388
stateBytes: []byte(`{"stateID":"state","orchestratorAddress":"0x1"}`),
@@ -376,7 +393,6 @@ func TestGenerateLivePayment_StateValidationErrors(t *testing.T) {
376393
{
377394
name: "invalid state json",
378395
stateBytes: []byte("not-json"),
379-
stateSig: sign([]byte("not-json")),
380396
wantStatus: http.StatusBadRequest,
381397
wantMsg: "invalid state",
382398
},
@@ -391,15 +407,6 @@ func TestGenerateLivePayment_StateValidationErrors(t *testing.T) {
391407
require.NoError(err)
392408
return state
393409
}(),
394-
stateSig: sign(func() []byte {
395-
state, err := json.Marshal(RemotePaymentState{
396-
StateID: "state",
397-
OrchestratorAddress: ethcommon.HexToAddress("0x1"),
398-
InitialPixelsPerUnit: 1,
399-
})
400-
require.NoError(err)
401-
return state
402-
}()),
403410
orchInfo: func() *net.OrchestratorInfo {
404411
oInfo := proto.Clone(orchInfo).(*net.OrchestratorInfo)
405412
oInfo.Address = ethcommon.HexToAddress("0x2").Bytes()
@@ -411,7 +418,6 @@ func TestGenerateLivePayment_StateValidationErrors(t *testing.T) {
411418
{
412419
name: "orchestrator price increased more than 2x",
413420
stateBytes: priceIncreaseStateBytes,
414-
stateSig: priceIncreaseStateSig,
415421
orchInfo: func() *net.OrchestratorInfo {
416422
oInfo := proto.Clone(orchInfo).(*net.OrchestratorInfo)
417423
oInfo.PriceInfo = &net.PriceInfo{PricePerUnit: 250, PixelsPerUnit: 1}
@@ -420,6 +426,23 @@ func TestGenerateLivePayment_StateValidationErrors(t *testing.T) {
420426
wantStatus: HTTPStatusPriceExceeded,
421427
wantMsg: "Orchestrator price has more than doubled",
422428
},
429+
{
430+
name: "zero tickets returns 482",
431+
stateBytes: func() []byte {
432+
stateBytes, err := json.Marshal(RemotePaymentState{
433+
StateID: "state",
434+
OrchestratorAddress: ethcommon.BytesToAddress(orchInfo.Address),
435+
// Existing balance large enough so StageUpdate yields NumTickets == 0.
436+
Balance: "1000",
437+
InitialPricePerUnit: 1,
438+
InitialPixelsPerUnit: 1,
439+
})
440+
require.NoError(err)
441+
return stateBytes
442+
}(),
443+
wantStatus: HTTPStatusNoTickets,
444+
wantMsg: "no tickets",
445+
},
423446
}
424447

425448
for _, tt := range tests {
@@ -431,10 +454,20 @@ func TestGenerateLivePayment_StateValidationErrors(t *testing.T) {
431454
orchBlob, err := proto.Marshal(oInfo)
432455
require.NoError(err)
433456

457+
var manifestID string
458+
if !tt.omitManifestID {
459+
manifestID = "manifest"
460+
}
461+
stateSig := tt.stateSig
462+
if stateSig == nil {
463+
stateSig = sign(tt.stateBytes)
464+
}
465+
434466
reqBody, err := json.Marshal(RemotePaymentRequest{
435467
Orchestrator: orchBlob,
468+
ManifestID: manifestID,
436469
InPixels: 1,
437-
State: RemotePaymentStateSig{State: tt.stateBytes, Sig: tt.stateSig},
470+
State: RemotePaymentStateSig{State: tt.stateBytes, Sig: stateSig},
438471
})
439472
require.NoError(err)
440473

@@ -533,9 +566,11 @@ func TestGenerateLivePayment_LV2V_Succeeds(t *testing.T) {
533566
}
534567
orchBlob, err := proto.Marshal(oInfo)
535568
require.NoError(err)
569+
const manifestID = "lv2v-manifest"
536570

537571
resp, payment := doPayment(RemotePaymentRequest{
538572
Orchestrator: orchBlob,
573+
ManifestID: manifestID,
539574
InPixels: inPixels,
540575
})
541576
require.NotEmpty(resp.Payment)
@@ -579,6 +614,7 @@ func TestGenerateLivePayment_LV2V_Succeeds(t *testing.T) {
579614
const inPixelsUpdated int64 = 2500
580615
resp2, payment2 := doPayment(RemotePaymentRequest{
581616
Orchestrator: orchBlob,
617+
ManifestID: manifestID,
582618
InPixels: inPixelsUpdated,
583619
State: resp.State,
584620
})

0 commit comments

Comments
 (0)