@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.
Files changed (67) hide show
  1. package/LICENSE +202 -21
  2. package/NOTICE +60 -0
  3. package/dist/axia.d.ts +1 -1
  4. package/dist/axia.js +41 -28
  5. package/dist/certificate.d.ts +37 -0
  6. package/dist/certificate.js +69 -0
  7. package/dist/cjs/axia.d.ts +33 -0
  8. package/dist/cjs/axia.js +145 -0
  9. package/dist/cjs/certificate.d.ts +37 -0
  10. package/dist/cjs/certificate.js +74 -0
  11. package/dist/cjs/device.d.ts +18 -0
  12. package/dist/cjs/device.js +34 -0
  13. package/dist/cjs/errors.d.ts +71 -0
  14. package/dist/cjs/errors.js +135 -0
  15. package/dist/cjs/hybrid.d.ts +37 -0
  16. package/dist/cjs/hybrid.js +145 -0
  17. package/dist/cjs/index.d.ts +17 -0
  18. package/dist/cjs/index.js +45 -0
  19. package/dist/cjs/journal.d.ts +30 -0
  20. package/dist/cjs/journal.js +369 -0
  21. package/dist/cjs/local.d.ts +83 -0
  22. package/dist/cjs/local.js +440 -0
  23. package/dist/cjs/onchain.d.ts +54 -0
  24. package/dist/cjs/onchain.js +314 -0
  25. package/dist/cjs/package.json +1 -0
  26. package/dist/cjs/quorum.d.ts +75 -0
  27. package/dist/cjs/quorum.js +307 -0
  28. package/dist/cjs/replicated-backup.d.ts +30 -0
  29. package/dist/cjs/replicated-backup.js +52 -0
  30. package/dist/cjs/seed.d.ts +41 -0
  31. package/dist/cjs/seed.js +84 -0
  32. package/dist/cjs/stubs.d.ts +50 -0
  33. package/dist/cjs/stubs.js +125 -0
  34. package/dist/cjs/types.d.ts +272 -0
  35. package/dist/cjs/types.js +5 -0
  36. package/dist/cjs/watermark.d.ts +51 -0
  37. package/dist/cjs/watermark.js +328 -0
  38. package/dist/device.js +2 -6
  39. package/dist/errors.d.ts +26 -0
  40. package/dist/errors.js +49 -25
  41. package/dist/hybrid.d.ts +4 -1
  42. package/dist/hybrid.js +72 -26
  43. package/dist/index.d.ts +6 -2
  44. package/dist/index.js +12 -38
  45. package/dist/journal.d.ts +14 -4
  46. package/dist/journal.js +327 -49
  47. package/dist/local.d.ts +31 -1
  48. package/dist/local.js +267 -68
  49. package/dist/onchain.d.ts +3 -1
  50. package/dist/onchain.js +63 -28
  51. package/dist/quorum.d.ts +15 -0
  52. package/dist/quorum.js +54 -15
  53. package/dist/replicated-backup.d.ts +30 -0
  54. package/dist/replicated-backup.js +48 -0
  55. package/dist/seed.d.ts +41 -0
  56. package/dist/seed.js +80 -0
  57. package/dist/stubs.d.ts +2 -0
  58. package/dist/stubs.js +20 -10
  59. package/dist/types.d.ts +74 -1
  60. package/dist/types.js +1 -2
  61. package/dist/watermark.d.ts +22 -1
  62. package/dist/watermark.js +133 -16
  63. package/package.json +25 -15
  64. package/rust/pkg/package.json +3 -3
  65. package/rust/pkg/wots_lease_wasm_bg.wasm +0 -0
  66. package/rust/pkg-node/package.json +3 -3
  67. package/rust/pkg-node/wots_lease_wasm_bg.wasm +0 -0
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,13 @@
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
- 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");
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';
30
28
  const MAX_L = 64;
29
+ const WATERMARK_COIN_STORAGE_KEY = 'wots-lease:onchain:watermark-coin';
31
30
  function multiConcat(parts) {
32
31
  if (parts.length === 0)
33
32
  return new Uint8Array(0);
@@ -46,7 +45,7 @@ function concatBytes(a, b) {
46
45
  function addressToHex(address) {
47
46
  const upper = address.startsWith('0x') || address.startsWith('0X') ? address.slice(2) : address;
48
47
  return upper.toUpperCase().startsWith('MX')
49
- ? (0, core_1.bytesToHex)((0, core_1.parseMxAddress)(address))
48
+ ? bytesToHex(parseMxAddress(address))
50
49
  : address.replace(/^0x/i, '');
51
50
  }
52
51
  function flatToHex(flat) {
@@ -77,19 +76,20 @@ function stateVarJson(port, hexValue) {
77
76
  return { port, svtype: 'hex', data: hexValue };
78
77
  }
79
78
  function buildWitnessBytes(sigs) {
80
- const parts = [(0, core_1.writeMiniNumber)(BigInt(sigs.length), 0)];
79
+ const parts = [writeMiniNumber(BigInt(sigs.length), 0)];
81
80
  for (const sig of sigs)
82
81
  parts.push(sig);
83
- parts.push((0, core_1.writeMiniNumber)(0n, 0));
84
- parts.push((0, core_1.writeMiniNumber)(0n, 0));
82
+ parts.push(writeMiniNumber(0n, 0));
83
+ parts.push(writeMiniNumber(0n, 0));
85
84
  return multiConcat(parts);
86
85
  }
87
- class OnchainWatermarkProvider {
86
+ export class OnchainWatermarkProvider {
88
87
  constructor(config) {
89
88
  this._initialized = false;
90
89
  this._lastPublishBlock = -1;
91
90
  this.chain = config.chain;
92
- this.watermarkCoinId = config.watermarkCoinId;
91
+ this.initialWatermarkCoinId = config.watermarkCoinId;
92
+ this.currentCoinId = config.watermarkCoinId;
93
93
  this.watermarkAddress = config.watermarkAddress;
94
94
  this.tokenId = config.tokenId ?? '0x00';
95
95
  this.amount = config.amount ?? '1';
@@ -97,10 +97,25 @@ class OnchainWatermarkProvider {
97
97
  this.signer = config.signer;
98
98
  this.statePort = config.statePort ?? 0;
99
99
  this.minBlocksBetweenPublishes = config.minBlocksBetweenPublishes ?? 1;
100
+ this.storage = config.storage;
100
101
  }
101
102
  async initialize() {
102
103
  if (this._initialized)
103
104
  return;
105
+ // Recover the advanced watermark coin identity across restarts.
106
+ if (this.storage) {
107
+ try {
108
+ const saved = await this.storage.get(WATERMARK_COIN_STORAGE_KEY);
109
+ if (saved?.coinId)
110
+ this.currentCoinId = saved.coinId;
111
+ if (typeof saved?.lastPublishBlock === 'number') {
112
+ this._lastPublishBlock = saved.lastPublishBlock;
113
+ }
114
+ }
115
+ catch {
116
+ // Storage unavailable — fall back to the configured coin.
117
+ }
118
+ }
104
119
  await this.local.initialize();
105
120
  this._initialized = true;
106
121
  }
@@ -127,6 +142,7 @@ class OnchainWatermarkProvider {
127
142
  expiresAt: reservation.expiresAt,
128
143
  signature: '',
129
144
  };
145
+ certificate.signature = await signCertificate(this.signer, certificate);
130
146
  return { ...reservation, certificate };
131
147
  }
132
148
  async commitKeyUse(reservationId, txId) {
@@ -153,7 +169,7 @@ class OnchainWatermarkProvider {
153
169
  async publishWatermark(treeId) {
154
170
  await this.ensureInit();
155
171
  const wm = await this.local.getLocalWatermark(treeId);
156
- const newFlat = (0, watermark_js_1.flatIndex)({
172
+ const newFlat = flatIndex({
157
173
  addressIndex: wm.addressCursor,
158
174
  l1: wm.l1Cursor,
159
175
  l2: wm.l2Cursor,
@@ -170,10 +186,11 @@ class OnchainWatermarkProvider {
170
186
  catch {
171
187
  // Tip unavailable — proceed without rate limiting.
172
188
  }
173
- // Read the current on-chain cursor so the input state matches the coin.
174
- const coin = await this.chain.getCoin(this.watermarkCoinId);
189
+ // Read the current on-chain cursor so the input state matches the coin
190
+ // anchored by the CURRENT watermark coin (which advances on every spend).
191
+ const coin = await this.chain.getCoin(this.currentCoinId);
175
192
  if (!coin) {
176
- throw new errors_js_1.OnchainWatermarkError(`Watermark coin not found: ${this.watermarkCoinId}`);
193
+ throw new OnchainWatermarkError(`Watermark coin not found: ${this.currentCoinId}`);
177
194
  }
178
195
  const currentState = coin.state ?? [];
179
196
  const currentFlat = readStateCursor(currentState, this.statePort);
@@ -182,7 +199,7 @@ class OnchainWatermarkProvider {
182
199
  return;
183
200
  }
184
201
  const inputCoin = {
185
- coinid: this.watermarkCoinId,
202
+ coinid: this.currentCoinId,
186
203
  address: addressToHex(this.watermarkAddress),
187
204
  amount: this.amount,
188
205
  tokenid: this.tokenId,
@@ -196,22 +213,39 @@ class OnchainWatermarkProvider {
196
213
  storestate: true,
197
214
  state: [stateVarJson(this.statePort, flatToHex(newFlat))],
198
215
  };
216
+ const rolledCoinId = rollCoinId(hexToBytes(this.currentCoinId), 0);
199
217
  const tx = {
200
218
  linkhash: '0x00',
201
219
  inputs: [inputCoin],
202
- outputs: [outputCoin],
220
+ outputs: [{ ...outputCoin, coinid: '0x' + bytesToHex(rolledCoinId) }],
203
221
  state: [],
204
222
  };
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);
223
+ const txBytes = serializeTransaction(JSON.stringify(tx));
224
+ const digest = computeTransactionDigest(txBytes);
208
225
  const sig = await this.signer.sign(digest);
209
226
  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');
227
+ const prng = sha3_256(new TextEncoder().encode(`watermark:${treeId}:${this.currentCoinId}:${newFlat}`));
228
+ const txHex = Buffer.from(serializeTxPoW(txBytes, witnessBytes, { prng })).toString('hex');
212
229
  const result = await this.chain.broadcastTxPoW(txHex);
213
230
  if (!result.success) {
214
- throw new errors_js_1.OnchainWatermarkError(`Watermark publish rejected: ${result.message ?? 'unknown error'}`);
231
+ throw new OnchainWatermarkError(`Watermark publish rejected: ${result.message ?? 'unknown error'}`);
232
+ }
233
+ // Rollover: the spent watermark coin is destroyed and replaced by a fresh
234
+ // coin with a new coin ID (output 0 of this spend). Track and persist the
235
+ // ADVANCED identity so the next publish spends the live coin, not the
236
+ // consumed one.
237
+ const newCoinId = '0x' + bytesToHex(rolledCoinId);
238
+ this.currentCoinId = newCoinId;
239
+ if (this.storage) {
240
+ try {
241
+ await this.storage.set(WATERMARK_COIN_STORAGE_KEY, {
242
+ coinId: newCoinId,
243
+ lastPublishBlock: this._lastPublishBlock,
244
+ });
245
+ }
246
+ catch {
247
+ // Storage unavailable — in-memory rollover still applies this session.
248
+ }
215
249
  }
216
250
  }
217
251
  // -------------------------------------------------------------------------
@@ -223,14 +257,14 @@ class OnchainWatermarkProvider {
223
257
  let advancedTo;
224
258
  for (const treeId of this.local.listTrees()) {
225
259
  const localWm = await this.local.getLocalWatermark(treeId);
226
- const localFlat = (0, watermark_js_1.flatIndex)({
260
+ const localFlat = flatIndex({
227
261
  addressIndex: localWm.addressCursor,
228
262
  l1: localWm.l1Cursor,
229
263
  l2: localWm.l2Cursor,
230
264
  });
231
265
  let onchainFlat = null;
232
266
  try {
233
- const coin = await this.chain.getCoin(this.watermarkCoinId);
267
+ const coin = await this.chain.getCoin(this.currentCoinId);
234
268
  if (coin)
235
269
  onchainFlat = readStateCursor(coin.state ?? [], this.statePort);
236
270
  }
@@ -269,7 +303,8 @@ class OnchainWatermarkProvider {
269
303
  return false;
270
304
  if (cert.expiresAt <= Date.now())
271
305
  return false;
306
+ if (!(await certificateSignatureVerified(cert, this.signer)))
307
+ return false;
272
308
  return cert.issuedBy === 'onchain-watermark';
273
309
  }
274
310
  }
275
- exports.OnchainWatermarkProvider = OnchainWatermarkProvider;
package/dist/quorum.d.ts CHANGED
@@ -34,6 +34,13 @@ export declare class P2PQuorumLeaseProvider implements WotsLeaseProvider {
34
34
  private readonly local;
35
35
  private readonly requestTimeoutMs;
36
36
  private readonly requireQuorumOnCommit;
37
+ private readonly certificateSigner?;
38
+ /**
39
+ * localReservationId -> (peerId -> the reservation id that peer issued).
40
+ * Commit/burn must reference each peer's own id, because a peer only knows
41
+ * the reservation it issued, not the local provider's id (AUD-044).
42
+ */
43
+ private readonly peerReservationIds;
37
44
  private _initialized;
38
45
  constructor(config: P2PQuorumLeaseProviderConfig);
39
46
  initialize(): Promise<void>;
@@ -52,6 +59,14 @@ export declare class P2PQuorumLeaseProvider implements WotsLeaseProvider {
52
59
  }): Promise<QuorumAttestation[]>;
53
60
  commitKeyUse(reservationId: string, txId: string): Promise<void>;
54
61
  burnReservation(reservationId: string, reason: string): Promise<void>;
62
+ /** Record which peer issued which reservation id for a local reservation. */
63
+ private recordPeerReservationIds;
64
+ /**
65
+ * The id to send a peer for a local reservation. Peers only know the id they
66
+ * issued; fall back to the local id only when no peer id was recorded (e.g.
67
+ * attestations gathered outside `reserveKeyUse`).
68
+ */
69
+ private peerReservationIdFor;
55
70
  private broadcastAck;
56
71
  getLocalWatermark(treeId: string): Promise<LocalWatermark>;
57
72
  publishWatermark(treeId: string): Promise<void>;
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,20 +27,26 @@
28
27
  * - If a peer reports the slot as taken, the local reservation is burned
29
28
  * and QuorumConflictError is thrown.
30
29
  */
31
- Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.P2PQuorumLeaseProvider = void 0;
33
- const errors_js_1 = require("./errors.js");
30
+ import { QuorumUnavailableError, QuorumConflictError, } from './errors.js';
31
+ import { signCertificate, certificateSignatureVerified, } from './certificate.js';
34
32
  function sameIndices(a, b) {
35
33
  return a.addressIndex === b.addressIndex && a.l1 === b.l1 && a.l2 === b.l2;
36
34
  }
37
- class P2PQuorumLeaseProvider {
35
+ export class P2PQuorumLeaseProvider {
38
36
  constructor(config) {
37
+ /**
38
+ * localReservationId -> (peerId -> the reservation id that peer issued).
39
+ * Commit/burn must reference each peer's own id, because a peer only knows
40
+ * the reservation it issued, not the local provider's id (AUD-044).
41
+ */
42
+ this.peerReservationIds = new Map();
39
43
  this._initialized = false;
40
44
  this.peers = config.peers;
41
45
  this.minAttestations = config.minAttestations ?? 1;
42
46
  this.local = config.local;
43
47
  this.requestTimeoutMs = config.requestTimeoutMs ?? 5000;
44
48
  this.requireQuorumOnCommit = config.requireQuorumOnCommit ?? false;
49
+ this.certificateSigner = config.certificateSigner;
45
50
  }
46
51
  async initialize() {
47
52
  if (this._initialized)
@@ -67,12 +72,13 @@ class P2PQuorumLeaseProvider {
67
72
  attestations = await this.attestKeyUse(params, reservation.indices);
68
73
  }
69
74
  catch (err) {
70
- const reason = err instanceof errors_js_1.QuorumConflictError
75
+ const reason = err instanceof QuorumConflictError
71
76
  ? `quorum conflict: ${err.message}`
72
77
  : 'quorum not reached';
73
78
  await this.local.burnReservation(reservation.reservationId, reason);
74
79
  throw err;
75
80
  }
81
+ this.recordPeerReservationIds(reservation.reservationId, attestations);
76
82
  const certificate = {
77
83
  reservationId: reservation.reservationId,
78
84
  treeId: params.treeId,
@@ -81,12 +87,13 @@ class P2PQuorumLeaseProvider {
81
87
  indices: reservation.indices,
82
88
  purpose: params.purpose,
83
89
  payloadHash: params.payloadHash,
84
- issuedBy: 'p2p-quorum',
90
+ issuedBy: this.certificateSigner?.name ?? 'p2p-quorum',
85
91
  issuedAt: Date.now(),
86
92
  expiresAt: reservation.expiresAt,
87
93
  signature: '',
88
94
  attestations,
89
95
  };
96
+ certificate.signature = this.certificateSigner ? await signCertificate(this.certificateSigner, certificate) : '';
90
97
  return { ...reservation, certificate };
91
98
  }
92
99
  /**
@@ -123,10 +130,14 @@ class P2PQuorumLeaseProvider {
123
130
  conflicts.push('peer returned mismatched indices');
124
131
  return;
125
132
  }
133
+ const peerReservationId = body.reservation?.reservationId;
126
134
  attestations.push({
127
135
  peerId: peer.peerId,
128
136
  indices,
129
137
  expiresAt: body.reservation?.expiresAt ?? Date.now() + (params.ttlMs ?? 120000),
138
+ ...(typeof peerReservationId === 'string' && peerReservationId.length > 0
139
+ ? { peerReservationId }
140
+ : {}),
130
141
  });
131
142
  }
132
143
  catch {
@@ -134,10 +145,10 @@ class P2PQuorumLeaseProvider {
134
145
  }
135
146
  }));
136
147
  if (conflicts.length > 0) {
137
- throw new errors_js_1.QuorumConflictError(params.treeId, indices);
148
+ throw new QuorumConflictError(params.treeId, indices);
138
149
  }
139
150
  if (attestations.length < this.minAttestations) {
140
- throw new errors_js_1.QuorumUnavailableError(this.minAttestations, attestations.length);
151
+ throw new QuorumUnavailableError(this.minAttestations, attestations.length);
141
152
  }
142
153
  return attestations;
143
154
  }
@@ -147,21 +158,46 @@ class P2PQuorumLeaseProvider {
147
158
  async commitKeyUse(reservationId, txId) {
148
159
  await this.ensureInit();
149
160
  await this.local.commitKeyUse(reservationId, txId);
150
- const acks = await this.broadcastAck('LEASE_COMMIT', { reservationId, txId });
161
+ const acks = await this.broadcastAck('LEASE_COMMIT', (peerId) => ({
162
+ reservationId: this.peerReservationIdFor(reservationId, peerId),
163
+ txId,
164
+ }));
151
165
  if (this.requireQuorumOnCommit && acks < this.minAttestations) {
152
- throw new errors_js_1.QuorumUnavailableError(this.minAttestations, acks);
166
+ throw new QuorumUnavailableError(this.minAttestations, acks);
153
167
  }
154
168
  }
155
169
  async burnReservation(reservationId, reason) {
156
170
  await this.ensureInit();
157
171
  await this.local.burnReservation(reservationId, reason);
158
- await this.broadcastAck('LEASE_BURN', { reservationId, reason });
172
+ await this.broadcastAck('LEASE_BURN', (peerId) => ({
173
+ reservationId: this.peerReservationIdFor(reservationId, peerId),
174
+ reason,
175
+ }));
176
+ }
177
+ /** Record which peer issued which reservation id for a local reservation. */
178
+ recordPeerReservationIds(localReservationId, attestations) {
179
+ const byPeer = new Map();
180
+ for (const attestation of attestations) {
181
+ if (attestation.peerReservationId) {
182
+ byPeer.set(attestation.peerId, attestation.peerReservationId);
183
+ }
184
+ }
185
+ if (byPeer.size > 0)
186
+ this.peerReservationIds.set(localReservationId, byPeer);
159
187
  }
160
- async broadcastAck(type, payload) {
188
+ /**
189
+ * The id to send a peer for a local reservation. Peers only know the id they
190
+ * issued; fall back to the local id only when no peer id was recorded (e.g.
191
+ * attestations gathered outside `reserveKeyUse`).
192
+ */
193
+ peerReservationIdFor(localReservationId, peerId) {
194
+ return this.peerReservationIds.get(localReservationId)?.get(peerId) ?? localReservationId;
195
+ }
196
+ async broadcastAck(type, payloadFor) {
161
197
  let acks = 0;
162
198
  await Promise.all(this.peers.map(async (peer) => {
163
199
  try {
164
- const resp = await peer.request({ type, payload }, this.requestTimeoutMs);
200
+ const resp = await peer.request({ type, payload: payloadFor(peer.peerId) }, this.requestTimeoutMs);
165
201
  if (resp.type !== 'ERROR')
166
202
  acks++;
167
203
  }
@@ -254,6 +290,10 @@ class P2PQuorumLeaseProvider {
254
290
  return false;
255
291
  if (cert.expiresAt <= Date.now())
256
292
  return false;
293
+ // An unsigned certificate carries no authenticity evidence.
294
+ if (!(await certificateSignatureVerified(cert, this.certificateSigner))) {
295
+ return false;
296
+ }
257
297
  const attestations = cert.attestations ?? [];
258
298
  if (attestations.length < this.minAttestations)
259
299
  return false;
@@ -261,4 +301,3 @@ class P2PQuorumLeaseProvider {
261
301
  a.expiresAt > Date.now());
262
302
  }
263
303
  }
264
- 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,48 @@
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 { StorageError } from '@totemsdk/storage/errors';
10
+ export class ReplicatedLeaseJournalBackup {
11
+ constructor(log) {
12
+ this.log = log;
13
+ }
14
+ /** Mirror one local journal entry. */
15
+ async append(entry) {
16
+ return this.log.append(entry);
17
+ }
18
+ /** Read all mirrored entries, verifying the local hash-chain links in order. */
19
+ async readAll() {
20
+ const entries = [];
21
+ for await (const entry of this.log.readSince())
22
+ entries.push(entry.record);
23
+ let previous;
24
+ for (const [index, entry] of entries.entries()) {
25
+ if (index === 0) {
26
+ if (entry.previousHash !== undefined) {
27
+ throw new StorageError('replicated lease journal first entry has previousHash', 'corrupt');
28
+ }
29
+ }
30
+ else if (entry.previousHash !== previous?.hash) {
31
+ throw new StorageError(`replicated lease journal chain mismatch at ${index}`, 'corrupt');
32
+ }
33
+ previous = entry;
34
+ }
35
+ return entries;
36
+ }
37
+ /**
38
+ * Restore into a local journal. The local journal's own duplicate-slot and
39
+ * reservation checks remain the final guard; this method only preserves the
40
+ * replicated order and links.
41
+ */
42
+ async restoreInto(target) {
43
+ const entries = await this.readAll();
44
+ for (const entry of entries)
45
+ await target.append(entry);
46
+ return entries.length;
47
+ }
48
+ }
package/dist/seed.d.ts ADDED
@@ -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/seed.js ADDED
@@ -0,0 +1,80 @@
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 { flatIndex, fromFlatIndex } from './watermark.js';
16
+ import { WatermarkSeedRequiredError } from './errors.js';
17
+ function flatOf(w) {
18
+ return flatIndex({ addressIndex: w.addressCursor, l1: w.l1Cursor, l2: w.l2Cursor });
19
+ }
20
+ /**
21
+ * Seed a local watermark from the best available source, requiring at least one
22
+ * source unless `fresh` is set. Returns the resulting local watermark.
23
+ *
24
+ * @throws {WatermarkSeedRequiredError} when no source is available and the
25
+ * keyspace is not declared fresh.
26
+ * @throws {WatermarkSeedRegressionError} when a supplied seed is behind the
27
+ * current cursor.
28
+ */
29
+ export async function seedLeaseWatermark(params) {
30
+ const { local, treeId } = params;
31
+ // 1. On-chain cursor — advances the same local watermark (monotonic).
32
+ if (params.onchain) {
33
+ try {
34
+ await params.onchain.syncLeaseJournal();
35
+ }
36
+ catch {
37
+ // Chain unreachable — fall through to exported/operator sources below.
38
+ }
39
+ }
40
+ const current = await local.getLocalWatermark(treeId);
41
+ let bestFlat = flatOf({ addressCursor: current.addressCursor, l1Cursor: current.l1Cursor, l2Cursor: current.l2Cursor });
42
+ let best;
43
+ // 2. Exported blob.
44
+ if (params.exported) {
45
+ if (flatOf(params.exported) > bestFlat) {
46
+ best = params.exported;
47
+ bestFlat = flatOf(params.exported);
48
+ }
49
+ }
50
+ // 3. Operator high-water mark.
51
+ if (params.operatorHighWaterMark !== undefined) {
52
+ const indices = fromFlatIndex(params.operatorHighWaterMark);
53
+ const candidate = {
54
+ version: 1,
55
+ treeId,
56
+ addressCursor: indices.addressIndex,
57
+ l1Cursor: indices.l1,
58
+ l2Cursor: indices.l2,
59
+ unavailable: [],
60
+ };
61
+ if (flatOf(candidate) > bestFlat) {
62
+ best = candidate;
63
+ bestFlat = flatOf(candidate);
64
+ }
65
+ }
66
+ if (best) {
67
+ await local.seedWatermark(best);
68
+ }
69
+ if (!params.onchain &&
70
+ !params.exported &&
71
+ params.operatorHighWaterMark === undefined &&
72
+ !params.fresh) {
73
+ throw new WatermarkSeedRequiredError(treeId);
74
+ }
75
+ return local.getLocalWatermark(treeId);
76
+ }
77
+ /** Export a portable watermark blob for backup/restore. */
78
+ export function exportLeaseWatermark(local, treeId) {
79
+ return local.exportWatermark(treeId);
80
+ }
package/dist/stubs.d.ts CHANGED
@@ -34,6 +34,8 @@ export declare class PersonalLeaseNodeProvider implements WotsLeaseProvider {
34
34
  private readonly baseUrl;
35
35
  private readonly nodePubkey;
36
36
  private readonly authToken?;
37
+ private readonly certificateSigner?;
38
+ private readonly verify?;
37
39
  constructor(config: PersonalLeaseNodeConfig);
38
40
  private headers;
39
41
  private post;
package/dist/stubs.js CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * Implementations for Layers 3–5 of the WotsLeaseProvider hierarchy.
4
3
  *
@@ -9,12 +8,9 @@
9
8
  * Layers 4–5 (P2PQuorumLeaseProvider, OnchainWatermarkProvider) — implemented in
10
9
  * quorum.ts and onchain.ts; re-exported here for backward compatibility.
11
10
  */
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.PersonalLeaseNodeProvider = exports.OnchainWatermarkProvider = exports.P2PQuorumLeaseProvider = void 0;
14
- var quorum_js_1 = require("./quorum.js");
15
- Object.defineProperty(exports, "P2PQuorumLeaseProvider", { enumerable: true, get: function () { return quorum_js_1.P2PQuorumLeaseProvider; } });
16
- var onchain_js_1 = require("./onchain.js");
17
- Object.defineProperty(exports, "OnchainWatermarkProvider", { enumerable: true, get: function () { return onchain_js_1.OnchainWatermarkProvider; } });
11
+ import { certificateSignatureVerified } from './certificate.js';
12
+ export { P2PQuorumLeaseProvider } from './quorum.js';
13
+ export { OnchainWatermarkProvider } from './onchain.js';
18
14
  /**
19
15
  * Layer 3 — personal lookup-node lease coordinator.
20
16
  *
@@ -34,11 +30,13 @@ Object.defineProperty(exports, "OnchainWatermarkProvider", { enumerable: true, g
34
30
  * });
35
31
  * ```
36
32
  */
37
- class PersonalLeaseNodeProvider {
33
+ export class PersonalLeaseNodeProvider {
38
34
  constructor(config) {
39
35
  this.baseUrl = config.nodeUrl.replace(/\/$/, '');
40
36
  this.nodePubkey = config.nodePubkey;
41
37
  this.authToken = config.authToken;
38
+ this.certificateSigner = config.certificateSigner;
39
+ this.verify = config.verify;
42
40
  }
43
41
  headers() {
44
42
  const h = { 'Content-Type': 'application/json' };
@@ -103,7 +101,19 @@ class PersonalLeaseNodeProvider {
103
101
  return false;
104
102
  if (cert.expiresAt <= Date.now())
105
103
  return false;
106
- return true;
104
+ // AUD-016: fail closed. The provider can only verify a certificate when it
105
+ // holds a verifier; an `issuedBy` match is not authenticity evidence.
106
+ const signer = this.certificateSigner?.verify
107
+ ? this.certificateSigner
108
+ : this.verify
109
+ ? {
110
+ publicKeyDigest: this.nodePubkey,
111
+ sign: async () => {
112
+ throw new Error('PersonalLeaseNodeProvider holds a verify-only signer and cannot sign');
113
+ },
114
+ verify: this.verify,
115
+ }
116
+ : undefined;
117
+ return certificateSignatureVerified(cert, signer);
107
118
  }
108
119
  }
109
- exports.PersonalLeaseNodeProvider = PersonalLeaseNodeProvider;