@totemsdk/wots-lease 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Stub implementations for Layers 3–5.
3
+ * These throw descriptive errors until the full implementations land.
4
+ */
5
+ import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, PersonalLeaseNodeConfig } from './types.js';
6
+ /**
7
+ * Layer 3 — personal p2p lease node.
8
+ *
9
+ * Verifies ed25519 certs issued by a configured node public key.
10
+ * Reserve/commit throws until the node is reachable — callers should
11
+ * wrap with HybridLeaseProvider using a LocalLeaseProvider as the local layer.
12
+ */
13
+ export declare class PersonalLeaseNodeProvider implements WotsLeaseProvider {
14
+ private readonly config;
15
+ constructor(config: PersonalLeaseNodeConfig);
16
+ reserveKeyUse(_params: ReserveParams): Promise<LeaseReservation>;
17
+ commitKeyUse(_reservationId: string, _txId: string): Promise<void>;
18
+ burnReservation(_reservationId: string, _reason: string): Promise<void>;
19
+ getLocalWatermark(_treeId: string): Promise<LocalWatermark>;
20
+ publishWatermark(_treeId: string): Promise<void>;
21
+ syncLeaseJournal(): Promise<SyncResult>;
22
+ verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
23
+ }
24
+ /** Layer 4 — p2p replicated lease witnesses. */
25
+ export declare class P2PQuorumLeaseProvider implements WotsLeaseProvider {
26
+ reserveKeyUse(_params: ReserveParams): Promise<LeaseReservation>;
27
+ commitKeyUse(_reservationId: string, _txId: string): Promise<void>;
28
+ burnReservation(_reservationId: string, _reason: string): Promise<void>;
29
+ getLocalWatermark(_treeId: string): Promise<LocalWatermark>;
30
+ publishWatermark(_treeId: string): Promise<void>;
31
+ syncLeaseJournal(): Promise<SyncResult>;
32
+ verifyLeaseCertificate(_cert?: LeaseCertificate): Promise<boolean>;
33
+ }
34
+ /** Layer 5 — on-chain watermark coin. */
35
+ export declare class OnchainWatermarkProvider implements WotsLeaseProvider {
36
+ reserveKeyUse(_params: ReserveParams): Promise<LeaseReservation>;
37
+ commitKeyUse(_reservationId: string, _txId: string): Promise<void>;
38
+ burnReservation(_reservationId: string, _reason: string): Promise<void>;
39
+ getLocalWatermark(_treeId: string): Promise<LocalWatermark>;
40
+ publishWatermark(_treeId: string): Promise<void>;
41
+ syncLeaseJournal(): Promise<SyncResult>;
42
+ verifyLeaseCertificate(_cert?: LeaseCertificate): Promise<boolean>;
43
+ }
package/dist/stubs.js ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Stub implementations for Layers 3–5.
3
+ * These throw descriptive errors until the full implementations land.
4
+ */
5
+ import { PersonalLeaseNodeNotConfiguredError, P2PQuorumNotImplementedError, OnchainWatermarkNotImplementedError, } from './errors.js';
6
+ /**
7
+ * Layer 3 — personal p2p lease node.
8
+ *
9
+ * Verifies ed25519 certs issued by a configured node public key.
10
+ * Reserve/commit throws until the node is reachable — callers should
11
+ * wrap with HybridLeaseProvider using a LocalLeaseProvider as the local layer.
12
+ */
13
+ export class PersonalLeaseNodeProvider {
14
+ constructor(config) {
15
+ this.config = config;
16
+ }
17
+ reserveKeyUse(_params) {
18
+ throw new PersonalLeaseNodeNotConfiguredError();
19
+ }
20
+ commitKeyUse(_reservationId, _txId) {
21
+ throw new PersonalLeaseNodeNotConfiguredError();
22
+ }
23
+ burnReservation(_reservationId, _reason) {
24
+ throw new PersonalLeaseNodeNotConfiguredError();
25
+ }
26
+ getLocalWatermark(_treeId) {
27
+ throw new PersonalLeaseNodeNotConfiguredError();
28
+ }
29
+ publishWatermark(_treeId) {
30
+ throw new PersonalLeaseNodeNotConfiguredError();
31
+ }
32
+ syncLeaseJournal() {
33
+ throw new PersonalLeaseNodeNotConfiguredError();
34
+ }
35
+ async verifyLeaseCertificate(cert) {
36
+ if (!cert)
37
+ return false;
38
+ if (cert.issuedBy !== this.config.nodePubkey)
39
+ return false;
40
+ if (cert.expiresAt <= Date.now())
41
+ return false;
42
+ return true;
43
+ }
44
+ }
45
+ /** Layer 4 — p2p replicated lease witnesses. */
46
+ export class P2PQuorumLeaseProvider {
47
+ reserveKeyUse(_params) {
48
+ throw new P2PQuorumNotImplementedError();
49
+ }
50
+ commitKeyUse(_reservationId, _txId) {
51
+ throw new P2PQuorumNotImplementedError();
52
+ }
53
+ burnReservation(_reservationId, _reason) {
54
+ throw new P2PQuorumNotImplementedError();
55
+ }
56
+ getLocalWatermark(_treeId) {
57
+ throw new P2PQuorumNotImplementedError();
58
+ }
59
+ publishWatermark(_treeId) {
60
+ throw new P2PQuorumNotImplementedError();
61
+ }
62
+ syncLeaseJournal() {
63
+ throw new P2PQuorumNotImplementedError();
64
+ }
65
+ verifyLeaseCertificate(_cert) {
66
+ throw new P2PQuorumNotImplementedError();
67
+ }
68
+ }
69
+ /** Layer 5 — on-chain watermark coin. */
70
+ export class OnchainWatermarkProvider {
71
+ reserveKeyUse(_params) {
72
+ throw new OnchainWatermarkNotImplementedError();
73
+ }
74
+ commitKeyUse(_reservationId, _txId) {
75
+ throw new OnchainWatermarkNotImplementedError();
76
+ }
77
+ burnReservation(_reservationId, _reason) {
78
+ throw new OnchainWatermarkNotImplementedError();
79
+ }
80
+ getLocalWatermark(_treeId) {
81
+ throw new OnchainWatermarkNotImplementedError();
82
+ }
83
+ publishWatermark(_treeId) {
84
+ throw new OnchainWatermarkNotImplementedError();
85
+ }
86
+ syncLeaseJournal() {
87
+ throw new OnchainWatermarkNotImplementedError();
88
+ }
89
+ verifyLeaseCertificate(_cert) {
90
+ throw new OnchainWatermarkNotImplementedError();
91
+ }
92
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @totemsdk/wots-lease — canonical v3 watermark types and WotsLeaseProvider interface
3
+ */
4
+ export type UnavailableReason = 'committed' | 'burned' | 'reserved-expired';
5
+ export interface SigningIndices {
6
+ addressIndex: number;
7
+ l1: number;
8
+ l2: number;
9
+ }
10
+ export type LeaseStatus = 'pending' | 'active' | 'expired' | 'finalized' | 'cancelled';
11
+ export interface TreeWatermark {
12
+ treeId: string;
13
+ deviceId?: string;
14
+ branchId?: string;
15
+ addressCursor: number;
16
+ l1Cursor: number;
17
+ l2Cursor: number;
18
+ unavailable: Record<number, UnavailableReason>;
19
+ lastSyncTimestamp?: number;
20
+ }
21
+ export interface WotsWatermarkState {
22
+ version: 3;
23
+ trees: Record<string, TreeWatermark>;
24
+ }
25
+ export interface LeaseCertificate {
26
+ reservationId: string;
27
+ treeId: string;
28
+ branchId?: string;
29
+ deviceId?: string;
30
+ indices: SigningIndices;
31
+ purpose?: string;
32
+ payloadHash?: string;
33
+ issuedBy: string;
34
+ issuedAt: number;
35
+ expiresAt: number;
36
+ signature: string;
37
+ }
38
+ export interface LeaseReservation {
39
+ reservationId: string;
40
+ indices: SigningIndices;
41
+ expiresAt: number;
42
+ certificate?: LeaseCertificate;
43
+ leaseToken?: string;
44
+ }
45
+ export interface ReserveParams {
46
+ treeId: string;
47
+ branchId?: string;
48
+ purpose?: string;
49
+ deviceId?: string;
50
+ ttlMs?: number;
51
+ payloadHash?: string;
52
+ valueHint?: string;
53
+ }
54
+ export interface LocalWatermark {
55
+ treeId: string;
56
+ addressCursor: number;
57
+ l1Cursor: number;
58
+ l2Cursor: number;
59
+ unavailableCount: number;
60
+ capacity: number;
61
+ lastSyncTimestamp?: number;
62
+ }
63
+ export interface ConflictRecord {
64
+ treeId: string;
65
+ localIndex: number;
66
+ remoteIndex: number;
67
+ timestamp: number;
68
+ }
69
+ export interface SyncResult {
70
+ synced: boolean;
71
+ conflicts: ConflictRecord[];
72
+ advancedTo?: SigningIndices;
73
+ }
74
+ export interface WotsLeaseProvider {
75
+ reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
76
+ commitKeyUse(reservationId: string, txId: string): Promise<void>;
77
+ burnReservation(reservationId: string, reason: string): Promise<void>;
78
+ getLocalWatermark(treeId: string): Promise<LocalWatermark>;
79
+ publishWatermark(treeId: string): Promise<void>;
80
+ syncLeaseJournal(): Promise<SyncResult>;
81
+ verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
82
+ }
83
+ export interface JournalEntry {
84
+ treeId: string;
85
+ branchId: string;
86
+ wotsIndex: number;
87
+ indices: SigningIndices;
88
+ status: 'reserved' | 'committed' | 'burned' | 'reserved-expired';
89
+ payloadHash?: string;
90
+ txId?: string;
91
+ timestamp: number;
92
+ deviceId: string;
93
+ }
94
+ export interface DeviceKeyRange {
95
+ deviceId: string;
96
+ startAddressIndex: number;
97
+ endAddressIndex: number;
98
+ addressCount: number;
99
+ }
100
+ export interface PersonalLeaseNodeConfig {
101
+ nodeUrl: string;
102
+ nodePubkey: string;
103
+ authToken?: string;
104
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @totemsdk/wots-lease — canonical v3 watermark types and WotsLeaseProvider interface
3
+ */
4
+ export {};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * WotsWatermarkStore — canonical v3 implementation
3
+ *
4
+ * - Reads v1 (flat usedIndices) and v2 (per-address usedIndices) automatically on load.
5
+ * - Writes only v3.
6
+ * - Enforces monotonicity: unavailable map can only grow, cursors can only advance.
7
+ * - Storage key unchanged ('totem_wots_watermark') to avoid storage churn.
8
+ */
9
+ import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
10
+ import type { WotsWatermarkState, TreeWatermark, SigningIndices, UnavailableReason, LocalWatermark } from './types.js';
11
+ export declare function flatIndex(idx: SigningIndices): number;
12
+ export declare function fromFlatIndex(flat: number): SigningIndices;
13
+ export declare class WotsWatermarkStore {
14
+ private readonly storage;
15
+ private state;
16
+ private _initialized;
17
+ private readonly logger;
18
+ constructor(storage: StorageAdapter, logger?: LoggerAdapter);
19
+ initialize(): Promise<void>;
20
+ private ensureInit;
21
+ private persist;
22
+ private getOrCreateTree;
23
+ getNextIndices(treeId: string): SigningIndices;
24
+ markUnavailable(treeId: string, indices: SigningIndices, reason: UnavailableReason): Promise<void>;
25
+ isUnavailable(treeId: string, indices: SigningIndices): boolean;
26
+ save(treeId: string, patch: Partial<TreeWatermark>): Promise<void>;
27
+ getLocalWatermark(treeId: string): LocalWatermark;
28
+ getRawState(): WotsWatermarkState;
29
+ clear(): Promise<void>;
30
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * WotsWatermarkStore — canonical v3 implementation
3
+ *
4
+ * - Reads v1 (flat usedIndices) and v2 (per-address usedIndices) automatically on load.
5
+ * - Writes only v3.
6
+ * - Enforces monotonicity: unavailable map can only grow, cursors can only advance.
7
+ * - Storage key unchanged ('totem_wots_watermark') to avoid storage churn.
8
+ */
9
+ import { NoopLogger } from '@totemsdk/core';
10
+ import { WatermarkMonotonicityError, WatermarkExhaustedError } from './errors.js';
11
+ const STORAGE_KEY = 'totem_wots_watermark';
12
+ const MAX_L = 64;
13
+ const CAPACITY_PER_TREE = MAX_L * MAX_L * MAX_L;
14
+ const DEFAULT_TREE = 'default';
15
+ export function flatIndex(idx) {
16
+ return idx.addressIndex * MAX_L * MAX_L + idx.l1 * MAX_L + idx.l2;
17
+ }
18
+ export function fromFlatIndex(flat) {
19
+ const addressIndex = Math.floor(flat / (MAX_L * MAX_L));
20
+ const rem = flat % (MAX_L * MAX_L);
21
+ const l1 = Math.floor(rem / MAX_L);
22
+ const l2 = rem % MAX_L;
23
+ return { addressIndex, l1, l2 };
24
+ }
25
+ function emptyTree(treeId) {
26
+ return { treeId, addressCursor: 0, l1Cursor: 0, l2Cursor: 0, unavailable: {} };
27
+ }
28
+ function nextIndices(cur) {
29
+ let { addressIndex, l1, l2 } = cur;
30
+ l2++;
31
+ if (l2 >= MAX_L) {
32
+ l2 = 0;
33
+ l1++;
34
+ }
35
+ if (l1 >= MAX_L) {
36
+ l1 = 0;
37
+ addressIndex++;
38
+ }
39
+ if (addressIndex >= MAX_L)
40
+ return null;
41
+ return { addressIndex, l1, l2 };
42
+ }
43
+ function migrateToV3(raw) {
44
+ if (!raw) {
45
+ return { version: 3, trees: {} };
46
+ }
47
+ if ('version' in raw && raw.version === 3) {
48
+ return raw;
49
+ }
50
+ if ('version' in raw && raw.version === 2) {
51
+ const v2 = raw;
52
+ const tree = emptyTree(DEFAULT_TREE);
53
+ let maxFlat = -1;
54
+ for (const [addrIdxStr, addrData] of Object.entries(v2.addresses)) {
55
+ const addressIndex = Number(addrIdxStr);
56
+ for (const [l1, l2] of addrData.usedIndices) {
57
+ const f = flatIndex({ addressIndex, l1, l2 });
58
+ tree.unavailable[f] = 'committed';
59
+ if (f > maxFlat)
60
+ maxFlat = f;
61
+ }
62
+ const curFlat = flatIndex({ addressIndex, l1: addrData.next_l1, l2: addrData.next_l2 });
63
+ if (curFlat > maxFlat)
64
+ maxFlat = curFlat;
65
+ }
66
+ if (maxFlat >= 0) {
67
+ const cur = fromFlatIndex(maxFlat);
68
+ tree.addressCursor = cur.addressIndex;
69
+ tree.l1Cursor = cur.l1;
70
+ tree.l2Cursor = cur.l2;
71
+ }
72
+ return { version: 3, trees: { [DEFAULT_TREE]: tree } };
73
+ }
74
+ const v1 = raw;
75
+ const tree = emptyTree(DEFAULT_TREE);
76
+ for (const [ai, l1, l2] of v1.usedIndices ?? []) {
77
+ const f = flatIndex({ addressIndex: ai, l1, l2 });
78
+ tree.unavailable[f] = 'committed';
79
+ }
80
+ tree.addressCursor = v1.next_addressIndex ?? 0;
81
+ tree.l1Cursor = v1.next_l1 ?? 0;
82
+ tree.l2Cursor = v1.next_l2 ?? 0;
83
+ return { version: 3, trees: { [DEFAULT_TREE]: tree } };
84
+ }
85
+ export class WotsWatermarkStore {
86
+ constructor(storage, logger = new NoopLogger()) {
87
+ this.storage = storage;
88
+ this.state = null;
89
+ this._initialized = false;
90
+ this.logger = logger;
91
+ }
92
+ async initialize() {
93
+ if (this._initialized)
94
+ return;
95
+ const raw = await this.storage.get(STORAGE_KEY);
96
+ this.state = migrateToV3(raw);
97
+ if (!('version' in (raw ?? {})) || raw.version !== 3) {
98
+ await this.persist();
99
+ this.logger.info('[WotsWatermarkStore] Migrated to v3');
100
+ }
101
+ this._initialized = true;
102
+ }
103
+ ensureInit() {
104
+ if (!this.state)
105
+ throw new Error('WotsWatermarkStore not initialized — call initialize() first');
106
+ return this.state;
107
+ }
108
+ async persist() {
109
+ await this.storage.set(STORAGE_KEY, this.state);
110
+ }
111
+ getOrCreateTree(treeId) {
112
+ const s = this.ensureInit();
113
+ if (!s.trees[treeId]) {
114
+ s.trees[treeId] = emptyTree(treeId);
115
+ }
116
+ return s.trees[treeId];
117
+ }
118
+ getNextIndices(treeId) {
119
+ const tree = this.getOrCreateTree(treeId);
120
+ let cur = {
121
+ addressIndex: tree.addressCursor,
122
+ l1: tree.l1Cursor,
123
+ l2: tree.l2Cursor,
124
+ };
125
+ let attempts = 0;
126
+ while (attempts < CAPACITY_PER_TREE) {
127
+ if (!tree.unavailable[flatIndex(cur)])
128
+ return cur;
129
+ const nxt = nextIndices(cur);
130
+ if (!nxt)
131
+ break;
132
+ cur = nxt;
133
+ attempts++;
134
+ }
135
+ throw new WatermarkExhaustedError(treeId);
136
+ }
137
+ async markUnavailable(treeId, indices, reason) {
138
+ const tree = this.getOrCreateTree(treeId);
139
+ const f = flatIndex(indices);
140
+ tree.unavailable[f] = reason;
141
+ const nxt = nextIndices(indices);
142
+ if (nxt) {
143
+ const nxtFlat = flatIndex(nxt);
144
+ const curFlat = flatIndex({ addressIndex: tree.addressCursor, l1: tree.l1Cursor, l2: tree.l2Cursor });
145
+ if (nxtFlat > curFlat) {
146
+ tree.addressCursor = nxt.addressIndex;
147
+ tree.l1Cursor = nxt.l1;
148
+ tree.l2Cursor = nxt.l2;
149
+ }
150
+ }
151
+ await this.persist();
152
+ }
153
+ isUnavailable(treeId, indices) {
154
+ const s = this.ensureInit();
155
+ const tree = s.trees[treeId];
156
+ if (!tree)
157
+ return false;
158
+ return flatIndex(indices) in tree.unavailable;
159
+ }
160
+ async save(treeId, patch) {
161
+ const s = this.ensureInit();
162
+ const existing = s.trees[treeId] ?? emptyTree(treeId);
163
+ const merged = { ...existing, ...patch, treeId };
164
+ const existingFlat = flatIndex({ addressIndex: existing.addressCursor, l1: existing.l1Cursor, l2: existing.l2Cursor });
165
+ const newFlat = flatIndex({ addressIndex: merged.addressCursor, l1: merged.l1Cursor, l2: merged.l2Cursor });
166
+ if (newFlat < existingFlat) {
167
+ throw new WatermarkMonotonicityError(`Cannot decrease watermark cursor for ${treeId}: ${existingFlat} → ${newFlat}`);
168
+ }
169
+ const existingUnavailCount = Object.keys(existing.unavailable).length;
170
+ const newUnavailCount = Object.keys(merged.unavailable).length;
171
+ if (newUnavailCount < existingUnavailCount) {
172
+ throw new WatermarkMonotonicityError(`Cannot shrink unavailable map for ${treeId}: ${existingUnavailCount} → ${newUnavailCount}`);
173
+ }
174
+ s.trees[treeId] = merged;
175
+ await this.persist();
176
+ }
177
+ getLocalWatermark(treeId) {
178
+ const s = this.ensureInit();
179
+ const tree = s.trees[treeId] ?? emptyTree(treeId);
180
+ return {
181
+ treeId,
182
+ addressCursor: tree.addressCursor,
183
+ l1Cursor: tree.l1Cursor,
184
+ l2Cursor: tree.l2Cursor,
185
+ unavailableCount: Object.keys(tree.unavailable).length,
186
+ capacity: CAPACITY_PER_TREE,
187
+ lastSyncTimestamp: tree.lastSyncTimestamp,
188
+ };
189
+ }
190
+ getRawState() {
191
+ return this.ensureInit();
192
+ }
193
+ async clear() {
194
+ this.state = { version: 3, trees: {} };
195
+ this._initialized = false;
196
+ await this.storage.remove(STORAGE_KEY);
197
+ }
198
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@totemsdk/wots-lease",
3
+ "version": "0.1.0",
4
+ "description": "WOTS key-use coordination — canonical v3 watermark, provider-based lease safety layers",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "clean": "rm -rf dist",
17
+ "test": "jest --passWithNoTests"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src"
22
+ ],
23
+ "dependencies": {
24
+ "@totemsdk/core": "workspace:*",
25
+ "@totemsdk/lookup-protocol": "workspace:*"
26
+ },
27
+ "devDependencies": {
28
+ "@noble/hashes": "^1.3.0",
29
+ "@types/jest": "^29.0.0",
30
+ "@types/node": "^20.0.0",
31
+ "jest": "^29.0.0",
32
+ "ts-jest": "^29.0.0",
33
+ "typescript": "^5.0.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "license": "MIT"
42
+ }