@totemsdk/wots-lease 0.1.6 → 1.0.1
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/dist/axia.d.ts +1 -1
- package/dist/axia.js +24 -7
- package/dist/certificate.d.ts +37 -0
- package/dist/certificate.js +74 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +42 -1
- package/dist/hybrid.d.ts +4 -1
- package/dist/hybrid.js +71 -21
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/journal.d.ts +14 -4
- package/dist/journal.js +321 -40
- package/dist/local.d.ts +31 -1
- package/dist/local.js +249 -46
- package/dist/onchain.d.ts +3 -1
- package/dist/onchain.js +46 -7
- package/dist/quorum.d.ts +15 -0
- package/dist/quorum.js +48 -5
- package/dist/seed.d.ts +41 -0
- package/dist/seed.js +84 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +17 -1
- package/dist/types.d.ts +74 -1
- package/dist/watermark.d.ts +22 -1
- package/dist/watermark.js +125 -1
- package/package.json +8 -7
- package/rust/pkg/package.json +1 -1
- package/rust/pkg/wots_lease_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +1 -1
- package/rust/pkg-node/wots_lease_wasm_bg.wasm +0 -0
package/dist/axia.d.ts
CHANGED
|
@@ -29,5 +29,5 @@ export declare class AxiaLeaseProvider implements WotsLeaseProvider {
|
|
|
29
29
|
getLocalWatermark(treeId: string): Promise<LocalWatermark>;
|
|
30
30
|
publishWatermark(_treeId: string): Promise<void>;
|
|
31
31
|
syncLeaseJournal(): Promise<SyncResult>;
|
|
32
|
-
verifyLeaseCertificate(
|
|
32
|
+
verifyLeaseCertificate(_cert?: LeaseCertificate): Promise<boolean>;
|
|
33
33
|
}
|
package/dist/axia.js
CHANGED
|
@@ -11,6 +11,7 @@ const core_2 = require("@totemsdk/core");
|
|
|
11
11
|
const watermark_js_1 = require("./watermark.js");
|
|
12
12
|
const journal_js_1 = require("./journal.js");
|
|
13
13
|
const errors_js_1 = require("./errors.js");
|
|
14
|
+
const errors_1 = require("@totemsdk/storage/errors");
|
|
14
15
|
class AxiaLeaseProvider {
|
|
15
16
|
constructor(config) {
|
|
16
17
|
this._initialized = false;
|
|
@@ -42,6 +43,12 @@ class AxiaLeaseProvider {
|
|
|
42
43
|
ttlMs: params.ttlMs ?? 120000,
|
|
43
44
|
});
|
|
44
45
|
const indices = resp.lease;
|
|
46
|
+
// RFC-020 C2: never accept an index this tree has already consumed, even if
|
|
47
|
+
// a replaying/hostile lease server hands the same slot out again.
|
|
48
|
+
await this.watermark.refresh();
|
|
49
|
+
if (this.watermark.isUnavailable(params.treeId, indices)) {
|
|
50
|
+
throw new errors_js_1.IndicesUnavailableError(params.treeId, indices);
|
|
51
|
+
}
|
|
45
52
|
const expiresAt = Date.now() + (params.ttlMs ?? 120000);
|
|
46
53
|
const reservationId = resp.txId;
|
|
47
54
|
const lease = {
|
|
@@ -75,10 +82,16 @@ class AxiaLeaseProvider {
|
|
|
75
82
|
throw new errors_js_1.LeaseNotFoundError(reservationId);
|
|
76
83
|
await (0, core_2.finalizeLease)(this.apiUrl, this.apiKey, lease.leaseToken, txId);
|
|
77
84
|
const indices = lease.indices;
|
|
78
|
-
|
|
85
|
+
// RFC-020 C2: key the watermark/journal by the real treeId, never the
|
|
86
|
+
// reservation id.
|
|
87
|
+
const treeId = lease.treeId;
|
|
88
|
+
if (!treeId) {
|
|
89
|
+
throw new errors_1.StorageError(`AxiaLeaseProvider: lease ${reservationId} has no treeId`, 'corrupt');
|
|
90
|
+
}
|
|
91
|
+
await this.watermark.markUnavailable(treeId, indices, 'committed');
|
|
79
92
|
await this.leaseStore.updateStatus(reservationId, 'finalized');
|
|
80
93
|
await this.journal.append({
|
|
81
|
-
treeId
|
|
94
|
+
treeId,
|
|
82
95
|
branchId: 'default',
|
|
83
96
|
wotsIndex: (0, watermark_js_1.flatIndex)(indices),
|
|
84
97
|
indices,
|
|
@@ -95,11 +108,15 @@ class AxiaLeaseProvider {
|
|
|
95
108
|
if (!lease)
|
|
96
109
|
throw new errors_js_1.LeaseNotFoundError(reservationId);
|
|
97
110
|
const indices = lease.indices;
|
|
98
|
-
|
|
111
|
+
const treeId = lease.treeId;
|
|
112
|
+
if (!treeId) {
|
|
113
|
+
throw new errors_1.StorageError(`AxiaLeaseProvider: lease ${reservationId} has no treeId`, 'corrupt');
|
|
114
|
+
}
|
|
115
|
+
await this.watermark.markUnavailable(treeId, indices, 'burned');
|
|
99
116
|
await this.leaseStore.updateStatus(reservationId, 'cancelled');
|
|
100
117
|
this.logger.warn(`[AxiaLeaseProvider] Burning reservation ${reservationId}: ${reason}`);
|
|
101
118
|
await this.journal.append({
|
|
102
|
-
treeId
|
|
119
|
+
treeId,
|
|
103
120
|
branchId: 'default',
|
|
104
121
|
wotsIndex: (0, watermark_js_1.flatIndex)(indices),
|
|
105
122
|
indices,
|
|
@@ -119,9 +136,9 @@ class AxiaLeaseProvider {
|
|
|
119
136
|
async syncLeaseJournal() {
|
|
120
137
|
return { synced: true, conflicts: [] };
|
|
121
138
|
}
|
|
122
|
-
async verifyLeaseCertificate(
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
async verifyLeaseCertificate(_cert) {
|
|
140
|
+
// RFC-020 C2: the Axia API does not return a server-signed certificate we
|
|
141
|
+
// can verify, so fail closed rather than asserting a trust we cannot prove.
|
|
125
142
|
return false;
|
|
126
143
|
}
|
|
127
144
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for authenticating lease certificates.
|
|
3
|
+
*
|
|
4
|
+
* A certificate is authenticated by a real signature over a canonical,
|
|
5
|
+
* deterministic byte encoding of its content. Providers that hold an issuing
|
|
6
|
+
* identity (`certificateSigner`, or the watermark coin owner on Layer 5) sign
|
|
7
|
+
* the payload before returning the certificate. Verification FAILS CLOSED:
|
|
8
|
+
* a certificate is rejected unless the caller supplies a `verify()` function
|
|
9
|
+
* and the signature cryptographically verifies against it. An empty signature,
|
|
10
|
+
* an absent verifier, or an identity without a verifier are all rejections —
|
|
11
|
+
* `issuedBy` alone is never treated as authenticity evidence.
|
|
12
|
+
*/
|
|
13
|
+
import type { CertificateSigner, LeaseCertificate } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Canonical byte encoding of a certificate's bindable content.
|
|
16
|
+
*
|
|
17
|
+
* Any change to a bound field changes the message, so a certificate cannot
|
|
18
|
+
* be re-targeted at a different reservation, device, purpose, or expiry.
|
|
19
|
+
*/
|
|
20
|
+
export declare function canonicalCertificateMessage(cert: Pick<LeaseCertificate, 'treeId' | 'branchId' | 'deviceId' | 'indices' | 'purpose' | 'payloadHash' | 'reservationId' | 'issuedBy' | 'issuedAt' | 'expiresAt'>): Uint8Array;
|
|
21
|
+
/** Sign a certificate via the issuer identity, returning hex of the signature. */
|
|
22
|
+
export declare function signCertificate(signer: {
|
|
23
|
+
sign(message: Uint8Array): Promise<Uint8Array>;
|
|
24
|
+
}, cert: Parameters<typeof canonicalCertificateMessage>[0]): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Verify a certificate's signature.
|
|
27
|
+
*
|
|
28
|
+
* Fails closed (AUD-016):
|
|
29
|
+
* - Empty/malformed signature -> false (an unsigned certificate is not
|
|
30
|
+
* authenticity evidence regardless of what issuedBy claims).
|
|
31
|
+
* - No `signer`, or a `signer` without a `verify()` function -> false. Knowing
|
|
32
|
+
* only an identity (e.g. a public key digest) cannot authenticate a
|
|
33
|
+
* signature; a verifier is required.
|
|
34
|
+
* - Otherwise the signature must cryptographically verify against the
|
|
35
|
+
* supplied verifier.
|
|
36
|
+
*/
|
|
37
|
+
export declare function certificateSignatureVerified(cert: LeaseCertificate, signer?: CertificateSigner): Promise<boolean>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared helpers for authenticating lease certificates.
|
|
4
|
+
*
|
|
5
|
+
* A certificate is authenticated by a real signature over a canonical,
|
|
6
|
+
* deterministic byte encoding of its content. Providers that hold an issuing
|
|
7
|
+
* identity (`certificateSigner`, or the watermark coin owner on Layer 5) sign
|
|
8
|
+
* the payload before returning the certificate. Verification FAILS CLOSED:
|
|
9
|
+
* a certificate is rejected unless the caller supplies a `verify()` function
|
|
10
|
+
* and the signature cryptographically verifies against it. An empty signature,
|
|
11
|
+
* an absent verifier, or an identity without a verifier are all rejections —
|
|
12
|
+
* `issuedBy` alone is never treated as authenticity evidence.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.canonicalCertificateMessage = canonicalCertificateMessage;
|
|
16
|
+
exports.signCertificate = signCertificate;
|
|
17
|
+
exports.certificateSignatureVerified = certificateSignatureVerified;
|
|
18
|
+
const core_1 = require("@totemsdk/core");
|
|
19
|
+
/**
|
|
20
|
+
* Canonical byte encoding of a certificate's bindable content.
|
|
21
|
+
*
|
|
22
|
+
* Any change to a bound field changes the message, so a certificate cannot
|
|
23
|
+
* be re-targeted at a different reservation, device, purpose, or expiry.
|
|
24
|
+
*/
|
|
25
|
+
function canonicalCertificateMessage(cert) {
|
|
26
|
+
const encoded = new TextEncoder().encode([
|
|
27
|
+
'TOTEM-LEASECERT',
|
|
28
|
+
'v1',
|
|
29
|
+
cert.issuedBy ?? '',
|
|
30
|
+
cert.treeId ?? '',
|
|
31
|
+
cert.branchId ?? '',
|
|
32
|
+
cert.deviceId ?? '',
|
|
33
|
+
cert.reservationId ?? '',
|
|
34
|
+
String(cert.indices?.addressIndex ?? 0),
|
|
35
|
+
String(cert.indices?.l1 ?? 0),
|
|
36
|
+
String(cert.indices?.l2 ?? 0),
|
|
37
|
+
cert.purpose ?? '',
|
|
38
|
+
cert.payloadHash ?? '',
|
|
39
|
+
String(cert.issuedAt ?? 0),
|
|
40
|
+
String(cert.expiresAt ?? 0),
|
|
41
|
+
].join('\x1f'));
|
|
42
|
+
return (0, core_1.sha3_256)(encoded);
|
|
43
|
+
}
|
|
44
|
+
/** Sign a certificate via the issuer identity, returning hex of the signature. */
|
|
45
|
+
async function signCertificate(signer, cert) {
|
|
46
|
+
return (0, core_1.bytesToHex)(await signer.sign(canonicalCertificateMessage(cert)));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Verify a certificate's signature.
|
|
50
|
+
*
|
|
51
|
+
* Fails closed (AUD-016):
|
|
52
|
+
* - Empty/malformed signature -> false (an unsigned certificate is not
|
|
53
|
+
* authenticity evidence regardless of what issuedBy claims).
|
|
54
|
+
* - No `signer`, or a `signer` without a `verify()` function -> false. Knowing
|
|
55
|
+
* only an identity (e.g. a public key digest) cannot authenticate a
|
|
56
|
+
* signature; a verifier is required.
|
|
57
|
+
* - Otherwise the signature must cryptographically verify against the
|
|
58
|
+
* supplied verifier.
|
|
59
|
+
*/
|
|
60
|
+
async function certificateSignatureVerified(cert, signer) {
|
|
61
|
+
if (typeof cert.signature !== 'string' || cert.signature.length === 0) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (!signer?.verify)
|
|
65
|
+
return false;
|
|
66
|
+
let sig;
|
|
67
|
+
try {
|
|
68
|
+
sig = (0, core_1.hexToBytes)(cert.signature);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return signer.verify(canonicalCertificateMessage(cert), sig);
|
|
74
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,6 +2,26 @@ import type { SigningIndices } from './types.js';
|
|
|
2
2
|
export declare class WatermarkMonotonicityError extends Error {
|
|
3
3
|
constructor(message: string);
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Raised when seeding would move the local watermark cursor backwards. This is
|
|
7
|
+
* the fail-closed guard against restoring a wallet below its true high-water
|
|
8
|
+
* mark and reusing a WOTS leaf (RFC-013 §9).
|
|
9
|
+
*/
|
|
10
|
+
export declare class WatermarkSeedRegressionError extends Error {
|
|
11
|
+
readonly treeId: string;
|
|
12
|
+
readonly currentCursor: number;
|
|
13
|
+
readonly seedCursor: number;
|
|
14
|
+
constructor(treeId: string, currentCursor: number, seedCursor: number);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Raised when a restore/import has no usable watermark source and the caller has
|
|
18
|
+
* not declared the keyspace fresh. Failing closed is required: starting at 0
|
|
19
|
+
* would reissue spent leaves (RFC-013 §9).
|
|
20
|
+
*/
|
|
21
|
+
export declare class WatermarkSeedRequiredError extends Error {
|
|
22
|
+
readonly treeId: string;
|
|
23
|
+
constructor(treeId: string);
|
|
24
|
+
}
|
|
5
25
|
export declare class WatermarkExhaustedError extends Error {
|
|
6
26
|
readonly treeId: string;
|
|
7
27
|
constructor(treeId: string);
|
|
@@ -10,6 +30,12 @@ export declare class LeaseNotFoundError extends Error {
|
|
|
10
30
|
readonly reservationId: string;
|
|
11
31
|
constructor(reservationId: string);
|
|
12
32
|
}
|
|
33
|
+
export declare class InvalidLeaseTransitionError extends Error {
|
|
34
|
+
readonly reservationId: string;
|
|
35
|
+
readonly currentStatus: string;
|
|
36
|
+
readonly requestedStatus: string;
|
|
37
|
+
constructor(reservationId: string, currentStatus: string, requestedStatus: string);
|
|
38
|
+
}
|
|
13
39
|
export declare class IndicesUnavailableError extends Error {
|
|
14
40
|
readonly treeId: string;
|
|
15
41
|
readonly indices: SigningIndices;
|
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.OnchainWatermarkError = exports.QuorumConflictError = exports.QuorumUnavailableError = exports.OnchainWatermarkNotImplementedError = exports.P2PQuorumNotImplementedError = exports.PersonalLeaseNodeNotConfiguredError = exports.IndicesUnavailableError = 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.InvalidLeaseTransitionError = exports.LeaseNotFoundError = exports.WatermarkExhaustedError = exports.WatermarkSeedRequiredError = exports.WatermarkSeedRegressionError = exports.WatermarkMonotonicityError = void 0;
|
|
4
4
|
class WatermarkMonotonicityError extends Error {
|
|
5
5
|
constructor(message) {
|
|
6
6
|
super(message);
|
|
@@ -8,6 +8,37 @@ class WatermarkMonotonicityError extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
exports.WatermarkMonotonicityError = WatermarkMonotonicityError;
|
|
11
|
+
/**
|
|
12
|
+
* Raised when seeding would move the local watermark cursor backwards. This is
|
|
13
|
+
* the fail-closed guard against restoring a wallet below its true high-water
|
|
14
|
+
* mark and reusing a WOTS leaf (RFC-013 §9).
|
|
15
|
+
*/
|
|
16
|
+
class WatermarkSeedRegressionError extends Error {
|
|
17
|
+
constructor(treeId, currentCursor, seedCursor) {
|
|
18
|
+
super(`Refusing to seed watermark for ${treeId} below the current cursor ` +
|
|
19
|
+
`(${seedCursor} < ${currentCursor}); this would re-expose used WOTS leaves.`);
|
|
20
|
+
this.treeId = treeId;
|
|
21
|
+
this.currentCursor = currentCursor;
|
|
22
|
+
this.seedCursor = seedCursor;
|
|
23
|
+
this.name = 'WatermarkSeedRegressionError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.WatermarkSeedRegressionError = WatermarkSeedRegressionError;
|
|
27
|
+
/**
|
|
28
|
+
* Raised when a restore/import has no usable watermark source and the caller has
|
|
29
|
+
* not declared the keyspace fresh. Failing closed is required: starting at 0
|
|
30
|
+
* would reissue spent leaves (RFC-013 §9).
|
|
31
|
+
*/
|
|
32
|
+
class WatermarkSeedRequiredError extends Error {
|
|
33
|
+
constructor(treeId) {
|
|
34
|
+
super(`No watermark source available for ${treeId}. Provide an on-chain cursor, an ` +
|
|
35
|
+
'exported watermark, or an operator high-water mark — or declare the keyspace fresh. ' +
|
|
36
|
+
'Refusing to start at 0.');
|
|
37
|
+
this.treeId = treeId;
|
|
38
|
+
this.name = 'WatermarkSeedRequiredError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.WatermarkSeedRequiredError = WatermarkSeedRequiredError;
|
|
11
42
|
class WatermarkExhaustedError extends Error {
|
|
12
43
|
constructor(treeId) {
|
|
13
44
|
super(`WOTS keyspace exhausted for tree: ${treeId}`);
|
|
@@ -24,6 +55,16 @@ class LeaseNotFoundError extends Error {
|
|
|
24
55
|
}
|
|
25
56
|
}
|
|
26
57
|
exports.LeaseNotFoundError = LeaseNotFoundError;
|
|
58
|
+
class InvalidLeaseTransitionError extends Error {
|
|
59
|
+
constructor(reservationId, currentStatus, requestedStatus) {
|
|
60
|
+
super(`Cannot transition lease ${reservationId} from ${currentStatus} to ${requestedStatus}`);
|
|
61
|
+
this.reservationId = reservationId;
|
|
62
|
+
this.currentStatus = currentStatus;
|
|
63
|
+
this.requestedStatus = requestedStatus;
|
|
64
|
+
this.name = 'InvalidLeaseTransitionError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.InvalidLeaseTransitionError = InvalidLeaseTransitionError;
|
|
27
68
|
class IndicesUnavailableError extends Error {
|
|
28
69
|
constructor(treeId, indices) {
|
|
29
70
|
super(`Indices (${indices.addressIndex}, ${indices.l1}, ${indices.l2}) are already unavailable for tree: ${treeId}`);
|
package/dist/hybrid.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Always uses local first, then gates on node cert and/or quorum attestation
|
|
5
5
|
* for transactions above a configurable value threshold.
|
|
6
6
|
*/
|
|
7
|
-
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult } from './types.js';
|
|
7
|
+
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, CertificateSigner } from './types.js';
|
|
8
8
|
import type { LocalLeaseProvider } from './local.js';
|
|
9
9
|
import type { PersonalLeaseNodeProvider } from './stubs.js';
|
|
10
10
|
import type { P2PQuorumLeaseProvider } from './stubs.js';
|
|
@@ -15,6 +15,8 @@ export interface HybridLeaseProviderConfig {
|
|
|
15
15
|
quorum?: P2PQuorumLeaseProvider;
|
|
16
16
|
onchain?: OnchainWatermarkProvider;
|
|
17
17
|
threshold?: number;
|
|
18
|
+
/** Identity that authenticates locally-assembled quorum certificates. */
|
|
19
|
+
certificateSigner?: CertificateSigner;
|
|
18
20
|
}
|
|
19
21
|
export declare class HybridLeaseProvider implements WotsLeaseProvider {
|
|
20
22
|
private readonly local;
|
|
@@ -22,6 +24,7 @@ export declare class HybridLeaseProvider implements WotsLeaseProvider {
|
|
|
22
24
|
private readonly quorum?;
|
|
23
25
|
private readonly onchain?;
|
|
24
26
|
private readonly threshold;
|
|
27
|
+
private readonly certificateSigner?;
|
|
25
28
|
constructor(config: HybridLeaseProviderConfig);
|
|
26
29
|
private isHighValue;
|
|
27
30
|
reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
|
package/dist/hybrid.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.HybridLeaseProvider = void 0;
|
|
10
|
+
const certificate_js_1 = require("./certificate.js");
|
|
10
11
|
class HybridLeaseProvider {
|
|
11
12
|
constructor(config) {
|
|
12
13
|
this.local = config.local;
|
|
@@ -14,6 +15,7 @@ class HybridLeaseProvider {
|
|
|
14
15
|
this.quorum = config.quorum;
|
|
15
16
|
this.onchain = config.onchain;
|
|
16
17
|
this.threshold = config.threshold ?? Infinity;
|
|
18
|
+
this.certificateSigner = config.certificateSigner;
|
|
17
19
|
}
|
|
18
20
|
isHighValue(params) {
|
|
19
21
|
if (!params.valueHint)
|
|
@@ -22,29 +24,41 @@ class HybridLeaseProvider {
|
|
|
22
24
|
return !isNaN(val) && val >= this.threshold;
|
|
23
25
|
}
|
|
24
26
|
async reserveKeyUse(params) {
|
|
27
|
+
// Cross-instance/restore safety (RFC-013 §8/§9): before a high-value
|
|
28
|
+
// reservation, anchor the local cursor to the on-chain watermark so a second
|
|
29
|
+
// instance of the same seed cannot be handed a leaf we already used.
|
|
30
|
+
if (this.isHighValue(params) && this.onchain) {
|
|
31
|
+
try {
|
|
32
|
+
await this.onchain.syncLeaseJournal();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Chain unreachable — local monotonic watermark still guards reuse.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
25
38
|
const reservation = await this.local.reserveKeyUse(params);
|
|
26
39
|
if (this.isHighValue(params)) {
|
|
27
40
|
// Layer 4 — quorum attestation over the SAME local slot.
|
|
28
41
|
if (this.quorum) {
|
|
29
42
|
try {
|
|
30
43
|
const attestations = await this.quorum.attestKeyUse(params, reservation.indices);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
signature: '',
|
|
45
|
-
attestations,
|
|
46
|
-
},
|
|
44
|
+
const certificate = {
|
|
45
|
+
reservationId: reservation.reservationId,
|
|
46
|
+
treeId: params.treeId,
|
|
47
|
+
branchId: params.branchId,
|
|
48
|
+
deviceId: params.deviceId,
|
|
49
|
+
indices: reservation.indices,
|
|
50
|
+
purpose: params.purpose,
|
|
51
|
+
payloadHash: params.payloadHash,
|
|
52
|
+
issuedBy: this.certificateSigner?.name ?? 'p2p-quorum',
|
|
53
|
+
issuedAt: Date.now(),
|
|
54
|
+
expiresAt: reservation.expiresAt,
|
|
55
|
+
signature: '',
|
|
56
|
+
attestations,
|
|
47
57
|
};
|
|
58
|
+
certificate.signature = this.certificateSigner
|
|
59
|
+
? await (0, certificate_js_1.signCertificate)(this.certificateSigner, certificate)
|
|
60
|
+
: '';
|
|
61
|
+
return { ...reservation, certificate };
|
|
48
62
|
}
|
|
49
63
|
catch {
|
|
50
64
|
// Quorum unavailable — fall through to node / local only.
|
|
@@ -72,21 +86,57 @@ class HybridLeaseProvider {
|
|
|
72
86
|
getLocalWatermark(treeId) {
|
|
73
87
|
return this.local.getLocalWatermark(treeId);
|
|
74
88
|
}
|
|
75
|
-
publishWatermark(treeId) {
|
|
89
|
+
async publishWatermark(treeId) {
|
|
90
|
+
// Anchor to the chain first (authoritative cross-instance cursor), then the
|
|
91
|
+
// personal node, then the local no-op.
|
|
92
|
+
if (this.onchain) {
|
|
93
|
+
try {
|
|
94
|
+
await this.onchain.publishWatermark(treeId);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// fall through
|
|
99
|
+
}
|
|
100
|
+
}
|
|
76
101
|
if (this.node) {
|
|
77
|
-
|
|
102
|
+
try {
|
|
103
|
+
await this.node.publishWatermark(treeId);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// fall through
|
|
108
|
+
}
|
|
78
109
|
}
|
|
79
|
-
|
|
110
|
+
await this.local.publishWatermark(treeId);
|
|
80
111
|
}
|
|
81
|
-
syncLeaseJournal() {
|
|
112
|
+
async syncLeaseJournal() {
|
|
113
|
+
// Read the authoritative on-chain cursor first so a restore/second instance
|
|
114
|
+
// advances past every leaf published by anyone holding this seed.
|
|
115
|
+
if (this.onchain) {
|
|
116
|
+
try {
|
|
117
|
+
return await this.onchain.syncLeaseJournal();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// fall through
|
|
121
|
+
}
|
|
122
|
+
}
|
|
82
123
|
if (this.node) {
|
|
83
|
-
|
|
124
|
+
try {
|
|
125
|
+
return await this.node.syncLeaseJournal();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// fall through
|
|
129
|
+
}
|
|
84
130
|
}
|
|
85
131
|
return this.local.syncLeaseJournal();
|
|
86
132
|
}
|
|
87
133
|
async verifyLeaseCertificate(cert) {
|
|
88
134
|
if (!cert)
|
|
89
135
|
return this.local.verifyLeaseCertificate(cert);
|
|
136
|
+
// Locally-assembled quorum certificates authenticate against our issuer.
|
|
137
|
+
if (cert.issuedBy === (this.certificateSigner?.name ?? 'p2p-quorum')) {
|
|
138
|
+
return (0, certificate_js_1.certificateSignatureVerified)(cert, this.certificateSigner);
|
|
139
|
+
}
|
|
90
140
|
if (this.node)
|
|
91
141
|
return this.node.verifyLeaseCertificate(cert);
|
|
92
142
|
return false;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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';
|
|
1
|
+
export type { UnavailableReason, SigningIndices, LeaseStatus, TreeWatermark, WotsWatermarkState, LeaseCertificate, LeaseReservation, ReserveParams, LocalWatermark, WatermarkSeed, SeedWatermarkOptions, ConflictRecord, SyncResult, WotsLeaseProvider, JournalEntry, DeviceKeyRange, PersonalLeaseNodeConfig, QuorumAttestation, QuorumPeer, CertificateSigner, 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';
|
|
5
|
+
export { seedLeaseWatermark, exportLeaseWatermark } from './seed.js';
|
|
6
|
+
export type { SeedLeaseWatermarkParams } from './seed.js';
|
|
5
7
|
export { AxiaLeaseProvider } from './axia.js';
|
|
6
8
|
export type { AxiaLeaseProviderConfig } from './axia.js';
|
|
7
9
|
export { HybridLeaseProvider } from './hybrid.js';
|
|
@@ -10,4 +12,4 @@ export { PersonalLeaseNodeProvider, P2PQuorumLeaseProvider, OnchainWatermarkProv
|
|
|
10
12
|
export { P2PQuorumLeaseProvider as QuorumLeaseProvider } from './quorum.js';
|
|
11
13
|
export { OnchainWatermarkProvider as ChainWatermarkProvider } from './onchain.js';
|
|
12
14
|
export { allocateDeviceRange, deviceSlotForAddressIndex } from './device.js';
|
|
13
|
-
export { WatermarkMonotonicityError, WatermarkExhaustedError, LeaseNotFoundError, IndicesUnavailableError, PersonalLeaseNodeNotConfiguredError, P2PQuorumNotImplementedError, OnchainWatermarkNotImplementedError, QuorumUnavailableError, QuorumConflictError, OnchainWatermarkError, DeviceRangeViolationError, } from './errors.js';
|
|
15
|
+
export { WatermarkMonotonicityError, WatermarkExhaustedError, WatermarkSeedRegressionError, WatermarkSeedRequiredError, 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.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;
|
|
3
|
+
exports.DeviceRangeViolationError = exports.OnchainWatermarkError = exports.QuorumConflictError = exports.QuorumUnavailableError = exports.OnchainWatermarkNotImplementedError = exports.P2PQuorumNotImplementedError = exports.PersonalLeaseNodeNotConfiguredError = exports.IndicesUnavailableError = exports.LeaseNotFoundError = exports.WatermarkSeedRequiredError = exports.WatermarkSeedRegressionError = exports.WatermarkExhaustedError = exports.WatermarkMonotonicityError = exports.deviceSlotForAddressIndex = exports.allocateDeviceRange = exports.ChainWatermarkProvider = exports.QuorumLeaseProvider = exports.OnchainWatermarkProvider = exports.P2PQuorumLeaseProvider = exports.PersonalLeaseNodeProvider = exports.HybridLeaseProvider = exports.AxiaLeaseProvider = exports.exportLeaseWatermark = exports.seedLeaseWatermark = 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; } });
|
|
@@ -9,6 +9,9 @@ var journal_js_1 = require("./journal.js");
|
|
|
9
9
|
Object.defineProperty(exports, "LeaseJournal", { enumerable: true, get: function () { return journal_js_1.LeaseJournal; } });
|
|
10
10
|
var local_js_1 = require("./local.js");
|
|
11
11
|
Object.defineProperty(exports, "LocalLeaseProvider", { enumerable: true, get: function () { return local_js_1.LocalLeaseProvider; } });
|
|
12
|
+
var seed_js_1 = require("./seed.js");
|
|
13
|
+
Object.defineProperty(exports, "seedLeaseWatermark", { enumerable: true, get: function () { return seed_js_1.seedLeaseWatermark; } });
|
|
14
|
+
Object.defineProperty(exports, "exportLeaseWatermark", { enumerable: true, get: function () { return seed_js_1.exportLeaseWatermark; } });
|
|
12
15
|
var axia_js_1 = require("./axia.js");
|
|
13
16
|
Object.defineProperty(exports, "AxiaLeaseProvider", { enumerable: true, get: function () { return axia_js_1.AxiaLeaseProvider; } });
|
|
14
17
|
var hybrid_js_1 = require("./hybrid.js");
|
|
@@ -27,6 +30,8 @@ Object.defineProperty(exports, "deviceSlotForAddressIndex", { enumerable: true,
|
|
|
27
30
|
var errors_js_1 = require("./errors.js");
|
|
28
31
|
Object.defineProperty(exports, "WatermarkMonotonicityError", { enumerable: true, get: function () { return errors_js_1.WatermarkMonotonicityError; } });
|
|
29
32
|
Object.defineProperty(exports, "WatermarkExhaustedError", { enumerable: true, get: function () { return errors_js_1.WatermarkExhaustedError; } });
|
|
33
|
+
Object.defineProperty(exports, "WatermarkSeedRegressionError", { enumerable: true, get: function () { return errors_js_1.WatermarkSeedRegressionError; } });
|
|
34
|
+
Object.defineProperty(exports, "WatermarkSeedRequiredError", { enumerable: true, get: function () { return errors_js_1.WatermarkSeedRequiredError; } });
|
|
30
35
|
Object.defineProperty(exports, "LeaseNotFoundError", { enumerable: true, get: function () { return errors_js_1.LeaseNotFoundError; } });
|
|
31
36
|
Object.defineProperty(exports, "IndicesUnavailableError", { enumerable: true, get: function () { return errors_js_1.IndicesUnavailableError; } });
|
|
32
37
|
Object.defineProperty(exports, "PersonalLeaseNodeNotConfiguredError", { enumerable: true, get: function () { return errors_js_1.PersonalLeaseNodeNotConfiguredError; } });
|
package/dist/journal.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LeaseJournal — append-only audit log for WOTS key-use events.
|
|
3
|
-
*
|
|
2
|
+
* LeaseJournal — physically append-only audit log for WOTS key-use events.
|
|
3
|
+
*
|
|
4
|
+
* Layout v1 stores one immutable record per sequence. The legacy whole-array
|
|
5
|
+
* key is retained as migration input/forensic history but is never rewritten.
|
|
6
|
+
* The SHA-256 chain format is intentionally unchanged (RFC-007 OQ3).
|
|
4
7
|
*/
|
|
5
8
|
import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
|
|
6
9
|
import type { JournalEntry } from './types.js';
|
|
@@ -8,11 +11,18 @@ export declare class LeaseJournal {
|
|
|
8
11
|
private readonly storage;
|
|
9
12
|
private readonly logger;
|
|
10
13
|
private entries;
|
|
11
|
-
private
|
|
14
|
+
private initialized;
|
|
15
|
+
private initializePromise;
|
|
16
|
+
private appendTail;
|
|
12
17
|
constructor(storage: StorageAdapter, logger?: LoggerAdapter);
|
|
13
18
|
initialize(): Promise<void>;
|
|
19
|
+
private initializeOnce;
|
|
20
|
+
private readLegacyEntries;
|
|
21
|
+
private readPhysicalRecords;
|
|
22
|
+
private writeRecord;
|
|
14
23
|
append(entry: JournalEntry): Promise<void>;
|
|
15
|
-
private
|
|
24
|
+
private appendOne;
|
|
25
|
+
private ensureInitialized;
|
|
16
26
|
getAll(): JournalEntry[];
|
|
17
27
|
getByTree(treeId: string): JournalEntry[];
|
|
18
28
|
getByReservation(reservationId: string): JournalEntry | undefined;
|