package operatorask import ( "crypto/sha256" "encoding/hex" "fmt" "sort" "strconv" "strings" ) // An Act is what an option of an ask binds (novox/hq ADR 0286 §10, §11): named fields, each a string — the verb, the // machine, the level, and each argument under a name of its own ("arg.delivery"). Flat on purpose: its digest is over // these names and values alone, never over how a language happens to encode a struct. It is mesh-sdk's (design 47 G // lead answers A-198, A-219), so the seat server's gate in go/process digests an act as every asker does; // operatorchannelclient re-exports it. type Act map[string]string // ActDigest is the digest an asker puts in an option's binds: SHA-256 over the act's canonical encoding, written // "sha256:". The same names and values give the same digest in any language, whatever order they were set in; a // field renamed, added or emptied gives another. func ActDigest(act Act) (string, error) { if len(act) == 0 { return "", fmt.Errorf("the act cannot be digested: it names nothing") } names := make([]string, 0, len(act)) for k := range act { if k == "" { return "", fmt.Errorf("the act cannot be digested: a field has no name") } names = append(names, k) } sort.Strings(names) var b strings.Builder b.WriteString("novox.act.v1\n") for _, k := range names { canonical(&b, k) canonical(&b, act[k]) } sum := sha256.Sum256([]byte(b.String())) return "sha256:" + hex.EncodeToString(sum[:]), nil } // canonical writes one value as its length in bytes, a colon, the bytes and a newline: no value can be read as // another's end or start, so two different sequences of values never encode the same. func canonical(b *strings.Builder, v string) { b.WriteString(strconv.Itoa(len(v))) b.WriteByte(':') b.WriteString(v) b.WriteByte('\n') }