@ethernity-cloud/sdk-js 1.7.0 → 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 +74 -50
- package/cas/config.js +6 -6
- package/cas/resolver.js +49 -18
- package/cas/session_registry.js +72 -24
- package/info.js +1 -1
- package/init.js +17 -2
- package/localKubo.js +160 -0
- package/network.js +51 -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/nodenithy/src/ec_helloworld_example.js +46 -42
- package/nodenithy/src/preStart.mjs +41 -51
- package/package.json +5 -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/pynithy/src/ec_helloworld_example.js +46 -42
- package/pynithy/src/preStart.mjs +41 -51
- package/revert.js +76 -0
- package/run.js +1 -39
- package/nodenithy/src/preStart.js +0 -45
package/pynithy/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('');
|
|
@@ -102,7 +103,7 @@ const extractSconeHash = (service) => {
|
|
|
102
103
|
// Returns '' if the file is absent (testnet builds).
|
|
103
104
|
const extractSignedMrenclave = (service) => {
|
|
104
105
|
try {
|
|
105
|
-
const command = `docker-compose -f docker-compose.yml run --no-deps --entrypoint cat ${service} /signed_mrenclave.txt`;
|
|
106
|
+
const command = `docker-compose -f docker-compose.yml run --rm --no-deps --entrypoint cat ${service} /signed_mrenclave.txt`;
|
|
106
107
|
const output = execSync(command, { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
107
108
|
const m = output.match(/\b[a-fA-F0-9]{64}\b/);
|
|
108
109
|
return m ? m[0] : '';
|
|
@@ -119,6 +120,79 @@ const main = async () => {
|
|
|
119
120
|
console.error("Error: MEMORY_TO_ALLOCATE is not set in .env; run ecld-build first.");
|
|
120
121
|
process.exit(1);
|
|
121
122
|
}
|
|
123
|
+
|
|
124
|
+
// Where the image goes. An IPFS API of the application's own
|
|
125
|
+
// (IPFS_ENDPOINT, when it is not the public one) takes precedence.
|
|
126
|
+
// Otherwise the publish runs its own Kubo: the public write API of
|
|
127
|
+
// ipfs.ethernity.cloud is closing, and the image is held by this Kubo,
|
|
128
|
+
// peered with the bootnode, until its certificate is on chain.
|
|
129
|
+
let localKubo = null;
|
|
130
|
+
let ipfsApi = process.env.IPFS_ENDPOINT || '';
|
|
131
|
+
if (!ownEndpoint(ipfsApi)) {
|
|
132
|
+
localKubo = new LocalKubo(process.env.PROJECT_NAME);
|
|
133
|
+
try {
|
|
134
|
+
await localKubo.start();
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.error(`Error: ${e.message}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
ipfsApi = localKubo.apiUrl;
|
|
140
|
+
console.log(`\t✔ IPFS node for this publish: ${ipfsApi}`);
|
|
141
|
+
process.on('exit', () => localKubo.stop());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The CIDs uploadToIpfs gives, from the same add calls with only-hash: the
|
|
145
|
+
// IPFS node stores and announces nothing. Set by hashForIpfs, after which
|
|
146
|
+
// uploadToIpfs refuses an upload that gives other CIDs.
|
|
147
|
+
let expectedCids = null;
|
|
148
|
+
const hashForIpfs = () => {
|
|
149
|
+
const hashOf = (target) => {
|
|
150
|
+
try {
|
|
151
|
+
const out = execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action hash ${target}`, { stdio: ['ignore', 'pipe', 'inherit'] });
|
|
152
|
+
return out.toString().trim().split('\n').pop().trim();
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error(`Error: could not hash ${target} for IPFS: ${e.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf('--filePath docker-compose-final.yml');
|
|
159
|
+
process.env.IPFS_HASH = hashOf(`--folderPath ${registryPath}`);
|
|
160
|
+
console.log("IPFS_DOCKER_COMPOSE_HASH (before upload): ", process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
161
|
+
console.log("IPFS_HASH (before upload): ", process.env.IPFS_HASH);
|
|
162
|
+
expectedCids = { image: process.env.IPFS_HASH, compose: process.env.IPFS_DOCKER_COMPOSE_HASH };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// The compose and the image tree, added through ipfsApi; their CIDs land
|
|
166
|
+
// in IPFS_DOCKER_COMPOSE_HASH / IPFS_HASH (files and environment).
|
|
167
|
+
const uploadToIpfs = () => {
|
|
168
|
+
for (const file of ['IPFS_HASH.ipfs', 'IPFS_DOCKER_COMPOSE_HASH.ipfs']) {
|
|
169
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
170
|
+
}
|
|
171
|
+
console.log('Upload docker-compose-final.yml to IPFS');
|
|
172
|
+
execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --filePath docker-compose-final.yml`, { stdio: "inherit" });
|
|
173
|
+
if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
|
|
174
|
+
console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
process.env.IPFS_DOCKER_COMPOSE_HASH = fs.readFileSync('IPFS_DOCKER_COMPOSE_HASH.ipfs', 'utf8').trim();
|
|
178
|
+
console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
179
|
+
writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
180
|
+
console.log('Upload docker registry to IPFS');
|
|
181
|
+
execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
|
|
182
|
+
if (!fs.existsSync('IPFS_HASH.ipfs')) {
|
|
183
|
+
console.error("Error: Could not upload registry to IPFS, please try again!");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
process.env.IPFS_HASH = fs.readFileSync('IPFS_HASH.ipfs', 'utf8').trim();
|
|
187
|
+
console.log("IPFS_HASH: ", process.env.IPFS_HASH);
|
|
188
|
+
writeEnv('IPFS_HASH', process.env.IPFS_HASH);
|
|
189
|
+
if (expectedCids && (process.env.IPFS_HASH !== expectedCids.image
|
|
190
|
+
|| process.env.IPFS_DOCKER_COMPOSE_HASH !== expectedCids.compose)) {
|
|
191
|
+
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}).`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
122
196
|
['docker-compose.yml', 'docker-compose-final.yml'].forEach(file => {
|
|
123
197
|
const template = `${file}.tmpl`;
|
|
124
198
|
if (!fs.existsSync(template)) {
|
|
@@ -238,17 +312,19 @@ const main = async () => {
|
|
|
238
312
|
// Where the session goes depends on who provisions the securelock. On a
|
|
239
313
|
// CAS-attested testnet it is registered ON-CHAIN in the ethernity-cas
|
|
240
314
|
// SessionRegistry, which the validator set reads and which accepts no
|
|
241
|
-
// POST
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
315
|
+
// POST. That happens once the image is registered, below: the
|
|
316
|
+
// SessionRegistry takes a securelock session only from the wallet that
|
|
317
|
+
// owns the image name. The publish then waits until the validators serve
|
|
318
|
+
// it, because the public-key harvest provisions the enclave from them. On
|
|
319
|
+
// mainnet it is registered with the Scontain CAS. A self-signing testnet
|
|
320
|
+
// has none: a CAS-issued certificate would not match the key the enclave
|
|
321
|
+
// derives.
|
|
322
|
+
const registerSessionOnChain = async () => {
|
|
248
323
|
const registered = await sessionRegistry.register(
|
|
249
324
|
casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
|
|
250
325
|
process.env.PRIVATE_KEY, fs.readFileSync('etny-securelock-test.yaml'),
|
|
251
|
-
|
|
326
|
+
ipfsApi, '');
|
|
327
|
+
if (localKubo) await localKubo.provide(registered.cid);
|
|
252
328
|
if (registered.registered) {
|
|
253
329
|
console.log(`\t✔ Session ${registered.name} registered on-chain (body ${registered.cid})`);
|
|
254
330
|
await sessionRegistry.waitVisible(
|
|
@@ -256,7 +332,10 @@ const main = async () => {
|
|
|
256
332
|
} else {
|
|
257
333
|
console.log(`\t✔ Session ${registered.name} already registered on-chain with this body (${registered.hash})`);
|
|
258
334
|
}
|
|
259
|
-
}
|
|
335
|
+
};
|
|
336
|
+
if (!cas) {
|
|
337
|
+
console.log(`\t✔ ${process.env.BLOCKCHAIN_NETWORK}: no CAS session; the securelock self-signs from MR_ENCLAVE`);
|
|
338
|
+
} else if (!casTestnet) {
|
|
260
339
|
// don't generate new keys if PREDECESSOR_HASH_SECURELOCK is not empty and the key.pem and cert.pem files exist
|
|
261
340
|
if (PREDECESSOR_HASH_SECURELOCK !== 'EMPTY' && fs.existsSync('key.pem') && fs.existsSync('cert.pem')) {
|
|
262
341
|
console.log("Skipping key pair generation and certificate creation.");
|
|
@@ -406,15 +485,20 @@ const main = async () => {
|
|
|
406
485
|
console.log("# Update docker-compose files");
|
|
407
486
|
|
|
408
487
|
// On a CAS-attested testnet the compose names the CAS both enclaves are
|
|
409
|
-
// provisioned from
|
|
410
|
-
//
|
|
411
|
-
//
|
|
488
|
+
// provisioned from and the trustedzone session the ImageRegistry records
|
|
489
|
+
// for this template. docker-compose.yml, which harvests the certificate
|
|
490
|
+
// here, names a validator that answered (or ECLD_CAS_ADDR); the published
|
|
491
|
+
// docker-compose-final.yml names the first validator in registry order,
|
|
492
|
+
// so that a rerun of this publish renders the same compose. The node and
|
|
493
|
+
// the extraction service resolve a CAS again before they run it.
|
|
412
494
|
let casAddr = null;
|
|
495
|
+
let publishedCasAddr = null;
|
|
413
496
|
let trustedZoneSession = null;
|
|
414
497
|
if (casTestnet) {
|
|
415
498
|
casAddr = await casResolver.casAddressFor(casChain.rpcUrl, casConfig.VALIDATOR_REGISTRY[templateName]);
|
|
416
|
-
|
|
417
|
-
|
|
499
|
+
publishedCasAddr = await casResolver.publishedCasAddress(casChain.rpcUrl, casConfig.VALIDATOR_REGISTRY[templateName]);
|
|
500
|
+
console.log(`\t✔ CAS for this network: ${casAddr} (the published compose names ${publishedCasAddr})`);
|
|
501
|
+
trustedZoneSession = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "getTrustedZoneSession"`).toString().trim();
|
|
418
502
|
if (!trustedZoneSession) {
|
|
419
503
|
console.error(`ERROR! The ImageRegistry records no trustedzone session for ${templateName}.`);
|
|
420
504
|
process.exit(1);
|
|
@@ -454,7 +538,8 @@ const main = async () => {
|
|
|
454
538
|
.replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK)
|
|
455
539
|
.replace(/__ENCLAVE_NAME_TRUSTEDZONE__/g, ENCLAVE_NAME_TRUSTEDZONE);
|
|
456
540
|
if (casTestnet) {
|
|
457
|
-
|
|
541
|
+
const fileCasAddr = file === 'docker-compose-final.yml' ? publishedCasAddr : casAddr;
|
|
542
|
+
updatedContent = updatedContent.replace(/SCONE_CAS_ADDR=scone-cas\.cf/g, `SCONE_CAS_ADDR=${fileCasAddr}`);
|
|
458
543
|
}
|
|
459
544
|
|
|
460
545
|
fs.writeFileSync(file, updatedContent, 'utf8');
|
|
@@ -478,7 +563,30 @@ const main = async () => {
|
|
|
478
563
|
// 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();
|
|
479
564
|
|
|
480
565
|
|
|
481
|
-
//
|
|
566
|
+
// The image and its compose go out before the certificate is extracted,
|
|
567
|
+
// whichever way it is: the extraction service fetches them by these CIDs.
|
|
568
|
+
// On a two-step registry (ECImageRegistryV2 and later) the wallet records
|
|
569
|
+
// the image first (registerImage, with the publish's node), under the
|
|
570
|
+
// CIDs the upload gives, hashed before anything is stored. On
|
|
571
|
+
// ECImageRegistryV3 that binds the image name to this wallet before the
|
|
572
|
+
// upload, the session registration or anything else discloses it. The
|
|
573
|
+
// bootnode's mirror then pins the image and the extraction service queues
|
|
574
|
+
// it from the chain; the certificate is written below by the same wallet
|
|
575
|
+
// (setImageCert). A V1 registry takes both in one addImage call.
|
|
576
|
+
const registryV2 = execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "v3" "isV2"`, { env: { ...process.env, PROJECT_NAME: securelock } }).toString().trim() === 'true';
|
|
577
|
+
let ipfsPeer = '';
|
|
578
|
+
if (registryV2) {
|
|
579
|
+
hashForIpfs();
|
|
580
|
+
if (localKubo) ipfsPeer = await localKubo.peerMultiaddr();
|
|
581
|
+
execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "v3" "registerImage"`, { stdio: "inherit", env: { ...process.env, PROJECT_NAME: securelock, IPFS_PEER: ipfsPeer } });
|
|
582
|
+
}
|
|
583
|
+
uploadToIpfs();
|
|
584
|
+
const registryEnv = { ...process.env, PROJECT_NAME: securelock, IPFS_PEER: ipfsPeer };
|
|
585
|
+
if (registryV2 && localKubo) {
|
|
586
|
+
await localKubo.provide(process.env.IPFS_HASH);
|
|
587
|
+
await localKubo.provide(process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
588
|
+
}
|
|
589
|
+
if (casTestnet) await registerSessionOnChain();
|
|
482
590
|
|
|
483
591
|
if (fs.existsSync('certificate.securelock.crt')) {
|
|
484
592
|
// delete it
|
|
@@ -487,7 +595,7 @@ const main = async () => {
|
|
|
487
595
|
|
|
488
596
|
let PUBLIC_KEY_SECURELOCK_RES = '';
|
|
489
597
|
try {
|
|
490
|
-
const output = execSync(`docker-compose run etny-securelock`, { cwd: runDir }).toString();
|
|
598
|
+
const output = execSync(`docker-compose run --rm etny-securelock`, { cwd: runDir }).toString();
|
|
491
599
|
console.log("Output of docker-compose run etny-securelock:");
|
|
492
600
|
// Extract the WHOLE multi-line PEM certificate block from the enclave
|
|
493
601
|
// output. A previous version grabbed only the single line containing
|
|
@@ -520,43 +628,6 @@ const main = async () => {
|
|
|
520
628
|
process.exit(0);
|
|
521
629
|
} else {
|
|
522
630
|
console.log("\nGenerating certificates using the Ethernity Cloud signing service...\n");
|
|
523
|
-
console.log("**** Started ipfs initial pining ****");
|
|
524
|
-
if (fs.existsSync('IPFS_HASH.ipfs')) {
|
|
525
|
-
fs.unlinkSync('IPFS_HASH.ipfs');
|
|
526
|
-
}
|
|
527
|
-
if (fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
|
|
528
|
-
fs.unlinkSync('IPFS_DOCKER_COMPOSE_HASH.ipfs');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
console.log('Upload docker-compose-final.yml to IPFS');
|
|
533
|
-
const dockerHash = execSync(`node ../ipfs.mjs --host "${process.env.IPFS_ENDPOINT}" --action upload --filePath docker-compose-final.yml`, { stdio: "inherit" });
|
|
534
|
-
if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
|
|
535
|
-
console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
|
|
536
|
-
process.exit(1);
|
|
537
|
-
}
|
|
538
|
-
process.env.IPFS_DOCKER_COMPOSE_HASH = fs.readFileSync('IPFS_DOCKER_COMPOSE_HASH.ipfs', 'utf8').trim();
|
|
539
|
-
console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
540
|
-
writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
541
|
-
console.log()
|
|
542
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
543
|
-
|
|
544
|
-
console.log('Upload docker registry to IPFS');
|
|
545
|
-
const repositoryHash = execSync(`node ../ipfs.mjs --host "${process.env.IPFS_ENDPOINT}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
|
|
546
|
-
if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
|
|
547
|
-
console.error("Error: Could not upload registry to IPFS, please try again!");
|
|
548
|
-
process.exit(1);
|
|
549
|
-
}
|
|
550
|
-
process.env.IPFS_HASH = fs.readFileSync(`./IPFS_HASH.ipfs`, 'utf8').trim();
|
|
551
|
-
console.log("IPFS_HASH: ", process.env.IPFS_HASH);
|
|
552
|
-
writeEnv('IPFS_HASH', process.env.IPFS_HASH);
|
|
553
|
-
|
|
554
|
-
console.log()
|
|
555
|
-
|
|
556
|
-
console.log("**** Finished ipfs initial pining ****");
|
|
557
|
-
console.log()
|
|
558
|
-
console.log()
|
|
559
|
-
|
|
560
631
|
console.log(`ENCLAVE_NAME_SECURELOCK: ${ENCLAVE_NAME_SECURELOCK}`);
|
|
561
632
|
execSync(`node ./public_key_service.js --enclave_name "${securelock}" --protocol_version "v3" --network "${process.env.BLOCKCHAIN_NETWORK}" --template_version "${process.env.VERSION}"`, { stdio: 'inherit' });
|
|
562
633
|
PUBLIC_KEY_SECURELOCK_RES = fs.readFileSync('PUBLIC_KEY.txt', 'utf8').trim();
|
|
@@ -591,7 +662,7 @@ const main = async () => {
|
|
|
591
662
|
}
|
|
592
663
|
|
|
593
664
|
// const scriptPath = path.resolve(__dirname, '/image_registry.js');
|
|
594
|
-
const trustedZoneCert = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "
|
|
665
|
+
const trustedZoneCert = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "getTrustedZoneCert"`).toString().trim();
|
|
595
666
|
|
|
596
667
|
console.log("trustedZoneCert: ", trustedZoneCert);
|
|
597
668
|
|
|
@@ -608,41 +679,42 @@ const main = async () => {
|
|
|
608
679
|
console.log("Listing certificate PUBLIC_KEY_TRUSTEDZONE:");
|
|
609
680
|
console.log(fs.readFileSync('certificate.trustedzone.crt', 'utf8'));
|
|
610
681
|
|
|
682
|
+
// image_registry.js reads the securelock certificate from registry/.
|
|
611
683
|
fs.copyFileSync('certificate.securelock.crt', `${registryPath}/certificate.securelock.crt`);
|
|
612
684
|
fs.copyFileSync('certificate.trustedzone.crt', `${registryPath}/certificate.trustedzone.crt`);
|
|
613
685
|
|
|
614
|
-
if (
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (
|
|
635
|
-
|
|
686
|
+
if (registryV2) {
|
|
687
|
+
// The registered hash is the tree uploaded before the extraction.
|
|
688
|
+
process.chdir(currentDir);
|
|
689
|
+
console.log(`Registering the certificate of SECURELOCK ${securelock} in the IMAGE REGISTRY smart contract...`);
|
|
690
|
+
execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "v3" "setImageCert"`, { stdio: "inherit", env: registryEnv });
|
|
691
|
+
} else {
|
|
692
|
+
// A V1 registry gets the tree that carries the certificate files,
|
|
693
|
+
// uploaded again, with the certificate, in one addImage call.
|
|
694
|
+
if (fs.existsSync('IPFS_HASH.ipfs')) {
|
|
695
|
+
fs.unlinkSync('IPFS_HASH.ipfs');
|
|
696
|
+
}
|
|
697
|
+
console.log('Upload docker registry to IPFS');
|
|
698
|
+
execSync(`node ../ipfs.mjs --host "${ipfsApi}" --action upload --folderPath ${registryPath}`, { stdio: "inherit" });
|
|
699
|
+
if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
|
|
700
|
+
console.error("Error: Could not upload registry to IPFS, please try again!");
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
process.env.IPFS_HASH = fs.readFileSync(`./IPFS_HASH.ipfs`, 'utf8').trim();
|
|
704
|
+
console.log("IPFS_HASH: ", process.env.IPFS_HASH);
|
|
705
|
+
writeEnv('IPFS_HASH', process.env.IPFS_HASH);
|
|
706
|
+
if (localKubo) await localKubo.provide(process.env.IPFS_HASH);
|
|
707
|
+
process.chdir(currentDir);
|
|
708
|
+
console.log(`Adding certificates for SECURELOCK ${securelock} into IMAGE REGISTRY smart contract...`);
|
|
709
|
+
// image_registry.js registers the image under PROJECT_NAME and prints
|
|
710
|
+
// why when it cannot.
|
|
711
|
+
registryEnv.IPFS_HASH = process.env.IPFS_HASH;
|
|
712
|
+
try {
|
|
713
|
+
execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "${securelock}" "${process.env.VERSION}" "registerSecureLockImage"`, { stdio: "inherit", env: registryEnv });
|
|
714
|
+
} catch (error) {
|
|
715
|
+
console.error("Error: Could not add the certificates of the SECURELOCK to the IMAGE REGISTRY smart contract");
|
|
716
|
+
process.exit(1);
|
|
636
717
|
}
|
|
637
|
-
existing = true;
|
|
638
|
-
} catch (error) {
|
|
639
|
-
// console.error("Error: Could not add certificates for SECURELOCK into IMAGE REGISTRY smart contract");
|
|
640
|
-
// console.error(error);
|
|
641
|
-
// process.exit(1);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
if (!existing) {
|
|
645
|
-
const res = execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "" "" "" "registerSecureLockImage"`, { stdio: "inherit", env: registryEnv });
|
|
646
718
|
}
|
|
647
719
|
// The on-chain session points at the image it admits, so a validator can
|
|
648
720
|
// pin the image beside the body it serves.
|
|
@@ -1,58 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
import './App.css';
|
|
2
2
|
import EthernityCloudRunner from "@ethernity-cloud/runner";
|
|
3
|
-
import {
|
|
4
|
-
import Web3 from 'web3';
|
|
3
|
+
import { ECStatus } from "@ethernity-cloud/runner/enums";
|
|
5
4
|
|
|
5
|
+
// Filled in from the project's .env by src/preStart.mjs on `npm start`: the
|
|
6
|
+
// securelock this dApp is published as, the trustedzone it was built against,
|
|
7
|
+
// the network's token address and chain id, the -unsafe network when the dApp
|
|
8
|
+
// runs without a CAS (null otherwise), and the IPFS endpoint.
|
|
6
9
|
const PROJECT_NAME = "";
|
|
10
|
+
const TRUSTED_ZONE_IMAGE = "";
|
|
11
|
+
const NETWORK_ADDRESS = "";
|
|
12
|
+
const CHAIN_ID = null;
|
|
13
|
+
const UNSAFE_NETWORK = null;
|
|
7
14
|
const IPFS_ENDPOINT = "";
|
|
8
15
|
|
|
16
|
+
// A call into src/serverless/backend.py; its return value is the task result.
|
|
9
17
|
const code = `hello("World")`;
|
|
10
18
|
|
|
11
19
|
function App() {
|
|
12
20
|
const executeTask = async () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
console.log(PROJECT_NAME)
|
|
18
|
-
const onTaskProgress = (e) => {
|
|
19
|
-
if (e.detail.status === ECStatus.ERROR) {
|
|
20
|
-
console.error(e.detail.message);
|
|
21
|
-
} else {
|
|
22
|
-
console.log(e.detail.message);
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const onTaskCompleted = (e) => {
|
|
27
|
-
console.log(`Task Result: ${e.detail.message.result}`);
|
|
28
|
-
// display the result in page below the buttons
|
|
29
|
-
const result = document.createElement("p");
|
|
30
|
-
result.innerHTML = `Task Result: ${e.detail.message.result}`;
|
|
31
|
-
document.body.appendChild(result);
|
|
21
|
+
// The browser wallet (MetaMask) signs and pays for the order.
|
|
22
|
+
const runner = new EthernityCloudRunner(NETWORK_ADDRESS, {}, CHAIN_ID || undefined);
|
|
23
|
+
if (UNSAFE_NETWORK) {
|
|
24
|
+
await runner.setNetwork(...UNSAFE_NETWORK);
|
|
32
25
|
}
|
|
26
|
+
runner.initializeStorage(IPFS_ENDPOINT);
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
runner.addEventListener(
|
|
28
|
+
// Events are named by task status; detail is { message, status, progress }.
|
|
29
|
+
runner.addEventListener(ECStatus.DEFAULT, (e) => {
|
|
30
|
+
console.log(`[${e.detail.progress}] ${e.detail.message}`);
|
|
31
|
+
});
|
|
32
|
+
runner.addEventListener(ECStatus.ERROR, (e) => {
|
|
33
|
+
console.error(e.detail.message);
|
|
34
|
+
});
|
|
35
|
+
runner.addEventListener(ECStatus.SUCCESS, async () => {
|
|
36
|
+
const result = await runner.getResult();
|
|
37
|
+
console.log(`Task Result: ${result}`);
|
|
38
|
+
// display the result in page below the buttons
|
|
39
|
+
const line = document.createElement("p");
|
|
40
|
+
line.textContent = `Task Result: ${result}`;
|
|
41
|
+
document.body.appendChild(line);
|
|
42
|
+
});
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'0xd58f5C1834279ABD601df85b3E4b2323aDD4E75e',
|
|
40
|
-
{ taskPrice: 6, cpu: 1, memory: 1, storage: 10, bandwidth: 1, duration: 1, validators: 1 });
|
|
44
|
+
const resources = { taskPrice: 10, cpu: 1, memory: 1, storage: 10, bandwidth: 1, duration: 1, validators: 1 };
|
|
45
|
+
await runner.run(resources, PROJECT_NAME, code, '', TRUSTED_ZONE_IMAGE);
|
|
41
46
|
};
|
|
47
|
+
|
|
42
48
|
const connectWallet = async () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
};
|
|
49
|
+
if (!window.ethereum) {
|
|
50
|
+
console.log('Please install MetaMask!');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await window.ethereum.request({ method: 'eth_requestAccounts' });
|
|
55
|
+
console.log("Wallet connected");
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("User denied account access");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
56
60
|
|
|
57
61
|
return (
|
|
58
62
|
<div className="container">
|
|
@@ -61,4 +65,4 @@ function App() {
|
|
|
61
65
|
</div>
|
|
62
66
|
);
|
|
63
67
|
}
|
|
64
|
-
export default App;
|
|
68
|
+
export default App;
|
package/pynithy/src/preStart.mjs
CHANGED
|
@@ -1,57 +1,47 @@
|
|
|
1
|
+
// Runs before `react-scripts start` (the project's `npm start`): fills the
|
|
2
|
+
// constants at the top of src/ec_helloworld_example.js from the project's
|
|
3
|
+
// .env, the way ecld-run reads the same values. PROJECT_NAME and
|
|
4
|
+
// TRUSTED_ZONE_IMAGE take their -unsafe form on an -unsafe network; the
|
|
5
|
+
// network's token address, chain id and -unsafe (network, type) come from the
|
|
6
|
+
// runner's own enums through the SDK's network table.
|
|
1
7
|
import fs from 'fs';
|
|
8
|
+
import { createRequire } from 'module';
|
|
2
9
|
import dotenv from 'dotenv';
|
|
3
|
-
|
|
10
|
+
import * as enums from '@ethernity-cloud/runner/enums.js';
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
'ecld-
|
|
12
|
+
dotenv.config();
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const casConfig = require('@ethernity-cloud/sdk-js/cas/config.js');
|
|
15
|
+
const { networkFor } = require('@ethernity-cloud/sdk-js/network.js');
|
|
16
|
+
|
|
17
|
+
const { PROJECT_NAME, TRUSTED_ZONE_IMAGE, BLOCKCHAIN_NETWORK, IPFS_ENDPOINT } = process.env;
|
|
18
|
+
if (!PROJECT_NAME || !BLOCKCHAIN_NETWORK) {
|
|
19
|
+
console.error('preStart: PROJECT_NAME and BLOCKCHAIN_NETWORK are not set in .env; run `npm run ecld-init` first.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const { network, known } = networkFor(enums, BLOCKCHAIN_NETWORK);
|
|
23
|
+
if (!network) {
|
|
24
|
+
console.error(`preStart: unknown BLOCKCHAIN_NETWORK '${BLOCKCHAIN_NETWORK}' in .env. Known: ${known.join(', ')}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const constants = {
|
|
29
|
+
PROJECT_NAME: casConfig.nameOnNetwork(PROJECT_NAME, BLOCKCHAIN_NETWORK),
|
|
30
|
+
TRUSTED_ZONE_IMAGE: casConfig.nameOnNetwork(TRUSTED_ZONE_IMAGE, BLOCKCHAIN_NETWORK) || '',
|
|
31
|
+
NETWORK_ADDRESS: network.address,
|
|
32
|
+
CHAIN_ID: network.chainId || null,
|
|
33
|
+
UNSAFE_NETWORK: network.unsafe || null,
|
|
34
|
+
IPFS_ENDPOINT: IPFS_ENDPOINT || 'https://ipfs.ethernity.cloud',
|
|
13
35
|
};
|
|
14
36
|
|
|
15
37
|
const filePath = 'src/ec_helloworld_example.js';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const imageRegistryPath = 'node_modules/@ethernity-cloud/runner/contract/operation/imageRegistryContract.js';
|
|
28
|
-
const imageRegistryContent = fs.readFileSync(imageRegistryPath, 'utf8');
|
|
29
|
-
|
|
30
|
-
// Log the original content for debugging
|
|
31
|
-
// console.log('Original imageRegistryContent:', imageRegistryContent);
|
|
32
|
-
|
|
33
|
-
// Replace the content in imageRegistryContract.js
|
|
34
|
-
// const updatedImageRegistryContent = imageRegistryContent.replace(/getLatestTrustedZoneImageCertPublicKey\([^)]*\);/, "getLatestTrustedZoneImageCertPublicKey('" + process.env.ENCLAVE_NAME_TRUSTEDZONE + "', 'v3')").replace(/getLatestImageVersionPublicKey\([^)]*\)/, "getLatestImageVersionPublicKey(imageName, '" + process.env.VERSION + "')");
|
|
35
|
-
const updatedImageRegistryContent = imageRegistryContent.replace(
|
|
36
|
-
/async getEnclaveDetailsV3\(.*?\{[\s\S]*?\}\s*\}/,
|
|
37
|
-
`async getEnclaveDetailsV3(imageName, version) {
|
|
38
|
-
try {
|
|
39
|
-
const trustedZonePublicKey = (await this.contract.getLatestTrustedZoneImageCertPublicKey('${process.env.ENCLAVE_NAME_TRUSTEDZONE}', 'v3'));
|
|
40
|
-
const imageDetails = await this.contract.getLatestImageVersionPublicKey(imageName, '${process.env.VERSION}');
|
|
41
|
-
return [imageDetails[0], trustedZonePublicKey[1], imageDetails[2]];
|
|
42
|
-
} catch (e) {
|
|
43
|
-
console.log(e);
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}`
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
fs.writeFileSync(imageRegistryPath, updatedImageRegistryContent, 'utf8');
|
|
51
|
-
|
|
52
|
-
const runnerPath = 'node_modules/@ethernity-cloud/runner/runner.js';
|
|
53
|
-
const runnerContent = fs.readFileSync(runnerPath, 'utf8');
|
|
54
|
-
|
|
55
|
-
const updatedRunnerContent = runnerContent.replace(/this.#enclaveImageIPFSHash}:.*?:/, "this.#enclaveImageIPFSHash}:"+process.env.ENCLAVE_NAME_TRUSTEDZONE+":").replace(/new ImageRegistryContract\([^)]*\);/, "new ImageRegistryContract(this.#networkAddress, '"+process.env.ENCLAVE_NAME_TRUSTEDZONE+"');");
|
|
56
|
-
|
|
57
|
-
fs.writeFileSync(runnerPath, updatedRunnerContent, 'utf8');
|
|
38
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
39
|
+
for (const [name, value] of Object.entries(constants)) {
|
|
40
|
+
const line = new RegExp(`^const ${name} = .*;$`, 'm');
|
|
41
|
+
if (!line.test(content)) {
|
|
42
|
+
console.error(`preStart: ${filePath} has no 'const ${name} = ...;' line to fill.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
content = content.replace(line, `const ${name} = ${JSON.stringify(value)};`);
|
|
46
|
+
}
|
|
47
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
package/revert.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// What the node answered when a contract call failed.
|
|
2
|
+
//
|
|
3
|
+
// ethers 5 reads revert data only from a geth-style JSON-RPC error, which it
|
|
4
|
+
// turns into the call's result and decodes into a CALL_EXCEPTION with no
|
|
5
|
+
// node error nested. The bloxberg node (Parity 2.7) answers eth_call with
|
|
6
|
+
// {"data": "Reverted 0x..."}, which ethers reports as a CALL_EXCEPTION with
|
|
7
|
+
// "missing revert data" and the node's error nested, and its eth_estimateGas
|
|
8
|
+
// error carries no data at all. ethers reports an eth_call that timed out as
|
|
9
|
+
// the same CALL_EXCEPTION, with the timeout nested. A refusal is therefore
|
|
10
|
+
// read from a simulated call (callStatic), out of the node's own error.
|
|
11
|
+
const { ethers } = require('ethers');
|
|
12
|
+
|
|
13
|
+
const ERROR_STRING = '0x08c379a0'; // Error(string)
|
|
14
|
+
const PANIC = '0x4e487b71'; // Panic(uint256)
|
|
15
|
+
|
|
16
|
+
function bodyError(body) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(body).error;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The JSON-RPC errors the node answered with: the errors nested in an ethers
|
|
25
|
+
// error, and in the response bodies it carries, that hold the node's numeric
|
|
26
|
+
// error code. A call the node never answered has none.
|
|
27
|
+
function nodeErrors(e) {
|
|
28
|
+
const found = [];
|
|
29
|
+
for (let err = e, depth = 0; err && depth < 5; err = err.error, depth++) {
|
|
30
|
+
for (const candidate of [err, bodyError(err.body)]) {
|
|
31
|
+
if (candidate && typeof candidate.code === 'number') found.push(candidate);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Whether the node answered the call with a revert, rather than failing to
|
|
38
|
+
// answer it (a timeout, an unreachable node, a rate limit).
|
|
39
|
+
function isRevert(e) {
|
|
40
|
+
if (e.code !== ethers.errors.CALL_EXCEPTION) return false;
|
|
41
|
+
if (!e.error) return true;
|
|
42
|
+
return nodeErrors(e).some((err) => (typeof err.data === 'string' && err.data.startsWith('Reverted'))
|
|
43
|
+
|| /revert/i.test(err.message || ''));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The revert data the node returned, as 0x-prefixed hex with at least a
|
|
47
|
+
// selector, or null.
|
|
48
|
+
function revertData(e) {
|
|
49
|
+
for (const err of nodeErrors(e)) {
|
|
50
|
+
if (typeof err.data !== 'string') continue;
|
|
51
|
+
const m = err.data.match(/^(?:Reverted )?(0x(?:[0-9a-fA-F]{2})*)$/);
|
|
52
|
+
if (m && m[1].length >= 10) return m[1];
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The refusal as the contract states it: a require message, or a custom
|
|
58
|
+
// error of `iface` with its arguments. Falls back to the error's own message
|
|
59
|
+
// when the node returned no revert data.
|
|
60
|
+
function revertReason(e, iface) {
|
|
61
|
+
if (e.errorName === 'Error' && e.reason) return e.reason;
|
|
62
|
+
if (e.errorName) return `${e.errorName}(${(e.errorArgs || []).map(String).join(', ')})`;
|
|
63
|
+
const data = revertData(e);
|
|
64
|
+
if (!data) return e.reason || e.message;
|
|
65
|
+
const args = `0x${data.slice(10)}`;
|
|
66
|
+
try {
|
|
67
|
+
if (data.startsWith(ERROR_STRING)) return ethers.utils.defaultAbiCoder.decode(['string'], args)[0];
|
|
68
|
+
if (data.startsWith(PANIC)) return `Panic(${ethers.utils.defaultAbiCoder.decode(['uint256'], args)[0]})`;
|
|
69
|
+
const parsed = iface.parseError(data);
|
|
70
|
+
return `${parsed.name}(${parsed.args.map(String).join(', ')})`;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return `reverted with data ${data}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { isRevert, revertData, revertReason };
|