Files
mesh-sdk/go/delivery/node_logins.go
T
jschoubben 69270a30d5 Add the vault's rotation verbs and the state node-logins, so a node's account rotates over two logins on one contract
ADR 0583 §1 (insight 2026-11-01) keeps which login a node uses in the vault's state node-logins, one key per node:
the login in use, the one rotating and the stage (begun, ready, confirmed), with the list revision that held both.
§2, §3 and §5 give the vault's seat rotate-node (with the recorded hand act's id), node-account-confirm (the caller
is the proof) and abandon, optional until the vault serves them; the events rotation-begun, node-account-ready,
rotation-done and the engine's finding node-account-fallback are named beside them. The codec refuses a stage without
a rotation and a rotation to anything but the node's other login; an empty rotating reads as none. Tested as rows of
TestNodeRecord and TestTheVaultsSeatPromisesItsIntakeVerbs, no new test function.

(issue 774, 774/S38)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:41:03 +01:00

131 lines
5.4 KiB
Go

package delivery
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// **A node's two logins** (novox/hq ADR 0583, issue 774). A node's bus account rotates between the login `node.<n>` and
// `node.<n>--b`, each with the node's permissions and its inbox `_INBOX.node.<n>` unchanged (§1). Which one a node uses
// is the vault's state `node-logins`, one key per node (§1, insight 2026-11-01), written by the vault alone: the login
// in use, the login rotating if any, and the rotation's stage. Each node-engine reads its own node's key alone; the
// grants part reads them all and composes both logins while a node rotates (§2). The key lives while the node does:
// its rotating part is cleared at rotation-done or abandon, and the key deleted when the node leaves placement.
const (
NodeLoginsState = "node-logins"
NodeLoginsBucket = VaultModule + "_" + NodeLoginsState
NodeLoginsRead = VaultModule + "." + NodeLoginsState
// The stages of a rotation (§2-§4): begun when the vault made the other login's value and wrote it rotating; ready
// when the bus node's have shows a list holding both, said node-account-ready; confirmed when the engine called
// node-account-confirm as the new login. Done (rotation-done) is no stage: the rotating part is cleared.
RotationBegun = "begun"
RotationReady = "ready"
RotationConfirmed = "confirmed"
// The vault's rotation verbs on its seat (§2, §3, §5).
RotateNodeVerb = "rotate-node"
NodeAccountConfirmVerb = "node-account-confirm"
AbandonVerb = "abandon"
// The vault's events of a rotation, said as its own (§2, §4), each carrying the node's NodeLogin.
RotationBegunEvent = "rotation-begun"
NodeAccountReadyEvent = "node-account-ready"
RotationDoneEvent = "rotation-done"
// NodeAccountFallbackFinding is what a node-engine says when it fell back to its old login after repeated
// authentication failures with the new one, before rotation-done; the watchman raises it (§6).
NodeAccountFallbackFinding = "node-account-fallback"
nodeLoginPrefix = "node."
// SecondLoginSuffix marks a node's second login, as a module's account rotates over `--b` (ADR 0114).
SecondLoginSuffix = "--b"
)
// RotationEventSubject is where the vault says event of a rotation.
func RotationEventSubject(event string) string { return "mesh.mod." + VaultModule + ".event." + event }
// NodeLogin is one key of `node-logins`, the node's name: the login in use, the login rotating to (empty when none, and
// then no stage), the rotation's stage, since when the key says what it says, and the user list's revision that held
// both logins, from ready on.
type NodeLogin struct {
V int `json:"v"`
Node string `json:"node"`
Login string `json:"login"`
Rotating string `json:"rotating,omitempty"`
Stage string `json:"stage,omitempty"`
Since time.Time `json:"since"`
ListRevision uint64 `json:"list-revision,omitempty"`
}
// FirstLogin is a node's first login, `node.<n>`, the one every node had before rotation; SecondLogin its other.
func FirstLogin(node string) string { return nodeLoginPrefix + node }
func SecondLogin(node string) string { return nodeLoginPrefix + node + SecondLoginSuffix }
// OtherLogin is the login a node rotates to from login: the second from the first, the first from the second.
func OtherLogin(node, login string) string {
if login == SecondLogin(node) {
return FirstLogin(node)
}
return SecondLogin(node)
}
// LoginNode is the node a node login names, either of its two, and whether login is one.
func LoginNode(login string) (string, bool) {
rest, ok := strings.CutPrefix(login, nodeLoginPrefix)
rest = strings.TrimSuffix(rest, SecondLoginSuffix)
return rest, ok && checkName("node", rest) == nil
}
// Rotates says the node is rotating: a login rotating is named.
func (l NodeLogin) Rotates() bool { return l.Rotating != "" }
func (l NodeLogin) check() error {
if err := checkName("node", l.Node); err != nil {
return err
}
if l.Login != FirstLogin(l.Node) && l.Login != SecondLogin(l.Node) {
return fmt.Errorf("delivery: the node %s logs in as %q, neither of its two logins", l.Node, l.Login)
}
if !l.Rotates() {
if l.Stage != "" || l.ListRevision != 0 {
return fmt.Errorf("delivery: the node %s rotates to nothing and names stage %q", l.Node, l.Stage)
}
return nil
}
if l.Rotating != OtherLogin(l.Node, l.Login) {
return fmt.Errorf("delivery: the node %s rotates from %s to %q, not its other login", l.Node, l.Login, l.Rotating)
}
switch l.Stage {
case RotationBegun, RotationReady, RotationConfirmed:
default:
return fmt.Errorf("delivery: the node %s's rotation is at stage %q, not begun, ready or confirmed", l.Node, l.Stage)
}
return nil
}
// EncodeNodeLogin is l as JSON at Version; it refuses what DecodeNodeLogin refuses.
func EncodeNodeLogin(l NodeLogin) ([]byte, error) {
if err := l.check(); err != nil {
return nil, err
}
l.V = Version
return json.Marshal(l)
}
// DecodeNodeLogin reads a key of `node-logins`; it refuses an unknown version (ErrUnknownVersion), a login that is not
// one of the node's two, a rotation to anything but the other, and a stage without a rotation or an unknown one. An
// empty rotating reads as none.
func DecodeNodeLogin(data []byte) (NodeLogin, error) {
var l NodeLogin
if err := decode(data, &l); err != nil {
return NodeLogin{}, err
}
if err := l.check(); err != nil {
return NodeLogin{}, err
}
return l, nil
}