go/asks: an ask may carry its Details whole, for a channel that proves who answers (hq issue 383)
mesh/merge-gate head f4745447ba6d merged into trunk a0fc82fdcd7c — pass, with a note: builds mesh-sdk (+40 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head f4745447ba6d merged into trunk a0fc82fdcd7c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery superseded: a newer head of the same pull request

The review of mesh-controller #304: a settings proposal with more than three keys showed only the names
of the rest, and its Details could carry no value under the content rule, so Approve bound values the
operator never saw. WholeDetails carries every value whole for the note on a proving channel, held to
the secret rule alone, as Whole is; it is digested only when given.
This commit is contained in:
jochen
2026-10-13 16:56:22 +02:00
parent 9588871d66
commit f4745447ba
2 changed files with 34 additions and 2 deletions
+12
View File
@@ -174,6 +174,12 @@ func (a Ask) Digest() string {
canonical(&b, a.Whole) canonical(&b, a.Whole)
canonical(&b, a.Details) canonical(&b, a.Details)
} }
// WholeDetails (novox/hq issue 383, the review of mesh-controller #304) follows only when given, so every
// ask without it digests as it did before it existed.
if a.WholeDetails != "" {
canonical(&b, "whole-details")
canonical(&b, a.WholeDetails)
}
sum := sha256.Sum256([]byte(b.String())) sum := sha256.Sum256([]byte(b.String()))
return "sha256:" + hex.EncodeToString(sum[:]) return "sha256:" + hex.EncodeToString(sum[:])
} }
@@ -207,6 +213,12 @@ type Ask struct {
// (Message.Note), after what the ask's options do and when it expires — never in its body (novox/hq // (Message.Note), after what the ask's options do and when it expires — never in its body (novox/hq
// issue 639). // issue 639).
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
// WholeDetails are the Details with their exact values whole, shown in place of Details where Whole is shown
// in place of the explanation (a kind that proves who answers, given the ask's answers): every value an
// approval sets, whole, so the person reads exactly what they approve (novox/hq issue 383). Held, like
// Whole, to the secret rule alone; the router refuses the ask when a secret's shape is left in it. Empty,
// Details are shown everywhere.
WholeDetails string `json:"whole-details,omitempty"`
} }
// What an ask's explanation is held to, so it reads at a glance on a phone (novox/hq issue 639): a summary // What an ask's explanation is held to, so it reads at a glance on a phone (novox/hq issue 639): a summary
+22 -2
View File
@@ -253,8 +253,8 @@ func TestDigestsAreCanonicalOverNamedFields(t *testing.T) {
t.Error("changing Whole beside Details left the digest the same") t.Error("changing Whole beside Details left the digest the same")
} }
// A field added to Ask or Option without being added to Digest fails here. // A field added to Ask or Option without being added to Digest fails here.
if n := reflect.TypeOf(Ask{}).NumField(); n != 11 { if n := reflect.TypeOf(Ask{}).NumField(); n != 12 {
t.Errorf("Ask has %d fields, and Digest covers 11: add the new one to Digest and to this test", n) t.Errorf("Ask has %d fields, and Digest covers 12: add the new one to Digest and to this test", n)
} }
if n := reflect.TypeOf(Option{}).NumField(); n != 5 { if n := reflect.TypeOf(Option{}).NumField(); n != 5 {
t.Errorf("Option has %d fields, and Digest covers 5: add the new one to Digest and to this test", n) t.Errorf("Option has %d fields, and Digest covers 5: add the new one to Digest and to this test", n)
@@ -317,3 +317,23 @@ func TestAMessageCarriesItsNoteApart(t *testing.T) {
t.Fatalf("an empty note is encoded: %s", raw) t.Fatalf("an empty note is encoded: %s", raw)
} }
} }
// WholeDetails is digested when given, and an ask without it digests as before it existed: a router and an asker
// of different builds still agree on every such ask.
func TestWholeDetailsAreDigestedOnlyWhenGiven(t *testing.T) {
a := Ask{ID: "a", Headline: "h", Explanation: "e", Details: "d"}
before := a.Digest()
a.WholeDetails = "+ shares: media=/storage/media"
if a.Digest() == before {
t.Fatal("the whole details are not in the digest")
}
b := a
b.WholeDetails = "+ shares: media=/storage/other"
if a.Digest() == b.Digest() {
t.Fatal("two whole details digest the same")
}
a.WholeDetails = ""
if a.Digest() != before {
t.Fatal("an ask without whole details digests otherwise than before")
}
}