@totemsdk/wots-lease 0.1.5 → 1.0.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.
- package/README.md +42 -3
- package/dist/errors.d.ts +19 -0
- package/dist/errors.js +37 -1
- package/dist/hybrid.js +27 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +9 -1
- package/dist/local.d.ts +23 -1
- package/dist/local.js +44 -2
- package/dist/onchain.d.ts +52 -0
- package/dist/onchain.js +275 -0
- package/dist/quorum.d.ts +60 -0
- package/dist/quorum.js +264 -0
- package/dist/stubs.d.ts +4 -22
- package/dist/stubs.js +7 -54
- package/dist/types.d.ts +92 -0
- package/package.json +15 -13
- package/rust/pkg/package.json +27 -0
- package/rust/pkg/wots_lease_wasm.d.ts +24 -0
- package/rust/pkg/wots_lease_wasm.js +9 -0
- package/rust/pkg/wots_lease_wasm_bg.js +547 -0
- package/rust/pkg/wots_lease_wasm_bg.wasm +0 -0
- package/rust/pkg/wots_lease_wasm_bg.wasm.d.ts +22 -0
- package/rust/pkg-node/package.json +21 -0
- package/rust/pkg-node/wots_lease_wasm.d.ts +24 -0
- package/rust/pkg-node/wots_lease_wasm.js +563 -0
- package/rust/pkg-node/wots_lease_wasm_bg.wasm +0 -0
- package/rust/pkg-node/wots_lease_wasm_bg.wasm.d.ts +22 -0
package/README.md
CHANGED
|
@@ -28,9 +28,9 @@ npm install @totemsdk/wots-lease
|
|
|
28
28
|
| `LocalLeaseProvider` | Single-device, in-process coordination |
|
|
29
29
|
| `AxiaLeaseProvider` | Cloud-coordinated leases via Axia API |
|
|
30
30
|
| `HybridLeaseProvider` | Local-first with Axia sync fallback |
|
|
31
|
-
| `PersonalLeaseNodeProvider` | Coordinates through your personal lookup node
|
|
32
|
-
| `P2PQuorumLeaseProvider` | Multi-device quorum consensus
|
|
33
|
-
| `OnchainWatermarkProvider` | On-chain watermark anchoring
|
|
31
|
+
| `PersonalLeaseNodeProvider` | Coordinates through your personal lookup node |
|
|
32
|
+
| `P2PQuorumLeaseProvider` | Multi-device quorum consensus — peers attest to the same slot before it is handed out |
|
|
33
|
+
| `OnchainWatermarkProvider` | On-chain watermark anchoring — spends a dedicated watermark coin whose STATE(0) holds the flat cursor |
|
|
34
34
|
|
|
35
35
|
### Device range splitting
|
|
36
36
|
|
|
@@ -90,6 +90,45 @@ const entries = await journal.getEntries({ addressIndex: 0 });
|
|
|
90
90
|
entries.forEach(e => console.log(e.flatIndex, e.allocatedAt, e.committedAt));
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### P2P quorum (multi-device)
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { P2PQuorumLeaseProvider, LocalLeaseProvider } from '@totemsdk/wots-lease';
|
|
97
|
+
|
|
98
|
+
const provider = new P2PQuorumLeaseProvider({
|
|
99
|
+
local: new LocalLeaseProvider(storage),
|
|
100
|
+
peers: [peerA, peerB, peerC], // QuorumPeer[] — any LEASE_* wire-protocol speaker
|
|
101
|
+
minAttestations: 2, // require 2 peers to attest before handing out a slot
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const reservation = await provider.reserveKeyUse({ treeId: 'wallet' });
|
|
105
|
+
// reservation.certificate.attestations — peer attestations for the same indices
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Peers speak the same `LEASE_RESERVE` / `LEASE_COMMIT` / `LEASE_BURN` /
|
|
109
|
+
`LEASE_WATERMARK` messages as the lookup protocol, so a personal lookup node
|
|
110
|
+
(`@totemsdk/lookup-node` with `lease.enabled`) or another device running this
|
|
111
|
+
provider can participate. If quorum cannot be reached the local reservation is
|
|
112
|
+
**burned** — a slot that might have been attested is never handed out again.
|
|
113
|
+
|
|
114
|
+
### On-chain watermark anchoring
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { OnchainWatermarkProvider, LocalLeaseProvider } from '@totemsdk/wots-lease';
|
|
118
|
+
|
|
119
|
+
const provider = new OnchainWatermarkProvider({
|
|
120
|
+
local: new LocalLeaseProvider(storage),
|
|
121
|
+
chain: chainProvider, // any ChainStateProvider (hosted / RPC / lookup node)
|
|
122
|
+
watermarkCoinId: '0x…', // dedicated watermark coin
|
|
123
|
+
watermarkAddress: '0x…', // its current spending address
|
|
124
|
+
signer: { publicKeyDigest, sign }, // SIGNEDBY key for the watermark coin
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await provider.publishWatermark('wallet');
|
|
128
|
+
// Spends the watermark coin back to itself with STATE(0) = flat cursor,
|
|
129
|
+
// so any third party can verify slot consumption on-chain.
|
|
130
|
+
```
|
|
131
|
+
|
|
93
132
|
## See also
|
|
94
133
|
|
|
95
134
|
- [`@totemsdk/core`](https://www.npmjs.com/package/@totemsdk/core) — WOTS primitives this package protects
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SigningIndices } from './types.js';
|
|
1
2
|
export declare class WatermarkMonotonicityError extends Error {
|
|
2
3
|
constructor(message: string);
|
|
3
4
|
}
|
|
@@ -9,6 +10,11 @@ export declare class LeaseNotFoundError extends Error {
|
|
|
9
10
|
readonly reservationId: string;
|
|
10
11
|
constructor(reservationId: string);
|
|
11
12
|
}
|
|
13
|
+
export declare class IndicesUnavailableError extends Error {
|
|
14
|
+
readonly treeId: string;
|
|
15
|
+
readonly indices: SigningIndices;
|
|
16
|
+
constructor(treeId: string, indices: SigningIndices);
|
|
17
|
+
}
|
|
12
18
|
export declare class PersonalLeaseNodeNotConfiguredError extends Error {
|
|
13
19
|
constructor();
|
|
14
20
|
}
|
|
@@ -18,6 +24,19 @@ export declare class P2PQuorumNotImplementedError extends Error {
|
|
|
18
24
|
export declare class OnchainWatermarkNotImplementedError extends Error {
|
|
19
25
|
constructor();
|
|
20
26
|
}
|
|
27
|
+
export declare class QuorumUnavailableError extends Error {
|
|
28
|
+
readonly required: number;
|
|
29
|
+
readonly available: number;
|
|
30
|
+
constructor(required: number, available: number);
|
|
31
|
+
}
|
|
32
|
+
export declare class QuorumConflictError extends Error {
|
|
33
|
+
readonly treeId: string;
|
|
34
|
+
readonly indices: SigningIndices;
|
|
35
|
+
constructor(treeId: string, indices: SigningIndices);
|
|
36
|
+
}
|
|
37
|
+
export declare class OnchainWatermarkError extends Error {
|
|
38
|
+
constructor(message: string);
|
|
39
|
+
}
|
|
21
40
|
export declare class DeviceRangeViolationError extends Error {
|
|
22
41
|
readonly addressIndex: number;
|
|
23
42
|
readonly allowedStart: number;
|
package/dist/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DeviceRangeViolationError = exports.OnchainWatermarkNotImplementedError = exports.P2PQuorumNotImplementedError = exports.PersonalLeaseNodeNotConfiguredError = exports.LeaseNotFoundError = exports.WatermarkExhaustedError = exports.WatermarkMonotonicityError = void 0;
|
|
3
|
+
exports.DeviceRangeViolationError = exports.OnchainWatermarkError = exports.QuorumConflictError = exports.QuorumUnavailableError = exports.OnchainWatermarkNotImplementedError = exports.P2PQuorumNotImplementedError = exports.PersonalLeaseNodeNotConfiguredError = exports.IndicesUnavailableError = exports.LeaseNotFoundError = exports.WatermarkExhaustedError = exports.WatermarkMonotonicityError = void 0;
|
|
4
4
|
class WatermarkMonotonicityError extends Error {
|
|
5
5
|
constructor(message) {
|
|
6
6
|
super(message);
|
|
@@ -24,6 +24,15 @@ class LeaseNotFoundError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
exports.LeaseNotFoundError = LeaseNotFoundError;
|
|
27
|
+
class IndicesUnavailableError extends Error {
|
|
28
|
+
constructor(treeId, indices) {
|
|
29
|
+
super(`Indices (${indices.addressIndex}, ${indices.l1}, ${indices.l2}) are already unavailable for tree: ${treeId}`);
|
|
30
|
+
this.treeId = treeId;
|
|
31
|
+
this.indices = indices;
|
|
32
|
+
this.name = 'IndicesUnavailableError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.IndicesUnavailableError = IndicesUnavailableError;
|
|
27
36
|
class PersonalLeaseNodeNotConfiguredError extends Error {
|
|
28
37
|
constructor() {
|
|
29
38
|
super('PersonalLeaseNodeProvider requires a running lookup node. ' +
|
|
@@ -46,6 +55,33 @@ class OnchainWatermarkNotImplementedError extends Error {
|
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
exports.OnchainWatermarkNotImplementedError = OnchainWatermarkNotImplementedError;
|
|
58
|
+
class QuorumUnavailableError extends Error {
|
|
59
|
+
constructor(required, available) {
|
|
60
|
+
super(`Quorum not reached: ${available}/${required} peers attested. ` +
|
|
61
|
+
'The reservation was burned locally — retry when more peers are reachable.');
|
|
62
|
+
this.required = required;
|
|
63
|
+
this.available = available;
|
|
64
|
+
this.name = 'QuorumUnavailableError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.QuorumUnavailableError = QuorumUnavailableError;
|
|
68
|
+
class QuorumConflictError extends Error {
|
|
69
|
+
constructor(treeId, indices) {
|
|
70
|
+
super(`Quorum conflict for tree ${treeId} at (${indices.addressIndex}, ${indices.l1}, ${indices.l2}): ` +
|
|
71
|
+
'a peer already holds a reservation for these indices.');
|
|
72
|
+
this.treeId = treeId;
|
|
73
|
+
this.indices = indices;
|
|
74
|
+
this.name = 'QuorumConflictError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.QuorumConflictError = QuorumConflictError;
|
|
78
|
+
class OnchainWatermarkError extends Error {
|
|
79
|
+
constructor(message) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = 'OnchainWatermarkError';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.OnchainWatermarkError = OnchainWatermarkError;
|
|
49
85
|
class DeviceRangeViolationError extends Error {
|
|
50
86
|
constructor(addressIndex, allowedStart, allowedEnd) {
|
|
51
87
|
super(`Address index ${addressIndex} is outside device range [${allowedStart}, ${allowedEnd}]`);
|
package/dist/hybrid.js
CHANGED
|
@@ -24,6 +24,33 @@ class HybridLeaseProvider {
|
|
|
24
24
|
async reserveKeyUse(params) {
|
|
25
25
|
const reservation = await this.local.reserveKeyUse(params);
|
|
26
26
|
if (this.isHighValue(params)) {
|
|
27
|
+
// Layer 4 — quorum attestation over the SAME local slot.
|
|
28
|
+
if (this.quorum) {
|
|
29
|
+
try {
|
|
30
|
+
const attestations = await this.quorum.attestKeyUse(params, reservation.indices);
|
|
31
|
+
return {
|
|
32
|
+
...reservation,
|
|
33
|
+
certificate: {
|
|
34
|
+
reservationId: reservation.reservationId,
|
|
35
|
+
treeId: params.treeId,
|
|
36
|
+
branchId: params.branchId,
|
|
37
|
+
deviceId: params.deviceId,
|
|
38
|
+
indices: reservation.indices,
|
|
39
|
+
purpose: params.purpose,
|
|
40
|
+
payloadHash: params.payloadHash,
|
|
41
|
+
issuedBy: 'p2p-quorum',
|
|
42
|
+
issuedAt: Date.now(),
|
|
43
|
+
expiresAt: reservation.expiresAt,
|
|
44
|
+
signature: '',
|
|
45
|
+
attestations,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Quorum unavailable — fall through to node / local only.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Layer 3 — personal lookup node certificate.
|
|
27
54
|
if (this.node) {
|
|
28
55
|
try {
|
|
29
56
|
const nodeReservation = await this.node.reserveKeyUse(params);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { UnavailableReason, SigningIndices, LeaseStatus, TreeWatermark, WotsWatermarkState, LeaseCertificate, LeaseReservation, ReserveParams, LocalWatermark, ConflictRecord, SyncResult, WotsLeaseProvider, JournalEntry, DeviceKeyRange, PersonalLeaseNodeConfig, } from './types.js';
|
|
1
|
+
export type { UnavailableReason, SigningIndices, LeaseStatus, TreeWatermark, WotsWatermarkState, LeaseCertificate, LeaseReservation, ReserveParams, LocalWatermark, ConflictRecord, SyncResult, WotsLeaseProvider, JournalEntry, DeviceKeyRange, PersonalLeaseNodeConfig, QuorumAttestation, QuorumPeer, P2PQuorumLeaseProviderConfig, OnchainWatermarkProviderConfig, } from './types.js';
|
|
2
2
|
export { WotsWatermarkStore, flatIndex, fromFlatIndex, } from './watermark.js';
|
|
3
3
|
export { LeaseJournal } from './journal.js';
|
|
4
4
|
export { LocalLeaseProvider } from './local.js';
|
|
@@ -7,5 +7,7 @@ export type { AxiaLeaseProviderConfig } from './axia.js';
|
|
|
7
7
|
export { HybridLeaseProvider } from './hybrid.js';
|
|
8
8
|
export type { HybridLeaseProviderConfig } from './hybrid.js';
|
|
9
9
|
export { PersonalLeaseNodeProvider, P2PQuorumLeaseProvider, OnchainWatermarkProvider, } from './stubs.js';
|
|
10
|
+
export { P2PQuorumLeaseProvider as QuorumLeaseProvider } from './quorum.js';
|
|
11
|
+
export { OnchainWatermarkProvider as ChainWatermarkProvider } from './onchain.js';
|
|
10
12
|
export { allocateDeviceRange, deviceSlotForAddressIndex } from './device.js';
|
|
11
|
-
export { WatermarkMonotonicityError, WatermarkExhaustedError, LeaseNotFoundError, PersonalLeaseNodeNotConfiguredError, P2PQuorumNotImplementedError, OnchainWatermarkNotImplementedError, DeviceRangeViolationError, } from './errors.js';
|
|
13
|
+
export { WatermarkMonotonicityError, WatermarkExhaustedError, LeaseNotFoundError, IndicesUnavailableError, PersonalLeaseNodeNotConfiguredError, P2PQuorumNotImplementedError, OnchainWatermarkNotImplementedError, QuorumUnavailableError, QuorumConflictError, OnchainWatermarkError, DeviceRangeViolationError, } from './errors.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DeviceRangeViolationError = exports.OnchainWatermarkNotImplementedError = exports.P2PQuorumNotImplementedError = exports.PersonalLeaseNodeNotConfiguredError = exports.LeaseNotFoundError = exports.WatermarkExhaustedError = exports.WatermarkMonotonicityError = exports.deviceSlotForAddressIndex = exports.allocateDeviceRange = exports.OnchainWatermarkProvider = exports.P2PQuorumLeaseProvider = exports.PersonalLeaseNodeProvider = exports.HybridLeaseProvider = exports.AxiaLeaseProvider = exports.LocalLeaseProvider = exports.LeaseJournal = exports.fromFlatIndex = exports.flatIndex = exports.WotsWatermarkStore = void 0;
|
|
3
|
+
exports.DeviceRangeViolationError = exports.OnchainWatermarkError = exports.QuorumConflictError = exports.QuorumUnavailableError = exports.OnchainWatermarkNotImplementedError = exports.P2PQuorumNotImplementedError = exports.PersonalLeaseNodeNotConfiguredError = exports.IndicesUnavailableError = exports.LeaseNotFoundError = exports.WatermarkExhaustedError = exports.WatermarkMonotonicityError = exports.deviceSlotForAddressIndex = exports.allocateDeviceRange = exports.ChainWatermarkProvider = exports.QuorumLeaseProvider = exports.OnchainWatermarkProvider = exports.P2PQuorumLeaseProvider = exports.PersonalLeaseNodeProvider = exports.HybridLeaseProvider = exports.AxiaLeaseProvider = exports.LocalLeaseProvider = exports.LeaseJournal = exports.fromFlatIndex = exports.flatIndex = exports.WotsWatermarkStore = void 0;
|
|
4
4
|
var watermark_js_1 = require("./watermark.js");
|
|
5
5
|
Object.defineProperty(exports, "WotsWatermarkStore", { enumerable: true, get: function () { return watermark_js_1.WotsWatermarkStore; } });
|
|
6
6
|
Object.defineProperty(exports, "flatIndex", { enumerable: true, get: function () { return watermark_js_1.flatIndex; } });
|
|
@@ -17,6 +17,10 @@ var stubs_js_1 = require("./stubs.js");
|
|
|
17
17
|
Object.defineProperty(exports, "PersonalLeaseNodeProvider", { enumerable: true, get: function () { return stubs_js_1.PersonalLeaseNodeProvider; } });
|
|
18
18
|
Object.defineProperty(exports, "P2PQuorumLeaseProvider", { enumerable: true, get: function () { return stubs_js_1.P2PQuorumLeaseProvider; } });
|
|
19
19
|
Object.defineProperty(exports, "OnchainWatermarkProvider", { enumerable: true, get: function () { return stubs_js_1.OnchainWatermarkProvider; } });
|
|
20
|
+
var quorum_js_1 = require("./quorum.js");
|
|
21
|
+
Object.defineProperty(exports, "QuorumLeaseProvider", { enumerable: true, get: function () { return quorum_js_1.P2PQuorumLeaseProvider; } });
|
|
22
|
+
var onchain_js_1 = require("./onchain.js");
|
|
23
|
+
Object.defineProperty(exports, "ChainWatermarkProvider", { enumerable: true, get: function () { return onchain_js_1.OnchainWatermarkProvider; } });
|
|
20
24
|
var device_js_1 = require("./device.js");
|
|
21
25
|
Object.defineProperty(exports, "allocateDeviceRange", { enumerable: true, get: function () { return device_js_1.allocateDeviceRange; } });
|
|
22
26
|
Object.defineProperty(exports, "deviceSlotForAddressIndex", { enumerable: true, get: function () { return device_js_1.deviceSlotForAddressIndex; } });
|
|
@@ -24,7 +28,11 @@ var errors_js_1 = require("./errors.js");
|
|
|
24
28
|
Object.defineProperty(exports, "WatermarkMonotonicityError", { enumerable: true, get: function () { return errors_js_1.WatermarkMonotonicityError; } });
|
|
25
29
|
Object.defineProperty(exports, "WatermarkExhaustedError", { enumerable: true, get: function () { return errors_js_1.WatermarkExhaustedError; } });
|
|
26
30
|
Object.defineProperty(exports, "LeaseNotFoundError", { enumerable: true, get: function () { return errors_js_1.LeaseNotFoundError; } });
|
|
31
|
+
Object.defineProperty(exports, "IndicesUnavailableError", { enumerable: true, get: function () { return errors_js_1.IndicesUnavailableError; } });
|
|
27
32
|
Object.defineProperty(exports, "PersonalLeaseNodeNotConfiguredError", { enumerable: true, get: function () { return errors_js_1.PersonalLeaseNodeNotConfiguredError; } });
|
|
28
33
|
Object.defineProperty(exports, "P2PQuorumNotImplementedError", { enumerable: true, get: function () { return errors_js_1.P2PQuorumNotImplementedError; } });
|
|
29
34
|
Object.defineProperty(exports, "OnchainWatermarkNotImplementedError", { enumerable: true, get: function () { return errors_js_1.OnchainWatermarkNotImplementedError; } });
|
|
35
|
+
Object.defineProperty(exports, "QuorumUnavailableError", { enumerable: true, get: function () { return errors_js_1.QuorumUnavailableError; } });
|
|
36
|
+
Object.defineProperty(exports, "QuorumConflictError", { enumerable: true, get: function () { return errors_js_1.QuorumConflictError; } });
|
|
37
|
+
Object.defineProperty(exports, "OnchainWatermarkError", { enumerable: true, get: function () { return errors_js_1.OnchainWatermarkError; } });
|
|
30
38
|
Object.defineProperty(exports, "DeviceRangeViolationError", { enumerable: true, get: function () { return errors_js_1.DeviceRangeViolationError; } });
|
package/dist/local.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* All WOTS safety guaranteed by the local WotsWatermarkStore + LeaseJournal.
|
|
6
6
|
*/
|
|
7
7
|
import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
|
|
8
|
-
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult } from './types.js';
|
|
8
|
+
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, SigningIndices } from './types.js';
|
|
9
|
+
import { LeaseJournal } from './journal.js';
|
|
9
10
|
export declare class LocalLeaseProvider implements WotsLeaseProvider {
|
|
10
11
|
private readonly logger;
|
|
11
12
|
private readonly watermark;
|
|
@@ -21,9 +22,30 @@ export declare class LocalLeaseProvider implements WotsLeaseProvider {
|
|
|
21
22
|
private burnUnresolvedReservations;
|
|
22
23
|
private recoverExpiredReservations;
|
|
23
24
|
reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
|
|
25
|
+
/**
|
|
26
|
+
* Reserve a specific set of indices (used by quorum coordination so every
|
|
27
|
+
* peer attests to the same slot). Throws IndicesUnavailableError when the
|
|
28
|
+
* slot is already taken.
|
|
29
|
+
*/
|
|
30
|
+
reserveSpecificKeyUse(params: ReserveParams, indices: SigningIndices): Promise<LeaseReservation>;
|
|
24
31
|
commitKeyUse(reservationId: string, txId: string): Promise<void>;
|
|
25
32
|
burnReservation(reservationId: string, reason: string): Promise<void>;
|
|
26
33
|
getLocalWatermark(treeId: string): Promise<LocalWatermark>;
|
|
34
|
+
/** List all tree IDs known to the local watermark store. */
|
|
35
|
+
listTrees(): string[];
|
|
36
|
+
/** Expose the journal for quorum/on-chain providers to merge remote entries. */
|
|
37
|
+
getJournal(): LeaseJournal;
|
|
38
|
+
/**
|
|
39
|
+
* Advance the local watermark to a remote cursor (monotonic merge).
|
|
40
|
+
* Used by quorum sync and the lookup-node LeaseCoordinator when a peer
|
|
41
|
+
* publishes a watermark ahead of ours. Returns false when the remote
|
|
42
|
+
* cursor is behind (no-op).
|
|
43
|
+
*/
|
|
44
|
+
advanceToRemoteWatermark(treeId: string, remote: {
|
|
45
|
+
addressCursor: number;
|
|
46
|
+
l1Cursor: number;
|
|
47
|
+
l2Cursor: number;
|
|
48
|
+
}): Promise<boolean>;
|
|
27
49
|
publishWatermark(_treeId: string): Promise<void>;
|
|
28
50
|
syncLeaseJournal(): Promise<SyncResult>;
|
|
29
51
|
verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
|
package/dist/local.js
CHANGED
|
@@ -81,12 +81,24 @@ class LocalLeaseProvider {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
async reserveKeyUse(params) {
|
|
84
|
+
if (!this._initialized)
|
|
85
|
+
await this.initialize();
|
|
86
|
+
const indices = this.watermark.getNextIndices(params.treeId);
|
|
87
|
+
return this.reserveSpecificKeyUse(params, indices);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reserve a specific set of indices (used by quorum coordination so every
|
|
91
|
+
* peer attests to the same slot). Throws IndicesUnavailableError when the
|
|
92
|
+
* slot is already taken.
|
|
93
|
+
*/
|
|
94
|
+
async reserveSpecificKeyUse(params, indices) {
|
|
84
95
|
if (!this._initialized)
|
|
85
96
|
await this.initialize();
|
|
86
97
|
const treeId = params.treeId;
|
|
87
98
|
const ttlMs = params.ttlMs ?? 120000;
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
if (this.watermark.isUnavailable(treeId, indices)) {
|
|
100
|
+
throw new errors_js_1.IndicesUnavailableError(treeId, indices);
|
|
101
|
+
}
|
|
90
102
|
// Validate device range BEFORE marking unavailable — prevents permanent index loss
|
|
91
103
|
if (params.deviceId) {
|
|
92
104
|
const range = this.getDeviceRange(params.deviceId);
|
|
@@ -172,6 +184,36 @@ class LocalLeaseProvider {
|
|
|
172
184
|
await this.initialize();
|
|
173
185
|
return this.watermark.getLocalWatermark(treeId);
|
|
174
186
|
}
|
|
187
|
+
/** List all tree IDs known to the local watermark store. */
|
|
188
|
+
listTrees() {
|
|
189
|
+
return Object.keys(this.watermark.getRawState().trees);
|
|
190
|
+
}
|
|
191
|
+
/** Expose the journal for quorum/on-chain providers to merge remote entries. */
|
|
192
|
+
getJournal() {
|
|
193
|
+
return this.journal;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Advance the local watermark to a remote cursor (monotonic merge).
|
|
197
|
+
* Used by quorum sync and the lookup-node LeaseCoordinator when a peer
|
|
198
|
+
* publishes a watermark ahead of ours. Returns false when the remote
|
|
199
|
+
* cursor is behind (no-op).
|
|
200
|
+
*/
|
|
201
|
+
async advanceToRemoteWatermark(treeId, remote) {
|
|
202
|
+
if (!this._initialized)
|
|
203
|
+
await this.initialize();
|
|
204
|
+
const local = this.watermark.getLocalWatermark(treeId);
|
|
205
|
+
const localFlat = (0, watermark_js_1.flatIndex)({ addressIndex: local.addressCursor, l1: local.l1Cursor, l2: local.l2Cursor });
|
|
206
|
+
const remoteFlat = (0, watermark_js_1.flatIndex)({ addressIndex: remote.addressCursor, l1: remote.l1Cursor, l2: remote.l2Cursor });
|
|
207
|
+
if (remoteFlat <= localFlat)
|
|
208
|
+
return false;
|
|
209
|
+
await this.watermark.save(treeId, {
|
|
210
|
+
addressCursor: remote.addressCursor,
|
|
211
|
+
l1Cursor: remote.l1Cursor,
|
|
212
|
+
l2Cursor: remote.l2Cursor,
|
|
213
|
+
lastSyncTimestamp: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
175
217
|
async publishWatermark(_treeId) {
|
|
176
218
|
// Layer 1: no-op
|
|
177
219
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OnchainWatermarkProvider — Layer 5: on-chain watermark anchoring.
|
|
3
|
+
*
|
|
4
|
+
* Safety model
|
|
5
|
+
* ────────────
|
|
6
|
+
* The local provider remains the authoritative watermark. This provider adds
|
|
7
|
+
* a verifiable public record: a dedicated watermark coin whose STATE(0) holds
|
|
8
|
+
* the flat watermark cursor. Every publish spends the coin back to itself
|
|
9
|
+
* with an advanced cursor, so any third party can read the on-chain state and
|
|
10
|
+
* prove that a given signing slot was consumed — without trusting this device.
|
|
11
|
+
*
|
|
12
|
+
* The watermark coin script is `RETURN SIGNEDBY(0x<digest>)` — the same
|
|
13
|
+
* single-sig pattern used by statechain reclaim addresses. The signer is the
|
|
14
|
+
* WOTS key owner; the digest is the 32-byte public key digest.
|
|
15
|
+
*
|
|
16
|
+
* Failure semantics
|
|
17
|
+
* ─────────────────
|
|
18
|
+
* - publishWatermark fails loudly (OnchainWatermarkError) when the chain
|
|
19
|
+
* rejects the TX — the local watermark is never rolled back.
|
|
20
|
+
* - syncLeaseJournal merges the on-chain cursor into the local watermark
|
|
21
|
+
* monotonically (remote ahead → advance; remote behind → conflict record).
|
|
22
|
+
*/
|
|
23
|
+
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, OnchainWatermarkProviderConfig } from './types.js';
|
|
24
|
+
export declare class OnchainWatermarkProvider implements WotsLeaseProvider {
|
|
25
|
+
private readonly chain;
|
|
26
|
+
private readonly watermarkCoinId;
|
|
27
|
+
private readonly watermarkAddress;
|
|
28
|
+
private readonly tokenId;
|
|
29
|
+
private readonly amount;
|
|
30
|
+
private readonly local;
|
|
31
|
+
private readonly signer;
|
|
32
|
+
private readonly statePort;
|
|
33
|
+
private readonly minBlocksBetweenPublishes;
|
|
34
|
+
private _initialized;
|
|
35
|
+
private _lastPublishBlock;
|
|
36
|
+
constructor(config: OnchainWatermarkProviderConfig);
|
|
37
|
+
initialize(): Promise<void>;
|
|
38
|
+
private ensureInit;
|
|
39
|
+
reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
|
|
40
|
+
commitKeyUse(reservationId: string, txId: string): Promise<void>;
|
|
41
|
+
burnReservation(reservationId: string, reason: string): Promise<void>;
|
|
42
|
+
getLocalWatermark(treeId: string): Promise<LocalWatermark>;
|
|
43
|
+
/**
|
|
44
|
+
* Publish the local watermark cursor on-chain by spending the watermark
|
|
45
|
+
* coin back to itself with STATE(statePort) = flat cursor.
|
|
46
|
+
*
|
|
47
|
+
* Rate-limited by minBlocksBetweenPublishes using the chain tip.
|
|
48
|
+
*/
|
|
49
|
+
publishWatermark(treeId: string): Promise<void>;
|
|
50
|
+
syncLeaseJournal(): Promise<SyncResult>;
|
|
51
|
+
verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
|
|
52
|
+
}
|
package/dist/onchain.js
ADDED
|
@@ -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;
|