@serfab/cadre-core 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cadre-node.d.ts +0 -27
- package/dist/cadre-node.js +2 -94
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-schema.js +1066 -1066
- package/dist/diagnostics/connection-path.d.ts +1 -18
- package/dist/diagnostics/connection-path.js +1 -20
- package/dist/diagnostics/connection-path.js.map +1 -1
- package/dist/identity-key.d.ts +0 -30
- package/dist/identity-key.js +7 -40
- package/dist/identity-key.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/relay-stun.d.ts +24 -0
- package/dist/relay-stun.js +51 -0
- package/dist/relay-stun.js.map +1 -0
- package/dist/seed-bootstrap.js +4 -4
- package/dist/strand-membership-writer.js +32 -32
- package/package.json +2 -2
- package/src/cadre-node.ts +8654 -8753
- package/src/control-authorization.ts +117 -117
- package/src/control-cohort.ts +128 -128
- package/src/control-formation-recorder.ts +353 -353
- package/src/control-read-retry.ts +216 -216
- package/src/control-schema.ts +1078 -1078
- package/src/control-write-retry.ts +567 -567
- package/src/diagnostics/connection-path.ts +1 -29
- package/src/formation-approval.ts +610 -610
- package/src/hibernation-manager.ts +638 -638
- package/src/identity-key.ts +7 -62
- package/src/index.ts +644 -647
- package/src/invitation-bootstrap.ts +40 -40
- package/src/joined-strand-store.ts +498 -498
- package/src/link-budget.ts +684 -684
- package/src/membership-connection-gater.ts +570 -570
- package/src/node-local-snapshot.ts +298 -298
- package/src/peer-dial.ts +315 -315
- package/src/peer-join-backfill.ts +710 -710
- package/src/pending-join-runner.ts +735 -735
- package/src/relay-reservation.ts +1121 -1121
- package/src/relay-server.ts +98 -98
- package/src/relay-stun.ts +58 -0
- package/src/seed-bootstrap.ts +1448 -1448
- package/src/strand-addr-protocol.ts +547 -547
- package/src/strand-database.ts +287 -287
- package/src/strand-first-sync-gate.ts +391 -391
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +725 -725
- package/src/strand-fret-addrs.ts +145 -145
- package/src/strand-instance-manager.ts +1789 -1789
- package/src/strand-membership-writer.ts +1974 -1974
- package/src/strand-network-state-file.ts +40 -40
- package/src/strand-network-state.ts +243 -243
- package/src/strand-revocation-enforcer.ts +654 -654
- package/src/strand-solicitation.ts +532 -532
- package/src/strand-wake-protocol.ts +401 -401
- package/src/types.ts +2222 -2222
- package/dist/diagnostics/webrtc-turn-tracker.d.ts +0 -50
- package/dist/diagnostics/webrtc-turn-tracker.js +0 -196
- package/dist/diagnostics/webrtc-turn-tracker.js.map +0 -1
- package/src/diagnostics/webrtc-turn-tracker.ts +0 -222
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The single definition of the field vector every signed control-plane message
|
|
3
|
-
* covers. Both producers of signed bytes — `control-database.ts`'s
|
|
4
|
-
* `buildAuthorizationMessage` (raw digest bytes) and `peer-authorization.ts`'s
|
|
5
|
-
* digest helpers (base64url digest strings) — build their vector here, so the
|
|
6
|
-
* byte layout cannot drift between them or away from the SQL constraints in
|
|
7
|
-
* `schemas/control.qsql`.
|
|
8
|
-
*
|
|
9
|
-
* Every vector leads with two fixed literals:
|
|
10
|
-
*
|
|
11
|
-
* digest(<domain>, <action>, <row field 1>, ..., <row field n>)
|
|
12
|
-
*
|
|
13
|
-
* so a signature verifies ONLY against the one rule it was minted for. Without
|
|
14
|
-
* the tags, several rules built byte-identical tuples (e.g. `ValidationKey`
|
|
15
|
-
* insert and `OwnerKey` insert both signed `digest(Key, StampId)`), so an
|
|
16
|
-
* approval for a narrow grant doubled as an approval for full ownership.
|
|
17
|
-
*
|
|
18
|
-
* This module deliberately has no Quereus / Optimystic / libp2p imports so the
|
|
19
|
-
* lightweight verifiers (`peer-authorization.ts`, consumed by the offline
|
|
20
|
-
* `cadre enroll register` check) can use it without pulling in the runtime.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Names of the CadreControl tables, in schema order. The single list: the
|
|
25
|
-
* {@link ControlTable} union is derived from it and `ControlDatabase.countRows`
|
|
26
|
-
* guards its dynamic `from` clause against it (keeping the table name off the
|
|
27
|
-
* SQL-injection surface), so a new table cannot be added to one and missed in
|
|
28
|
-
* the other.
|
|
29
|
-
*
|
|
30
|
-
* `Revocation` is here for the same two reasons as every other entry: it
|
|
31
|
-
* derives a `'CadreControl.Revocation'` domain tag — its `Authorized` CHECK
|
|
32
|
-
* verifies an owner signature over the `'remove'`-tagged digest that
|
|
33
|
-
* `peer-authorization.ts`'s `revocationDigest` mints, and its `AuthorizedReissue`
|
|
34
|
-
* CHECK the `'reissue'`-tagged digest `ControlDatabase.reissueRevocations`
|
|
35
|
-
* signs — and `countRows` counts it.
|
|
36
|
-
*/
|
|
37
|
-
export const CONTROL_TABLES = [
|
|
38
|
-
'OwnerKey',
|
|
39
|
-
'ValidationKey',
|
|
40
|
-
'Strand',
|
|
41
|
-
'StrandPartyKey',
|
|
42
|
-
'JoinedStrand',
|
|
43
|
-
'PendingJoin',
|
|
44
|
-
'CadrePeer',
|
|
45
|
-
'DeviceToken',
|
|
46
|
-
'FormationInvite',
|
|
47
|
-
'FormationUsage',
|
|
48
|
-
'Revocation',
|
|
49
|
-
] as const;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Name of a CadreControl table. Doubles as the table half of a
|
|
53
|
-
* {@link ControlDomain} tag.
|
|
54
|
-
*/
|
|
55
|
-
export type ControlTable = typeof CONTROL_TABLES[number];
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The control tables whose rows carry a single-use `StampId` retired into
|
|
59
|
-
* `CadreControl.Revocation` on delete — i.e. the ones the schema's
|
|
60
|
-
* `NotRevoked` / `RevocationRecorded` CHECK pair guards, and the only values
|
|
61
|
-
* `Revocation.RowIsGone` accepts in `TableName`. `Extract` from
|
|
62
|
-
* {@link ControlTable} rather than a fresh literal list, so a renamed table is a
|
|
63
|
-
* compile error here instead of a silently dead branch.
|
|
64
|
-
*
|
|
65
|
-
* Lives in this import-free module rather than beside its main consumer
|
|
66
|
-
* (`control-database.ts`) so the lightweight signers — `peer-authorization.ts`'s
|
|
67
|
-
* `revocationDigest` — can type against it without pulling in the runtime.
|
|
68
|
-
*/
|
|
69
|
-
export type RevocableTable = Extract<ControlTable, 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' | 'PendingJoin' | 'DeviceToken'>;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* What a signature authorizes, scoped to one table rule — or, for
|
|
73
|
-
* `'Cadre.Enrollment'`, the offline peer vouch that `cadre enroll register`
|
|
74
|
-
* verifies (no table checks it, so it needs its own domain to stay disjoint
|
|
75
|
-
* from every table rule).
|
|
76
|
-
*/
|
|
77
|
-
export type ControlDomain = `CadreControl.${ControlTable}` | 'Cadre.Enrollment';
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* The action half of the tag:
|
|
81
|
-
* - `'add'` / `'remove'` — insert / delete of the named row.
|
|
82
|
-
* - `'vouch'` — an owner (or validation key) vouches the row's semantics
|
|
83
|
-
* without adding or removing it: the `CadrePeer` membership vouch (shared,
|
|
84
|
-
* deliberately, by its insert and its owner-update branch), the
|
|
85
|
-
* `FormationUsage` disclosure validation, and the offline enrollment vouch.
|
|
86
|
-
* `DeviceToken` has no `'vouch'` digest — its owner re-touch branch was
|
|
87
|
-
* removed (it rewrote the row outside the monotonicity guard), so an owner
|
|
88
|
-
* correcting a token deletes and re-inserts it.
|
|
89
|
-
* - `'publish'` — a peer self-signs its OWN record (the `CadrePeer` /
|
|
90
|
-
* `DeviceToken` self-update branches), with its own key rather than an
|
|
91
|
-
* owner key.
|
|
92
|
-
* - `'consent'` — a peer self-signs its OWN `FormationUsage` redemption (the
|
|
93
|
-
* joiner proving it agreed to join), with its own key. Distinct from the
|
|
94
|
-
* approver's `'vouch'` over the same table so the two stored signatures are
|
|
95
|
-
* never interchangeable.
|
|
96
|
-
* - `'reissue'` — an owner re-writes an existing `Revocation` tombstone,
|
|
97
|
-
* bumping its `ReissuedAt` counter so a tombstone committed while the node
|
|
98
|
-
* was alone can be re-broadcast. Distinct from `'remove'` so a tombstone
|
|
99
|
-
* append approval can never be replayed as a re-issue and vice versa.
|
|
100
|
-
*/
|
|
101
|
-
export type ControlAction = 'add' | 'remove' | 'vouch' | 'publish' | 'consent' | 'reissue';
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* The full ordered field vector a control-plane signature covers. Digest this
|
|
105
|
-
* with the crypto plugin's injective multi-field encoding (every field TEXT);
|
|
106
|
-
* the SQL mirror passes the same literals as leading `digest(...)` arguments:
|
|
107
|
-
*
|
|
108
|
-
* TS: digest(controlAuthorizationFields('CadreControl.X', 'add', [a, b]), 'sha256', ...)
|
|
109
|
-
* SQL: digest('CadreControl.X', 'add', new.A, new.B)
|
|
110
|
-
*/
|
|
111
|
-
export function controlAuthorizationFields(
|
|
112
|
-
domain: ControlDomain,
|
|
113
|
-
action: ControlAction,
|
|
114
|
-
rowFields: string[],
|
|
115
|
-
): string[] {
|
|
116
|
-
return [domain, action, ...rowFields];
|
|
117
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The single definition of the field vector every signed control-plane message
|
|
3
|
+
* covers. Both producers of signed bytes — `control-database.ts`'s
|
|
4
|
+
* `buildAuthorizationMessage` (raw digest bytes) and `peer-authorization.ts`'s
|
|
5
|
+
* digest helpers (base64url digest strings) — build their vector here, so the
|
|
6
|
+
* byte layout cannot drift between them or away from the SQL constraints in
|
|
7
|
+
* `schemas/control.qsql`.
|
|
8
|
+
*
|
|
9
|
+
* Every vector leads with two fixed literals:
|
|
10
|
+
*
|
|
11
|
+
* digest(<domain>, <action>, <row field 1>, ..., <row field n>)
|
|
12
|
+
*
|
|
13
|
+
* so a signature verifies ONLY against the one rule it was minted for. Without
|
|
14
|
+
* the tags, several rules built byte-identical tuples (e.g. `ValidationKey`
|
|
15
|
+
* insert and `OwnerKey` insert both signed `digest(Key, StampId)`), so an
|
|
16
|
+
* approval for a narrow grant doubled as an approval for full ownership.
|
|
17
|
+
*
|
|
18
|
+
* This module deliberately has no Quereus / Optimystic / libp2p imports so the
|
|
19
|
+
* lightweight verifiers (`peer-authorization.ts`, consumed by the offline
|
|
20
|
+
* `cadre enroll register` check) can use it without pulling in the runtime.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Names of the CadreControl tables, in schema order. The single list: the
|
|
25
|
+
* {@link ControlTable} union is derived from it and `ControlDatabase.countRows`
|
|
26
|
+
* guards its dynamic `from` clause against it (keeping the table name off the
|
|
27
|
+
* SQL-injection surface), so a new table cannot be added to one and missed in
|
|
28
|
+
* the other.
|
|
29
|
+
*
|
|
30
|
+
* `Revocation` is here for the same two reasons as every other entry: it
|
|
31
|
+
* derives a `'CadreControl.Revocation'` domain tag — its `Authorized` CHECK
|
|
32
|
+
* verifies an owner signature over the `'remove'`-tagged digest that
|
|
33
|
+
* `peer-authorization.ts`'s `revocationDigest` mints, and its `AuthorizedReissue`
|
|
34
|
+
* CHECK the `'reissue'`-tagged digest `ControlDatabase.reissueRevocations`
|
|
35
|
+
* signs — and `countRows` counts it.
|
|
36
|
+
*/
|
|
37
|
+
export const CONTROL_TABLES = [
|
|
38
|
+
'OwnerKey',
|
|
39
|
+
'ValidationKey',
|
|
40
|
+
'Strand',
|
|
41
|
+
'StrandPartyKey',
|
|
42
|
+
'JoinedStrand',
|
|
43
|
+
'PendingJoin',
|
|
44
|
+
'CadrePeer',
|
|
45
|
+
'DeviceToken',
|
|
46
|
+
'FormationInvite',
|
|
47
|
+
'FormationUsage',
|
|
48
|
+
'Revocation',
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Name of a CadreControl table. Doubles as the table half of a
|
|
53
|
+
* {@link ControlDomain} tag.
|
|
54
|
+
*/
|
|
55
|
+
export type ControlTable = typeof CONTROL_TABLES[number];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The control tables whose rows carry a single-use `StampId` retired into
|
|
59
|
+
* `CadreControl.Revocation` on delete — i.e. the ones the schema's
|
|
60
|
+
* `NotRevoked` / `RevocationRecorded` CHECK pair guards, and the only values
|
|
61
|
+
* `Revocation.RowIsGone` accepts in `TableName`. `Extract` from
|
|
62
|
+
* {@link ControlTable} rather than a fresh literal list, so a renamed table is a
|
|
63
|
+
* compile error here instead of a silently dead branch.
|
|
64
|
+
*
|
|
65
|
+
* Lives in this import-free module rather than beside its main consumer
|
|
66
|
+
* (`control-database.ts`) so the lightweight signers — `peer-authorization.ts`'s
|
|
67
|
+
* `revocationDigest` — can type against it without pulling in the runtime.
|
|
68
|
+
*/
|
|
69
|
+
export type RevocableTable = Extract<ControlTable, 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' | 'PendingJoin' | 'DeviceToken'>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What a signature authorizes, scoped to one table rule — or, for
|
|
73
|
+
* `'Cadre.Enrollment'`, the offline peer vouch that `cadre enroll register`
|
|
74
|
+
* verifies (no table checks it, so it needs its own domain to stay disjoint
|
|
75
|
+
* from every table rule).
|
|
76
|
+
*/
|
|
77
|
+
export type ControlDomain = `CadreControl.${ControlTable}` | 'Cadre.Enrollment';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The action half of the tag:
|
|
81
|
+
* - `'add'` / `'remove'` — insert / delete of the named row.
|
|
82
|
+
* - `'vouch'` — an owner (or validation key) vouches the row's semantics
|
|
83
|
+
* without adding or removing it: the `CadrePeer` membership vouch (shared,
|
|
84
|
+
* deliberately, by its insert and its owner-update branch), the
|
|
85
|
+
* `FormationUsage` disclosure validation, and the offline enrollment vouch.
|
|
86
|
+
* `DeviceToken` has no `'vouch'` digest — its owner re-touch branch was
|
|
87
|
+
* removed (it rewrote the row outside the monotonicity guard), so an owner
|
|
88
|
+
* correcting a token deletes and re-inserts it.
|
|
89
|
+
* - `'publish'` — a peer self-signs its OWN record (the `CadrePeer` /
|
|
90
|
+
* `DeviceToken` self-update branches), with its own key rather than an
|
|
91
|
+
* owner key.
|
|
92
|
+
* - `'consent'` — a peer self-signs its OWN `FormationUsage` redemption (the
|
|
93
|
+
* joiner proving it agreed to join), with its own key. Distinct from the
|
|
94
|
+
* approver's `'vouch'` over the same table so the two stored signatures are
|
|
95
|
+
* never interchangeable.
|
|
96
|
+
* - `'reissue'` — an owner re-writes an existing `Revocation` tombstone,
|
|
97
|
+
* bumping its `ReissuedAt` counter so a tombstone committed while the node
|
|
98
|
+
* was alone can be re-broadcast. Distinct from `'remove'` so a tombstone
|
|
99
|
+
* append approval can never be replayed as a re-issue and vice versa.
|
|
100
|
+
*/
|
|
101
|
+
export type ControlAction = 'add' | 'remove' | 'vouch' | 'publish' | 'consent' | 'reissue';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The full ordered field vector a control-plane signature covers. Digest this
|
|
105
|
+
* with the crypto plugin's injective multi-field encoding (every field TEXT);
|
|
106
|
+
* the SQL mirror passes the same literals as leading `digest(...)` arguments:
|
|
107
|
+
*
|
|
108
|
+
* TS: digest(controlAuthorizationFields('CadreControl.X', 'add', [a, b]), 'sha256', ...)
|
|
109
|
+
* SQL: digest('CadreControl.X', 'add', new.A, new.B)
|
|
110
|
+
*/
|
|
111
|
+
export function controlAuthorizationFields(
|
|
112
|
+
domain: ControlDomain,
|
|
113
|
+
action: ControlAction,
|
|
114
|
+
rowFields: string[],
|
|
115
|
+
): string[] {
|
|
116
|
+
return [domain, action, ...rowFields];
|
|
117
|
+
}
|
package/src/control-cohort.ts
CHANGED
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Control-cohort dial selection: the pure policy behind
|
|
3
|
-
* {@link CadreNode.reconcileControlCohort}. Decides WHICH known cadre siblings a
|
|
4
|
-
* node proactively dials each pass to keep the `CadreControl` collections' cohort
|
|
5
|
-
* connected (and thus replicating), without devolving into an N² mesh.
|
|
6
|
-
*
|
|
7
|
-
* FRET only needs a *connected* graph with the block clusters reachable, not a
|
|
8
|
-
* full mesh. Storage/owner nodes are the stable, publicly-dialable backbone
|
|
9
|
-
* that hold the control blocks, so the policy is **backbone-preferential with a
|
|
10
|
-
* bounded out-degree**:
|
|
11
|
-
*
|
|
12
|
-
* - **Always dial every backbone (owner) member** — the owner set is
|
|
13
|
-
* small by design, and routing cohort formation through publicly-dialable
|
|
14
|
-
* owner nodes keeps the cohort ≥2 where owner-signed writes originate.
|
|
15
|
-
* - **Fill the remainder** up to `targetDegree` with non-owner members,
|
|
16
|
-
* ordered deterministically (peerId sort) so every node makes the same stable
|
|
17
|
-
* choice across passes and load spreads predictably.
|
|
18
|
-
*
|
|
19
|
-
* For a small party (members ≤ degree) this degenerates to a full mesh, which is
|
|
20
|
-
* correct, cheap, and matches what the convergence tests do by hand; for a large
|
|
21
|
-
* party it bounds out-degree to `backbone + targetDegree`.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { ed25519PublicKeyB64FromPeerId } from './seed-bootstrap.js';
|
|
25
|
-
import type { CohortPeerRow } from './strand-cohort.js';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Default recurring cadence for {@link CadreNode.reconcileControlCohort} (ms).
|
|
29
|
-
* In the same spirit as the 5s strand-watch poll but lighter, since a control
|
|
30
|
-
* cohort changes far less often than strand activity.
|
|
31
|
-
*
|
|
32
|
-
* An interval between pass starts, not a deadline on a pass. Passes are
|
|
33
|
-
* single-flight, so a pass that runs longer than this is joined rather than
|
|
34
|
-
* stacked, and the effective cadence becomes the pass length: one unreachable
|
|
35
|
-
* sibling's dial alone can take the whole per-peer dial budget (86 s at the
|
|
36
|
-
* default declared link).
|
|
37
|
-
*/
|
|
38
|
-
export const DEFAULT_CONTROL_COHORT_RECONCILE_MS = 15_000;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Default cap on the number of **non-owner** members a single reconcile pass
|
|
42
|
-
* proactively dials. Backbone (owner) members are always dialed and do NOT
|
|
43
|
-
* count against this cap.
|
|
44
|
-
*/
|
|
45
|
-
export const DEFAULT_CONTROL_COHORT_TARGET_DEGREE = 6;
|
|
46
|
-
|
|
47
|
-
// The per-sibling and per-address dial time limits live in `peer-dial.ts`
|
|
48
|
-
// (`DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS`,
|
|
49
|
-
// `DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS`), beside the dial they
|
|
50
|
-
// bound, which `seed-bootstrap.ts` also uses and cannot import from here.
|
|
51
|
-
|
|
52
|
-
/** Outcome of {@link selectControlCohortDials}: who to dial + what the cap dropped. */
|
|
53
|
-
export interface ControlCohortSelection {
|
|
54
|
-
/**
|
|
55
|
-
* Siblings to dial this pass, backbone first then the bounded non-owner
|
|
56
|
-
* fill. Already self-excluded (the caller must pass self-excluded `siblings`).
|
|
57
|
-
* Still includes peers that may turn out to be already-connected — the caller
|
|
58
|
-
* diffs against live connections separately.
|
|
59
|
-
*/
|
|
60
|
-
dials: CohortPeerRow[];
|
|
61
|
-
/** Non-owner members dropped by the `targetDegree` cap (0 when none). */
|
|
62
|
-
cappedNonOwner: number;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** What one {@link CadreNode.reconcileControlCohort} pass did. */
|
|
66
|
-
export interface ControlCohortReconcileResult {
|
|
67
|
-
/**
|
|
68
|
-
* Peer ids this pass dialled and whose dial resolved, in dial order: selected
|
|
69
|
-
* siblings on the steady-state pass, retained bootstrap peers on the cold-start
|
|
70
|
-
* pass (the pass takes one branch or the other, never both). A peer already
|
|
71
|
-
* connected when the pass began is never listed — the pass skips it. A resolved
|
|
72
|
-
* dial can still be refused moments later by the remote's membership gate, so
|
|
73
|
-
* an entry means "this pass opened a connection", not "that connection is still
|
|
74
|
-
* open".
|
|
75
|
-
*/
|
|
76
|
-
dialed: string[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Stable peerId ordering so the bounded fill is identical across passes/nodes. */
|
|
80
|
-
function byPeerId(a: CohortPeerRow, b: CohortPeerRow): number {
|
|
81
|
-
if (a.peerId < b.peerId) return -1;
|
|
82
|
-
if (a.peerId > b.peerId) return 1;
|
|
83
|
-
return 0;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Backbone-preferential, bounded-out-degree selection of control-cohort siblings
|
|
88
|
-
* to dial.
|
|
89
|
-
*
|
|
90
|
-
* A member is **backbone** iff the ed25519 key derived from its peerId
|
|
91
|
-
* (see {@link ed25519PublicKeyB64FromPeerId}) is in `ownerKeys`. All backbone
|
|
92
|
-
* members are selected; non-owner members fill up to `targetDegree` in
|
|
93
|
-
* deterministic peerId order.
|
|
94
|
-
*
|
|
95
|
-
* @param siblings - cadre members EXCLUDING self (the caller filters self out).
|
|
96
|
-
* @param ownerKeys - the converged `OwnerKey` set; may be empty/partial
|
|
97
|
-
* before owner convergence, in which case no member classifies as backbone
|
|
98
|
-
* yet and the bounded fill still makes progress (preference sharpens later).
|
|
99
|
-
* @param targetDegree - cap on non-owner dials (negative is treated as 0).
|
|
100
|
-
*/
|
|
101
|
-
export function selectControlCohortDials(
|
|
102
|
-
siblings: CohortPeerRow[],
|
|
103
|
-
ownerKeys: ReadonlySet<string>,
|
|
104
|
-
targetDegree: number,
|
|
105
|
-
): ControlCohortSelection {
|
|
106
|
-
const backbone: CohortPeerRow[] = [];
|
|
107
|
-
const rest: CohortPeerRow[] = [];
|
|
108
|
-
|
|
109
|
-
for (const peer of siblings) {
|
|
110
|
-
const pubKey = ed25519PublicKeyB64FromPeerId(peer.peerId);
|
|
111
|
-
if (pubKey !== null && ownerKeys.has(pubKey)) {
|
|
112
|
-
backbone.push(peer);
|
|
113
|
-
} else {
|
|
114
|
-
rest.push(peer);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
backbone.sort(byPeerId);
|
|
119
|
-
rest.sort(byPeerId);
|
|
120
|
-
|
|
121
|
-
const cap = Math.max(0, targetDegree);
|
|
122
|
-
const fill = rest.slice(0, cap);
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
dials: [...backbone, ...fill],
|
|
126
|
-
cappedNonOwner: rest.length - fill.length,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Control-cohort dial selection: the pure policy behind
|
|
3
|
+
* {@link CadreNode.reconcileControlCohort}. Decides WHICH known cadre siblings a
|
|
4
|
+
* node proactively dials each pass to keep the `CadreControl` collections' cohort
|
|
5
|
+
* connected (and thus replicating), without devolving into an N² mesh.
|
|
6
|
+
*
|
|
7
|
+
* FRET only needs a *connected* graph with the block clusters reachable, not a
|
|
8
|
+
* full mesh. Storage/owner nodes are the stable, publicly-dialable backbone
|
|
9
|
+
* that hold the control blocks, so the policy is **backbone-preferential with a
|
|
10
|
+
* bounded out-degree**:
|
|
11
|
+
*
|
|
12
|
+
* - **Always dial every backbone (owner) member** — the owner set is
|
|
13
|
+
* small by design, and routing cohort formation through publicly-dialable
|
|
14
|
+
* owner nodes keeps the cohort ≥2 where owner-signed writes originate.
|
|
15
|
+
* - **Fill the remainder** up to `targetDegree` with non-owner members,
|
|
16
|
+
* ordered deterministically (peerId sort) so every node makes the same stable
|
|
17
|
+
* choice across passes and load spreads predictably.
|
|
18
|
+
*
|
|
19
|
+
* For a small party (members ≤ degree) this degenerates to a full mesh, which is
|
|
20
|
+
* correct, cheap, and matches what the convergence tests do by hand; for a large
|
|
21
|
+
* party it bounds out-degree to `backbone + targetDegree`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { ed25519PublicKeyB64FromPeerId } from './seed-bootstrap.js';
|
|
25
|
+
import type { CohortPeerRow } from './strand-cohort.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default recurring cadence for {@link CadreNode.reconcileControlCohort} (ms).
|
|
29
|
+
* In the same spirit as the 5s strand-watch poll but lighter, since a control
|
|
30
|
+
* cohort changes far less often than strand activity.
|
|
31
|
+
*
|
|
32
|
+
* An interval between pass starts, not a deadline on a pass. Passes are
|
|
33
|
+
* single-flight, so a pass that runs longer than this is joined rather than
|
|
34
|
+
* stacked, and the effective cadence becomes the pass length: one unreachable
|
|
35
|
+
* sibling's dial alone can take the whole per-peer dial budget (86 s at the
|
|
36
|
+
* default declared link).
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_CONTROL_COHORT_RECONCILE_MS = 15_000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default cap on the number of **non-owner** members a single reconcile pass
|
|
42
|
+
* proactively dials. Backbone (owner) members are always dialed and do NOT
|
|
43
|
+
* count against this cap.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_CONTROL_COHORT_TARGET_DEGREE = 6;
|
|
46
|
+
|
|
47
|
+
// The per-sibling and per-address dial time limits live in `peer-dial.ts`
|
|
48
|
+
// (`DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS`,
|
|
49
|
+
// `DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS`), beside the dial they
|
|
50
|
+
// bound, which `seed-bootstrap.ts` also uses and cannot import from here.
|
|
51
|
+
|
|
52
|
+
/** Outcome of {@link selectControlCohortDials}: who to dial + what the cap dropped. */
|
|
53
|
+
export interface ControlCohortSelection {
|
|
54
|
+
/**
|
|
55
|
+
* Siblings to dial this pass, backbone first then the bounded non-owner
|
|
56
|
+
* fill. Already self-excluded (the caller must pass self-excluded `siblings`).
|
|
57
|
+
* Still includes peers that may turn out to be already-connected — the caller
|
|
58
|
+
* diffs against live connections separately.
|
|
59
|
+
*/
|
|
60
|
+
dials: CohortPeerRow[];
|
|
61
|
+
/** Non-owner members dropped by the `targetDegree` cap (0 when none). */
|
|
62
|
+
cappedNonOwner: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** What one {@link CadreNode.reconcileControlCohort} pass did. */
|
|
66
|
+
export interface ControlCohortReconcileResult {
|
|
67
|
+
/**
|
|
68
|
+
* Peer ids this pass dialled and whose dial resolved, in dial order: selected
|
|
69
|
+
* siblings on the steady-state pass, retained bootstrap peers on the cold-start
|
|
70
|
+
* pass (the pass takes one branch or the other, never both). A peer already
|
|
71
|
+
* connected when the pass began is never listed — the pass skips it. A resolved
|
|
72
|
+
* dial can still be refused moments later by the remote's membership gate, so
|
|
73
|
+
* an entry means "this pass opened a connection", not "that connection is still
|
|
74
|
+
* open".
|
|
75
|
+
*/
|
|
76
|
+
dialed: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Stable peerId ordering so the bounded fill is identical across passes/nodes. */
|
|
80
|
+
function byPeerId(a: CohortPeerRow, b: CohortPeerRow): number {
|
|
81
|
+
if (a.peerId < b.peerId) return -1;
|
|
82
|
+
if (a.peerId > b.peerId) return 1;
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Backbone-preferential, bounded-out-degree selection of control-cohort siblings
|
|
88
|
+
* to dial.
|
|
89
|
+
*
|
|
90
|
+
* A member is **backbone** iff the ed25519 key derived from its peerId
|
|
91
|
+
* (see {@link ed25519PublicKeyB64FromPeerId}) is in `ownerKeys`. All backbone
|
|
92
|
+
* members are selected; non-owner members fill up to `targetDegree` in
|
|
93
|
+
* deterministic peerId order.
|
|
94
|
+
*
|
|
95
|
+
* @param siblings - cadre members EXCLUDING self (the caller filters self out).
|
|
96
|
+
* @param ownerKeys - the converged `OwnerKey` set; may be empty/partial
|
|
97
|
+
* before owner convergence, in which case no member classifies as backbone
|
|
98
|
+
* yet and the bounded fill still makes progress (preference sharpens later).
|
|
99
|
+
* @param targetDegree - cap on non-owner dials (negative is treated as 0).
|
|
100
|
+
*/
|
|
101
|
+
export function selectControlCohortDials(
|
|
102
|
+
siblings: CohortPeerRow[],
|
|
103
|
+
ownerKeys: ReadonlySet<string>,
|
|
104
|
+
targetDegree: number,
|
|
105
|
+
): ControlCohortSelection {
|
|
106
|
+
const backbone: CohortPeerRow[] = [];
|
|
107
|
+
const rest: CohortPeerRow[] = [];
|
|
108
|
+
|
|
109
|
+
for (const peer of siblings) {
|
|
110
|
+
const pubKey = ed25519PublicKeyB64FromPeerId(peer.peerId);
|
|
111
|
+
if (pubKey !== null && ownerKeys.has(pubKey)) {
|
|
112
|
+
backbone.push(peer);
|
|
113
|
+
} else {
|
|
114
|
+
rest.push(peer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
backbone.sort(byPeerId);
|
|
119
|
+
rest.sort(byPeerId);
|
|
120
|
+
|
|
121
|
+
const cap = Math.max(0, targetDegree);
|
|
122
|
+
const fill = rest.slice(0, cap);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
dials: [...backbone, ...fill],
|
|
126
|
+
cappedNonOwner: rest.length - fill.length,
|
|
127
|
+
};
|
|
128
|
+
}
|