Issue an account whose credential is not sealed, and forget an unassigned module's accounts (issue 1945, ADR 0543)
Met a second time live on 2026-10-25 at 20:04 (note 35): mesh-build-go was
assigned to ace again, and its account rows, minted at 17:19, had outlived the
unassignment. The issue minted only when no row stood, so it minted and sealed
nothing; the node had no credential and the bus no user, and the module
crash-looped 20 times. The watch still said "the grants written".
- A bus account now says when its credential was sealed to its node (migration
0108; every account standing before is taken as sealed). An issue under the
assign act, the assignments' watch or a registration mints only an account
that is not issued, a row with no seal counting as none, under the store's
lock on the account across its look, mint and seal, and marks it sealed once
sealed. A seal that fails leaves it unsealed for the next issue.
- Unassigning a module forgets its bus accounts and their credentials on that
node, so an assignment again issues them afresh.
- Only sealed accounts count as minted for a composition and are named in a
grant, so the assigner, which now wants a node only once the grant names its
account (mesh-delivery, same branch), holds the node until it is.
- The assignment's log line says which accounts were issued and sealed, or that
they stood issued and sealed.
The process-only issue drops the atomic insert of the review of 9ff47f6a for
the lock, which also covers the seal. Tests: cases in the existing tables.
Research 072: the grants piece (memberships and credentials heard from placement).
This commit is contained in:
@@ -4,7 +4,9 @@ package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -63,48 +65,54 @@ func TestMintingAgainRotatesAndMintingIfAbsentHasOneWinner(t *testing.T) {
|
||||
t.Fatal("the previous password still verifies, so a rotation revoked nothing")
|
||||
}
|
||||
|
||||
// **Minting only if absent never rotates, and of several at once exactly one wins** (ADR 0543, the review of issue
|
||||
// 1945): the assign act and the assignments' watch may both find an account missing; the winner's password is the
|
||||
// one the store's hash verifies, so the one it seals is the one the bus accepts, and a loser seals nothing.
|
||||
if _, won, err := inv.MintBusPasswordIfAbsent(ctx, aBusUser("shop")); err != nil || won {
|
||||
t.Fatalf("a mint-if-absent over a minted account won %v (%v): it would replace a sealed password", won, err)
|
||||
// **An issue's mint is not issued until its seal is marked, and forgotten with its assignment** (ADR 0543 §2, issue
|
||||
// 1945, live on 2026-10-25 at 20:04): a row whose credential the node does not hold is not issued.
|
||||
if minted, sealed, err := inv.BusAccountIssued(ctx, "one.shop"); err != nil || !minted || !sealed {
|
||||
t.Fatalf("a mint that seals in its own act: minted %v sealed %v (%v)", minted, sealed, err)
|
||||
}
|
||||
const racers = 8
|
||||
type minted struct {
|
||||
password string
|
||||
won bool
|
||||
err error
|
||||
if _, err := inv.MintBusPasswordUnsealed(ctx, aBusUser("racer")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
results := make(chan minted, racers)
|
||||
for range racers {
|
||||
go func() {
|
||||
p, won, err := inv.MintBusPasswordIfAbsent(ctx, aBusUser("racer"))
|
||||
results <- minted{p, won, err}
|
||||
}()
|
||||
if minted, sealed, err := inv.BusAccountIssued(ctx, "one.racer"); err != nil || !minted || sealed {
|
||||
t.Fatalf("an issue's mint before its seal: minted %v sealed %v (%v)", minted, sealed, err)
|
||||
}
|
||||
var winner string
|
||||
wins := 0
|
||||
for range racers {
|
||||
r := <-results
|
||||
if r.err != nil {
|
||||
t.Fatal(r.err)
|
||||
}
|
||||
if r.won {
|
||||
wins++
|
||||
winner = r.password
|
||||
} else if r.password != "" {
|
||||
t.Fatal("a caller that lost was handed a password to seal")
|
||||
}
|
||||
if names, err := inv.BusMinted(ctx); err != nil || slices.Contains(names, "one.racer") {
|
||||
t.Fatalf("an account not sealed counts as minted: %v (%v)", names, err)
|
||||
}
|
||||
if wins != 1 {
|
||||
t.Fatalf("%d of %d concurrent mints won, want exactly one", wins, racers)
|
||||
if err := inv.MarkBusAccountSealed(ctx, "one.racer"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, _, err := inv.BusUserHash(ctx, "one.racer")
|
||||
if _, sealed, _ := inv.BusAccountIssued(ctx, "one.racer"); !sealed {
|
||||
t.Fatal("an account marked sealed is not")
|
||||
}
|
||||
// The lock on an account holds a second issuer until the first lets go.
|
||||
release, err := inv.LockBusAccount(ctx, "one.racer")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(winner)); err != nil {
|
||||
t.Fatal("the stored hash is not the winner's password: the node would be sealed a password the bus refuses")
|
||||
waiting := make(chan struct{})
|
||||
go func() {
|
||||
again, err := inv.LockBusAccount(ctx, "one.racer")
|
||||
if err == nil {
|
||||
again()
|
||||
}
|
||||
close(waiting)
|
||||
}()
|
||||
select {
|
||||
case <-waiting:
|
||||
t.Fatal("a second issuer took the lock the first held")
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
}
|
||||
release()
|
||||
<-waiting
|
||||
if err := inv.ForgetBusAccountsOf(ctx, "one", "racer"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if minted, _, _ := inv.BusAccountIssued(ctx, "one.racer"); minted {
|
||||
t.Fatal("an unassigned module's account was kept")
|
||||
}
|
||||
if minted, _, _ := inv.BusAccountIssued(ctx, "one.shop"); !minted {
|
||||
t.Fatal("forgetting one module's accounts forgot another's")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user