Files
mesh-sdk/go/delivery/view_answers.go
T
jschoubben d6cef5829b Have a want name its composition and a backup answer; give the node its own want, the seventh rule, the held phase and the backup and record asks (issue 1943, S3)
The node agent applies only the composition its want names, and the assigner writes a want only once that
composition exists (ADR 0539 clauses 1, 6, 14 and 16).
2026-10-24 16:29:24 +02:00

316 lines
12 KiB
Go

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.<group>.<heads hash>`, 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.<module>.<fingerprint hex>`: release it or
// keep it held) and the restore of a data module set back (`restore.<node>.<module>.<fingerprint hex>`: 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 `<ask key>.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.<module>.<hex>`.
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.<node>.<module>.<hex>`.
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.<node>.<module>.<hex>`.
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.<node>.<module>.<digest hex>`, 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.<node>.<module>.<fingerprint hex>"))
}
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.<node>.<module>.<fingerprint hex>"))
}
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>.<digest hex>"))
}
node, module, isNode, err := ParseHaveKey(parts[0] + "." + parts[1])
if err != nil || isNode {
return refused(fmt.Errorf("a record ask is record.<node>.<module>.<digest hex>"))
}
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: `<ask key>.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, <ask 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)
}
if _, ok := ask.Ask.Option(a.Option); !ok {
return fmt.Errorf("delivery: the view's answer chose %q, which the asking %s does not offer", a.Option, ask.Ask.ID)
}
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 }