@totemsdk/wots-lease 1.0.1 → 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.
- package/LICENSE +202 -21
- package/NOTICE +60 -0
- package/dist/axia.js +21 -25
- package/dist/certificate.js +7 -12
- package/dist/cjs/axia.d.ts +33 -0
- package/dist/cjs/axia.js +145 -0
- package/dist/cjs/certificate.d.ts +37 -0
- package/dist/cjs/certificate.js +74 -0
- package/dist/cjs/device.d.ts +18 -0
- package/dist/cjs/device.js +34 -0
- package/dist/cjs/errors.d.ts +71 -0
- package/dist/cjs/errors.js +135 -0
- package/dist/cjs/hybrid.d.ts +37 -0
- package/dist/cjs/hybrid.js +145 -0
- package/dist/cjs/index.d.ts +17 -0
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/journal.d.ts +30 -0
- package/dist/cjs/journal.js +369 -0
- package/dist/cjs/local.d.ts +83 -0
- package/dist/cjs/local.js +440 -0
- package/dist/cjs/onchain.d.ts +54 -0
- package/dist/cjs/onchain.js +314 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/quorum.d.ts +75 -0
- package/dist/cjs/quorum.js +307 -0
- package/dist/cjs/replicated-backup.d.ts +30 -0
- package/dist/cjs/replicated-backup.js +52 -0
- package/dist/cjs/seed.d.ts +41 -0
- package/dist/cjs/seed.js +84 -0
- package/dist/cjs/stubs.d.ts +50 -0
- package/dist/cjs/stubs.js +125 -0
- package/dist/cjs/types.d.ts +272 -0
- package/dist/cjs/types.js +5 -0
- package/dist/cjs/watermark.d.ts +51 -0
- package/dist/cjs/watermark.js +328 -0
- package/dist/device.js +2 -6
- package/dist/errors.js +14 -31
- package/dist/hybrid.js +4 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -43
- package/dist/journal.js +35 -38
- package/dist/local.js +29 -33
- package/dist/onchain.js +23 -27
- package/dist/quorum.js +9 -13
- package/dist/replicated-backup.d.ts +30 -0
- package/dist/replicated-backup.js +48 -0
- package/dist/seed.js +7 -11
- package/dist/stubs.js +5 -11
- package/dist/types.js +1 -2
- package/dist/watermark.js +11 -18
- package/package.json +22 -13
- package/rust/pkg/package.json +2 -2
- package/rust/pkg/wots_lease_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +2 -2
- package/rust/pkg-node/wots_lease_wasm_bg.wasm +0 -0
|
@@ -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.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* @module @totemsdk/wots-lease/seed
|
|
4
3
|
*
|
|
@@ -13,13 +12,10 @@
|
|
|
13
12
|
* If none is available and the caller has not declared the keyspace fresh,
|
|
14
13
|
* seeding **fails closed** — starting at 0 would reissue spent leaves.
|
|
15
14
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
exports.exportLeaseWatermark = exportLeaseWatermark;
|
|
19
|
-
const watermark_js_1 = require("./watermark.js");
|
|
20
|
-
const errors_js_1 = require("./errors.js");
|
|
15
|
+
import { flatIndex, fromFlatIndex } from './watermark.js';
|
|
16
|
+
import { WatermarkSeedRequiredError } from './errors.js';
|
|
21
17
|
function flatOf(w) {
|
|
22
|
-
return
|
|
18
|
+
return flatIndex({ addressIndex: w.addressCursor, l1: w.l1Cursor, l2: w.l2Cursor });
|
|
23
19
|
}
|
|
24
20
|
/**
|
|
25
21
|
* Seed a local watermark from the best available source, requiring at least one
|
|
@@ -30,7 +26,7 @@ function flatOf(w) {
|
|
|
30
26
|
* @throws {WatermarkSeedRegressionError} when a supplied seed is behind the
|
|
31
27
|
* current cursor.
|
|
32
28
|
*/
|
|
33
|
-
async function seedLeaseWatermark(params) {
|
|
29
|
+
export async function seedLeaseWatermark(params) {
|
|
34
30
|
const { local, treeId } = params;
|
|
35
31
|
// 1. On-chain cursor — advances the same local watermark (monotonic).
|
|
36
32
|
if (params.onchain) {
|
|
@@ -53,7 +49,7 @@ async function seedLeaseWatermark(params) {
|
|
|
53
49
|
}
|
|
54
50
|
// 3. Operator high-water mark.
|
|
55
51
|
if (params.operatorHighWaterMark !== undefined) {
|
|
56
|
-
const indices =
|
|
52
|
+
const indices = fromFlatIndex(params.operatorHighWaterMark);
|
|
57
53
|
const candidate = {
|
|
58
54
|
version: 1,
|
|
59
55
|
treeId,
|
|
@@ -74,11 +70,11 @@ async function seedLeaseWatermark(params) {
|
|
|
74
70
|
!params.exported &&
|
|
75
71
|
params.operatorHighWaterMark === undefined &&
|
|
76
72
|
!params.fresh) {
|
|
77
|
-
throw new
|
|
73
|
+
throw new WatermarkSeedRequiredError(treeId);
|
|
78
74
|
}
|
|
79
75
|
return local.getLocalWatermark(treeId);
|
|
80
76
|
}
|
|
81
77
|
/** Export a portable watermark blob for backup/restore. */
|
|
82
|
-
function exportLeaseWatermark(local, treeId) {
|
|
78
|
+
export function exportLeaseWatermark(local, treeId) {
|
|
83
79
|
return local.exportWatermark(treeId);
|
|
84
80
|
}
|
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,13 +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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
var quorum_js_1 = require("./quorum.js");
|
|
16
|
-
Object.defineProperty(exports, "P2PQuorumLeaseProvider", { enumerable: true, get: function () { return quorum_js_1.P2PQuorumLeaseProvider; } });
|
|
17
|
-
var onchain_js_1 = require("./onchain.js");
|
|
18
|
-
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';
|
|
19
14
|
/**
|
|
20
15
|
* Layer 3 — personal lookup-node lease coordinator.
|
|
21
16
|
*
|
|
@@ -35,7 +30,7 @@ Object.defineProperty(exports, "OnchainWatermarkProvider", { enumerable: true, g
|
|
|
35
30
|
* });
|
|
36
31
|
* ```
|
|
37
32
|
*/
|
|
38
|
-
class PersonalLeaseNodeProvider {
|
|
33
|
+
export class PersonalLeaseNodeProvider {
|
|
39
34
|
constructor(config) {
|
|
40
35
|
this.baseUrl = config.nodeUrl.replace(/\/$/, '');
|
|
41
36
|
this.nodePubkey = config.nodePubkey;
|
|
@@ -119,7 +114,6 @@ class PersonalLeaseNodeProvider {
|
|
|
119
114
|
verify: this.verify,
|
|
120
115
|
}
|
|
121
116
|
: undefined;
|
|
122
|
-
return
|
|
117
|
+
return certificateSignatureVerified(cert, signer);
|
|
123
118
|
}
|
|
124
119
|
}
|
|
125
|
-
exports.PersonalLeaseNodeProvider = PersonalLeaseNodeProvider;
|
package/dist/types.js
CHANGED
package/dist/watermark.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* WotsWatermarkStore — canonical v3 implementation
|
|
4
3
|
*
|
|
@@ -7,22 +6,17 @@
|
|
|
7
6
|
* - Enforces monotonicity: unavailable map can only grow, cursors can only advance.
|
|
8
7
|
* - Storage key unchanged ('totem_wots_watermark') to avoid storage churn.
|
|
9
8
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
exports.flatIndex = flatIndex;
|
|
13
|
-
exports.fromFlatIndex = fromFlatIndex;
|
|
14
|
-
exports.assertValidSigningIndices = assertValidSigningIndices;
|
|
15
|
-
const core_1 = require("@totemsdk/core");
|
|
16
|
-
const errors_js_1 = require("./errors.js");
|
|
9
|
+
import { NoopLogger } from '@totemsdk/core';
|
|
10
|
+
import { WatermarkMonotonicityError, WatermarkExhaustedError, WatermarkSeedRegressionError, } from './errors.js';
|
|
17
11
|
const STORAGE_KEY = 'totem_wots_watermark';
|
|
18
12
|
const MAX_L = 64;
|
|
19
13
|
const CAPACITY_PER_TREE = MAX_L * MAX_L * MAX_L;
|
|
20
14
|
const DEFAULT_TREE = 'default';
|
|
21
|
-
function flatIndex(idx) {
|
|
15
|
+
export function flatIndex(idx) {
|
|
22
16
|
assertValidSigningIndices(idx);
|
|
23
17
|
return idx.addressIndex * MAX_L * MAX_L + idx.l1 * MAX_L + idx.l2;
|
|
24
18
|
}
|
|
25
|
-
function fromFlatIndex(flat) {
|
|
19
|
+
export function fromFlatIndex(flat) {
|
|
26
20
|
if (!Number.isSafeInteger(flat) || flat < 0 || flat >= CAPACITY_PER_TREE) {
|
|
27
21
|
throw new RangeError(`WOTS flat index must be an integer in [0, ${CAPACITY_PER_TREE - 1}]`);
|
|
28
22
|
}
|
|
@@ -32,7 +26,7 @@ function fromFlatIndex(flat) {
|
|
|
32
26
|
const l2 = rem % MAX_L;
|
|
33
27
|
return { addressIndex, l1, l2 };
|
|
34
28
|
}
|
|
35
|
-
function assertValidSigningIndices(indices) {
|
|
29
|
+
export function assertValidSigningIndices(indices) {
|
|
36
30
|
for (const name of ['addressIndex', 'l1', 'l2']) {
|
|
37
31
|
const value = indices[name];
|
|
38
32
|
if (!Number.isSafeInteger(value) || value < 0 || value >= MAX_L) {
|
|
@@ -131,8 +125,8 @@ function migrateToV3(raw) {
|
|
|
131
125
|
tree.l2Cursor = v1.next_l2 ?? 0;
|
|
132
126
|
return { version: 3, trees: { [DEFAULT_TREE]: tree } };
|
|
133
127
|
}
|
|
134
|
-
class WotsWatermarkStore {
|
|
135
|
-
constructor(storage, logger = new
|
|
128
|
+
export class WotsWatermarkStore {
|
|
129
|
+
constructor(storage, logger = new NoopLogger()) {
|
|
136
130
|
this.storage = storage;
|
|
137
131
|
this.state = null;
|
|
138
132
|
this._initialized = false;
|
|
@@ -194,7 +188,7 @@ class WotsWatermarkStore {
|
|
|
194
188
|
cur = nxt;
|
|
195
189
|
attempts++;
|
|
196
190
|
}
|
|
197
|
-
throw new
|
|
191
|
+
throw new WatermarkExhaustedError(treeId);
|
|
198
192
|
}
|
|
199
193
|
async markUnavailable(treeId, indices, reason) {
|
|
200
194
|
const tree = this.getOrCreateTree(treeId);
|
|
@@ -232,12 +226,12 @@ class WotsWatermarkStore {
|
|
|
232
226
|
const existingFlat = flatIndex({ addressIndex: existing.addressCursor, l1: existing.l1Cursor, l2: existing.l2Cursor });
|
|
233
227
|
const newFlat = flatIndex({ addressIndex: merged.addressCursor, l1: merged.l1Cursor, l2: merged.l2Cursor });
|
|
234
228
|
if (newFlat < existingFlat) {
|
|
235
|
-
throw new
|
|
229
|
+
throw new WatermarkMonotonicityError(`Cannot decrease watermark cursor for ${treeId}: ${existingFlat} → ${newFlat}`);
|
|
236
230
|
}
|
|
237
231
|
const existingUnavailCount = Object.keys(existing.unavailable).length;
|
|
238
232
|
const newUnavailCount = Object.keys(merged.unavailable).length;
|
|
239
233
|
if (newUnavailCount < existingUnavailCount) {
|
|
240
|
-
throw new
|
|
234
|
+
throw new WatermarkMonotonicityError(`Cannot shrink unavailable map for ${treeId}: ${existingUnavailCount} → ${newUnavailCount}`);
|
|
241
235
|
}
|
|
242
236
|
s.trees[treeId] = merged;
|
|
243
237
|
await this.persist();
|
|
@@ -295,7 +289,7 @@ class WotsWatermarkStore {
|
|
|
295
289
|
l2: existing.l2Cursor,
|
|
296
290
|
});
|
|
297
291
|
if (!options.allowRegression && seedCursor < existingCursor) {
|
|
298
|
-
throw new
|
|
292
|
+
throw new WatermarkSeedRegressionError(seed.treeId, existingCursor, seedCursor);
|
|
299
293
|
}
|
|
300
294
|
const unavailable = { ...existing.unavailable };
|
|
301
295
|
for (const flat of seed.unavailable ?? []) {
|
|
@@ -325,4 +319,3 @@ class WotsWatermarkStore {
|
|
|
325
319
|
await this.storage.remove(STORAGE_KEY);
|
|
326
320
|
}
|
|
327
321
|
}
|
|
328
|
-
exports.WotsWatermarkStore = WotsWatermarkStore;
|
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/wots-lease",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "WOTS key-use coordination — canonical v3 watermark, provider-based lease safety layers",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
5
|
+
"main": "dist/cjs/index.js",
|
|
6
|
+
"types": "dist/cjs/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/cjs/index.d.ts",
|
|
15
|
+
"default": "./dist/cjs/index.js"
|
|
16
|
+
}
|
|
12
17
|
}
|
|
13
18
|
},
|
|
14
19
|
"files": [
|
|
@@ -16,13 +21,16 @@
|
|
|
16
21
|
"rust/pkg/",
|
|
17
22
|
"rust/pkg-node/",
|
|
18
23
|
"README.md",
|
|
19
|
-
"LICENSE"
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"NOTICE"
|
|
20
26
|
],
|
|
21
27
|
"dependencies": {
|
|
22
|
-
"@
|
|
23
|
-
"@totemsdk/
|
|
24
|
-
"@totemsdk/txpow": "^1.
|
|
25
|
-
"@totemsdk/
|
|
28
|
+
"@noble/hashes": "^1.8.0",
|
|
29
|
+
"@totemsdk/core": "^1.4.2",
|
|
30
|
+
"@totemsdk/txpow": "^1.1.1",
|
|
31
|
+
"@totemsdk/replication": "^0.2.0",
|
|
32
|
+
"@totemsdk/lookup-protocol": "^0.3.0",
|
|
33
|
+
"@totemsdk/storage": "^0.2.2"
|
|
26
34
|
},
|
|
27
35
|
"devDependencies": {
|
|
28
36
|
"@types/jest": "^30.0.0",
|
|
@@ -37,7 +45,7 @@
|
|
|
37
45
|
"publishConfig": {
|
|
38
46
|
"access": "public"
|
|
39
47
|
},
|
|
40
|
-
"license": "
|
|
48
|
+
"license": "Apache-2.0",
|
|
41
49
|
"author": "Totem SDK",
|
|
42
50
|
"homepage": "https://docs.totem.ing",
|
|
43
51
|
"bugs": {
|
|
@@ -57,8 +65,9 @@
|
|
|
57
65
|
"wots",
|
|
58
66
|
"utxo"
|
|
59
67
|
],
|
|
68
|
+
"type": "module",
|
|
60
69
|
"scripts": {
|
|
61
|
-
"build": "
|
|
70
|
+
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist/cjs/package.json', JSON.stringify({type:'commonjs'})+'\\n')\"",
|
|
62
71
|
"build:wasm": "cd rust && wasm-pack build --target bundler --out-dir pkg && wasm-pack build --target nodejs --out-dir pkg-node",
|
|
63
72
|
"clean": "rm -rf dist rust/pkg rust/pkg-node rust/target",
|
|
64
73
|
"test": "jest"
|
package/rust/pkg/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
],
|
|
7
7
|
"description": "WOTS key-use coordination — canonical v3 watermark, flat index computation, monotonicity enforcement, device keyspace partitioning (Rust/WASM)",
|
|
8
8
|
"version": "0.1.0",
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/Totem-Edge/totem-sdk"
|
|
@@ -24,4 +24,4 @@
|
|
|
24
24
|
"./wots_lease_wasm.js",
|
|
25
25
|
"./snippets/*"
|
|
26
26
|
]
|
|
27
|
-
}
|
|
27
|
+
}
|
|
Binary file
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
],
|
|
6
6
|
"description": "WOTS key-use coordination — canonical v3 watermark, flat index computation, monotonicity enforcement, device keyspace partitioning (Rust/WASM)",
|
|
7
7
|
"version": "0.1.0",
|
|
8
|
-
"license": "
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "https://github.com/Totem-Edge/totem-sdk"
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
"main": "wots_lease_wasm.js",
|
|
19
19
|
"homepage": "https://totem.ing",
|
|
20
20
|
"types": "wots_lease_wasm.d.ts"
|
|
21
|
-
}
|
|
21
|
+
}
|
|
Binary file
|