@ethernity-cloud/sdk-js 1.5.9 → 1.5.12
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 +38 -3
- package/nodenithy/build/securelock/src/ecld_state.js +115 -12
- package/nodenithy/build/securelock/src/esr_local.js +161 -0
- package/nodenithy/build/securelock/src/etny_exec.js +230 -220
- package/nodenithy/build/securelock/src/task_status.js +30 -28
- package/nodenithy/run.js +0 -0
- package/package.json +16 -12
- package/pynithy/run.js +0 -0
- package/run.js +245 -0
- package/test.js +286 -204
package/README.md
CHANGED
|
@@ -65,11 +65,46 @@ The sdk has been tested on the following operating systems:
|
|
|
65
65
|
```
|
|
66
66
|
Required after build, to build and integrate the secure certificates that will be used during executions and to register the project to the Ethernity Cloud Image Register.
|
|
67
67
|
|
|
68
|
-
- **
|
|
68
|
+
- **Test (local, no chain)**: To run your backend locally with the enclave's own
|
|
69
|
+
executor — no SGX, no gas, instant — run:
|
|
69
70
|
```sh
|
|
70
|
-
|
|
71
|
+
npx ecld-test 'hello("World")'
|
|
72
|
+
npx ecld-test --file payload.js
|
|
73
|
+
npx ecld-test --input data.json 'processData(___etny_data_set___)'
|
|
74
|
+
```
|
|
75
|
+
This validates the *call* (function names, arguments, result wiring) before a
|
|
76
|
+
single on-chain request is paid for. If your dApp uses ESR (Enclave State
|
|
77
|
+
Registry), state is emulated locally and **on by default**: `require('../ecld_state').StateRegistry`
|
|
78
|
+
get/commit, ownership/ACL, and `taskCaller()` all work in-process against an
|
|
79
|
+
in-memory registry, and persist between runs in `.ecld-esr-local.*.json`. The
|
|
80
|
+
task caller defaults to your developer address; override it with `--caller`.
|
|
81
|
+
`npx ecld-test serve` starts a local API so the runner's LOCAL mode drives your
|
|
82
|
+
real integration end to end. Exit code 0 on `SUCCESS`, 1 otherwise.
|
|
83
|
+
|
|
84
|
+
- **Run (on the network)**: To submit a payload to the network and print the
|
|
85
|
+
decrypted result, run:
|
|
86
|
+
```sh
|
|
87
|
+
npx ecld-run 'hello("World")'
|
|
88
|
+
npx ecld-run --file payload.js
|
|
89
|
+
npx ecld-run --input data.json 'processData(___etny_data_set___)'
|
|
90
|
+
npx ecld-run --json 'esrIncrement()'
|
|
91
|
+
```
|
|
92
|
+
This is the network-side sibling of `ecld-test`: instead of executing locally
|
|
93
|
+
it drives the runner end to end — encrypt → IPFS → on-chain request → wait for
|
|
94
|
+
a node → download and decrypt the result. It **costs gas and needs a funded
|
|
95
|
+
key**. Network and enclaves come from `.env` (`BLOCKCHAIN_NETWORK`,
|
|
96
|
+
`PROJECT_NAME`, `TRUSTED_ZONE_IMAGE`), overridable with
|
|
97
|
+
`--network`/`--securelock`/`--trustedzone`; the signing key is `PRIVATE_KEY`
|
|
98
|
+
(or `ECLD_PRIVATE_KEY`) — a funded `0x` key that pays for the order. Resources
|
|
99
|
+
are tunable with `--task-price`/`--cpu`/`--memory`/`--storage`/`--bandwidth`/`--duration`/`--validators`.
|
|
100
|
+
Exit code 0 on a `SUCCESS` task result, 1 otherwise.
|
|
101
|
+
|
|
102
|
+
- **Inspect (read-only)**: To read enclave and on-chain diagnostics — network,
|
|
103
|
+
trustedzone/securelock registration, and ESR state — without spending gas, run:
|
|
104
|
+
```sh
|
|
105
|
+
npx ecld-info
|
|
106
|
+
npx ecld-info esr state <key>
|
|
71
107
|
```
|
|
72
|
-
command to start the demo application and test the integration.
|
|
73
108
|
|
|
74
109
|
## Project Structure
|
|
75
110
|
|
|
@@ -50,6 +50,16 @@ let _bucket = null;
|
|
|
50
50
|
let _contractAddress = null;
|
|
51
51
|
let _provider = null;
|
|
52
52
|
|
|
53
|
+
// LOCAL TESTING ONLY (esr_local). In the real enclave these stay null: the node
|
|
54
|
+
// applies commitFor on-chain and _contract() builds a real ethers.Contract. The
|
|
55
|
+
// local ESR emulator overrides the "contract" with an in-memory object and
|
|
56
|
+
// applies commits itself, so ecld-test runs state-using backends with no chain.
|
|
57
|
+
let _contractOverride = null;
|
|
58
|
+
let _localCommitApply = null;
|
|
59
|
+
|
|
60
|
+
function setContractOverride(obj) { _contractOverride = obj; }
|
|
61
|
+
function setLocalCommitApply(fn) { _localCommitApply = fn; }
|
|
62
|
+
|
|
53
63
|
/**
|
|
54
64
|
* Wire the registry to the enclave's identity, storage and chain access.
|
|
55
65
|
* Called by securelock at task start; payload code never calls this.
|
|
@@ -91,6 +101,11 @@ const CONTAINER_MARK = '_ecld_state';
|
|
|
91
101
|
|
|
92
102
|
class StatePermissionError extends Error {}
|
|
93
103
|
|
|
104
|
+
// A commit's idempotency nonce was already used: the dApp supplied `nonce` on
|
|
105
|
+
// commit() and it is not greater than the last accepted nonce for the key --
|
|
106
|
+
// a duplicate (or out-of-order) submission. The state was NOT changed.
|
|
107
|
+
class StateNonceError extends Error {}
|
|
108
|
+
|
|
94
109
|
function unwrapStored(stored) {
|
|
95
110
|
if (stored && typeof stored === 'object' && stored[CONTAINER_MARK] === 1) {
|
|
96
111
|
return [stored.acl || null, stored.data];
|
|
@@ -98,13 +113,25 @@ function unwrapStored(stored) {
|
|
|
98
113
|
return [null, stored];
|
|
99
114
|
}
|
|
100
115
|
|
|
101
|
-
|
|
116
|
+
/** Last accepted idempotency nonce carried by a stored blob (0 if none). */
|
|
117
|
+
function storedNonce(stored) {
|
|
118
|
+
if (stored && typeof stored === 'object' && stored[CONTAINER_MARK] === 1) {
|
|
119
|
+
const n = parseInt(stored.nonce, 10);
|
|
120
|
+
return Number.isFinite(n) ? n : 0;
|
|
121
|
+
}
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function wrapStored(acl, data, committedBy = null, nonce = null) {
|
|
102
126
|
// For owned state, bind the authoring caller (committedBy) INTO the blob, so
|
|
103
127
|
// it lands in the CID -- the field the securelock signs -- making the caller
|
|
104
128
|
// cryptographically bound to the commit, not merely a re-stampable sidecar.
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
// Unowned state keeps the legacy bare shape -- unless a nonce must be
|
|
130
|
+
// carried, which forces the container so the nonce survives in the blob.
|
|
131
|
+
if (!acl && nonce == null) return data;
|
|
132
|
+
const container = { [CONTAINER_MARK]: 1, acl: acl || null, data };
|
|
107
133
|
if (committedBy) container.committedBy = committedBy;
|
|
134
|
+
if (nonce != null) container.nonce = Math.trunc(nonce);
|
|
108
135
|
return container;
|
|
109
136
|
}
|
|
110
137
|
|
|
@@ -318,6 +345,15 @@ class StateRegistry {
|
|
|
318
345
|
return esr_wallet.deriveWalletAddress(_identityPriv);
|
|
319
346
|
}
|
|
320
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Alias of walletAddress — the ENCLAVE's own on-chain address (its identity,
|
|
350
|
+
* and the namespace all its state keys live under). Preferred name in new
|
|
351
|
+
* code; walletAddress remains for compatibility.
|
|
352
|
+
*/
|
|
353
|
+
get enclaveAddress() {
|
|
354
|
+
return this.walletAddress;
|
|
355
|
+
}
|
|
356
|
+
|
|
321
357
|
_signer() {
|
|
322
358
|
// Reuse esr_wallet's derivation rather than repeating it here: two copies
|
|
323
359
|
// would eventually disagree, and the address a client funds would stop
|
|
@@ -329,6 +365,8 @@ class StateRegistry {
|
|
|
329
365
|
}
|
|
330
366
|
|
|
331
367
|
_contract(signerOrProvider) {
|
|
368
|
+
// Local emulator: use the in-memory contract object instead of a real one.
|
|
369
|
+
if (_contractOverride) return _contractOverride;
|
|
332
370
|
return new ethers.Contract(_contractAddress, ESR_ABI, signerOrProvider || _provider);
|
|
333
371
|
}
|
|
334
372
|
|
|
@@ -356,11 +394,11 @@ class StateRegistry {
|
|
|
356
394
|
return data;
|
|
357
395
|
}
|
|
358
396
|
|
|
359
|
-
/** [acl, data, version, cid] for `key`; NO permission check here. */
|
|
397
|
+
/** [acl, data, version, cid, nonce] for `key`; NO permission check here. */
|
|
360
398
|
async _readContainer(key, fallback = {}) {
|
|
361
399
|
const [cid, version] = await this._contract().getState(this.walletAddress, keyHash(key));
|
|
362
400
|
if (!version || !cid) {
|
|
363
|
-
return [null, fallback, 0, null];
|
|
401
|
+
return [null, fallback, 0, null, 0];
|
|
364
402
|
}
|
|
365
403
|
if (!looksLikeCID(cid)) {
|
|
366
404
|
throw new Error(
|
|
@@ -371,7 +409,23 @@ class StateRegistry {
|
|
|
371
409
|
const blob = await this._fetch(key, cid);
|
|
372
410
|
const stored = JSON.parse(decrypt(blob).toString('utf8'));
|
|
373
411
|
const [acl, data] = unwrapStored(stored);
|
|
374
|
-
return [acl, data, Number(version), cid];
|
|
412
|
+
return [acl, data, Number(version), cid, storedNonce(stored)];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The last accepted idempotency nonce for `key` (0 if none was ever used).
|
|
417
|
+
* A dApp that wants duplicate suppression reads this, picks a greater value
|
|
418
|
+
* (e.g. +1, or a timestamp), and passes it to commit(..., { nonce }).
|
|
419
|
+
*/
|
|
420
|
+
async getNonce(key) {
|
|
421
|
+
const [acl, , , , nonce] = await this._readContainer(key);
|
|
422
|
+
if (!canRead(acl)) {
|
|
423
|
+
throw new StatePermissionError(
|
|
424
|
+
`caller ${_taskCaller || '<anonymous>'} has no read permission on ` +
|
|
425
|
+
`state key '${key}' (owner: ${acl.owner})`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
return nonce;
|
|
375
429
|
}
|
|
376
430
|
|
|
377
431
|
/**
|
|
@@ -381,7 +435,14 @@ class StateRegistry {
|
|
|
381
435
|
* commit lands in between, the contract rejects ours and we re-read and
|
|
382
436
|
* retry, so concurrent tasks cannot silently lose updates.
|
|
383
437
|
*/
|
|
384
|
-
async commit(key, mutate,
|
|
438
|
+
async commit(key, mutate, opts = 3) {
|
|
439
|
+
// Back-compat: the third arg used to be `attempts` (a number); it may now
|
|
440
|
+
// also be { attempts, nonce }. `nonce` is an idempotency guard the dApp
|
|
441
|
+
// controls: it must be STRICTLY GREATER than the last accepted nonce for
|
|
442
|
+
// the key (see getNonce). A duplicate or stale nonce throws
|
|
443
|
+
// StateNonceError and the state is NOT changed.
|
|
444
|
+
const { attempts = 3, nonce = null } =
|
|
445
|
+
typeof opts === 'number' ? { attempts: opts } : (opts || {});
|
|
385
446
|
const transform = async (acl, data) => {
|
|
386
447
|
if (!canWrite(acl)) {
|
|
387
448
|
throw new StatePermissionError(
|
|
@@ -393,7 +454,7 @@ class StateRegistry {
|
|
|
393
454
|
if (!nextAcl && _taskCaller) nextAcl = newAcl(_taskCaller); // first-writer-owns
|
|
394
455
|
return [nextAcl, await mutate(data)];
|
|
395
456
|
};
|
|
396
|
-
const [, newData] = await this._commitTransform(key, transform, attempts);
|
|
457
|
+
const [, newData] = await this._commitTransform(key, transform, attempts, nonce);
|
|
397
458
|
return newData;
|
|
398
459
|
}
|
|
399
460
|
|
|
@@ -403,16 +464,33 @@ class StateRegistry {
|
|
|
403
464
|
* data while unowned). On a version race we re-read and retry, so
|
|
404
465
|
* concurrent tasks cannot silently lose updates.
|
|
405
466
|
*/
|
|
406
|
-
async _commitTransform(key, transform, attempts = 3) {
|
|
467
|
+
async _commitTransform(key, transform, attempts = 3, nonce = null) {
|
|
407
468
|
let lastError = null;
|
|
408
469
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
409
470
|
// eslint-disable-next-line no-await-in-loop
|
|
410
|
-
const [acl, data, currentVersion] = await this._readContainer(key);
|
|
471
|
+
const [acl, data, currentVersion, , priorNonce] = await this._readContainer(key);
|
|
472
|
+
// The nonce check re-runs on every retry against the freshly read state,
|
|
473
|
+
// so a racing commit that consumed the same nonce fails this one.
|
|
474
|
+
let finalNonce;
|
|
475
|
+
if (nonce != null) {
|
|
476
|
+
const n = Math.trunc(nonce);
|
|
477
|
+
if (n <= priorNonce) {
|
|
478
|
+
throw new StateNonceError(
|
|
479
|
+
`nonce ${n} was already used for state key '${key}' (last accepted: ` +
|
|
480
|
+
`${priorNonce}); duplicate commit suppressed, state unchanged`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
finalNonce = n;
|
|
484
|
+
} else {
|
|
485
|
+
// Preserve the stored nonce so a nonce-less commit cannot reset the
|
|
486
|
+
// guard and reopen replays.
|
|
487
|
+
finalNonce = priorNonce > 0 ? priorNonce : null;
|
|
488
|
+
}
|
|
411
489
|
// eslint-disable-next-line no-await-in-loop
|
|
412
490
|
const [nextAcl, newData] = await transform(acl, data, currentVersion);
|
|
413
491
|
// Bind the authoring caller into the blob (hence the CID/signature) for
|
|
414
492
|
// owned state.
|
|
415
|
-
const stored = wrapStored(nextAcl, newData, nextAcl ? _taskCaller : null);
|
|
493
|
+
const stored = wrapStored(nextAcl, newData, nextAcl ? _taskCaller : null, finalNonce);
|
|
416
494
|
|
|
417
495
|
const blob = encrypt(Buffer.from(JSON.stringify(stored), 'utf8'));
|
|
418
496
|
// Compute the CID from OUR bytes — never accept one from the node.
|
|
@@ -515,6 +593,14 @@ class StateRegistry {
|
|
|
515
593
|
await _swift.putFileContent(
|
|
516
594
|
_bucket, `esr.commit.${Number(relayNonce)}.json`, '',
|
|
517
595
|
Buffer.from(JSON.stringify(auth), 'utf8'));
|
|
596
|
+
|
|
597
|
+
// LOCAL TESTING: with no node to relay commitFor, apply the commit to the
|
|
598
|
+
// in-memory registry now so getState/getVersion reflect it. No-op in the
|
|
599
|
+
// real enclave (hook is null) -- there the node applies it on-chain.
|
|
600
|
+
if (_localCommitApply) {
|
|
601
|
+
await _localCommitApply(enclave, keyHashHex, cid, Number(expectedVersion));
|
|
602
|
+
}
|
|
603
|
+
|
|
518
604
|
return { relayed: true, relayNonce: Number(relayNonce) };
|
|
519
605
|
}
|
|
520
606
|
}
|
|
@@ -600,6 +686,16 @@ async function esrAcl(key) {
|
|
|
600
686
|
return { ...acl };
|
|
601
687
|
}
|
|
602
688
|
|
|
689
|
+
function setTaskCaller(caller) { _taskCaller = normAddr(caller); }
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* The last accepted idempotency nonce for `key` (0 if none). See
|
|
693
|
+
* StateRegistry.getNonce / commit(key, mutate, { nonce }).
|
|
694
|
+
*/
|
|
695
|
+
async function esrNonce(key) {
|
|
696
|
+
return new StateRegistry().getNonce(key);
|
|
697
|
+
}
|
|
698
|
+
|
|
603
699
|
module.exports = {
|
|
604
700
|
StateRegistry,
|
|
605
701
|
configure,
|
|
@@ -607,12 +703,19 @@ module.exports = {
|
|
|
607
703
|
looksLikeCID,
|
|
608
704
|
ledgerSnapshot,
|
|
609
705
|
taskCaller,
|
|
706
|
+
setTaskCaller,
|
|
610
707
|
restampLedgerCaller,
|
|
611
708
|
StatePermissionError,
|
|
709
|
+
StateNonceError,
|
|
612
710
|
esrGrant,
|
|
613
711
|
esrRevoke,
|
|
614
712
|
esrSetPublicRead,
|
|
615
713
|
esrTransfer,
|
|
616
714
|
esrOwner,
|
|
617
|
-
esrAcl
|
|
715
|
+
esrAcl,
|
|
716
|
+
esrNonce,
|
|
717
|
+
// local-testing hooks (esr_local only)
|
|
718
|
+
setContractOverride,
|
|
719
|
+
setLocalCommitApply,
|
|
720
|
+
normAddr
|
|
618
721
|
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local ESR emulator for `ecld-test` -- run state-using backends with no chain.
|
|
3
|
+
*
|
|
4
|
+
* In-memory stand-ins for the two external things StateRegistry talks to, so
|
|
5
|
+
* get/commit/esrGrant/... behave EXACTLY as on-chain but in-process, instantly,
|
|
6
|
+
* with zero orders and zero gas:
|
|
7
|
+
*
|
|
8
|
+
* MemSwift -- state-blob store (replaces SwiftStream): putFileContent /
|
|
9
|
+
* getFileContentBytes over a Map, optionally mirrored to a JSON
|
|
10
|
+
* file so state survives across `ecld-test` runs.
|
|
11
|
+
* MemContract -- the ESR contract methods StateRegistry calls
|
|
12
|
+
* (getState / getVersion / relayNonce / commitDigest) plus the
|
|
13
|
+
* commit APPLY the node would do on-chain, keyed by
|
|
14
|
+
* (enclave, keyHash) -> { cid, version }.
|
|
15
|
+
*
|
|
16
|
+
* install(ecldState, { caller, filePrefix }) wires these into ecld_state via its
|
|
17
|
+
* local-testing hooks. Used only by the local test harness; the real enclave
|
|
18
|
+
* never requires this file.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const { ethers } = require('ethers');
|
|
25
|
+
|
|
26
|
+
function key(enclave, keyHash) {
|
|
27
|
+
return `${String(enclave).toLowerCase()}|${String(keyHash).toLowerCase()}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class MemSwift {
|
|
31
|
+
constructor(file) {
|
|
32
|
+
this._file = file || null;
|
|
33
|
+
this._store = new Map();
|
|
34
|
+
if (this._file && fs.existsSync(this._file)) {
|
|
35
|
+
try {
|
|
36
|
+
const raw = JSON.parse(fs.readFileSync(this._file, 'utf8'));
|
|
37
|
+
for (const [k, v] of Object.entries(raw)) this._store.set(k, Buffer.from(v, 'base64'));
|
|
38
|
+
} catch (e) { /* start empty */ }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_persist() {
|
|
43
|
+
if (!this._file) return;
|
|
44
|
+
try {
|
|
45
|
+
const obj = {};
|
|
46
|
+
for (const [k, v] of this._store) obj[k] = Buffer.from(v).toString('base64');
|
|
47
|
+
fs.writeFileSync(this._file + '.tmp', JSON.stringify(obj));
|
|
48
|
+
fs.renameSync(this._file + '.tmp', this._file);
|
|
49
|
+
} catch (e) { /* best-effort */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async putFileContent(bucket, name, _prefix, data) {
|
|
53
|
+
const buf = Buffer.isBuffer(data) ? data
|
|
54
|
+
: (typeof data === 'string' ? Buffer.from(data, 'utf8') : Buffer.from(data));
|
|
55
|
+
this._store.set(`${bucket}/${name}`, buf);
|
|
56
|
+
this._persist();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getFileContentBytes(bucket, name) {
|
|
61
|
+
const v = this._store.get(`${bucket}/${name}`);
|
|
62
|
+
return v === undefined ? null : v;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getFileContent(bucket, name) {
|
|
66
|
+
const v = await this.getFileContentBytes(bucket, name);
|
|
67
|
+
return v === null ? null : v.toString('utf8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createBucket() { return true; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class MemContract {
|
|
74
|
+
constructor(file) {
|
|
75
|
+
this._file = file || null;
|
|
76
|
+
this._entries = {}; // key -> { cid, version, updatedAt }
|
|
77
|
+
this._nonce = {}; // enclave -> n
|
|
78
|
+
if (this._file && fs.existsSync(this._file)) {
|
|
79
|
+
try {
|
|
80
|
+
const d = JSON.parse(fs.readFileSync(this._file, 'utf8'));
|
|
81
|
+
this._entries = d.entries || {};
|
|
82
|
+
this._nonce = d.nonce || {};
|
|
83
|
+
} catch (e) { /* start empty */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_persist() {
|
|
88
|
+
if (!this._file) return;
|
|
89
|
+
try {
|
|
90
|
+
fs.writeFileSync(this._file + '.tmp', JSON.stringify({ entries: this._entries, nonce: this._nonce }));
|
|
91
|
+
fs.renameSync(this._file + '.tmp', this._file);
|
|
92
|
+
} catch (e) { /* best-effort */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- the ethers.Contract methods StateRegistry calls ----
|
|
96
|
+
async getState(enclave, keyHash) {
|
|
97
|
+
const e = this._entries[key(enclave, keyHash)];
|
|
98
|
+
if (!e) return ['', ethers.BigNumber.from(0), ethers.BigNumber.from(0)];
|
|
99
|
+
return [e.cid, ethers.BigNumber.from(e.version), ethers.BigNumber.from(e.updatedAt || 0)];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getVersion(enclave, keyHash) {
|
|
103
|
+
const e = this._entries[key(enclave, keyHash)];
|
|
104
|
+
return ethers.BigNumber.from(e ? e.version : 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async relayNonce(enclave) {
|
|
108
|
+
return ethers.BigNumber.from(this._nonce[String(enclave).toLowerCase()] || 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async commitDigest(enclave, keyHash, cid, expectedVersion, relayNonce) {
|
|
112
|
+
// Deterministic 32-byte digest (no real signing needed locally; the
|
|
113
|
+
// securelock still signs it with the local test key).
|
|
114
|
+
return ethers.utils.id(JSON.stringify([String(enclave).toLowerCase(), keyHash, cid,
|
|
115
|
+
Number(expectedVersion), Number(relayNonce)]));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async entryCount() {
|
|
119
|
+
return ethers.BigNumber.from(Object.keys(this._entries).length);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- the on-chain commitFor, applied locally ----
|
|
123
|
+
applyCommit(enclave, keyHash, cid, expectedVersion) {
|
|
124
|
+
const k = key(enclave, keyHash);
|
|
125
|
+
const cur = this._entries[k] ? Number(this._entries[k].version) : 0;
|
|
126
|
+
if (Number(expectedVersion) !== cur) {
|
|
127
|
+
throw new Error(`VersionMismatch: expected ${expectedVersion} but current is ${cur}`);
|
|
128
|
+
}
|
|
129
|
+
this._entries[k] = { cid, version: cur + 1, updatedAt: 0 };
|
|
130
|
+
const e = String(enclave).toLowerCase();
|
|
131
|
+
this._nonce[e] = (this._nonce[e] || 0) + 1;
|
|
132
|
+
this._persist();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A fixed local test identity so walletAddress is stable across runs.
|
|
137
|
+
const LOCAL_IDENTITY = Buffer.from('ecld-local-test-identity-key-0001');
|
|
138
|
+
|
|
139
|
+
function install(ecldState, { caller = null, filePrefix = '.ecld-esr-local' } = {}) {
|
|
140
|
+
const swiftFile = filePrefix ? `${filePrefix}.swift.json` : null;
|
|
141
|
+
const regFile = filePrefix ? `${filePrefix}.registry.json` : null;
|
|
142
|
+
const swift = new MemSwift(swiftFile);
|
|
143
|
+
const contract = new MemContract(regFile);
|
|
144
|
+
|
|
145
|
+
ecldState.configure({
|
|
146
|
+
identityPriv: LOCAL_IDENTITY,
|
|
147
|
+
swiftStreamService: swift,
|
|
148
|
+
bucket: 'ecld-local',
|
|
149
|
+
contractAddress: '0xLOCAL',
|
|
150
|
+
provider: null,
|
|
151
|
+
caller,
|
|
152
|
+
});
|
|
153
|
+
ecldState.setContractOverride(contract);
|
|
154
|
+
ecldState.setLocalCommitApply(
|
|
155
|
+
(enclave, keyHash, cid, expectedVersion) =>
|
|
156
|
+
contract.applyCommit(enclave, keyHash, cid, expectedVersion));
|
|
157
|
+
|
|
158
|
+
return { swift, contract };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { install, MemSwift, MemContract };
|