@totemsdk/wots-lease 0.1.4 → 0.1.6

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.
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ /**
3
+ * OnchainWatermarkProvider — Layer 5: on-chain watermark anchoring.
4
+ *
5
+ * Safety model
6
+ * ────────────
7
+ * The local provider remains the authoritative watermark. This provider adds
8
+ * a verifiable public record: a dedicated watermark coin whose STATE(0) holds
9
+ * the flat watermark cursor. Every publish spends the coin back to itself
10
+ * with an advanced cursor, so any third party can read the on-chain state and
11
+ * prove that a given signing slot was consumed — without trusting this device.
12
+ *
13
+ * The watermark coin script is `RETURN SIGNEDBY(0x<digest>)` — the same
14
+ * single-sig pattern used by statechain reclaim addresses. The signer is the
15
+ * WOTS key owner; the digest is the 32-byte public key digest.
16
+ *
17
+ * Failure semantics
18
+ * ─────────────────
19
+ * - publishWatermark fails loudly (OnchainWatermarkError) when the chain
20
+ * rejects the TX — the local watermark is never rolled back.
21
+ * - syncLeaseJournal merges the on-chain cursor into the local watermark
22
+ * monotonically (remote ahead → advance; remote behind → conflict record).
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.OnchainWatermarkProvider = void 0;
26
+ const watermark_js_1 = require("./watermark.js");
27
+ const errors_js_1 = require("./errors.js");
28
+ const core_1 = require("@totemsdk/core");
29
+ const txpow_1 = require("@totemsdk/txpow");
30
+ const MAX_L = 64;
31
+ function multiConcat(parts) {
32
+ if (parts.length === 0)
33
+ return new Uint8Array(0);
34
+ let result = parts[0];
35
+ for (let i = 1; i < parts.length; i++) {
36
+ result = concatBytes(result, parts[i]);
37
+ }
38
+ return result;
39
+ }
40
+ function concatBytes(a, b) {
41
+ const out = new Uint8Array(a.length + b.length);
42
+ out.set(a, 0);
43
+ out.set(b, a.length);
44
+ return out;
45
+ }
46
+ function addressToHex(address) {
47
+ const upper = address.startsWith('0x') || address.startsWith('0X') ? address.slice(2) : address;
48
+ return upper.toUpperCase().startsWith('MX')
49
+ ? (0, core_1.bytesToHex)((0, core_1.parseMxAddress)(address))
50
+ : address.replace(/^0x/i, '');
51
+ }
52
+ function flatToHex(flat) {
53
+ let hex = BigInt(flat).toString(16);
54
+ if (hex.length % 2 !== 0)
55
+ hex = '0' + hex;
56
+ return hex || '00';
57
+ }
58
+ function hexToFlat(hex) {
59
+ const clean = (hex.startsWith('0x') ? hex.slice(2) : hex).replace(/^0+/, '') || '0';
60
+ return Number(BigInt('0x' + clean));
61
+ }
62
+ function readStateCursor(state, statePort) {
63
+ for (const raw of state) {
64
+ const entry = raw;
65
+ if (typeof entry !== 'object' || entry === null)
66
+ continue;
67
+ if (entry.port !== statePort)
68
+ continue;
69
+ if (typeof entry.data === 'string')
70
+ return hexToFlat(entry.data);
71
+ if (entry.value !== undefined)
72
+ return Number(entry.value);
73
+ }
74
+ return null;
75
+ }
76
+ function stateVarJson(port, hexValue) {
77
+ return { port, svtype: 'hex', data: hexValue };
78
+ }
79
+ function buildWitnessBytes(sigs) {
80
+ const parts = [(0, core_1.writeMiniNumber)(BigInt(sigs.length), 0)];
81
+ for (const sig of sigs)
82
+ parts.push(sig);
83
+ parts.push((0, core_1.writeMiniNumber)(0n, 0));
84
+ parts.push((0, core_1.writeMiniNumber)(0n, 0));
85
+ return multiConcat(parts);
86
+ }
87
+ class OnchainWatermarkProvider {
88
+ constructor(config) {
89
+ this._initialized = false;
90
+ this._lastPublishBlock = -1;
91
+ this.chain = config.chain;
92
+ this.watermarkCoinId = config.watermarkCoinId;
93
+ this.watermarkAddress = config.watermarkAddress;
94
+ this.tokenId = config.tokenId ?? '0x00';
95
+ this.amount = config.amount ?? '1';
96
+ this.local = config.local;
97
+ this.signer = config.signer;
98
+ this.statePort = config.statePort ?? 0;
99
+ this.minBlocksBetweenPublishes = config.minBlocksBetweenPublishes ?? 1;
100
+ }
101
+ async initialize() {
102
+ if (this._initialized)
103
+ return;
104
+ await this.local.initialize();
105
+ this._initialized = true;
106
+ }
107
+ async ensureInit() {
108
+ if (!this._initialized)
109
+ await this.initialize();
110
+ }
111
+ // -------------------------------------------------------------------------
112
+ // Reserve / commit / burn — local authoritative
113
+ // -------------------------------------------------------------------------
114
+ async reserveKeyUse(params) {
115
+ await this.ensureInit();
116
+ const reservation = await this.local.reserveKeyUse(params);
117
+ const certificate = {
118
+ reservationId: reservation.reservationId,
119
+ treeId: params.treeId,
120
+ branchId: params.branchId,
121
+ deviceId: params.deviceId,
122
+ indices: reservation.indices,
123
+ purpose: params.purpose,
124
+ payloadHash: params.payloadHash,
125
+ issuedBy: 'onchain-watermark',
126
+ issuedAt: Date.now(),
127
+ expiresAt: reservation.expiresAt,
128
+ signature: '',
129
+ };
130
+ return { ...reservation, certificate };
131
+ }
132
+ async commitKeyUse(reservationId, txId) {
133
+ await this.ensureInit();
134
+ await this.local.commitKeyUse(reservationId, txId);
135
+ }
136
+ async burnReservation(reservationId, reason) {
137
+ await this.ensureInit();
138
+ await this.local.burnReservation(reservationId, reason);
139
+ }
140
+ // -------------------------------------------------------------------------
141
+ // Watermark — local read, on-chain publish
142
+ // -------------------------------------------------------------------------
143
+ async getLocalWatermark(treeId) {
144
+ await this.ensureInit();
145
+ return this.local.getLocalWatermark(treeId);
146
+ }
147
+ /**
148
+ * Publish the local watermark cursor on-chain by spending the watermark
149
+ * coin back to itself with STATE(statePort) = flat cursor.
150
+ *
151
+ * Rate-limited by minBlocksBetweenPublishes using the chain tip.
152
+ */
153
+ async publishWatermark(treeId) {
154
+ await this.ensureInit();
155
+ const wm = await this.local.getLocalWatermark(treeId);
156
+ const newFlat = (0, watermark_js_1.flatIndex)({
157
+ addressIndex: wm.addressCursor,
158
+ l1: wm.l1Cursor,
159
+ l2: wm.l2Cursor,
160
+ });
161
+ // Rate limit: skip when the last publish was too recent.
162
+ try {
163
+ const tip = await this.chain.getTip?.();
164
+ if (tip && this._lastPublishBlock >= 0 && tip.block - this._lastPublishBlock < this.minBlocksBetweenPublishes) {
165
+ return;
166
+ }
167
+ if (tip)
168
+ this._lastPublishBlock = tip.block;
169
+ }
170
+ catch {
171
+ // Tip unavailable — proceed without rate limiting.
172
+ }
173
+ // Read the current on-chain cursor so the input state matches the coin.
174
+ const coin = await this.chain.getCoin(this.watermarkCoinId);
175
+ if (!coin) {
176
+ throw new errors_js_1.OnchainWatermarkError(`Watermark coin not found: ${this.watermarkCoinId}`);
177
+ }
178
+ const currentState = coin.state ?? [];
179
+ const currentFlat = readStateCursor(currentState, this.statePort);
180
+ if (currentFlat !== null && currentFlat >= newFlat) {
181
+ // On-chain cursor already at or ahead of local — nothing to publish.
182
+ return;
183
+ }
184
+ const inputCoin = {
185
+ coinid: this.watermarkCoinId,
186
+ address: addressToHex(this.watermarkAddress),
187
+ amount: this.amount,
188
+ tokenid: this.tokenId,
189
+ storestate: true,
190
+ state: currentState,
191
+ };
192
+ const outputCoin = {
193
+ address: addressToHex(this.watermarkAddress),
194
+ amount: this.amount,
195
+ tokenid: this.tokenId,
196
+ storestate: true,
197
+ state: [stateVarJson(this.statePort, flatToHex(newFlat))],
198
+ };
199
+ const tx = {
200
+ linkhash: '0x00',
201
+ inputs: [inputCoin],
202
+ outputs: [outputCoin],
203
+ state: [],
204
+ };
205
+ const txBytes = (0, core_1.serializeTransaction)(JSON.stringify(tx));
206
+ (0, core_1.precomputeTransactionCoinID)(txBytes, 0);
207
+ const digest = (0, core_1.computeTransactionDigest)(txBytes);
208
+ const sig = await this.signer.sign(digest);
209
+ const witnessBytes = buildWitnessBytes([sig]);
210
+ const prng = (0, core_1.sha3_256)(new TextEncoder().encode(`watermark:${treeId}:${this.watermarkCoinId}:${newFlat}`));
211
+ const txHex = Buffer.from((0, txpow_1.serializeTxPoW)(txBytes, witnessBytes, { prng })).toString('hex');
212
+ const result = await this.chain.broadcastTxPoW(txHex);
213
+ if (!result.success) {
214
+ throw new errors_js_1.OnchainWatermarkError(`Watermark publish rejected: ${result.message ?? 'unknown error'}`);
215
+ }
216
+ }
217
+ // -------------------------------------------------------------------------
218
+ // Sync — merge on-chain cursor into local watermark
219
+ // -------------------------------------------------------------------------
220
+ async syncLeaseJournal() {
221
+ await this.ensureInit();
222
+ const conflicts = [];
223
+ let advancedTo;
224
+ for (const treeId of this.local.listTrees()) {
225
+ const localWm = await this.local.getLocalWatermark(treeId);
226
+ const localFlat = (0, watermark_js_1.flatIndex)({
227
+ addressIndex: localWm.addressCursor,
228
+ l1: localWm.l1Cursor,
229
+ l2: localWm.l2Cursor,
230
+ });
231
+ let onchainFlat = null;
232
+ try {
233
+ const coin = await this.chain.getCoin(this.watermarkCoinId);
234
+ if (coin)
235
+ onchainFlat = readStateCursor(coin.state ?? [], this.statePort);
236
+ }
237
+ catch {
238
+ // Chain unreachable — skip.
239
+ }
240
+ if (onchainFlat === null)
241
+ continue;
242
+ if (onchainFlat > localFlat) {
243
+ const addressIndex = Math.floor(onchainFlat / (MAX_L * MAX_L));
244
+ const rem = onchainFlat % (MAX_L * MAX_L);
245
+ const l1 = Math.floor(rem / MAX_L);
246
+ const l2 = rem % MAX_L;
247
+ const advanced = await this.local.advanceToRemoteWatermark(treeId, {
248
+ addressCursor: addressIndex,
249
+ l1Cursor: l1,
250
+ l2Cursor: l2,
251
+ });
252
+ if (advanced) {
253
+ advancedTo = { addressIndex, l1, l2 };
254
+ }
255
+ }
256
+ else if (onchainFlat < localFlat) {
257
+ conflicts.push({
258
+ treeId,
259
+ localIndex: localFlat,
260
+ remoteIndex: onchainFlat,
261
+ timestamp: Date.now(),
262
+ });
263
+ }
264
+ }
265
+ return { synced: conflicts.length === 0, conflicts, advancedTo };
266
+ }
267
+ async verifyLeaseCertificate(cert) {
268
+ if (!cert)
269
+ return false;
270
+ if (cert.expiresAt <= Date.now())
271
+ return false;
272
+ return cert.issuedBy === 'onchain-watermark';
273
+ }
274
+ }
275
+ exports.OnchainWatermarkProvider = OnchainWatermarkProvider;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * P2PQuorumLeaseProvider — Layer 4: multi-device quorum lease coordination.
3
+ *
4
+ * Safety model
5
+ * ────────────
6
+ * The local provider remains the authoritative watermark (it can only move
7
+ * forward). The quorum adds a second, independent check: before a reservation
8
+ * is handed to the caller, at least `minAttestations` peers must confirm that
9
+ * the same (treeId, indices) slot is free in *their* view of the world. This
10
+ * catches the failure mode where two devices share a seed and both believe
11
+ * they own the next slot — the peers see the double-reservation and refuse
12
+ * to attest.
13
+ *
14
+ * Wire protocol
15
+ * ─────────────
16
+ * Peers speak the same LEASE_RESERVE / LEASE_COMMIT / LEASE_BURN /
17
+ * LEASE_WATERMARK message shapes as the lookup protocol, so any peer that
18
+ * implements that protocol can participate: a personal lookup node, another
19
+ * device running this provider, or an in-memory test peer.
20
+ *
21
+ * Failure semantics
22
+ * ─────────────────
23
+ * - If quorum cannot be reached, the local reservation is burned and
24
+ * QuorumUnavailableError is thrown. The slot is NOT reusable (burned),
25
+ * which is the safe direction: a slot that might have been attested by
26
+ * some peers must never be handed out again.
27
+ * - If a peer reports the slot as taken, the local reservation is burned
28
+ * and QuorumConflictError is thrown.
29
+ */
30
+ import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, QuorumAttestation, P2PQuorumLeaseProviderConfig } from './types.js';
31
+ export declare class P2PQuorumLeaseProvider implements WotsLeaseProvider {
32
+ private readonly peers;
33
+ private readonly minAttestations;
34
+ private readonly local;
35
+ private readonly requestTimeoutMs;
36
+ private readonly requireQuorumOnCommit;
37
+ private _initialized;
38
+ constructor(config: P2PQuorumLeaseProviderConfig);
39
+ initialize(): Promise<void>;
40
+ private ensureInit;
41
+ reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
42
+ /**
43
+ * Collect quorum attestations for a reservation that was already made
44
+ * locally (used by HybridLeaseProvider so the local slot and the attested
45
+ * slot are the same). Throws QuorumUnavailableError / QuorumConflictError
46
+ * on failure — the caller owns the local reservation and must burn it.
47
+ */
48
+ attestKeyUse(params: ReserveParams, indices: {
49
+ addressIndex: number;
50
+ l1: number;
51
+ l2: number;
52
+ }): Promise<QuorumAttestation[]>;
53
+ commitKeyUse(reservationId: string, txId: string): Promise<void>;
54
+ burnReservation(reservationId: string, reason: string): Promise<void>;
55
+ private broadcastAck;
56
+ getLocalWatermark(treeId: string): Promise<LocalWatermark>;
57
+ publishWatermark(treeId: string): Promise<void>;
58
+ syncLeaseJournal(): Promise<SyncResult>;
59
+ verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
60
+ }
package/dist/quorum.js ADDED
@@ -0,0 +1,264 @@
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
+ function sameIndices(a, b) {
35
+ return a.addressIndex === b.addressIndex && a.l1 === b.l1 && a.l2 === b.l2;
36
+ }
37
+ class P2PQuorumLeaseProvider {
38
+ constructor(config) {
39
+ this._initialized = false;
40
+ this.peers = config.peers;
41
+ this.minAttestations = config.minAttestations ?? 1;
42
+ this.local = config.local;
43
+ this.requestTimeoutMs = config.requestTimeoutMs ?? 5000;
44
+ this.requireQuorumOnCommit = config.requireQuorumOnCommit ?? false;
45
+ }
46
+ async initialize() {
47
+ if (this._initialized)
48
+ return;
49
+ await this.local.initialize();
50
+ this._initialized = true;
51
+ }
52
+ async ensureInit() {
53
+ if (!this._initialized)
54
+ await this.initialize();
55
+ }
56
+ // -------------------------------------------------------------------------
57
+ // Reserve — local first, then quorum attestation
58
+ // -------------------------------------------------------------------------
59
+ async reserveKeyUse(params) {
60
+ await this.ensureInit();
61
+ // 1. Local reservation (authoritative, monotonic watermark).
62
+ const reservation = await this.local.reserveKeyUse(params);
63
+ // 2. Collect attestations from peers for the SAME indices.
64
+ // On failure the local reservation is burned — the slot is never reused.
65
+ let attestations;
66
+ try {
67
+ attestations = await this.attestKeyUse(params, reservation.indices);
68
+ }
69
+ catch (err) {
70
+ const reason = err instanceof errors_js_1.QuorumConflictError
71
+ ? `quorum conflict: ${err.message}`
72
+ : 'quorum not reached';
73
+ await this.local.burnReservation(reservation.reservationId, reason);
74
+ throw err;
75
+ }
76
+ const certificate = {
77
+ reservationId: reservation.reservationId,
78
+ treeId: params.treeId,
79
+ branchId: params.branchId,
80
+ deviceId: params.deviceId,
81
+ indices: reservation.indices,
82
+ purpose: params.purpose,
83
+ payloadHash: params.payloadHash,
84
+ issuedBy: 'p2p-quorum',
85
+ issuedAt: Date.now(),
86
+ expiresAt: reservation.expiresAt,
87
+ signature: '',
88
+ attestations,
89
+ };
90
+ return { ...reservation, certificate };
91
+ }
92
+ /**
93
+ * Collect quorum attestations for a reservation that was already made
94
+ * locally (used by HybridLeaseProvider so the local slot and the attested
95
+ * slot are the same). Throws QuorumUnavailableError / QuorumConflictError
96
+ * on failure — the caller owns the local reservation and must burn it.
97
+ */
98
+ async attestKeyUse(params, indices) {
99
+ await this.ensureInit();
100
+ const attestations = [];
101
+ const conflicts = [];
102
+ await Promise.all(this.peers.map(async (peer) => {
103
+ try {
104
+ const resp = await peer.request({
105
+ type: 'LEASE_RESERVE',
106
+ payload: {
107
+ treeId: params.treeId,
108
+ branchId: params.branchId,
109
+ deviceId: params.deviceId,
110
+ ttlMs: params.ttlMs,
111
+ payloadHash: params.payloadHash,
112
+ purpose: params.purpose,
113
+ indices,
114
+ },
115
+ }, this.requestTimeoutMs);
116
+ if (resp.type === 'ERROR') {
117
+ conflicts.push(String(resp.payload.message ?? 'peer error'));
118
+ return;
119
+ }
120
+ const body = resp.payload;
121
+ const peerIndices = body.reservation?.indices;
122
+ if (!peerIndices || !sameIndices(peerIndices, indices)) {
123
+ conflicts.push('peer returned mismatched indices');
124
+ return;
125
+ }
126
+ attestations.push({
127
+ peerId: peer.peerId,
128
+ indices,
129
+ expiresAt: body.reservation?.expiresAt ?? Date.now() + (params.ttlMs ?? 120000),
130
+ });
131
+ }
132
+ catch {
133
+ // Peer unreachable — counted as no attestation.
134
+ }
135
+ }));
136
+ if (conflicts.length > 0) {
137
+ throw new errors_js_1.QuorumConflictError(params.treeId, indices);
138
+ }
139
+ if (attestations.length < this.minAttestations) {
140
+ throw new errors_js_1.QuorumUnavailableError(this.minAttestations, attestations.length);
141
+ }
142
+ return attestations;
143
+ }
144
+ // -------------------------------------------------------------------------
145
+ // Commit / burn — local authoritative, broadcast to peers
146
+ // -------------------------------------------------------------------------
147
+ async commitKeyUse(reservationId, txId) {
148
+ await this.ensureInit();
149
+ await this.local.commitKeyUse(reservationId, txId);
150
+ const acks = await this.broadcastAck('LEASE_COMMIT', { reservationId, txId });
151
+ if (this.requireQuorumOnCommit && acks < this.minAttestations) {
152
+ throw new errors_js_1.QuorumUnavailableError(this.minAttestations, acks);
153
+ }
154
+ }
155
+ async burnReservation(reservationId, reason) {
156
+ await this.ensureInit();
157
+ await this.local.burnReservation(reservationId, reason);
158
+ await this.broadcastAck('LEASE_BURN', { reservationId, reason });
159
+ }
160
+ async broadcastAck(type, payload) {
161
+ let acks = 0;
162
+ await Promise.all(this.peers.map(async (peer) => {
163
+ try {
164
+ const resp = await peer.request({ type, payload }, this.requestTimeoutMs);
165
+ if (resp.type !== 'ERROR')
166
+ acks++;
167
+ }
168
+ catch {
169
+ // Peer unreachable — no ack.
170
+ }
171
+ }));
172
+ return acks;
173
+ }
174
+ // -------------------------------------------------------------------------
175
+ // Watermark / journal
176
+ // -------------------------------------------------------------------------
177
+ async getLocalWatermark(treeId) {
178
+ await this.ensureInit();
179
+ return this.local.getLocalWatermark(treeId);
180
+ }
181
+ async publishWatermark(treeId) {
182
+ await this.ensureInit();
183
+ const wm = await this.local.getLocalWatermark(treeId);
184
+ await Promise.all(this.peers.map(async (peer) => {
185
+ try {
186
+ await peer.request({
187
+ type: 'LEASE_WATERMARK',
188
+ payload: {
189
+ treeId,
190
+ addressCursor: wm.addressCursor,
191
+ l1Cursor: wm.l1Cursor,
192
+ l2Cursor: wm.l2Cursor,
193
+ unavailableCount: wm.unavailableCount,
194
+ lastSyncTimestamp: wm.lastSyncTimestamp ?? Date.now(),
195
+ },
196
+ }, this.requestTimeoutMs);
197
+ }
198
+ catch {
199
+ // Peer unreachable — watermark publish is best-effort.
200
+ }
201
+ }));
202
+ }
203
+ async syncLeaseJournal() {
204
+ await this.ensureInit();
205
+ const conflicts = [];
206
+ let advancedTo;
207
+ for (const treeId of this.local.listTrees()) {
208
+ const localWm = await this.local.getLocalWatermark(treeId);
209
+ for (const peer of this.peers) {
210
+ try {
211
+ const resp = await peer.request({
212
+ type: 'LEASE_WATERMARK',
213
+ payload: { treeId, query: true },
214
+ }, this.requestTimeoutMs);
215
+ if (resp.type === 'ERROR')
216
+ continue;
217
+ const p = resp.payload;
218
+ if (typeof p.addressCursor !== 'number')
219
+ continue;
220
+ const remoteFlat = p.addressCursor * 64 * 64 + p.l1Cursor * 64 + p.l2Cursor;
221
+ const localFlat = localWm.addressCursor * 64 * 64 + localWm.l1Cursor * 64 + localWm.l2Cursor;
222
+ if (remoteFlat > localFlat) {
223
+ const advanced = await this.local.advanceToRemoteWatermark(treeId, {
224
+ addressCursor: p.addressCursor,
225
+ l1Cursor: p.l1Cursor,
226
+ l2Cursor: p.l2Cursor,
227
+ });
228
+ if (advanced) {
229
+ advancedTo = {
230
+ addressIndex: p.addressCursor,
231
+ l1: p.l1Cursor,
232
+ l2: p.l2Cursor,
233
+ };
234
+ }
235
+ }
236
+ else if (remoteFlat < localFlat) {
237
+ conflicts.push({
238
+ treeId,
239
+ localIndex: localFlat,
240
+ remoteIndex: remoteFlat,
241
+ timestamp: Date.now(),
242
+ });
243
+ }
244
+ }
245
+ catch {
246
+ // Peer unreachable — skip.
247
+ }
248
+ }
249
+ }
250
+ return { synced: conflicts.length === 0, conflicts, advancedTo };
251
+ }
252
+ async verifyLeaseCertificate(cert) {
253
+ if (!cert)
254
+ return false;
255
+ if (cert.expiresAt <= Date.now())
256
+ return false;
257
+ const attestations = cert.attestations ?? [];
258
+ if (attestations.length < this.minAttestations)
259
+ return false;
260
+ return attestations.every((a) => sameIndices(a.indices, cert.indices) &&
261
+ a.expiresAt > Date.now());
262
+ }
263
+ }
264
+ exports.P2PQuorumLeaseProvider = P2PQuorumLeaseProvider;
package/dist/stubs.d.ts CHANGED
@@ -5,10 +5,12 @@
5
5
  * that has lease coordination enabled ({ lease: { enabled: true } }).
6
6
  * The node exposes REST endpoints under /v1/lease/*.
7
7
  *
8
- * Layers 4–5 (P2PQuorumLeaseProvider, OnchainWatermarkProvider) — require
9
- * distributed infrastructure not yet available; they throw descriptive errors.
8
+ * Layers 4–5 (P2PQuorumLeaseProvider, OnchainWatermarkProvider) — implemented in
9
+ * quorum.ts and onchain.ts; re-exported here for backward compatibility.
10
10
  */
11
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';
12
14
  /**
13
15
  * Layer 3 — personal lookup-node lease coordinator.
14
16
  *
@@ -44,23 +46,3 @@ export declare class PersonalLeaseNodeProvider implements WotsLeaseProvider {
44
46
  syncLeaseJournal(): Promise<SyncResult>;
45
47
  verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
46
48
  }
47
- /** Layer 4 — p2p replicated lease witnesses. */
48
- export declare class P2PQuorumLeaseProvider implements WotsLeaseProvider {
49
- reserveKeyUse(_params: ReserveParams): Promise<LeaseReservation>;
50
- commitKeyUse(_reservationId: string, _txId: string): Promise<void>;
51
- burnReservation(_reservationId: string, _reason: string): Promise<void>;
52
- getLocalWatermark(_treeId: string): Promise<LocalWatermark>;
53
- publishWatermark(_treeId: string): Promise<void>;
54
- syncLeaseJournal(): Promise<SyncResult>;
55
- verifyLeaseCertificate(_cert?: LeaseCertificate): Promise<boolean>;
56
- }
57
- /** Layer 5 — on-chain watermark coin. */
58
- export declare class OnchainWatermarkProvider implements WotsLeaseProvider {
59
- reserveKeyUse(_params: ReserveParams): Promise<LeaseReservation>;
60
- commitKeyUse(_reservationId: string, _txId: string): Promise<void>;
61
- burnReservation(_reservationId: string, _reason: string): Promise<void>;
62
- getLocalWatermark(_treeId: string): Promise<LocalWatermark>;
63
- publishWatermark(_treeId: string): Promise<void>;
64
- syncLeaseJournal(): Promise<SyncResult>;
65
- verifyLeaseCertificate(_cert?: LeaseCertificate): Promise<boolean>;
66
- }