253 lines
7.9 KiB
Go
253 lines
7.9 KiB
Go
package main
|
|
|
|
// A wanted account no longer wanted is retired within ADR 0230's bounds (novox/hq ADR 0561 §2, issue 774).
|
|
//
|
|
// The grants part deletes a wanted account's key when no input names its user any more. The vault then retires the
|
|
// account: it takes its hash out of `bus-accounts`, so the grants part's next list drops the user, and marks the value
|
|
// retired in its store, keeping it (a value is deleted only by a person, ADR 0230). As ADR 0230 §15 and §18 have it, the
|
|
// same set of accounts gone must stand for five passes and ten minutes first, and a set of more than three, or more than
|
|
// half of the accounts held, waits for a person, who retires them with `account_retire`. Wanted again, an account is
|
|
// re-enabled at once: the mark taken off and its hash written again. Nothing is retired while the controller holds the
|
|
// lease (it forgets the accounts it mints), or before the wanted accounts are read whole.
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// retiring is what the passes keep of the accounts no longer wanted, under minting.mu.
|
|
type retiring struct {
|
|
wantsRead bool
|
|
key string
|
|
passes int
|
|
first time.Time
|
|
waiting []string
|
|
why string
|
|
retired []string
|
|
}
|
|
|
|
// RetireState is what vault_status says of retirement.
|
|
type RetireState struct {
|
|
Gone []string `json:"gone"`
|
|
Passes int `json:"passes"`
|
|
Since time.Time `json:"since,omitzero"`
|
|
Waiting []string `json:"waiting_for_a_person"`
|
|
Why string `json:"why"`
|
|
Retired []string `json:"retired"`
|
|
}
|
|
|
|
// RetireState answers how retirement stands.
|
|
func (v *Vault) RetireState() RetireState {
|
|
m := &v.minting
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
r := m.retiring
|
|
out := RetireState{Passes: r.passes, Since: r.first, Why: r.why, Waiting: append([]string{}, r.waiting...),
|
|
Retired: append([]string{}, r.retired...), Gone: []string{}}
|
|
if r.key != "" {
|
|
out.Gone = strings.Split(r.key, " ")
|
|
}
|
|
return out
|
|
}
|
|
|
|
// WantsRead says the wanted accounts were read whole once (the watch handed every current key): only then is an account
|
|
// with no key known to be no longer wanted.
|
|
func (v *Vault) WantsRead() {
|
|
v.minting.mu.Lock()
|
|
defer v.minting.mu.Unlock()
|
|
v.minting.retiring.wantsRead = true
|
|
}
|
|
|
|
// wantGone is a wanted account's key deleted: forgotten as wanted, its retirement left to the passes.
|
|
func (v *Vault) wantGone(user string) {
|
|
v.minting.mu.Lock()
|
|
defer v.minting.mu.Unlock()
|
|
delete(v.minting.wants, user)
|
|
delete(v.minting.heldBack, user)
|
|
}
|
|
|
|
// wantBack is a wanted account's key written: a retired account of it is re-enabled at once.
|
|
func (v *Vault) wantBack(user string) {
|
|
w, ok := v.wanted(user)
|
|
if !ok {
|
|
return
|
|
}
|
|
secret, err := AccountSecret(w)
|
|
if err != nil {
|
|
return
|
|
}
|
|
rec, held, err := v.Store.Read(secret)
|
|
if err != nil || !held || rec.Retired == nil {
|
|
return
|
|
}
|
|
rec.Retired = nil
|
|
if err := v.Store.Write(rec); err != nil {
|
|
v.log("%s is wanted again and NOT RE-ENABLED: its mark could not be taken off: %v", user, err)
|
|
return
|
|
}
|
|
if _, err := v.HashAccount(secret, true); err != nil {
|
|
v.hashes.owe(secret, err.Error())
|
|
v.log("%s is wanted again and its hash is not written yet, written at the next pass: %v", user, err)
|
|
}
|
|
v.minting.mu.Lock()
|
|
r := &v.minting.retiring
|
|
for i, u := range r.retired {
|
|
if u == user {
|
|
r.retired = append(r.retired[:i], r.retired[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
v.minting.mu.Unlock()
|
|
v.log("%s is wanted again: re-enabled, its hash written again", user)
|
|
}
|
|
|
|
// goneAccounts are the users whose hash the vault wrote from a value it holds and that no wanted key names, sorted, and
|
|
// how many such hashes it holds in all.
|
|
func (v *Vault) goneAccounts() ([]string, int, error) {
|
|
users, err := v.Accounts.Users()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
var gone []string
|
|
held := 0
|
|
for _, u := range users {
|
|
a, err := v.Accounts.Get(u)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if a == nil || a.Adopted || a.Of == "" {
|
|
continue
|
|
}
|
|
held++
|
|
if _, wanted := v.wanted(u); !wanted {
|
|
gone = append(gone, u)
|
|
}
|
|
}
|
|
sort.Strings(gone)
|
|
return gone, held, nil
|
|
}
|
|
|
|
// RetirePass is one pass of retirement, at the vault's minute pass.
|
|
func (v *Vault) RetirePass() {
|
|
if v.Accounts == nil {
|
|
return
|
|
}
|
|
v.leaseAfresh()
|
|
on, leaseWhy := v.mintingOn()
|
|
m := &v.minting
|
|
m.mu.Lock()
|
|
read := m.retiring.wantsRead
|
|
m.mu.Unlock()
|
|
reset := func(why string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.retiring.key, m.retiring.passes, m.retiring.first, m.retiring.waiting, m.retiring.why = "", 0, time.Time{}, nil, why
|
|
}
|
|
switch {
|
|
case !read:
|
|
reset("the wanted accounts are not read whole yet, so none is known to be gone")
|
|
return
|
|
case !on:
|
|
reset("nothing is retired: " + leaseWhy)
|
|
return
|
|
}
|
|
gone, held, err := v.goneAccounts()
|
|
if err != nil {
|
|
reset("the accounts held could not be read: " + err.Error())
|
|
return
|
|
}
|
|
if len(gone) == 0 {
|
|
reset("")
|
|
return
|
|
}
|
|
now := v.Now().UTC()
|
|
key := strings.Join(gone, " ")
|
|
m.mu.Lock()
|
|
r := &m.retiring
|
|
if r.key != key {
|
|
r.key, r.passes, r.first, r.waiting = key, 0, now, nil
|
|
}
|
|
r.passes++
|
|
passes, since := r.passes, r.first
|
|
m.mu.Unlock()
|
|
setWhy := func(why string, waiting []string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.retiring.why, m.retiring.waiting = why, waiting
|
|
}
|
|
if passes < StablePasses || now.Sub(since) < StableFor {
|
|
setWhy(fmt.Sprintf("%d no longer wanted (%s): retired once the same set stands for %d passes and %s (ADR 0230)",
|
|
len(gone), key, StablePasses, StableFor), nil)
|
|
return
|
|
}
|
|
if len(gone) > RetireAtOnce || float64(len(gone)) > RetireFraction*float64(held) {
|
|
why := fmt.Sprintf("%d accounts are no longer wanted, more than %d or more than half of the %d held at once: they "+
|
|
"wait for a person (account_retire), ADR 0230 §18", len(gone), RetireAtOnce, held)
|
|
setWhy(why, gone)
|
|
v.log("%s: %s", why, key)
|
|
return
|
|
}
|
|
for _, u := range gone {
|
|
if err := v.retireOne(u, "no longer wanted by the grants part", "the vault's pass"); err != nil {
|
|
v.log("%s is not retired: %v", u, err)
|
|
}
|
|
}
|
|
reset("")
|
|
}
|
|
|
|
// retireOne marks the value of user's account retired and takes its hash out of bus-accounts.
|
|
func (v *Vault) retireOne(user, why, by string) error {
|
|
a, err := v.Accounts.Get(user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if a == nil || a.Adopted || a.Of == "" {
|
|
return fmt.Errorf("the vault holds no account of %s it made or was handed: an adopted hash is never retired here", user)
|
|
}
|
|
rec, held, err := v.Store.Read(a.Of)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if held {
|
|
rec.Retired = &RetiredMark{At: v.Now().UTC().Format(time.RFC3339), Why: why + ", by " + by}
|
|
if err := v.Store.Write(rec); err != nil {
|
|
return fmt.Errorf("its value could not be marked retired: %w", err)
|
|
}
|
|
}
|
|
if err := v.Accounts.Delete(user); err != nil {
|
|
return fmt.Errorf("its hash could not be taken out of %s: %w", BusAccountsState, err)
|
|
}
|
|
v.minting.mu.Lock()
|
|
v.minting.retiring.retired = append(v.minting.retiring.retired, user)
|
|
v.minting.mu.Unlock()
|
|
v.log("retired the bus account %s (%s): its hash is out of %s and its value kept, marked; %s", user, a.Of,
|
|
BusAccountsState, why)
|
|
return nil
|
|
}
|
|
|
|
// RetireAccounts is the tool account_retire: a person retires accounts no longer wanted, past ADR 0230's bound, with why.
|
|
func (v *Vault) RetireAccounts(caller string, users []string, why string) (any, error) {
|
|
if strings.TrimSpace(why) == "" {
|
|
return nil, errors.New("account_retire says why (`why`)")
|
|
}
|
|
if len(users) == 0 {
|
|
return nil, errors.New("account_retire names the accounts (`users`), as vault_status lists them waiting")
|
|
}
|
|
for _, u := range users {
|
|
if _, wanted := v.wanted(u); wanted {
|
|
return nil, fmt.Errorf("%s is still wanted by the grants part: only an account no longer wanted is retired; nothing was retired", u)
|
|
}
|
|
}
|
|
var done []string
|
|
for _, u := range users {
|
|
if err := v.retireOne(u, strings.TrimSpace(why), someoneOr("", caller)); err != nil {
|
|
return map[string]any{"retired": done}, fmt.Errorf("%s: %w", u, err)
|
|
}
|
|
done = append(done, u)
|
|
}
|
|
return map[string]any{"retired": done}, nil
|
|
}
|