@ethernity-cloud/sdk-js 1.5.4 → 1.5.5

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.
@@ -1,586 +1,586 @@
1
- // Force stdout/stderr to be synchronous (blocking). Under SCONE 6 + Node, when
2
- // the process exits quickly (e.g. the PUBLIC_KEY cert-harvest run, which prints
3
- // the cert then returns) Node's pipe-buffered stdout can be torn down before it
4
- // flushes, so the harvested "PUBLIC_KEY:" line is lost and the CI cannot read
5
- // the certificate. Setting the fd handles to blocking makes every console.log
6
- // write synchronously.
7
- try { if (process.stdout._handle && process.stdout._handle.setBlocking) process.stdout._handle.setBlocking(true); } catch (e) {}
8
- try { if (process.stderr._handle && process.stderr._handle.setBlocking) process.stderr._handle.setBlocking(true); } catch (e) {}
9
-
10
- require('dotenv').config();
11
- require('dotenv').config({path: './app/.env'});
12
- require('dotenv').config({path: '/app/.env'});
13
-
14
- const ethers = require('ethers');
15
- const fs = require("fs");
16
- const etny_crypto = require("./etny_crypto");
17
- const {DOReqMetadata, Order} = require("./models");
18
- const {getWalletAddress} = require("./wallet");
19
- const {executeTask} = require("./etny_exec");
20
- const {TaskStatus} = require("./task_status");
21
- const SwiftStreamService = require("./swift_stream_service");
22
-
23
- const contract_abi = require('./pox_abi');
24
- const image_registry_abi = require('./image_registry_abi');
25
- const {sha256} = require("./etny_crypto");
26
- const {generateCertFromMrEnclave} = require("./self_sign");
27
- const esr_wallet = require("./esr_wallet");
28
-
29
- // Baked per network by CI (render_network.sh): "mainnet" | "testnet". On testnet
30
- // the enclave self-signs its cert from MR_ENCLAVE (non-CAS mode); on mainnet the
31
- // cert/key are provisioned by CAS into the injection paths.
32
- const NETWORK_TYPE = "__ETNY_NETWORK_TYPE__";
33
-
34
- class EtnySecureLock {
35
- static debug = false;
36
-
37
- constructor(swiftStreamClient) {
38
- console.log('Initializing...');
39
- this.version = 'v3';
40
- this.swiftStreamService = swiftStreamClient;
41
- this.etny_bucket = "__ETNY_BUCKET__";
42
- this.setInitializers();
43
- }
44
-
45
- setInitializers() {
46
- this.is_valid_client_data = true;
47
- // Testnet holds the self-derived identity key HERE, in memory only, and
48
- // never writes it. Stays null on mainnet, where CAS injects the key into
49
- // key_file and every consumer reads that path exactly as before.
50
- this.identity_private_key_pem = null;
51
- this.key_file = "/private/__ENCLAVE_NAME_SECURELOCK__/key.pem";
52
- this.cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/cert.pem";
53
- this.pub_cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/enclave_pub_cert.pem";
54
- this.payload = "payload.etny.securelock";
55
- this.input = "input.txt.securelock";
56
- this.result_file = "/app/result.txt";
57
- this.transaction_file = "/app/transaction.txt";
58
- this.smart_contract_address = "__ETNY_SMART_CONTRACT_ADDRESS__";
59
- this.image_registry_address = '__ETNY_IMAGE_REGISTRY_ADDRESS__';
60
- this.chain_id = __ETNY_CHAIN_ID__;
61
- this.web3_provider = "__ETNY_WEB3_PROVIDER__";
62
- this.signed_tx_as_bytes = null;
63
- this.client_challenge = "";
64
-
65
- if (this.etny_mode === "TEST") {
66
- this.key_file = "/private/key.pem";
67
- this.cert_file = "/app/cert.pem";
68
- this.payload = "./app/payload.js";
69
- this.input = "./app/input.txt";
70
- this.result_file = "./app/result.txt";
71
- this.transaction_file = "./app/transaction.txt";
72
- this.pub_cert_file = "./app/enclave_pub_cert.pem";
73
- }
74
-
75
- if (this.etny_mode === "LOCAL_TEST") {
76
- this.key_file = "./certs/key.pem";
77
- this.cert_file = "./certs/cert.pem";
78
- this.payload = "./app/payload.js";
79
- this.input = "./app/input.txt";
80
- this.result_file = "./app/result.txt";
81
- this.transaction_file = "./app/transaction.txt";
82
- this.pub_cert_file = "./app/enclave_pub_cert.pem";
83
- }
84
- }
85
-
86
- async create() {
87
- //overriding default values with those read from .env file
88
- this.loadEnv();
89
- this.printEnv();
90
- await this.savePublicCert();
91
- this.initWeb3();
92
- if (this.etny_mode === "PRODUCTION") {
93
- this.resetPrivateCertFile();
94
- }
95
-
96
- return true;
97
- }
98
-
99
- loadEnv() {
100
- if (process.env.ETNY_CHAIN_ID) {
101
- this.chain_id = parseInt(process.env.ETNY_CHAIN_ID);
102
- }
103
-
104
- if (process.env.ETNY_SMART_CONTRACT_ADDRESS) {
105
- this.smart_contract_address = process.env.ETNY_SMART_CONTRACT_ADDRESS.trim();
106
- }
107
-
108
- if (process.env.ETNY_WEB3_PROVIDER) {
109
- this.web3_provider = process.env.ETNY_WEB3_PROVIDER.trim();
110
- }
111
-
112
- if (process.env.IMAGE_REGISTRY_ADDRESS) {
113
- this.image_registry_address = process.env.IMAGE_REGISTRY_ADDRESS.trim();
114
- }
115
-
116
- if (process.env.ETNY_BUCKET) {
117
- this.etny_bucket = process.env.ETNY_BUCKET.trim();
118
- }
119
-
120
- if (process.env.ETNY_CLIENT_CHALLENGE) {
121
- this.client_challenge = process.env.ETNY_CLIENT_CHALLENGE.trim();
122
- }
123
- if (process.env.ETNY_ORDER_ID) {
124
- this.order_id = parseInt(process.env.ETNY_ORDER_ID);
125
- }
126
- }
127
-
128
- printEnv() {
129
- console.log('CHAIN ID:', this.chain_id);
130
- console.log('SMART CONTRACT ADDRESS:', this.smart_contract_address);
131
- console.log('WEB3 PROVIDER:', this.web3_provider);
132
- console.log('ENCRYPTED CHALLENGE:', this.client_challenge);
133
- console.log('ORDER ID:', this.order_id);
134
- }
135
-
136
- initWeb3() {
137
- // Create an ethers.js provider connected to the Bloxberg network
138
- this.w3 = new ethers.providers.JsonRpcProvider(this.web3_provider);
139
- // Create an instance of the contract using the ABI and address
140
- this.etny = new ethers.Contract(this.smart_contract_address, contract_abi, this.w3);
141
- this.image_registry = new ethers.Contract(this.image_registry_address, image_registry_abi, this.w3);
142
- // this.iamge_registry_signer = new ethers.Contract(this.image_registry_address, image_registry_abi.abi, this.walletSigner);
143
- }
144
-
145
- async readEnv() {
146
- try {
147
- console.log('Getting .env if exists...');
148
- const envFile = await this.swiftStreamService.getFileContent(this.etny_bucket, ".env");
149
- console.log("Env:", envFile);
150
- console.log("Saving .env file...");
151
- await fs.promises.writeFile('.env', envFile);
152
- require('dotenv').config();
153
-
154
- this.version = 'v3';
155
- this.etny_mode = process.env.ETNY_MODE;
156
- console.log(`ETNY Nodenithy [${this.version}] - running in ${this.etny_mode} MODE`);
157
- //console.log(process.env);
158
-
159
- return true;
160
- } catch (e) {
161
- return false;
162
- }
163
- }
164
-
165
- // v4 checksum validation: the DO-request is submitted on-chain by the owner,
166
- // so the metadata checksum is already authenticated by the transaction sender;
167
- // we only confirm the file content hashes to that recorded checksum. Empty
168
- // input carries no checksum (has_checksum === false) and is skipped. Kept
169
- // identical across securelock / trustedzone / validator so all three agree.
170
- async validateClientPayload(payload_data, input_data) {
171
- await this.getDORequestMetadata();
172
-
173
- const pmeta = this._metadata.payload_metadata_obj;
174
- if (pmeta.has_checksum) {
175
- const expected = pmeta.checksum.toLowerCase().replace(/^0x/, '');
176
- if (sha256(payload_data) !== expected) {
177
- this.task_code = TaskStatus.PAYLOAD_CHECKSUM_ERROR;
178
- this.task_result = 'PAYLOAD CHECKSUM DOESN\'T MATCH';
179
- this.is_valid_client_data = false;
180
- return;
181
- }
182
- } else {
183
- // payload is required and must carry a checksum
184
- this.task_code = TaskStatus.PAYLOAD_CHECKSUM_ERROR;
185
- this.task_result = 'PAYLOAD CHECKSUM DOESN\'T EXIST';
186
- this.is_valid_client_data = false;
187
- return;
188
- }
189
-
190
- const imeta = this._metadata.input_metadata_obj;
191
- if (imeta.has_checksum) {
192
- const expected = imeta.checksum.toLowerCase().replace(/^0x/, '');
193
- if (sha256(input_data) !== expected) {
194
- this.task_code = TaskStatus.INPUT_CHECKSUM_ERROR;
195
- this.task_result = 'INPUT CHECKSUM DOESN\'T MATCH';
196
- this.is_valid_client_data = false;
197
- }
198
- }
199
- }
200
-
201
- wait(timeout) {
202
- return new Promise(resolve => {
203
- setTimeout(resolve, timeout);
204
- });
205
- }
206
-
207
- async waitForObjectInBucket(bucketName, objectName, timeout = 600) {
208
- let i = 0;
209
- console.log(`Checking if object ${objectName} exists in bucket ${bucketName}`);
210
- while (true) {
211
- await this.wait(1000);
212
- i++;
213
- if (i % 20 === 0)
214
- console.log(`Checking if object ${objectName} exists in bucket ${bucketName}`);
215
- if (i > timeout) {
216
- break;
217
- }
218
- const [status, result] = await this.swiftStreamService.isObjectInBucket(bucketName, objectName);
219
- if (result) {
220
- break;
221
- }
222
- }
223
-
224
- console.log('TrustedZone finished the execution');
225
- }
226
-
227
- async waitForPayloadAndInput() {
228
- console.log('Waiting for payload and input');
229
- await this.waitForObjectInBucket(this.etny_bucket, this.payload);
230
- await this.waitForObjectInBucket(this.etny_bucket, this.input);
231
- }
232
-
233
- async getFileContentAndDecrypt(bucketName, objectName) {
234
- try {
235
- const encryptedBase64 = await this.swiftStreamService.getFileContent(bucketName, objectName);
236
-
237
- const encryptedTuple = etny_crypto.encryptedDataFromBase64Json(encryptedBase64);
238
- const decryptedResult = etny_crypto.decrypt(this.privateKeyMaterial(), encryptedTuple);
239
- return decryptedResult.toString();
240
- } catch (e) {
241
- console.log(`Failed to get ${objectName} file`);
242
- return false;
243
- }
244
- }
245
-
246
- async getTrustedZonePublicKey() {
247
- try {
248
- console.log('Getting the public key of the trusted zone enclave');
249
- let imageHash = this._metadata.image_hash;
250
- console.log('imageHash:', imageHash);
251
- if (EtnySecureLock.debug) {
252
- imageHash = 'v3-hash-1';
253
- }
254
- this.trustedZonePublicKey = await this.image_registry.getTrustedZoneImageCertPublicKey(imageHash);
255
- console.log('Public key of the TrustedZone enclave from smart contract:', this.trustedZonePublicKey);
256
- } catch (e) {
257
- console.log('getTrustedZonePublicKey', e);
258
- }
259
- }
260
-
261
- async encryptFileAndPushToSwiftStream(fileData, fileName) {
262
- try {
263
- console.log(`Started saving file ${fileName}.securelock...`);
264
- const encryptedInput = etny_crypto.encrypt(this.trustedZonePublicKey, fileData);
265
- const encryptedInputBase64 = etny_crypto.encryptedDataToBase64Json(encryptedInput);
266
-
267
- // File name should end with the '.securelock' extension
268
- fileName += '.securelock';
269
- // const data = Buffer.from(encryptedInputBase64, 'utf8');
270
- const status = this.swiftStreamService.putFileContent(this.etny_bucket, fileName, '', encryptedInputBase64);
271
- if (status) {
272
- console.log(`File ${fileName} encrypted and saved to swift stream successfully`);
273
- }
274
- } catch (e) {
275
- console.log('encryptFileAndPushToSwiftStream', e);
276
- }
277
- }
278
-
279
- async getDORequestMetadata() {
280
- const o_result = await this.etny.functions._getOrder(this.order_id);
281
- const order = new Order(o_result, this.order_id);
282
- //console.log('order', order);
283
- this._order_metadata = order;
284
- const o_metadata = await this.etny.functions._getDORequestMetadata(order.do_req);
285
- this._metadata = new DOReqMetadata(o_metadata, order.do_req);
286
- //console.log('metadata', this._metadata);
287
- }
288
-
289
- resetPrivateCertFile() {
290
- // Wipes the CAS-injected key from disk after it has been loaded
291
- // (mainnet PRODUCTION hygiene). On testnet the key was never written --
292
- // it lives only in memory -- so there is nothing on disk to wipe and the
293
- // truncate/read below would throw on a file that does not exist.
294
- //
295
- // The in-memory key is deliberately NOT cleared here: waitForPayloadAndInput
296
- // still needs it later to decrypt the payload/input, which runs after
297
- // create() has called this. Clearing it would break the task on testnet only.
298
- if (this.identity_private_key_pem) {
299
- return;
300
- }
301
- try {
302
- fs.truncate(this.key_file, 0, function () {
303
- //console.log('Private certificate file was cleaned.')
304
- });
305
- } catch (e) {
306
- //console.log('Error while removing the private key file content', e);
307
- }
308
- try {
309
- fs.readFileSync(this.key_file, 'utf8');
310
- } catch (e) {
311
- // Nothing to read back; the wipe above is what mattered.
312
- }
313
- }
314
-
315
- // Testnet non-CAS mode: derive a deterministic self-signed cert/key from this
316
- // enclave's identity. The CERTIFICATE is written (it is public, and
317
- // savePublicCert reads it back to publish it); the PRIVATE KEY is kept ONLY
318
- // in memory and never touches the filesystem.
319
- //
320
- // Why memory-only: on testnet there is no CAS and no attestation, so nothing
321
- // provisions or protects the key from outside the enclave. Not writing it at
322
- // all removes every at-rest attack on it outright, instead of relying on the
323
- // file protection being keyed by something trustworthy on this path.
324
- //
325
- // MAINNET IS UNCHANGED: this method returns immediately there, CAS still
326
- // injects cert+key into key_file/cert_file, and every consumer still reads
327
- // key_file exactly as before (this.identity_private_key_pem stays null, and
328
- // privateKeyMaterial() falls back to the path).
329
- //
330
- // Idempotent: the key is a pure function of the enclave identity, so it is
331
- // identical across restarts without needing to persist it.
332
- generateSelfSignedCertIfTestnet() {
333
- if (NETWORK_TYPE !== "testnet") return;
334
- console.log("[SecureLock] TESTNET: self-signing certificate from MR_ENCLAVE (no CAS) [TESTNET-INSECURE]");
335
- const {privateKeyPem, certPem, mrEnclaveHex} = generateCertFromMrEnclave("SecureLock Enclave");
336
- console.log(`[SecureLock] MR_ENCLAVE: ${mrEnclaveHex}`);
337
- for (const f of [this.cert_file, this.pub_cert_file]) {
338
- try { fs.mkdirSync(require('path').dirname(f), {recursive: true}); } catch (e) {}
339
- }
340
- // Private key: memory only. Deliberately NOT written to this.key_file.
341
- this.identity_private_key_pem = privateKeyPem;
342
- fs.writeFileSync(this.cert_file, certPem);
343
- }
344
-
345
- // The enclave's identity private key, in whatever form this network provides
346
- // it: the in-memory PEM on testnet, or the CAS-injected key_file path on
347
- // mainnet. etny_crypto accepts either, so callers do not branch on network.
348
- privateKeyMaterial() {
349
- return this.identity_private_key_pem || this.key_file;
350
- }
351
-
352
- async savePublicCert() {
353
- try {
354
- const content = await fs.promises.readFile(this.cert_file, 'utf8');
355
- await fs.promises.writeFile(this.pub_cert_file, content);
356
- //console.log(`Public certificate saved to ${this.pub_cert_file}`);
357
- console.log(`PUBLIC_KEY:`);
358
- console.log(content);
359
-
360
- // ESR (RFC §5.2): emit the enclave's ESR wallet ADDRESS on the same
361
- // harvest channel as the cert, so publish (or the remote extraction
362
- // service) can capture it. It MUST be derived here rather than by the
363
- // service: on mainnet the identity key is CAS-provisioned and exists
364
- // only inside this enclave, so nobody outside can compute the address.
365
- // Only the address is emitted — the private key never leaves.
366
- this.emitEsrWalletAddress();
367
-
368
- await this.swiftStreamService.putFileContent(this.etny_bucket, 'cert.pem', this.pub_cert_file);
369
- } catch (err) {
370
- console.error(err);
371
- }
372
- }
373
-
374
- // ESR wallet address emission (RFC §5.2). Gated on ESR_CONTRACT_ADDRESS being
375
- // set, exactly like the Python securelock, so non-ESR images print nothing.
376
- // Derived from the enclave's own identity private key (the certificate key);
377
- // only the ADDRESS is logged, never the key. Any failure is reported as
378
- // ESR_WALLET_ERROR and never breaks cert harvest or a task.
379
- emitEsrWalletAddress() {
380
- if (!String(process.env.ESR_CONTRACT_ADDRESS || "").trim()) return;
381
- try {
382
- // The identity key the enclave actually holds: the CAS-provisioned
383
- // key_file on mainnet (nothing in-enclave can re-derive it), or the
384
- // in-memory PEM on testnet (never written to disk). Hashed as-is,
385
- // matching the Python SDK.
386
- const identityPriv = this.identity_private_key_pem
387
- || fs.readFileSync(this.key_file, 'utf8');
388
- const esrAddress = esr_wallet.deriveWalletAddress(identityPriv);
389
- console.log(`ESR_WALLET_ADDRESS: ${esrAddress}`);
390
- if (!esr_wallet.isSecretIdentity(NETWORK_TYPE)) {
391
- console.warn(esr_wallet.INSECURE_IDENTITY_WARNING);
392
- }
393
- } catch (e) {
394
- // Never let ESR derivation break harvest/a task: report it the way
395
- // every other enclave-side config problem is reported.
396
- console.error(`ESR_WALLET_ERROR: ${e && e.message ? e.message : e}`);
397
- }
398
- }
399
-
400
- async saveResult() {
401
- try {
402
- await this.getTrustedZonePublicKey();
403
- await this.encryptFileAndPushToSwiftStream(this.task_result, 'result.txt');
404
- await this.encryptFileAndPushToSwiftStream(this.task_code.toString(), 'result_code.txt');
405
-
406
- const encrypted = etny_crypto.encryptWithPublicKey(this._metadata.public_key, this.task_result);
407
- //console.log('Encrypted result:', encrypted.toString('hex'));
408
- const result = await this.swiftStreamService.putFileContent(this.etny_bucket, 'result.txt', this.result_file, encrypted.toString('hex'));
409
- //console.log('Save result put: ', result);
410
- } catch (e) {
411
- console.log('Save result: ', e);
412
- }
413
- }
414
-
415
- // Make StateRegistry usable from payload code, if ESR is enabled.
416
- //
417
- // Wires ecld_state to this enclave's identity key, the SwiftStream client
418
- // and bucket the node shares with us, the registry address baked in at build
419
- // time, and the provider. Payload code then just does:
420
- //
421
- // const {StateRegistry} = require('./ecld_state');
422
- // await new StateRegistry().commit('key', s => ...);
423
- //
424
- // No-op when ESR is disabled, so non-ESR enclaves are unaffected. Never
425
- // throws: a setup problem must not stop the task from running -- payload
426
- // code that then uses StateRegistry gets a clear error from the registry
427
- // itself rather than a task that died before starting.
428
- configureStateRegistry() {
429
- const contractAddress = String(process.env.ESR_CONTRACT_ADDRESS || "").trim();
430
- if (!contractAddress) return;
431
- try {
432
- const ecld_state = require("./ecld_state");
433
- ecld_state.configure({
434
- identityPriv: this.identity_private_key_pem || fs.readFileSync(this.key_file, 'utf8'),
435
- swiftStreamService: this.swiftStreamService,
436
- bucket: this.etny_bucket,
437
- contractAddress: contractAddress,
438
- provider: this.w3,
439
- });
440
- console.log(`StateRegistry ready (ESR ${contractAddress})`);
441
- } catch (e) {
442
- console.error(`ESR_STATE_ERROR: could not configure StateRegistry: ${e && e.message ? e.message : e}`);
443
- }
444
- }
445
-
446
- async execute() {
447
- const payload_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.payload);
448
- const input_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.input);
449
- console.log('Validating client payload and input...');
450
- await this.validateClientPayload(payload_data, input_data);
451
-
452
- if (this.is_valid_client_data) {
453
- console.log('Client payload and input are valid');
454
- } else {
455
- console.log('Client payload and input are NOT valid');
456
- return false;
457
- }
458
-
459
- this.configureStateRegistry();
460
-
461
- const task_result = executeTask(payload_data, input_data);
462
- console.log('TASK RESULT (code, data):', task_result);
463
- this.task_code = task_result[0];
464
- this.task_result = task_result[1];
465
-
466
- return true;
467
- }
468
-
469
- }
470
-
471
- function initSwiftStreamClient() {
472
- console.log('Initializing SwiftStream Service...');
473
- const swiftStreamClient = new SwiftStreamService(
474
- EtnySecureLock.debug ? "localhost" : "etny-swift-stream",
475
- 9000,
476
- "swiftstreamadmin",
477
- "swiftstreamadmin");
478
- console.log('SwiftStream Service started.');
479
- console.log('################################');
480
-
481
- return swiftStreamClient;
482
- }
483
-
484
- // Set as soon as the enclave object exists so the top-level catch can report a
485
- // failure result even when the throw happens deep inside execute().
486
- let currentApp = null;
487
-
488
- /**
489
- * Best-effort: write a failure result so the trustedzone stops waiting.
490
- *
491
- * Securelock only publishes the result and its code; the trustedzone reads
492
- * those and builds the signed transaction. Without them that wait runs to its
493
- * timeout and the whole order stalls with no diagnosis.
494
- *
495
- * Guarded throughout: this runs while already handling a failure, so it must
496
- * never throw or hang.
497
- */
498
- async function reportFailure(app, err) {
499
- if (!app) {
500
- console.log('cannot report failure: enclave died before initialization');
501
- return;
502
- }
503
- try {
504
- // Keep a specific code if one was already set (checksum mismatch, ...).
505
- if (app.task_code === undefined || app.task_code === null || app.task_code === '') {
506
- app.task_code = TaskStatus.SYSTEM_ERROR;
507
- }
508
- if (!app.task_result) {
509
- app.task_result = `ENCLAVE ERROR: ${err && err.message ? err.message : err}`;
510
- }
511
- await app.saveResult();
512
- console.log(`failure reported (task_code=${app.task_code})`);
513
- } catch (reportErr) {
514
- console.log('could not report the failure result:', reportErr);
515
- }
516
- }
517
-
518
- async function execute() {
519
- console.log('########################################');
520
- console.log('########################################');
521
- console.log('########## [ETNY SecureLock] ##########');
522
- console.log('########################################');
523
- console.log('########################################');
524
- console.log('');
525
- console.log('');
526
- console.log('');
527
-
528
- const swiftStreamClient = initSwiftStreamClient();
529
-
530
- const app = new EtnySecureLock(swiftStreamClient);
531
- // Exposed so the top-level handler can still publish a failure result if
532
- // anything below throws (see reportFailure).
533
- currentApp = app;
534
-
535
- // Testnet: materialize the self-signed cert/key from MR_ENCLAVE into the
536
- // file paths before anything reads them (harvest + decrypt). No-op on mainnet
537
- // (CAS provides those files).
538
- app.generateSelfSignedCertIfTestnet();
539
-
540
- const hasReadEnvFile = await app.readEnv();
541
- if (!hasReadEnvFile && !EtnySecureLock.debug) {
542
- console.log(`Failed reading .env file.`);
543
- await app.savePublicCert();
544
- return;
545
- }
546
-
547
- if (process.env.ETNY_MODE === "PUBLIC_KEY") {
548
- await app.savePublicCert();
549
- } else {
550
- console.log('Creating the application...');
551
- const created = await app.create();
552
-
553
- if (!created) {
554
- console.log(`Failed at creation step, not able decoding input metadata, exiting.`);
555
- return;
556
- }
557
-
558
- console.log('Waiting for payload and input to be downloaded...');
559
- await app.waitForPayloadAndInput();
560
-
561
- if (app.is_valid_client_data) {
562
- console.log('Executing client code...');
563
- const wasExecuted = await app.execute();
564
- if (!wasExecuted) {
565
- console.log('Failed execution of SecureLock enclave.')
566
- return;
567
- }
568
- } else {
569
- console.log(`Failed client payload and input validation, ${app.task_result}, exiting.`);
570
- return;
571
- }
572
-
573
- await app.saveResult();
574
- console.log('Finished process.');
575
- }
576
- }
577
-
578
- (async () => {
579
- try {
580
- await execute();
581
- } catch (e) {
582
- console.log(e);
583
- await reportFailure(currentApp, e);
584
- process.exitCode = 1;
585
- }
586
- })();
1
+ // Force stdout/stderr to be synchronous (blocking). Under SCONE 6 + Node, when
2
+ // the process exits quickly (e.g. the PUBLIC_KEY cert-harvest run, which prints
3
+ // the cert then returns) Node's pipe-buffered stdout can be torn down before it
4
+ // flushes, so the harvested "PUBLIC_KEY:" line is lost and the CI cannot read
5
+ // the certificate. Setting the fd handles to blocking makes every console.log
6
+ // write synchronously.
7
+ try { if (process.stdout._handle && process.stdout._handle.setBlocking) process.stdout._handle.setBlocking(true); } catch (e) {}
8
+ try { if (process.stderr._handle && process.stderr._handle.setBlocking) process.stderr._handle.setBlocking(true); } catch (e) {}
9
+
10
+ require('dotenv').config();
11
+ require('dotenv').config({path: './app/.env'});
12
+ require('dotenv').config({path: '/app/.env'});
13
+
14
+ const ethers = require('ethers');
15
+ const fs = require("fs");
16
+ const etny_crypto = require("./etny_crypto");
17
+ const {DOReqMetadata, Order} = require("./models");
18
+ const {getWalletAddress} = require("./wallet");
19
+ const {executeTask} = require("./etny_exec");
20
+ const {TaskStatus} = require("./task_status");
21
+ const SwiftStreamService = require("./swift_stream_service");
22
+
23
+ const contract_abi = require('./pox_abi');
24
+ const image_registry_abi = require('./image_registry_abi');
25
+ const {sha256} = require("./etny_crypto");
26
+ const {generateCertFromMrEnclave} = require("./self_sign");
27
+ const esr_wallet = require("./esr_wallet");
28
+
29
+ // Baked per network by CI (render_network.sh): "mainnet" | "testnet". On testnet
30
+ // the enclave self-signs its cert from MR_ENCLAVE (non-CAS mode); on mainnet the
31
+ // cert/key are provisioned by CAS into the injection paths.
32
+ const NETWORK_TYPE = "__ETNY_NETWORK_TYPE__";
33
+
34
+ class EtnySecureLock {
35
+ static debug = false;
36
+
37
+ constructor(swiftStreamClient) {
38
+ console.log('Initializing...');
39
+ this.version = 'v3';
40
+ this.swiftStreamService = swiftStreamClient;
41
+ this.etny_bucket = "__ETNY_BUCKET__";
42
+ this.setInitializers();
43
+ }
44
+
45
+ setInitializers() {
46
+ this.is_valid_client_data = true;
47
+ // Testnet holds the self-derived identity key HERE, in memory only, and
48
+ // never writes it. Stays null on mainnet, where CAS injects the key into
49
+ // key_file and every consumer reads that path exactly as before.
50
+ this.identity_private_key_pem = null;
51
+ this.key_file = "/private/__ENCLAVE_NAME_SECURELOCK__/key.pem";
52
+ this.cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/cert.pem";
53
+ this.pub_cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/enclave_pub_cert.pem";
54
+ this.payload = "payload.etny.securelock";
55
+ this.input = "input.txt.securelock";
56
+ this.result_file = "/app/result.txt";
57
+ this.transaction_file = "/app/transaction.txt";
58
+ this.smart_contract_address = "__ETNY_SMART_CONTRACT_ADDRESS__";
59
+ this.image_registry_address = '__ETNY_IMAGE_REGISTRY_ADDRESS__';
60
+ this.chain_id = __ETNY_CHAIN_ID__;
61
+ this.web3_provider = "__ETNY_WEB3_PROVIDER__";
62
+ this.signed_tx_as_bytes = null;
63
+ this.client_challenge = "";
64
+
65
+ if (this.etny_mode === "TEST") {
66
+ this.key_file = "/private/key.pem";
67
+ this.cert_file = "/app/cert.pem";
68
+ this.payload = "./app/payload.js";
69
+ this.input = "./app/input.txt";
70
+ this.result_file = "./app/result.txt";
71
+ this.transaction_file = "./app/transaction.txt";
72
+ this.pub_cert_file = "./app/enclave_pub_cert.pem";
73
+ }
74
+
75
+ if (this.etny_mode === "LOCAL_TEST") {
76
+ this.key_file = "./certs/key.pem";
77
+ this.cert_file = "./certs/cert.pem";
78
+ this.payload = "./app/payload.js";
79
+ this.input = "./app/input.txt";
80
+ this.result_file = "./app/result.txt";
81
+ this.transaction_file = "./app/transaction.txt";
82
+ this.pub_cert_file = "./app/enclave_pub_cert.pem";
83
+ }
84
+ }
85
+
86
+ async create() {
87
+ //overriding default values with those read from .env file
88
+ this.loadEnv();
89
+ this.printEnv();
90
+ await this.savePublicCert();
91
+ this.initWeb3();
92
+ if (this.etny_mode === "PRODUCTION") {
93
+ this.resetPrivateCertFile();
94
+ }
95
+
96
+ return true;
97
+ }
98
+
99
+ loadEnv() {
100
+ if (process.env.ETNY_CHAIN_ID) {
101
+ this.chain_id = parseInt(process.env.ETNY_CHAIN_ID);
102
+ }
103
+
104
+ if (process.env.ETNY_SMART_CONTRACT_ADDRESS) {
105
+ this.smart_contract_address = process.env.ETNY_SMART_CONTRACT_ADDRESS.trim();
106
+ }
107
+
108
+ if (process.env.ETNY_WEB3_PROVIDER) {
109
+ this.web3_provider = process.env.ETNY_WEB3_PROVIDER.trim();
110
+ }
111
+
112
+ if (process.env.IMAGE_REGISTRY_ADDRESS) {
113
+ this.image_registry_address = process.env.IMAGE_REGISTRY_ADDRESS.trim();
114
+ }
115
+
116
+ if (process.env.ETNY_BUCKET) {
117
+ this.etny_bucket = process.env.ETNY_BUCKET.trim();
118
+ }
119
+
120
+ if (process.env.ETNY_CLIENT_CHALLENGE) {
121
+ this.client_challenge = process.env.ETNY_CLIENT_CHALLENGE.trim();
122
+ }
123
+ if (process.env.ETNY_ORDER_ID) {
124
+ this.order_id = parseInt(process.env.ETNY_ORDER_ID);
125
+ }
126
+ }
127
+
128
+ printEnv() {
129
+ console.log('CHAIN ID:', this.chain_id);
130
+ console.log('SMART CONTRACT ADDRESS:', this.smart_contract_address);
131
+ console.log('WEB3 PROVIDER:', this.web3_provider);
132
+ console.log('ENCRYPTED CHALLENGE:', this.client_challenge);
133
+ console.log('ORDER ID:', this.order_id);
134
+ }
135
+
136
+ initWeb3() {
137
+ // Create an ethers.js provider connected to the Bloxberg network
138
+ this.w3 = new ethers.providers.JsonRpcProvider(this.web3_provider);
139
+ // Create an instance of the contract using the ABI and address
140
+ this.etny = new ethers.Contract(this.smart_contract_address, contract_abi, this.w3);
141
+ this.image_registry = new ethers.Contract(this.image_registry_address, image_registry_abi, this.w3);
142
+ // this.iamge_registry_signer = new ethers.Contract(this.image_registry_address, image_registry_abi.abi, this.walletSigner);
143
+ }
144
+
145
+ async readEnv() {
146
+ try {
147
+ console.log('Getting .env if exists...');
148
+ const envFile = await this.swiftStreamService.getFileContent(this.etny_bucket, ".env");
149
+ console.log("Env:", envFile);
150
+ console.log("Saving .env file...");
151
+ await fs.promises.writeFile('.env', envFile);
152
+ require('dotenv').config();
153
+
154
+ this.version = 'v3';
155
+ this.etny_mode = process.env.ETNY_MODE;
156
+ console.log(`ETNY Nodenithy [${this.version}] - running in ${this.etny_mode} MODE`);
157
+ //console.log(process.env);
158
+
159
+ return true;
160
+ } catch (e) {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ // v4 checksum validation: the DO-request is submitted on-chain by the owner,
166
+ // so the metadata checksum is already authenticated by the transaction sender;
167
+ // we only confirm the file content hashes to that recorded checksum. Empty
168
+ // input carries no checksum (has_checksum === false) and is skipped. Kept
169
+ // identical across securelock / trustedzone / validator so all three agree.
170
+ async validateClientPayload(payload_data, input_data) {
171
+ await this.getDORequestMetadata();
172
+
173
+ const pmeta = this._metadata.payload_metadata_obj;
174
+ if (pmeta.has_checksum) {
175
+ const expected = pmeta.checksum.toLowerCase().replace(/^0x/, '');
176
+ if (sha256(payload_data) !== expected) {
177
+ this.task_code = TaskStatus.PAYLOAD_CHECKSUM_ERROR;
178
+ this.task_result = 'PAYLOAD CHECKSUM DOESN\'T MATCH';
179
+ this.is_valid_client_data = false;
180
+ return;
181
+ }
182
+ } else {
183
+ // payload is required and must carry a checksum
184
+ this.task_code = TaskStatus.PAYLOAD_CHECKSUM_ERROR;
185
+ this.task_result = 'PAYLOAD CHECKSUM DOESN\'T EXIST';
186
+ this.is_valid_client_data = false;
187
+ return;
188
+ }
189
+
190
+ const imeta = this._metadata.input_metadata_obj;
191
+ if (imeta.has_checksum) {
192
+ const expected = imeta.checksum.toLowerCase().replace(/^0x/, '');
193
+ if (sha256(input_data) !== expected) {
194
+ this.task_code = TaskStatus.INPUT_CHECKSUM_ERROR;
195
+ this.task_result = 'INPUT CHECKSUM DOESN\'T MATCH';
196
+ this.is_valid_client_data = false;
197
+ }
198
+ }
199
+ }
200
+
201
+ wait(timeout) {
202
+ return new Promise(resolve => {
203
+ setTimeout(resolve, timeout);
204
+ });
205
+ }
206
+
207
+ async waitForObjectInBucket(bucketName, objectName, timeout = 600) {
208
+ let i = 0;
209
+ console.log(`Checking if object ${objectName} exists in bucket ${bucketName}`);
210
+ while (true) {
211
+ await this.wait(1000);
212
+ i++;
213
+ if (i % 20 === 0)
214
+ console.log(`Checking if object ${objectName} exists in bucket ${bucketName}`);
215
+ if (i > timeout) {
216
+ break;
217
+ }
218
+ const [status, result] = await this.swiftStreamService.isObjectInBucket(bucketName, objectName);
219
+ if (result) {
220
+ break;
221
+ }
222
+ }
223
+
224
+ console.log('TrustedZone finished the execution');
225
+ }
226
+
227
+ async waitForPayloadAndInput() {
228
+ console.log('Waiting for payload and input');
229
+ await this.waitForObjectInBucket(this.etny_bucket, this.payload);
230
+ await this.waitForObjectInBucket(this.etny_bucket, this.input);
231
+ }
232
+
233
+ async getFileContentAndDecrypt(bucketName, objectName) {
234
+ try {
235
+ const encryptedBase64 = await this.swiftStreamService.getFileContent(bucketName, objectName);
236
+
237
+ const encryptedTuple = etny_crypto.encryptedDataFromBase64Json(encryptedBase64);
238
+ const decryptedResult = etny_crypto.decrypt(this.privateKeyMaterial(), encryptedTuple);
239
+ return decryptedResult.toString();
240
+ } catch (e) {
241
+ console.log(`Failed to get ${objectName} file`);
242
+ return false;
243
+ }
244
+ }
245
+
246
+ async getTrustedZonePublicKey() {
247
+ try {
248
+ console.log('Getting the public key of the trusted zone enclave');
249
+ let imageHash = this._metadata.image_hash;
250
+ console.log('imageHash:', imageHash);
251
+ if (EtnySecureLock.debug) {
252
+ imageHash = 'v3-hash-1';
253
+ }
254
+ this.trustedZonePublicKey = await this.image_registry.getTrustedZoneImageCertPublicKey(imageHash);
255
+ console.log('Public key of the TrustedZone enclave from smart contract:', this.trustedZonePublicKey);
256
+ } catch (e) {
257
+ console.log('getTrustedZonePublicKey', e);
258
+ }
259
+ }
260
+
261
+ async encryptFileAndPushToSwiftStream(fileData, fileName) {
262
+ try {
263
+ console.log(`Started saving file ${fileName}.securelock...`);
264
+ const encryptedInput = etny_crypto.encrypt(this.trustedZonePublicKey, fileData);
265
+ const encryptedInputBase64 = etny_crypto.encryptedDataToBase64Json(encryptedInput);
266
+
267
+ // File name should end with the '.securelock' extension
268
+ fileName += '.securelock';
269
+ // const data = Buffer.from(encryptedInputBase64, 'utf8');
270
+ const status = this.swiftStreamService.putFileContent(this.etny_bucket, fileName, '', encryptedInputBase64);
271
+ if (status) {
272
+ console.log(`File ${fileName} encrypted and saved to swift stream successfully`);
273
+ }
274
+ } catch (e) {
275
+ console.log('encryptFileAndPushToSwiftStream', e);
276
+ }
277
+ }
278
+
279
+ async getDORequestMetadata() {
280
+ const o_result = await this.etny.functions._getOrder(this.order_id);
281
+ const order = new Order(o_result, this.order_id);
282
+ //console.log('order', order);
283
+ this._order_metadata = order;
284
+ const o_metadata = await this.etny.functions._getDORequestMetadata(order.do_req);
285
+ this._metadata = new DOReqMetadata(o_metadata, order.do_req);
286
+ //console.log('metadata', this._metadata);
287
+ }
288
+
289
+ resetPrivateCertFile() {
290
+ // Wipes the CAS-injected key from disk after it has been loaded
291
+ // (mainnet PRODUCTION hygiene). On testnet the key was never written --
292
+ // it lives only in memory -- so there is nothing on disk to wipe and the
293
+ // truncate/read below would throw on a file that does not exist.
294
+ //
295
+ // The in-memory key is deliberately NOT cleared here: waitForPayloadAndInput
296
+ // still needs it later to decrypt the payload/input, which runs after
297
+ // create() has called this. Clearing it would break the task on testnet only.
298
+ if (this.identity_private_key_pem) {
299
+ return;
300
+ }
301
+ try {
302
+ fs.truncate(this.key_file, 0, function () {
303
+ //console.log('Private certificate file was cleaned.')
304
+ });
305
+ } catch (e) {
306
+ //console.log('Error while removing the private key file content', e);
307
+ }
308
+ try {
309
+ fs.readFileSync(this.key_file, 'utf8');
310
+ } catch (e) {
311
+ // Nothing to read back; the wipe above is what mattered.
312
+ }
313
+ }
314
+
315
+ // Testnet non-CAS mode: derive a deterministic self-signed cert/key from this
316
+ // enclave's identity. The CERTIFICATE is written (it is public, and
317
+ // savePublicCert reads it back to publish it); the PRIVATE KEY is kept ONLY
318
+ // in memory and never touches the filesystem.
319
+ //
320
+ // Why memory-only: on testnet there is no CAS and no attestation, so nothing
321
+ // provisions or protects the key from outside the enclave. Not writing it at
322
+ // all removes every at-rest attack on it outright, instead of relying on the
323
+ // file protection being keyed by something trustworthy on this path.
324
+ //
325
+ // MAINNET IS UNCHANGED: this method returns immediately there, CAS still
326
+ // injects cert+key into key_file/cert_file, and every consumer still reads
327
+ // key_file exactly as before (this.identity_private_key_pem stays null, and
328
+ // privateKeyMaterial() falls back to the path).
329
+ //
330
+ // Idempotent: the key is a pure function of the enclave identity, so it is
331
+ // identical across restarts without needing to persist it.
332
+ generateSelfSignedCertIfTestnet() {
333
+ if (NETWORK_TYPE !== "testnet") return;
334
+ console.log("[SecureLock] TESTNET: self-signing certificate from MR_ENCLAVE (no CAS) [TESTNET-INSECURE]");
335
+ const {privateKeyPem, certPem, mrEnclaveHex} = generateCertFromMrEnclave("SecureLock Enclave");
336
+ console.log(`[SecureLock] MR_ENCLAVE: ${mrEnclaveHex}`);
337
+ for (const f of [this.cert_file, this.pub_cert_file]) {
338
+ try { fs.mkdirSync(require('path').dirname(f), {recursive: true}); } catch (e) {}
339
+ }
340
+ // Private key: memory only. Deliberately NOT written to this.key_file.
341
+ this.identity_private_key_pem = privateKeyPem;
342
+ fs.writeFileSync(this.cert_file, certPem);
343
+ }
344
+
345
+ // The enclave's identity private key, in whatever form this network provides
346
+ // it: the in-memory PEM on testnet, or the CAS-injected key_file path on
347
+ // mainnet. etny_crypto accepts either, so callers do not branch on network.
348
+ privateKeyMaterial() {
349
+ return this.identity_private_key_pem || this.key_file;
350
+ }
351
+
352
+ async savePublicCert() {
353
+ try {
354
+ const content = await fs.promises.readFile(this.cert_file, 'utf8');
355
+ await fs.promises.writeFile(this.pub_cert_file, content);
356
+ //console.log(`Public certificate saved to ${this.pub_cert_file}`);
357
+ console.log(`PUBLIC_KEY:`);
358
+ console.log(content);
359
+
360
+ // ESR (RFC §5.2): emit the enclave's ESR wallet ADDRESS on the same
361
+ // harvest channel as the cert, so publish (or the remote extraction
362
+ // service) can capture it. It MUST be derived here rather than by the
363
+ // service: on mainnet the identity key is CAS-provisioned and exists
364
+ // only inside this enclave, so nobody outside can compute the address.
365
+ // Only the address is emitted — the private key never leaves.
366
+ this.emitEsrWalletAddress();
367
+
368
+ await this.swiftStreamService.putFileContent(this.etny_bucket, 'cert.pem', this.pub_cert_file);
369
+ } catch (err) {
370
+ console.error(err);
371
+ }
372
+ }
373
+
374
+ // ESR wallet address emission (RFC §5.2). Gated on ESR_CONTRACT_ADDRESS being
375
+ // set, exactly like the Python securelock, so non-ESR images print nothing.
376
+ // Derived from the enclave's own identity private key (the certificate key);
377
+ // only the ADDRESS is logged, never the key. Any failure is reported as
378
+ // ESR_WALLET_ERROR and never breaks cert harvest or a task.
379
+ emitEsrWalletAddress() {
380
+ if (!String(process.env.ESR_CONTRACT_ADDRESS || "").trim()) return;
381
+ try {
382
+ // The identity key the enclave actually holds: the CAS-provisioned
383
+ // key_file on mainnet (nothing in-enclave can re-derive it), or the
384
+ // in-memory PEM on testnet (never written to disk). Hashed as-is,
385
+ // matching the Python SDK.
386
+ const identityPriv = this.identity_private_key_pem
387
+ || fs.readFileSync(this.key_file, 'utf8');
388
+ const esrAddress = esr_wallet.deriveWalletAddress(identityPriv);
389
+ console.log(`ESR_WALLET_ADDRESS: ${esrAddress}`);
390
+ if (!esr_wallet.isSecretIdentity(NETWORK_TYPE)) {
391
+ console.warn(esr_wallet.INSECURE_IDENTITY_WARNING);
392
+ }
393
+ } catch (e) {
394
+ // Never let ESR derivation break harvest/a task: report it the way
395
+ // every other enclave-side config problem is reported.
396
+ console.error(`ESR_WALLET_ERROR: ${e && e.message ? e.message : e}`);
397
+ }
398
+ }
399
+
400
+ async saveResult() {
401
+ try {
402
+ await this.getTrustedZonePublicKey();
403
+ await this.encryptFileAndPushToSwiftStream(this.task_result, 'result.txt');
404
+ await this.encryptFileAndPushToSwiftStream(this.task_code.toString(), 'result_code.txt');
405
+
406
+ const encrypted = etny_crypto.encryptWithPublicKey(this._metadata.public_key, this.task_result);
407
+ //console.log('Encrypted result:', encrypted.toString('hex'));
408
+ const result = await this.swiftStreamService.putFileContent(this.etny_bucket, 'result.txt', this.result_file, encrypted.toString('hex'));
409
+ //console.log('Save result put: ', result);
410
+ } catch (e) {
411
+ console.log('Save result: ', e);
412
+ }
413
+ }
414
+
415
+ // Make StateRegistry usable from payload code, if ESR is enabled.
416
+ //
417
+ // Wires ecld_state to this enclave's identity key, the SwiftStream client
418
+ // and bucket the node shares with us, the registry address baked in at build
419
+ // time, and the provider. Payload code then just does:
420
+ //
421
+ // const {StateRegistry} = require('./ecld_state');
422
+ // await new StateRegistry().commit('key', s => ...);
423
+ //
424
+ // No-op when ESR is disabled, so non-ESR enclaves are unaffected. Never
425
+ // throws: a setup problem must not stop the task from running -- payload
426
+ // code that then uses StateRegistry gets a clear error from the registry
427
+ // itself rather than a task that died before starting.
428
+ configureStateRegistry() {
429
+ const contractAddress = String(process.env.ESR_CONTRACT_ADDRESS || "").trim();
430
+ if (!contractAddress) return;
431
+ try {
432
+ const ecld_state = require("./ecld_state");
433
+ ecld_state.configure({
434
+ identityPriv: this.identity_private_key_pem || fs.readFileSync(this.key_file, 'utf8'),
435
+ swiftStreamService: this.swiftStreamService,
436
+ bucket: this.etny_bucket,
437
+ contractAddress: contractAddress,
438
+ provider: this.w3,
439
+ });
440
+ console.log(`StateRegistry ready (ESR ${contractAddress})`);
441
+ } catch (e) {
442
+ console.error(`ESR_STATE_ERROR: could not configure StateRegistry: ${e && e.message ? e.message : e}`);
443
+ }
444
+ }
445
+
446
+ async execute() {
447
+ const payload_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.payload);
448
+ const input_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.input);
449
+ console.log('Validating client payload and input...');
450
+ await this.validateClientPayload(payload_data, input_data);
451
+
452
+ if (this.is_valid_client_data) {
453
+ console.log('Client payload and input are valid');
454
+ } else {
455
+ console.log('Client payload and input are NOT valid');
456
+ return false;
457
+ }
458
+
459
+ this.configureStateRegistry();
460
+
461
+ const task_result = executeTask(payload_data, input_data);
462
+ console.log('TASK RESULT (code, data):', task_result);
463
+ this.task_code = task_result[0];
464
+ this.task_result = task_result[1];
465
+
466
+ return true;
467
+ }
468
+
469
+ }
470
+
471
+ function initSwiftStreamClient() {
472
+ console.log('Initializing SwiftStream Service...');
473
+ const swiftStreamClient = new SwiftStreamService(
474
+ EtnySecureLock.debug ? "localhost" : "etny-swift-stream",
475
+ 9000,
476
+ "swiftstreamadmin",
477
+ "swiftstreamadmin");
478
+ console.log('SwiftStream Service started.');
479
+ console.log('################################');
480
+
481
+ return swiftStreamClient;
482
+ }
483
+
484
+ // Set as soon as the enclave object exists so the top-level catch can report a
485
+ // failure result even when the throw happens deep inside execute().
486
+ let currentApp = null;
487
+
488
+ /**
489
+ * Best-effort: write a failure result so the trustedzone stops waiting.
490
+ *
491
+ * Securelock only publishes the result and its code; the trustedzone reads
492
+ * those and builds the signed transaction. Without them that wait runs to its
493
+ * timeout and the whole order stalls with no diagnosis.
494
+ *
495
+ * Guarded throughout: this runs while already handling a failure, so it must
496
+ * never throw or hang.
497
+ */
498
+ async function reportFailure(app, err) {
499
+ if (!app) {
500
+ console.log('cannot report failure: enclave died before initialization');
501
+ return;
502
+ }
503
+ try {
504
+ // Keep a specific code if one was already set (checksum mismatch, ...).
505
+ if (app.task_code === undefined || app.task_code === null || app.task_code === '') {
506
+ app.task_code = TaskStatus.SYSTEM_ERROR;
507
+ }
508
+ if (!app.task_result) {
509
+ app.task_result = `ENCLAVE ERROR: ${err && err.message ? err.message : err}`;
510
+ }
511
+ await app.saveResult();
512
+ console.log(`failure reported (task_code=${app.task_code})`);
513
+ } catch (reportErr) {
514
+ console.log('could not report the failure result:', reportErr);
515
+ }
516
+ }
517
+
518
+ async function execute() {
519
+ console.log('########################################');
520
+ console.log('########################################');
521
+ console.log('########## [ETNY SecureLock] ##########');
522
+ console.log('########################################');
523
+ console.log('########################################');
524
+ console.log('');
525
+ console.log('');
526
+ console.log('');
527
+
528
+ const swiftStreamClient = initSwiftStreamClient();
529
+
530
+ const app = new EtnySecureLock(swiftStreamClient);
531
+ // Exposed so the top-level handler can still publish a failure result if
532
+ // anything below throws (see reportFailure).
533
+ currentApp = app;
534
+
535
+ // Testnet: materialize the self-signed cert/key from MR_ENCLAVE into the
536
+ // file paths before anything reads them (harvest + decrypt). No-op on mainnet
537
+ // (CAS provides those files).
538
+ app.generateSelfSignedCertIfTestnet();
539
+
540
+ const hasReadEnvFile = await app.readEnv();
541
+ if (!hasReadEnvFile && !EtnySecureLock.debug) {
542
+ console.log(`Failed reading .env file.`);
543
+ await app.savePublicCert();
544
+ return;
545
+ }
546
+
547
+ if (process.env.ETNY_MODE === "PUBLIC_KEY") {
548
+ await app.savePublicCert();
549
+ } else {
550
+ console.log('Creating the application...');
551
+ const created = await app.create();
552
+
553
+ if (!created) {
554
+ console.log(`Failed at creation step, not able decoding input metadata, exiting.`);
555
+ return;
556
+ }
557
+
558
+ console.log('Waiting for payload and input to be downloaded...');
559
+ await app.waitForPayloadAndInput();
560
+
561
+ if (app.is_valid_client_data) {
562
+ console.log('Executing client code...');
563
+ const wasExecuted = await app.execute();
564
+ if (!wasExecuted) {
565
+ console.log('Failed execution of SecureLock enclave.')
566
+ return;
567
+ }
568
+ } else {
569
+ console.log(`Failed client payload and input validation, ${app.task_result}, exiting.`);
570
+ return;
571
+ }
572
+
573
+ await app.saveResult();
574
+ console.log('Finished process.');
575
+ }
576
+ }
577
+
578
+ (async () => {
579
+ try {
580
+ await execute();
581
+ } catch (e) {
582
+ console.log(e);
583
+ await reportFailure(currentApp, e);
584
+ process.exitCode = 1;
585
+ }
586
+ })();