@ethernity-cloud/sdk-js 1.7.1 → 1.7.7
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 +28 -4
- package/cas/config.js +2 -2
- package/cas/resolver.js +49 -18
- package/cas/session_registry.js +72 -24
- package/info.js +1 -1
- package/init.js +13 -0
- package/localKubo.js +160 -0
- package/nodenithy/build/securelock/src/etny_crypto.js +20 -2
- package/nodenithy/build/securelock/src/models.js +3 -5
- package/nodenithy/build/securelock/src/securelock.js.tmpl +136 -70
- package/nodenithy/build.mjs +8 -7
- package/nodenithy/ipfs.mjs +37 -13
- package/nodenithy/run/image_registry.abi +1 -1
- package/nodenithy/run/image_registry.js +258 -68
- package/nodenithy/run/public_key_service.js +10 -3
- package/nodenithy/run/public_key_service.py +12 -6
- package/nodenithy/run.js +160 -88
- package/package.json +4 -2
- package/publish.js +20 -8
- package/pynithy/build/securelock/src/securelock.py.tmpl +21 -9
- package/pynithy/build.mjs +8 -7
- package/pynithy/ipfs.mjs +37 -13
- package/pynithy/run/image_registry.abi +1 -1
- package/pynithy/run/image_registry.js +258 -68
- package/pynithy/run/public_key_service.js +10 -3
- package/pynithy/run.js +160 -88
- package/revert.js +76 -0
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.stop());
|
|
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
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
411
|
-
//
|
|
412
|
-
//
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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,30 @@ 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
|
-
//
|
|
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
|
+
}
|
|
584
|
+
uploadToIpfs();
|
|
585
|
+
const registryEnv = { ...process.env, PROJECT_NAME: securelock, IPFS_PEER: ipfsPeer };
|
|
586
|
+
if (registryV2 && localKubo) {
|
|
587
|
+
await localKubo.provide(process.env.IPFS_HASH);
|
|
588
|
+
await localKubo.provide(process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
589
|
+
}
|
|
590
|
+
if (casTestnet) await registerSessionOnChain();
|
|
483
591
|
|
|
484
592
|
if (fs.existsSync('certificate.securelock.crt')) {
|
|
485
593
|
// delete it
|
|
@@ -488,7 +596,7 @@ const main = async () => {
|
|
|
488
596
|
|
|
489
597
|
let PUBLIC_KEY_SECURELOCK_RES = '';
|
|
490
598
|
try {
|
|
491
|
-
const output = execSync(`docker-compose run etny-securelock`, { cwd: runDir }).toString();
|
|
599
|
+
const output = execSync(`docker-compose run --rm etny-securelock`, { cwd: runDir }).toString();
|
|
492
600
|
console.log("Output of docker-compose run etny-securelock:");
|
|
493
601
|
// Extract the WHOLE multi-line PEM certificate block from the enclave
|
|
494
602
|
// output. A previous version grabbed only the single line containing
|
|
@@ -521,43 +629,6 @@ const main = async () => {
|
|
|
521
629
|
process.exit(0);
|
|
522
630
|
} else {
|
|
523
631
|
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
632
|
console.log(`ENCLAVE_NAME_SECURELOCK: ${ENCLAVE_NAME_SECURELOCK}`);
|
|
562
633
|
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
634
|
PUBLIC_KEY_SECURELOCK_RES = fs.readFileSync('PUBLIC_KEY.txt', 'utf8').trim();
|
|
@@ -614,7 +685,7 @@ const main = async () => {
|
|
|
614
685
|
}
|
|
615
686
|
|
|
616
687
|
// const scriptPath = path.resolve(__dirname, '/image_registry.js');
|
|
617
|
-
const trustedZoneCert = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "
|
|
688
|
+
const trustedZoneCert = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "getTrustedZoneCert"`).toString().trim();
|
|
618
689
|
|
|
619
690
|
console.log("trustedZoneCert: ", trustedZoneCert);
|
|
620
691
|
|
|
@@ -631,41 +702,42 @@ const main = async () => {
|
|
|
631
702
|
console.log("Listing certificate PUBLIC_KEY_TRUSTEDZONE:");
|
|
632
703
|
console.log(fs.readFileSync('certificate.trustedzone.crt', 'utf8'));
|
|
633
704
|
|
|
705
|
+
// image_registry.js reads the securelock certificate from registry/.
|
|
634
706
|
fs.copyFileSync('certificate.securelock.crt', `${registryPath}/certificate.securelock.crt`);
|
|
635
707
|
fs.copyFileSync('certificate.trustedzone.crt', `${registryPath}/certificate.trustedzone.crt`);
|
|
636
708
|
|
|
637
|
-
if (
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if (
|
|
658
|
-
|
|
709
|
+
if (registryV2) {
|
|
710
|
+
// The registered hash is the tree uploaded before the extraction.
|
|
711
|
+
process.chdir(currentDir);
|
|
712
|
+
console.log(`Registering the certificate of SECURELOCK ${securelock} in the IMAGE REGISTRY smart contract...`);
|
|
713
|
+
execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "v3" "setImageCert"`, { stdio: "inherit", env: registryEnv });
|
|
714
|
+
} else {
|
|
715
|
+
// A V1 registry gets the tree that carries the certificate files,
|
|
716
|
+
// uploaded again, with the certificate, in one addImage call.
|
|
717
|
+
if (fs.existsSync('IPFS_HASH.ipfs')) {
|
|
718
|
+
fs.unlinkSync('IPFS_HASH.ipfs');
|
|
719
|
+
}
|
|
720
|
+
console.log('Upload docker registry to IPFS');
|
|
721
|
+
execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
|
|
722
|
+
if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
|
|
723
|
+
console.error("Error: Could not upload registry to IPFS, please try again!");
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
process.env.IPFS_HASH = fs.readFileSync(`./IPFS_HASH.ipfs`, 'utf8').trim();
|
|
727
|
+
console.log("IPFS_HASH: ", process.env.IPFS_HASH);
|
|
728
|
+
writeEnv('IPFS_HASH', process.env.IPFS_HASH);
|
|
729
|
+
if (localKubo) await localKubo.provide(process.env.IPFS_HASH);
|
|
730
|
+
process.chdir(currentDir);
|
|
731
|
+
console.log(`Adding certificates for SECURELOCK ${securelock} into IMAGE REGISTRY smart contract...`);
|
|
732
|
+
// image_registry.js registers the image under PROJECT_NAME and prints
|
|
733
|
+
// why when it cannot.
|
|
734
|
+
registryEnv.IPFS_HASH = process.env.IPFS_HASH;
|
|
735
|
+
try {
|
|
736
|
+
execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "${process.env.VERSION}" "registerSecureLockImage"`, { stdio: "inherit", env: registryEnv });
|
|
737
|
+
} catch (error) {
|
|
738
|
+
console.error("Error: Could not add the certificates of the SECURELOCK to the IMAGE REGISTRY smart contract");
|
|
739
|
+
process.exit(1);
|
|
659
740
|
}
|
|
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
741
|
}
|
|
670
742
|
// The on-chain session points at the image it admits, so a validator can
|
|
671
743
|
// pin the image beside the body it serves.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethernity-cloud/sdk-js",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.7",
|
|
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",
|
|
@@ -32,11 +32,13 @@
|
|
|
32
32
|
"info.js",
|
|
33
33
|
"run.js",
|
|
34
34
|
"network.js",
|
|
35
|
+
"localKubo.js",
|
|
36
|
+
"revert.js",
|
|
35
37
|
"!**/__pycache__/**",
|
|
36
38
|
"!**/*.pyc"
|
|
37
39
|
],
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@ethernity-cloud/runner": "^0.6.
|
|
41
|
+
"@ethernity-cloud/runner": "^0.6.3",
|
|
40
42
|
"acorn": "^8.11.0",
|
|
41
43
|
"adm-zip": "^0.5.14",
|
|
42
44
|
"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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
35
|
-
'etny-pynithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '
|
|
36
|
-
'etny-nodenithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '
|
|
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', '
|
|
56
|
-
'etny-nodenithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '
|
|
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
|
|