From f4745447ba6d0c4fd7dc6b1071e26ce012806233 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 13 Oct 2026 16:56:22 +0200 Subject: [PATCH] go/asks: an ask may carry its Details whole, for a channel that proves who answers (hq issue 383) 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. --- go/asks/asks.go | 12 ++++++++++++ go/asks/asks_test.go | 24 ++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/go/asks/asks.go b/go/asks/asks.go index 0f6f3aa..8915af8 100644 --- a/go/asks/asks.go +++ b/go/asks/asks.go @@ -174,6 +174,12 @@ func (a Ask) Digest() string { canonical(&b, a.Whole) 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())) 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 // issue 639). 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 diff --git a/go/asks/asks_test.go b/go/asks/asks_test.go index c64d49c..3ce66d6 100644 --- a/go/asks/asks_test.go +++ b/go/asks/asks_test.go @@ -253,8 +253,8 @@ func TestDigestsAreCanonicalOverNamedFields(t *testing.T) { 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. - if n := reflect.TypeOf(Ask{}).NumField(); n != 11 { - t.Errorf("Ask has %d fields, and Digest covers 11: add the new one to Digest and to this test", n) + if n := reflect.TypeOf(Ask{}).NumField(); n != 12 { + 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 { 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) } } + +// 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") + } +}