Name the enrolment an address is reserved for, from its ask, and the issued event's id in one place

ADR 0589 §6: an address key gains the issue's ask and time, written when the ask is raised. The key is written at the
reservation, before the ask, so both are empty then and both set from the ask; one without the other is refused, as
is no node. EncodeEnrolmentAddress takes node, ask and issued. EnrolmentIssuedID builds the enrolment-issued event's
id, enrolment-issued.<node>.<issue time in Unix nanoseconds>, as the holder names it today. Tested as rows of
TestNodeRecord; no new test function (1438 unit tests).

(issue 774, review round 1b)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 01:41:36 +01:00
parent 2d717777b1
commit c816666f83
2 changed files with 57 additions and 13 deletions
+35 -10
View File
@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"net/netip"
"strconv"
"strings"
"time"
@@ -15,7 +16,7 @@ import (
// **The state of the seat mesh-enrolment** (novox/hq ADR 0587 §2, issue 774): the holder's bucket `enrolments`, holding
// no secret, with two kinds of key. `node.<node>`, one per open enrolment, holds an Enrolment; `address.<address>`
// reserves one private-network address for one open enrolment and holds only the node's name. Every write is a
// reserves one private-network address for one open enrolment and holds the node's name and, from the ask, the ask and the issue's time. Every write is a
// compare-and-set at the revision read. The bucket keeps per-key expiry (`key-ttl`): a key at stage live carries the
// token's expiry, at asked and redeeming none (§5). It is read by the holder and, as a named reader, by the grants part,
// which composes the user `enrol.<node>` from a key at live and the vault's hash (§4). These constants are the seat's
@@ -63,10 +64,31 @@ type Enrolment struct {
OverlayKey string `json:"overlay-key"`
}
// EnrolmentAddress is one key `address.<address>`: the node the address is reserved for, and nothing else.
// EnrolmentAddress is one key `address.<address>`: the node the address is reserved for and, from when the issue's ask
// is raised, that ask's id and the issue's time (ADR 0589 §6), so the reservation names the enrolment it holds the
// address for. The key is written when the address is reserved, before the ask: Ask and Issued are then both empty, and
// from the ask both set; one without the other is refused.
type EnrolmentAddress struct {
V int `json:"v"`
Node string `json:"node"`
V int `json:"v"`
Node string `json:"node"`
Ask string `json:"ask,omitempty"`
Issued time.Time `json:"issued,omitzero"`
}
func (a EnrolmentAddress) check() error {
if err := checkName("node", a.Node); err != nil {
return err
}
if (strings.TrimSpace(a.Ask) == "") != a.Issued.IsZero() {
return fmt.Errorf("delivery: the address reserved for %s names its ask without the issue's time, or the reverse", a.Node)
}
return nil
}
// EnrolmentIssuedID is the id of the enrolment-issued event of node's enrolment issued at issued:
// `enrolment-issued.<node>.<issue time in Unix nanoseconds>`, what enrolment-expired names (EnrolmentExpiry).
func EnrolmentIssuedID(node string, issued time.Time) string {
return catalogue.EnrolmentIssuedEvent + "." + node + "." + strconv.FormatInt(issued.UnixNano(), 10)
}
// EnrolmentNodeKey is a node's key in `enrolments`.
@@ -148,21 +170,24 @@ func DecodeEnrolment(data []byte) (Enrolment, error) {
return e, nil
}
// EncodeEnrolmentAddress is a key `address.<address>` reserving it for node.
func EncodeEnrolmentAddress(node string) ([]byte, error) {
if err := checkName("node", node); err != nil {
// EncodeEnrolmentAddress is a key `address.<address>` reserving it for node: with no ask and a zero issued when it is
// reserved, with the issue's ask and time once the ask is raised. It refuses what DecodeEnrolmentAddress refuses.
func EncodeEnrolmentAddress(node, ask string, issued time.Time) ([]byte, error) {
a := EnrolmentAddress{V: Version, Node: node, Ask: ask, Issued: issued}
if err := a.check(); err != nil {
return nil, err
}
return json.Marshal(EnrolmentAddress{V: Version, Node: node})
return json.Marshal(a)
}
// DecodeEnrolmentAddress reads a key `address.<address>`.
// DecodeEnrolmentAddress reads a key `address.<address>`; it refuses no node, and an ask without the issue's time or
// the reverse.
func DecodeEnrolmentAddress(data []byte) (EnrolmentAddress, error) {
var a EnrolmentAddress
if err := decode(data, &a); err != nil {
return EnrolmentAddress{}, err
}
if err := checkName("node", a.Node); err != nil {
if err := a.check(); err != nil {
return EnrolmentAddress{}, err
}
return a, nil
+22 -3
View File
@@ -4,6 +4,7 @@ import (
"encoding/base64"
"reflect"
"regexp"
"strconv"
"strings"
"testing"
"time"
@@ -91,10 +92,28 @@ func TestNodeRecord(t *testing.T) {
t.Errorf("enrolment round trip: %+v, %v", back, err)
}
}
if raw, err := EncodeEnrolmentAddress("lab"); err != nil {
// ADR 0589 §6: reserved before the ask with neither ask nor issue time, and from the ask with both.
if raw, err := EncodeEnrolmentAddress("lab", "", time.Time{}); err != nil {
t.Fatal(err)
} else if back, err := DecodeEnrolmentAddress(raw); err != nil || back.Node != "lab" {
t.Errorf("address round trip: %+v, %v", back, err)
} else if back, err := DecodeEnrolmentAddress(raw); err != nil || back.Node != "lab" || back.Ask != "" || strings.Contains(string(raw), "issued") {
t.Errorf("address round trip: %+v, %v (%s)", back, err, raw)
}
if raw, err := EncodeEnrolmentAddress("lab", "a1", issued); err != nil {
t.Fatal(err)
} else if back, err := DecodeEnrolmentAddress(raw); err != nil || back.Ask != "a1" || !back.Issued.Equal(issued) {
t.Errorf("address at the ask round trip: %+v, %v", back, err)
}
for name, raw := range map[string]string{
"no node": `{"v":1}`,
"an ask with no issue time": `{"v":1,"node":"lab","ask":"a1"}`,
"an issue time with no ask": `{"v":1,"node":"lab","issued":"2026-11-02T01:00:00Z"}`,
} {
if _, err := DecodeEnrolmentAddress([]byte(raw)); err == nil {
t.Errorf("an address with %s: taken", name)
}
}
if id := EnrolmentIssuedID("lab", issued); id != "enrolment-issued.lab."+strconv.FormatInt(issued.UnixNano(), 10) {
t.Errorf("the issued event's id: %q", id)
}
for name, e := range map[string]Enrolment{
"keys before redeem": {Node: "lab", Address: "10.0.0.7", Why: "w", Caller: "c", Stage: EnrolmentLive,