@ethernity-cloud/sdk-js 1.7.1 → 1.7.8

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/nodenithy/run.js CHANGED
@@ -8,6 +8,7 @@ const https = require('https');
8
8
  const casConfig = require('../cas/config.js');
9
9
  const casResolver = require('../cas/resolver.js');
10
10
  const sessionRegistry = require('../cas/session_registry.js');
11
+ const { LocalKubo, ownEndpoint } = require('../localKubo.js');
11
12
 
12
13
  if (!fs.existsSync('.env')) {
13
14
  console.error("Error: .env file not found");
@@ -82,7 +83,7 @@ process.env.REGISTRY_PATH = `${currentDir}/registry`;
82
83
  const registryPath = process.env.REGISTRY_PATH;
83
84
 
84
85
  const runDockerCommand = (service) => {
85
- const command = `docker-compose run -e SCONE_LOG=INFO -e SCONE_HASH=1 ${service}`;
86
+ const command = `docker-compose run --rm -e SCONE_LOG=INFO -e SCONE_HASH=1 ${service}`;
86
87
  const output = execSync(command).toString().trim();
87
88
  console.log(`Output of ${command}: ${output}`);
88
89
  return output.split('\n').filter(line => !/Creating|Pulling|latest|Digest/.test(line)).join('');
@@ -103,7 +104,7 @@ const extractSconeHash = (service) => {
103
104
  // extract_signed_mrenclave. Returns '' if the file is absent (testnet builds).
104
105
  const extractSignedMrenclave = (service) => {
105
106
  try {
106
- const command = `docker-compose -f docker-compose.yml run --no-deps --entrypoint cat ${service} /signed_mrenclave.txt`;
107
+ const command = `docker-compose -f docker-compose.yml run --rm --no-deps --entrypoint cat ${service} /signed_mrenclave.txt`;
107
108
  const output = execSync(command, { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
108
109
  const m = output.match(/\b[a-fA-F0-9]{64}\b/);
109
110
  return m ? m[0] : '';
@@ -118,6 +119,79 @@ const main = async () => {
118
119
  // (etny-nodenithy v3/run/docker-compose.yml). The mainnet session named
119
120
  // below comes from the features/v3 branches, which ran it with 256M.
120
121
  const trustedZoneHeap = isMainnet ? '256M' : '1024M';
122
+
123
+ // Where the image goes. An IPFS API of the application's own
124
+ // (IPFS_ENDPOINT, when it is not the public one) takes precedence.
125
+ // Otherwise the publish runs its own Kubo: the public write API of
126
+ // ipfs.ethernity.cloud is closing, and the image is held by this Kubo,
127
+ // peered with the bootnode, until its certificate is on chain.
128
+ let localKubo = null;
129
+ let ipfsApi = process.env.IPFS_ENDPOINT || '';
130
+ if (!ownEndpoint(ipfsApi)) {
131
+ localKubo = new LocalKubo(process.env.PROJECT_NAME);
132
+ try {
133
+ await localKubo.start();
134
+ } catch (e) {
135
+ console.error(`Error: ${e.message}`);
136
+ process.exit(1);
137
+ }
138
+ ipfsApi = localKubo.apiUrl;
139
+ console.log(`\t✔ IPFS node for this publish: ${ipfsApi}`);
140
+ process.on('exit', () => localKubo.release());
141
+ }
142
+
143
+ // The CIDs uploadToIpfs gives, from the same add calls with only-hash: the
144
+ // IPFS node stores and announces nothing. Set by hashForIpfs, after which
145
+ // uploadToIpfs refuses an upload that gives other CIDs.
146
+ let expectedCids = null;
147
+ const hashForIpfs = () => {
148
+ const hashOf = (target) => {
149
+ try {
150
+ const out = execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action hash ${target}`, { stdio: ['ignore', 'pipe', 'inherit'] });
151
+ return out.toString().trim().split('\n').pop().trim();
152
+ } catch (e) {
153
+ console.error(`Error: could not hash ${target} for IPFS: ${e.message}`);
154
+ process.exit(1);
155
+ }
156
+ };
157
+ process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf('--filePath docker-compose-final.yml');
158
+ process.env.IPFS_HASH = hashOf(`--folderPath ${registryPath}`);
159
+ console.log("IPFS_DOCKER_COMPOSE_HASH (before upload): ", process.env.IPFS_DOCKER_COMPOSE_HASH);
160
+ console.log("IPFS_HASH (before upload): ", process.env.IPFS_HASH);
161
+ expectedCids = { image: process.env.IPFS_HASH, compose: process.env.IPFS_DOCKER_COMPOSE_HASH };
162
+ };
163
+
164
+ // The compose and the image tree, added through ipfsApi; their CIDs land
165
+ // in IPFS_DOCKER_COMPOSE_HASH / IPFS_HASH (files and environment).
166
+ const uploadToIpfs = () => {
167
+ for (const file of ['IPFS_HASH.ipfs', 'IPFS_DOCKER_COMPOSE_HASH.ipfs']) {
168
+ if (fs.existsSync(file)) fs.unlinkSync(file);
169
+ }
170
+ console.log('Upload docker-compose-final.yml to IPFS');
171
+ execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --filePath docker-compose-final.yml`, { stdio: "inherit" });
172
+ if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
173
+ console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
174
+ process.exit(1);
175
+ }
176
+ process.env.IPFS_DOCKER_COMPOSE_HASH = fs.readFileSync('IPFS_DOCKER_COMPOSE_HASH.ipfs', 'utf8').trim();
177
+ console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
178
+ writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
179
+ console.log('Upload docker registry to IPFS');
180
+ execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
181
+ if (!fs.existsSync('IPFS_HASH.ipfs')) {
182
+ console.error("Error: Could not upload registry to IPFS, please try again!");
183
+ process.exit(1);
184
+ }
185
+ process.env.IPFS_HASH = fs.readFileSync('IPFS_HASH.ipfs', 'utf8').trim();
186
+ console.log("IPFS_HASH: ", process.env.IPFS_HASH);
187
+ writeEnv('IPFS_HASH', process.env.IPFS_HASH);
188
+ if (expectedCids && (process.env.IPFS_HASH !== expectedCids.image
189
+ || process.env.IPFS_DOCKER_COMPOSE_HASH !== expectedCids.compose)) {
190
+ console.error(`Error: the upload gave ${process.env.IPFS_HASH} (compose ${process.env.IPFS_DOCKER_COMPOSE_HASH}), not the registered ${expectedCids.image} (compose ${expectedCids.compose}).`);
191
+ process.exit(1);
192
+ }
193
+ };
194
+
121
195
  ['docker-compose.yml', 'docker-compose-final.yml'].forEach(file => {
122
196
  const template = `${file}.tmpl`;
123
197
  if (!fs.existsSync(template)) {
@@ -239,17 +313,19 @@ const main = async () => {
239
313
  // Where the session goes depends on who provisions the securelock. On a
240
314
  // CAS-attested testnet it is registered ON-CHAIN in the ethernity-cas
241
315
  // SessionRegistry, which the validator set reads and which accepts no
242
- // POST; the publish then waits until the validators serve it, because the
243
- // public-key harvest below provisions the enclave from them. On mainnet it
244
- // is registered with the Scontain CAS. A self-signing testnet has none: a
245
- // CAS-issued certificate would not match the key the enclave derives.
246
- if (!cas) {
247
- console.log(`\t✔ ${process.env.BLOCKCHAIN_NETWORK}: no CAS session; the securelock self-signs from MR_ENCLAVE`);
248
- } else if (casTestnet) {
316
+ // POST. That happens once the image is registered, below: the
317
+ // SessionRegistry takes a securelock session only from the wallet that
318
+ // owns the image name. The publish then waits until the validators serve
319
+ // it, because the public-key harvest provisions the enclave from them. On
320
+ // mainnet it is registered with the Scontain CAS. A self-signing testnet
321
+ // has none: a CAS-issued certificate would not match the key the enclave
322
+ // derives.
323
+ const registerSessionOnChain = async () => {
249
324
  const registered = await sessionRegistry.register(
250
325
  casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
251
326
  process.env.PRIVATE_KEY, fs.readFileSync('etny-securelock-test.yaml'),
252
- process.env.IPFS_ENDPOINT, '');
327
+ ipfsApi, '');
328
+ if (localKubo) await localKubo.provide(registered.cid);
253
329
  if (registered.registered) {
254
330
  console.log(`\t✔ Session ${registered.name} registered on-chain (body ${registered.cid})`);
255
331
  await sessionRegistry.waitVisible(
@@ -257,7 +333,10 @@ const main = async () => {
257
333
  } else {
258
334
  console.log(`\t✔ Session ${registered.name} already registered on-chain with this body (${registered.hash})`);
259
335
  }
260
- } else {
336
+ };
337
+ if (!cas) {
338
+ console.log(`\t✔ ${process.env.BLOCKCHAIN_NETWORK}: no CAS session; the securelock self-signs from MR_ENCLAVE`);
339
+ } else if (!casTestnet) {
261
340
  // don't generate new keys if PREDECESSOR_HASH_SECURELOCK is not empty and the key.pem and cert.pem files exist
262
341
  if (PREDECESSOR_HASH_SECURELOCK !== 'EMPTY' && fs.existsSync('key.pem') && fs.existsSync('cert.pem')) {
263
342
  console.log("Skipping keypair generation and certificate creation.");
@@ -407,15 +486,20 @@ const main = async () => {
407
486
  console.log("# Update docker-compose files");
408
487
 
409
488
  // On a CAS-attested testnet the compose names the CAS both enclaves are
410
- // provisioned from (a validator resolved from the registry, or
411
- // ECLD_CAS_ADDR; the node re-resolves it before each task) and the
412
- // trustedzone session the ImageRegistry records for this template.
489
+ // provisioned from and the trustedzone session the ImageRegistry records
490
+ // for this template. docker-compose.yml, which harvests the certificate
491
+ // here, names a validator that answered (or ECLD_CAS_ADDR); the published
492
+ // docker-compose-final.yml names the first validator in registry order,
493
+ // so that a rerun of this publish renders the same compose. The node and
494
+ // the extraction service resolve a CAS again before they run it.
413
495
  let casAddr = null;
496
+ let publishedCasAddr = null;
414
497
  let trustedZoneSession = null;
415
498
  if (casTestnet) {
416
499
  casAddr = await casResolver.casAddressFor(casChain.rpcUrl, casConfig.VALIDATOR_REGISTRY[templateName]);
417
- console.log(`\t✔ CAS for this network: ${casAddr}`);
418
- trustedZoneSession = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "" "getTrustedZoneSession"`).toString().trim();
500
+ publishedCasAddr = await casResolver.publishedCasAddress(casChain.rpcUrl, casConfig.VALIDATOR_REGISTRY[templateName]);
501
+ console.log(`\t✔ CAS for this network: ${casAddr} (the published compose names ${publishedCasAddr})`);
502
+ trustedZoneSession = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "getTrustedZoneSession"`).toString().trim();
419
503
  if (!trustedZoneSession) {
420
504
  console.error(`ERROR! The ImageRegistry records no trustedzone session for ${templateName}.`);
421
505
  process.exit(1);
@@ -455,7 +539,8 @@ const main = async () => {
455
539
  .replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK)
456
540
  .replace(/__ENCLAVE_NAME_TRUSTEDZONE__/g, ENCLAVE_NAME_TRUSTEDZONE);
457
541
  if (casTestnet) {
458
- updatedContent = updatedContent.replace(/SCONE_CAS_ADDR=scone-cas\.cf/g, `SCONE_CAS_ADDR=${casAddr}`);
542
+ const fileCasAddr = file === 'docker-compose-final.yml' ? publishedCasAddr : casAddr;
543
+ updatedContent = updatedContent.replace(/SCONE_CAS_ADDR=scone-cas\.cf/g, `SCONE_CAS_ADDR=${fileCasAddr}`);
459
544
  }
460
545
 
461
546
  fs.writeFileSync(file, updatedContent, 'utf8');
@@ -479,7 +564,33 @@ const main = async () => {
479
564
  // let PUBLIC_KEY_SECURELOCK_RES = execSync(`docker-compose run etny-securelock 2>/dev/null | grep -v Creating | grep -v Pulling | grep -v latest | grep -v Digest | sed 's/.*PUBLIC_KEY:\\s*//' | tr -d '\\r'`).toString().trim();
480
565
 
481
566
 
482
- // TODO: calculate hash of the files localy, and query the public key service, if the hash is already there it means we dont have to do the below.
567
+ // The image and its compose go out before the certificate is extracted,
568
+ // whichever way it is: the extraction service fetches them by these CIDs.
569
+ // On a two-step registry (ECImageRegistryV2 and later) the wallet records
570
+ // the image first (registerImage, with the publish's node), under the
571
+ // CIDs the upload gives, hashed before anything is stored. On
572
+ // ECImageRegistryV3 that binds the image name to this wallet before the
573
+ // upload, the session registration or anything else discloses it. The
574
+ // bootnode's mirror then pins the image and the extraction service queues
575
+ // it from the chain; the certificate is written below by the same wallet
576
+ // (setImageCert). A V1 registry takes both in one addImage call.
577
+ const registryV2 = execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "v3" "isV2"`, { env: { ...process.env, PROJECT_NAME: securelock } }).toString().trim() === 'true';
578
+ let ipfsPeer = '';
579
+ if (registryV2) {
580
+ hashForIpfs();
581
+ if (localKubo) ipfsPeer = await localKubo.peerMultiaddr();
582
+ execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "v3" "registerImage"`, { stdio: "inherit", env: { ...process.env, PROJECT_NAME: securelock, IPFS_PEER: ipfsPeer } });
583
+ // From here the registered image's source is this Kubo, until its
584
+ // certificate is on chain.
585
+ if (localKubo) localKubo.serving = true;
586
+ }
587
+ uploadToIpfs();
588
+ const registryEnv = { ...process.env, PROJECT_NAME: securelock, IPFS_PEER: ipfsPeer };
589
+ if (registryV2 && localKubo) {
590
+ await localKubo.provide(process.env.IPFS_HASH);
591
+ await localKubo.provide(process.env.IPFS_DOCKER_COMPOSE_HASH);
592
+ }
593
+ if (casTestnet) await registerSessionOnChain();
483
594
 
484
595
  if (fs.existsSync('certificate.securelock.crt')) {
485
596
  // delete it
@@ -488,7 +599,7 @@ const main = async () => {
488
599
 
489
600
  let PUBLIC_KEY_SECURELOCK_RES = '';
490
601
  try {
491
- const output = execSync(`docker-compose run etny-securelock`, { cwd: runDir }).toString();
602
+ const output = execSync(`docker-compose run --rm etny-securelock`, { cwd: runDir }).toString();
492
603
  console.log("Output of docker-compose run etny-securelock:");
493
604
  // Extract the WHOLE multi-line PEM certificate block from the enclave
494
605
  // output. A previous version grabbed only the single line containing
@@ -521,43 +632,6 @@ const main = async () => {
521
632
  process.exit(0);
522
633
  } else {
523
634
  console.log("\nGenerating certificates using the Ethernity Cloud signing service...\n");
524
- console.log("**** Started ipfs initial pining ****");
525
- if (fs.existsSync('IPFS_HASH.ipfs')) {
526
- fs.unlinkSync('IPFS_HASH.ipfs');
527
- }
528
- if (fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
529
- fs.unlinkSync('IPFS_DOCKER_COMPOSE_HASH.ipfs');
530
- }
531
-
532
-
533
- console.log('Upload docker-compose-final.yml to IPFS');
534
- const dockerHash = execSync(`node ../ipfs.mjs --host "${process.env.IPFS_ENDPOINT}" --action upload --filePath docker-compose-final.yml`, { stdio: "inherit" });
535
- if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
536
- console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
537
- process.exit(1);
538
- }
539
- process.env.IPFS_DOCKER_COMPOSE_HASH = fs.readFileSync('IPFS_DOCKER_COMPOSE_HASH.ipfs', 'utf8').trim();
540
- console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
541
- writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
542
- console.log()
543
- await new Promise(resolve => setTimeout(resolve, 3000));
544
-
545
- console.log('Upload docker registry to IPFS');
546
- const repositoryHash = execSync(`node ../ipfs.mjs --host "${process.env.IPFS_ENDPOINT}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
547
- if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
548
- console.error("Error: Could not upload registry to IPFS, please try again!");
549
- process.exit(1);
550
- }
551
- process.env.IPFS_HASH = fs.readFileSync(`./IPFS_HASH.ipfs`, 'utf8').trim();
552
- console.log("IPFS_HASH: ", process.env.IPFS_HASH);
553
- writeEnv('IPFS_HASH', process.env.IPFS_HASH);
554
-
555
- console.log()
556
-
557
- console.log("**** Finished ipfs initial pining ****");
558
- console.log()
559
- console.log()
560
-
561
635
  console.log(`ENCLAVE_NAME_SECURELOCK: ${ENCLAVE_NAME_SECURELOCK}`);
562
636
  execSync(`node ./public_key_service.js --enclave_name "${securelock}" --protocol_version "v3" --network "${process.env.BLOCKCHAIN_NETWORK}" --template_version "${process.env.VERSION}"`, { stdio: 'inherit' });
563
637
  PUBLIC_KEY_SECURELOCK_RES = fs.readFileSync('PUBLIC_KEY.txt', 'utf8').trim();
@@ -614,7 +688,7 @@ const main = async () => {
614
688
  }
615
689
 
616
690
  // const scriptPath = path.resolve(__dirname, '/image_registry.js');
617
- const trustedZoneCert = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "" "getTrustedZoneCert"`).toString().trim();
691
+ const trustedZoneCert = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "getTrustedZoneCert"`).toString().trim();
618
692
 
619
693
  console.log("trustedZoneCert: ", trustedZoneCert);
620
694
 
@@ -631,41 +705,43 @@ const main = async () => {
631
705
  console.log("Listing certificate PUBLIC_KEY_TRUSTEDZONE:");
632
706
  console.log(fs.readFileSync('certificate.trustedzone.crt', 'utf8'));
633
707
 
708
+ // image_registry.js reads the securelock certificate from registry/.
634
709
  fs.copyFileSync('certificate.securelock.crt', `${registryPath}/certificate.securelock.crt`);
635
710
  fs.copyFileSync('certificate.trustedzone.crt', `${registryPath}/certificate.trustedzone.crt`);
636
711
 
637
- if (fs.existsSync('IPFS_HASH.ipfs')) {
638
- fs.unlinkSync('IPFS_HASH.ipfs');
639
- }
640
-
641
- console.log('Upload docker registry to IPFS');
642
- execSync(`node ../ipfs.mjs --host "${process.env.IPFS_ENDPOINT}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
643
- if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
644
- console.error("Error: Could not upload registry to IPFS, please try again!");
645
- process.exit(1);
646
- }
647
- process.env.IPFS_HASH = fs.readFileSync(`./IPFS_HASH.ipfs`, 'utf8').trim();
648
- console.log("IPFS_HASH: ", process.env.IPFS_HASH);
649
- writeEnv('IPFS_HASH', process.env.IPFS_HASH);
650
- process.chdir(currentDir);
651
- console.log(`Adding certificates for SECURELOCK ${securelock} into IMAGE REGISTRY smart contract...`);
652
- // image_registry.js registers the image under PROJECT_NAME.
653
- const registryEnv = { ...process.env, PROJECT_NAME: securelock };
654
- let existing = false;
655
- try {
656
- const existingImages = execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "${process.env.VERSION}" "${process.env.PRIVATE_KEY}" "registerSecureLockImage"`, { stdio: "inherit", env: registryEnv });
657
- if (existingImages.toString().trim().replace('Image hash: ', '') === process.env.IPFS_HASH) {
658
- console.log("Certificates for SECURELOCK already added to IMAGE REGISTRY smart contract");
712
+ if (registryV2) {
713
+ // The registered hash is the tree uploaded before the extraction.
714
+ process.chdir(currentDir);
715
+ console.log(`Registering the certificate of SECURELOCK ${securelock} in the IMAGE REGISTRY smart contract...`);
716
+ execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "v3" "setImageCert"`, { stdio: "inherit", env: registryEnv });
717
+ if (localKubo) localKubo.serving = false;
718
+ } else {
719
+ // A V1 registry gets the tree that carries the certificate files,
720
+ // uploaded again, with the certificate, in one addImage call.
721
+ if (fs.existsSync('IPFS_HASH.ipfs')) {
722
+ fs.unlinkSync('IPFS_HASH.ipfs');
723
+ }
724
+ console.log('Upload docker registry to IPFS');
725
+ execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
726
+ if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
727
+ console.error("Error: Could not upload registry to IPFS, please try again!");
728
+ process.exit(1);
729
+ }
730
+ process.env.IPFS_HASH = fs.readFileSync(`./IPFS_HASH.ipfs`, 'utf8').trim();
731
+ console.log("IPFS_HASH: ", process.env.IPFS_HASH);
732
+ writeEnv('IPFS_HASH', process.env.IPFS_HASH);
733
+ if (localKubo) await localKubo.provide(process.env.IPFS_HASH);
734
+ process.chdir(currentDir);
735
+ console.log(`Adding certificates for SECURELOCK ${securelock} into IMAGE REGISTRY smart contract...`);
736
+ // image_registry.js registers the image under PROJECT_NAME and prints
737
+ // why when it cannot.
738
+ registryEnv.IPFS_HASH = process.env.IPFS_HASH;
739
+ try {
740
+ execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "${process.env.VERSION}" "registerSecureLockImage"`, { stdio: "inherit", env: registryEnv });
741
+ } catch (error) {
742
+ console.error("Error: Could not add the certificates of the SECURELOCK to the IMAGE REGISTRY smart contract");
743
+ process.exit(1);
659
744
  }
660
- existing = true;
661
- } catch (error) {
662
- // console.error("Error: Could not add certificates for SECURELOCK into IMAGE REGISTRY smart contract");
663
- // console.error(error);
664
- // process.exit(1);
665
- }
666
-
667
- if (!existing) {
668
- const res = execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "" "" "" "registerSecureLockImage"`, { stdio: "inherit", env: registryEnv });
669
745
  }
670
746
  // The on-chain session points at the image it admits, so a validator can
671
747
  // pin the image beside the body it serves.
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.7.1",
3
+ "version": "1.7.8",
4
4
  "description": "A package to run ethernity cloud build scripts",
5
5
  "scripts": {
6
+ "test": "node --test \"test/**/*.test.js\"",
6
7
  "ecld-build": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/build.js",
7
8
  "ecld-publish": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/publish.js",
8
9
  "ecld-init": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/init.js",
@@ -32,11 +33,13 @@
32
33
  "info.js",
33
34
  "run.js",
34
35
  "network.js",
36
+ "localKubo.js",
37
+ "revert.js",
35
38
  "!**/__pycache__/**",
36
39
  "!**/*.pyc"
37
40
  ],
38
41
  "dependencies": {
39
- "@ethernity-cloud/runner": "^0.6.1",
42
+ "@ethernity-cloud/runner": "^0.6.3",
40
43
  "acorn": "^8.11.0",
41
44
  "adm-zip": "^0.5.14",
42
45
  "axios": "^1.7.2",
package/publish.js CHANGED
@@ -46,6 +46,10 @@ async function prompt(question) {
46
46
  const { PROJECT_NAME, BLOCKCHAIN_NETWORK, PRIVATE_KEY, DEVELOPER_FEE, SERVICE_TYPE } = process.env;
47
47
  const scriptPath = path.resolve(__dirname, 'nodenithy/run/image_registry.js');
48
48
  process.env.NODE_NO_WARNINGS = 1
49
+ // image_registry.js reads the wallet's key from PRIVATE_KEY in its
50
+ // environment, never from its command line.
51
+ const registry = (args, privateKey) => execSync(`node ${scriptPath} ${args}`,
52
+ { env: { ...process.env, PRIVATE_KEY: privateKey } }).toString().trim();
49
53
  let result = '';
50
54
  if (!PROJECT_NAME || !BLOCKCHAIN_NETWORK || !PRIVATE_KEY || !DEVELOPER_FEE) {
51
55
  const hasWallet = await prompt('Do you have an existing wallet? (yes/no) (default value: no) ') || 'no';
@@ -57,12 +61,12 @@ async function prompt(question) {
57
61
  }
58
62
 
59
63
  let privateKey = await prompt('Enter your private key: ');
60
- result = execSync(`node ${scriptPath} "" "" "" "${privateKey}" "validateAddress"`).toString().trim();
64
+ result = registry('"" "" "" "validateAddress"', privateKey);
61
65
 
62
66
  while (result !== 'OK') {
63
67
  console.log(result);
64
68
  privateKey = await prompt('Invalid private key. Please enter a valid private key: ');
65
- result = execSync(`node ${scriptPath} "" "" "" "${privateKey}" "validateAddress"`).toString().trim();
69
+ result = registry('"" "" "" "validateAddress"', privateKey);
66
70
  }
67
71
 
68
72
  console.log('Inputted Private key is valid.');
@@ -70,32 +74,40 @@ async function prompt(question) {
70
74
  console.log()
71
75
  console.log('Checking blockchain for required funds...');
72
76
 
73
- result = execSync(`node ${scriptPath} "" "" "" "" "checkBalance"`).toString().trim();
77
+ result = registry('"" "" "" "checkBalance"', privateKey);
74
78
 
75
79
  console.log(`Available funds: ${result}`);
76
80
  console.log()
77
81
  const imageName = casConfig.nameOnNetwork(PROJECT_NAME, BLOCKCHAIN_NETWORK);
78
82
  console.log(`Checking if project name ${imageName} is available on ${BLOCKCHAIN_NETWORK} network and ownership...`);
79
- result = execSync(`node ${scriptPath} ${BLOCKCHAIN_NETWORK} ${imageName} ${process.env.VERSION} ${process.env.PRIVATE_KEY}`,).toString().trim();
83
+ // Exits 1, with the owner, when another wallet owns the name.
84
+ try {
85
+ result = registry(`${BLOCKCHAIN_NETWORK} ${imageName} ${process.env.VERSION}`, privateKey);
86
+ } catch (e) {
87
+ console.log((e.stdout || '').toString().trim());
88
+ process.exit(1);
89
+ }
80
90
  console.log(result);
81
91
  console.log()
82
92
 
83
- const taskPercentage = await prompt('Please specify the % of a task which will be transferred to your wallet upon successful execution (default 10%): ') || '10';
93
+ // The publisher's fee, in percent of each task's base price: PoX adds it
94
+ // to what the dApp user pays and pays it to the image's reward address.
95
+ const taskPercentage = await prompt('Your fee as the publisher of this image, in % of each task\'s base price, 0 to 15 (default 0): ') || '0';
84
96
  writeEnv('DEVELOPER_FEE', taskPercentage);
85
97
  } else {
86
98
  console.log('Using PROJECT_NAME, BLOCKCHAIN_NETWORK, PRIVATE_KEY, DEVELOPER_FEE from .env');
87
99
  console.log('Checking blockchain for required funds...');
88
- result = result = execSync(`node ${scriptPath} "" "" "" "" "checkBalance"`).toString().trim();
100
+ result = registry('"" "" "" "checkBalance"', PRIVATE_KEY);
89
101
 
90
102
  console.log(`Available funds: ${result}`);
91
103
  console.log()
92
104
  let privateKey = PRIVATE_KEY;
93
- result = execSync(`node ${scriptPath} "" "" "" "${privateKey}" "validateAddress"`).toString().trim();
105
+ result = registry('"" "" "" "validateAddress"', privateKey);
94
106
 
95
107
  while (result !== 'OK') {
96
108
  console.log(result);
97
109
  privateKey = await prompt('Invalid private key. Please enter a valid private key: ');
98
- result = execSync(`node ${scriptPath} "" "" "" "${privateKey}" "validateAddress"`).toString().trim();
110
+ result = registry('"" "" "" "validateAddress"', privateKey);
99
111
  }
100
112
  writeEnv('PRIVATE_KEY', privateKey);
101
113
  }
@@ -149,6 +149,16 @@ class EtnySecureLock:
149
149
  logging.info("Generated key and cert in memory based on MR_ENCLAVE")
150
150
  return private_key_pem, cert_pem
151
151
 
152
+ # The node-supplied bucket .env may only contribute the per-task order id.
153
+ # Everything else the securelock trusts is a per-network build constant
154
+ # (chain id, PoX contract, Image Registry, Web3 provider, ValidatorRegistry,
155
+ # bucket) set in __set_initializers, or a variable the CAS injects into the
156
+ # enclave process environment (ECAS_*, SCONE_*). Any key outside this set,
157
+ # and any key already present in the environment, is ignored.
158
+ NODE_ENV_ALLOWLIST = frozenset({
159
+ 'ETNY_ORDER_ID',
160
+ })
161
+
152
162
  @staticmethod
153
163
  def read_env_str(env_str: str):
154
164
  with io.StringIO(env_str) as f:
@@ -156,7 +166,13 @@ class EtnySecureLock:
156
166
  if '=' not in line:
157
167
  continue
158
168
  key, value = line.split('=', 1)
159
- os.environ[key.strip()] = re.sub(r'\n', '', value.strip())
169
+ key = key.strip()
170
+ if key not in EtnySecureLock.NODE_ENV_ALLOWLIST:
171
+ logging.info(f"Ignoring non-allowlisted .env key from the node bucket: {key}")
172
+ continue
173
+ if key in os.environ:
174
+ continue
175
+ os.environ[key] = re.sub(r'\n', '', value.strip())
160
176
  def validate_client_payload(self):
161
177
  payload_data = self.__get_file_content_and_decrypt(self.payload)
162
178
  input_data = self.__get_file_content_and_decrypt(self.input)
@@ -588,14 +604,10 @@ class EtnySecureLock:
588
604
  with open(path, 'r') as f:
589
605
  return f.read()
590
606
  def __load_env(self):
591
- if os.getenv('ETNY_CHAIN_ID'):
592
- self.chain_id = int(os.getenv('ETNY_CHAIN_ID'))
593
- if os.getenv('ETNY_SMART_CONTRACT_ADDRESS'):
594
- self.smart_contract_address = os.getenv('ETNY_SMART_CONTRACT_ADDRESS').rstrip()
595
- if os.getenv('ETNY_WEB3_PROVIDER'):
596
- self.web3_provider = os.getenv('ETNY_WEB3_PROVIDER').rstrip()
597
- if os.getenv('IMAGE_REGISTRY_ADDRESS'):
598
- self.image_registry_address = os.getenv('IMAGE_REGISTRY_ADDRESS').rstrip()
607
+ # The chain id, PoX contract, Image Registry and Web3 provider are the
608
+ # build constants of __set_initializers and are never read from the
609
+ # environment: the node supplies the environment, and an override would
610
+ # let it choose the registry the trustedzone's certificate comes from.
599
611
  self.order_id = int(os.getenv('ETNY_ORDER_ID'))
600
612
 
601
613
  def save_result(self):
package/pynithy/build.mjs CHANGED
@@ -31,9 +31,9 @@ const writeEnv = (key, value) => {
31
31
  // Kept in sync with etny-{pynithy,nodenithy}/v3/networks.yaml (the authoritative
32
32
  // on-chain constants). Covers all supported networks for both dApp types.
33
33
  export const ECRunner = {
34
- // --- bloxberg (the testnet reads ECImageRegistryV2, mainnet the original) ---
35
- 'etny-pynithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8', 'https://bloxberg.ethernity.cloud', 8995],
36
- 'etny-nodenithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8', 'https://bloxberg.ethernity.cloud', 8995],
34
+ // --- bloxberg (the testnet reads ECImageRegistryV3, mainnet the original) ---
35
+ 'etny-pynithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
36
+ 'etny-nodenithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
37
37
  'etny-pynithy': ['0x549A6E06BB2084100148D50F51CF77a3436C3Ae7', '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31', 'https://bloxberg.ethernity.cloud', 8995],
38
38
  'etny-nodenithy': ['0x549A6E06BB2084100148D50F51CF77a3436C3Ae7', '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31', 'https://bloxberg.ethernity.cloud', 8995],
39
39
  // --- polygon mainnet ---
@@ -52,8 +52,8 @@ export const ECRunner = {
52
52
  'ecld-pynithy-litvm-testnet': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
53
53
  'ecld-nodenithy-litvm-testnet': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
54
54
  // --- the -unsafe networks: the contracts of the network each is named after ---
55
- 'etny-pynithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8', 'https://bloxberg.ethernity.cloud', 8995],
56
- 'etny-nodenithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8', 'https://bloxberg.ethernity.cloud', 8995],
55
+ 'etny-pynithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
56
+ 'etny-nodenithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
57
57
  'ecld-pynithy-litvm-testnet-unsafe': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
58
58
  'ecld-nodenithy-litvm-testnet-unsafe': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441]
59
59
  };
@@ -99,11 +99,12 @@ const runCommand = (command, canPass = false) => {
99
99
 
100
100
  // Removes the containers this build and publish create, matched by exact name:
101
101
  // `--filter name=` matches a substring, so name=las would also match the LAS of
102
- // a CAS validator set on the same host.
102
+ // a CAS validator set on the same host. `-v` removes their anonymous volumes
103
+ // too: registry:2 keeps its pushed images in one, about 1.3 GB per build.
103
104
  const removeContainer = (name) => {
104
105
  const ids = shell.exec(`docker ps -a -q --filter "name=^/${name}$"`, { silent: true }).stdout.trim();
105
106
  if (ids) {
106
- runCommand(`docker rm -f ${ids.split('\n').join(' ')}`);
107
+ runCommand(`docker rm -f -v ${ids.split('\n').join(' ')}`);
107
108
  }
108
109
  };
109
110