@totemsdk/wots-lease 1.0.1 → 1.0.2
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/LICENSE +202 -21
- package/NOTICE +60 -0
- package/dist/axia.js +21 -25
- package/dist/certificate.js +7 -12
- package/dist/cjs/axia.d.ts +33 -0
- package/dist/cjs/axia.js +145 -0
- package/dist/cjs/certificate.d.ts +37 -0
- package/dist/cjs/certificate.js +74 -0
- package/dist/cjs/device.d.ts +18 -0
- package/dist/cjs/device.js +34 -0
- package/dist/cjs/errors.d.ts +71 -0
- package/dist/cjs/errors.js +135 -0
- package/dist/cjs/hybrid.d.ts +37 -0
- package/dist/cjs/hybrid.js +145 -0
- package/dist/cjs/index.d.ts +17 -0
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/journal.d.ts +30 -0
- package/dist/cjs/journal.js +369 -0
- package/dist/cjs/local.d.ts +83 -0
- package/dist/cjs/local.js +440 -0
- package/dist/cjs/onchain.d.ts +54 -0
- package/dist/cjs/onchain.js +314 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/quorum.d.ts +75 -0
- package/dist/cjs/quorum.js +307 -0
- package/dist/cjs/replicated-backup.d.ts +30 -0
- package/dist/cjs/replicated-backup.js +52 -0
- package/dist/cjs/seed.d.ts +41 -0
- package/dist/cjs/seed.js +84 -0
- package/dist/cjs/stubs.d.ts +50 -0
- package/dist/cjs/stubs.js +125 -0
- package/dist/cjs/types.d.ts +272 -0
- package/dist/cjs/types.js +5 -0
- package/dist/cjs/watermark.d.ts +51 -0
- package/dist/cjs/watermark.js +328 -0
- package/dist/device.js +2 -6
- package/dist/errors.js +14 -31
- package/dist/hybrid.js +4 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -43
- package/dist/journal.js +35 -38
- package/dist/local.js +29 -33
- package/dist/onchain.js +23 -27
- package/dist/quorum.js +9 -13
- package/dist/replicated-backup.d.ts +30 -0
- package/dist/replicated-backup.js +48 -0
- package/dist/seed.js +7 -11
- package/dist/stubs.js +5 -11
- package/dist/types.js +1 -2
- package/dist/watermark.js +11 -18
- package/package.json +22 -13
- package/rust/pkg/package.json +2 -2
- package/rust/pkg/wots_lease_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +2 -2
- package/rust/pkg-node/wots_lease_wasm_bg.wasm +0 -0
package/dist/journal.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* LeaseJournal — physically append-only audit log for WOTS key-use events.
|
|
4
3
|
*
|
|
@@ -6,11 +5,10 @@
|
|
|
6
5
|
* key is retained as migration input/forensic history but is never rewritten.
|
|
7
6
|
* The SHA-256 chain format is intentionally unchanged (RFC-007 OQ3).
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const errors_1 = require("@totemsdk/storage/errors");
|
|
8
|
+
import { sha256 } from '@noble/hashes/sha256.js';
|
|
9
|
+
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
10
|
+
import { NoopLogger } from '@totemsdk/core';
|
|
11
|
+
import { StorageError } from '@totemsdk/storage/errors';
|
|
14
12
|
const LEGACY_STORAGE_KEY = 'totem_wots_journal';
|
|
15
13
|
const ENTRY_PREFIX = 'totem_wots_journal:v1:entry:';
|
|
16
14
|
const RECORD_VERSION = 1;
|
|
@@ -45,7 +43,7 @@ function hashEntry(entry) {
|
|
|
45
43
|
d: entry.deviceId,
|
|
46
44
|
};
|
|
47
45
|
const bytes = encoder.encode(JSON.stringify(data));
|
|
48
|
-
return (
|
|
46
|
+
return bytesToHex(sha256(bytes));
|
|
49
47
|
}
|
|
50
48
|
function cloneEntry(entry) {
|
|
51
49
|
return {
|
|
@@ -65,14 +63,14 @@ function cloneEntry(entry) {
|
|
|
65
63
|
}
|
|
66
64
|
function asObject(value, key) {
|
|
67
65
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
68
|
-
throw new
|
|
66
|
+
throw new StorageError(`LeaseJournal: invalid object at ${key}`, 'corrupt', { key });
|
|
69
67
|
}
|
|
70
68
|
return value;
|
|
71
69
|
}
|
|
72
70
|
function requiredString(object, field, key) {
|
|
73
71
|
const value = object[field];
|
|
74
72
|
if (typeof value !== 'string' || value.length === 0) {
|
|
75
|
-
throw new
|
|
73
|
+
throw new StorageError(`LeaseJournal: invalid ${field} at ${key}`, 'corrupt', { key });
|
|
76
74
|
}
|
|
77
75
|
return value;
|
|
78
76
|
}
|
|
@@ -81,14 +79,14 @@ function optionalString(object, field, key) {
|
|
|
81
79
|
if (value === undefined)
|
|
82
80
|
return undefined;
|
|
83
81
|
if (typeof value !== 'string') {
|
|
84
|
-
throw new
|
|
82
|
+
throw new StorageError(`LeaseJournal: invalid ${field} at ${key}`, 'corrupt', { key });
|
|
85
83
|
}
|
|
86
84
|
return value;
|
|
87
85
|
}
|
|
88
86
|
function requiredInteger(object, field, key) {
|
|
89
87
|
const value = object[field];
|
|
90
88
|
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
91
|
-
throw new
|
|
89
|
+
throw new StorageError(`LeaseJournal: invalid ${field} at ${key}`, 'corrupt', { key });
|
|
92
90
|
}
|
|
93
91
|
return value;
|
|
94
92
|
}
|
|
@@ -97,11 +95,11 @@ function validateEntry(value, key) {
|
|
|
97
95
|
const indices = asObject(object.indices, `${key}.indices`);
|
|
98
96
|
const status = requiredString(object, 'status', key);
|
|
99
97
|
if (!STATUSES.has(status)) {
|
|
100
|
-
throw new
|
|
98
|
+
throw new StorageError(`LeaseJournal: invalid status at ${key}`, 'corrupt', { key });
|
|
101
99
|
}
|
|
102
100
|
const timestamp = object.timestamp;
|
|
103
101
|
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp < 0) {
|
|
104
|
-
throw new
|
|
102
|
+
throw new StorageError(`LeaseJournal: invalid timestamp at ${key}`, 'corrupt', { key });
|
|
105
103
|
}
|
|
106
104
|
const validated = cloneEntry({
|
|
107
105
|
treeId: requiredString(object, 'treeId', key),
|
|
@@ -123,11 +121,11 @@ function validateEntry(value, key) {
|
|
|
123
121
|
});
|
|
124
122
|
const { addressIndex, l1, l2 } = validated.indices;
|
|
125
123
|
if (addressIndex > MAX_INDEX_COMPONENT || l1 > MAX_INDEX_COMPONENT || l2 > MAX_INDEX_COMPONENT) {
|
|
126
|
-
throw new
|
|
124
|
+
throw new StorageError(`LeaseJournal: indices out of range at ${key}`, 'corrupt', { key });
|
|
127
125
|
}
|
|
128
126
|
const expectedIndex = addressIndex * 64 * 64 + l1 * 64 + l2;
|
|
129
127
|
if (validated.wotsIndex !== expectedIndex) {
|
|
130
|
-
throw new
|
|
128
|
+
throw new StorageError(`LeaseJournal: wotsIndex/indices mismatch at ${key}`, 'corrupt', { key });
|
|
131
129
|
}
|
|
132
130
|
return validated;
|
|
133
131
|
}
|
|
@@ -135,19 +133,19 @@ function verifyChain(entries, source) {
|
|
|
135
133
|
for (let i = 0; i < entries.length; i += 1) {
|
|
136
134
|
const entry = entries[i];
|
|
137
135
|
if (entry.hash === undefined || !HASH_PATTERN.test(entry.hash)) {
|
|
138
|
-
throw new
|
|
136
|
+
throw new StorageError(`LeaseJournal: invalid hash at ${source}[${i}]`, 'corrupt');
|
|
139
137
|
}
|
|
140
138
|
const expectedHash = hashEntry(entry);
|
|
141
139
|
if (entry.hash !== expectedHash) {
|
|
142
|
-
throw new
|
|
140
|
+
throw new StorageError(`LeaseJournal: hash mismatch at ${source}[${i}]`, 'corrupt');
|
|
143
141
|
}
|
|
144
142
|
if (i === 0) {
|
|
145
143
|
if (entry.previousHash !== undefined) {
|
|
146
|
-
throw new
|
|
144
|
+
throw new StorageError(`LeaseJournal: first entry has previousHash at ${source}[0]`, 'corrupt');
|
|
147
145
|
}
|
|
148
146
|
}
|
|
149
147
|
else if (entry.previousHash !== entries[i - 1].hash) {
|
|
150
|
-
throw new
|
|
148
|
+
throw new StorageError(`LeaseJournal: previousHash mismatch at ${source}[${i}]`, 'corrupt');
|
|
151
149
|
}
|
|
152
150
|
}
|
|
153
151
|
}
|
|
@@ -177,16 +175,16 @@ function parseRecordKey(key) {
|
|
|
177
175
|
const relative = key.slice(ENTRY_PREFIX.length);
|
|
178
176
|
const match = KEY_PATTERN.exec(relative);
|
|
179
177
|
if (!match) {
|
|
180
|
-
throw new
|
|
178
|
+
throw new StorageError(`LeaseJournal: malformed record key ${key}`, 'corrupt', { key });
|
|
181
179
|
}
|
|
182
180
|
const sequence = Number(match[1]);
|
|
183
181
|
if (!Number.isSafeInteger(sequence)) {
|
|
184
|
-
throw new
|
|
182
|
+
throw new StorageError(`LeaseJournal: invalid sequence in ${key}`, 'corrupt', { key });
|
|
185
183
|
}
|
|
186
184
|
return { key, sequence, hash: match[2] };
|
|
187
185
|
}
|
|
188
|
-
class LeaseJournal {
|
|
189
|
-
constructor(storage, logger = new
|
|
186
|
+
export class LeaseJournal {
|
|
187
|
+
constructor(storage, logger = new NoopLogger()) {
|
|
190
188
|
this.storage = storage;
|
|
191
189
|
this.logger = logger;
|
|
192
190
|
this.entries = [];
|
|
@@ -212,7 +210,7 @@ class LeaseJournal {
|
|
|
212
210
|
const commonLength = Math.min(physical.length, legacy.length);
|
|
213
211
|
for (let i = 0; i < commonLength; i += 1) {
|
|
214
212
|
if (!entriesEqual(physical[i], legacy[i])) {
|
|
215
|
-
throw new
|
|
213
|
+
throw new StorageError(`LeaseJournal: legacy/v1 divergence at sequence ${i}`, 'corrupt');
|
|
216
214
|
}
|
|
217
215
|
}
|
|
218
216
|
for (let i = physical.length; i < legacy.length; i += 1) {
|
|
@@ -230,7 +228,7 @@ class LeaseJournal {
|
|
|
230
228
|
if (raw === null)
|
|
231
229
|
return null;
|
|
232
230
|
if (!Array.isArray(raw)) {
|
|
233
|
-
throw new
|
|
231
|
+
throw new StorageError('LeaseJournal: legacy journal is not an array', 'corrupt', { key: LEGACY_STORAGE_KEY });
|
|
234
232
|
}
|
|
235
233
|
const entries = raw.map((entry, index) => validateEntry(entry, `${LEGACY_STORAGE_KEY}[${index}]`));
|
|
236
234
|
verifyChain(entries, LEGACY_STORAGE_KEY);
|
|
@@ -243,33 +241,33 @@ class LeaseJournal {
|
|
|
243
241
|
.sort((a, b) => a.sequence - b.sequence || a.key.localeCompare(b.key));
|
|
244
242
|
for (let i = 0; i < parsed.length; i += 1) {
|
|
245
243
|
if (i > 0 && parsed[i - 1].sequence === parsed[i].sequence) {
|
|
246
|
-
throw new
|
|
244
|
+
throw new StorageError(`LeaseJournal: fork at sequence ${parsed[i].sequence}`, 'corrupt');
|
|
247
245
|
}
|
|
248
246
|
if (parsed[i].sequence !== i) {
|
|
249
|
-
throw new
|
|
247
|
+
throw new StorageError(`LeaseJournal: sequence gap at ${i}`, 'corrupt');
|
|
250
248
|
}
|
|
251
249
|
}
|
|
252
250
|
const entries = [];
|
|
253
251
|
for (const descriptor of parsed) {
|
|
254
252
|
const raw = await this.storage.get(descriptor.key);
|
|
255
253
|
if (raw === null) {
|
|
256
|
-
throw new
|
|
254
|
+
throw new StorageError(`LeaseJournal: listed record is missing`, 'corrupt', { key: descriptor.key });
|
|
257
255
|
}
|
|
258
256
|
const record = asObject(raw, descriptor.key);
|
|
259
257
|
const formatVersion = record.formatVersion;
|
|
260
258
|
if (formatVersion !== RECORD_VERSION) {
|
|
261
|
-
throw new
|
|
259
|
+
throw new StorageError('LeaseJournal: unsupported record version', 'corrupt', {
|
|
262
260
|
key: descriptor.key,
|
|
263
261
|
detectedVersion: typeof formatVersion === 'number' ? formatVersion : undefined,
|
|
264
262
|
unsupportedVersion: typeof formatVersion === 'number' ? formatVersion : undefined,
|
|
265
263
|
});
|
|
266
264
|
}
|
|
267
265
|
if (record.sequence !== descriptor.sequence) {
|
|
268
|
-
throw new
|
|
266
|
+
throw new StorageError('LeaseJournal: sequence/key mismatch', 'corrupt', { key: descriptor.key });
|
|
269
267
|
}
|
|
270
268
|
const entry = validateEntry(record.entry, `${descriptor.key}.entry`);
|
|
271
269
|
if (entry.hash !== descriptor.hash) {
|
|
272
|
-
throw new
|
|
270
|
+
throw new StorageError('LeaseJournal: hash/key mismatch', 'corrupt', { key: descriptor.key });
|
|
273
271
|
}
|
|
274
272
|
entries.push(entry);
|
|
275
273
|
}
|
|
@@ -278,7 +276,7 @@ class LeaseJournal {
|
|
|
278
276
|
}
|
|
279
277
|
async writeRecord(sequence, entry) {
|
|
280
278
|
if (entry.hash === undefined) {
|
|
281
|
-
throw new
|
|
279
|
+
throw new StorageError(`LeaseJournal: entry ${sequence} has no hash`, 'corrupt');
|
|
282
280
|
}
|
|
283
281
|
const key = recordKey(sequence, entry.hash);
|
|
284
282
|
const record = {
|
|
@@ -301,14 +299,14 @@ class LeaseJournal {
|
|
|
301
299
|
this.entries = (await this.readPhysicalRecords()).map(cloneEntry);
|
|
302
300
|
const entry = validateEntry(cloneEntry(input), 'append');
|
|
303
301
|
if (entry.status === 'reserved' && this.entries.some((existing) => existing.treeId === entry.treeId && existing.wotsIndex === entry.wotsIndex)) {
|
|
304
|
-
throw new
|
|
302
|
+
throw new StorageError(`LeaseJournal: WOTS slot ${entry.treeId}/${entry.wotsIndex} is already recorded`, 'write-failed');
|
|
305
303
|
}
|
|
306
304
|
if (entry.status !== 'reserved' && entry.reservationId) {
|
|
307
305
|
const latest = [...this.entries].reverse().find((existing) => existing.reservationId === entry.reservationId);
|
|
308
306
|
if (latest?.status === entry.status) {
|
|
309
307
|
if (latest.treeId !== entry.treeId || latest.wotsIndex !== entry.wotsIndex
|
|
310
308
|
|| (entry.status === 'committed' && latest.txId !== entry.txId)) {
|
|
311
|
-
throw new
|
|
309
|
+
throw new StorageError(`LeaseJournal: reservation ${entry.reservationId} is already ${latest.status} for a different use`, 'write-failed');
|
|
312
310
|
}
|
|
313
311
|
if (latest.previousHash === undefined)
|
|
314
312
|
delete input.previousHash;
|
|
@@ -318,7 +316,7 @@ class LeaseJournal {
|
|
|
318
316
|
return;
|
|
319
317
|
}
|
|
320
318
|
if (latest && latest.status !== 'reserved') {
|
|
321
|
-
throw new
|
|
319
|
+
throw new StorageError(`LeaseJournal: reservation ${entry.reservationId} is already ${latest.status}`, 'write-failed');
|
|
322
320
|
}
|
|
323
321
|
}
|
|
324
322
|
const previous = this.entries[this.entries.length - 1];
|
|
@@ -330,7 +328,7 @@ class LeaseJournal {
|
|
|
330
328
|
const sequence = this.entries.length;
|
|
331
329
|
const sequencePrefix = `${ENTRY_PREFIX}${sequenceText(sequence)}:`;
|
|
332
330
|
if ((await this.storage.keys()).some((key) => key.startsWith(sequencePrefix))) {
|
|
333
|
-
throw new
|
|
331
|
+
throw new StorageError(`LeaseJournal: sequence ${sequence} already exists`, 'write-failed');
|
|
334
332
|
}
|
|
335
333
|
await this.writeRecord(sequence, entry);
|
|
336
334
|
this.entries.push(entry);
|
|
@@ -362,7 +360,6 @@ class LeaseJournal {
|
|
|
362
360
|
return entry ? cloneEntry(entry) : undefined;
|
|
363
361
|
}
|
|
364
362
|
async clear() {
|
|
365
|
-
throw new
|
|
363
|
+
throw new StorageError('LeaseJournal is append-only and cannot be cleared', 'write-failed');
|
|
366
364
|
}
|
|
367
365
|
}
|
|
368
|
-
exports.LeaseJournal = LeaseJournal;
|
package/dist/local.js
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* LocalLeaseProvider — Layer 1: local durable ledger.
|
|
4
3
|
*
|
|
5
4
|
* Single-device, no network coordination.
|
|
6
5
|
* All WOTS safety guaranteed by the local WotsWatermarkStore + LeaseJournal.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const journal_js_1 = require("./journal.js");
|
|
14
|
-
const errors_js_1 = require("./errors.js");
|
|
7
|
+
import { NoopLogger } from '@totemsdk/core';
|
|
8
|
+
import { LeaseStore } from '@totemsdk/core';
|
|
9
|
+
import { WotsWatermarkStore, flatIndex } from './watermark.js';
|
|
10
|
+
import { LeaseJournal } from './journal.js';
|
|
11
|
+
import { LeaseNotFoundError, InvalidLeaseTransitionError, DeviceRangeViolationError, IndicesUnavailableError, } from './errors.js';
|
|
15
12
|
function randomId() {
|
|
16
13
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
17
14
|
return crypto.randomUUID();
|
|
18
15
|
}
|
|
19
16
|
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
20
17
|
}
|
|
21
|
-
class LocalLeaseProvider {
|
|
22
|
-
constructor(storage, logger = new
|
|
18
|
+
export class LocalLeaseProvider {
|
|
19
|
+
constructor(storage, logger = new NoopLogger(), deviceId = 'local') {
|
|
23
20
|
this.logger = logger;
|
|
24
21
|
this._initialized = false;
|
|
25
22
|
this.initializePromise = null;
|
|
26
23
|
this.mutationTail = Promise.resolve();
|
|
27
|
-
this.watermark = new
|
|
28
|
-
this.leaseStore = new
|
|
29
|
-
this.journal = new
|
|
24
|
+
this.watermark = new WotsWatermarkStore(storage, logger);
|
|
25
|
+
this.leaseStore = new LeaseStore(storage, logger);
|
|
26
|
+
this.journal = new LeaseJournal(storage, logger);
|
|
30
27
|
this.deviceId = deviceId;
|
|
31
28
|
this.cas = LocalLeaseProvider.detectCas(storage);
|
|
32
29
|
}
|
|
@@ -96,7 +93,7 @@ class LocalLeaseProvider {
|
|
|
96
93
|
await this.journal.append({
|
|
97
94
|
treeId,
|
|
98
95
|
branchId: 'default',
|
|
99
|
-
wotsIndex:
|
|
96
|
+
wotsIndex: flatIndex(lease.indices),
|
|
100
97
|
indices: lease.indices,
|
|
101
98
|
status,
|
|
102
99
|
reservationId: lease.leaseId,
|
|
@@ -120,7 +117,7 @@ class LocalLeaseProvider {
|
|
|
120
117
|
await this.journal.append({
|
|
121
118
|
treeId,
|
|
122
119
|
branchId: 'default',
|
|
123
|
-
wotsIndex:
|
|
120
|
+
wotsIndex: flatIndex(lease.indices),
|
|
124
121
|
indices: lease.indices,
|
|
125
122
|
status: 'committed',
|
|
126
123
|
reservationId: lease.leaseId,
|
|
@@ -134,7 +131,7 @@ class LocalLeaseProvider {
|
|
|
134
131
|
await this.journal.append({
|
|
135
132
|
treeId,
|
|
136
133
|
branchId: 'default',
|
|
137
|
-
wotsIndex:
|
|
134
|
+
wotsIndex: flatIndex(lease.indices),
|
|
138
135
|
indices: lease.indices,
|
|
139
136
|
status: 'burned',
|
|
140
137
|
reservationId: lease.leaseId,
|
|
@@ -157,7 +154,7 @@ class LocalLeaseProvider {
|
|
|
157
154
|
await this.journal.append({
|
|
158
155
|
treeId: 'default',
|
|
159
156
|
branchId: 'default',
|
|
160
|
-
wotsIndex:
|
|
157
|
+
wotsIndex: flatIndex(lease.indices),
|
|
161
158
|
indices: lease.indices,
|
|
162
159
|
status: 'burned',
|
|
163
160
|
reservationId: lease.leaseId,
|
|
@@ -179,7 +176,7 @@ class LocalLeaseProvider {
|
|
|
179
176
|
await this.journal.append({
|
|
180
177
|
treeId,
|
|
181
178
|
branchId: 'default',
|
|
182
|
-
wotsIndex:
|
|
179
|
+
wotsIndex: flatIndex(lease.indices),
|
|
183
180
|
indices: lease.indices,
|
|
184
181
|
status: 'reserved-expired',
|
|
185
182
|
reservationId: lease.leaseId,
|
|
@@ -223,13 +220,13 @@ class LocalLeaseProvider {
|
|
|
223
220
|
const treeId = params.treeId;
|
|
224
221
|
const ttlMs = params.ttlMs ?? 120000;
|
|
225
222
|
if (this.watermark.isUnavailable(treeId, indices)) {
|
|
226
|
-
throw new
|
|
223
|
+
throw new IndicesUnavailableError(treeId, indices);
|
|
227
224
|
}
|
|
228
225
|
// Validate device range BEFORE marking unavailable — prevents permanent index loss
|
|
229
226
|
if (params.deviceId) {
|
|
230
227
|
const range = this.getDeviceRange(params.deviceId);
|
|
231
228
|
if (range && (indices.addressIndex < range.start || indices.addressIndex > range.end)) {
|
|
232
|
-
throw new
|
|
229
|
+
throw new DeviceRangeViolationError(indices.addressIndex, range.start, range.end);
|
|
233
230
|
}
|
|
234
231
|
}
|
|
235
232
|
const reservationId = randomId();
|
|
@@ -239,7 +236,7 @@ class LocalLeaseProvider {
|
|
|
239
236
|
// race and both pick the same slot. Claim it atomically first; a lost race
|
|
240
237
|
// means another instance owns it, so the slot is not re-allocated.
|
|
241
238
|
if (this.cas) {
|
|
242
|
-
const claimKey = `totem_wots_claim:${treeId}:${
|
|
239
|
+
const claimKey = `totem_wots_claim:${treeId}:${flatIndex(indices)}`;
|
|
243
240
|
const claim = await this.cas.conditionalUpdate(claimKey, (current) => current
|
|
244
241
|
? { abort: 'slot already claimed' }
|
|
245
242
|
: {
|
|
@@ -250,7 +247,7 @@ class LocalLeaseProvider {
|
|
|
250
247
|
},
|
|
251
248
|
});
|
|
252
249
|
if (!claim.applied) {
|
|
253
|
-
throw new
|
|
250
|
+
throw new IndicesUnavailableError(treeId, indices);
|
|
254
251
|
}
|
|
255
252
|
}
|
|
256
253
|
// Write-ahead safety record: once this succeeds, recovery will never
|
|
@@ -258,7 +255,7 @@ class LocalLeaseProvider {
|
|
|
258
255
|
await this.journal.append({
|
|
259
256
|
treeId,
|
|
260
257
|
branchId: params.branchId ?? 'default',
|
|
261
|
-
wotsIndex:
|
|
258
|
+
wotsIndex: flatIndex(indices),
|
|
262
259
|
indices,
|
|
263
260
|
status: 'reserved',
|
|
264
261
|
reservationId,
|
|
@@ -294,13 +291,13 @@ class LocalLeaseProvider {
|
|
|
294
291
|
await this.watermark.refresh();
|
|
295
292
|
const lease = this.leaseStore.get(reservationId);
|
|
296
293
|
if (!lease)
|
|
297
|
-
throw new
|
|
294
|
+
throw new LeaseNotFoundError(reservationId);
|
|
298
295
|
const treeId = lease.treeId ?? 'default';
|
|
299
296
|
const indices = lease.indices;
|
|
300
297
|
const latest = this.journal.getByReservation(reservationId);
|
|
301
298
|
if (latest?.status === 'committed') {
|
|
302
299
|
if (latest.txId !== txId) {
|
|
303
|
-
throw new
|
|
300
|
+
throw new InvalidLeaseTransitionError(reservationId, 'committed', 'committed');
|
|
304
301
|
}
|
|
305
302
|
await this.watermark.markUnavailable(treeId, indices, 'committed');
|
|
306
303
|
if (lease.status !== 'finalized')
|
|
@@ -309,12 +306,12 @@ class LocalLeaseProvider {
|
|
|
309
306
|
}
|
|
310
307
|
if (latest?.status === 'burned' || latest?.status === 'reserved-expired'
|
|
311
308
|
|| lease.status === 'cancelled' || lease.status === 'expired') {
|
|
312
|
-
throw new
|
|
309
|
+
throw new InvalidLeaseTransitionError(reservationId, latest?.status ?? lease.status, 'committed');
|
|
313
310
|
}
|
|
314
311
|
await this.journal.append({
|
|
315
312
|
treeId,
|
|
316
313
|
branchId: 'default',
|
|
317
|
-
wotsIndex:
|
|
314
|
+
wotsIndex: flatIndex(indices),
|
|
318
315
|
indices,
|
|
319
316
|
status: 'committed',
|
|
320
317
|
reservationId,
|
|
@@ -334,7 +331,7 @@ class LocalLeaseProvider {
|
|
|
334
331
|
await this.watermark.refresh();
|
|
335
332
|
const lease = this.leaseStore.get(reservationId);
|
|
336
333
|
if (!lease)
|
|
337
|
-
throw new
|
|
334
|
+
throw new LeaseNotFoundError(reservationId);
|
|
338
335
|
const treeId = lease.treeId ?? 'default';
|
|
339
336
|
const indices = lease.indices;
|
|
340
337
|
const latest = this.journal.getByReservation(reservationId);
|
|
@@ -346,12 +343,12 @@ class LocalLeaseProvider {
|
|
|
346
343
|
}
|
|
347
344
|
if (latest?.status === 'committed' || latest?.status === 'reserved-expired'
|
|
348
345
|
|| lease.status === 'finalized' || lease.status === 'expired') {
|
|
349
|
-
throw new
|
|
346
|
+
throw new InvalidLeaseTransitionError(reservationId, latest?.status ?? lease.status, 'burned');
|
|
350
347
|
}
|
|
351
348
|
await this.journal.append({
|
|
352
349
|
treeId,
|
|
353
350
|
branchId: 'default',
|
|
354
|
-
wotsIndex:
|
|
351
|
+
wotsIndex: flatIndex(indices),
|
|
355
352
|
indices,
|
|
356
353
|
status: 'burned',
|
|
357
354
|
reservationId,
|
|
@@ -405,8 +402,8 @@ class LocalLeaseProvider {
|
|
|
405
402
|
if (!this._initialized)
|
|
406
403
|
await this.initialize();
|
|
407
404
|
const local = this.watermark.getLocalWatermark(treeId);
|
|
408
|
-
const localFlat =
|
|
409
|
-
const remoteFlat =
|
|
405
|
+
const localFlat = flatIndex({ addressIndex: local.addressCursor, l1: local.l1Cursor, l2: local.l2Cursor });
|
|
406
|
+
const remoteFlat = flatIndex({ addressIndex: remote.addressCursor, l1: remote.l1Cursor, l2: remote.l2Cursor });
|
|
410
407
|
if (remoteFlat <= localFlat)
|
|
411
408
|
return false;
|
|
412
409
|
await this.watermark.save(treeId, {
|
|
@@ -437,4 +434,3 @@ class LocalLeaseProvider {
|
|
|
437
434
|
return { start: slot * 8, end: slot * 8 + 7 };
|
|
438
435
|
}
|
|
439
436
|
}
|
|
440
|
-
exports.LocalLeaseProvider = LocalLeaseProvider;
|
package/dist/onchain.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* OnchainWatermarkProvider — Layer 5: on-chain watermark anchoring.
|
|
4
3
|
*
|
|
@@ -21,13 +20,11 @@
|
|
|
21
20
|
* - syncLeaseJournal merges the on-chain cursor into the local watermark
|
|
22
21
|
* monotonically (remote ahead → advance; remote behind → conflict record).
|
|
23
22
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const txpow_1 = require("@totemsdk/txpow");
|
|
30
|
-
const certificate_js_1 = require("./certificate.js");
|
|
23
|
+
import { flatIndex } from './watermark.js';
|
|
24
|
+
import { OnchainWatermarkError } from './errors.js';
|
|
25
|
+
import { sha3_256, bytesToHex, hexToBytes, parseMxAddress, serializeTransaction, computeTransactionDigest, rollCoinId, writeMiniNumber, } from '@totemsdk/core';
|
|
26
|
+
import { serializeTxPoW } from '@totemsdk/txpow';
|
|
27
|
+
import { signCertificate, certificateSignatureVerified } from './certificate.js';
|
|
31
28
|
const MAX_L = 64;
|
|
32
29
|
const WATERMARK_COIN_STORAGE_KEY = 'wots-lease:onchain:watermark-coin';
|
|
33
30
|
function multiConcat(parts) {
|
|
@@ -48,7 +45,7 @@ function concatBytes(a, b) {
|
|
|
48
45
|
function addressToHex(address) {
|
|
49
46
|
const upper = address.startsWith('0x') || address.startsWith('0X') ? address.slice(2) : address;
|
|
50
47
|
return upper.toUpperCase().startsWith('MX')
|
|
51
|
-
?
|
|
48
|
+
? bytesToHex(parseMxAddress(address))
|
|
52
49
|
: address.replace(/^0x/i, '');
|
|
53
50
|
}
|
|
54
51
|
function flatToHex(flat) {
|
|
@@ -79,14 +76,14 @@ function stateVarJson(port, hexValue) {
|
|
|
79
76
|
return { port, svtype: 'hex', data: hexValue };
|
|
80
77
|
}
|
|
81
78
|
function buildWitnessBytes(sigs) {
|
|
82
|
-
const parts = [
|
|
79
|
+
const parts = [writeMiniNumber(BigInt(sigs.length), 0)];
|
|
83
80
|
for (const sig of sigs)
|
|
84
81
|
parts.push(sig);
|
|
85
|
-
parts.push(
|
|
86
|
-
parts.push(
|
|
82
|
+
parts.push(writeMiniNumber(0n, 0));
|
|
83
|
+
parts.push(writeMiniNumber(0n, 0));
|
|
87
84
|
return multiConcat(parts);
|
|
88
85
|
}
|
|
89
|
-
class OnchainWatermarkProvider {
|
|
86
|
+
export class OnchainWatermarkProvider {
|
|
90
87
|
constructor(config) {
|
|
91
88
|
this._initialized = false;
|
|
92
89
|
this._lastPublishBlock = -1;
|
|
@@ -145,7 +142,7 @@ class OnchainWatermarkProvider {
|
|
|
145
142
|
expiresAt: reservation.expiresAt,
|
|
146
143
|
signature: '',
|
|
147
144
|
};
|
|
148
|
-
certificate.signature = await
|
|
145
|
+
certificate.signature = await signCertificate(this.signer, certificate);
|
|
149
146
|
return { ...reservation, certificate };
|
|
150
147
|
}
|
|
151
148
|
async commitKeyUse(reservationId, txId) {
|
|
@@ -172,7 +169,7 @@ class OnchainWatermarkProvider {
|
|
|
172
169
|
async publishWatermark(treeId) {
|
|
173
170
|
await this.ensureInit();
|
|
174
171
|
const wm = await this.local.getLocalWatermark(treeId);
|
|
175
|
-
const newFlat =
|
|
172
|
+
const newFlat = flatIndex({
|
|
176
173
|
addressIndex: wm.addressCursor,
|
|
177
174
|
l1: wm.l1Cursor,
|
|
178
175
|
l2: wm.l2Cursor,
|
|
@@ -193,7 +190,7 @@ class OnchainWatermarkProvider {
|
|
|
193
190
|
// anchored by the CURRENT watermark coin (which advances on every spend).
|
|
194
191
|
const coin = await this.chain.getCoin(this.currentCoinId);
|
|
195
192
|
if (!coin) {
|
|
196
|
-
throw new
|
|
193
|
+
throw new OnchainWatermarkError(`Watermark coin not found: ${this.currentCoinId}`);
|
|
197
194
|
}
|
|
198
195
|
const currentState = coin.state ?? [];
|
|
199
196
|
const currentFlat = readStateCursor(currentState, this.statePort);
|
|
@@ -216,28 +213,28 @@ class OnchainWatermarkProvider {
|
|
|
216
213
|
storestate: true,
|
|
217
214
|
state: [stateVarJson(this.statePort, flatToHex(newFlat))],
|
|
218
215
|
};
|
|
216
|
+
const rolledCoinId = rollCoinId(hexToBytes(this.currentCoinId), 0);
|
|
219
217
|
const tx = {
|
|
220
218
|
linkhash: '0x00',
|
|
221
219
|
inputs: [inputCoin],
|
|
222
|
-
outputs: [outputCoin],
|
|
220
|
+
outputs: [{ ...outputCoin, coinid: '0x' + bytesToHex(rolledCoinId) }],
|
|
223
221
|
state: [],
|
|
224
222
|
};
|
|
225
|
-
const txBytes =
|
|
226
|
-
|
|
227
|
-
const digest = (0, core_1.computeTransactionDigest)(txBytes);
|
|
223
|
+
const txBytes = serializeTransaction(JSON.stringify(tx));
|
|
224
|
+
const digest = computeTransactionDigest(txBytes);
|
|
228
225
|
const sig = await this.signer.sign(digest);
|
|
229
226
|
const witnessBytes = buildWitnessBytes([sig]);
|
|
230
|
-
const prng =
|
|
231
|
-
const txHex = Buffer.from(
|
|
227
|
+
const prng = sha3_256(new TextEncoder().encode(`watermark:${treeId}:${this.currentCoinId}:${newFlat}`));
|
|
228
|
+
const txHex = Buffer.from(serializeTxPoW(txBytes, witnessBytes, { prng })).toString('hex');
|
|
232
229
|
const result = await this.chain.broadcastTxPoW(txHex);
|
|
233
230
|
if (!result.success) {
|
|
234
|
-
throw new
|
|
231
|
+
throw new OnchainWatermarkError(`Watermark publish rejected: ${result.message ?? 'unknown error'}`);
|
|
235
232
|
}
|
|
236
233
|
// Rollover: the spent watermark coin is destroyed and replaced by a fresh
|
|
237
234
|
// coin with a new coin ID (output 0 of this spend). Track and persist the
|
|
238
235
|
// ADVANCED identity so the next publish spends the live coin, not the
|
|
239
236
|
// consumed one.
|
|
240
|
-
const newCoinId = '0x' +
|
|
237
|
+
const newCoinId = '0x' + bytesToHex(rolledCoinId);
|
|
241
238
|
this.currentCoinId = newCoinId;
|
|
242
239
|
if (this.storage) {
|
|
243
240
|
try {
|
|
@@ -260,7 +257,7 @@ class OnchainWatermarkProvider {
|
|
|
260
257
|
let advancedTo;
|
|
261
258
|
for (const treeId of this.local.listTrees()) {
|
|
262
259
|
const localWm = await this.local.getLocalWatermark(treeId);
|
|
263
|
-
const localFlat =
|
|
260
|
+
const localFlat = flatIndex({
|
|
264
261
|
addressIndex: localWm.addressCursor,
|
|
265
262
|
l1: localWm.l1Cursor,
|
|
266
263
|
l2: localWm.l2Cursor,
|
|
@@ -306,9 +303,8 @@ class OnchainWatermarkProvider {
|
|
|
306
303
|
return false;
|
|
307
304
|
if (cert.expiresAt <= Date.now())
|
|
308
305
|
return false;
|
|
309
|
-
if (!(await
|
|
306
|
+
if (!(await certificateSignatureVerified(cert, this.signer)))
|
|
310
307
|
return false;
|
|
311
308
|
return cert.issuedBy === 'onchain-watermark';
|
|
312
309
|
}
|
|
313
310
|
}
|
|
314
|
-
exports.OnchainWatermarkProvider = OnchainWatermarkProvider;
|
package/dist/quorum.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* P2PQuorumLeaseProvider — Layer 4: multi-device quorum lease coordination.
|
|
4
3
|
*
|
|
@@ -28,14 +27,12 @@
|
|
|
28
27
|
* - If a peer reports the slot as taken, the local reservation is burned
|
|
29
28
|
* and QuorumConflictError is thrown.
|
|
30
29
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const errors_js_1 = require("./errors.js");
|
|
34
|
-
const certificate_js_1 = require("./certificate.js");
|
|
30
|
+
import { QuorumUnavailableError, QuorumConflictError, } from './errors.js';
|
|
31
|
+
import { signCertificate, certificateSignatureVerified, } from './certificate.js';
|
|
35
32
|
function sameIndices(a, b) {
|
|
36
33
|
return a.addressIndex === b.addressIndex && a.l1 === b.l1 && a.l2 === b.l2;
|
|
37
34
|
}
|
|
38
|
-
class P2PQuorumLeaseProvider {
|
|
35
|
+
export class P2PQuorumLeaseProvider {
|
|
39
36
|
constructor(config) {
|
|
40
37
|
/**
|
|
41
38
|
* localReservationId -> (peerId -> the reservation id that peer issued).
|
|
@@ -75,7 +72,7 @@ class P2PQuorumLeaseProvider {
|
|
|
75
72
|
attestations = await this.attestKeyUse(params, reservation.indices);
|
|
76
73
|
}
|
|
77
74
|
catch (err) {
|
|
78
|
-
const reason = err instanceof
|
|
75
|
+
const reason = err instanceof QuorumConflictError
|
|
79
76
|
? `quorum conflict: ${err.message}`
|
|
80
77
|
: 'quorum not reached';
|
|
81
78
|
await this.local.burnReservation(reservation.reservationId, reason);
|
|
@@ -96,7 +93,7 @@ class P2PQuorumLeaseProvider {
|
|
|
96
93
|
signature: '',
|
|
97
94
|
attestations,
|
|
98
95
|
};
|
|
99
|
-
certificate.signature = this.certificateSigner ? await
|
|
96
|
+
certificate.signature = this.certificateSigner ? await signCertificate(this.certificateSigner, certificate) : '';
|
|
100
97
|
return { ...reservation, certificate };
|
|
101
98
|
}
|
|
102
99
|
/**
|
|
@@ -148,10 +145,10 @@ class P2PQuorumLeaseProvider {
|
|
|
148
145
|
}
|
|
149
146
|
}));
|
|
150
147
|
if (conflicts.length > 0) {
|
|
151
|
-
throw new
|
|
148
|
+
throw new QuorumConflictError(params.treeId, indices);
|
|
152
149
|
}
|
|
153
150
|
if (attestations.length < this.minAttestations) {
|
|
154
|
-
throw new
|
|
151
|
+
throw new QuorumUnavailableError(this.minAttestations, attestations.length);
|
|
155
152
|
}
|
|
156
153
|
return attestations;
|
|
157
154
|
}
|
|
@@ -166,7 +163,7 @@ class P2PQuorumLeaseProvider {
|
|
|
166
163
|
txId,
|
|
167
164
|
}));
|
|
168
165
|
if (this.requireQuorumOnCommit && acks < this.minAttestations) {
|
|
169
|
-
throw new
|
|
166
|
+
throw new QuorumUnavailableError(this.minAttestations, acks);
|
|
170
167
|
}
|
|
171
168
|
}
|
|
172
169
|
async burnReservation(reservationId, reason) {
|
|
@@ -294,7 +291,7 @@ class P2PQuorumLeaseProvider {
|
|
|
294
291
|
if (cert.expiresAt <= Date.now())
|
|
295
292
|
return false;
|
|
296
293
|
// An unsigned certificate carries no authenticity evidence.
|
|
297
|
-
if (!(await
|
|
294
|
+
if (!(await certificateSignatureVerified(cert, this.certificateSigner))) {
|
|
298
295
|
return false;
|
|
299
296
|
}
|
|
300
297
|
const attestations = cert.attestations ?? [];
|
|
@@ -304,4 +301,3 @@ class P2PQuorumLeaseProvider {
|
|
|
304
301
|
a.expiresAt > Date.now());
|
|
305
302
|
}
|
|
306
303
|
}
|
|
307
|
-
exports.P2PQuorumLeaseProvider = P2PQuorumLeaseProvider;
|