From 6cbdb3712e27b049ada944d2158d1a9b984e13ea Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 10:33:25 +0100 Subject: [PATCH] =?UTF-8?q?Judgements=20and=20refused=20setuid=20searches,?= =?UTF-8?q?=20which=20any=20caller=20may=20ask,=20roll=20in=20one=20bounde?= =?UTF-8?q?d=20key=20of=20the=20node's=20and=20add=20no=20key=20to=20the?= =?UTF-8?q?=20bucket=20every=20node=20shares;=20a=20key=20per=20act=20is?= =?UTF-8?q?=20kept=20for=20hand-overs,=20access=20acts=20and=20searches=20?= =?UTF-8?q?started=20(issue=20774,=20ADR=200563=20=C2=A72;=20rev-l3=20roun?= =?UTF-8?q?d=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/link/handacts.go | 64 ++++++++++++++++++++++++++++- internal/link/setuid_search_test.go | 29 +++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/internal/link/handacts.go b/internal/link/handacts.go index fb736b1..a411516 100644 --- a/internal/link/handacts.go +++ b/internal/link/handacts.go @@ -242,6 +242,54 @@ func (h *HandActs) readOne(key string) (*HandAct, uint64, error) { return &a, e.Revision, nil } +// judgedKey keeps the node's judgements and refused searches, the last keptJudged of them: any caller may ask them, so +// they roll in one key and never add one (ADR 0563 §2: a wrong caller gains nothing, and fills no bucket). +const ( + judgedKey = "judged" + keptJudged = 50 +) + +// roll adds a to the rolling key by compare-and-set, the oldest falling out. +func (h *HandActs) roll(a HandAct) error { + for range 5 { + e, err := h.State.Get(judgedKey) + if err != nil { + return err + } + var kept []HandAct + if e != nil { + if err := json.Unmarshal(e.Value, &kept); err != nil { + kept = nil // an unreadable roll is started again, never a reason to refuse + } + } + kept = append(kept, a) + if len(kept) > keptJudged { + kept = kept[len(kept)-keptJudged:] + } + if e == nil { + _, err = h.State.Create(judgedKey, kept) + } else { + _, err = h.State.Update(judgedKey, kept, e.Revision) + } + if !errors.Is(err, stdio.ErrChanged) { + return err + } + } + return errors.New("the engine's judgements kept changing under its write") +} + +func (h *HandActs) rolled() ([]HandAct, error) { + e, err := h.State.Get(judgedKey) + if err != nil || e == nil { + return nil, err + } + var kept []HandAct + if err := json.Unmarshal(e.Value, &kept); err != nil { + return nil, fmt.Errorf("the engine's judgements cannot be read: %w", err) + } + return kept, nil +} + func (h *HandActs) record(a HandAct) error { _, err := h.State.Create(actKey(a.ID), a) return err @@ -286,6 +334,12 @@ func (h *HandActs) Acts(limit int) (any, error) { if err != nil { return nil, err } + judged, err := h.rolled() + if err != nil { + return nil, err + } + acts = append(acts, judged...) + sort.SliceStable(acts, func(i, j int) bool { return acts[i].At.Before(acts[j].At) }) out := make([]HandAct, 0, len(acts)) for i := len(acts) - 1; i >= 0; i-- { out = append(out, acts[i]) @@ -308,7 +362,7 @@ func (h *HandActs) Judge(caller string) (any, error) { if err != nil { a.State, a.Said = HandRefused, err.Error() } - if rerr := h.record(a); rerr != nil && err == nil { + if rerr := h.roll(a); rerr != nil && err == nil { return nil, rerr } if err != nil { @@ -339,7 +393,13 @@ func (h *HandActs) Search(caller, why string) (any, error) { if err != nil { a.State, a.Said = HandRefused, err.Error() } - if rerr := h.record(a); rerr != nil { + // A search started is bounded to one an hour and kept as an act of its own; a refused one, which any caller may + // cause, rolls with the judgements. + record := h.record + if err != nil { + record = h.roll + } + if rerr := record(a); rerr != nil { return nil, rerr } if err != nil { diff --git a/internal/link/setuid_search_test.go b/internal/link/setuid_search_test.go index 3ff0354..8b333e7 100644 --- a/internal/link/setuid_search_test.go +++ b/internal/link/setuid_search_test.go @@ -105,5 +105,34 @@ func TestASetuidSearchAskIsBelievedOnlySignedForThisNodeAndOnce(t *testing.T) { if _, err := h.Call(EngineCall{Verb: ActSetuidSearch, Caller: "ace.node-tools"}); err == nil || !strings.Contains(err.Error(), "the next may start") || started != 2 { t.Errorf("a search with the acts gone: %v, %d started", err, started) } + // rev-l3 round 2 (ADR 0563 §2, a wrong caller gains nothing): judgements and refused searches, which any caller + // may ask, keep one rolling key of the node's, so ten thousand of them write a bounded number of bytes and add no + // key to the bucket every node shares. + keysNow := func() (int, int) { + all, _ := h.State.Keys("") + size := 0 + for _, k := range all { + if e, _ := h.State.Get(k); e != nil { + size += len(e.Value) + } + } + return len(all), size + } + _, _ = h.Call(EngineCall{Verb: ActJudgeDirectories, Caller: "unproven"}) + keysBefore, _ := keysNow() + for i := range 10000 { + verb := ActJudgeDirectories + if i%10 == 0 { + verb = ActSetuidSearch + } + _, _ = h.Call(EngineCall{Verb: verb, Caller: "unproven"}) + } + if n, size := keysNow(); n != keysBefore || size > 64<<10 { + t.Errorf("ten thousand judgements and refused searches left %d keys (was %d) of %d bytes", n, keysBefore, size) + } + listed, _ := h.Acts(0) + if got := listed.(map[string]any)["acts"].([]HandAct); len(got) == 0 || got[0].Caller != "unproven" { + t.Errorf("acts does not show the latest judgement first: %d", len(got)) + } }) }