package delivery import ( "encoding/json" "fmt" "strings" "git.novox.be/novox/mesh-sdk/go/asks" ) // The asks the view may answer, and its answers (novox/hq ADR 0528 clause 6, ADR 0429 §3 and §6, ADR 0437 §10). The // gate's merge ask (`merge..`, MergeAskKey) needs proof of who answered (ADR 0421 §1), so it is // answered on the phone alone until issue 953 closes. The assigner's asks need no such proof, and the view may // answer them: the hold of a fingerprint whose health check failed (`hold..`: release it or // keep it held) and the restore of a data module set back (`restore...`: restore and // lose newer writes, or leave it stopped, ADR 0429 §6). The assigner writes them in its bucket asks; the view writes // its answer `.view` in its bucket answers, and the teller, which alone fills an ask's answer, takes it from // there as it takes a warrant from the phone. const ( // ViewModule is the module of the view part. ViewModule = "mesh-delivery-view" // ViewAnswersBucket is the view's bucket of answers, the only state the view writes. ViewAnswersBucket = ViewModule + "_answers" // ViewAnswersHistory is the revisions the bucket keeps per key. ViewAnswersHistory = 5 // AssignerAsksBucket is the assigner's bucket of asks that need no proof of who answered. AssignerAsksBucket = AssignerModule + "_asks" // AssignerAsksHistory is the revisions the bucket keeps per key. AssignerAsksHistory = 5 // AskKindHold is the kind of the assigner's ask about a held fingerprint. AskKindHold = "hold" // AskKindRestore is the kind of the assigner's ask about a data module set back. AskKindRestore = "restore" // AskKindBackup is the kind of the assigner's ask about a backup before an apply that failed: back up again, or go // on without one (novox/hq ADR 0539 clause 16). AskKindBackup = "backup" // AskKindRecord is the kind of the assigner's ask about a move a person must allow, for a module of policy record: // a composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14). AskKindRecord = "record" viewWord = "view" ) // AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's // Module and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest // Composed of the composition it would allow. type AskKey struct { Kind string Group string Hash string Node string Module string FP string Composed string } // AskNeedsProof says whether an ask of this kind must prove who answered it, so the view may not answer it: the // merge's Approve does (ADR 0421 §1, issue 953), the assigner's hold and restore do not. An unknown kind is refused. func AskNeedsProof(kind string) (bool, error) { switch kind { case AskKindMerge: return true, nil case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord: return false, nil } return false, fmt.Errorf("delivery: %q is not a kind of ask, %s, %s, %s, %s or %s", kind, AskKindMerge, AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord) } // HoldAskKey is the key, in AssignerAsksBucket, of the ask about a module's held fingerprint: `hold..`. func HoldAskKey(module, fingerprint string) (string, error) { key, err := BuilderBuildKey(module, fingerprint) if err != nil { return "", err } return AskKindHold + "." + key, nil } // RestoreAskKey is the key, in AssignerAsksBucket, of the ask about a data module set back on a node from a // fingerprint: `restore...`. func RestoreAskKey(node, module, fingerprint string) (string, error) { if err := checkName("node", node); err != nil { return "", err } key, err := BuilderBuildKey(module, fingerprint) if err != nil { return "", err } return AskKindRestore + "." + node + "." + key, nil } // BackupAskKey is the key, in AssignerAsksBucket, of the ask about a module's failed backup on a node before it applies // a fingerprint: `backup...`. func BackupAskKey(node, module, fingerprint string) (string, error) { key, err := ComposedKey(node, module, fingerprint) if err != nil { return "", err } return AskKindBackup + "." + key, nil } // RecordAskKey is the key, in AssignerAsksBucket, of the ask about a move of a module on a node to the composition // whose digest it names: `record...`, so a new revision at the same fingerprint is asked // again (ADR 0539 clause 14). func RecordAskKey(node, module, composed string) (string, error) { have, err := HaveKey(node, module) if err != nil { return "", err } if err := checkDigest(composed, "a record ask's composition digest"); err != nil { return "", err } return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil } // ParseAnyAskKey reads an ask key of any kind (MergeAskKey, HoldAskKey, RestoreAskKey, BackupAskKey, RecordAskKey); it // refuses an unknown kind // and a key whose parts do not fit its kind, naming the key. func ParseAnyAskKey(key string) (AskKey, error) { kind, rest, _ := strings.Cut(key, ".") refused := func(err error) (AskKey, error) { return AskKey{}, fmt.Errorf("delivery: %q is not an ask key: %w", key, err) } switch kind { case AskKindMerge: _, group, hash, err := ParseAskKey(key) if err != nil { return AskKey{}, err } return AskKey{Kind: kind, Group: group, Hash: hash}, nil case AskKindHold: module, fp, err := ParseBuilderBuildKey(rest) if err != nil { return refused(err) } return AskKey{Kind: kind, Module: module, FP: fp}, nil case AskKindRestore: node, build, ok := strings.Cut(rest, ".") if !ok { return refused(fmt.Errorf("a restore ask is restore...")) } if err := checkName("node", node); err != nil { return refused(err) } module, fp, err := ParseBuilderBuildKey(build) if err != nil { return refused(err) } return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil case AskKindBackup: node, module, fp, isNode, err := ParseComposedKey(rest) if err != nil || isNode { return refused(fmt.Errorf("a backup ask is backup...")) } return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil case AskKindRecord: parts := strings.Split(rest, ".") if len(parts) != 3 { return refused(fmt.Errorf("a record ask is record...")) } node, module, isNode, err := ParseHaveKey(parts[0] + "." + parts[1]) if err != nil || isNode { return refused(fmt.Errorf("a record ask is record...")) } composed := "sha256:" + parts[2] if err := checkDigest(composed, "a record ask's composition digest"); err != nil { return refused(err) } return AskKey{Kind: kind, Node: node, Module: module, Composed: composed}, nil } _, err := AskNeedsProof(kind) return refused(err) } // ViewAnswerKey is the key, in ViewAnswersBucket, of the view's answer to the ask at askKey: `.view`. An ask // that needs proof of who answered is refused with its reason: it is answered on the phone (issue 953). func ViewAnswerKey(askKey string) (string, error) { ask, err := ParseAnyAskKey(askKey) if err != nil { return "", err } if proof, _ := AskNeedsProof(ask.Kind); proof { return "", fmt.Errorf("delivery: the ask %s needs proof of who answered: answer it on the phone (issue 953)", askKey) } return askKey + "." + viewWord, nil } // ParseViewAnswerKey is the inverse of ViewAnswerKey: the ask's key. It refuses a key without `.view`, an ask key of // a kind that needs proof, and a key of another bucket, naming it. func ParseViewAnswerKey(key string) (askKey string, err error) { askKey, ok := strings.CutSuffix(key, "."+viewWord) if !ok { return "", fmt.Errorf("delivery: %q is not a view answer key, .view", key) } if _, err := ViewAnswerKey(askKey); err != nil { return "", fmt.Errorf("delivery: %q is not a view answer key: %w", key, err) } return askKey, nil } // AssignerAsk is the value of a key in AssignerAsksBucket: the router's ask whole, as the assigner composes it, so the // teller publishes it unchanged. Asking counts the times the assigner wrote the ask at this key, as the gate's Ask // does: 0 or 1 for the first, n for the n-th, and the ask's id is AskingID(key, Asking), so each asking has its own id // and an answer to an earlier asking is never taken for a later one. type AssignerAsk struct { V int `json:"v"` Ask asks.Ask `json:"ask"` Asking int `json:"asking,omitempty"` } func (a AssignerAsk) check(key string) error { ask, err := ParseAnyAskKey(key) if err != nil { return err } if proof, _ := AskNeedsProof(ask.Kind); proof { return fmt.Errorf("delivery: the assigner asks no %s ask: it needs proof of who answered", ask.Kind) } if a.Asking < 0 { return fmt.Errorf("delivery: the ask's asking %d is negative", a.Asking) } if want := AskingID(key, a.Asking); a.Ask.ID != want { return fmt.Errorf("delivery: the ask's id %q is not %q, the id its key and asking %d give", a.Ask.ID, want, a.Asking) } if len(a.Ask.Options) == 0 { return fmt.Errorf("delivery: the ask has no option") } return nil } // EncodeAssignerAsk is a, the ask at key, as JSON at Version; it refuses a key of a kind that needs proof or of no // kind, an id other than the one the key and the asking give, and an ask with no option. func EncodeAssignerAsk(key string, a AssignerAsk) ([]byte, error) { if err := a.check(key); err != nil { return nil, err } a.V = Version return json.Marshal(a) } // DecodeAssignerAsk reads the ask at key; it refuses an unknown version (ErrUnknownVersion) and what EncodeAssignerAsk // refuses. func DecodeAssignerAsk(key string, data []byte) (AssignerAsk, error) { var a AssignerAsk if err := decode(data, &a); err != nil { return AssignerAsk{}, err } if err := a.check(key); err != nil { return AssignerAsk{}, err } return a, nil } // SameAssignerAsk says whether two asks are the same value. func SameAssignerAsk(a, b AssignerAsk) bool { return a.V == b.V && a.Asking == b.Asking && a.Ask.Digest() == b.Ask.Digest() } // ViewAnswer is the view's answer to an ask: the id of the asking it answers (the ask's id, AskingID of its key and // asking), the option the operator chose and their words. The entry's own time says when. type ViewAnswer struct { V int `json:"v"` Asking string `json:"asking"` Option string `json:"option"` Words string `json:"words,omitempty"` } func (a ViewAnswer) check() error { if strings.TrimSpace(a.Asking) == "" { return fmt.Errorf("delivery: a view answer names no asking") } if strings.TrimSpace(a.Option) == "" { return fmt.Errorf("delivery: a view answer names no option") } return nil } // ViewAnswerFits says whether the view's answer a answers the ask as it stands now: it refuses an answer to another // asking of the ask (a stale answer to an earlier writing of the same key) and an option the ask does not offer, // naming both. func ViewAnswerFits(a ViewAnswer, ask AssignerAsk) error { if a.Asking != ask.Ask.ID { return fmt.Errorf("delivery: the view's answer is to the asking %s, and the ask stands at the asking %s", a.Asking, ask.Ask.ID) } o, ok := ask.Ask.Option(a.Option) if !ok { return fmt.Errorf("delivery: the view's answer chose %q, which the asking %s does not offer", a.Option, ask.Ask.ID) } if o.Level != "" && o.Level != asks.Acknowledge { // An option that authorises is answered on a channel that proves who answered (ADR 0259 §5, ADR 0546 clause 8), // never in the view, which proves nobody. return fmt.Errorf("delivery: the view's answer chose %q, an option of level %s, which only a channel that proves who answered takes", a.Option, o.Level) } return nil } // EncodeViewAnswer is a as JSON at Version; it refuses an answer naming no asking or no option. func EncodeViewAnswer(a ViewAnswer) ([]byte, error) { if err := a.check(); err != nil { return nil, err } a.V = Version return json.Marshal(a) } // DecodeViewAnswer reads a view answer; it refuses an unknown version (ErrUnknownVersion) and an answer naming no // asking or no option. func DecodeViewAnswer(data []byte) (ViewAnswer, error) { var a ViewAnswer if err := decode(data, &a); err != nil { return ViewAnswer{}, err } if err := a.check(); err != nil { return ViewAnswer{}, err } return a, nil } // SameViewAnswer says whether two view answers are the same value. func SameViewAnswer(a, b ViewAnswer) bool { return a == b }