@quo-systems/quo 0.2.15 → 0.2.16

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/README.md CHANGED
@@ -28,12 +28,13 @@ no dependencies. The package ships JavaScript with declarations, emitted by
28
28
  types nowhere under `node_modules`.
29
29
 
30
30
  ```
31
+ protocol/ the shelf a kit in any language reads: SPEC.md and the vectors, no code
32
+ protocol/vectors/ fixed inputs and outputs, so another language proves its bytes
31
33
  src/being/ the Being side: what a being author imports, if anything
32
34
  src/ward/ the ward: the Ground contract, door, seal, arithmetic, heirs, stance, allowance
33
35
  src/harbor/ MemoryHarbor, and the harbor core with its store, reach and dialer
34
36
  src/conformance/ behaviours any ward must show, written against the truth
35
37
  test/ the suites
36
- vectors/ fixed inputs and outputs, so another language proves its bytes
37
38
  ```
38
39
 
39
40
  ## Requirements
@@ -77,27 +78,37 @@ import { conform } from '@quo-systems/quo/conformance';
77
78
 
78
79
  ## Another language
79
80
 
80
- The hand to a kit in another language is two things of two kinds. The
81
- vectors are the byte-level hand, everything a stranger can observe:
82
- `vectors/arithmetic.json`, the primitives the seal rests on, SHA-256,
83
- Ed25519, X25519, HKDF and AES-256-GCM; `vectors/framing.json`, Quo's own,
84
- the ward pk, the digest, the signed ask body, the sealed shapes, the
85
- invitation and the knock; `vectors/wire.json`, the frames on a socket and
86
- the one request a door takes. They import by name,
87
- `@quo-systems/quo/vectors/framing.json`, so a kit's own suite can read them
88
- from the package. A kit reproduces them or it is not this
89
- protocol. `src/conformance/` is the behavioural hand, and it is a checklist
90
- a kit ports, not a harness it runs: the beings it is shown with run only in
91
- a TypeScript ward. `quo-kit.md` "The tree" says the whole of it.
81
+ The hand to a kit in another language is one folder, `protocol/`, and it
82
+ holds two things of two kinds. `SPEC.md` is the protocol, and it assumes
83
+ nothing: a kit is written against it and against nothing else here. The
84
+ vectors beside it are the byte-level hand, everything a stranger can
85
+ observe: `protocol/vectors/arithmetic.json`, the primitives the seal rests
86
+ on, SHA-256, Ed25519, X25519, HKDF and AES-256-GCM;
87
+ `protocol/vectors/framing.json`, Quo's own, the ward pk, the digest, the
88
+ signed ask body, the sealed shapes, the invitation and the knock;
89
+ `protocol/vectors/wire.json`, the frames on a socket and the one request a
90
+ door takes; and `protocol/vectors/door.json`, the door's thirteen cases,
91
+ each one an arrival a ward will not answer, with the bytes that arrive, the
92
+ bytes that leave and the partition's digest on both sides of the judgement.
93
+ They import by name,
94
+ `@quo-systems/quo/protocol/vectors/framing.json`, so a kit's own suite can
95
+ read them from the package. A kit reproduces them or it is not this
96
+ protocol.
97
+
98
+ Nothing outside that folder is the protocol. `src/` is this kit's
99
+ interpretation, and `src/conformance/` is a checklist a kit ports rather
100
+ than a harness it runs: it imports the base class, the silence spelling and
101
+ this kit's store and reach, so the beings it is shown with run only in a
102
+ TypeScript ward. `quo-kit.md` "The three shelves" says which is which.
92
103
 
93
104
  ## Publishing
94
105
 
95
106
  Published from 0.1.0, the first version under this name, with no
96
107
  compatibility promise before 1.0.0: the words may still move. `npm pack
97
- --dry-run` shows what ships: the emitted `dist/`, the source, the vectors,
98
- the spec, this file, the licence and the notice, and no tests and no
99
- configs. The spec ships because it is the truth the source and the vectors
100
- are read against. Publishing is gated from the root of the repository:
108
+ --dry-run` shows what ships: the emitted `dist/`, the source, the protocol
109
+ shelf with the spec inside it, this file, the licence and the notice, and no
110
+ tests and no configs. The spec ships because it is the truth the source and
111
+ the vectors are read against. Publishing is gated from the root of the repository:
101
112
  `npm run release:quo` there runs both gates, `check` and `check:terrain`,
102
113
  and publishes on green; `npm publish` inside this package refuses and says
103
114
  so. This package carries the version of its own work and is bound to no
@@ -17,6 +17,7 @@ export declare class MemoryHarbor {
17
17
  readonly partitions: Map<string, Record<string, unknown>>;
18
18
  readonly wards: Map<string, Booted>;
19
19
  readonly objects: WeakMap<object, BeingLike>;
20
+ random: Ground['random'];
20
21
  route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
21
22
  link(harbor: MemoryHarbor): void;
22
23
  boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted>;
@@ -10,6 +10,12 @@ export class MemoryHarbor {
10
10
  partitions = new Map(); // seed -> memory. the harbor keeps it and reads nothing
11
11
  wards = new Map(); // seed -> pointers
12
12
  objects = new WeakMap(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
13
+ // Where the wards this harbor boots draw their entropy. The device's own
14
+ // by default. A harbor may hand a fixed stream instead, which is what pins
15
+ // bytes: every key a ward mints and every nonce it seals under comes from
16
+ // here, so a ward on a known seed with a known stream answers the same
17
+ // bytes every run, and a corpus of them is a corpus and not a sample.
18
+ random = entropy;
13
19
  // the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
14
20
  async route(farPk, bytes) {
15
21
  if (this.down.has(farPk))
@@ -55,7 +61,7 @@ export class MemoryHarbor {
55
61
  const back = await this.route(farPk, new Uint8Array(bytes));
56
62
  return back === undefined ? undefined : new Uint8Array(back);
57
63
  },
58
- random: entropy,
64
+ random: (n) => this.random(n),
59
65
  };
60
66
  const w = await Ward(ground);
61
67
  const booted = { ...w, pk: await learnPk(w) };
@@ -7,6 +7,10 @@ export declare function unhex(text: string): Uint8Array;
7
7
  export declare function concat(parts: Uint8Array[]): Uint8Array;
8
8
  export declare function sameBytes(a: Uint8Array, b: Uint8Array): boolean;
9
9
  export declare const smallOrder: (pk: Uint8Array) => boolean;
10
+ export declare const heldKeys: () => {
11
+ held: number;
12
+ bound: number;
13
+ };
10
14
  export declare function sha256(...parts: Uint8Array[]): Promise<Uint8Array>;
11
15
  export type Pair = {
12
16
  secret: Uint8Array;
@@ -86,27 +86,97 @@ const key32 = (value, what) => {
86
86
  return value;
87
87
  };
88
88
  const pkcs8 = (prefix, value, what) => concat([prefix, key32(value, what)]);
89
- const secretKey = (alg, prefix, value, what, uses) => subtle().importKey('pkcs8', pkcs8(prefix, value, what), alg, true, uses);
90
- const publicKey = (alg, value, what, uses) => subtle().importKey('raw', key32(value, what), alg, true, uses);
91
- // Subtle exports the public half of a private key only through a JWK, where `x` is the 32 raw bytes in base64url.
92
- async function rawPublic(secret) {
93
- const jwk = await subtle().exportKey('jwk', secret);
94
- const binary = atob(jwk.x.replaceAll('-', '+').replaceAll('_', '/'));
95
- const out = new Uint8Array(binary.length);
96
- for (let at = 0; at < binary.length; at += 1)
97
- out[at] = binary.charCodeAt(at);
98
- return out;
89
+ // Importing a key is the most expensive thing on the path of an ask, and most
90
+ // of the imports are the same key again: a ward signs every reply with the one
91
+ // key, opens every ask with the one padlock, and verifies a relation under the
92
+ // key it verified it under last time. Measured over a round trip, fifteen of
93
+ // the twenty-five imports were bytes already imported once.
94
+ //
95
+ // So an imported key is kept, by the bytes it was imported from. A CryptoKey
96
+ // cannot be changed once it exists, so handing the same one out twice is
97
+ // handing out what a second import would have built. Nothing here is a
98
+ // decision a peer can see: two wards that cache differently, or not at all,
99
+ // speak the same bytes.
100
+ //
101
+ // It is bounded, and that is not a detail. A relation mints a fresh key on
102
+ // every ask, so a ward that talked all day would otherwise hold a key for
103
+ // every ask it ever made. Past the bound the least recently used goes, which
104
+ // is the key of a relation that has fallen quiet, and importing it again
105
+ // costs what it cost the first time.
106
+ //
107
+ // The secret keys in here are the ones the partition already holds in this
108
+ // process, as seeds. The cache is another shape of what the ward is already
109
+ // standing on, and never a second place a secret comes from.
110
+ const KEYS = 512;
111
+ const imported = new Map();
112
+ const keep = (id, make) => {
113
+ const had = imported.get(id);
114
+ if (had !== undefined) {
115
+ imported.delete(id); // and set again below: the most recently used goes last
116
+ imported.set(id, had);
117
+ return had;
118
+ }
119
+ const made = make();
120
+ // A key that would not import is not kept: the next call asks subtle again
121
+ // and hears the same refusal, rather than reading one this cache remembered.
122
+ // Node takes any thirty-two bytes as a public key and finds out at verify,
123
+ // so nothing here reaches this line; a terrain that checks the point at the
124
+ // import does, and a refusal it remembered would be a relation killed for
125
+ // good by one bad arrival.
126
+ made.catch(() => imported.delete(id));
127
+ imported.set(id, made);
128
+ // One in, at most one out: a map keeps what was put in the order it was put,
129
+ // so the first key it names is the one used longest ago.
130
+ if (imported.size > KEYS)
131
+ imported.delete(imported.keys().next().value);
132
+ return made;
133
+ };
134
+ // How many imported keys are held, and the bound they are held under. Nothing
135
+ // in the ward reads either: they are here to be looked at, and for the suite
136
+ // that holds the bound to what it says.
137
+ export const heldKeys = () => ({ held: imported.size, bound: KEYS });
138
+ const secretKey = (alg, prefix, value, what, uses) => {
139
+ const bytes = pkcs8(prefix, value, what);
140
+ return keep(`${alg.name}|${uses.join('+')}|${hex(bytes)}`, () => subtle().importKey('pkcs8', bytes, alg, true, uses));
141
+ };
142
+ const publicKey = (alg, value, what, uses) => {
143
+ const bytes = key32(value, what);
144
+ return keep(`${alg.name}|${uses.join('+')}|pk|${hex(bytes)}`, () => subtle().importKey('raw', bytes, alg, true, uses));
145
+ };
146
+ // Subtle exports the public half of a private key only through a JWK, where
147
+ // `x` is the 32 raw bytes in base64url. The answer is a fact about the key and
148
+ // never changes, so it is kept beside the key it was read from and goes when
149
+ // the key does.
150
+ const publics = new WeakMap();
151
+ function rawPublic(secret) {
152
+ const had = publics.get(secret);
153
+ if (had !== undefined)
154
+ return had;
155
+ const read = (async () => {
156
+ const jwk = await subtle().exportKey('jwk', secret);
157
+ const binary = atob(jwk.x.replaceAll('-', '+').replaceAll('_', '/'));
158
+ const out = new Uint8Array(binary.length);
159
+ for (let at = 0; at < binary.length; at += 1)
160
+ out[at] = binary.charCodeAt(at);
161
+ return out;
162
+ })();
163
+ publics.set(secret, read);
164
+ return read;
99
165
  }
100
166
  export async function sha256(...parts) {
101
167
  return new Uint8Array(await subtle().digest('SHA-256', concat(parts)));
102
168
  }
169
+ // Both halves are copies. The seed and the public key are kept behind the two
170
+ // caches above, and a pair is handed to whoever asked for it: what she does
171
+ // with the bytes in her hand is hers, and must not reach what the next caller
172
+ // is given.
103
173
  export async function signingPair(seed) {
104
174
  const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
105
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
175
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
106
176
  }
107
177
  export async function sealingPair(seed) {
108
178
  const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
109
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
179
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
110
180
  }
111
181
  export async function sign(message, secret) {
112
182
  const key = await secretKey(ED, ED_SECRET, secret, 'secret', ['sign']);
package/dist/ward/seal.js CHANGED
@@ -26,7 +26,7 @@ export const SIZE = 1024 * 1024;
26
26
  // designs and no separation at all: one secret would be doing two jobs with
27
27
  // nothing said about it, and a second kit would have to reproduce a
28
28
  // construction nobody named. HKDF-SHA-256 under a label is the separation
29
- // said out loud, and it is what `vectors/framing.json` pins.
29
+ // said out loud, and it is what `protocol/vectors/framing.json` pins.
30
30
  //
31
31
  // Bytes are key material and text is not. A seed handed in as bytes of the
32
32
  // key length is taken as it stands, which is what a harbor mints; anything
package/dist/ward/ward.js CHANGED
@@ -279,10 +279,14 @@ class Self {
279
279
  return unreached();
280
280
  }
281
281
  const { bytes, ephemeral } = sealed;
282
- // The wait is bounded, and this is the one thing the ward times. A being
283
- // holds three answers and a wait that does not end is none of them: a
284
- // relation that comes back round holds a lane the answer needs, and only a
285
- // bound on the wait can break that. What comes back late is not read.
282
+ // The wait is bounded here, and again around the lane in the stance. Two
283
+ // bounds and not one, because they end two different things: the stance's
284
+ // ends the wait a being is held in, and this one ends the occupancy of the
285
+ // relation's lane. A being holds three answers and a wait that does not
286
+ // end is none of them, and a lane nobody ever leaves is a relation the
287
+ // next ask never reaches. Only a bound on the wire breaks the second, and
288
+ // taking it out would leave one quiet far side holding the lane for good.
289
+ // What comes back late is not read.
286
290
  //
287
291
  // A wait that ran out is `late`, never unreached. Unreached promises
288
292
  // nothing was delivered and is safe to retry; a bound that expired knows
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/quo",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
5
5
  "keywords": [
6
6
  "quo",
@@ -35,11 +35,11 @@
35
35
  "types": "./dist/conformance/index.d.ts",
36
36
  "default": "./dist/conformance/index.js"
37
37
  },
38
- "./vectors/*": "./vectors/*",
38
+ "./protocol/*": "./protocol/*",
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "scripts": {
42
- "build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../../papers/SPEC.md SPEC.md && cp ../../papers/quo-kit.md quo-kit.md",
42
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../../papers/SPEC.md protocol/SPEC.md && cp ../../papers/quo-kit.md quo-kit.md",
43
43
  "test": "node --test \"test/*.test.ts\"",
44
44
  "check:terrain": "node --test \"test/terrain/*.test.ts\"",
45
45
  "prepublishOnly": "test \"$QUO_GATED\" = 1 || { echo 'publish from the root, gated once: npm run release:quo' >&2; exit 1; }"
@@ -50,8 +50,7 @@
50
50
  "files": [
51
51
  "dist",
52
52
  "src",
53
- "vectors",
54
- "SPEC.md",
53
+ "protocol",
55
54
  "quo-kit.md",
56
55
  "README.md",
57
56
  "LICENSE",
@@ -710,6 +710,12 @@ spent, so bytes leave and the door's silence comes back: after take the
710
710
  invitation names the standing and nothing else, so it is exactly as alive as
711
711
  the standing is.
712
712
 
713
+ A lane is released by the bound and by nothing else. An ask that never comes
714
+ back holds its relation's lane, and every later ask on that relation waits
715
+ behind it, so the bound ends an occupancy and not only a wait. A kit that
716
+ bounds where a being is waiting, and not where the lane is held, has a
717
+ relation that one quiet far side closes for good.
718
+
713
719
  ### The public being
714
720
 
715
721
  A ward may have one public being, and no more. She is an ordinary being,
@@ -898,7 +904,8 @@ the default would leave her only the narrowing half, and would give the ward
898
904
  no way to allow one long piece of work without making every ask that
899
905
  patient.
900
906
 
901
- The sender's wait has an end, and this is the one thing the ward times. The
907
+ The sender's wait has an end, and time is the only thing the ward measures:
908
+ no count of doors, no count of bytes, no count of tries. The
902
909
  bound covers the whole of an ask, from the moment she calls: a relation that
903
910
  comes back round on itself is stopped at its own lane, before a byte is
904
911
  sealed, and a bound that watched only the wire would never see it. This is
@@ -1045,9 +1052,14 @@ in pieces, by a being who knows how her own work divides.
1045
1052
 
1046
1053
  The hand to a kit in another language is two things, and they are of two
1047
1054
  kinds. `vectors/` is the byte-level hand: fixed inputs and outputs for
1048
- everything a stranger can observe, the arithmetic, the ward pk, the digest,
1049
- the signed body, the sealed shapes, the invitation, the knock, and the
1050
- frames on a socket and the one request a door takes. A kit reproduces them
1055
+ everything a stranger can observe, in four areas. `arithmetic.json` is the
1056
+ primitives the seal rests on. `framing.json` is the ward pk, the digest, the
1057
+ signed body, the sealed shapes, the invitation and the knock.
1058
+ `wire.json` is the frames on a socket and the one request a door takes.
1059
+ `door.json` is the door's thirteen cases, each one an arrival: the bytes that
1060
+ come in, the bytes that go out, what those bytes open to where a hand holds
1061
+ the lid, and the partition's digest before and after, so `nothing written`
1062
+ is a value a kit checks and never a sentence it reads. A kit reproduces them
1051
1063
  or it is not this protocol. The conformance suite is the behavioural hand,
1052
1064
  and it is a checklist and not a harness: one ward is one runtime and one
1053
1065
  language, so the beings a suite is shown with run only in the ward its kit
@@ -0,0 +1,345 @@
1
+ {
2
+ "corpus": "@quo-systems/quo",
3
+ "encoding": "hex",
4
+ "area": "door",
5
+ "note": "The door's thirteen cases as fixed bytes: what a ward answers to every arrival it will not answer, and what it writes while doing so. Each record is one arrival at one door: the ask exactly as it arrives, the reply exactly as it leaves, what that reply opens to where the hand holds the lid's secret, and the ward partition's digest before and after, so `nothing written` is a value a kit checks and not a sentence it reads. Every random is fixed. The world is two harbors linked as a wire, wards on the seeds A, B and P, and every byte of entropy any of them draws comes from SplitMix64 seeded at 20250901: the state starts at the seed, each draw adds 0x9e3779b97f4a7c15, mixes with two xor-shift-multiplies, and the sixty-four bit result is spent eight bytes at a time, least significant byte first. The stream is reset at the start of every record, so a record is reproduced without running the ones before it. D1 noise is the one record whose reply nobody can open: the door seals it to the first thirty-two bytes of the noise, and no one holds a secret for those. That is the case, not a gap in it. A word to a bound key is a different length from a silence to a stranger, on purpose. The law of one silence equalizes what strangers see and nothing else, so every record of kind `silence` is one length and every record of kind `word` or `object` is not. `ward` is the seed of the ward whose door judged the arrival. `state` says what that ward was in the middle of when it did, which is the only thing a kit needs beyond the bytes to stand a door in the same place.",
6
+ "vectors": [
7
+ {
8
+ "case": "D1",
9
+ "name": "wrong padlock",
10
+ "state": "a heir the door holds, sealed to another ward's padlock",
11
+ "ward": "B",
12
+ "kind": "silence",
13
+ "heard": false,
14
+ "ask": "d1fc81101fc0d82fb5c7298d598aadd7146299554890bd22c140350d5efabd3a17c7fe10142dec3537204579e051a006349247a4078cd3723d854b8ca07afd4a7b17fca2078eef5a624baa41381687774e11164b4a60ae5ecef6b93f6f793bc3445be287c45e83d8fd03e210637aeaa177a418ab6f21db665af8a03c7d2ccc390b3c90d8526a883f28acd478109f4b3691ad6f0e07d111fc1a2517899c3874219edf5f14b97801924296115153b18e9fdaf74ba9124e3e0ff2793189af09f25c6c0793f4241303b3ada7529eabb144c28c8545c7c8421d06cbb4fcdcdc18c682fd36462747cd6d1ae3f59de12b072d7357293f2e71c48af57bbecefb19df2439d99ae1233e20bf7d77fbc1dad12681913f2ee386c1fec9e836e3f1f5456ed4c53fb1dfff6a7689ab4929d46cd33532073eb545739b4efe216a1f46757c4ea0b1",
15
+ "reply": "84637ecbb7afd6785f3d54e8a1f8f03161d14cdadae513c3a0fd8524f8836220e2e9b1e962a94de7d12632b93c811f561f8cf7ab1071afe44abbd59d18272319624a3ad1c2630c1ea8f02f3364869642ed30f568d6885f42db8a68e14f77549098683dd5c826a5abf0c658eff571e2786d9f808bd613e4c7b580d1c19e933c6d",
16
+ "opens": {
17
+ "silence": true
18
+ },
19
+ "before": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
20
+ "after": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
21
+ "wrote": false
22
+ },
23
+ {
24
+ "case": "D1",
25
+ "name": "noise",
26
+ "state": "bytes that are not a sealed ask at all",
27
+ "ward": "B",
28
+ "kind": "silence",
29
+ "heard": false,
30
+ "ask": "01c0b7c9e315dc00c2913141db31df59a760f3cea15ad41d5c51eadb39d927b4035fe63be4f7794aa739a173fea2c4c0606717001e28c219b638bec66a9d299f9da2f1ef860034d7e23a6e203f8139996d912d084d4df8923d5745aa53cfce040b9b16cfb47ffb4dd79a9b391c116cd235",
31
+ "reply": "eea19c6ed20cc8562606ce2695bd54d5524a3b0cbf3a224fd5a944a920dba85440682158d87d5f046a69925d12015bac1416b51fe89e1fa7f10f69478f29234a0eda4ab1468d813c3f00562c2a740d187ecf7c11fb15857a3c8023fa991dbd59bd605dec985ae25631ce152bc3e2596e6e0481f78218ba2e27859d00dc3c0d8e",
32
+ "opens": null,
33
+ "before": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
34
+ "after": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
35
+ "wrote": false
36
+ },
37
+ {
38
+ "case": "D2",
39
+ "name": "method is not a string",
40
+ "state": "a bound key, one field of the wrong shape",
41
+ "ward": "B",
42
+ "kind": "silence",
43
+ "heard": false,
44
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35f177345d075948a5a84e8878a41acb54aef478fe080923c1a58c639ddf51cda073d6ace5734b0dabebbca1ae8b0bacf1cfe3a555ddcc7d497c9deb4b89ebc5ff0bcc14ed95d0c73f5fc98456e9daca100b9e68bbd19caec646792e270abb88105bc93c34ea5c08f44a3ddd29d2f3c016cc6239e158fa",
45
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74cfedc99744f293794909c6a1f57c8e4a787dbe3591c2f543539f1013d8a86f6810e7f24e485766763b2db3642dedf361f92900101a4e9a8cdded78fe397e952f7e7f446149cbb238ab3596b772ee9767b9f8d2ecb1dfa3ba33d75cad543",
46
+ "opens": {
47
+ "silence": true
48
+ },
49
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
50
+ "after": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
51
+ "wrote": false
52
+ },
53
+ {
54
+ "case": "D2",
55
+ "name": "args is not an object",
56
+ "state": "a bound key, one field of the wrong shape",
57
+ "ward": "B",
58
+ "kind": "silence",
59
+ "heard": false,
60
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35f177345d075948a5a84e8878a41acb54aef478fe080923c1a58c76d9985cd3a822d8b4ff7c51daef31e729c80716985e463fb7ba473e8e191954a84f44681e490edfc9f2998df9794c9136df18841541da782c9e49ccd859161e3d2ca93fdc1f5fa5160f8d578e54b2ee94318f415685278f49295cb4ea3da40eae",
61
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74cfedc99744f293794909c6a1f57c8e4a787dbe3591c2f543539f1013d8a86f6810e7f24e485766763b2db3642dedf361f92900101a4e9a8cdded78fe397e952f7e7f446149cbb238ab3596b772ee9767b9f8d2ecb1dfa3ba33d75cad543",
62
+ "opens": {
63
+ "silence": true
64
+ },
65
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
66
+ "after": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
67
+ "wrote": false
68
+ },
69
+ {
70
+ "case": "D2",
71
+ "name": "seq is zero",
72
+ "state": "a bound key, one field of the wrong shape",
73
+ "ward": "B",
74
+ "kind": "silence",
75
+ "heard": false,
76
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b34ed657218484803f2fe5c827ba406d909e7a27df418437687efd338dd921293e56186f1ed2c0cd2b0767052ead4c22c91120e7934a9e4a6846af69cfa849fba769e872443dfeaea6a42209de7ea7bf1b671312ed95d3d4b60024c2944b43d66c5cd8edd6a2398c6ff9f319ee5c5886d96111ad5b7d2216f8249",
77
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74cfedc99744f293794909c6a1f57c8e4a787dbe3591c2f543539f1013d8a86f6810e7f24e485766763b2db3642dedf361f92900101a4e9a8cdded78fe397e952f7e7f446149cbb238ab3596b772ee9767b9f8d2ecb1dfa3ba33d75cad543",
78
+ "opens": {
79
+ "silence": true
80
+ },
81
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
82
+ "after": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
83
+ "wrote": false
84
+ },
85
+ {
86
+ "case": "D2",
87
+ "name": "time is negative",
88
+ "state": "a bound key, one field of the wrong shape",
89
+ "ward": "B",
90
+ "kind": "silence",
91
+ "heard": false,
92
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35f177345d075948a5a84e8866a506d909e7a27df418437687efd338dd921293e56186f1ed2c0cd2b076d82048d224068df7986c2df5654e7121a0262e0a5b71110a8401cd9845c8ff6d34128fa6767a47b43a9aeaf3a28f194a98860a2b870e25e8d404df48144b0e92c707111060764444fdbfe6cfe8d52dd5",
93
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74cfedc99744f293794909c6a1f57c8e4a787dbe3591c2f543539f1013d8a86f6810e7f24e485766763b2db3642dedf361f92900101a4e9a8cdded78fe397e952f7e7f446149cbb238ab3596b772ee9767b9f8d2ecb1dfa3ba33d75cad543",
94
+ "opens": {
95
+ "silence": true
96
+ },
97
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
98
+ "after": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
99
+ "wrote": false
100
+ },
101
+ {
102
+ "case": "D2",
103
+ "name": "by is not a key",
104
+ "state": "a bound key, one field of the wrong shape",
105
+ "ward": "B",
106
+ "kind": "silence",
107
+ "heard": false,
108
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cd81a87f82978e703cd1af5e88c66f32e9a99a659a90ae5b351ad235d2971643caf163308c76d7ac09d1971f6e4a988881cf9d982ebfc6508bf7520eba44bce53ffb1338e6b8c4535b70444541a3fa4bf589b8ff6961343d221866eede55bb7497d0ad205966d315a04d0207d9bf4baa822cbf5c5d0946fb86b2e8bb3e29168f025f052078b09dfc6556ea5542afebb2c0bdf5ef0056",
109
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74cfedc99744f293794909c6a1f57c8e4a787dbe3591c2f543539f1013d8a86f6810e7f24e485766763b2db3642dedf361f92900101a4e9a8cdded78fe397e952f7e7f446149cbb238ab3596b772ee9767b9f8d2ecb1dfa3ba33d75cad543",
110
+ "opens": {
111
+ "silence": true
112
+ },
113
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
114
+ "after": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
115
+ "wrote": false
116
+ },
117
+ {
118
+ "case": "D2",
119
+ "name": "hops at zero",
120
+ "state": "a bound key, and a chain that has run out",
121
+ "ward": "B",
122
+ "kind": "silence",
123
+ "heard": false,
124
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35f177345d075948a5a84e8878a41acb54aef478fe080923c1a58c76d9985cd3a822d8b4ff7c51daef31ab29c03b7882cf8d868ab1391fcaf6e8cf7ea3c990d262268ea4d28fe61810e87190c30f41d68644d3511cf3a58cab78a278cca9229a50fbbeb2fb0e6607face7ecd82419c65ff2baa6607094e06eb80acb728fb3bd6b628be45a4b9",
125
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74cfedc99744f293794909c6a1f57c8e4a787dbe3591c2f543539f1013d8a86f6810e7f24e485766763b2db3642dedf361f92900101a4e9a8cdded78fe397e952f7e7f446149cbb238ab3596b772ee9767b9f8d2ecb1dfa3ba33d75cad543",
126
+ "opens": {
127
+ "silence": true
128
+ },
129
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
130
+ "after": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
131
+ "wrote": false
132
+ },
133
+ {
134
+ "case": "D3",
135
+ "name": "for nobody, nobody home",
136
+ "state": "a ward with no public being",
137
+ "ward": "B",
138
+ "kind": "silence",
139
+ "heard": false,
140
+ "ask": "84637ecbb7afd6785f3d54e8a1f8f03161d14cdadae513c3a0fd8524f8836220ba17e2a2e1fde39c1b7f215273348cf6d0e5e0bdf2045199fea841fcb99ae6842d14b40710c9e7f2a9ad106487cb41e16a1b8bc35ac660911cf7e35c3ae1c9a3e600dd40e8a85d32d1a78b05c185e12496169c6fefe493e9e631bf439f3314cb61ce8c5c2cf328ca15247763bed2a5c072e758578504aded3f75b8923939307aeb59eaf924a4e7ef1df23ad8eef50ad880b960185795b762f38b9bb7ea9629505c25296244b5665e9c0d1d9a2c1690c02d77c167882a2f1f9180ca8f2b11a37bbd263dc9fbff020b31812ca12b76930dfbb06e6ef00074ff05614bf62d14b22530d3",
141
+ "reply": "5f1426463a519ded7693446c34eb6751015d932d4d929557abf3ee32ecfdcc192baea2488556aacc13b62ccbb07b7888dc2af01a2a7e59152b9546b1e185e0c50b34a4c7e9665fc5592cdb61993f571bebc20a713c733b58acbd703408206c5a42f2b644ad19e79ce07ec8b51064c369635a3ce7353dc49a112788ef910aad10",
142
+ "opens": {
143
+ "silence": true
144
+ },
145
+ "before": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
146
+ "after": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
147
+ "wrote": false
148
+ },
149
+ {
150
+ "case": "D4",
151
+ "name": "for nobody, the signature fails",
152
+ "state": "a ward with a public being, a body signed by another key",
153
+ "ward": "P",
154
+ "kind": "silence",
155
+ "heard": false,
156
+ "ask": "5f1426463a519ded7693446c34eb6751015d932d4d929557abf3ee32ecfdcc19f7cb3d88928e737cc1502a253c5a301097cad7810345026dc168c010c0ae3e2e01ad94f6278407c1874cfea971c97e46d2adf623ca548ba6f972fcd20bdffa32b7a25faaf36f8f41053079df352547ca562be02c22e4ace975d9c4926e82152e0c24eb7de188b7ce11e81a2bed397363832c78ae09ad96e09e53b024eabf686c89ccb453cfb35fa368f46f31d321a67eb16a9c67788272def37a1dc55d6a5489d4aae9b132f446fffaa89d0e09f03f33e70cddfe1eb50e7254dd85821c9d3265d0c3362fb0499e7bde0a277b7d2021fd7aa970edc2cea02560bbaee91624d2f1d9ee",
157
+ "reply": "9f1bf06332529ed4df06d6000f037f02e2d822d0e8c6256842817e568383c372f50e74b15f3135af6b35a9b3776809dc400adf30b1fc380b2d77c40544424dd0fcb48c5808822b51a7260219bcda18d1321c626a71351958d941833ec7c62b9615a7120992ddddfeecc26276a833d3975021d2906a4ec771350770e925a01463",
158
+ "opens": {
159
+ "silence": true
160
+ },
161
+ "before": "cc8ba34d418b81459d6654b95f81ddd6f9a5296bb88d8a405614b1db45dd6ed9",
162
+ "after": "cc8ba34d418b81459d6654b95f81ddd6f9a5296bb88d8a405614b1db45dd6ed9",
163
+ "wrote": false
164
+ },
165
+ {
166
+ "case": "D5",
167
+ "name": "a heir not held",
168
+ "state": "a heir this ward never minted",
169
+ "ward": "B",
170
+ "kind": "silence",
171
+ "heard": false,
172
+ "ask": "84637ecbb7afd6785f3d54e8a1f8f03161d14cdadae513c3a0fd8524f8836220ba17e2a2e1fdaf8f11756b16772bc8aa94b7b6b8a55b54cfaeaf14fcbdc8b7db2f10ea584098e5a7f7a91430809517e63f49db9c0d9660c64ca3e30b38bfc9a6b0518944b7ad0c26c9e1db1d87dcf276c5578174e2b1ddfcf76db302c4665c8b2a92cb4972f12098463a342fe7def3d67eed5951d305b7fd3421a8c2303e2277fb53b6f67ffdf1a70df22cdcacb15381d1e6acc757bf4535f4c28e1283269e8139b10710ecd0648e63ef3aa6536a694eb28ef6662d14d7f9492015e9ef85734c6e420dc0eef0fbe560d2bf4029fb5d379660b94644eed49986ea9ad9642fb391dd4fca23035ffef81a2e5543ccf5dd2f0991e9e97fed86d0d86fba15b2c2026f2a5b04c96212583a38c9b1870bcf40cd2b64ed6c706c2a2415513e255f10869d",
173
+ "reply": "5f1426463a519ded7693446c34eb6751015d932d4d929557abf3ee32ecfdcc192baea2488556aacc13b62ccbb07b7888dc2af01a2a7e59152b9546b1e185e0c50b34a4c7e9665fc5592cdb61993f571bebc20a713c733b58acbd703408206c5a42f2b644ad19e79ce07ec8b51064c369635a3ce7353dc49a112788ef910aad10",
174
+ "opens": {
175
+ "silence": true
176
+ },
177
+ "before": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
178
+ "after": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
179
+ "wrote": false
180
+ },
181
+ {
182
+ "case": "D6",
183
+ "name": "a key not admitted",
184
+ "state": "a heir the door holds, a key nobody announced",
185
+ "ward": "B",
186
+ "kind": "silence",
187
+ "heard": false,
188
+ "ask": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a34b31ba0403a808d63af3f5b706095ed7def0893fe00e2fc7c2aabe76cea0b61af4c594718fb40c235b6a35696800ea29d4d9277fcd470671c7bcc5e18c79f9aa5c0afcd7f83274ec57f1730594ac30b836507c76e4b814465a7c0e1085d834389bdc1caeca09ce3c77137a4f46b54982cdcfe313c35ac39d9f44efd792103b87ba4c808383ed50f77534c9e4d801160d5ca2fc87a91e007aed7993d25ce4255c89372e3b5be9aa263f2bb34bf0f1732d0c7ca6be29b5c2737a888f480b3e86e83dd1ce923fc5954409cfedb9727d6db42ef81e7a22d571dbbf7e66221e161fb37305c424ea1bcf03686fc0cac293144c507a27a15b1f949f4268e13bf918f2fb0a84b5866c4658aeea297519181c4301b349fb79224575ed898dbf9ceac9f724e",
189
+ "reply": "dd9a4e87c36a6c2db4a3d6b33bd5a9d471d2ac9410cc2596cb769cb57407125051baef6786856eb312eaea0b6709252f32a9ea831eadc8fcad6d1d031f01484c70dcfe83061a57e421d9321ab846aa33a213e89dd8f6aa116af8d5bb447abcd04f4edd8edf63bf2b6a606f9f21a72181ab5555a4cd13ce7cb5224077254972e8",
190
+ "opens": {
191
+ "silence": true
192
+ },
193
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
194
+ "after": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
195
+ "wrote": false
196
+ },
197
+ {
198
+ "case": "D7",
199
+ "name": "the signature fails under an admitted key",
200
+ "state": "a bound key named in by, a body signed by another",
201
+ "ward": "B",
202
+ "kind": "silence",
203
+ "heard": false,
204
+ "ask": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a34b31ba0403a808d63af3f5b706095ed7def0893fe00e2fc7c2aabe76cea0b61af4c594718fb40c235b6a35696800ea29d4d9277fcd470671c7bcc5e18c79f9aa5c0afcd7f83274ec57f1730594ac30b826751c16b4a8b4735aa94b25e0cd5128fbfc3c8e1a1cfe0c57f64f2a16a5cc82ddca9333735fd67daa94dfb2a720dee7bf39a026a36d40774074a984ed11b65d5ca2fc87a91e007aed7993d25ce4255c89372e3b5be9aa263f2bb34bf0f1732d0c7ca6be29b5c2737a888f480b3e86e83dd1ce923fc5954409cfedb9727d6db424d264e8cdcbd7dc8e2b6af09e8f2d7c189cf2d1f3dfd05ffa99e48d7d8e1d79f89a11e15fef1a8124bf13cd07a1036b806f46f16e87ad790496b579ac490db131979d762571fe600cba5a04f6a695b9c",
205
+ "reply": "dd9a4e87c36a6c2db4a3d6b33bd5a9d471d2ac9410cc2596cb769cb57407125051baef6786856eb312eaea0b6709252f32a9ea831eadc8fcad6d1d031f01484c70dcfe83061a57e421d9321ab846aa33a213e89dd8f6aa116af8d5bb447abcd04f4edd8edf63bf2b6a606f9f21a72181ab5555a4cd13ce7cb5224077254972e8",
206
+ "opens": {
207
+ "silence": true
208
+ },
209
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
210
+ "after": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
211
+ "wrote": false
212
+ },
213
+ {
214
+ "case": "D8",
215
+ "name": "she is not there",
216
+ "state": "the ward restarted without her class: her heirs stand and she does not",
217
+ "ward": "B",
218
+ "kind": "word",
219
+ "heard": false,
220
+ "ask": "d1fc81101fc0d82fb5c7298d598aadd7146299554890bd22c140350d5efabd3af181a0bd75844672e9f707bb65dc951964f73a82e009ef628bcfed3bc81502e96dc21f4a8d206183b3613631a09475b93505b8e0d549a5e3c5c0172ec4a4bd79961dd1772ed0a49d4ff4b0524de8e7f35995367237ea4e8429089ef618f29f2bc7e59350d694cb7f1019a5e456c72ef608f0040461e8aff6acc8b3b934936d09655450287f7083e2f1ab2a8677ee52e7ecc97fdc0671fe65f0895685ce567091ab8888eeb8d37259c0efb78e4418839ea7f4f8c68909f0cdcacfc8669547c7524600d57180a97435ec91f033c05a8782bc1df6755f31770522eeff58a23da34ca3036797fa411083f210af546844ba40b640bd05383e4f4beeda12149741faaf2db903acdc81712c3c6c1bd1fd2ce5c049f84bbdd5d3eea4ac5e9180d62c913fbe6b48974783e08e6cbc5768adce3c753ac2058c5f00a06896c1e90acef9580a20c1d03b3e1a7260c52bba5194164a3d1a04c2cb4fde6d8b0265b7e1f28c",
221
+ "reply": "84637ecbb7afd6785f3d54e8a1f8f03161d14cdadae513c3a0fd8524f8836220e2e9b1e962a94de7d12632b93c811f561f8cf7ab1071afe44abbd59d18272319624a3ad1c2630c1ea8f02f3364869642ed30f568d6885f42db8a68e14f77549098683dd5c826a5abf0c658eff571e2786d9f808bd613e4c7b580d1c19e933c6d",
222
+ "opens": {
223
+ "silence": true
224
+ },
225
+ "before": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
226
+ "after": "0cef327059b38467e3f7bb2ccc494150ef80e690a31e633181ffd64534b2df32",
227
+ "wrote": false
228
+ },
229
+ {
230
+ "case": "D8",
231
+ "name": "she is there and the record is not",
232
+ "state": "the being is booted and the occupant her heir was opened for is gone",
233
+ "ward": "B",
234
+ "kind": "word",
235
+ "heard": true,
236
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35f177345d075948a5a84e8878a41acb54aef478fe080923c1a58c76d9985cd3a822d8b4ff7c51daef31ab2991225bfd71f871729fd0e6ec4fdc6d81cc0db739af5c768653bb2757b20dce0d210bbbacacb37779203717be35a85f937a266cbb9f563ef35ab6dfb2aa268b825f3f1d5ec5cc1a4e2bc03299c34149b180",
237
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74ee2dfde200e3e70c38b987a1e08811d4d79177fad4387bd22187dcfb6a0d200e61decd85bebb9a83822d7bfda170d83c5baada11f4787f46b41e998375e5e800bb3751a9a8a66a70172edc19447a3975841b33edd84d64f66952b6d0582c5",
238
+ "opens": {
239
+ "quo": "removed"
240
+ },
241
+ "before": "ac56803213dec9a818db15c7b0b946fad9b05ee6e81f7f9388450fbd8648958f",
242
+ "after": "ac56803213dec9a818db15c7b0b946fad9b05ee6e81f7f9388450fbd8648958f",
243
+ "wrote": false
244
+ },
245
+ {
246
+ "case": "D9",
247
+ "name": "a heir that announces nothing",
248
+ "state": "a fresh heir, and no next in the body",
249
+ "ward": "B",
250
+ "kind": "word",
251
+ "heard": true,
252
+ "ask": "84637ecbb7afd6785f3d54e8a1f8f03161d14cdadae513c3a0fd8524f8836220ba17e2a2e1fdafdd47703c49727d98adc1b7b2eaf40456cbf0f044adbf9de9df2b44ed06169fb0f5a7f6436080c247b23f1ed9c20d93369718a7bc0e69ef9ba4e4568b41e0ad0926c9e1db1d87dcf276c5578174e2b1ddfcf76db302c4665c8b2a92cb4972f12098463a342fe7def3d67eed5951d305b7fd3421a8c2303e2277fb53b6f67ffdf1a70df22cdcacb15381d1e6acc757bf4535f4c28e1283269e8139b10710ecd0648e63ef3aa6536a694eb28ef6662d14d7f9492015e9ef85734c6e420dc0eef0fbe560d2bf4029fb5d37e38972cbf17e9d9b7809b4d4200ad6098aba8fca967288509acc3c22056a7cef0da21ab8ac7fa279061d5b824464a21b63704485aead00bca83c724e4999b7c247130a027532de913cdd64ebf9b0ce3c",
253
+ "reply": "5f1426463a519ded7693446c34eb6751015d932d4d929557abf3ee32ecfdcc192baea0548611fe8d03fa77d1ac61689b8ba92c3370c3e2d832328ac1595d9d421c8d48e32b9a0b246bf24614a286d0df0ad99cdbd637102870d1e8e95c620498fac0bcd1ba582435b1327de076f8d2e22679b9c1e0a1e14527e1bed1941a6f582141b7b5b5",
254
+ "opens": {
255
+ "quo": "unannounced"
256
+ },
257
+ "before": "d0eef8ac1466acb55969bc26b3abe1efc7da1536279b8f5579429308a5ce0367",
258
+ "after": "d0eef8ac1466acb55969bc26b3abe1efc7da1536279b8f5579429308a5ce0367",
259
+ "wrote": false
260
+ },
261
+ {
262
+ "case": "D9",
263
+ "name": "a bound key that announces nothing",
264
+ "state": "a key the door already vouched for, and no next in the body",
265
+ "ward": "B",
266
+ "kind": "object",
267
+ "heard": true,
268
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35f177345d075948a5a84e8878a41acb54aef478fe080923c1a58c76d9985cd3a822d8b4ff7c51daef31ab2991225bfd71f871729fd0e6ec4fdc6d81cc0db739af5c768653bb2757b20dce0d210bbbacacb37779203717be35a85f937a266cbb9f563ef35ab6dfb2aa268b825f3f1d5ec5cc1a4e2bc03299c34149b180",
269
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a750f5da9979586e2fd5c686765810de652eae81f6d65bb704d6820eb59123f51202a9f7b952bfe24eaa05da66b584ad9697dd8c5f86f18e8124a50d54b06a30061bc9a8e219ec0dd8fac9d312991f9b5c12240d44f14f84d6bb8fa3daf822b9dbc48ca9552a86d8bf1b12a22468ca6fcd43192b26040e9b78b8b7a17532c7c9775ce18a7c26b0a3f8af2d65b15682c82cb3a804c77072bbe1a14562af60fdfe99204e7cab5271fbf3edeb1d3ffc71",
270
+ "opens": {
271
+ "object": {
272
+ "hi": "g1"
273
+ },
274
+ "seen": "b51674a53e5fa1e707b2cf1f322ffaefa54bde51436f12b5853741570771618f"
275
+ },
276
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
277
+ "after": "eb0d99882afb6353165fdad8799f31414f8f9fba268bd077993b096654588f31",
278
+ "wrote": true
279
+ },
280
+ {
281
+ "case": "D10",
282
+ "name": "a number already spent",
283
+ "state": "a bound key, a seq at or below the mark",
284
+ "ward": "B",
285
+ "kind": "word",
286
+ "heard": true,
287
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35ed657218484803f2fe5c827ba406d909e7a27df418437687efd338dd921293e56186f1ed2c0cd2b07648aacadc56034e397a15c8399fed53930390d4119cdd52312169f1ece0b84c02aaa408ccdd1acb079879ea0f2d2ebb15a15394e7a0e22ded4fe77d6879f23a982d28bef4aee07a2241e7fab279af3254",
288
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74ee2dfde200e3e70de818f6b1f4ede7fc3dd501392b9f23dfff145b7d1fd60c8dbb0ab26ff7dd66f6363e24d2c6a2efa26e468654c0ee8cfe8f033fd70096bad181116f4d9af00e1f81746a28b31fd67038b4536107326ff221138148bc59727",
289
+ "opens": {
290
+ "quo": "repeated"
291
+ },
292
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
293
+ "after": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
294
+ "wrote": false
295
+ },
296
+ {
297
+ "case": "D11",
298
+ "name": "she threw",
299
+ "state": "a bound key on an ask that raises",
300
+ "ward": "B",
301
+ "kind": "word",
302
+ "heard": true,
303
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35f177345d075948a5a84e8878a41acb54aef478fe080923c1a58c76d3925fd2e52cd6f7ec69458bf770ad295d490ae9cb2baaeb0e491098845f38d60f1801811937181f651667622b8f02054378fb442a71e81bf80b4255a068975b7f6a0e95bb906d7ced6000349b06859a29f516bd2070aec3d53bc9c42747cf62",
304
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74ee2dfde200e387ddc81993d07f5ad7025106aae5e4741c9be9d82f9e9ad5d90b806f1d8303ee0c75f106f7652a73c6a1312d11b7947537198eeca32946be853e37d96a62ce2d2b95f54519faef2dc35227d4becd78da6cb5d940ca1e4",
305
+ "opens": {
306
+ "quo": "threw"
307
+ },
308
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
309
+ "after": "0af31511f1fb9f98fc84fd817efa847e6be922f7e537f83f60b9a7298306ebd0",
310
+ "wrote": true
311
+ },
312
+ {
313
+ "case": "D12",
314
+ "name": "she said silence",
315
+ "state": "a bound key on an ask that answers the value silence",
316
+ "ward": "B",
317
+ "kind": "silence",
318
+ "heard": true,
319
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35f177345d075948a5a84e8878a41acb54aef478fe080923c1a58c76c08859dab322d8b4ff7c51daef31ab29915344dccc90cf697d08ea0b2bb00e6776c286ab75b4da33844f177a096a0d955fb83096bbb748bb099b0eb1b1f6ae519aa3f1d93795c190ecee293e03ec3ef159bd0ce64ea70728540a68ed570e031c24",
320
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74cfedc99744f293794909c6a1f57c8e4a787dbe3591c2f543539f1013d8a86f6810e7f24e485766763b2db3642dedf361f92900101a4e9a8cdded78fe397e952f7e7f446149cbb238ab3596b772ee9767b9f8d2ecb1dfa3ba33d75cad543",
321
+ "opens": {
322
+ "silence": true
323
+ },
324
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
325
+ "after": "0af31511f1fb9f98fc84fd817efa847e6be922f7e537f83f60b9a7298306ebd0",
326
+ "wrote": true
327
+ },
328
+ {
329
+ "case": "D13",
330
+ "name": "a word out of her",
331
+ "state": "a bound key on an ask that returns a shape only a ward may make",
332
+ "ward": "B",
333
+ "kind": "word",
334
+ "heard": true,
335
+ "ask": "3f43ed1fa30e3cfa3b4fd56b428e3d3c9f8561a11a6a20c76c38332f468ead0124c1b80f327342ce552b30a7ace0af22f5bb02737198288e8342a5bcfb41321c078f47c0f4c288681e134f1aa1c6262ce862b0a391196060d3107054933b5794d3c099133add19e1532fed4f8609cdd6f13d83d492606286e34bcccb6523b0f2d02188ccef483c4fd53283d8570ec0f837248276d7fe5fd7df0a361aded88bc98bc735b39b0adffd1410ac10acbb79fb0561f5a04e1990f3cedc9b13352805c8038b1b35f177345d075948a5a84e8878a41acb54aef478fe080923c1a58c76d39c549deb2295e4f97d1493b676adbc9ac462bc76aeca060a3dfb5b547dde4b43504507eaceb2ea2829389ec62c359df96cf6e0fbf0c5edb96ffc5f254994cce731764d7cf5adc66ef71afb41b3d29788ef4b6c2803ae9627306220e5ffeb",
336
+ "reply": "a8cf51bfc797db309b9d1d0ff388ab9681f00d6d96c4a32fc5c4f2c71b130a3402a74ee2dfde200e387ddc81993d07f5ad7025106aae5e4741c9be9d82f9e9ad5d90b806f1d8303ee0c75f106f7652a73c6a1312d11b7947537198eeca32946be853e37d96a62ce2d2b95f54519faef2dc35227d4becd78da6cb5d940ca1e4",
337
+ "opens": {
338
+ "quo": "threw"
339
+ },
340
+ "before": "d2330d40c0e9dc0cba9f5f6a5a1a6a79cb02d3b93f91642d1f19225e1c53a743",
341
+ "after": "0af31511f1fb9f98fc84fd817efa847e6be922f7e537f83f60b9a7298306ebd0",
342
+ "wrote": true
343
+ }
344
+ ]
345
+ }
package/quo-kit.md CHANGED
@@ -33,24 +33,67 @@ which is how a consumer does. `test/package.test.ts` is the one
33
33
  suite that meets the package as a stranger: it packs the tarball,
34
34
  installs it into an empty folder, and imports every entry point under
35
35
  plain Node. `npm pack --dry-run` shows what ships: `dist/`, the four
36
- source folders, the vectors, the papers, the README, the licence and
37
- the notice, and nothing else. Two papers ship, `SPEC.md` and `quo-kit.md`,
38
- both copied in by the build: the protocol is the truth the source and the
39
- vectors are read against, and a kit in another language is written against it
40
- and nothing else, while this paper says which of what it sees here was a
41
- choice. Publishing runs both
36
+ source folders, the protocol shelf, this paper, the README, the licence and
37
+ the notice, and nothing else. Two papers ship and the build copies both in,
38
+ each to where it belongs: the spec into `protocol/`, beside the vectors it
39
+ is read against and away from every line of TypeScript, because that folder
40
+ is what a kit in another language is written against and nothing else; this
41
+ paper to the package root, where it says which of what a reader sees here
42
+ was a choice. Publishing runs both
42
43
  gates first, `npm run check` and `npm run check:terrain`, and refuses on a
43
44
  failure.
44
45
 
45
46
  ```
47
+ protocol/ the shelf a second kit reads. SPEC.md, copied in by the build, and the vectors beside it.
48
+ protocol/vectors/ fixed inputs and outputs: the arithmetic, Quo's own framing, the frames on the wire, the door's thirteen cases.
46
49
  src/being/ the Being side. types, the base class, silence, the digest.
47
50
  src/ward/ the ward. the Ground contract, door, seal, arithmetic, heirs, stance, owner, partition, cells, allowance.
48
51
  src/harbor/ the memory harbor, the store, the reach, the dialer and the harbor core.
49
52
  src/conformance/ the behaviours any ward must show, and the fixed beings they are shown with.
50
53
  test/ the suites.
51
- vectors/ fixed inputs and outputs: the arithmetic, Quo's own framing, the frames on the wire.
52
54
  ```
53
55
 
56
+ ## The three shelves
57
+
58
+ The package holds three kinds of thing and they are not interchangeable.
59
+ Knowing which shelf a file is on decides whether a kit in another language
60
+ may read it, whether this paper or the spec governs it, and whether it
61
+ travels the day a second kit exists.
62
+
63
+ | shelf | what stands there | language |
64
+ | -------- | ---------------------------------------------------- | ----------------------- |
65
+ | protocol | `protocol/`: `SPEC.md` and the vectors | none, prose and bytes |
66
+ | kit | `src/being/`, `src/ward/`, `src/harbor/`, this paper | TypeScript, chosen here |
67
+ | harness | `src/conformance/`, `test/` | TypeScript, ours |
68
+
69
+ The protocol shelf is the only one a kit in Rust or Go reads. It is a
70
+ document that assumes nothing and four files of fixed bytes, and it is
71
+ written so that folder alone, lifted into an empty repository, is the whole
72
+ protocol. Nothing on it imports anything, because nothing on it is code.
73
+
74
+ The kit shelf is this interpretation and another kit may refuse every line
75
+ of it: a base class, a spelling for silence, a store interface, a reach, a
76
+ dialer, a file layout.
77
+
78
+ The harness shelf proves the kit shelf, and it is not the protocol however
79
+ much it reads like it. `src/conformance/` imports `Being`, `silence.ts`,
80
+ `digest.ts` and the kit's own `Store` and `Reach`, so a kit in another
81
+ language cannot run one line of it. It is exported all the same, as
82
+ `@quo-systems/quo/conformance`, because every harbor the dock stands on is
83
+ a TypeScript harbor and the suite is how one is accepted. The day a second
84
+ kit exists, the conformance suite does not run there. It is ported: a kit
85
+ writes the six obligations of the probe, carries the suite and its beings
86
+ into its own language, and reads them beside its own tests.
87
+
88
+ The hand to that kit is two things of two kinds, as the spec has it. The
89
+ vectors are the byte-level hand and the ported checklist is the
90
+ behavioural one. The suite does not become data, because the beings it is
91
+ shown with run only in the ward its kit wrote. The four areas are the
92
+ whole byte-level hand: `arithmetic.json`, `framing.json`, `wire.json` and
93
+ `door.json`, the last being the door's thirteen cases, each an arrival with
94
+ the bytes in, the bytes out and the partition's digest on both sides of the
95
+ judgement. The shelf owes nothing beyond them.
96
+
54
97
  Entry points: `@quo-systems/quo` is the Being side, `@quo-systems/quo/ward`
55
98
  is `Ward`, `@quo-systems/quo/harbor` is `MemoryHarbor`, `Harbor`, the
56
99
  store, the reach and the dialer, and `@quo-systems/quo/conformance` is
@@ -295,6 +338,19 @@ The seal is `src/ward/seal.ts` and the arithmetic is `src/ward/arithmetic.ts`.
295
338
  before a reply is. `test/ward.test.ts` reads every byte string that crossed,
296
339
  so that nothing inner is readable in the bytes.
297
340
 
341
+ An ask over two wards on two harbors spends thirty-four calls into
342
+ `crypto.subtle`, fourteen of them key imports, and `test/cost.test.ts` holds
343
+ it to forty and sixteen. Importing a key is the most expensive thing on that
344
+ path and most of the imports are the same key again: a ward signs every reply
345
+ with one key, opens every ask with one padlock, and verifies a relation under
346
+ the key it verified it under last time. So the arithmetic keeps an imported
347
+ key by the bytes it was imported from, and the public half beside the key it
348
+ was read from. The cache is bounded at `KEYS`, five hundred and twelve, least
349
+ recently used out, because a relation mints a fresh key on every ask and one
350
+ that only grew would hold a key for every ask a ward ever made. None of it is
351
+ a decision a peer can see: a kit that caches nothing speaks the same bytes,
352
+ which is why this is the kit's paper and not the protocol's.
353
+
298
354
  ## The harbor side
299
355
 
300
356
  The **store** is `src/harbor/store.ts`, the **reach** is
@@ -378,7 +434,15 @@ passes the same suite. Those tests are the checklist, not the mock.
378
434
  by its numbers: the five cases and two unreacheds of her ward, the thirteen
379
435
  cases of the door with a partition snapshot under each, seven strangers
380
436
  met with one silence and six bound keys hearing their word, hops refused
381
- at zero, and the six lines of the law of one silence.
437
+ at zero, and the six lines of the law of one silence. The world it runs in
438
+ and the thirteen cases as records are `test/door.ts`, which draws no
439
+ entropy from the device: every byte comes from one SplitMix64 stream, so
440
+ the cases are the same bytes twice. The suite reads
441
+ `protocol/vectors/door.json` and holds the tree to it, and the two laws it
442
+ states over the whole list, one length for every stranger and one
443
+ plaintext under the box, run over the corpus rather than a table beside
444
+ it. `test/write-door-vectors.ts` writes that corpus, by a hand and never
445
+ by the gate: a diff in it is a protocol change.
382
446
  - `test/blueprint.test.ts`: blueprints and instantiation through a real
383
447
  ward. What a boot leaves behind, what a restart brings back when the code
384
448
  moved under the cells, the gate and the door, the reserved ids.
@@ -393,9 +457,9 @@ passes the same suite. Those tests are the checklist, not the mock.
393
457
  again under the same id; a taker that will not take leaves the faculty no
394
458
  occupant and no key, and the next lend stands; and a being taken out of her
395
459
  ward lends nothing.
396
- - `test/seal.test.ts`: the arithmetic against `vectors/arithmetic.json`,
460
+ - `test/seal.test.ts`: the arithmetic against `protocol/vectors/arithmetic.json`,
397
461
  then the seal, round trip, and what it refuses; then the framing against
398
- `vectors/framing.json`. The arithmetic is standard and any language has
462
+ `protocol/vectors/framing.json`. The arithmetic is standard and any language has
399
463
  it. The framing is Quo's own, and a kit that reproduces the hashes and
400
464
  not the ward pk, the digest, the signed ask body, the two sealed shapes,
401
465
  the invitation or the knock is not this protocol. Every seed in that file
@@ -441,7 +505,7 @@ passes the same suite. Those tests are the checklist, not the mock.
441
505
  the store with the hints; the dialer over a stubbed line, announce, a claim proven
442
506
  at a real door before it binds and a claim nobody there holds left
443
507
  unbound, fallback, unbind and the wait before it dials again; and the frames against
444
- `vectors/wire.json`, the ask, the reply, nothing delivered and the
508
+ `protocol/vectors/wire.json`, the ask, the reply, nothing delivered and the
445
509
  announce, so a kit reproduces the bytes on a socket; and the request
446
510
  reach against the request record in the same file, over a fetch that
447
511
  sees what a listener would, one POST with the suite in its header, the
@@ -461,6 +525,14 @@ passes the same suite. Those tests are the checklist, not the mock.
461
525
  one by one; that the arithmetic spends every one of them; and the two ways
462
526
  a terrain can be short -- no `crypto.subtle` at all, and a subtle without
463
527
  the curves -- each failing at the first call, in one sentence.
528
+ - `test/cost.test.ts`: what an ask costs. Every other suite is blind to it:
529
+ a change that doubles the work per ask leaves all of them green. The
530
+ assertion is the count of calls into `crypto.subtle`, which is the same
531
+ number on every terrain and does not move because another lane of the gate
532
+ is running; a regression in it is somebody deriving again what the ward
533
+ already holds. Beside it, that the kept keys stay under their bound however
534
+ long a ward talks, and one wall clock set an order of magnitude wide, which
535
+ is a smoke alarm and not a measurement.
464
536
  - `test/package.test.ts`: the package as a stranger meets it. The tarball
465
537
  is packed, installed into an empty folder with nothing but Node, and
466
538
  every entry point the exports map names is imported. Every other suite
@@ -21,6 +21,12 @@ export class MemoryHarbor {
21
21
  readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
22
22
  readonly wards = new Map<string, Booted>(); // seed -> pointers
23
23
  readonly objects = new WeakMap<object, BeingLike>(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
24
+ // Where the wards this harbor boots draw their entropy. The device's own
25
+ // by default. A harbor may hand a fixed stream instead, which is what pins
26
+ // bytes: every key a ward mints and every nonce it seals under comes from
27
+ // here, so a ward on a known seed with a known stream answers the same
28
+ // bytes every run, and a corpus of them is a corpus and not a sample.
29
+ random: Ground['random'] = entropy;
24
30
 
25
31
  // the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
26
32
  async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
@@ -66,7 +72,7 @@ export class MemoryHarbor {
66
72
  const back = await this.route(farPk, new Uint8Array(bytes));
67
73
  return back === undefined ? undefined : new Uint8Array(back);
68
74
  },
69
- random: entropy,
75
+ random: (n) => this.random(n),
70
76
  };
71
77
  const w = await Ward(ground);
72
78
  const booted: Booted = { ...w, pk: await learnPk(w) };
@@ -85,18 +85,83 @@ const key32 = (value: Uint8Array, what: string): Uint8Array => {
85
85
  return value;
86
86
  };
87
87
  const pkcs8 = (prefix: Uint8Array, value: Uint8Array, what: string) => concat([prefix, key32(value, what)]);
88
- const secretKey = (alg: { name: string }, prefix: Uint8Array, value: Uint8Array, what: string, uses: KeyUsage[]) =>
89
- subtle().importKey('pkcs8', pkcs8(prefix, value, what) as BufferSource, alg, true, uses);
90
- const publicKey = (alg: { name: string }, value: Uint8Array, what: string, uses: KeyUsage[]) =>
91
- subtle().importKey('raw', key32(value, what) as BufferSource, alg, true, uses);
92
-
93
- // Subtle exports the public half of a private key only through a JWK, where `x` is the 32 raw bytes in base64url.
94
- async function rawPublic(secret: CryptoKey): Promise<Uint8Array> {
95
- const jwk = await subtle().exportKey('jwk', secret);
96
- const binary = atob(jwk.x!.replaceAll('-', '+').replaceAll('_', '/'));
97
- const out = new Uint8Array(binary.length);
98
- for (let at = 0; at < binary.length; at += 1) out[at] = binary.charCodeAt(at);
99
- return out;
88
+
89
+ // Importing a key is the most expensive thing on the path of an ask, and most
90
+ // of the imports are the same key again: a ward signs every reply with the one
91
+ // key, opens every ask with the one padlock, and verifies a relation under the
92
+ // key it verified it under last time. Measured over a round trip, fifteen of
93
+ // the twenty-five imports were bytes already imported once.
94
+ //
95
+ // So an imported key is kept, by the bytes it was imported from. A CryptoKey
96
+ // cannot be changed once it exists, so handing the same one out twice is
97
+ // handing out what a second import would have built. Nothing here is a
98
+ // decision a peer can see: two wards that cache differently, or not at all,
99
+ // speak the same bytes.
100
+ //
101
+ // It is bounded, and that is not a detail. A relation mints a fresh key on
102
+ // every ask, so a ward that talked all day would otherwise hold a key for
103
+ // every ask it ever made. Past the bound the least recently used goes, which
104
+ // is the key of a relation that has fallen quiet, and importing it again
105
+ // costs what it cost the first time.
106
+ //
107
+ // The secret keys in here are the ones the partition already holds in this
108
+ // process, as seeds. The cache is another shape of what the ward is already
109
+ // standing on, and never a second place a secret comes from.
110
+ const KEYS = 512;
111
+ const imported = new Map<string, Promise<CryptoKey>>();
112
+ const keep = (id: string, make: () => Promise<CryptoKey>): Promise<CryptoKey> => {
113
+ const had = imported.get(id);
114
+ if (had !== undefined) {
115
+ imported.delete(id); // and set again below: the most recently used goes last
116
+ imported.set(id, had);
117
+ return had;
118
+ }
119
+ const made = make();
120
+ // A key that would not import is not kept: the next call asks subtle again
121
+ // and hears the same refusal, rather than reading one this cache remembered.
122
+ // Node takes any thirty-two bytes as a public key and finds out at verify,
123
+ // so nothing here reaches this line; a terrain that checks the point at the
124
+ // import does, and a refusal it remembered would be a relation killed for
125
+ // good by one bad arrival.
126
+ made.catch(() => imported.delete(id));
127
+ imported.set(id, made);
128
+ // One in, at most one out: a map keeps what was put in the order it was put,
129
+ // so the first key it names is the one used longest ago.
130
+ if (imported.size > KEYS) imported.delete(imported.keys().next().value!);
131
+ return made;
132
+ };
133
+
134
+ // How many imported keys are held, and the bound they are held under. Nothing
135
+ // in the ward reads either: they are here to be looked at, and for the suite
136
+ // that holds the bound to what it says.
137
+ export const heldKeys = (): { held: number; bound: number } => ({ held: imported.size, bound: KEYS });
138
+
139
+ const secretKey = (alg: { name: string }, prefix: Uint8Array, value: Uint8Array, what: string, uses: KeyUsage[]) => {
140
+ const bytes = pkcs8(prefix, value, what);
141
+ return keep(`${alg.name}|${uses.join('+')}|${hex(bytes)}`, () => subtle().importKey('pkcs8', bytes as BufferSource, alg, true, uses));
142
+ };
143
+ const publicKey = (alg: { name: string }, value: Uint8Array, what: string, uses: KeyUsage[]) => {
144
+ const bytes = key32(value, what);
145
+ return keep(`${alg.name}|${uses.join('+')}|pk|${hex(bytes)}`, () => subtle().importKey('raw', bytes as BufferSource, alg, true, uses));
146
+ };
147
+
148
+ // Subtle exports the public half of a private key only through a JWK, where
149
+ // `x` is the 32 raw bytes in base64url. The answer is a fact about the key and
150
+ // never changes, so it is kept beside the key it was read from and goes when
151
+ // the key does.
152
+ const publics = new WeakMap<CryptoKey, Promise<Uint8Array>>();
153
+ function rawPublic(secret: CryptoKey): Promise<Uint8Array> {
154
+ const had = publics.get(secret);
155
+ if (had !== undefined) return had;
156
+ const read = (async () => {
157
+ const jwk = await subtle().exportKey('jwk', secret);
158
+ const binary = atob(jwk.x!.replaceAll('-', '+').replaceAll('_', '/'));
159
+ const out = new Uint8Array(binary.length);
160
+ for (let at = 0; at < binary.length; at += 1) out[at] = binary.charCodeAt(at);
161
+ return out;
162
+ })();
163
+ publics.set(secret, read);
164
+ return read;
100
165
  }
101
166
 
102
167
  export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
@@ -104,13 +169,17 @@ export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
104
169
  }
105
170
 
106
171
  export type Pair = { secret: Uint8Array; pk: Uint8Array };
172
+ // Both halves are copies. The seed and the public key are kept behind the two
173
+ // caches above, and a pair is handed to whoever asked for it: what she does
174
+ // with the bytes in her hand is hers, and must not reach what the next caller
175
+ // is given.
107
176
  export async function signingPair(seed: Uint8Array): Promise<Pair> {
108
177
  const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
109
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
178
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
110
179
  }
111
180
  export async function sealingPair(seed: Uint8Array): Promise<Pair> {
112
181
  const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
113
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
182
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
114
183
  }
115
184
 
116
185
  export async function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array> {
package/src/ward/seal.ts CHANGED
@@ -54,7 +54,7 @@ export const SIZE = 1024 * 1024;
54
54
  // designs and no separation at all: one secret would be doing two jobs with
55
55
  // nothing said about it, and a second kit would have to reproduce a
56
56
  // construction nobody named. HKDF-SHA-256 under a label is the separation
57
- // said out loud, and it is what `vectors/framing.json` pins.
57
+ // said out loud, and it is what `protocol/vectors/framing.json` pins.
58
58
  //
59
59
  // Bytes are key material and text is not. A seed handed in as bytes of the
60
60
  // key length is taken as it stands, which is what a harbor mints; anything
package/src/ward/ward.ts CHANGED
@@ -289,10 +289,14 @@ class Self implements BeingLike {
289
289
  return unreached();
290
290
  }
291
291
  const { bytes, ephemeral } = sealed;
292
- // The wait is bounded, and this is the one thing the ward times. A being
293
- // holds three answers and a wait that does not end is none of them: a
294
- // relation that comes back round holds a lane the answer needs, and only a
295
- // bound on the wait can break that. What comes back late is not read.
292
+ // The wait is bounded here, and again around the lane in the stance. Two
293
+ // bounds and not one, because they end two different things: the stance's
294
+ // ends the wait a being is held in, and this one ends the occupancy of the
295
+ // relation's lane. A being holds three answers and a wait that does not
296
+ // end is none of them, and a lane nobody ever leaves is a relation the
297
+ // next ask never reaches. Only a bound on the wire breaks the second, and
298
+ // taking it out would leave one quiet far side holding the lane for good.
299
+ // What comes back late is not read.
296
300
  //
297
301
  // A wait that ran out is `late`, never unreached. Unreached promises
298
302
  // nothing was delivered and is safe to retry; a bound that expired knows
File without changes
File without changes
File without changes