322 lines
12 KiB
Go
322 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)
|
|
}
|
|
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 }
|