Name the placement ask and a proposal's again and warrant, so the holder asks through the teller as its other asks (ADR 0567 §5, §10, issue 1126)

This commit is contained in:
2026-10-28 18:20:27 +01:00
parent 5a2a7eebfd
commit 0282229fd9
3 changed files with 41 additions and 11 deletions
+8 -4
View File
@@ -361,6 +361,9 @@ type PlacementProposal struct {
Ask string `json:"ask,omitempty"`
// Words is what happened last, in words (laid again over moved keys, asked again).
Words string `json:"words,omitempty"`
// Again is when `again` asked the holder to judge it now and raise its ask again (§10); the holder does once per
// value.
Again time.Time `json:"again,omitempty"`
}
// Nodes is every node whose key the proposal changes, sorted.
@@ -447,11 +450,12 @@ func ChangesDigest(p PlacementProposal) string {
return "sha256:" + hex.EncodeToString(sum[:])
}
// PlacementWarrant is who approved a proposal, through which channel and when (§5, §6), and the ask it answered.
// PlacementWarrant is who approved a proposal, through which channel and when (§5, §6): the warrant's id as the teller
// wrote it (the ask's id and when), and its words, which name the person and the channel that proved them
// (asks.Warrant.Says).
type PlacementWarrant struct {
Ask string `json:"ask"`
Who string `json:"who"`
Channel string `json:"channel"`
Warrant string `json:"warrant"`
Words string `json:"words"`
At time.Time `json:"at"`
}
+22 -4
View File
@@ -38,14 +38,18 @@ const (
// AskKindRecord is the kind of the assigner's ask about a move a person must allow, for a module of policy record:
// a composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14).
AskKindRecord = "record"
// AskKindPlacement is the kind of the seat mesh-placement's authorising ask about a proposal (novox/hq ADR 0567 §5):
// `placement.<proposal id>`. Its yes is an Approve that only a channel proving who answered takes, never the view.
AskKindPlacement = "placement"
viewWord = "view"
)
// AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's
// Module and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest
// Composed of the composition it would allow.
// Composed of the composition it would allow, a placement's Proposal.
type AskKey struct {
Proposal string
Kind string
Group string
Hash string
@@ -59,13 +63,13 @@ type AskKey struct {
// merge's Approve does (ADR 0421 §1, issue 953), the assigner's hold and restore do not. An unknown kind is refused.
func AskNeedsProof(kind string) (bool, error) {
switch kind {
case AskKindMerge:
case AskKindMerge, AskKindPlacement:
return true, nil
case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord:
return false, nil
}
return false, fmt.Errorf("delivery: %q is not a kind of ask, %s, %s, %s, %s or %s", kind, AskKindMerge, AskKindHold,
AskKindRestore, AskKindBackup, AskKindRecord)
return false, fmt.Errorf("delivery: %q is not a kind of ask, %s, %s, %s, %s, %s or %s", kind, AskKindMerge, AskKindHold,
AskKindRestore, AskKindBackup, AskKindRecord, AskKindPlacement)
}
// HoldAskKey is the key, in AssignerAsksBucket, of the ask about a module's held fingerprint: `hold.<module>.<hex>`.
@@ -100,6 +104,15 @@ func BackupAskKey(node, module, fingerprint string) (string, error) {
return AskKindBackup + "." + key, nil
}
// PlacementAskKey is the key, in AssignerAsksBucket, of the authorising ask about a proposal of placement:
// `placement.<proposal id>` (ADR 0567 §5).
func PlacementAskKey(proposal string) (string, error) {
if err := CheckProposalID(proposal); err != nil {
return "", err
}
return AskKindPlacement + "." + proposal, nil
}
// RecordAskKey is the key, in AssignerAsksBucket, of the ask about a move of a module on a node to the composition
// whose digest it names: `record.<node>.<module>.<digest hex>`, so a new revision at the same fingerprint is asked
// again (ADR 0539 clause 14).
@@ -154,6 +167,11 @@ func ParseAnyAskKey(key string) (AskKey, error) {
return refused(fmt.Errorf("a backup ask is backup.<node>.<module>.<fingerprint hex>"))
}
return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil
case AskKindPlacement:
if err := CheckProposalID(rest); err != nil {
return refused(err)
}
return AskKey{Kind: kind, Proposal: rest}, nil
case AskKindRecord:
parts := strings.Split(rest, ".")
if len(parts) != 3 {
+11 -3
View File
@@ -33,9 +33,17 @@ func TestParseAnyAskKey(t *testing.T) {
if err != nil || restore != "restore.ace.mesh-mail."+strings.Repeat("d", 64) {
t.Fatalf("RestoreAskKey = %q, %v", restore, err)
}
placement, err := PlacementAskKey("p-one")
if err != nil || placement != "placement.p-one" {
t.Fatalf("PlacementAskKey = %q, %v", placement, err)
}
if proof, err := AskNeedsProof(AskKindPlacement); !proof || err != nil {
t.Error("a placement ask may be answered without proof of who answered")
}
for key, want := range map[string]AskKey{
hold: {Kind: AskKindHold, Module: "mesh-mail", FP: fp},
restore: {Kind: AskKindRestore, Node: "ace", Module: "mesh-mail", FP: fp},
hold: {Kind: AskKindHold, Module: "mesh-mail", FP: fp},
restore: {Kind: AskKindRestore, Node: "ace", Module: "mesh-mail", FP: fp},
placement: {Kind: AskKindPlacement, Proposal: "p-one"},
} {
if got, err := ParseAnyAskKey(key); err != nil || got != want {
t.Errorf("ParseAnyAskKey(%q) = %+v, %v", key, got, err)
@@ -44,7 +52,7 @@ func TestParseAnyAskKey(t *testing.T) {
if got, err := ParseAnyAskKey(merge); err != nil || got.Kind != AskKindMerge || got.Group != "feat/r4g" || len(got.Hash) != 12 {
t.Errorf("ParseAnyAskKey(%q) = %+v, %v", merge, got, err)
}
for _, bad := range []string{"want.ace.mesh-mail", "hold.mesh-mail", "restore.mesh-mail." + strings.Repeat("d", 64), "approve.x.y", "restore.a.b.c.d", ""} {
for _, bad := range []string{"want.ace.mesh-mail", "hold.mesh-mail", "restore.mesh-mail." + strings.Repeat("d", 64), "approve.x.y", "restore.a.b.c.d", "placement.P", ""} {
if _, err := ParseAnyAskKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("ParseAnyAskKey(%q) = %v, want a refusal naming it", bad, err)
}