@ethernity-cloud/sdk-js 1.5.8 → 1.5.11

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 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
- - **Run**: To run the project, run:
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
- npm run start
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
 
package/info.js ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ecld-info: everything a developer needs to read or troubleshoot an enclave.
4
+ *
5
+ * Read-only and free (eth_call + event logs -- no task, no gas, no private
6
+ * key). Reads your project's identity from .env (PROJECT_NAME / IPFS_HASH /
7
+ * BLOCKCHAIN_NETWORK), then reports:
8
+ *
9
+ * NETWORK name, type, chain id, RPC, contract addresses, live block
10
+ * TRUSTEDZONE the gatekeeper enclave's Image Registry record
11
+ * SECURELOCK the executor enclave's Image Registry record
12
+ * ESR registry address, total keys, and -- with --enclave <wallet>
13
+ * -- this enclave's recent StateCommitted events
14
+ *
15
+ * Subcommands (default: the full summary):
16
+ * ecld-info full summary
17
+ * ecld-info network just the network section
18
+ * ecld-info trustedzone just the trustedzone registration
19
+ * ecld-info securelock just the securelock registration
20
+ * ecld-info esr address|count|state|version|list ESR inspection
21
+ *
22
+ * Registration is keyed by the image's IPFS hash: pass --ipfs <hash> for an
23
+ * exact record, or --name <n> to resolve the latest registered version of a
24
+ * name. With neither, .env IPFS_HASH / PROJECT_NAME are used.
25
+ *
26
+ * Network: --network NAME, else ESR_NETWORK env, else .env BLOCKCHAIN_NETWORK,
27
+ * else BLOXBERG_TESTNET.
28
+ */
29
+
30
+ 'use strict';
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+ const { ethers } = require('ethers');
35
+ try { require('dotenv').config(); } catch (e) { /* dotenv optional */ }
36
+
37
+ // Authoritative per-network values (mirror of the Python SDK's network table).
38
+ const NETWORKS = {
39
+ BLOXBERG_MAINNET: {
40
+ type: 'mainnet', chainId: 8995, rpc: 'https://core.bloxberg.org',
41
+ protocol: '0x549A6E06BB2084100148D50F51CF77a3436C3Ae7',
42
+ imageRegistry: '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31',
43
+ esr: '0xF76469A5659670B6ade366dE635e6463aaB8f3D8',
44
+ },
45
+ BLOXBERG_TESTNET: {
46
+ type: 'testnet', chainId: 8995, rpc: 'https://core.bloxberg.org',
47
+ protocol: '0x02882F03097fE8cD31afbdFbB5D72a498B41112c',
48
+ imageRegistry: '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31',
49
+ esr: '0xda5e68Bb5e68ee14D73b8de2a4D3Ca15736fACfb',
50
+ },
51
+ LITVM_LITEFORGE: {
52
+ type: 'testnet', chainId: 0, rpc: 'https://liteforge.rpc.caldera.xyz/infra-partner-http',
53
+ protocol: '', imageRegistry: '', esr: '0xbAa7F9E3287ff95D177104eD469E6d0Fd19dBB0F',
54
+ },
55
+ };
56
+
57
+ const IMAGE_REGISTRY_ABI = [
58
+ 'function imageDetails(string) view returns (address owner, string ipfsHash, string version, string session, uint256 fee, address rewardAddress, bool validated, bool published, string certPublicKey, string dockerComposeHash, string name)',
59
+ 'function trustedZoneImageDetails(string) view returns (address owner, string ipfsHash, string version, string session, uint256 fee, address rewardAddress, bool validated, bool published, string certPublicKey, string dockerComposeHash, string name)',
60
+ 'function imageVersions(string, uint256) view returns (string)',
61
+ 'function trustedZoneImageVersions(string, uint256) view returns (string)',
62
+ ];
63
+
64
+ const ESR_ABI = [
65
+ 'function getState(address enclave, bytes32 key) view returns (string cid, uint256 version, uint64 updatedAt)',
66
+ 'function getVersion(address enclave, bytes32 key) view returns (uint256)',
67
+ 'function exists(address enclave, bytes32 key) view returns (bool)',
68
+ 'function entryCount() view returns (uint256)',
69
+ 'function getEntriesFrom(uint256 start, uint256 limit) view returns (address[] enclaves, bytes32[] keys, string[] cids, uint256[] versions, uint64[] updatedAts, uint256 total)',
70
+ 'event StateCommitted(address indexed enclave, bytes32 indexed key, string cid, uint256 version, uint256 seq)',
71
+ ];
72
+
73
+ function resolveNetwork(name) {
74
+ name = (name || process.env.ESR_NETWORK || process.env.BLOCKCHAIN_NETWORK || 'BLOXBERG_TESTNET')
75
+ .toUpperCase();
76
+ const net = NETWORKS[name];
77
+ if (!net) {
78
+ throw new Error(`Unknown network '${name}'. Known: ${Object.keys(NETWORKS).join(', ')}`);
79
+ }
80
+ return { name, net };
81
+ }
82
+
83
+ function provider(rpc) {
84
+ return new ethers.providers.JsonRpcProvider(rpc);
85
+ }
86
+
87
+ function keyHash(key) {
88
+ return ethers.utils.id(key); // keccak256(utf8(key))
89
+ }
90
+
91
+ function looksLikeCID(cid) {
92
+ return typeof cid === 'string' && (cid.startsWith('Qm') || cid.startsWith('bafk')) && cid.length > 20;
93
+ }
94
+
95
+ function decodeDetails(d, fallbackName) {
96
+ return {
97
+ name: d.name || fallbackName,
98
+ published: Boolean(d.published),
99
+ validated: Boolean(d.validated),
100
+ image_ipfs_hash: d.ipfsHash || null,
101
+ image_version: d.version || null,
102
+ reward_address: d.rewardAddress,
103
+ owner: d.owner,
104
+ docker_compose_hash: d.dockerComposeHash || null,
105
+ attestation_session: Boolean(d.session),
106
+ cert_present: Boolean(d.certPublicKey),
107
+ };
108
+ }
109
+
110
+ async function lookupRegistration(reg, kind, ipfs, name) {
111
+ const detailsFn = kind === 'trustedzone' ? 'trustedZoneImageDetails' : 'imageDetails';
112
+ const versionsFn = kind === 'trustedzone' ? 'trustedZoneImageVersions' : 'imageVersions';
113
+ if (ipfs) {
114
+ try { return decodeDetails(await reg[detailsFn](ipfs), ipfs); }
115
+ catch (e) { return { found: false, error: e.message }; }
116
+ }
117
+ if (name) {
118
+ let latest = null;
119
+ for (let i = 0; i < 64; i += 1) {
120
+ let v;
121
+ try { v = await reg[versionsFn](name, i); } catch (e) { break; }
122
+ if (!v) break;
123
+ latest = v;
124
+ }
125
+ if (latest) {
126
+ try { return decodeDetails(await reg[detailsFn](latest), name); }
127
+ catch (e) { return { found: false, error: e.message }; }
128
+ }
129
+ return { found: false, note: `no registered version for name '${name}' -- the Image Registry is keyed by image IPFS hash; pass --ipfs <hash>` };
130
+ }
131
+ return { found: false, note: 'no --ipfs or --name to look up' };
132
+ }
133
+
134
+ async function sectionNetwork(prov, name, net) {
135
+ const out = {
136
+ network: name, type: net.type, chain_id: net.chainId, rpc: net.rpc,
137
+ protocol_contract: net.protocol || '(n/a)',
138
+ image_registry: net.imageRegistry || '(n/a)',
139
+ esr_contract: net.esr || '(not deployed)',
140
+ };
141
+ try {
142
+ out.latest_block = await prov.getBlockNumber();
143
+ out.connected = true;
144
+ } catch (e) {
145
+ out.connected = false; out.error = e.message;
146
+ }
147
+ return out;
148
+ }
149
+
150
+ async function sectionRegistration(prov, net, kind, ipfs, name) {
151
+ if (!net.imageRegistry) return { note: 'no image registry on this network' };
152
+ const reg = new ethers.Contract(net.imageRegistry, IMAGE_REGISTRY_ABI, prov);
153
+ return lookupRegistration(reg, kind, ipfs, name);
154
+ }
155
+
156
+ async function sectionEsr(prov, net, enclaveWallet, eventsN) {
157
+ if (!net.esr) return { note: 'ESR is not deployed on this network' };
158
+ const esr = new ethers.Contract(net.esr, ESR_ABI, prov);
159
+ const out = { esr_contract: net.esr };
160
+ try { out.total_registry_entries = (await esr.entryCount()).toNumber(); } catch (e) { /* ignore */ }
161
+ if (enclaveWallet) {
162
+ try {
163
+ const latest = await prov.getBlockNumber();
164
+ const from = Math.max(0, latest - 500000);
165
+ const flt = esr.filters.StateCommitted(ethers.utils.getAddress(enclaveWallet));
166
+ let logs = await esr.queryFilter(flt, from, 'latest');
167
+ logs = logs.slice(-eventsN);
168
+ out.recent_commits = logs.map((l) => ({
169
+ key_hash: l.args.key,
170
+ version: l.args.version.toNumber(),
171
+ cid: l.args.cid,
172
+ seq: l.args.seq.toNumber(),
173
+ block: l.blockNumber,
174
+ }));
175
+ } catch (e) {
176
+ out.recent_commits_error = e.message;
177
+ }
178
+ } else {
179
+ out.recent_commits_note = 'pass --enclave <ESR wallet> to list this enclave\'s state commits';
180
+ }
181
+ return out;
182
+ }
183
+
184
+ async function esrQuery(prov, name, net, args) {
185
+ const esrAddr = args.contract || net.esr;
186
+ const sub = args.esrCmd || 'address';
187
+ if (sub === 'address') return { network: name, esr_contract: esrAddr || '(not deployed)', rpc: net.rpc };
188
+ if (!esrAddr) throw new Error(`ESR is not deployed on ${name}.`);
189
+ const c = new ethers.Contract(esrAddr, ESR_ABI, prov);
190
+ if (sub === 'count') return { network: name, entry_count: (await c.entryCount()).toNumber() };
191
+ if (sub === 'version') {
192
+ const v = (await c.getVersion(ethers.utils.getAddress(args.enclave), keyHash(args.key))).toNumber();
193
+ return { enclave: args.enclave, key: args.key, version: v };
194
+ }
195
+ if (sub === 'state') {
196
+ const enclave = ethers.utils.getAddress(args.enclave);
197
+ const kh = keyHash(args.key);
198
+ const [cid, version, updated] = await c.getState(enclave, kh);
199
+ const exists = await c.exists(enclave, kh);
200
+ return {
201
+ network: name, enclave: args.enclave, key: args.key, key_hash: kh,
202
+ exists, version: version.toNumber(), cid: cid || null, cid_valid: looksLikeCID(cid),
203
+ updated_at: updated.toNumber(),
204
+ note: 'cid points at ENCRYPTED state; only the enclave can decrypt it',
205
+ };
206
+ }
207
+ if (sub === 'list') {
208
+ const start = parseInt(args.start || 0, 10);
209
+ const limit = parseInt(args.limit || 50, 10);
210
+ const [encs, keys, cids, versions, updated, total] = await c.getEntriesFrom(start, limit);
211
+ const entries = encs.map((e, i) => ({
212
+ enclave: e, key_hash: keys[i], cid: cids[i] || null,
213
+ version: versions[i].toNumber(), updated_at: updated[i].toNumber(),
214
+ }));
215
+ return { network: name, total: total.toNumber(), start, returned: entries.length, entries };
216
+ }
217
+ throw new Error(`unknown esr subcommand: ${sub}`);
218
+ }
219
+
220
+ // ---- arg parsing (tiny, dependency-free) -----------------------------------
221
+
222
+ function parseArgs(argv) {
223
+ const a = { _: [], json: false };
224
+ for (let i = 0; i < argv.length; i += 1) {
225
+ const t = argv[i];
226
+ if (t === '--json') a.json = true;
227
+ else if (t.startsWith('--')) { a[t.slice(2)] = argv[i + 1]; i += 1; }
228
+ else a._.push(t);
229
+ }
230
+ return a;
231
+ }
232
+
233
+ function emit(obj, asJson) {
234
+ if (asJson) { console.log(JSON.stringify(obj, null, 2)); return; }
235
+ for (const [k, v] of Object.entries(obj)) {
236
+ if (Array.isArray(v)) { console.log(`${k}:`); v.forEach((it) => console.log(' ' + JSON.stringify(it))); }
237
+ else console.log(`${k}: ${v}`);
238
+ }
239
+ }
240
+
241
+ function printFull(info) {
242
+ const n = info.network;
243
+ console.log('NETWORK');
244
+ console.log(` ${n.network} (${n.type}, chain ${n.chain_id}) block ${n.latest_block || '?'} ${n.connected ? 'connected' : 'NOT CONNECTED'}`);
245
+ console.log(` rpc: ${n.rpc}`);
246
+ console.log(` protocol: ${n.protocol_contract}`);
247
+ console.log(` image registry: ${n.image_registry}`);
248
+ console.log(` esr: ${n.esr_contract}`);
249
+ for (const [label, key] of [['TRUSTEDZONE', 'trustedzone'], ['SECURELOCK', 'securelock']]) {
250
+ const r = info[key] || {};
251
+ console.log(label);
252
+ if (r.published || r.validated || r.image_ipfs_hash) {
253
+ console.log(` published: ${r.published ? 'yes' : 'no'} validated: ${r.validated ? 'yes' : 'no'}`);
254
+ console.log(` image hash: ${r.image_ipfs_hash}`);
255
+ console.log(` version: ${r.image_version}`);
256
+ console.log(` reward addr: ${r.reward_address}`);
257
+ console.log(` attestation: ${r.attestation_session ? 'MRENCLAVE session present' : 'none'}`);
258
+ } else {
259
+ console.log(` not found (${r.error || r.note || 'not registered'})`);
260
+ }
261
+ }
262
+ const e = info.esr || {};
263
+ console.log('ESR');
264
+ if (e.note) { console.log(` ${e.note}`); return; }
265
+ console.log(` registry: ${e.esr_contract}`);
266
+ if ('total_registry_entries' in e) console.log(` total keys: ${e.total_registry_entries} (all enclaves)`);
267
+ if (e.recent_commits) {
268
+ console.log(` recent state commits for this enclave: ${e.recent_commits.length}`);
269
+ e.recent_commits.forEach((c) => console.log(` v${c.version} seq ${c.seq} ${c.key_hash} ${c.cid} (block ${c.block})`));
270
+ } else if (e.recent_commits_note) console.log(` ${e.recent_commits_note}`);
271
+ else if (e.recent_commits_error) console.log(` (could not read commits: ${e.recent_commits_error})`);
272
+ }
273
+
274
+ async function main() {
275
+ const args = parseArgs(process.argv.slice(2));
276
+ const section = args._[0]; // network | trustedzone | securelock | esr | undefined
277
+ const { name: netName, net } = resolveNetwork(args.network);
278
+ const prov = provider(net.rpc);
279
+
280
+ const ipfs = args.ipfs || process.env.IPFS_HASH || null;
281
+ const projName = args.name || process.env.PROJECT_NAME || null;
282
+
283
+ if (section === 'esr') {
284
+ args.esrCmd = args._[1];
285
+ const obj = await esrQuery(prov, netName, net, args);
286
+ emit(obj, args.json);
287
+ return;
288
+ }
289
+ if (section === 'network') { emit(await sectionNetwork(prov, netName, net), args.json); return; }
290
+ if (section === 'trustedzone') { emit(await sectionRegistration(prov, net, 'trustedzone', ipfs, projName), args.json); return; }
291
+ if (section === 'securelock') { emit(await sectionRegistration(prov, net, 'securelock', ipfs, projName), args.json); return; }
292
+
293
+ const info = {
294
+ project: { name: projName || '(unknown)', dapp_type: process.env.SERVICE_TYPE || process.env.DAPP_TYPE, version: process.env.VERSION },
295
+ network: await sectionNetwork(prov, netName, net),
296
+ trustedzone: await sectionRegistration(prov, net, 'trustedzone', ipfs, projName),
297
+ securelock: await sectionRegistration(prov, net, 'securelock', ipfs, projName),
298
+ esr: await sectionEsr(prov, net, args.enclave || null, parseInt(args.events || 10, 10)),
299
+ };
300
+ if (args.json) { console.log(JSON.stringify(info, null, 2)); return; }
301
+ const pr = info.project;
302
+ console.log(`PROJECT ${pr.name}${pr.dapp_type ? ' type ' + pr.dapp_type : ''}${pr.version != null ? ' version ' + pr.version : ''}`);
303
+ printFull(info);
304
+ }
305
+
306
+ main().catch((e) => { console.error(`ecld-info: ${e.message}`); process.exit(1); });
@@ -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.
@@ -329,6 +339,8 @@ class StateRegistry {
329
339
  }
330
340
 
331
341
  _contract(signerOrProvider) {
342
+ // Local emulator: use the in-memory contract object instead of a real one.
343
+ if (_contractOverride) return _contractOverride;
332
344
  return new ethers.Contract(_contractAddress, ESR_ABI, signerOrProvider || _provider);
333
345
  }
334
346
 
@@ -515,6 +527,14 @@ class StateRegistry {
515
527
  await _swift.putFileContent(
516
528
  _bucket, `esr.commit.${Number(relayNonce)}.json`, '',
517
529
  Buffer.from(JSON.stringify(auth), 'utf8'));
530
+
531
+ // LOCAL TESTING: with no node to relay commitFor, apply the commit to the
532
+ // in-memory registry now so getState/getVersion reflect it. No-op in the
533
+ // real enclave (hook is null) -- there the node applies it on-chain.
534
+ if (_localCommitApply) {
535
+ await _localCommitApply(enclave, keyHashHex, cid, Number(expectedVersion));
536
+ }
537
+
518
538
  return { relayed: true, relayNonce: Number(relayNonce) };
519
539
  }
520
540
  }
@@ -600,6 +620,8 @@ async function esrAcl(key) {
600
620
  return { ...acl };
601
621
  }
602
622
 
623
+ function setTaskCaller(caller) { _taskCaller = normAddr(caller); }
624
+
603
625
  module.exports = {
604
626
  StateRegistry,
605
627
  configure,
@@ -607,6 +629,7 @@ module.exports = {
607
629
  looksLikeCID,
608
630
  ledgerSnapshot,
609
631
  taskCaller,
632
+ setTaskCaller,
610
633
  restampLedgerCaller,
611
634
  StatePermissionError,
612
635
  esrGrant,
@@ -614,5 +637,9 @@ module.exports = {
614
637
  esrSetPublicRead,
615
638
  esrTransfer,
616
639
  esrOwner,
617
- esrAcl
640
+ esrAcl,
641
+ // local-testing hooks (esr_local only)
642
+ setContractOverride,
643
+ setLocalCommitApply,
644
+ normAddr
618
645
  };
@@ -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 };
@@ -121,7 +121,7 @@ function emptyRequiredConfig() {
121
121
  );
122
122
  }
123
123
 
124
- function executeTask(payload, input) {
124
+ async function executeTask(payload, input) {
125
125
  const missing = emptyRequiredConfig();
126
126
  if (missing.length) {
127
127
  return [
@@ -161,7 +161,7 @@ function executeTask(payload, input) {
161
161
  return exec(payload, input, scope);
162
162
  }
163
163
 
164
- function exec(payload, input, globals = null) {
164
+ async function exec(payload, input, globals = null) {
165
165
  try {
166
166
  if (payload && payload !== "") {
167
167
  const scope = globals || {};
@@ -172,7 +172,12 @@ function exec(payload, input, globals = null) {
172
172
  // the payload by bare name — matching how payloads are written
173
173
  // (e.g. `processData(___etny_data_set___)`).
174
174
  with (scope) {
175
- const value = eval(payload);
175
+ // ESR reads/commits are async, so a state-using payload
176
+ // (`esrIncrement()`, `await reg.commit(...)`) evaluates to a
177
+ // Promise. Await it so the finished value -- not the pending
178
+ // Promise -- is what gets wrapped. Non-async payloads await to
179
+ // themselves, so this is transparent for plain results.
180
+ const value = await eval(payload);
176
181
  // A payload that called ecldResult/___etny_result___ itself
177
182
  // already holds the finished [0, envelopeJson] tuple -- pass
178
183
  // it through instead of wrapping the result a second time.
package/nodenithy/run.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,21 +1,24 @@
1
1
  {
2
2
  "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.5.8",
3
+ "version": "1.5.11",
4
4
  "description": "A package to run ethernity cloud build scripts",
5
5
  "scripts": {
6
6
  "ecld-build": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/build.js",
7
7
  "ecld-publish": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/publish.js",
8
8
  "ecld-init": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/init.js",
9
- "ecld-run": "cross-env ecld-run",
9
+ "ecld-run": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/run.js",
10
10
  "postinstall": "node postinstall.js",
11
- "ecld-test": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/test.js"
11
+ "ecld-test": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/test.js",
12
+ "ecld-info": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/info.js"
12
13
  },
13
14
  "bin": {
14
15
  "ecld-build": "./build.js",
15
16
  "ecld-publish": "./publish.js",
16
17
  "ecld-init": "./init.js",
17
18
  "postinstall": "./postinstall.js",
18
- "ecld-test": "./test.js"
19
+ "ecld-test": "./test.js",
20
+ "ecld-info": "./info.js",
21
+ "ecld-run": "./run.js"
19
22
  },
20
23
  "files": [
21
24
  "nodenithy/*",
@@ -23,28 +26,31 @@
23
26
  "build.js",
24
27
  "init.js",
25
28
  "publish.js",
26
- "postinstall.js"
29
+ "postinstall.js",
30
+ "test.js",
31
+ "info.js",
32
+ "run.js"
27
33
  ],
28
34
  "dependencies": {
29
- "@ethernity-cloud/runner": "^0.0.26",
35
+ "@ethernity-cloud/runner": "^0.4.4",
36
+ "acorn": "^8.11.0",
37
+ "adm-zip": "^0.5.14",
30
38
  "axios": "^1.7.2",
39
+ "cli-progress": "^3.12.0",
31
40
  "commander": "^12.1.0",
41
+ "cross-env": "^7.0.3",
32
42
  "dotenv": "^16.4.5",
33
43
  "eth-lib": "^0.1.29",
34
44
  "ethers": "^5.5.4",
35
45
  "kubo-rpc-client": "^4.1.1",
36
- "web3": "^4.9.0",
37
- "cross-env": "^7.0.3",
38
- "shelljs": "^0.8.5",
39
- "shx": "^0.3.4",
40
46
  "node-forge": "^1.3.1",
41
- "cli-progress": "^3.12.0",
42
47
  "react": "^18.3.1",
43
48
  "react-dom": "^18.3.1",
44
49
  "react-scripts": "^5.0.1",
50
+ "shelljs": "^0.8.5",
51
+ "shx": "^0.3.4",
45
52
  "web-vitals": "^2.1.4",
46
- "adm-zip": "^0.5.14",
47
- "acorn": "^8.11.0"
53
+ "web3": "^4.9.0"
48
54
  },
49
55
  "author": "Alexandru Luca",
50
56
  "license": "AGPL-3.0"
package/pynithy/run.js CHANGED
File without changes
package/run.js ADDED
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ /* ecld-run — submit a payload to the Ethernity Cloud network and print the result.
3
+ *
4
+ * The network-side sibling of `ecld-test`: same payload flags (a code string,
5
+ * --file, --input/--input-text), but instead of executing locally it drives the
6
+ * real runner (@ethernity-cloud/runner's EthernityCloudRunner) end to end —
7
+ * encrypt the payload, push to IPFS, place the on-chain DO request, wait for a
8
+ * node to process it, download and decrypt the result.
9
+ *
10
+ * Where ecld-test answers "does my CALL work?", ecld-run answers "does it work
11
+ * on the network, on the enclave I published?" — it costs gas and needs a
12
+ * funded key.
13
+ *
14
+ * Network and enclave come from the project's .env (written by ecld-init /
15
+ * ecld-publish), matching how ethernity_task drives a run:
16
+ *
17
+ * BLOCKCHAIN_NETWORK e.g. "Bloxberg_Testnet" -> the network token address
18
+ * PROJECT_NAME the securelock enclave to run
19
+ * TRUSTED_ZONE_IMAGE the trustedzone enclave
20
+ * PRIVATE_KEY a funded 0x key — it pays for the order
21
+ *
22
+ * Usage, from the project root:
23
+ *
24
+ * npx ecld-run 'hello("World")'
25
+ * npx ecld-run --file payload.js
26
+ * npx ecld-run --input data.json 'processData(___etny_data_set___)'
27
+ * npx ecld-run --network BLOXBERG_TESTNET --task-price 3 'esrIncrement()'
28
+ * npx ecld-run --json 'hello("World")' # machine-readable result
29
+ *
30
+ * The runner is ESM; this CJS entry loads it with dynamic import().
31
+ *
32
+ * Exit code 0 on a SUCCESS task result, 1 otherwise.
33
+ */
34
+
35
+ 'use strict';
36
+
37
+ const fs = require('fs');
38
+ try { require('dotenv').config(); } catch (e) { /* dotenv optional */ }
39
+
40
+ const DEFAULT_IPFS = 'https://ipfs.ethernity.cloud/api/v0';
41
+
42
+ /* Map a BLOCKCHAIN_NETWORK token (as ecld-init writes it, spaces -> underscores)
43
+ * to the runner's network token ADDRESS. Built from the runner's own ECAddress
44
+ * so it can't drift. Accepts a few spellings per network. */
45
+ function networkAddress(ECAddress, raw) {
46
+ const key = String(raw || '').trim().toUpperCase().replace(/\s+/g, '_');
47
+ const table = {
48
+ BLOXBERG_TESTNET: ECAddress.BLOXBERG.TESTNET_ADDRESS,
49
+ BLOXBERG_MAINNET: ECAddress.BLOXBERG.MAINNET_ADDRESS,
50
+ POLYGON_MAINNET: ECAddress.POLYGON.MAINNET_ADDRESS,
51
+ POLYGON_AMOY: ECAddress.POLYGON.TESTNET_ADDRESS,
52
+ POLYGON_AMOY_TESTNET: ECAddress.POLYGON.TESTNET_ADDRESS,
53
+ IOTEX_TESTNET: ECAddress.IOTEX && ECAddress.IOTEX.TESTNET_ADDRESS,
54
+ ETHEREUM_SEPOLIA: ECAddress.SEPOLIA && ECAddress.SEPOLIA.TESTNET_ADDRESS,
55
+ SEPOLIA: ECAddress.SEPOLIA && ECAddress.SEPOLIA.TESTNET_ADDRESS,
56
+ LITVM_LITEFORGE: ECAddress.LITVM && ECAddress.LITVM.TESTNET_ADDRESS,
57
+ LITVM: ECAddress.LITVM && ECAddress.LITVM.TESTNET_ADDRESS,
58
+ };
59
+ return { key, address: table[key] || null, known: Object.keys(table) };
60
+ }
61
+
62
+ function parseArgs(argv) {
63
+ const opts = { ipfs: DEFAULT_IPFS, timeout: 600, taskPrice: 3, cpu: 1, memory: 1, storage: 10, bandwidth: 1, duration: 1, validators: 1, node: '' };
64
+ const positional = [];
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const a = argv[i];
67
+ if (a === '--file' || a === '-f') opts.file = argv[++i];
68
+ else if (a === '--input' || a === '-i') opts.inputFile = argv[++i];
69
+ else if (a === '--input-text') opts.inputText = argv[++i];
70
+ else if (a === '--network') opts.network = argv[++i];
71
+ else if (a === '--securelock') opts.securelock = argv[++i];
72
+ else if (a === '--trustedzone') opts.trustedzone = argv[++i];
73
+ else if (a === '--node') opts.node = argv[++i];
74
+ else if (a === '--ipfs') opts.ipfs = argv[++i];
75
+ else if (a === '--timeout') opts.timeout = parseInt(argv[++i], 10);
76
+ else if (a === '--task-price') opts.taskPrice = parseInt(argv[++i], 10);
77
+ else if (a === '--cpu') opts.cpu = parseInt(argv[++i], 10);
78
+ else if (a === '--memory') opts.memory = parseInt(argv[++i], 10);
79
+ else if (a === '--storage') opts.storage = parseInt(argv[++i], 10);
80
+ else if (a === '--bandwidth') opts.bandwidth = parseInt(argv[++i], 10);
81
+ else if (a === '--duration') opts.duration = parseInt(argv[++i], 10);
82
+ else if (a === '--validators') opts.validators = parseInt(argv[++i], 10);
83
+ else if (a === '--json') opts.json = true;
84
+ else if (a === '--help' || a === '-h') opts.help = true;
85
+ else positional.push(a);
86
+ }
87
+ return { opts, positional };
88
+ }
89
+
90
+ const HELP = `ecld-run — submit a payload to the Ethernity Cloud network (costs gas; needs a funded key).
91
+ Local sibling: ecld-test.
92
+
93
+ usage: ecld-run 'hello("World")' | ecld-run --file payload.js | ecld-run --input data.json 'fn(___etny_data_set___)'
94
+
95
+ payload: a code string, or --file <path>; input via --input <file> / --input-text <str>
96
+ network: --network BLOXBERG_TESTNET (default: BLOCKCHAIN_NETWORK from .env)
97
+ enclaves: --securelock <name> (default PROJECT_NAME), --trustedzone <name> (default TRUSTED_ZONE_IMAGE)
98
+ key: PRIVATE_KEY (a funded 0x key) from .env / env, or ECLD_PRIVATE_KEY
99
+ resources: --task-price 3 --cpu 1 --memory 1 --storage 10 --bandwidth 1 --duration 1 --validators 1
100
+ other: --node <addr> --ipfs <url> --timeout <sec> --json`;
101
+
102
+ async function main() {
103
+ const { opts, positional } = parseArgs(process.argv.slice(2));
104
+ if (opts.help) { console.log(HELP); process.exit(0); }
105
+
106
+ // ---- payload ----
107
+ let code;
108
+ if (opts.file) code = fs.readFileSync(opts.file, 'utf8');
109
+ else if (positional.length) code = positional.join(' ');
110
+ else if ((process.env.ECLD_TEST_CODE || '').trim()) code = process.env.ECLD_TEST_CODE.trim();
111
+ else { console.error("ecld-run: provide a payload string, --file, or ECLD_TEST_CODE\n\n" + HELP); process.exit(2); }
112
+
113
+ if (opts.inputFile !== undefined && opts.inputText !== undefined) {
114
+ console.error('ecld-run: --input and --input-text are mutually exclusive'); process.exit(2);
115
+ }
116
+ let input = null;
117
+ if (opts.inputFile !== undefined) input = fs.readFileSync(opts.inputFile, 'utf8');
118
+ else if (opts.inputText !== undefined) input = opts.inputText;
119
+ // The enclave executor exposes the input as ___etny_data_set___; the runner
120
+ // passes it as part of the code's scope, so a payload references it by name.
121
+ // ecld-run mirrors ecld-test: input is delivered via the payload call itself.
122
+
123
+ // ---- runner (ESM) ----
124
+ let EthernityCloudRunner, ECStatus, ECEvent, ECAddress, ECOrderTaskStatus;
125
+ try {
126
+ const runnerMod = await import('@ethernity-cloud/runner');
127
+ EthernityCloudRunner = runnerMod.default || runnerMod.EthernityCloudRunner;
128
+ const enums = await import('@ethernity-cloud/runner/enums.js');
129
+ ({ ECStatus, ECEvent, ECAddress, ECOrderTaskStatus } = enums);
130
+ } catch (e) {
131
+ console.error('ecld-run: the runner package is not installed or is too old '
132
+ + '(need @ethernity-cloud/runner >= 0.4.4): ' + e.message);
133
+ process.exit(1);
134
+ }
135
+
136
+ // ---- network / enclaves / key ----
137
+ const rawNetwork = opts.network || process.env.BLOCKCHAIN_NETWORK || 'BLOXBERG_TESTNET';
138
+ const { key: netKey, address, known } = networkAddress(ECAddress, rawNetwork);
139
+ if (!address) {
140
+ console.error(`ecld-run: unknown network '${rawNetwork}'. Known: ${known.join(', ')}`);
141
+ process.exit(1);
142
+ }
143
+ const securelock = opts.securelock || process.env.PROJECT_NAME;
144
+ const trustedzone = opts.trustedzone || process.env.TRUSTED_ZONE_IMAGE; // may be undefined -> run() default
145
+ if (!securelock) {
146
+ console.error('ecld-run: no securelock enclave: set PROJECT_NAME in .env '
147
+ + '(run ecld-init/ecld-publish) or pass --securelock.');
148
+ process.exit(1);
149
+ }
150
+
151
+ let privateKey = (process.env.PRIVATE_KEY || process.env.ECLD_PRIVATE_KEY || '').trim();
152
+ if (!privateKey) {
153
+ console.error('ecld-run: no signing key. Set PRIVATE_KEY (a funded 0x key) in .env '
154
+ + '(ecld-publish stores it) or ECLD_PRIVATE_KEY.');
155
+ process.exit(1);
156
+ }
157
+ if (!privateKey.startsWith('0x')) privateKey = '0x' + privateKey;
158
+
159
+ console.log(`network : ${netKey} (${address})`);
160
+ console.log(`securelock : ${securelock}`);
161
+ console.log(`trustedzone: ${trustedzone || '(runner default)'}`);
162
+ console.log(`payload : ${JSON.stringify(code)}`);
163
+ console.log(`input : ${input === null ? '<empty>' : JSON.stringify(input.slice(0, 80))}`);
164
+
165
+ // ---- drive the runner ----
166
+ let runner;
167
+ try {
168
+ runner = new EthernityCloudRunner(address, { privateKey });
169
+ } catch (e) {
170
+ console.error('ecld-run: could not initialise the runner: ' + e.message);
171
+ process.exit(1);
172
+ }
173
+ runner.initializeStorage(opts.ipfs);
174
+
175
+ let lastPhase = null;
176
+ const onProgress = (e) => {
177
+ const d = (e && e.detail) || {};
178
+ if (d.progress !== lastPhase) { lastPhase = d.progress; console.log(`[${d.progress}] ${d.message}`); }
179
+ else if (d.message) console.log(` ${d.message}`);
180
+ };
181
+ runner.addEventListener(ECStatus.DEFAULT, onProgress);
182
+
183
+ const resources = {
184
+ taskPrice: opts.taskPrice, cpu: opts.cpu, memory: opts.memory,
185
+ storage: opts.storage, bandwidth: opts.bandwidth, duration: opts.duration, validators: opts.validators,
186
+ };
187
+ console.log(`resources : ${JSON.stringify(resources)}`);
188
+ console.log('submitting order... (this places an on-chain request and costs gas)');
189
+
190
+ const done = new Promise((resolve) => {
191
+ runner.addEventListener(ECStatus.SUCCESS, () => resolve('SUCCESS'));
192
+ runner.addEventListener(ECStatus.ERROR, (e) => {
193
+ const msg = (e && e.detail && e.detail.message) || 'unknown error';
194
+ resolve({ error: msg });
195
+ });
196
+ });
197
+
198
+ // run() resolves when the task terminates; the events resolve `done`. Race a
199
+ // timeout so a stuck order doesn't hang the CLI forever.
200
+ const timeoutMs = (opts.timeout || 600) * 1000;
201
+ let timer;
202
+ const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve('TIMEOUT'), timeoutMs); });
203
+
204
+ try {
205
+ const runArgs = [resources, securelock, code, opts.node];
206
+ if (trustedzone) runArgs.push(trustedzone);
207
+ // Fire the run; its own promise also settles on completion.
208
+ const runPromise = runner.run(...runArgs).catch((e) => ({ error: e.message }));
209
+ const outcome = await Promise.race([done, timeout, runPromise]);
210
+ clearTimeout(timer);
211
+
212
+ if (outcome === 'TIMEOUT') {
213
+ console.error(`\necld-run: no result after ${opts.timeout}s; the order may still process on-chain.`);
214
+ process.exit(1);
215
+ }
216
+ if (outcome && outcome.error) {
217
+ console.error(`\nstatus : ERROR`);
218
+ console.error(`error : ${outcome.error}`);
219
+ process.exit(1);
220
+ }
221
+
222
+ // SUCCESS
223
+ const result = await runner.getResult();
224
+ const typed = (runner.getStructuredResult && runner.getStructuredResult()) || null;
225
+ const taskCode = runner.resultTaskCode;
226
+ const taskName = runner.resultTaskCodeName
227
+ || (ECOrderTaskStatus && taskCode !== undefined ? ECOrderTaskStatus[taskCode] : undefined);
228
+
229
+ if (opts.json) {
230
+ console.log(JSON.stringify({ task_code: taskCode, task_code_name: taskName, value: result, structured: typed }, null, 2));
231
+ } else {
232
+ if (taskName && taskName !== 'SUCCESS') console.log(`task code : ${taskCode} (${taskName})`);
233
+ console.log('result :');
234
+ console.log(result);
235
+ }
236
+ // A non-zero enclave task code is a failed run even though the order landed.
237
+ process.exit(taskCode === undefined || taskCode === 0 ? 0 : 1);
238
+ } catch (e) {
239
+ clearTimeout(timer);
240
+ console.error('ecld-run: run failed: ' + (e && e.stack ? e.stack : e));
241
+ process.exit(1);
242
+ }
243
+ }
244
+
245
+ main().catch((e) => { console.error(e && e.stack ? e.stack : String(e)); process.exit(1); });
package/test.js CHANGED
@@ -24,6 +24,14 @@
24
24
  * @ethernity-cloud/runner >= 0.3.6 executes every run() against this API
25
25
  * instead of the blockchain.
26
26
  *
27
+ * ESR (Enclave State Registry) is emulated locally and ON by default: a
28
+ * state-using backend -- `require('../ecld_state').StateRegistry`, get/commit,
29
+ * the ownership/ACL API -- runs fully in-process against an in-memory registry
30
+ * + store, with no chain, node, or SGX. State persists between runs in
31
+ * .ecld-esr-local.*.json so a counter advances exactly as it would on-chain.
32
+ * The task caller (the DO owner the trustedzone attests) defaults to your
33
+ * developer address and is set for every task; override with --caller.
34
+ *
27
35
  * Exit code 0 on TaskStatus SUCCESS, 1 otherwise.
28
36
  */
29
37
 
@@ -32,6 +40,7 @@ const os = require('os');
32
40
  const path = require('path');
33
41
  const http = require('http');
34
42
  const crypto = require('crypto');
43
+ const { ethers } = require('ethers');
35
44
 
36
45
  const TASK_STATUS_NAMES = {
37
46
  0: 'SUCCESS',
@@ -62,10 +71,21 @@ function copyDir(src, dest) {
62
71
  /* Stage the executor + the project's backend the way ecld-build does inside
63
72
  * the enclave image, then load the REAL executor from the staged copy. */
64
73
  function loadExecutor(projectSrc) {
65
- const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'ecld-test-'));
66
- for (const f of ['etny_exec.js', 'task_status.js']) {
67
- fs.copyFileSync(path.join(VENDORED_SRC, f), path.join(stage, f));
74
+ // Stage INSIDE the SDK tree (not the OS temp dir) so the executor's
75
+ // `require('ethers')` and other runtime deps resolve by walking up to the
76
+ // SDK's own node_modules -- exactly what binary-fs gives the enclave.
77
+ const stageRoot = path.join(VENDORED_SRC, '..', '.ecld-test-stage');
78
+ fs.mkdirSync(stageRoot, { recursive: true });
79
+ const stage = fs.mkdtempSync(path.join(stageRoot, 'run-'));
80
+ const cleanup = () => { try { fs.rmSync(stage, { recursive: true, force: true }); } catch (e) {} };
81
+ process.on('exit', cleanup);
82
+ // `serve` runs until interrupted -- clean the stage on Ctrl-C / kill too.
83
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
84
+ process.on(sig, () => { cleanup(); process.exit(130); });
68
85
  }
86
+ // Stage the ENTIRE vendored securelock src so `require('./ecld_state')`,
87
+ // esr_local, and their deps all resolve, exactly as inside the enclave image.
88
+ copyDir(VENDORED_SRC, stage);
69
89
  const backendDir = path.join(projectSrc, 'serverless');
70
90
  const backendJs = path.join(backendDir, 'backend.js');
71
91
  if (fs.existsSync(backendJs)) {
@@ -78,11 +98,56 @@ function loadExecutor(projectSrc) {
78
98
  } else {
79
99
  console.log('backend : none found — running with bare globals, like a stock enclave');
80
100
  }
81
- return require(path.join(stage, 'etny_exec.js'));
101
+ const executor = require(path.join(stage, 'etny_exec.js'));
102
+ executor.__stageDir = stage;
103
+ return executor;
104
+ }
105
+
106
+ /* The task caller for local runs -- the DEVELOPER'S address, mirroring the
107
+ * trustedzone-attested DO owner the real securelock receives. Resolution:
108
+ * 1. --caller / ECLD_TEST_CALLER
109
+ * (a) WALLET_ADDRESS in .env (written at publish time)
110
+ * (b) derive from ECLD_PRIVATE_KEY
111
+ * else DEFAULT_TEST_CALLER (a well-known dev address). */
112
+ const DEFAULT_TEST_CALLER = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
113
+
114
+ function resolveCaller(explicit) {
115
+ const chosen = explicit || process.env.ECLD_TEST_CALLER;
116
+ if (chosen) return chosen.trim();
117
+ if (process.env.WALLET_ADDRESS) return process.env.WALLET_ADDRESS.trim(); // (a)
118
+ const pk = process.env.ECLD_PRIVATE_KEY; // (b)
119
+ if (pk) {
120
+ try { return new ethers.Wallet(pk.startsWith('0x') ? pk : '0x' + pk).address; }
121
+ catch (e) { /* fall through */ }
122
+ }
123
+ console.log(`[ecld-test] no developer address configured; using default test caller `
124
+ + `${DEFAULT_TEST_CALLER} (set ECLD_TEST_CALLER or WALLET_ADDRESS)`);
125
+ return DEFAULT_TEST_CALLER;
126
+ }
127
+
128
+ /* Install the local ESR emulator + set the task caller (ON by default). */
129
+ function installEsr(executor, caller) {
130
+ try {
131
+ const stage = executor.__stageDir;
132
+ const ecldState = require(path.join(stage, 'ecld_state.js'));
133
+ const esrLocal = require(path.join(stage, 'esr_local.js'));
134
+ esrLocal.install(ecldState, { caller, filePrefix: '.ecld-esr-local' });
135
+ return { ecldState };
136
+ } catch (e) {
137
+ console.log(`esr : emulation unavailable (${e.message})`);
138
+ return null;
139
+ }
82
140
  }
83
141
 
84
- function runTask(executor, payload, input) {
85
- const [code, resultRaw] = executor.executeTask(payload, input || '');
142
+ async function runTask(executor, payload, input, esr, caller) {
143
+ // Set the task caller EXACTLY as the real securelock does after the
144
+ // trustedzone hands it the DO owner -- for every task, ESR or not.
145
+ if (esr && esr.ecldState) {
146
+ try { esr.ecldState.setTaskCaller(caller || null); } catch (e) { /* ignore */ }
147
+ }
148
+ // executeTask is async (ESR reads/commits await); the real securelock awaits
149
+ // it too (securelock.js: `await executeTask(...)`).
150
+ const [code, resultRaw] = await executor.executeTask(payload, input || '');
86
151
  const result = typeof resultRaw === 'string' ? resultRaw : JSON.stringify(resultRaw);
87
152
  const checksum = crypto.createHash('sha256').update(result, 'utf8').digest('hex');
88
153
  const challenge = Array.from({ length: 20 }, () =>
@@ -98,7 +163,7 @@ function runTask(executor, payload, input) {
98
163
  };
99
164
  }
100
165
 
101
- function serve(executor, host, port) {
166
+ function serve(executor, host, port, esr, defaultCaller) {
102
167
  const server = http.createServer((req, res) => {
103
168
  const send = (status, obj) => {
104
169
  const body = JSON.stringify(obj);
@@ -128,9 +193,13 @@ function serve(executor, host, port) {
128
193
  if (input !== undefined && input !== null && typeof input !== 'string') {
129
194
  input = JSON.stringify(input);
130
195
  }
131
- const out = runTask(executor, body.payload, input);
132
- console.log(`[local-api] task -> ${out.task_code} (${out.task_code_name})`);
133
- return send(200, out);
196
+ const caller = body.caller !== undefined ? body.caller : defaultCaller;
197
+ runTask(executor, body.payload, input, esr, caller)
198
+ .then((out) => {
199
+ console.log(`[local-api] task -> ${out.task_code} (${out.task_code_name})`);
200
+ send(200, out);
201
+ })
202
+ .catch((e) => send(500, { error: e.message }));
134
203
  } catch (e) {
135
204
  if (e instanceof SyntaxError) return send(400, { error: 'invalid JSON body' });
136
205
  return send(500, { error: e.message });
@@ -145,7 +214,7 @@ function serve(executor, host, port) {
145
214
  });
146
215
  }
147
216
 
148
- function main() {
217
+ async function main() {
149
218
  const argv = process.argv.slice(2);
150
219
  const opts = { src: 'src', host: '127.0.0.1', port: 8745 };
151
220
  const positional = [];
@@ -157,6 +226,7 @@ function main() {
157
226
  else if (a === '--src') opts.src = argv[++i];
158
227
  else if (a === '--host') opts.host = argv[++i];
159
228
  else if (a === '--port') opts.port = parseInt(argv[++i], 10);
229
+ else if (a === '--caller') opts.caller = argv[++i];
160
230
  else positional.push(a);
161
231
  }
162
232
 
@@ -169,8 +239,16 @@ function main() {
169
239
  serve.backendFunctions = [];
170
240
  }
171
241
 
242
+ // ESR emulation ON by default so a state-using backend runs locally exactly
243
+ // as in an ESR-enabled enclave (no chain/node/SGX). Caller (default: your dev
244
+ // address) is set for every task, so taskCaller() and the ACL behave as
245
+ // on-chain.
246
+ const caller = resolveCaller(opts.caller);
247
+ const esr = installEsr(executor, caller);
248
+ if (esr) console.log(`esr : local emulation ON caller ${caller}`);
249
+
172
250
  if (positional[0] === 'serve') {
173
- return serve(executor, opts.host, opts.port);
251
+ return serve(executor, opts.host, opts.port, esr, caller);
174
252
  }
175
253
 
176
254
  let payload;
@@ -191,7 +269,7 @@ function main() {
191
269
 
192
270
  console.log(`payload : ${JSON.stringify(payload)}`);
193
271
  console.log(`input : ${input === null ? '<empty>' : JSON.stringify(input.slice(0, 80))}`);
194
- const out = runTask(executor, payload, input);
272
+ const out = await runTask(executor, payload, input, esr, caller);
195
273
  console.log(`task code: ${out.task_code} (${out.task_code_name})`);
196
274
  console.log(`result : ${JSON.stringify(out.result)}`);
197
275
  if (out.task_code !== 0) {
@@ -201,4 +279,4 @@ function main() {
201
279
  process.exit(out.task_code === 0 ? 0 : 1);
202
280
  }
203
281
 
204
- main();
282
+ main().catch((e) => { console.error(e && e.stack ? e.stack : String(e)); process.exit(1); });