Merge pull request 'The bus node writes the grants part's signed user list alone once the lease is handed (issue 774)' (#194) from feat/l3-removal into main

This commit was merged in pull request #194.
This commit is contained in:
2026-11-02 00:35:43 +00:00
4 changed files with 49 additions and 6 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/novox/mesh-host
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101235037-c21d8fd9692c
github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
+2
View File
@@ -1,5 +1,7 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4 h1:xeuMBwOHNKrVUdGVg7Bdv5tkdueBvngwJFyk6n10rp0= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4 h1:xeuMBwOHNKrVUdGVg7Bdv5tkdueBvngwJFyk6n10rp0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101235037-c21d8fd9692c h1:/YCxh+NYfqry3PxZYY1nQXaccupobxpsFt8c9BYuGBg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101235037-c21d8fd9692c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+23 -4
View File
@@ -27,6 +27,11 @@
// against the key's digest, and written to the same file (mode 0600) only when the file's digest differs; otherwise the // against the key's digest, and written to the same file (mode 0600) only when the file's digest differs; otherwise the
// controller's compositions are the source, as before. Never both. The have says which it follows and this node's key, // controller's compositions are the source, as before. Never both. The have says which it follows and this node's key,
// which the grants part seals to. // which the grants part seals to.
//
// **From the lease write it never follows the controller's list again** (ADR 0571 clause 8, ADR 0547 clause 11). The
// grants part's key then says the lease is handed (delivery.GrantsUserList.LeaseHanded): a key that later cannot be read or
// is deleted is said, the key last heard stands, and the list loaded stays; no list is written from a composition of the
// controller.
package userlist package userlist
import ( import (
@@ -250,23 +255,36 @@ func (f *Follower) busNode() bool {
} }
// takeGrants keeps the grants part's key as heard; a value it cannot read is said and leaves the controller's list // takeGrants keeps the grants part's key as heard; a value it cannot read is said and leaves the controller's list
// followed (no list is followed that the node cannot check). // followed (no list is followed that the node cannot check), unless the key last heard says the lease is handed: then
// the controller's list is never followed again (novox/hq ADR 0571 clause 8, ADR 0547 clause 11), the key as last heard
// stands, and the list loaded stays.
func (f *Follower) takeGrants(u agent.Update) { func (f *Follower) takeGrants(u agent.Update) {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
f.grantsRev = u.Revision handed := f.grants != nil && f.grants.LeaseHanded
if u.Deleted { if u.Deleted {
if handed {
f.sayOnceLocked("the grants part's user-list key is deleted, and the lease is handed, so the list loaded is kept")
return
}
f.grantsRev = u.Revision
f.grants = nil f.grants = nil
f.keep(nil, 0) f.keep(nil, 0)
return return
} }
g, err := delivery.DecodeGrantsUserList(u.Value) g, err := delivery.DecodeGrantsUserList(u.Value)
if err != nil { if err != nil {
if handed {
f.sayOnceLocked("the grants part's user-list key cannot be read, and the lease is handed, so the list loaded is kept: " + err.Error())
return
}
f.grantsRev = u.Revision
f.grants = nil f.grants = nil
f.keep(nil, 0) f.keep(nil, 0)
f.sayOnceLocked("the grants part's user-list key cannot be read, so the controller's list is followed: " + err.Error()) f.sayOnceLocked("the grants part's user-list key cannot be read, so the controller's list is followed: " + err.Error())
return return
} }
f.grantsRev = u.Revision
f.grants = &g f.grants = &g
f.keep(u.Value, u.Revision) f.keep(u.Value, u.Revision)
} }
@@ -278,7 +296,8 @@ func (f *Follower) keep(value []byte, rev uint64) {
} }
} }
// Following is which list the follower takes now: the grants part's while its key says so, the controller's otherwise. // Following is which list the follower takes now: the grants part's while its key says so or says the lease is handed, the
// controller's otherwise.
func (f *Follower) Following() string { func (f *Follower) Following() string {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
@@ -286,7 +305,7 @@ func (f *Follower) Following() string {
} }
func (f *Follower) followingLocked() string { func (f *Follower) followingLocked() string {
if f.grants != nil && f.grants.Follow == delivery.FollowGrants { if f.grants != nil && (f.grants.Follow == delivery.FollowGrants || f.grants.LeaseHanded) {
return delivery.FollowGrants return delivery.FollowGrants
} }
return delivery.FollowController return delivery.FollowController
+23 -1
View File
@@ -132,6 +132,9 @@ type gput struct {
otherKey, badDigest bool otherKey, badDigest bool
// after puts it once the grants part's list was written, as a hand-back comes after a take-over. // after puts it once the grants part's list was written, as a hand-back comes after a take-over.
after bool after bool
// handed says the controller's lease is handed (ADR 0582 clause 3); unreadable and deleted make the key a value that
// does not read, or a deleted key, instead of a list (774/S31).
handed, unreadable, deleted bool
} }
type put struct { type put struct {
@@ -226,6 +229,20 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
{name: "in shadow the grants part's list is not taken", {name: "in shadow the grants part's list is not taken",
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowController, content: three}}, before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowController, content: three}},
wantWrites: []string{two}, wantUsers: 3, wantSource: "novox.nats." + fpOf("a")[len(delivery.SourcePrefix2):], wantFollows: delivery.FollowController}, wantWrites: []string{two}, wantUsers: 3, wantSource: "novox.nats." + fpOf("a")[len(delivery.SourcePrefix2):], wantFollows: delivery.FollowController},
// 774/S31 (ADR 0571 clause 8, ADR 0547 clause 11): once the key says the lease is handed, the controller's list is never
// followed again, whatever becomes of the key; without the flag the controller's list is still the fallback.
{name: "a handed lease: a grants key that turns unreadable keeps the loaded list and never writes the controller's",
before: []put{{module: "nats", fp: fpOf("a"), content: two}},
grants: []gput{{follow: delivery.FollowGrants, content: three, handed: true}, {unreadable: true, after: true}},
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
{name: "a handed lease: a grants key that is deleted keeps the loaded list and never writes the controller's",
before: []put{{module: "nats", fp: fpOf("a"), content: two}},
grants: []gput{{follow: delivery.FollowGrants, content: three, handed: true}, {deleted: true, after: true}},
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
{name: "no handed lease: a grants key that turns unreadable falls back to the controller's list",
before: []put{{module: "nats", fp: fpOf("a"), content: two}},
grants: []gput{{follow: delivery.FollowGrants, content: three}, {unreadable: true, after: true}},
wantWrites: []string{three, two}, wantUsers: 3, wantSource: "novox.nats." + fpOf("a")[len(delivery.SourcePrefix2):], wantFollows: delivery.FollowController},
{name: "a grants list that does not open to its digest is refused and the file kept", {name: "a grants list that does not open to its digest is refused and the file kept",
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three, badDigest: true}}, before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three, badDigest: true}},
onDisk: two, wantErr: "could not be had", wantRefused: true, wantSource: delivery.GrantsUserListSource}, onDisk: two, wantErr: "could not be had", wantRefused: true, wantSource: delivery.GrantsUserListSource},
@@ -256,6 +273,11 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
gs = append(append([]gput(nil), gs...), *c.kept) gs = append(append([]gput(nil), gs...), *c.kept)
} }
for i, g := range gs { for i, g := range gs {
if g.unreadable || g.deleted {
u := agent.Update{Key: delivery.GrantsUserListKey, Value: []byte("not a user-list key"), Revision: uint64(i + 1), Deleted: g.deleted}
grantsAfter = append(grantsAfter, u)
continue
}
to := nodeKey.PublicKey to := nodeKey.PublicKey
if g.otherKey { if g.otherKey {
other, _ := seal.GenerateKeyPair() other, _ := seal.GenerateKeyPair()
@@ -271,7 +293,7 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
} }
sealed[delivery.UserListObjectName(digest)] = object sealed[delivery.UserListObjectName(digest)] = object
raw, err := delivery.EncodeGrantsUserList(delivery.GrantsUserList{Follow: g.follow, Digest: digest, raw, err := delivery.EncodeGrantsUserList(delivery.GrantsUserList{Follow: g.follow, Digest: digest,
Object: delivery.UserListObjectName(digest), SealedTo: delivery.SealingKeyID(to), Node: node, Revision: uint64(i + 1)}) Object: delivery.UserListObjectName(digest), SealedTo: delivery.SealingKeyID(to), Node: node, Revision: uint64(i + 1), LeaseHanded: g.handed})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }