provared 0.1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +1062 -0
  2. package/LICENSE +202 -0
  3. package/LICENSE-CC-BY-4.0.txt +396 -0
  4. package/README.md +689 -0
  5. package/bin/provared-check.mjs +565 -0
  6. package/bin/provared-headers.mjs +110 -0
  7. package/docs/threat-model.md +997 -0
  8. package/net/headers.js +403 -0
  9. package/package.json +57 -0
  10. package/page/check-page.js +162 -0
  11. package/page/index.html +47 -0
  12. package/page/render.js +530 -0
  13. package/page/style.css +150 -0
  14. package/samples/office-supplies.expected.json +82 -0
  15. package/samples/office-supplies.jsonl +10 -0
  16. package/samples/office-supplies.show.json +59 -0
  17. package/spec/provared-format.md +1672 -0
  18. package/src/actions.js +90 -0
  19. package/src/approval.js +73 -0
  20. package/src/blockstamp.js +198 -0
  21. package/src/book.js +2232 -0
  22. package/src/check.d.ts +21 -0
  23. package/src/check.js +15 -0
  24. package/src/compare.js +276 -0
  25. package/src/cover.js +198 -0
  26. package/src/der.js +192 -0
  27. package/src/encoding.js +286 -0
  28. package/src/fields.js +94 -0
  29. package/src/guard.js +171 -0
  30. package/src/headers.js +380 -0
  31. package/src/index.d.ts +2 -0
  32. package/src/index.js +37 -0
  33. package/src/jws.js +235 -0
  34. package/src/keys.js +149 -0
  35. package/src/pass.js +67 -0
  36. package/src/passkey-browser.d.ts +20 -0
  37. package/src/passkey-browser.js +171 -0
  38. package/src/recorder.js +1115 -0
  39. package/src/seal.js +133 -0
  40. package/src/service.js +124 -0
  41. package/src/signatures.js +147 -0
  42. package/src/slip.js +401 -0
  43. package/src/standing.js +181 -0
  44. package/src/stub.js +140 -0
  45. package/src/timestamp.js +351 -0
  46. package/src/tools.js +192 -0
  47. package/src/tree.js +143 -0
  48. package/src/types.d.ts +628 -0
  49. package/src/webauthn.js +119 -0
package/src/guard.js ADDED
@@ -0,0 +1,171 @@
1
+ // The check before acting: would this action be outside the slip?
2
+ //
3
+ // The software around an agent can ask this before each action, and stop the
4
+ // action when the answer is no. It uses the same comparison as the checker
5
+ // (compare.js), so nothing is allowed here that the checker would report
6
+ // afterwards. It refuses more in one case only: an action that would add to
7
+ // a period which a stub dated after it already overfills. It writes
8
+ // nothing and changes nothing.
9
+
10
+ import { MAX_ENTRIES, MAX_LINE_BYTES, checkApprovalRecord, checkBookKeepingState, fixedOptions, keysOf, passAsSlip } from './book.js';
11
+ import { compareWithSlip } from './compare.js';
12
+ import { Refusal, canonicalJson, formatTime, parseTime, problemFrom, utf8 } from './encoding.js';
13
+ import * as f from './fields.js';
14
+ import { validateRequest } from './stub.js';
15
+
16
+ const notSound = (why) => new Refusal('record-not-sound', why);
17
+
18
+ // An upper bound on the line that the record of an action will make: the
19
+ // stub with its two signatures, the approval that goes with it, and the
20
+ // other side's countersignature. A line of a book may not be longer than
21
+ // 131,072 bytes, and an action whose record would not fit is not allowed.
22
+ const STUB_BOUND = 6700 + 600;
23
+ const COUNTERSIGNATURE_BOUND = 6900;
24
+ function lineBound(c, approval) {
25
+ let approvalBytes = 0;
26
+ if (approval !== undefined) {
27
+ try {
28
+ approvalBytes = utf8(canonicalJson(approval)).length;
29
+ } catch {
30
+ approvalBytes = 0; // not a record at all: its own check refuses it
31
+ }
32
+ }
33
+ return Math.ceil((utf8(canonicalJson(c)).length * 4) / 3) + STUB_BOUND + approvalBytes + COUNTERSIGNATURE_BOUND + 64;
34
+ }
35
+
36
+ /**
37
+ * Ask whether an action the agent is about to take would be outside its slip.
38
+ *
39
+ * @param {string} book the book so far: the slip and every stub written under it
40
+ * @param {object} proposal
41
+ * @param {string} proposal.slip the fingerprint of the slip
42
+ * @param {string} proposal.action
43
+ * @param {{unit: string, value: number}} [proposal.amount]
44
+ * @param {string} [proposal.with] the id of the service, as the slip names it
45
+ * @param {{name: string, sha256: string}[]} [proposal.details]
46
+ * @param {object} [proposal.approval] the person's signed approval of exactly this action, where the slip asks for one
47
+ * @param {string} [proposal.terms] the fingerprint of the service's terms for agents, if they are in the book
48
+ * @param {string} [proposal.pass] for a helper agent: the fingerprint of the pass it acts under
49
+ * @param {number|Date} [proposal.when] defaults to now
50
+ * @param {object} options the same options as checkBook
51
+ * @param {string[]} options.issuerKeys the thumbprints of the passkeys this check trusts. It must be given:
52
+ * a slip signed by any other key allows nothing
53
+ * @returns {Promise<{allowed: boolean, problems: {code: string, message: string}[], breaches: {code: string, message: string}[], needs: string[]}>}
54
+ * "allowed" is true only if the record so far is sound, this device
55
+ * checked every signature in it, and the action would be inside the slip.
56
+ * "needs" lists what the slip asks to go with the action. A needed
57
+ * approval must be handed over, or the action is not allowed. A needed
58
+ * countersignature cannot be known beforehand: the action is allowed, and
59
+ * is inside the slip only if the other side then countersigns it.
60
+ */
61
+ export function checkBefore(book, proposal, options = {}) {
62
+ // The options are read once, through the fixed copy a whole check reads
63
+ // them through, for the book and for the decision alike.
64
+ const fixed = fixedOptions(options);
65
+ return decide(() => checkBookKeepingState(book, fixed), proposal, fixed);
66
+ }
67
+
68
+ /**
69
+ * The same check, for a book that has already been read and is carried on
70
+ * (recorder.js): nothing is read again. Not part of the public interface.
71
+ * @param {() => Promise<{result: object, state: object, beside: Set<string>}>} read gives the answer for the book as it stands (answerOf in book.js)
72
+ * @param {object} proposal as for checkBefore
73
+ * @param {object} options as for checkBefore
74
+ */
75
+ export function checkBeforeRead(read, proposal, options = {}) {
76
+ return decide(read, proposal, options);
77
+ }
78
+
79
+ async function decide(read, proposal, options) {
80
+ const answer = { allowed: false, problems: [], breaches: [], needs: [] };
81
+ try {
82
+ if (options === null || typeof options !== 'object') options = {};
83
+ if (!Array.isArray(options.issuerKeys) || options.issuerKeys.length === 0) {
84
+ throw notSound('The check before acting must be told which passkeys it trusts. A slip signed by any other key allows nothing.');
85
+ }
86
+ const { result, state, beside } = await read();
87
+ if (result.summary.problemFound) throw notSound('A problem was found in the record so far. It must be looked at before the agent acts again.');
88
+ // A record this device could only partly check is not a pass, here either.
89
+ if (!result.summary.fullyChecked) throw notSound('This device could not check every signature in the record so far.');
90
+ if (!result.entries.every((e) => (e.kind === 'stub' || e.kind === 'pass' ? e.compared === true : true))) {
91
+ throw notSound('This device could not compare every stub and every pass in the record so far with its slip.');
92
+ }
93
+ if (result.size >= MAX_ENTRIES) throw new Refusal('too-large', 'The book is full: it holds 100,000 entries.');
94
+
95
+ f.members(proposal, ['action', 'slip'], ['amount', 'approval', 'details', 'pass', 'terms', 'when', 'with'], 'proposal');
96
+ const c = { action: proposal.action, when: formatTime(proposal.when ?? Date.now()) };
97
+ for (const name of ['amount', 'details', 'with']) if (proposal[name] !== undefined) c[name] = proposal[name];
98
+ validateRequest(c);
99
+ const when = f.time(c.when, 'when');
100
+ if (lineBound(c, proposal.approval) > MAX_LINE_BYTES) {
101
+ throw new Refusal('too-large', 'The record of this action would be longer than a line of a book may be.');
102
+ }
103
+
104
+ const slip = state.slips.get(proposal.slip);
105
+ if (!slip) throw new Refusal('slip-missing', 'The slip is not in the book.');
106
+ if (!slip.verified) throw notSound('This device could not check the slip\'s signature.');
107
+ // A helper agent acts under a pass, in a chain of its own.
108
+ let pass = null;
109
+ if (proposal.pass !== undefined) {
110
+ pass = state.passes.get(proposal.pass);
111
+ if (!pass || !pass.usable) throw new Refusal('pass-missing', 'The pass is not in the book, or did not pass its own check.');
112
+ if (pass.content.slip !== proposal.slip) throw new Refusal('pass-mismatch', 'The pass was given under another slip.');
113
+ }
114
+ // The chain allows no stub dated before the one ahead of it.
115
+ if (when < (pass ?? slip).lastWhen) throw new Refusal('time-went-backwards', 'The action is dated before the last stub in its chain.');
116
+
117
+ const service = Object.hasOwn(c, 'with') ? slip.content.with.find((s) => s.id === c.with) : undefined;
118
+ let terms;
119
+ if (proposal.terms !== undefined) {
120
+ const t = state.terms.get(proposal.terms);
121
+ if (!t || !t.usable || !t.verified) throw new Refusal('terms-not-found', 'The terms are not in the book, or did not pass their check.');
122
+ if (!keysOf(service) || canonicalJson(service.keys) !== canonicalJson(t.content.by.keys)) {
123
+ throw new Refusal('terms-mismatch', 'The terms were not signed with the keys the slip gives for the service named.');
124
+ }
125
+ terms = t.content;
126
+ }
127
+
128
+ // Cancelled in the book, or by a cancellation that the person kept and handed over beside it.
129
+ const cancelled = slip.cancelled !== null || beside.has(proposal.slip);
130
+ const shown = compareWithSlip(slip.content, slip.tally, c, { service, terms, cancelled }, false);
131
+ answer.needs = shown.needs;
132
+ const breaches = [...shown.breaches];
133
+ if (pass) {
134
+ // The pass the helper acts under, and every pass above it, as the checker compares them.
135
+ for (let p = pass; p; p = p.parent) {
136
+ const under = compareWithSlip(passAsSlip(p.content), p.tally, c, { service, what: p === pass ? 'pass' : 'earlier-pass' }, false);
137
+ for (const b of under.breaches) if (!breaches.some((x) => x.code === b.code)) breaches.push(b);
138
+ }
139
+ if (pass.notAllowed) breaches.push({ code: 'pass-not-allowed', message: 'The slip does not allow the pass this action would be taken under.' });
140
+ }
141
+
142
+ // The person's approval, checked as the checker will check it.
143
+ if (proposal.approval !== undefined) {
144
+ const ids = new Set(state.ids);
145
+ ids.approvals = new Set(state.ids.approvals);
146
+ const without = Array.isArray(options.withoutMethods) ? options.withoutMethods : [];
147
+ const approval = await checkApprovalRecord(proposal.approval, c, proposal.slip, slip, ids, without);
148
+ if (approval.state !== 'valid') throw notSound('This device could not check the approval\'s signature.');
149
+ if (parseTime(approval.when) > when + 300 * 1000) {
150
+ throw new Refusal('approval-dated-after-stub', 'The approval is dated later than the action.');
151
+ }
152
+ } else if (shown.needs.includes('approval')) {
153
+ breaches.push({ code: 'approval-missing', message: 'The slip asks for the person\'s own approval of this action, and none was handed to this check.' });
154
+ }
155
+ // A countersignature cannot be known beforehand. But where the action
156
+ // names no service, or one for which the slip gives no keys, none can
157
+ // ever be given.
158
+ if (shown.needs.includes('countersignature') && !keysOf(service)) {
159
+ breaches.push({
160
+ code: 'countersignature-missing',
161
+ message: 'The slip asks for the other side to countersign this action, and the action names no service whose keys the slip gives. No countersignature could be given.',
162
+ });
163
+ }
164
+
165
+ answer.breaches = breaches;
166
+ answer.allowed = breaches.length === 0;
167
+ } catch (e) {
168
+ answer.problems.push(problemFrom(e));
169
+ }
170
+ return answer;
171
+ }
package/src/headers.js ADDED
@@ -0,0 +1,380 @@
1
+ // A chain of block headers, checked on this device: the means to know,
2
+ // without asking anyone, that a block which a block time-stamp leads to is
3
+ // part of the Bitcoin blockchain.
4
+ //
5
+ // Nothing here makes a request. The headers come from a file the person
6
+ // has; the command provared-headers fetches them from Bitcoin nodes the
7
+ // person names (net/headers.js), and nothing else in this library does.
8
+ //
9
+ // Each header is checked against the one before it, by the rules every
10
+ // Bitcoin node applies to headers: it names the block before it; its
11
+ // double SHA-256 meets the target written in it; that target follows the
12
+ // difficulty rule (unchanged within a run of 2,016 blocks, recomputed at
13
+ // the start of each run from the time the run before it took); its time is
14
+ // later than the middle of the eleven before it, and not more than two
15
+ // hours ahead of this device's clock. The first header must be the
16
+ // chain's own first block, and the chain must hold the blocks listed
17
+ // below at their places. Those known blocks are what stops a chain that
18
+ // branches off early and keeps its difficulty low: such a chain would have
19
+ // to hold each of them, which no one can make without the real chain's
20
+ // work.
21
+
22
+ import { Refusal, formatTime } from './encoding.js';
23
+
24
+ /** The bytes of one block header. */
25
+ export const HEADER_BYTES = 80;
26
+
27
+ /** A block counts as part of the chain only with at least this many blocks after it. */
28
+ export const MIN_BLOCKS_AFTER = 6;
29
+
30
+ /** The most headers a chain file may hold: room for the chain to about the year 2045. */
31
+ export const MAX_HEADERS = 2_000_000;
32
+
33
+ // Node.js's own SHA-256 is far faster than Web Crypto for a million short
34
+ // inputs; it is reached without an import, so this file still loads in a
35
+ // browser, where Web Crypto is used.
36
+ const nodeCrypto = globalThis.process && typeof globalThis.process.getBuiltinModule === 'function' ? globalThis.process.getBuiltinModule('node:crypto') : null;
37
+
38
+ async function doubleSha256(bytes, webCrypto = false) {
39
+ if (nodeCrypto && !webCrypto) {
40
+ const once = nodeCrypto.createHash('sha256').update(bytes).digest();
41
+ return new Uint8Array(nodeCrypto.createHash('sha256').update(once).digest());
42
+ }
43
+ const once = await globalThis.crypto.subtle.digest('SHA-256', bytes);
44
+ return new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', once));
45
+ }
46
+
47
+ const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
48
+ const fromHex = (text) => new Uint8Array(text.match(/../g).map((pair) => parseInt(pair, 16)));
49
+ /** A block's fingerprint as it is written everywhere: the hash's bytes in reverse order, in hex. */
50
+ const written = (hash) => hex(Uint8Array.from(hash).reverse());
51
+
52
+ /**
53
+ * The rules of the Bitcoin chain that headers are checked by. The known
54
+ * blocks were read on 5 October 2026 from the whole chain of headers, which
55
+ * was checked by these rules from the first block on this computer, and
56
+ * which a second node on another network agreed with.
57
+ */
58
+ export const BITCOIN = Object.freeze({
59
+ first: '0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c',
60
+ // The easiest target a block may have.
61
+ limit: (1n << 224n) - 1n,
62
+ // The difficulty is recomputed every 2,016 blocks, aiming at two weeks for each run.
63
+ interval: 2016,
64
+ timespan: 14 * 24 * 60 * 60,
65
+ retarget: true,
66
+ // From these blocks on, a header's version must be at least this (the
67
+ // upgrades of 2013 and 2015 that every node enforces).
68
+ versions: Object.freeze([
69
+ [227931, 2],
70
+ [363725, 3],
71
+ [388381, 4],
72
+ ]),
73
+ // After the last known block, no target may be more than four times
74
+ // easier than that block's. Every change of difficulty in the real chain
75
+ // so far eased it by less than one and a half times; a branch made after
76
+ // the last known block, with dates pushed forward to ease it at every
77
+ // change, is held to work no less than a quarter of the real chain's then.
78
+ ease: 4n,
79
+ known: Object.freeze([
80
+ [100000, '000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506'],
81
+ [200000, '000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf'],
82
+ [300000, '000000000000000082ccf8f1557c5d40b21edabb18d2d691cfbf87118bac7254'],
83
+ [400000, '000000000000000004ec466ce4732fe6f1ed1cddc2ed4b328fff5224276e3f6f'],
84
+ [500000, '00000000000000000024fb37364cbf81fd49cc2d51c09c75c35433c3a1945d04'],
85
+ [600000, '00000000000000000007316856900e76b4f7a9139cfbfba89842c8d196cd5f91'],
86
+ [700000, '0000000000000000000590fc0f3eba193a278534220b2b37e9849e1a770ca959'],
87
+ [800000, '00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72728a054'],
88
+ [900000, '000000000000000000010538edbfd2d5b809a33dd83f284aeea41c6d0d96968a'],
89
+ [969696, '000000000000000000001e39df127cbab82a824ac43cfcdbf62e59f6a08b9f0b'],
90
+ ]),
91
+ });
92
+
93
+ const invalid = (message) => new Refusal('headers-invalid', message);
94
+
95
+ /** The target that a compact "bits" value stands for. */
96
+ function targetOf(bits) {
97
+ if (bits & 0x00800000) return -1n; // a negative target: no hash meets it
98
+ const exponent = bits >>> 24;
99
+ const mantissa = BigInt(bits & 0x007fffff);
100
+ return exponent <= 3 ? mantissa >> BigInt(8 * (3 - exponent)) : mantissa << BigInt(8 * (exponent - 3));
101
+ }
102
+
103
+ /** The compact "bits" value of a target, as Bitcoin writes it. */
104
+ function compactOf(target) {
105
+ let size = Math.ceil(target.toString(16).length / 2);
106
+ let c = size <= 3 ? Number(target << BigInt(8 * (3 - size))) : Number(target >> BigInt(8 * (size - 3)));
107
+ if (c & 0x00800000) {
108
+ c >>>= 8;
109
+ size++;
110
+ }
111
+ return (c | (size << 24)) >>> 0;
112
+ }
113
+
114
+ /**
115
+ * The "bits" at the start of a run: the target of the last block, scaled by
116
+ * how long the run took against how long it should, by at most four times
117
+ * either way, and never easier than the limit. Not part of the public
118
+ * interface.
119
+ * @param {number} bits the last block's bits
120
+ * @param {number} firstTime the time of the run's first block, in seconds
121
+ * @param {number} lastTime the time of the run's last block, in seconds
122
+ * @param {object} [rules]
123
+ */
124
+ export function nextBits(bits, firstTime, lastTime, rules = BITCOIN) {
125
+ const span = Math.min(Math.max(lastTime - firstTime, rules.timespan / 4), rules.timespan * 4);
126
+ let target = (targetOf(bits) * BigInt(Math.floor(span))) / BigInt(rules.timespan);
127
+ if (target > rules.limit) target = rules.limit;
128
+ return compactOf(target);
129
+ }
130
+
131
+ /**
132
+ * A chain of headers being read, one header at a time. Not part of the
133
+ * public interface: checkHeaderChain reads a whole file with it, and the
134
+ * command provared-headers adds what it fetches.
135
+ * @param {object} [rules]
136
+ */
137
+ export function startHeaderChain(rules = BITCOIN, { webCrypto = false } = {}) {
138
+ let room = 1024;
139
+ let headers = new Uint8Array(room * HEADER_BYTES);
140
+ let hashes = new Uint8Array(room * 32);
141
+ let count = 0;
142
+ const known = new Map(rules.known.map(([height, fingerprint]) => [height, fingerprint]));
143
+ let view = new DataView(headers.buffer);
144
+ const timeAt = (h) => view.getUint32(h * HEADER_BYTES + 68, true);
145
+ const bitsAt = (h) => view.getUint32(h * HEADER_BYTES + 72, true);
146
+ const hashAt = (h) => hashes.subarray(h * 32, h * 32 + 32);
147
+ // The target of the bits last seen, as 32 bytes, most significant first,
148
+ // so that a hash is compared with it byte by byte.
149
+ let targetBits = -1;
150
+ let targetBytes = null;
151
+ const meets = (hash, bits) => {
152
+ if (bits !== targetBits) {
153
+ const target = targetOf(bits);
154
+ targetBits = bits;
155
+ targetBytes = target <= 0n || target > rules.limit ? null : fromHex(target.toString(16).padStart(64, '0'));
156
+ }
157
+ if (targetBytes === null) return false;
158
+ // The hash is a number written least significant byte first.
159
+ for (let i = 0; i < 32; i++) {
160
+ const a = hash[31 - i];
161
+ const b = targetBytes[i];
162
+ if (a !== b) return a < b;
163
+ }
164
+ return true;
165
+ };
166
+ // The last known block, and its target once the chain has reached it.
167
+ const lastKnown = rules.known.length ? rules.known.at(-1)[0] : -1;
168
+ let floor = null;
169
+ const versions = rules.versions ?? [];
170
+ const grow = () => {
171
+ room *= 2;
172
+ const moreHeaders = new Uint8Array(room * HEADER_BYTES);
173
+ moreHeaders.set(headers.subarray(0, count * HEADER_BYTES));
174
+ const moreHashes = new Uint8Array(room * 32);
175
+ moreHashes.set(hashes.subarray(0, count * 32));
176
+ headers = moreHeaders;
177
+ hashes = moreHashes;
178
+ view = new DataView(headers.buffer);
179
+ };
180
+ const chain = {
181
+ rules,
182
+ /** The number of the latest block held; -1 before the first. */
183
+ get height() {
184
+ return count - 1;
185
+ },
186
+ /**
187
+ * Check one header as the next of the chain, and add it.
188
+ * @param {Uint8Array} header 80 bytes
189
+ * @param {number} now this device's clock, in milliseconds
190
+ */
191
+ async add(header, now) {
192
+ const h = count;
193
+ if (!(header instanceof Uint8Array) || header.length !== HEADER_BYTES) throw invalid(`Block ${h}: a header must be 80 bytes.`);
194
+ if (h >= MAX_HEADERS) throw invalid(`The chain holds more than ${MAX_HEADERS} headers.`);
195
+ const hash = await doubleSha256(header, webCrypto);
196
+ const fields = new DataView(header.buffer, header.byteOffset, HEADER_BYTES);
197
+ if (h === 0) {
198
+ if (hex(header) !== rules.first) throw invalid('Block 0: this is not the chain\'s first block.');
199
+ } else {
200
+ const previous = hashAt(h - 1);
201
+ for (let i = 0; i < 32; i++) if (header[4 + i] !== previous[i]) throw invalid(`Block ${h}: it does not name the block before it.`);
202
+ const bits = fields.getUint32(72, true);
203
+ const expected = rules.retarget && h % rules.interval === 0 ? nextBits(bitsAt(h - 1), timeAt(h - rules.interval), timeAt(h - 1), rules) : bitsAt(h - 1);
204
+ if (bits !== expected) throw invalid(`Block ${h}: its difficulty does not follow the rule.`);
205
+ if (!meets(hash, bits)) throw invalid(`Block ${h}: its hash does not meet its target, so it does not carry the work it claims.`);
206
+ if (floor !== null && h > lastKnown && targetOf(bits) > floor * (rules.ease ?? 4n)) {
207
+ throw invalid(`Block ${h}: its target is more than four times easier than that of block ${lastKnown}, the last block known to be part of the chain.`);
208
+ }
209
+ const version = fields.getInt32(0, true);
210
+ for (const [from, least] of versions) if (h >= from && version < least) throw invalid(`Block ${h}: its version is lower than every block from ${from} on must have.`);
211
+ const time = fields.getUint32(68, true);
212
+ const before = [];
213
+ for (let k = Math.max(0, h - 11); k < h; k++) before.push(timeAt(k));
214
+ before.sort((a, b) => a - b);
215
+ if (time <= before[before.length >> 1]) throw invalid(`Block ${h}: its time is not later than the middle of the eleven blocks before it.`);
216
+ if (time * 1000 > now + 7200 * 1000) throw invalid(`Block ${h}: its time is more than two hours ahead of this device's clock.`);
217
+ }
218
+ if (known.has(h) && written(hash) !== known.get(h)) throw invalid(`Block ${h}: it is not the block known to be at that place in the chain.`);
219
+ if (h === lastKnown) floor = targetOf(fields.getUint32(72, true));
220
+ if (h >= room) grow();
221
+ headers.set(header, h * HEADER_BYTES);
222
+ hashes.set(hash, h * 32);
223
+ count++;
224
+ },
225
+ /** Keep the blocks up to and including this one, where another chain parts from this one near its end. */
226
+ keepTo(height) {
227
+ if (height < 0 || height >= count) throw new RangeError('no such block');
228
+ count = height + 1;
229
+ if (height < lastKnown) floor = null;
230
+ },
231
+ /** A copy that shares nothing with this chain, to try another node's headers on. */
232
+ copy() {
233
+ const other = startHeaderChain(rules, { webCrypto });
234
+ other[INSIDE].set({ headers: headers.slice(0, Math.max(count, 1) * HEADER_BYTES), hashes: hashes.slice(0, Math.max(count, 1) * 32), count, floor });
235
+ return other;
236
+ },
237
+ /** Take on what another chain holds: a branch that carries more work. */
238
+ adopt(other) {
239
+ const state = other[INSIDE].get();
240
+ headers = state.headers.slice();
241
+ hashes = state.hashes.slice();
242
+ room = headers.length / HEADER_BYTES;
243
+ view = new DataView(headers.buffer);
244
+ count = state.count;
245
+ floor = state.floor;
246
+ targetBits = -1;
247
+ },
248
+ /** The work of the blocks after this one, to the latest: the number of hashes their targets ask for, together. */
249
+ workAfter(height) {
250
+ let work = 0n;
251
+ for (let h = height + 1; h < count; h++) {
252
+ const target = targetOf(bitsAt(h));
253
+ if (target > 0n) work += (1n << 256n) / (target + 1n);
254
+ }
255
+ return work;
256
+ },
257
+ /** The fingerprint of a block, 64 hex characters. */
258
+ fingerprintAt(h) {
259
+ return written(hashAt(h));
260
+ },
261
+ /** The hash of a block, as it is named inside the next header and in a request for headers. */
262
+ hashAt(h) {
263
+ return Uint8Array.from(hashAt(h));
264
+ },
265
+ /** The time a block states, in milliseconds. */
266
+ timeAt(h) {
267
+ return timeAt(h) * 1000;
268
+ },
269
+ /** The number of the block with this fingerprint, or -1 where the chain does not hold it. */
270
+ heightOf(fingerprint) {
271
+ if (typeof fingerprint !== 'string' || !/^[0-9a-f]{64}$/.test(fingerprint)) return -1;
272
+ const wanted = fromHex(fingerprint).reverse();
273
+ outer: for (let h = count - 1; h >= 0; h--) {
274
+ const at = h * 32;
275
+ for (let i = 31; i >= 0; i--) if (hashes[at + i] !== wanted[i]) continue outer;
276
+ return h;
277
+ }
278
+ return -1;
279
+ },
280
+ /** Every header held, one after another, as a chain file holds them. */
281
+ bytes() {
282
+ return headers.slice(0, count * HEADER_BYTES);
283
+ },
284
+ [INSIDE]: {
285
+ get: () => ({ headers: headers.subarray(0, count * HEADER_BYTES), hashes: hashes.subarray(0, count * 32), count, floor }),
286
+ set: (state) => {
287
+ headers = state.headers;
288
+ hashes = state.hashes;
289
+ room = Math.max(1, headers.length / HEADER_BYTES);
290
+ view = new DataView(headers.buffer);
291
+ count = state.count;
292
+ floor = state.floor;
293
+ targetBits = -1;
294
+ },
295
+ },
296
+ };
297
+ return chain;
298
+ }
299
+
300
+ // What one chain hands another, and nothing else can reach.
301
+ const INSIDE = Symbol('inside');
302
+
303
+ /**
304
+ * Read a whole chain file into a chain being read. Not part of the public
305
+ * interface. Throws a Refusal "headers-invalid" at the first header that
306
+ * breaks a rule. A chain that ends before the last known block is refused,
307
+ * unless "partial": the command that fetches headers carries such a chain
308
+ * on; nothing may count a block in it.
309
+ * @param {Uint8Array} bytes
310
+ * @param {{now?: number, rules?: object, partial?: boolean}} [o]
311
+ */
312
+ export async function readHeaderChain(bytes, { now = Date.now(), rules = BITCOIN, partial = false, webCrypto = false } = {}) {
313
+ if (!(bytes instanceof Uint8Array) || bytes.length === 0 || bytes.length % HEADER_BYTES !== 0) {
314
+ throw invalid('A chain file holds whole headers of 80 bytes each, one after another, from the first block.');
315
+ }
316
+ if (bytes.length / HEADER_BYTES > MAX_HEADERS) throw invalid(`The chain holds more than ${MAX_HEADERS} headers.`);
317
+ const chain = startHeaderChain(rules, { webCrypto });
318
+ for (let at = 0; at < bytes.length; at += HEADER_BYTES) await chain.add(bytes.subarray(at, at + HEADER_BYTES), now);
319
+ const last = rules.known.at(-1);
320
+ if (!partial && last && chain.height < last[0]) throw invalid(`The chain ends at block ${chain.height}, before block ${last[0]}, which is known to be part of it. Fetch more headers.`);
321
+ return chain;
322
+ }
323
+
324
+ /**
325
+ * Check a chain of Bitcoin block headers held in a file: every header, from
326
+ * the chain's first block, by the rules above. Nothing is asked of anyone.
327
+ *
328
+ * @param {Uint8Array} bytes the headers, 80 bytes each, one after another, from the first block
329
+ * @param {{now?: number}} [o] this device's clock, in milliseconds; by default the device's own
330
+ * @returns {Promise<{height: number, tip: string, tipWhen: string, find: (fingerprint: string) => {height: number, after: number} | null}>}
331
+ * the number and fingerprint of the latest block, the time it states, and a way to find a block:
332
+ * its number, and how many blocks come after it in this chain
333
+ * @throws {Refusal} "headers-invalid", at the first header that breaks a rule
334
+ */
335
+ export async function checkHeaderChain(bytes, { now = Date.now() } = {}) {
336
+ return answerFor(await readHeaderChain(bytes, { now }));
337
+ }
338
+
339
+ /**
340
+ * What checkHeaderChain gives, for a chain that has been read. Not part of
341
+ * the public interface.
342
+ * @param {ReturnType<typeof startHeaderChain>} chain
343
+ */
344
+ export function answerFor(chain) {
345
+ const height = chain.height;
346
+ return {
347
+ height,
348
+ tip: chain.fingerprintAt(height),
349
+ tipWhen: formatTime(chain.timeAt(height)),
350
+ find(fingerprint) {
351
+ const at = chain.heightOf(fingerprint);
352
+ return at < 0 ? null : { height: at, after: height - at };
353
+ },
354
+ };
355
+ }
356
+
357
+ /**
358
+ * The blocks a check's block time-stamps lead to that a chain of headers
359
+ * holds with at least MIN_BLOCKS_AFTER blocks after them: the blocks a
360
+ * person who checked that chain can name as trusted. Each block
361
+ * time-stamp the result shows is looked for, with what was found.
362
+ *
363
+ * @param {object} result what checkBook or checkShow returned
364
+ * @param {{find: Function}} chain what checkHeaderChain returned
365
+ * @returns {{blocks: string[], found: {block: string, stated: number, height: number|null, after: number|null, counted: boolean}[]}}
366
+ */
367
+ export function blocksInChain(result, chain) {
368
+ const stamps = [];
369
+ for (const e of result.entries ?? []) stamps.push(...(e.stamps ?? []));
370
+ for (const h of result.held ?? []) stamps.push(...(h.stamps ?? []));
371
+ const found = [];
372
+ const seen = new Set();
373
+ for (const s of stamps) {
374
+ if (s.kind !== 'block' || typeof s.authority !== 'string' || seen.has(s.authority)) continue;
375
+ seen.add(s.authority);
376
+ const at = chain.find(s.authority);
377
+ found.push({ block: s.authority, stated: s.height, height: at ? at.height : null, after: at ? at.after : null, counted: at !== null && at.after >= MIN_BLOCKS_AFTER });
378
+ }
379
+ return { blocks: found.filter((f) => f.counted).map((f) => f.block), found };
380
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Type declarations for the whole library. See types.d.ts.
2
+ export * from './types.js';
package/src/index.js ADDED
@@ -0,0 +1,37 @@
1
+ // Provared, draft version 0: proof of what an AI agent was allowed to do,
2
+ // and what it then did. Evidence, not a verdict.
3
+ //
4
+ // Checking: checkBook, checkShow, checkSlip
5
+ // openChecker (a checker that is carried on as the book grows)
6
+ // checkBefore (would this action be outside the slip?)
7
+ // Beside an agent: openRecorder (ask first, act, write the stub),
8
+ // recordTools (the agent's tools, each behind the stub writer)
9
+ // Writing: prepareSlip + assembleSlip (the person, with a passkey),
10
+ // prepareApproval + assembleApproval (the person's yes to one action),
11
+ // writeStub (the agent), countersign, writeRefusal, writeTerms
12
+ // (the service), writeSeal (whoever keeps the book), blockStampItem
13
+ // (a proof from a public blockchain, to put beside a seal),
14
+ // prepareCancellation + assembleCancellation (the person ends a slip),
15
+ // writeAcknowledgement (the agent's side says it was handed a cancellation),
16
+ // writeVouching, writeWithdrawal (an organisation vouches for a name),
17
+ // writePass (an agent hands part of its permission to a helper agent),
18
+ // writeBook, makeShow
19
+ // Keys: generateKeySet, generateSealKeySet, thumbprint, keySetFingerprint
20
+ // Names: the shared list of actions, the kinds of action, the rules of conduct
21
+
22
+ export * from './check.js';
23
+ export { checkBefore } from './guard.js';
24
+ export { openRecorder } from './recorder.js';
25
+ export { recordTools, argumentsFingerprint, NotTaken } from './tools.js';
26
+ export { prepareSlip, assembleSlip, COVERABLE } from './slip.js';
27
+ export { prepareApproval, assembleApproval } from './approval.js';
28
+ export { writeStub, countersign } from './stub.js';
29
+ export { writeRefusal, writeTerms } from './service.js';
30
+ export { writePass } from './pass.js';
31
+ export { writeSeal } from './seal.js';
32
+ export { blockStampItem } from './blockstamp.js';
33
+ export { prepareCancellation, assembleCancellation, writeAcknowledgement, writeVouching, writeWithdrawal, VOUCHED_KINDS } from './standing.js';
34
+ export { entryLine, writeBook, makeShow } from './book.js';
35
+ export { generateKeySet, generateSealKeySet } from './signatures.js';
36
+ export { ACTION_KINDS, SHARED_ACTIONS, CONDUCT_RULES, RESERVED_PREFIX, actionKind } from './actions.js';
37
+ export { fingerprint, formatTime, randomId, toBase64url, fromBase64url, utf8, sha256 } from './encoding.js';