tinyrelayfile
tinyrelay/internal/gates/jobs.go/

internal/gates/jobs.go raw
1package gates
2
3// Long tasks follow NIP-90: a member or agent publishes a job request
4// (kind 5000 to 5127 or 5129 to 5999), and a serving agent or member answers it with
5// feedback (kind 7000) and a result (the request kind plus 1000). The relay
6// keeps every event signed by its author; it only checks that requests,
7// feedback and results carry the tags the protocol needs, that they are
8// published by members or agents, and that an agent's answers name a
9// request the relay holds.
10
11import (
12 "context"
13 "errors"
14 "fmt"
15 "strconv"
16
17 "github.com/FelineStateMachine/tinyrelay/internal/event"
18 "github.com/FelineStateMachine/tinyrelay/internal/policy"
19 "github.com/FelineStateMachine/tinyrelay/internal/relay"
20 "github.com/FelineStateMachine/tinyrelay/internal/storage"
21)
22
23// JobShape checks the tags of a job request, result or feedback event. It
24// returns nil for other event kinds.
25func JobShape(e event.Event) error {
26 switch {
27 case event.IsJobRequest(e.Kind):
28 return jobRequestShape(e)
29 case event.IsJobResult(e.Kind):
30 return jobReplyShape(e, "job result")
31 case e.Kind == event.KIND_JOB_FEEDBACK:
32 if err := jobReplyShape(e, "job feedback"); err != nil {
33 return err
34 }
35 if !event.IsJobFeedbackStatus(event.Tag(e, "status")) {
36 return errors.New("invalid: job feedback status must be payment-required, processing, error, success or partial")
37 }
38 }
39 return nil
40}
41
42func jobRequestShape(e event.Event) error {
43 for _, tag := range e.Tags {
44 if len(tag) == 0 || tag[0] != "i" {
45 continue
46 }
47 if len(tag) < 3 || !event.IsJobInputType(tag[2]) {
48 return errors.New("invalid: job input i tag needs data and a type of url, event, job or text")
49 }
50 if (tag[2] == "event" || tag[2] == "job") && !jobID(tag[1]) {
51 return errors.New("invalid: job input of type event or job must name an event id")
52 }
53 }
54 if hasTag(e, "encrypted") && !jobID(event.Tag(e, "p")) {
55 return errors.New("invalid: encrypted job request must name the service provider in a p tag")
56 }
57 if bid := event.Tag(e, "bid"); bid != "" && !jobAmount(bid) {
58 return errors.New("invalid: job bid must be an amount in millisats")
59 }
60 return nil
61}
62
63func jobReplyShape(e event.Event, what string) error {
64 if !jobID(event.Tag(e, "e")) {
65 return fmt.Errorf("invalid: %s must name the job request in an e tag", what)
66 }
67 if !jobID(event.Tag(e, "p")) {
68 return fmt.Errorf("invalid: %s must name the requester in a p tag", what)
69 }
70 if amount := event.Tag(e, "amount"); amount != "" && !jobAmount(amount) {
71 return fmt.Errorf("invalid: %s amount must be in millisats", what)
72 }
73 return nil
74}
75
76// jobFeature refuses job kinds while the owner has long tasks switched off.
77func jobFeature(p policy.Policy, e event.Event) error {
78 if event.IsJobKind(e.Kind) && !p.Features.Jobs {
79 return errors.New("restricted: long tasks are switched off on this relay")
80 }
81 return nil
82}
83
84// jobWriter admits job kinds from members and agents only. An agent is a
85// member with the agent role, so one membership check covers both.
86func jobWriter(e event.Event, a policy.Access) error {
87 if event.IsJobKind(e.Kind) && !a.Member && !a.Owner {
88 return errors.New("restricted: long tasks are accepted from members and agents")
89 }
90 return nil
91}
92
93// jobReply checks a result or feedback against the request it names. When
94// the relay holds the request, the result kind must match it and the p tag
95// must name its author. An agent may only answer a request the relay holds
96// and the agent may read; a member may also answer requests made elsewhere.
97func (g *Gate) jobReply(ctx context.Context, e event.Event, now int64, agent bool) error {
98 if !event.IsJobResult(e.Kind) && e.Kind != event.KIND_JOB_FEEDBACK {
99 return nil
100 }
101 missing := errors.New("restricted: agent grant allows job results and feedback only for requests this relay holds")
102 if g.cfg.Store == nil {
103 if agent {
104 return missing
105 }
106 return nil
107 }
108 result, err := g.cfg.Store.Query(ctx, event.Filter{IDs: []string{event.Tag(e, "e")}}, storage.QueryOptions{Now: now, Access: storage.Access{All: true}, Limit: 1})
109 if err != nil {
110 return fmt.Errorf("check job request: %w", err)
111 }
112 if len(result.Events) == 0 || !event.IsJobRequest(result.Events[0].Kind) {
113 if agent {
114 return missing
115 }
116 return nil
117 }
118 request := result.Events[0]
119 if agent && !g.CanSee(ctx, request, relay.Session{PubKeys: []string{e.PubKey}}, nil) {
120 return missing
121 }
122 if event.IsJobResult(e.Kind) && e.Kind != event.JobResultKind(request.Kind) {
123 return fmt.Errorf("invalid: job result kind %d does not answer a kind %d request", e.Kind, request.Kind)
124 }
125 if room := event.Tag(request, "h"); room != "" && event.Tag(e, "h") != room {
126 return errors.New("invalid: job result or feedback must keep the request's room h tag")
127 }
128 if event.Tag(e, "p") != request.PubKey {
129 return errors.New("invalid: job result or feedback p tag must name the requester")
130 }
131 return nil
132}
133
134func jobID(value string) bool {
135 return len(value) == 64 && isLowerHex(value)
136}
137
138func jobAmount(value string) bool {
139 _, err := strconv.ParseUint(value, 10, 64)
140 return err == nil
141}
142
143func isLowerHex(value string) bool {
144 for _, ch := range value {
145 if (ch < '0' || ch > '9') && (ch < 'a' || ch > 'f') {
146 return false
147 }
148 }
149 return true
150}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs