diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 2ea008b..db1ba96 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -1302,6 +1302,21 @@ func TestGrants(t *testing.T) { if e := effect(); !strings.HasPrefix(e, "switched") { t.Errorf("effect once the bus node loaded the written list = %s", e) } + // Steady (issue 1126, 2026-10-28 00:16: the shadow list moved every 3 s, 99270 to 99357, with nothing + // differing): the bus node saying again that it loaded the same list, at a later time and the key's + // later revision, moves neither the shadow list nor the sealed key. + _, sealedRev, _ := p.Sealed() + p.Compose() + for i := 0; i < 3; i++ { + withKey(delivery.UserListWritten{Source: delivery.GrantsUserListSource, Revision: sealedRev + uint64(i+1), Wanted: g.Digest, + Digest: g.Digest, Users: g.Users, Follows: delivery.FollowGrants, At: time.Date(2026, 10, 28, 0, 16, i, 0, time.UTC)}) + if res := p.Compose(); res.Written { + t.Errorf("pass %d: the shadow list is written again for the bus node saying the same list again (revision %d)", i, res.Revision) + } + if _, rev, _ := p.Sealed(); rev != sealedRev { + t.Errorf("pass %d: the sealed key is written again with nothing changed: revision %d, was %d", i, rev, sealedRev) + } + } withKey(loadedBefore) // After take-over a list composed past a failed read, or one dropping a user of the list in force for // want of a hash, is not sealed: the list in force stays, and the pass says why. diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index 16d177f..9692077 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -77,6 +77,13 @@ type UserListState struct { func (s UserListState) inputs() string { s.At, s.Inputs = time.Time{}, "" + // When the bus node said it loaded a list is no input: the same list said again, at a later time and the key's later + // revision, composes nothing new (issue 1126: written again every 3 s, 2026-10-28). + if s.Loaded != nil { + l := *s.Loaded + l.Revision, l.At = 0, time.Time{} + s.Loaded = &l + } raw, _ := json.Marshal(s) sum := sha256.Sum256(raw) return "sha256:" + hex.EncodeToString(sum[:]) diff --git a/modules/mesh-grants/internal/grants/sealed.go b/modules/mesh-grants/internal/grants/sealed.go index c8d8eb8..eb76a44 100644 --- a/modules/mesh-grants/internal/grants/sealed.go +++ b/modules/mesh-grants/internal/grants/sealed.go @@ -177,7 +177,16 @@ func (p *Part) sayHeld(why string) { const HeldCondition = "mesh-grants.user-list-held" func sameSealed(a, b delivery.GrantsUserList) bool { + // Its time is no change, and no user left out is the same written as [] or null: the key read back says [] for the + // nil a pass composes, which read as a change wrote the key again at every pass, and each write moved the bus node's + // report and woke the next pass (issue 1126: the shadow list's revision 99270 to 99357 in 90 s, 2026-10-28). a.At, b.At = b.At, a.At + if len(a.LeftOut) == 0 { + a.LeftOut = nil + } + if len(b.LeftOut) == 0 { + b.LeftOut = nil + } x, _ := json.Marshal(a) y, _ := json.Marshal(b) return string(x) == string(y)