@totemsdk/wots-lease 1.0.0 → 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.d.ts +1 -1
- package/dist/axia.js +41 -28
- package/dist/certificate.d.ts +37 -0
- package/dist/certificate.js +69 -0
- 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.d.ts +26 -0
- package/dist/errors.js +49 -25
- package/dist/hybrid.d.ts +4 -1
- package/dist/hybrid.js +72 -26
- package/dist/index.d.ts +6 -2
- package/dist/index.js +12 -38
- package/dist/journal.d.ts +14 -4
- package/dist/journal.js +327 -49
- package/dist/local.d.ts +31 -1
- package/dist/local.js +267 -68
- package/dist/onchain.d.ts +3 -1
- package/dist/onchain.js +63 -28
- package/dist/quorum.d.ts +15 -0
- package/dist/quorum.js +54 -15
- package/dist/replicated-backup.d.ts +30 -0
- package/dist/replicated-backup.js +48 -0
- package/dist/seed.d.ts +41 -0
- package/dist/seed.js +80 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +20 -10
- package/dist/types.d.ts +74 -1
- package/dist/types.js +1 -2
- package/dist/watermark.d.ts +22 -1
- package/dist/watermark.js +133 -16
- package/package.json +25 -15
- package/rust/pkg/package.json +3 -3
- package/rust/pkg/wots_lease_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +3 -3
- package/rust/pkg-node/wots_lease_wasm_bg.wasm +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* P2PQuorumLeaseProvider — Layer 4: multi-device quorum lease coordination.
|
|
4
|
+
*
|
|
5
|
+
* Safety model
|
|
6
|
+
* ────────────
|
|
7
|
+
* The local provider remains the authoritative watermark (it can only move
|
|
8
|
+
* forward). The quorum adds a second, independent check: before a reservation
|
|
9
|
+
* is handed to the caller, at least `minAttestations` peers must confirm that
|
|
10
|
+
* the same (treeId, indices) slot is free in *their* view of the world. This
|
|
11
|
+
* catches the failure mode where two devices share a seed and both believe
|
|
12
|
+
* they own the next slot — the peers see the double-reservation and refuse
|
|
13
|
+
* to attest.
|
|
14
|
+
*
|
|
15
|
+
* Wire protocol
|
|
16
|
+
* ─────────────
|
|
17
|
+
* Peers speak the same LEASE_RESERVE / LEASE_COMMIT / LEASE_BURN /
|
|
18
|
+
* LEASE_WATERMARK message shapes as the lookup protocol, so any peer that
|
|
19
|
+
* implements that protocol can participate: a personal lookup node, another
|
|
20
|
+
* device running this provider, or an in-memory test peer.
|
|
21
|
+
*
|
|
22
|
+
* Failure semantics
|
|
23
|
+
* ─────────────────
|
|
24
|
+
* - If quorum cannot be reached, the local reservation is burned and
|
|
25
|
+
* QuorumUnavailableError is thrown. The slot is NOT reusable (burned),
|
|
26
|
+
* which is the safe direction: a slot that might have been attested by
|
|
27
|
+
* some peers must never be handed out again.
|
|
28
|
+
* - If a peer reports the slot as taken, the local reservation is burned
|
|
29
|
+
* and QuorumConflictError is thrown.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.P2PQuorumLeaseProvider = void 0;
|
|
33
|
+
const errors_js_1 = require("./errors.js");
|
|
34
|
+
const certificate_js_1 = require("./certificate.js");
|
|
35
|
+
function sameIndices(a, b) {
|
|
36
|
+
return a.addressIndex === b.addressIndex && a.l1 === b.l1 && a.l2 === b.l2;
|
|
37
|
+
}
|
|
38
|
+
class P2PQuorumLeaseProvider {
|
|
39
|
+
constructor(config) {
|
|
40
|
+
/**
|
|
41
|
+
* localReservationId -> (peerId -> the reservation id that peer issued).
|
|
42
|
+
* Commit/burn must reference each peer's own id, because a peer only knows
|
|
43
|
+
* the reservation it issued, not the local provider's id (AUD-044).
|
|
44
|
+
*/
|
|
45
|
+
this.peerReservationIds = new Map();
|
|
46
|
+
this._initialized = false;
|
|
47
|
+
this.peers = config.peers;
|
|
48
|
+
this.minAttestations = config.minAttestations ?? 1;
|
|
49
|
+
this.local = config.local;
|
|
50
|
+
this.requestTimeoutMs = config.requestTimeoutMs ?? 5000;
|
|
51
|
+
this.requireQuorumOnCommit = config.requireQuorumOnCommit ?? false;
|
|
52
|
+
this.certificateSigner = config.certificateSigner;
|
|
53
|
+
}
|
|
54
|
+
async initialize() {
|
|
55
|
+
if (this._initialized)
|
|
56
|
+
return;
|
|
57
|
+
await this.local.initialize();
|
|
58
|
+
this._initialized = true;
|
|
59
|
+
}
|
|
60
|
+
async ensureInit() {
|
|
61
|
+
if (!this._initialized)
|
|
62
|
+
await this.initialize();
|
|
63
|
+
}
|
|
64
|
+
// -------------------------------------------------------------------------
|
|
65
|
+
// Reserve — local first, then quorum attestation
|
|
66
|
+
// -------------------------------------------------------------------------
|
|
67
|
+
async reserveKeyUse(params) {
|
|
68
|
+
await this.ensureInit();
|
|
69
|
+
// 1. Local reservation (authoritative, monotonic watermark).
|
|
70
|
+
const reservation = await this.local.reserveKeyUse(params);
|
|
71
|
+
// 2. Collect attestations from peers for the SAME indices.
|
|
72
|
+
// On failure the local reservation is burned — the slot is never reused.
|
|
73
|
+
let attestations;
|
|
74
|
+
try {
|
|
75
|
+
attestations = await this.attestKeyUse(params, reservation.indices);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const reason = err instanceof errors_js_1.QuorumConflictError
|
|
79
|
+
? `quorum conflict: ${err.message}`
|
|
80
|
+
: 'quorum not reached';
|
|
81
|
+
await this.local.burnReservation(reservation.reservationId, reason);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
this.recordPeerReservationIds(reservation.reservationId, attestations);
|
|
85
|
+
const certificate = {
|
|
86
|
+
reservationId: reservation.reservationId,
|
|
87
|
+
treeId: params.treeId,
|
|
88
|
+
branchId: params.branchId,
|
|
89
|
+
deviceId: params.deviceId,
|
|
90
|
+
indices: reservation.indices,
|
|
91
|
+
purpose: params.purpose,
|
|
92
|
+
payloadHash: params.payloadHash,
|
|
93
|
+
issuedBy: this.certificateSigner?.name ?? 'p2p-quorum',
|
|
94
|
+
issuedAt: Date.now(),
|
|
95
|
+
expiresAt: reservation.expiresAt,
|
|
96
|
+
signature: '',
|
|
97
|
+
attestations,
|
|
98
|
+
};
|
|
99
|
+
certificate.signature = this.certificateSigner ? await (0, certificate_js_1.signCertificate)(this.certificateSigner, certificate) : '';
|
|
100
|
+
return { ...reservation, certificate };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Collect quorum attestations for a reservation that was already made
|
|
104
|
+
* locally (used by HybridLeaseProvider so the local slot and the attested
|
|
105
|
+
* slot are the same). Throws QuorumUnavailableError / QuorumConflictError
|
|
106
|
+
* on failure — the caller owns the local reservation and must burn it.
|
|
107
|
+
*/
|
|
108
|
+
async attestKeyUse(params, indices) {
|
|
109
|
+
await this.ensureInit();
|
|
110
|
+
const attestations = [];
|
|
111
|
+
const conflicts = [];
|
|
112
|
+
await Promise.all(this.peers.map(async (peer) => {
|
|
113
|
+
try {
|
|
114
|
+
const resp = await peer.request({
|
|
115
|
+
type: 'LEASE_RESERVE',
|
|
116
|
+
payload: {
|
|
117
|
+
treeId: params.treeId,
|
|
118
|
+
branchId: params.branchId,
|
|
119
|
+
deviceId: params.deviceId,
|
|
120
|
+
ttlMs: params.ttlMs,
|
|
121
|
+
payloadHash: params.payloadHash,
|
|
122
|
+
purpose: params.purpose,
|
|
123
|
+
indices,
|
|
124
|
+
},
|
|
125
|
+
}, this.requestTimeoutMs);
|
|
126
|
+
if (resp.type === 'ERROR') {
|
|
127
|
+
conflicts.push(String(resp.payload.message ?? 'peer error'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const body = resp.payload;
|
|
131
|
+
const peerIndices = body.reservation?.indices;
|
|
132
|
+
if (!peerIndices || !sameIndices(peerIndices, indices)) {
|
|
133
|
+
conflicts.push('peer returned mismatched indices');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const peerReservationId = body.reservation?.reservationId;
|
|
137
|
+
attestations.push({
|
|
138
|
+
peerId: peer.peerId,
|
|
139
|
+
indices,
|
|
140
|
+
expiresAt: body.reservation?.expiresAt ?? Date.now() + (params.ttlMs ?? 120000),
|
|
141
|
+
...(typeof peerReservationId === 'string' && peerReservationId.length > 0
|
|
142
|
+
? { peerReservationId }
|
|
143
|
+
: {}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Peer unreachable — counted as no attestation.
|
|
148
|
+
}
|
|
149
|
+
}));
|
|
150
|
+
if (conflicts.length > 0) {
|
|
151
|
+
throw new errors_js_1.QuorumConflictError(params.treeId, indices);
|
|
152
|
+
}
|
|
153
|
+
if (attestations.length < this.minAttestations) {
|
|
154
|
+
throw new errors_js_1.QuorumUnavailableError(this.minAttestations, attestations.length);
|
|
155
|
+
}
|
|
156
|
+
return attestations;
|
|
157
|
+
}
|
|
158
|
+
// -------------------------------------------------------------------------
|
|
159
|
+
// Commit / burn — local authoritative, broadcast to peers
|
|
160
|
+
// -------------------------------------------------------------------------
|
|
161
|
+
async commitKeyUse(reservationId, txId) {
|
|
162
|
+
await this.ensureInit();
|
|
163
|
+
await this.local.commitKeyUse(reservationId, txId);
|
|
164
|
+
const acks = await this.broadcastAck('LEASE_COMMIT', (peerId) => ({
|
|
165
|
+
reservationId: this.peerReservationIdFor(reservationId, peerId),
|
|
166
|
+
txId,
|
|
167
|
+
}));
|
|
168
|
+
if (this.requireQuorumOnCommit && acks < this.minAttestations) {
|
|
169
|
+
throw new errors_js_1.QuorumUnavailableError(this.minAttestations, acks);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async burnReservation(reservationId, reason) {
|
|
173
|
+
await this.ensureInit();
|
|
174
|
+
await this.local.burnReservation(reservationId, reason);
|
|
175
|
+
await this.broadcastAck('LEASE_BURN', (peerId) => ({
|
|
176
|
+
reservationId: this.peerReservationIdFor(reservationId, peerId),
|
|
177
|
+
reason,
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
/** Record which peer issued which reservation id for a local reservation. */
|
|
181
|
+
recordPeerReservationIds(localReservationId, attestations) {
|
|
182
|
+
const byPeer = new Map();
|
|
183
|
+
for (const attestation of attestations) {
|
|
184
|
+
if (attestation.peerReservationId) {
|
|
185
|
+
byPeer.set(attestation.peerId, attestation.peerReservationId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (byPeer.size > 0)
|
|
189
|
+
this.peerReservationIds.set(localReservationId, byPeer);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The id to send a peer for a local reservation. Peers only know the id they
|
|
193
|
+
* issued; fall back to the local id only when no peer id was recorded (e.g.
|
|
194
|
+
* attestations gathered outside `reserveKeyUse`).
|
|
195
|
+
*/
|
|
196
|
+
peerReservationIdFor(localReservationId, peerId) {
|
|
197
|
+
return this.peerReservationIds.get(localReservationId)?.get(peerId) ?? localReservationId;
|
|
198
|
+
}
|
|
199
|
+
async broadcastAck(type, payloadFor) {
|
|
200
|
+
let acks = 0;
|
|
201
|
+
await Promise.all(this.peers.map(async (peer) => {
|
|
202
|
+
try {
|
|
203
|
+
const resp = await peer.request({ type, payload: payloadFor(peer.peerId) }, this.requestTimeoutMs);
|
|
204
|
+
if (resp.type !== 'ERROR')
|
|
205
|
+
acks++;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Peer unreachable — no ack.
|
|
209
|
+
}
|
|
210
|
+
}));
|
|
211
|
+
return acks;
|
|
212
|
+
}
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
// Watermark / journal
|
|
215
|
+
// -------------------------------------------------------------------------
|
|
216
|
+
async getLocalWatermark(treeId) {
|
|
217
|
+
await this.ensureInit();
|
|
218
|
+
return this.local.getLocalWatermark(treeId);
|
|
219
|
+
}
|
|
220
|
+
async publishWatermark(treeId) {
|
|
221
|
+
await this.ensureInit();
|
|
222
|
+
const wm = await this.local.getLocalWatermark(treeId);
|
|
223
|
+
await Promise.all(this.peers.map(async (peer) => {
|
|
224
|
+
try {
|
|
225
|
+
await peer.request({
|
|
226
|
+
type: 'LEASE_WATERMARK',
|
|
227
|
+
payload: {
|
|
228
|
+
treeId,
|
|
229
|
+
addressCursor: wm.addressCursor,
|
|
230
|
+
l1Cursor: wm.l1Cursor,
|
|
231
|
+
l2Cursor: wm.l2Cursor,
|
|
232
|
+
unavailableCount: wm.unavailableCount,
|
|
233
|
+
lastSyncTimestamp: wm.lastSyncTimestamp ?? Date.now(),
|
|
234
|
+
},
|
|
235
|
+
}, this.requestTimeoutMs);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Peer unreachable — watermark publish is best-effort.
|
|
239
|
+
}
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
async syncLeaseJournal() {
|
|
243
|
+
await this.ensureInit();
|
|
244
|
+
const conflicts = [];
|
|
245
|
+
let advancedTo;
|
|
246
|
+
for (const treeId of this.local.listTrees()) {
|
|
247
|
+
const localWm = await this.local.getLocalWatermark(treeId);
|
|
248
|
+
for (const peer of this.peers) {
|
|
249
|
+
try {
|
|
250
|
+
const resp = await peer.request({
|
|
251
|
+
type: 'LEASE_WATERMARK',
|
|
252
|
+
payload: { treeId, query: true },
|
|
253
|
+
}, this.requestTimeoutMs);
|
|
254
|
+
if (resp.type === 'ERROR')
|
|
255
|
+
continue;
|
|
256
|
+
const p = resp.payload;
|
|
257
|
+
if (typeof p.addressCursor !== 'number')
|
|
258
|
+
continue;
|
|
259
|
+
const remoteFlat = p.addressCursor * 64 * 64 + p.l1Cursor * 64 + p.l2Cursor;
|
|
260
|
+
const localFlat = localWm.addressCursor * 64 * 64 + localWm.l1Cursor * 64 + localWm.l2Cursor;
|
|
261
|
+
if (remoteFlat > localFlat) {
|
|
262
|
+
const advanced = await this.local.advanceToRemoteWatermark(treeId, {
|
|
263
|
+
addressCursor: p.addressCursor,
|
|
264
|
+
l1Cursor: p.l1Cursor,
|
|
265
|
+
l2Cursor: p.l2Cursor,
|
|
266
|
+
});
|
|
267
|
+
if (advanced) {
|
|
268
|
+
advancedTo = {
|
|
269
|
+
addressIndex: p.addressCursor,
|
|
270
|
+
l1: p.l1Cursor,
|
|
271
|
+
l2: p.l2Cursor,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else if (remoteFlat < localFlat) {
|
|
276
|
+
conflicts.push({
|
|
277
|
+
treeId,
|
|
278
|
+
localIndex: localFlat,
|
|
279
|
+
remoteIndex: remoteFlat,
|
|
280
|
+
timestamp: Date.now(),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Peer unreachable — skip.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { synced: conflicts.length === 0, conflicts, advancedTo };
|
|
290
|
+
}
|
|
291
|
+
async verifyLeaseCertificate(cert) {
|
|
292
|
+
if (!cert)
|
|
293
|
+
return false;
|
|
294
|
+
if (cert.expiresAt <= Date.now())
|
|
295
|
+
return false;
|
|
296
|
+
// An unsigned certificate carries no authenticity evidence.
|
|
297
|
+
if (!(await (0, certificate_js_1.certificateSignatureVerified)(cert, this.certificateSigner))) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
const attestations = cert.attestations ?? [];
|
|
301
|
+
if (attestations.length < this.minAttestations)
|
|
302
|
+
return false;
|
|
303
|
+
return attestations.every((a) => sameIndices(a.indices, cert.indices) &&
|
|
304
|
+
a.expiresAt > Date.now());
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
exports.P2PQuorumLeaseProvider = P2PQuorumLeaseProvider;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-057 — replicated WOTS lease-journal backup.
|
|
3
|
+
*
|
|
4
|
+
* The local `LeaseJournal` remains authoritative for allocation. This adapter
|
|
5
|
+
* mirrors the physically append-only journal into a replicated log for audit
|
|
6
|
+
* and disaster recovery, and restores entries into a local journal in order.
|
|
7
|
+
* It never allocates or advances a signing slot.
|
|
8
|
+
*/
|
|
9
|
+
import type { ReplicatedLog } from '@totemsdk/replication';
|
|
10
|
+
import type { JournalEntry } from './types.js';
|
|
11
|
+
export interface LeaseJournalRestoreTarget {
|
|
12
|
+
append(entry: JournalEntry): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare class ReplicatedLeaseJournalBackup {
|
|
15
|
+
private readonly log;
|
|
16
|
+
constructor(log: ReplicatedLog<JournalEntry>);
|
|
17
|
+
/** Mirror one local journal entry. */
|
|
18
|
+
append(entry: JournalEntry): Promise<{
|
|
19
|
+
length: number;
|
|
20
|
+
head: string;
|
|
21
|
+
}>;
|
|
22
|
+
/** Read all mirrored entries, verifying the local hash-chain links in order. */
|
|
23
|
+
readAll(): Promise<JournalEntry[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Restore into a local journal. The local journal's own duplicate-slot and
|
|
26
|
+
* reservation checks remain the final guard; this method only preserves the
|
|
27
|
+
* replicated order and links.
|
|
28
|
+
*/
|
|
29
|
+
restoreInto(target: LeaseJournalRestoreTarget): Promise<number>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RFC-057 — replicated WOTS lease-journal backup.
|
|
4
|
+
*
|
|
5
|
+
* The local `LeaseJournal` remains authoritative for allocation. This adapter
|
|
6
|
+
* mirrors the physically append-only journal into a replicated log for audit
|
|
7
|
+
* and disaster recovery, and restores entries into a local journal in order.
|
|
8
|
+
* It never allocates or advances a signing slot.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.ReplicatedLeaseJournalBackup = void 0;
|
|
12
|
+
const errors_1 = require("@totemsdk/storage/errors");
|
|
13
|
+
class ReplicatedLeaseJournalBackup {
|
|
14
|
+
constructor(log) {
|
|
15
|
+
this.log = log;
|
|
16
|
+
}
|
|
17
|
+
/** Mirror one local journal entry. */
|
|
18
|
+
async append(entry) {
|
|
19
|
+
return this.log.append(entry);
|
|
20
|
+
}
|
|
21
|
+
/** Read all mirrored entries, verifying the local hash-chain links in order. */
|
|
22
|
+
async readAll() {
|
|
23
|
+
const entries = [];
|
|
24
|
+
for await (const entry of this.log.readSince())
|
|
25
|
+
entries.push(entry.record);
|
|
26
|
+
let previous;
|
|
27
|
+
for (const [index, entry] of entries.entries()) {
|
|
28
|
+
if (index === 0) {
|
|
29
|
+
if (entry.previousHash !== undefined) {
|
|
30
|
+
throw new errors_1.StorageError('replicated lease journal first entry has previousHash', 'corrupt');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (entry.previousHash !== previous?.hash) {
|
|
34
|
+
throw new errors_1.StorageError(`replicated lease journal chain mismatch at ${index}`, 'corrupt');
|
|
35
|
+
}
|
|
36
|
+
previous = entry;
|
|
37
|
+
}
|
|
38
|
+
return entries;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Restore into a local journal. The local journal's own duplicate-slot and
|
|
42
|
+
* reservation checks remain the final guard; this method only preserves the
|
|
43
|
+
* replicated order and links.
|
|
44
|
+
*/
|
|
45
|
+
async restoreInto(target) {
|
|
46
|
+
const entries = await this.readAll();
|
|
47
|
+
for (const entry of entries)
|
|
48
|
+
await target.append(entry);
|
|
49
|
+
return entries.length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.ReplicatedLeaseJournalBackup = ReplicatedLeaseJournalBackup;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @totemsdk/wots-lease/seed
|
|
3
|
+
*
|
|
4
|
+
* Watermark seeding for import/restore (RFC-013 §9). The safety property is
|
|
5
|
+
* "never reissue a used leaf". A local watermark alone is insufficient across
|
|
6
|
+
* devices/restores, so seeding draws from, in priority order:
|
|
7
|
+
*
|
|
8
|
+
* 1. the on-chain watermark cursor (advances the local watermark),
|
|
9
|
+
* 2. an exported watermark blob,
|
|
10
|
+
* 3. an operator-provided high-water mark.
|
|
11
|
+
*
|
|
12
|
+
* If none is available and the caller has not declared the keyspace fresh,
|
|
13
|
+
* seeding **fails closed** — starting at 0 would reissue spent leaves.
|
|
14
|
+
*/
|
|
15
|
+
import type { LocalLeaseProvider } from './local.js';
|
|
16
|
+
import type { OnchainWatermarkProvider } from './onchain.js';
|
|
17
|
+
import type { LocalWatermark, WatermarkSeed } from './types.js';
|
|
18
|
+
export interface SeedLeaseWatermarkParams {
|
|
19
|
+
readonly local: LocalLeaseProvider;
|
|
20
|
+
readonly treeId: string;
|
|
21
|
+
/** Layer 5 — the on-chain cursor is authoritative when reachable. */
|
|
22
|
+
readonly onchain?: OnchainWatermarkProvider;
|
|
23
|
+
/** Exported watermark blob. */
|
|
24
|
+
readonly exported?: WatermarkSeed;
|
|
25
|
+
/** Operator-provided flat high-water mark. */
|
|
26
|
+
readonly operatorHighWaterMark?: number;
|
|
27
|
+
/** Declares the keyspace fresh/unspent, so cursor 0 is safe. */
|
|
28
|
+
readonly fresh?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Seed a local watermark from the best available source, requiring at least one
|
|
32
|
+
* source unless `fresh` is set. Returns the resulting local watermark.
|
|
33
|
+
*
|
|
34
|
+
* @throws {WatermarkSeedRequiredError} when no source is available and the
|
|
35
|
+
* keyspace is not declared fresh.
|
|
36
|
+
* @throws {WatermarkSeedRegressionError} when a supplied seed is behind the
|
|
37
|
+
* current cursor.
|
|
38
|
+
*/
|
|
39
|
+
export declare function seedLeaseWatermark(params: SeedLeaseWatermarkParams): Promise<LocalWatermark>;
|
|
40
|
+
/** Export a portable watermark blob for backup/restore. */
|
|
41
|
+
export declare function exportLeaseWatermark(local: LocalLeaseProvider, treeId: string): Promise<WatermarkSeed>;
|
package/dist/cjs/seed.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module @totemsdk/wots-lease/seed
|
|
4
|
+
*
|
|
5
|
+
* Watermark seeding for import/restore (RFC-013 §9). The safety property is
|
|
6
|
+
* "never reissue a used leaf". A local watermark alone is insufficient across
|
|
7
|
+
* devices/restores, so seeding draws from, in priority order:
|
|
8
|
+
*
|
|
9
|
+
* 1. the on-chain watermark cursor (advances the local watermark),
|
|
10
|
+
* 2. an exported watermark blob,
|
|
11
|
+
* 3. an operator-provided high-water mark.
|
|
12
|
+
*
|
|
13
|
+
* If none is available and the caller has not declared the keyspace fresh,
|
|
14
|
+
* seeding **fails closed** — starting at 0 would reissue spent leaves.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.seedLeaseWatermark = seedLeaseWatermark;
|
|
18
|
+
exports.exportLeaseWatermark = exportLeaseWatermark;
|
|
19
|
+
const watermark_js_1 = require("./watermark.js");
|
|
20
|
+
const errors_js_1 = require("./errors.js");
|
|
21
|
+
function flatOf(w) {
|
|
22
|
+
return (0, watermark_js_1.flatIndex)({ addressIndex: w.addressCursor, l1: w.l1Cursor, l2: w.l2Cursor });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Seed a local watermark from the best available source, requiring at least one
|
|
26
|
+
* source unless `fresh` is set. Returns the resulting local watermark.
|
|
27
|
+
*
|
|
28
|
+
* @throws {WatermarkSeedRequiredError} when no source is available and the
|
|
29
|
+
* keyspace is not declared fresh.
|
|
30
|
+
* @throws {WatermarkSeedRegressionError} when a supplied seed is behind the
|
|
31
|
+
* current cursor.
|
|
32
|
+
*/
|
|
33
|
+
async function seedLeaseWatermark(params) {
|
|
34
|
+
const { local, treeId } = params;
|
|
35
|
+
// 1. On-chain cursor — advances the same local watermark (monotonic).
|
|
36
|
+
if (params.onchain) {
|
|
37
|
+
try {
|
|
38
|
+
await params.onchain.syncLeaseJournal();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Chain unreachable — fall through to exported/operator sources below.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const current = await local.getLocalWatermark(treeId);
|
|
45
|
+
let bestFlat = flatOf({ addressCursor: current.addressCursor, l1Cursor: current.l1Cursor, l2Cursor: current.l2Cursor });
|
|
46
|
+
let best;
|
|
47
|
+
// 2. Exported blob.
|
|
48
|
+
if (params.exported) {
|
|
49
|
+
if (flatOf(params.exported) > bestFlat) {
|
|
50
|
+
best = params.exported;
|
|
51
|
+
bestFlat = flatOf(params.exported);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 3. Operator high-water mark.
|
|
55
|
+
if (params.operatorHighWaterMark !== undefined) {
|
|
56
|
+
const indices = (0, watermark_js_1.fromFlatIndex)(params.operatorHighWaterMark);
|
|
57
|
+
const candidate = {
|
|
58
|
+
version: 1,
|
|
59
|
+
treeId,
|
|
60
|
+
addressCursor: indices.addressIndex,
|
|
61
|
+
l1Cursor: indices.l1,
|
|
62
|
+
l2Cursor: indices.l2,
|
|
63
|
+
unavailable: [],
|
|
64
|
+
};
|
|
65
|
+
if (flatOf(candidate) > bestFlat) {
|
|
66
|
+
best = candidate;
|
|
67
|
+
bestFlat = flatOf(candidate);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (best) {
|
|
71
|
+
await local.seedWatermark(best);
|
|
72
|
+
}
|
|
73
|
+
if (!params.onchain &&
|
|
74
|
+
!params.exported &&
|
|
75
|
+
params.operatorHighWaterMark === undefined &&
|
|
76
|
+
!params.fresh) {
|
|
77
|
+
throw new errors_js_1.WatermarkSeedRequiredError(treeId);
|
|
78
|
+
}
|
|
79
|
+
return local.getLocalWatermark(treeId);
|
|
80
|
+
}
|
|
81
|
+
/** Export a portable watermark blob for backup/restore. */
|
|
82
|
+
function exportLeaseWatermark(local, treeId) {
|
|
83
|
+
return local.exportWatermark(treeId);
|
|
84
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementations for Layers 3–5 of the WotsLeaseProvider hierarchy.
|
|
3
|
+
*
|
|
4
|
+
* Layer 3 (PersonalLeaseNodeProvider) — HTTP client for a personal @totemsdk/lookup-node
|
|
5
|
+
* that has lease coordination enabled ({ lease: { enabled: true } }).
|
|
6
|
+
* The node exposes REST endpoints under /v1/lease/*.
|
|
7
|
+
*
|
|
8
|
+
* Layers 4–5 (P2PQuorumLeaseProvider, OnchainWatermarkProvider) — implemented in
|
|
9
|
+
* quorum.ts and onchain.ts; re-exported here for backward compatibility.
|
|
10
|
+
*/
|
|
11
|
+
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, PersonalLeaseNodeConfig } from './types.js';
|
|
12
|
+
export { P2PQuorumLeaseProvider } from './quorum.js';
|
|
13
|
+
export { OnchainWatermarkProvider } from './onchain.js';
|
|
14
|
+
/**
|
|
15
|
+
* Layer 3 — personal lookup-node lease coordinator.
|
|
16
|
+
*
|
|
17
|
+
* Calls the HTTP REST API exposed by a running @totemsdk/lookup-node that has
|
|
18
|
+
* lease coordination enabled. All write operations are forwarded to the node's
|
|
19
|
+
* LeaseCoordinator so the node acts as the source-of-truth watermark journal
|
|
20
|
+
* for high-value transactions.
|
|
21
|
+
*
|
|
22
|
+
* Wrap with HybridLeaseProvider so local reservations always succeed even when
|
|
23
|
+
* the personal node is temporarily unreachable:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const provider = new HybridLeaseProvider({
|
|
27
|
+
* local: new LocalLeaseProvider(storage),
|
|
28
|
+
* node: new PersonalLeaseNodeProvider({ nodeUrl, nodePubkey }),
|
|
29
|
+
* threshold: 10, // escalate to node for txns >= 10 MIN
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare class PersonalLeaseNodeProvider implements WotsLeaseProvider {
|
|
34
|
+
private readonly baseUrl;
|
|
35
|
+
private readonly nodePubkey;
|
|
36
|
+
private readonly authToken?;
|
|
37
|
+
private readonly certificateSigner?;
|
|
38
|
+
private readonly verify?;
|
|
39
|
+
constructor(config: PersonalLeaseNodeConfig);
|
|
40
|
+
private headers;
|
|
41
|
+
private post;
|
|
42
|
+
private get;
|
|
43
|
+
reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
|
|
44
|
+
commitKeyUse(reservationId: string, txId: string): Promise<void>;
|
|
45
|
+
burnReservation(reservationId: string, reason: string): Promise<void>;
|
|
46
|
+
getLocalWatermark(treeId: string): Promise<LocalWatermark>;
|
|
47
|
+
publishWatermark(treeId: string): Promise<void>;
|
|
48
|
+
syncLeaseJournal(): Promise<SyncResult>;
|
|
49
|
+
verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
|
|
50
|
+
}
|