@ethernity-cloud/sdk-js 1.7.10 → 1.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,20 +59,26 @@ The sdk has been tested on the following operating systems:
59
59
  validator set (SessionRegistry `0xfA70B5Df...`, ValidatorRegistry
60
60
  `0xbE3759f3...`).
61
61
  - LiteForge Unsafe - the unsafe network beside LiteForge (see below).
62
+ - Ethereum:
63
+ - Sepolia - CAS-attested the same way, by Sepolia's own ethernity-cas
64
+ validator set (SessionRegistry `0xDbB249E1...`, ValidatorRegistry
65
+ `0x8ad24b3F...`), on the PoX family of 2026-10-14.
66
+ - Sepolia Unsafe - the unsafe network beside Sepolia (see below).
62
67
  - Polyhon:
63
68
  - Amoy Testnet - to be provided during the following updates
64
69
  - Mainnet - to be provided during the following updates
65
70
 
66
- The remaining testnets (Amoy, IoTeX, Sepolia) have no ethernity-cas
71
+ The remaining testnets (Amoy, IoTeX) have no ethernity-cas
67
72
  deployment: their enclaves are debug-signed and sign their own certificates
68
73
  from their measurement, and `ecld-publish` registers no CAS session for them.
69
74
 
70
75
  ### The testnets and their unsafe networks
71
- `Bloxberg Testnet` and `LitVM LiteForge` run your securelock in an enclave
76
+ `Bloxberg Testnet`, `LitVM LiteForge` and `Ethereum Sepolia` run your securelock in an enclave
72
77
  attested through that chain's ethernity-cas validator set, on nodes whose
73
78
  platform the CAS can attest (DCAP). Each has an unsafe network beside it,
74
- `Bloxberg Testnet Unsafe` (`Bloxberg_Testnet_Unsafe`) and `LitVM LiteForge
75
- Unsafe` (`LitVM_LiteForge_Unsafe`): the same chain and contracts, for hardware
79
+ `Bloxberg Testnet Unsafe` (`Bloxberg_Testnet_Unsafe`), `LitVM LiteForge
80
+ Unsafe` (`LitVM_LiteForge_Unsafe`) and `Ethereum Sepolia Unsafe`
81
+ (`Ethereum_Sepolia_Unsafe`): the same chain and contracts, for hardware
76
82
  SGX platforms the CAS cannot attest (EPID-only, SGX1). No CAS and no LAS: the
77
83
  enclaves are debug-signed and sign their own certificates from their
78
84
  measurement, so a result proves which image ran but not that an enclave ran
@@ -82,7 +88,8 @@ always says so in the network it names. A dApp publishes one securelock per
82
88
  network from two project directories with the same project name: the unsafe
83
89
  one is registered as `<project>-unsafe` and runs on the
84
90
  `etny-<nodenithy|pynithy>-testnet-unsafe`
85
- (`ecld-<nodenithy|pynithy>-litvm-testnet-unsafe`) trustedzone. Both dApp types
91
+ (`ecld-<nodenithy|pynithy>-litvm-testnet-unsafe`,
92
+ `ecld-<nodenithy|pynithy>-ethereum-sepolia-unsafe`) trustedzone. Both dApp types
86
93
  build for both networks. Mainnet has no unsafe network.
87
94
 
88
95
  ### Pynithy dApps
@@ -135,7 +142,11 @@ sets the enclave heap (default `1024M`).
135
142
  bootnode and reports its progress) and writes it from the same wallet
136
143
  (`setImageCert`). Keep the publish running until it prints the registration:
137
144
  your node is the image's first source. `ECLD_KUBO_SWARM_PORT` (default
138
- 4001) is the port your node listens on when it is free.
145
+ 4001) is the port your node listens on when it is free. `ECLD_ASSUME_YES=1`
146
+ accepts the extraction-service prompt without asking, for a publish run
147
+ from a script. On a two-step registry the publish waits up to three
148
+ minutes for the service to queue the image from the chain event before
149
+ posting the hash itself.
139
150
 
140
151
  - **Test (local, no chain)**: To run your backend locally with the enclave's own
141
152
  executor — no SGX, no gas, instant — run:
@@ -175,9 +186,12 @@ sets the enclave heap (default `1024M`).
175
186
  intake once the request is on chain; `--ipfs <url>` writes and reads through
176
187
  that IPFS RPC API instead, with `ECLD_IPFS_TOKEN` (or `IPFS_TOKEN`) as its
177
188
  Authorization header, and a URL naming the public API is refused without one.
189
+ The chain calls of a run go through the network's own RPC (publicnode's Sepolia
190
+ endpoint, LitVM's Caldera endpoint, IoTeX's babel API); the runner's defaults
191
+ cover the other networks.
178
192
  The intake serves the networks the bootnode's agent runs, today the bloxberg
179
- testnet and LitVM LiteForge, each with its unsafe twin: a dApp on any other
180
- network (Bloxberg mainnet, Polygon, IoTeX, Sepolia) needs an IPFS API of its
193
+ testnet, LitVM LiteForge and Ethereum Sepolia, each with its unsafe twin: a dApp on any other
194
+ network (Bloxberg mainnet, Polygon, IoTeX) needs an IPFS API of its
181
195
  own (`IPFS_ENDPOINT` for the Hello World template, `--ipfs <url>` for `ecld-run`).
182
196
  Exit code 0 on a `SUCCESS` task result, 1 otherwise. `--unsafe` runs the
183
197
  network's unsafe variant; on an unsafe network the securelock and
package/cas/config.js CHANGED
@@ -18,6 +18,8 @@ const VALIDATOR_REGISTRY = {
18
18
  'etny-nodenithy-testnet': '0x0014aB102FEa6b02e3D793FF4b9a0Af309fB4E14',
19
19
  'ecld-pynithy-litvm-testnet': '0xbE3759f327e8643fC5A1717fb2072A0dAcBAD29E',
20
20
  'ecld-nodenithy-litvm-testnet': '0xbE3759f327e8643fC5A1717fb2072A0dAcBAD29E',
21
+ 'ecld-pynithy-ethereum-sepolia': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
22
+ 'ecld-nodenithy-ethereum-sepolia': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
21
23
  };
22
24
 
23
25
  const SESSION_REGISTRY = {
@@ -25,14 +27,20 @@ const SESSION_REGISTRY = {
25
27
  'etny-nodenithy-testnet': '0x444429681fCb26470Cb12B5436E48402cF7478A3',
26
28
  'ecld-pynithy-litvm-testnet': '0xfA70B5Df7C773A21164cAc5413704117e4dEaB76',
27
29
  'ecld-nodenithy-litvm-testnet': '0xfA70B5Df7C773A21164cAc5413704117e4dEaB76',
30
+ // Bound to Sepolia's ECImageRegistryV3 of 2026-10-14.
31
+ 'ecld-pynithy-ethereum-sepolia': '0xDbB249E108d19fC1691483Bf897bA8BA0dA99c06',
32
+ 'ecld-nodenithy-ethereum-sepolia': '0xDbB249E108d19fC1691483Bf897bA8BA0dA99c06',
28
33
  };
29
34
 
30
- // The chain the registries above live on.
35
+ // The chain the registries above live on. Sepolia's RPC is Tenderly's gateway:
36
+ // publicnode caps eth_getLogs at 50,000 blocks and prunes older ranges.
31
37
  const CHAIN = {
32
38
  'etny-pynithy-testnet': { rpcUrl: 'https://bloxberg.ethernity.cloud', chainId: 8995 },
33
39
  'etny-nodenithy-testnet': { rpcUrl: 'https://bloxberg.ethernity.cloud', chainId: 8995 },
34
40
  'ecld-pynithy-litvm-testnet': { rpcUrl: 'https://liteforge.rpc.caldera.xyz/infra-partner-http', chainId: 4441 },
35
41
  'ecld-nodenithy-litvm-testnet': { rpcUrl: 'https://liteforge.rpc.caldera.xyz/infra-partner-http', chainId: 4441 },
42
+ 'ecld-pynithy-ethereum-sepolia': { rpcUrl: 'https://sepolia.gateway.tenderly.co', chainId: 11155111 },
43
+ 'ecld-nodenithy-ethereum-sepolia': { rpcUrl: 'https://sepolia.gateway.tenderly.co', chainId: 11155111 },
36
44
  };
37
45
 
38
46
  // Whether the securelock takes its certificate from a CAS session: mainnet
@@ -55,6 +63,7 @@ const isMainnetNetwork = (network) =>
55
63
  const UNSAFE_NETWORKS = {
56
64
  Bloxberg_Testnet_Unsafe: 'Bloxberg_Testnet',
57
65
  LitVM_LiteForge_Unsafe: 'LitVM_LiteForge',
66
+ Ethereum_Sepolia_Unsafe: 'Ethereum_Sepolia',
58
67
  };
59
68
 
60
69
  // Case-insensitive: ecld-init writes Bloxberg_Testnet_Unsafe, a --network
@@ -62,6 +71,26 @@ const UNSAFE_NETWORKS = {
62
71
  const isUnsafeNetwork = (network) =>
63
72
  Object.keys(UNSAFE_NETWORKS).some((name) => name.toUpperCase() === String(network || '').toUpperCase());
64
73
 
74
+ // The testnets, by BLOCKCHAIN_NETWORK: every network ecld-init offers but the
75
+ // two mainnets, the -unsafe ones included. A session relaxes attestation on
76
+ // these alone (sessionAttestation).
77
+ const TESTNETS = [
78
+ 'Bloxberg_Testnet', 'Polygon_Amoy_Testnet', 'IoTeX_Testnet', 'Ethereum_Sepolia', 'LitVM_LiteForge',
79
+ ...Object.keys(UNSAFE_NETWORKS),
80
+ ];
81
+ const isTestnetNetwork = (network) =>
82
+ TESTNETS.some((name) => name.toUpperCase() === String(network || '').toUpperCase());
83
+
84
+ // The attestation rules a securelock session declares on `network`, as the
85
+ // YAML flow sequences its template takes for __TOLERATE__ and
86
+ // __IGNORE_ADVISORIES__. A testnet tolerates a debug-signed enclave and ignores
87
+ // every Intel advisory. Any other network, a mainnet or a name not in TESTNETS,
88
+ // gets the production rules: no debug-mode, and INTEL-SA-00615 the one
89
+ // advisory ignored.
90
+ const sessionAttestation = (network) => (isTestnetNetwork(network)
91
+ ? { tolerate: '[hyperthreading, outdated-tcb, software-hardening-needed, debug-mode]', ignoreAdvisories: '["*"]' }
92
+ : { tolerate: '[hyperthreading, outdated-tcb, software-hardening-needed]', ignoreAdvisories: '["INTEL-SA-00615"]' });
93
+
65
94
  // The name an image is registered and run under on `network`: <name>-unsafe
66
95
  // on an -unsafe network. A dApp's securelock is <project>-unsafe there, so its
67
96
  // two variants never share an image name, and an -unsafe network's
@@ -90,5 +119,6 @@ const renderCompose = (content, cas, unsafe) => content
90
119
 
91
120
  module.exports = {
92
121
  VALIDATOR_REGISTRY, SESSION_REGISTRY, CHAIN, casProvisioned, isMainnetNetwork,
93
- UNSAFE_NETWORKS, isUnsafeNetwork, nameOnNetwork, sessionTag, renderCompose,
122
+ UNSAFE_NETWORKS, isUnsafeNetwork, TESTNETS, isTestnetNetwork, sessionAttestation,
123
+ nameOnNetwork, sessionTag, renderCompose,
94
124
  };
package/info.js CHANGED
@@ -55,6 +55,14 @@ const NETWORKS = {
55
55
  imageRegistry: '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D',
56
56
  esr: '0x709052Fe77Af543d3d9FE2Ac06a15c635c8D4Be5',
57
57
  },
58
+ // The family of 2026-10-14, owned by 0xA247...; the previous PoX 0x29D3eC87...
59
+ // and V1 registry 0x55e0ad45... are abandoned.
60
+ ETHEREUM_SEPOLIA: {
61
+ type: 'testnet', chainId: 11155111, rpc: 'https://sepolia.gateway.tenderly.co',
62
+ protocol: '0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c',
63
+ imageRegistry: '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F',
64
+ esr: '0x9927809B61122B2af3f3b3A3303875e0687b8eE3',
65
+ },
58
66
  };
59
67
  // The -unsafe networks: the chain and contracts of the network each is the
60
68
  // unsafe variant of, without a CAS.
package/init.js CHANGED
@@ -214,6 +214,7 @@ const main = async () => {
214
214
  "LitVM LiteForge",
215
215
  "Bloxberg Testnet Unsafe",
216
216
  "LitVM LiteForge Unsafe",
217
+ "Ethereum Sepolia Unsafe",
217
218
  ];
218
219
  const blockchainNetwork = await promptOptions(
219
220
  "On which Blockchain network do you want to have the app set up, as a starting point? (default is Bloxberg Testnet): ",
@@ -390,6 +391,7 @@ const main = async () => {
390
391
  "LitVM LiteForge": `ecld-${svc}-litvm-testnet`,
391
392
  "Bloxberg Testnet Unsafe": `etny-${svc}-testnet-unsafe`,
392
393
  "LitVM LiteForge Unsafe": `ecld-${svc}-litvm-testnet-unsafe`,
394
+ "Ethereum Sepolia Unsafe": `ecld-${svc}-ethereum-sepolia-unsafe`,
393
395
  };
394
396
  const trustedZoneImage = imageByNetwork[blockchainNetwork] || `etny-${svc}-testnet`;
395
397
  writeEnv("TRUSTED_ZONE_IMAGE", trustedZoneImage);
package/network.js CHANGED
@@ -14,13 +14,14 @@
14
14
  const casConfig = require('./cas/config.js');
15
15
 
16
16
  /* Accepts a few spellings per network. Returns the normalised key, the
17
- * network ({ address, chainId?, unsafe? }) or null, and the known keys. */
17
+ * network ({ address, chainId?, unsafe? }) or null, and the known keys; rpcUrl names
18
+ * the RPC of a network whose token the runner cannot key an RPC by. */
18
19
  function networkFor({ ECAddress, ECNetworkByChainId }, raw) {
19
20
  const key = String(raw || '').trim().toUpperCase().replace(/\s+/g, '_');
20
21
  const bloxbergTestnet = { address: ECAddress.BLOXBERG.TESTNET_ADDRESS };
21
22
  const amoy = { address: ECAddress.POLYGON.TESTNET_ADDRESS };
22
- const sepolia = { address: ECAddress.SEPOLIA.TESTNET_ADDRESS, chainId: ECNetworkByChainId.SEPOLIA.TESTNET };
23
- const litvm = { address: ECAddress.LITVM.TESTNET_ADDRESS, chainId: ECNetworkByChainId.LITVM.TESTNET };
23
+ const sepolia = { address: ECAddress.SEPOLIA.TESTNET_ADDRESS, chainId: ECNetworkByChainId.SEPOLIA.TESTNET, rpcUrl: 'https://ethereum-sepolia-rpc.publicnode.com' };
24
+ const litvm = { address: ECAddress.LITVM.TESTNET_ADDRESS, chainId: ECNetworkByChainId.LITVM.TESTNET, rpcUrl: 'https://liteforge.rpc.caldera.xyz/infra-partner-http' };
24
25
  const table = {
25
26
  BLOXBERG_TESTNET: bloxbergTestnet,
26
27
  BLOXBERG_TESTNET_UNSAFE: { ...bloxbergTestnet, unsafe: ['BLOXBERG', 'TESTNET_UNSAFE'] },
@@ -28,9 +29,10 @@ function networkFor({ ECAddress, ECNetworkByChainId }, raw) {
28
29
  POLYGON_MAINNET: { address: ECAddress.POLYGON.MAINNET_ADDRESS },
29
30
  POLYGON_AMOY: amoy,
30
31
  POLYGON_AMOY_TESTNET: amoy,
31
- IOTEX_TESTNET: { address: ECAddress.IOTEX.TESTNET_ADDRESS, chainId: ECNetworkByChainId.IOTEX.TESTNET },
32
+ IOTEX_TESTNET: { address: ECAddress.IOTEX.TESTNET_ADDRESS, chainId: ECNetworkByChainId.IOTEX.TESTNET, rpcUrl: 'https://babel-api.testnet.iotex.io' },
32
33
  ETHEREUM_SEPOLIA: sepolia,
33
34
  SEPOLIA: sepolia,
35
+ ETHEREUM_SEPOLIA_UNSAFE: { ...sepolia, unsafe: ['SEPOLIA', 'TESTNET_UNSAFE'] },
34
36
  LITVM_LITEFORGE: litvm,
35
37
  LITVM: litvm,
36
38
  LITVM_LITEFORGE_UNSAFE: { ...litvm, unsafe: ['LITVM', 'TESTNET_UNSAFE'] },
@@ -45,9 +45,11 @@ export const ECRunner = {
45
45
  // --- iotex testnet ---
46
46
  'ecld-pynithy-iotex-testnet': ['0xD56385A97413Ed80E28B1b54A193b98F2C49c975', '0xa7467A6391816be9367a1cC52E0ef0c15FfE3cCC', 'https://babel-api.testnet.iotex.io', 4690],
47
47
  'ecld-nodenithy-iotex-testnet': ['0xD56385A97413Ed80E28B1b54A193b98F2C49c975', '0xa7467A6391816be9367a1cC52E0ef0c15FfE3cCC', 'https://babel-api.testnet.iotex.io', 4690],
48
- // --- ethereum sepolia ---
49
- 'ecld-pynithy-ethereum-sepolia': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
50
- 'ecld-nodenithy-ethereum-sepolia': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
48
+ // --- ethereum sepolia: the family of 2026-10-14 (PoX EthernityImplementationV2 1.1, ECImageRegistryV3) ---
49
+ 'ecld-pynithy-ethereum-sepolia': ['0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c', '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
50
+ 'ecld-nodenithy-ethereum-sepolia': ['0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c', '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
51
+ 'ecld-pynithy-ethereum-sepolia-unsafe': ['0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c', '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
52
+ 'ecld-nodenithy-ethereum-sepolia-unsafe': ['0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c', '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
51
53
  // --- litvm liteforge testnet: the PoX is the EthernityImplementationV2 1.1 of 2026-10-11; the image registry is the ECImageRegistryV3 of 2026-10-12 ---
52
54
  'ecld-pynithy-litvm-testnet': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
53
55
  'ecld-nodenithy-litvm-testnet': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
@@ -259,7 +261,7 @@ const esrEnvBlock = ESR_ENABLED ? `ENV ESR_CONTRACT_ADDRESS=${ESR_CONTRACT_ADDRE
259
261
 
260
262
  // runCommand(`cat Dockerfile.tmpl | sed s/"__ENCLAVE_NAME_SECURELOCK__"/"${ENCLAVE_NAME_SECURELOCK}"/g > Dockerfile`);
261
263
  const dockerfileSecureTemplate = fs.readFileSync('Dockerfile.tmpl', 'utf8');
262
- let dockerfileSecureContent = dockerfileSecureTemplate.replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK).replace(/__NETWORK_TYPE__/g, isMainnet ? 'mainnet' : 'testnet').replace(/__BUCKET_NAME__/g, templateName + "-v3").replace(/__SMART_CONTRACT_ADDRESS__/g, ECRunner[templateName][0]).replace(/__IMAGE_REGISTRY_ADDRESS__/g, ECRunner[templateName][1]).replace(/__RPC_URL__/g, ECRunner[templateName][2]).replace(/__CHAIN_ID__/g, ECRunner[templateName][3]).replace(/__ETNY_VALIDATOR_REGISTRY_ADDRESS__/g, VALIDATOR_REGISTRY[templateName] || '').replace(/^__ESR_ENV__\n/m, esrEnvBlock ? `${esrEnvBlock}\n` : '');
264
+ let dockerfileSecureContent = dockerfileSecureTemplate.replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK).replace(/__NETWORK_TYPE__/g, isMainnet ? 'mainnet' : 'testnet').replace(/__BUCKET_NAME__/g, templateName + "-v3").replace(/__SMART_CONTRACT_ADDRESS__/g, ECRunner[templateName][0]).replace(/__IMAGE_REGISTRY_ADDRESS__/g, ECRunner[templateName][1]).replace(/__RPC_URL__/g, ECRunner[templateName][2]).replace(/__CHAIN_ID__/g, ECRunner[templateName][3]).replace(/__ETNY_VALIDATOR_REGISTRY_ADDRESS__/g, VALIDATOR_REGISTRY[templateName] || '').replace(/^__ESR_ENV__\r?\n/m, esrEnvBlock ? `${esrEnvBlock}\n` : '');
263
265
 
264
266
  // Amount of enclave heap to allocate (SCONE_HEAP). Kept in sync with the
265
267
  // run/docker-compose securelock service; overridable via ECLD_MEMORY_TO_ALLOCATE.
@@ -41,6 +41,17 @@ function setVars(network = "") {
41
41
  NETWORK_RPC = "https://liteforge.rpc.caldera.xyz/infra-partner-http";
42
42
  IMAGE_REGISTRY_ADDRESS = "0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D";
43
43
  CHAIN_ID = 4441;
44
+ } else if (BLOCKCHAIN_NETWORK.includes("Sepolia")) {
45
+ // Sepolia's ECImageRegistryV3 of 2026-10-14; Tenderly's gateway serves
46
+ // the log ranges publicnode prunes.
47
+ NETWORK_RPC = "https://sepolia.gateway.tenderly.co";
48
+ IMAGE_REGISTRY_ADDRESS = "0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F";
49
+ CHAIN_ID = 11155111;
50
+ // Sepolia's EVM charges storage-heavy calls several times the local
51
+ // EVM's gas (a contract creation about seven times); the base fee is
52
+ // under 0.0001 gwei, so 0.1 gwei is paid in full as the tip.
53
+ GAS = 20000000;
54
+ GAS_PRICE = 100000000;
44
55
  }
45
56
  }
46
57
 
@@ -1,9 +1,19 @@
1
+ // The publish's client of publickey.ethernity.cloud, the certificate
2
+ // extraction service: queues the image and polls for its certificate.
3
+ //
4
+ // An image registered on chain first (--chain-registered, the two-step
5
+ // registries) is queued by the service's registry watcher from the
6
+ // ImageRegistered event, with this publish's IPFS peer; the hash is posted to
7
+ // /api/addHash only when that row has not appeared within
8
+ // CHAIN_ROW_GRACE_SECONDS. Other images are posted at once.
1
9
  const axios = require('axios');
2
10
  const fs = require('fs');
3
11
  const { Command } = require('commander');
4
- const program = new Command();
5
12
 
6
13
  const BASE_URL = "https://publickey.ethernity.cloud";
14
+ // The watcher polls the registries every 30 s.
15
+ const CHAIN_ROW_GRACE_SECONDS = 180;
16
+ const POLL_SECONDS = 10;
7
17
 
8
18
  async function submitIpfsHash(hash, enclaveName, protocolVersion, network, templateVersion, dockerComposerHash) {
9
19
  const url = `${BASE_URL}/api/addHash`;
@@ -17,7 +27,6 @@ async function submitIpfsHash(hash, enclaveName, protocolVersion, network, templ
17
27
  };
18
28
  try {
19
29
  const response = await axios.post(url, payload);
20
- // console.log(`response: ${response}`);
21
30
  return response.data;
22
31
  } catch (error) {
23
32
  console.error("Error:", error.response ? error.response.data : error.message);
@@ -25,51 +34,84 @@ async function submitIpfsHash(hash, enclaveName, protocolVersion, network, templ
25
34
  }
26
35
  }
27
36
 
37
+ // The service's answer for the hash, or null while the service does not
38
+ // know the hash yet (404).
28
39
  async function checkIpfsHashStatus(hash) {
29
40
  const url = `${BASE_URL}/api/checkHash/${hash}`;
30
41
  try {
31
42
  const response = await axios.get(url);
32
43
  return response.data;
33
44
  } catch (error) {
45
+ if (error.response && error.response.status === 404) return null;
34
46
  console.error("Error:", error.response ? error.response.data : error.message);
35
47
  process.exit(1);
36
48
  }
37
49
  }
38
50
 
39
- program
40
- .requiredOption('--enclave_name <enclaveName>', 'Enclave name')
41
- .requiredOption('--protocol_version <protocolVersion>', 'Protocol version')
42
- .requiredOption('--network <network>', 'Network')
43
- .requiredOption('--template_version <templateVersion>', 'Template version');
51
+ // Queues the image as described above and returns the service's first
52
+ // answer for it. `submit`, `check`, `sleep` and `now` are the functions
53
+ // above and the clock, replaceable for tests.
54
+ async function queueImage({ hash, chainRegistered, submitArgs, submit = submitIpfsHash, check = checkIpfsHashStatus,
55
+ sleep = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000)), now = () => Date.now() / 1000,
56
+ log = console.log, graceSeconds = CHAIN_ROW_GRACE_SECONDS, pollSeconds = POLL_SECONDS }) {
57
+ if (chainRegistered) {
58
+ const deadline = now() + graceSeconds;
59
+ while (true) {
60
+ const answer = await check(hash);
61
+ if (answer !== null) return answer;
62
+ if (now() >= deadline) {
63
+ log(`The certificate service has not picked the registration up from the chain in ${graceSeconds} s; submitting the hash`);
64
+ break;
65
+ }
66
+ log("Waiting for the certificate service to pick the registration up from the chain...");
67
+ await sleep(pollSeconds);
68
+ }
69
+ }
70
+ const submitResponse = await submit(hash, ...submitArgs);
71
+ log("Submit IPFS Hash Response:", submitResponse);
72
+ return null;
73
+ }
44
74
 
45
- program.parse(process.argv);
75
+ async function main() {
76
+ const program = new Command();
77
+ program
78
+ .requiredOption('--enclave_name <enclaveName>', 'Enclave name')
79
+ .requiredOption('--protocol_version <protocolVersion>', 'Protocol version')
80
+ .requiredOption('--network <network>', 'Network')
81
+ .requiredOption('--template_version <templateVersion>', 'Template version')
82
+ .option('--chain-registered', 'the image is registered on chain; wait for the service to queue it from the event before posting the hash');
83
+ program.parse(process.argv);
46
84
 
47
- const options = program.opts();
48
- const enclaveName = options.enclave_name;
49
- const protocolVersion = options.protocol_version;
50
- const network = options.network.toLowerCase().split("_")[1];
51
- const templateVersion = options.template_version;
85
+ const options = program.opts();
86
+ const enclaveName = options.enclave_name;
87
+ const protocolVersion = options.protocol_version;
88
+ const network = options.network.toLowerCase().split("_")[1];
89
+ const templateVersion = options.template_version;
52
90
 
53
- const hhash = fs.readFileSync('IPFS_HASH.ipfs', 'utf8').trim();
54
- const dockerComposerHash = fs.readFileSync('IPFS_DOCKER_COMPOSE_HASH.ipfs', 'utf8').trim();
91
+ const hhash = fs.readFileSync('IPFS_HASH.ipfs', 'utf8').trim();
92
+ const dockerComposerHash = fs.readFileSync('IPFS_DOCKER_COMPOSE_HASH.ipfs', 'utf8').trim();
55
93
 
56
- console.log("IPFS Hash:", hhash);
57
- console.log("Enclave Name:", enclaveName);
58
- console.log("Protocol Version:", protocolVersion);
59
- console.log("Network:", network);
60
- console.log("Template Version:", templateVersion);
61
- console.log("Docker Composer Hash:", dockerComposerHash);
94
+ console.log("IPFS Hash:", hhash);
95
+ console.log("Enclave Name:", enclaveName);
96
+ console.log("Protocol Version:", protocolVersion);
97
+ console.log("Network:", network);
98
+ console.log("Template Version:", templateVersion);
99
+ console.log("Docker Composer Hash:", dockerComposerHash);
62
100
 
63
- (async () => {
64
- // Submit IPFS Hash
65
- const submitResponse = await submitIpfsHash(hhash, enclaveName, protocolVersion, network, templateVersion, dockerComposerHash);
66
- console.log("Submit IPFS Hash Response:", submitResponse);
101
+ let checkResponse = await queueImage({
102
+ hash: hhash, chainRegistered: Boolean(options.chainRegistered),
103
+ submitArgs: [enclaveName, protocolVersion, network, templateVersion, dockerComposerHash],
104
+ });
67
105
 
68
106
  // The service answers publicKey 0 while the image is queued or running
69
107
  // (with status, percent and queuePosition), -1 when the extraction failed
70
108
  // (with reason) and the certificate when done.
71
109
  while (true) {
72
- const checkResponse = await checkIpfsHashStatus(hhash);
110
+ if (checkResponse === null) {
111
+ await new Promise(resolve => setTimeout(resolve, POLL_SECONDS * 1000));
112
+ checkResponse = await checkIpfsHashStatus(hhash);
113
+ continue;
114
+ }
73
115
  if ("publicKey" in checkResponse) {
74
116
  if (checkResponse.publicKey === 0) {
75
117
  if (checkResponse.status === "running") {
@@ -103,6 +145,11 @@ console.log("Docker Composer Hash:", dockerComposerHash);
103
145
  process.exit(1);
104
146
  }
105
147
 
106
- await new Promise(resolve => setTimeout(resolve, 10000)); // Wait for 10 seconds before checking again
148
+ await new Promise(resolve => setTimeout(resolve, POLL_SECONDS * 1000));
149
+ checkResponse = await checkIpfsHashStatus(hhash);
107
150
  }
108
- })();
151
+ }
152
+
153
+ if (require.main === module) main();
154
+
155
+ module.exports = { queueImage, CHAIN_ROW_GRACE_SECONDS, POLL_SECONDS };
package/nodenithy/run.js CHANGED
@@ -177,7 +177,7 @@ const main = async () => {
177
177
  if (fs.existsSync(file)) fs.unlinkSync(file);
178
178
  }
179
179
  console.log('Upload docker-compose-final.yml to IPFS');
180
- ipfsCli(['--action', 'upload', '--filePath', 'docker-compose-final.yml'], 'inherit');
180
+ ipfsCli(['--action', 'upload', '--filePath', 'docker-compose-final.yml'], ['ignore', 'inherit', 'inherit']);
181
181
  if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
182
182
  console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
183
183
  process.exit(1);
@@ -186,7 +186,7 @@ const main = async () => {
186
186
  console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
187
187
  writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
188
188
  console.log('Upload docker registry to IPFS');
189
- ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
189
+ ipfsCli(['--action', 'upload', '--folderPath', registryPath], ['ignore', 'inherit', 'inherit']);
190
190
  if (!fs.existsSync('IPFS_HASH.ipfs')) {
191
191
  console.error("Error: Could not upload registry to IPFS, please try again!");
192
192
  process.exit(1);
@@ -258,15 +258,13 @@ const main = async () => {
258
258
  const regex = new RegExp(`__${key}__`, 'g');
259
259
  content = content.replace(regex, value);
260
260
  }
261
- // Mainnet attests production enclaves only; a testnet tolerates a
262
- // debug-signed one. Both lines were edited by text replacement, which
263
- // had to repeat the advisory list verbatim to delete it and broke the
264
- // moment either string moved. Written out per network instead.
261
+ // The session's attestation rules for this network: a debug-signed
262
+ // enclave and every Intel advisory are tolerated on a testnet only
263
+ // (cas/config.js sessionAttestation).
264
+ const attestation = casConfig.sessionAttestation(process.env.BLOCKCHAIN_NETWORK);
265
265
  content = content
266
- .replace(/__TOLERATE__/g, isMainnet
267
- ? '[hyperthreading, outdated-tcb, software-hardening-needed]'
268
- : '[hyperthreading, outdated-tcb, software-hardening-needed, debug-mode]')
269
- .replace(/__IGNORE_ADVISORIES__/g, isMainnet ? '["INTEL-SA-00615"]' : '["*"]');
266
+ .replace(/__TOLERATE__/g, attestation.tolerate)
267
+ .replace(/__IGNORE_ADVISORIES__/g, attestation.ignoreAdvisories);
270
268
 
271
269
  fs.writeFileSync(outputFile, content);
272
270
  // console.log(`Contents of ${outputFile}:`);
@@ -657,7 +655,7 @@ const main = async () => {
657
655
  console.log("");
658
656
 
659
657
  const opt = ["yes", "no"];
660
- const choice = await promptOptions("Do you want to continue by generating the necessary certificates using the Ethernity Cloud public certificate extraction services? (yes/no) (default: no): ", opt, "no");
658
+ const choice = process.env.ECLD_ASSUME_YES ? 'yes' : await promptOptions("Do you want to continue by generating the necessary certificates using the Ethernity Cloud public certificate extraction services? (yes/no) (default: no): ", opt, "no");
661
659
 
662
660
  if (choice.toLowerCase() !== 'yes') {
663
661
  console.log("Exiting.");
@@ -665,7 +663,7 @@ const main = async () => {
665
663
  } else {
666
664
  console.log("\nGenerating certificates using the Ethernity Cloud signing service...\n");
667
665
  console.log(`ENCLAVE_NAME_SECURELOCK: ${ENCLAVE_NAME_SECURELOCK}`);
668
- execSync(`node ./public_key_service.js --enclave_name "${securelock}" --protocol_version "v3" --network "${process.env.BLOCKCHAIN_NETWORK}" --template_version "${process.env.VERSION}"`, { stdio: 'inherit' });
666
+ execSync(`node ./public_key_service.js --enclave_name "${securelock}" --protocol_version "v3" --network "${process.env.BLOCKCHAIN_NETWORK}" --template_version "${process.env.VERSION}"${registryV2 ? ' --chain-registered' : ''}`, { stdio: 'inherit' });
669
667
  PUBLIC_KEY_SECURELOCK_RES = fs.readFileSync('PUBLIC_KEY.txt', 'utf8').trim();
670
668
  console.log(`PUBLIC_KEY_SECURELOCK_RES: ${PUBLIC_KEY_SECURELOCK_RES}`);
671
669
  if (!PUBLIC_KEY_SECURELOCK_RES || PUBLIC_KEY_SECURELOCK_RES === '-1') {
@@ -754,7 +752,7 @@ const main = async () => {
754
752
  fs.unlinkSync('IPFS_HASH.ipfs');
755
753
  }
756
754
  console.log('Upload docker registry to IPFS');
757
- ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
755
+ ipfsCli(['--action', 'upload', '--folderPath', registryPath], ['ignore', 'inherit', 'inherit']);
758
756
  if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
759
757
  console.error("Error: Could not upload registry to IPFS, please try again!");
760
758
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.7.10",
3
+ "version": "1.7.13",
4
4
  "description": "A package to run ethernity cloud build scripts",
5
5
  "scripts": {
6
6
  "test": "node --test \"test/**/*.test.js\"",
@@ -39,7 +39,7 @@
39
39
  "!**/*.pyc"
40
40
  ],
41
41
  "dependencies": {
42
- "@ethernity-cloud/runner": "^0.6.5",
42
+ "@ethernity-cloud/runner": "^0.6.6",
43
43
  "acorn": "^8.11.0",
44
44
  "adm-zip": "^0.5.14",
45
45
  "axios": "^1.7.2",
@@ -1,131 +1,131 @@
1
- # syntax=docker/dockerfile:1
2
- #
3
- # Securelock enclave build. The whole build is SGX-free: heavy Python work
4
- # (pip installs + PyInstaller freeze) runs on a STOCK python:3.14-alpine with
5
- # no SCONE and no SGX; only the binary-fs packaging and signing use the SCONE
6
- # build tools, which run in simulation/build mode without an SGX device.
7
-
8
- ########################################################################
9
- # Stage 1 - SGX report helper (unchanged, crosscompiler build tool)
10
- ########################################################################
11
- FROM registry.ethernity.cloud:443/debuggingdelight/ethernity-cloud-sdk-registry/sconecuratedimages/crosscompilers:alpine-scone6.0.7 AS build-sgx-module
12
- # The SDK ships the SGX key-gen module ONLY as a PREBUILT .so
13
- # (get_sgx_report.so) -- never the .c source. The source of truth for
14
- # get_sgx_report.c lives in the etny-pynithy / etny-nodenithy repos; the .so is
15
- # produced once from it via scripts/build_keygen_so.sh (scone-gcc, hardened +
16
- # stripped) and committed here. No source is compiled during the developer's
17
- # build, so a developer inspecting the wheel or build tree never sees the
18
- # derivation algorithm.
19
- #
20
- # NOTE (see esr_wallet.py): the .so still contains the executable logic and the
21
- # enclave image is public, so this hides the SOURCE, not the algorithm; on
22
- # testnet the key remains reproducible because there is no attestation.
23
- COPY src/get_sgx_report.so /etny-securelock/get_sgx_report.so
24
-
25
- ########################################################################
26
- # Stage 2 - PyInstaller freeze on a NORMAL python 3.14 (no SCONE / no SGX)
27
- # Installs the user's requirements.txt + PyInstaller, renders securelock.py,
28
- # and runs pyinstaller to produce the frozen COLLECT dir. Same python
29
- # version + musl as the SCONE runtime python, so the output is compatible.
30
- ########################################################################
31
- FROM python:3.14-alpine AS pyfreeze
32
- RUN apk add --no-cache bash gcc g++ musl-dev libffi-dev openssl-dev rust cargo make libsodium-dev zlib-dev binutils
33
- ENV SODIUM_INSTALL=system
34
- WORKDIR /etny-securelock
35
-
36
- # Core securelock runtime deps + PyInstaller (built here, off-SCONE, where pip
37
- # and native builds work normally).
38
- RUN pip install --no-cache-dir \
39
- pyinstaller \
40
- python-dotenv "web3>=6" cryptography ecdsa pyasn1 tinyec minio pynacl
41
-
42
- # User-supplied additional modules (the customization hook).
43
- COPY ./src/serverless/requirements.txt /requirements.txt
44
- RUN pip install --no-cache-dir -r /requirements.txt || true
45
-
46
- COPY ./src /etny-securelock/
47
- COPY ./scripts/* /etny-securelock/
48
- COPY --from=build-sgx-module /etny-securelock/get_sgx_report.so /etny-securelock/get_sgx_report.so
49
-
50
- # Render securelock.py from the template, then run PyInstaller (off-SCONE).
51
- ENV SECURELOCK_SESSION=__SECURELOCK_SESSION__
52
- ENV BUCKET_NAME=__BUCKET_NAME__
53
- ENV SMART_CONTRACT_ADDRESS=__SMART_CONTRACT_ADDRESS__
54
- ENV IMAGE_REGISTRY_ADDRESS=__IMAGE_REGISTRY_ADDRESS__
55
- ENV RPC_URL=__RPC_URL__
56
- ENV CHAIN_ID=__CHAIN_ID__
57
- ENV TRUSTED_ZONE_IMAGE=__TRUSTED_ZONE_IMAGE__
58
- ENV NETWORK_TYPE=__NETWORK_TYPE__
59
- ENV VALIDATOR_REGISTRY_ADDRESS=__VALIDATOR_REGISTRY_ADDRESS__
60
- RUN chmod +x /etny-securelock/pyfreeze.sh && /etny-securelock/pyfreeze.sh
61
-
62
- ########################################################################
63
- # Stage 3 - binary-fs packaging with the SCONE build tools (no SGX).
64
- # Assembles the SCONE python + stdlib + the (off-SCONE) installed site-packages
65
- # + the rendered securelock.py + PyInstaller's collected .so list, and bakes
66
- # them into the measured binary-fs.
67
- ########################################################################
68
- FROM registry.ethernity.cloud:443/debuggingdelight/ethernity-cloud-sdk-registry/sconecuratedimages/crosscompilers:alpine-scone6.0.7 AS binfs
69
- RUN mkdir -p /binary-fs-dir /etny-securelock
70
- # SCONE-built static python + its stdlib, from the runtime base image
71
- COPY --from=etny-securelock-serverless /usr/local/bin/python3 /usr/local/bin/python3
72
- COPY --from=etny-securelock-serverless /usr/local/lib/python3.14 /usr/local/lib/python3.14
73
- # The ENTIRE /etny-securelock tree (rendered securelock.py + its sibling modules
74
- # etny_crypto.py, etny_exec.py, models.py, swift_stream_service.py, the abi/
75
- # dir, get_sgx_report.so, scripts) - all must be baked into binary-fs or the
76
- # enclave fails at import (ModuleNotFoundError: No module named 'etny_crypto').
77
- # site-packages + COLLECT toc also come from the OFF-SCONE pyfreeze stage.
78
- COPY --from=pyfreeze /etny-securelock /etny-securelock
79
- COPY --from=pyfreeze /etny-securelock/build/securelock/COLLECT-00.toc /etny-securelock/COLLECT-00.toc
80
- COPY --from=pyfreeze /usr/local/lib/python3.14/site-packages /usr/local/lib/python3.14/site-packages
81
- RUN chmod +x /etny-securelock/binary-fs-build.sh && /etny-securelock/binary-fs-build.sh
82
- RUN cd /binary-fs-dir && scone gcc ./binary_fs_blob.s ./libbinary_fs_template.a -shared -o /libbinary-fs.so
83
-
84
- ########################################################################
85
- # Stage 4 - final SCONE runtime image (lean scone python base) + sign.
86
- ########################################################################
87
- FROM etny-securelock-serverless
88
-
89
- COPY --from=binfs /usr/local/bin/scone /usr/local/bin/scone
90
- COPY --from=binfs /libbinary-fs.so /lib/libbinary-fs.so
91
-
92
- RUN openssl genrsa -3 -out /enclave-key.pem 3072
93
-
94
- # SCONE enclaves run with no HOME; libraries calling os.path.expanduser('~')
95
- # abort with "$HOME is not defined" and kill the enclave before it emits its
96
- # public key. Set a writable HOME so startup (and cert emission) succeeds.
97
- ENV HOME=/tmp
98
- ENV SCONE_HEAP=__MEMORY_TO_ALLOCATE__
99
- ENV SCONE_LOG=FATAL
100
- ENV SCONE_DEBUG=0
101
- ENV SCONE_STACK=4M
102
- __SCONE_ALLOW_DLOPEN__
103
- ENV SCONE_EXTENSIONS_PATH=/lib/libbinary-fs.so
104
- # securelock.py does relative imports of its sibling modules (etny_crypto,
105
- # models, ...) and reads abi/ by relative path; run from its own directory so
106
- # those resolve (the base image's build WORKDIR /b/Python-3.14.6 otherwise leaks
107
- # in as the enclave PWD).
108
- ENV SCONE_PWD=/etny-securelock
109
- __ESR_ENV__
110
- WORKDIR /etny-securelock
111
-
112
- # Rendered by build.py: signs the EXECUTED binary (/usr/local/bin/python) with
113
- # explicit enclave-creation params; mainnet adds --production, testnet omits it
114
- # (non-CAS self-sign mode).
115
- __SCONE_SIGN__
116
-
117
- # Record the SIGNED MRENCLAVE so publish.py can assert the runtime enclave
118
- # measurement matches what was signed BEFORE registering the CAS session /
119
- # publishing (mismatch => SCONE recomputed at load => debug => CAS rejects).
120
- # scone-signer info prints two tab-indented "MRENCLAVE:" lines (non-EDMM first;
121
- # runtime has EDMM disabled, so the first is what SCONE_HASH produces). Capture
122
- # to a file first: piping into `head` kills scone-signer with SIGPIPE (exit 141)
123
- # under buildkit's `ash -eo pipefail`.
124
- RUN scone-signer info /usr/local/bin/python > /tmp/siginfo.txt 2>&1; \
125
- grep -iE "MRENCLAVE:" /tmp/siginfo.txt | sed -n '1p' \
126
- | sed -E 's/.*MRENCLAVE:[[:space:]]*//I' | tr -d '[:space:]' > /signed_mrenclave.txt && \
127
- echo "SIGNED_MRENCLAVE=$(cat /signed_mrenclave.txt)"
128
-
129
- RUN rm -rf /enclave-key.pem
130
-
131
- ENTRYPOINT ["/usr/local/bin/python", "/etny-securelock/securelock.py"]
1
+ # syntax=docker/dockerfile:1
2
+ #
3
+ # Securelock enclave build. The whole build is SGX-free: heavy Python work
4
+ # (pip installs + PyInstaller freeze) runs on a STOCK python:3.14-alpine with
5
+ # no SCONE and no SGX; only the binary-fs packaging and signing use the SCONE
6
+ # build tools, which run in simulation/build mode without an SGX device.
7
+
8
+ ########################################################################
9
+ # Stage 1 - SGX report helper (unchanged, crosscompiler build tool)
10
+ ########################################################################
11
+ FROM registry.ethernity.cloud:443/debuggingdelight/ethernity-cloud-sdk-registry/sconecuratedimages/crosscompilers:alpine-scone6.0.7 AS build-sgx-module
12
+ # The SDK ships the SGX key-gen module ONLY as a PREBUILT .so
13
+ # (get_sgx_report.so) -- never the .c source. The source of truth for
14
+ # get_sgx_report.c lives in the etny-pynithy / etny-nodenithy repos; the .so is
15
+ # produced once from it via scripts/build_keygen_so.sh (scone-gcc, hardened +
16
+ # stripped) and committed here. No source is compiled during the developer's
17
+ # build, so a developer inspecting the wheel or build tree never sees the
18
+ # derivation algorithm.
19
+ #
20
+ # NOTE (see esr_wallet.py): the .so still contains the executable logic and the
21
+ # enclave image is public, so this hides the SOURCE, not the algorithm; on
22
+ # testnet the key remains reproducible because there is no attestation.
23
+ COPY src/get_sgx_report.so /etny-securelock/get_sgx_report.so
24
+
25
+ ########################################################################
26
+ # Stage 2 - PyInstaller freeze on a NORMAL python 3.14 (no SCONE / no SGX)
27
+ # Installs the user's requirements.txt + PyInstaller, renders securelock.py,
28
+ # and runs pyinstaller to produce the frozen COLLECT dir. Same python
29
+ # version + musl as the SCONE runtime python, so the output is compatible.
30
+ ########################################################################
31
+ FROM python:3.14-alpine AS pyfreeze
32
+ RUN apk add --no-cache bash gcc g++ musl-dev libffi-dev openssl-dev rust cargo make libsodium-dev zlib-dev binutils
33
+ ENV SODIUM_INSTALL=system
34
+ WORKDIR /etny-securelock
35
+
36
+ # Core securelock runtime deps + PyInstaller (built here, off-SCONE, where pip
37
+ # and native builds work normally).
38
+ RUN pip install --no-cache-dir \
39
+ pyinstaller \
40
+ python-dotenv "web3>=6" cryptography ecdsa pyasn1 tinyec minio pynacl
41
+
42
+ # User-supplied additional modules (the customization hook).
43
+ COPY ./src/serverless/requirements.txt /requirements.txt
44
+ RUN pip install --no-cache-dir -r /requirements.txt || true
45
+
46
+ COPY ./src /etny-securelock/
47
+ COPY ./scripts/* /etny-securelock/
48
+ COPY --from=build-sgx-module /etny-securelock/get_sgx_report.so /etny-securelock/get_sgx_report.so
49
+
50
+ # Render securelock.py from the template, then run PyInstaller (off-SCONE).
51
+ ENV SECURELOCK_SESSION=__SECURELOCK_SESSION__
52
+ ENV BUCKET_NAME=__BUCKET_NAME__
53
+ ENV SMART_CONTRACT_ADDRESS=__SMART_CONTRACT_ADDRESS__
54
+ ENV IMAGE_REGISTRY_ADDRESS=__IMAGE_REGISTRY_ADDRESS__
55
+ ENV RPC_URL=__RPC_URL__
56
+ ENV CHAIN_ID=__CHAIN_ID__
57
+ ENV TRUSTED_ZONE_IMAGE=__TRUSTED_ZONE_IMAGE__
58
+ ENV NETWORK_TYPE=__NETWORK_TYPE__
59
+ ENV VALIDATOR_REGISTRY_ADDRESS=__VALIDATOR_REGISTRY_ADDRESS__
60
+ RUN chmod +x /etny-securelock/pyfreeze.sh && /etny-securelock/pyfreeze.sh
61
+
62
+ ########################################################################
63
+ # Stage 3 - binary-fs packaging with the SCONE build tools (no SGX).
64
+ # Assembles the SCONE python + stdlib + the (off-SCONE) installed site-packages
65
+ # + the rendered securelock.py + PyInstaller's collected .so list, and bakes
66
+ # them into the measured binary-fs.
67
+ ########################################################################
68
+ FROM registry.ethernity.cloud:443/debuggingdelight/ethernity-cloud-sdk-registry/sconecuratedimages/crosscompilers:alpine-scone6.0.7 AS binfs
69
+ RUN mkdir -p /binary-fs-dir /etny-securelock
70
+ # SCONE-built static python + its stdlib, from the runtime base image
71
+ COPY --from=etny-securelock-serverless /usr/local/bin/python3 /usr/local/bin/python3
72
+ COPY --from=etny-securelock-serverless /usr/local/lib/python3.14 /usr/local/lib/python3.14
73
+ # The ENTIRE /etny-securelock tree (rendered securelock.py + its sibling modules
74
+ # etny_crypto.py, etny_exec.py, models.py, swift_stream_service.py, the abi/
75
+ # dir, get_sgx_report.so, scripts) - all must be baked into binary-fs or the
76
+ # enclave fails at import (ModuleNotFoundError: No module named 'etny_crypto').
77
+ # site-packages + COLLECT toc also come from the OFF-SCONE pyfreeze stage.
78
+ COPY --from=pyfreeze /etny-securelock /etny-securelock
79
+ COPY --from=pyfreeze /etny-securelock/build/securelock/COLLECT-00.toc /etny-securelock/COLLECT-00.toc
80
+ COPY --from=pyfreeze /usr/local/lib/python3.14/site-packages /usr/local/lib/python3.14/site-packages
81
+ RUN chmod +x /etny-securelock/binary-fs-build.sh && /etny-securelock/binary-fs-build.sh
82
+ RUN cd /binary-fs-dir && scone gcc ./binary_fs_blob.s ./libbinary_fs_template.a -shared -o /libbinary-fs.so
83
+
84
+ ########################################################################
85
+ # Stage 4 - final SCONE runtime image (lean scone python base) + sign.
86
+ ########################################################################
87
+ FROM etny-securelock-serverless
88
+
89
+ COPY --from=binfs /usr/local/bin/scone /usr/local/bin/scone
90
+ COPY --from=binfs /libbinary-fs.so /lib/libbinary-fs.so
91
+
92
+ RUN openssl genrsa -3 -out /enclave-key.pem 3072
93
+
94
+ # SCONE enclaves run with no HOME; libraries calling os.path.expanduser('~')
95
+ # abort with "$HOME is not defined" and kill the enclave before it emits its
96
+ # public key. Set a writable HOME so startup (and cert emission) succeeds.
97
+ ENV HOME=/tmp
98
+ ENV SCONE_HEAP=__MEMORY_TO_ALLOCATE__
99
+ ENV SCONE_LOG=FATAL
100
+ ENV SCONE_DEBUG=0
101
+ ENV SCONE_STACK=4M
102
+ __SCONE_ALLOW_DLOPEN__
103
+ ENV SCONE_EXTENSIONS_PATH=/lib/libbinary-fs.so
104
+ # securelock.py does relative imports of its sibling modules (etny_crypto,
105
+ # models, ...) and reads abi/ by relative path; run from its own directory so
106
+ # those resolve (the base image's build WORKDIR /b/Python-3.14.6 otherwise leaks
107
+ # in as the enclave PWD).
108
+ ENV SCONE_PWD=/etny-securelock
109
+ __ESR_ENV__
110
+ WORKDIR /etny-securelock
111
+
112
+ # Rendered by build.py: signs the EXECUTED binary (/usr/local/bin/python) with
113
+ # explicit enclave-creation params; mainnet adds --production, testnet omits it
114
+ # (non-CAS self-sign mode).
115
+ __SCONE_SIGN__
116
+
117
+ # Record the SIGNED MRENCLAVE so publish.py can assert the runtime enclave
118
+ # measurement matches what was signed BEFORE registering the CAS session /
119
+ # publishing (mismatch => SCONE recomputed at load => debug => CAS rejects).
120
+ # scone-signer info prints two tab-indented "MRENCLAVE:" lines (non-EDMM first;
121
+ # runtime has EDMM disabled, so the first is what SCONE_HASH produces). Capture
122
+ # to a file first: piping into `head` kills scone-signer with SIGPIPE (exit 141)
123
+ # under buildkit's `ash -eo pipefail`.
124
+ RUN scone-signer info /usr/local/bin/python > /tmp/siginfo.txt 2>&1; \
125
+ grep -iE "MRENCLAVE:" /tmp/siginfo.txt | sed -n '1p' \
126
+ | sed -E 's/.*MRENCLAVE:[[:space:]]*//I' | tr -d '[:space:]' > /signed_mrenclave.txt && \
127
+ echo "SIGNED_MRENCLAVE=$(cat /signed_mrenclave.txt)"
128
+
129
+ RUN rm -rf /enclave-key.pem
130
+
131
+ ENTRYPOINT ["/usr/local/bin/python", "/etny-securelock/securelock.py"]
@@ -616,6 +616,12 @@ class EtnySecureLock:
616
616
  # attests this order and this run's challenge; the data itself is
617
617
  # pushed unchanged. Without a binding (older trustedzone) the bare
618
618
  # data is signed, which that trustedzone verifies.
619
+ if not self.trustedzone_public_key_pem:
620
+ # A failure before execute() (the client-data check) reaches here
621
+ # with neither the trustedzone's certificate, which the result is
622
+ # encrypted to, nor the task binding it is signed over.
623
+ self.__get_trustedzone_public_key()
624
+ self.__load_task_binding()
619
625
  binding = getattr(self, "task_binding", None)
620
626
  prefix = f"{binding['challenge']}:{binding['order_id']}:" if binding else ""
621
627
  self.encrypt_file_and_push_to_swifstream(str(self.task_result), self.result, signed_prefix=prefix)
@@ -817,6 +823,9 @@ class EtnySecureLock:
817
823
  f"No attested task caller supplied "
818
824
  f"({e.__class__.__name__}); treating task as anonymous")
819
825
 
826
+ self.__load_task_binding()
827
+
828
+ def __load_task_binding(self):
820
829
  # Task binding: the order id and enclave challenge the trustedzone
821
830
  # generated for this task, forwarded as task.securelock + .sig. When
822
831
  # present (and only with a valid trustedzone signature) the result and
package/pynithy/build.mjs CHANGED
@@ -45,9 +45,11 @@ export const ECRunner = {
45
45
  // --- iotex testnet ---
46
46
  'ecld-pynithy-iotex-testnet': ['0xD56385A97413Ed80E28B1b54A193b98F2C49c975', '0xa7467A6391816be9367a1cC52E0ef0c15FfE3cCC', 'https://babel-api.testnet.iotex.io', 4690],
47
47
  'ecld-nodenithy-iotex-testnet': ['0xD56385A97413Ed80E28B1b54A193b98F2C49c975', '0xa7467A6391816be9367a1cC52E0ef0c15FfE3cCC', 'https://babel-api.testnet.iotex.io', 4690],
48
- // --- ethereum sepolia ---
49
- 'ecld-pynithy-ethereum-sepolia': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
50
- 'ecld-nodenithy-ethereum-sepolia': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
48
+ // --- ethereum sepolia: the family of 2026-10-14 (PoX EthernityImplementationV2 1.1, ECImageRegistryV3) ---
49
+ 'ecld-pynithy-ethereum-sepolia': ['0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c', '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
50
+ 'ecld-nodenithy-ethereum-sepolia': ['0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c', '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
51
+ 'ecld-pynithy-ethereum-sepolia-unsafe': ['0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c', '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
52
+ 'ecld-nodenithy-ethereum-sepolia-unsafe': ['0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c', '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
51
53
  // --- litvm liteforge testnet: the PoX is the EthernityImplementationV2 1.1 of 2026-10-11; the image registry is the ECImageRegistryV3 of 2026-10-12 ---
52
54
  'ecld-pynithy-litvm-testnet': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
53
55
  'ecld-nodenithy-litvm-testnet': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
@@ -224,7 +226,7 @@ fs.writeFileSync('Dockerfile', fs.readFileSync('Dockerfile.tpl', 'utf8')
224
226
  .replace(/__NETWORK_TYPE__/g, isMainnet ? 'mainnet' : 'testnet')
225
227
  .replace(/__VALIDATOR_REGISTRY_ADDRESS__/g, VALIDATOR_REGISTRY[templateName] || '')
226
228
  .replace(/__MEMORY_TO_ALLOCATE__/g, MEMORY_TO_ALLOCATE)
227
- .replace(/__ESR_ENV__\n/, esrEnvBlock)
229
+ .replace(/__ESR_ENV__\r?\n/, esrEnvBlock)
228
230
  .replace('__SCONE_ALLOW_DLOPEN__', 'ENV SCONE_ALLOW_DLOPEN=1')
229
231
  .replace('__SCONE_SIGN__',
230
232
  `RUN scone-signer sign ${signFlags}${casProvisioned ? ' --production' : ''} /usr/local/bin/python`));
@@ -41,6 +41,17 @@ function setVars(network = "") {
41
41
  NETWORK_RPC = "https://liteforge.rpc.caldera.xyz/infra-partner-http";
42
42
  IMAGE_REGISTRY_ADDRESS = "0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D";
43
43
  CHAIN_ID = 4441;
44
+ } else if (BLOCKCHAIN_NETWORK.includes("Sepolia")) {
45
+ // Sepolia's ECImageRegistryV3 of 2026-10-14; Tenderly's gateway serves
46
+ // the log ranges publicnode prunes.
47
+ NETWORK_RPC = "https://sepolia.gateway.tenderly.co";
48
+ IMAGE_REGISTRY_ADDRESS = "0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F";
49
+ CHAIN_ID = 11155111;
50
+ // Sepolia's EVM charges storage-heavy calls several times the local
51
+ // EVM's gas (a contract creation about seven times); the base fee is
52
+ // under 0.0001 gwei, so 0.1 gwei is paid in full as the tip.
53
+ GAS = 20000000;
54
+ GAS_PRICE = 100000000;
44
55
  }
45
56
  }
46
57
 
@@ -1,9 +1,19 @@
1
+ // The publish's client of publickey.ethernity.cloud, the certificate
2
+ // extraction service: queues the image and polls for its certificate.
3
+ //
4
+ // An image registered on chain first (--chain-registered, the two-step
5
+ // registries) is queued by the service's registry watcher from the
6
+ // ImageRegistered event, with this publish's IPFS peer; the hash is posted to
7
+ // /api/addHash only when that row has not appeared within
8
+ // CHAIN_ROW_GRACE_SECONDS. Other images are posted at once.
1
9
  const axios = require('axios');
2
10
  const fs = require('fs');
3
11
  const { Command } = require('commander');
4
- const program = new Command();
5
12
 
6
13
  const BASE_URL = "https://publickey.ethernity.cloud";
14
+ // The watcher polls the registries every 30 s.
15
+ const CHAIN_ROW_GRACE_SECONDS = 180;
16
+ const POLL_SECONDS = 10;
7
17
 
8
18
  async function submitIpfsHash(hash, enclaveName, protocolVersion, network, templateVersion, dockerComposerHash) {
9
19
  const url = `${BASE_URL}/api/addHash`;
@@ -17,7 +27,6 @@ async function submitIpfsHash(hash, enclaveName, protocolVersion, network, templ
17
27
  };
18
28
  try {
19
29
  const response = await axios.post(url, payload);
20
- // console.log(`response: ${response}`);
21
30
  return response.data;
22
31
  } catch (error) {
23
32
  console.error("Error:", error.response ? error.response.data : error.message);
@@ -25,51 +34,84 @@ async function submitIpfsHash(hash, enclaveName, protocolVersion, network, templ
25
34
  }
26
35
  }
27
36
 
37
+ // The service's answer for the hash, or null while the service does not
38
+ // know the hash yet (404).
28
39
  async function checkIpfsHashStatus(hash) {
29
40
  const url = `${BASE_URL}/api/checkHash/${hash}`;
30
41
  try {
31
42
  const response = await axios.get(url);
32
43
  return response.data;
33
44
  } catch (error) {
45
+ if (error.response && error.response.status === 404) return null;
34
46
  console.error("Error:", error.response ? error.response.data : error.message);
35
47
  process.exit(1);
36
48
  }
37
49
  }
38
50
 
39
- program
40
- .requiredOption('--enclave_name <enclaveName>', 'Enclave name')
41
- .requiredOption('--protocol_version <protocolVersion>', 'Protocol version')
42
- .requiredOption('--network <network>', 'Network')
43
- .requiredOption('--template_version <templateVersion>', 'Template version');
51
+ // Queues the image as described above and returns the service's first
52
+ // answer for it. `submit`, `check`, `sleep` and `now` are the functions
53
+ // above and the clock, replaceable for tests.
54
+ async function queueImage({ hash, chainRegistered, submitArgs, submit = submitIpfsHash, check = checkIpfsHashStatus,
55
+ sleep = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000)), now = () => Date.now() / 1000,
56
+ log = console.log, graceSeconds = CHAIN_ROW_GRACE_SECONDS, pollSeconds = POLL_SECONDS }) {
57
+ if (chainRegistered) {
58
+ const deadline = now() + graceSeconds;
59
+ while (true) {
60
+ const answer = await check(hash);
61
+ if (answer !== null) return answer;
62
+ if (now() >= deadline) {
63
+ log(`The certificate service has not picked the registration up from the chain in ${graceSeconds} s; submitting the hash`);
64
+ break;
65
+ }
66
+ log("Waiting for the certificate service to pick the registration up from the chain...");
67
+ await sleep(pollSeconds);
68
+ }
69
+ }
70
+ const submitResponse = await submit(hash, ...submitArgs);
71
+ log("Submit IPFS Hash Response:", submitResponse);
72
+ return null;
73
+ }
44
74
 
45
- program.parse(process.argv);
75
+ async function main() {
76
+ const program = new Command();
77
+ program
78
+ .requiredOption('--enclave_name <enclaveName>', 'Enclave name')
79
+ .requiredOption('--protocol_version <protocolVersion>', 'Protocol version')
80
+ .requiredOption('--network <network>', 'Network')
81
+ .requiredOption('--template_version <templateVersion>', 'Template version')
82
+ .option('--chain-registered', 'the image is registered on chain; wait for the service to queue it from the event before posting the hash');
83
+ program.parse(process.argv);
46
84
 
47
- const options = program.opts();
48
- const enclaveName = options.enclave_name;
49
- const protocolVersion = options.protocol_version;
50
- const network = options.network.toLowerCase().split("_")[1];
51
- const templateVersion = options.template_version;
85
+ const options = program.opts();
86
+ const enclaveName = options.enclave_name;
87
+ const protocolVersion = options.protocol_version;
88
+ const network = options.network.toLowerCase().split("_")[1];
89
+ const templateVersion = options.template_version;
52
90
 
53
- const hhash = fs.readFileSync('IPFS_HASH.ipfs', 'utf8').trim();
54
- const dockerComposerHash = fs.readFileSync('IPFS_DOCKER_COMPOSE_HASH.ipfs', 'utf8').trim();
91
+ const hhash = fs.readFileSync('IPFS_HASH.ipfs', 'utf8').trim();
92
+ const dockerComposerHash = fs.readFileSync('IPFS_DOCKER_COMPOSE_HASH.ipfs', 'utf8').trim();
55
93
 
56
- console.log("IPFS Hash:", hhash);
57
- console.log("Enclave Name:", enclaveName);
58
- console.log("Protocol Version:", protocolVersion);
59
- console.log("Network:", network);
60
- console.log("Template Version:", templateVersion);
61
- console.log("Docker Composer Hash:", dockerComposerHash);
94
+ console.log("IPFS Hash:", hhash);
95
+ console.log("Enclave Name:", enclaveName);
96
+ console.log("Protocol Version:", protocolVersion);
97
+ console.log("Network:", network);
98
+ console.log("Template Version:", templateVersion);
99
+ console.log("Docker Composer Hash:", dockerComposerHash);
62
100
 
63
- (async () => {
64
- // Submit IPFS Hash
65
- const submitResponse = await submitIpfsHash(hhash, enclaveName, protocolVersion, network, templateVersion, dockerComposerHash);
66
- console.log("Submit IPFS Hash Response:", submitResponse);
101
+ let checkResponse = await queueImage({
102
+ hash: hhash, chainRegistered: Boolean(options.chainRegistered),
103
+ submitArgs: [enclaveName, protocolVersion, network, templateVersion, dockerComposerHash],
104
+ });
67
105
 
68
106
  // The service answers publicKey 0 while the image is queued or running
69
107
  // (with status, percent and queuePosition), -1 when the extraction failed
70
108
  // (with reason) and the certificate when done.
71
109
  while (true) {
72
- const checkResponse = await checkIpfsHashStatus(hhash);
110
+ if (checkResponse === null) {
111
+ await new Promise(resolve => setTimeout(resolve, POLL_SECONDS * 1000));
112
+ checkResponse = await checkIpfsHashStatus(hhash);
113
+ continue;
114
+ }
73
115
  if ("publicKey" in checkResponse) {
74
116
  if (checkResponse.publicKey === 0) {
75
117
  if (checkResponse.status === "running") {
@@ -85,6 +127,17 @@ console.log("Docker Composer Hash:", dockerComposerHash);
85
127
  console.log("Public Key:", checkResponse.publicKey);
86
128
  // Save public key to file
87
129
  fs.writeFileSync('PUBLIC_KEY.txt', checkResponse.publicKey);
130
+ // ESR (RFC §5.2): the enclave also prints ESR_WALLET_ADDRESS on the
131
+ // cert channel. The service currently returns only "publicKey"; when
132
+ // it starts relaying the enclave's output (or adds an
133
+ // "esrWalletAddress" field) it is picked up here and written to a
134
+ // sibling file the caller reads -- no other changes needed. Until
135
+ // then, ESR projects extract locally on an SGX host once to learn
136
+ // the address (publish then persists it).
137
+ if (checkResponse.esrWalletAddress) {
138
+ console.log("ESR Wallet Address:", checkResponse.esrWalletAddress);
139
+ fs.writeFileSync('ESR_WALLET_ADDRESS.txt', checkResponse.esrWalletAddress);
140
+ }
88
141
  break;
89
142
  }
90
143
  } else {
@@ -92,6 +145,11 @@ console.log("Docker Composer Hash:", dockerComposerHash);
92
145
  process.exit(1);
93
146
  }
94
147
 
95
- await new Promise(resolve => setTimeout(resolve, 10000)); // Wait for 10 seconds before checking again
148
+ await new Promise(resolve => setTimeout(resolve, POLL_SECONDS * 1000));
149
+ checkResponse = await checkIpfsHashStatus(hhash);
96
150
  }
97
- })();
151
+ }
152
+
153
+ if (require.main === module) main();
154
+
155
+ module.exports = { queueImage, CHAIN_ROW_GRACE_SECONDS, POLL_SECONDS };
package/pynithy/run.js CHANGED
@@ -178,7 +178,7 @@ const main = async () => {
178
178
  if (fs.existsSync(file)) fs.unlinkSync(file);
179
179
  }
180
180
  console.log('Upload docker-compose-final.yml to IPFS');
181
- ipfsCli(['--action', 'upload', '--filePath', 'docker-compose-final.yml'], 'inherit');
181
+ ipfsCli(['--action', 'upload', '--filePath', 'docker-compose-final.yml'], ['ignore', 'inherit', 'inherit']);
182
182
  if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
183
183
  console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
184
184
  process.exit(1);
@@ -187,7 +187,7 @@ const main = async () => {
187
187
  console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
188
188
  writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
189
189
  console.log('Upload docker registry to IPFS');
190
- ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
190
+ ipfsCli(['--action', 'upload', '--folderPath', registryPath], ['ignore', 'inherit', 'inherit']);
191
191
  if (!fs.existsSync('IPFS_HASH.ipfs')) {
192
192
  console.error("Error: Could not upload registry to IPFS, please try again!");
193
193
  process.exit(1);
@@ -256,16 +256,13 @@ const main = async () => {
256
256
  const regex = new RegExp(`__${key}__`, 'g');
257
257
  content = content.replace(regex, value);
258
258
  }
259
- // Mainnet attests production enclaves only; a testnet tolerates a
260
- // debug-signed one. The tolerate list was edited by text replacement
261
- // ('", debug-mode"'), which never matched this template's LEADING
262
- // "debug-mode," -- so mainnet sessions were published tolerating debug
263
- // enclaves. Written out per network instead.
259
+ // The session's attestation rules for this network: a debug-signed
260
+ // enclave and every Intel advisory are tolerated on a testnet only
261
+ // (cas/config.js sessionAttestation).
262
+ const attestation = casConfig.sessionAttestation(process.env.BLOCKCHAIN_NETWORK);
264
263
  content = content
265
- .replace(/__TOLERATE__/g, isMainnet
266
- ? '[hyperthreading, outdated-tcb, software-hardening-needed]'
267
- : '[debug-mode, hyperthreading, outdated-tcb, software-hardening-needed]')
268
- .replace(/__IGNORE_ADVISORIES__/g, isMainnet ? '["INTEL-SA-00615"]' : '["*"]');
264
+ .replace(/__TOLERATE__/g, attestation.tolerate)
265
+ .replace(/__IGNORE_ADVISORIES__/g, attestation.ignoreAdvisories);
269
266
 
270
267
  fs.writeFileSync(outputFile, content);
271
268
  // console.log(`Contents of ${outputFile}:`);
@@ -656,7 +653,7 @@ const main = async () => {
656
653
  console.log("");
657
654
 
658
655
  const opt = ["yes", "no"];
659
- const choice = await promptOptions("Do you want to continue by generating the necessary certificates using the Ethernity Cloud public certificate extraction services? (yes/no) (default: no): ", opt, "no");
656
+ const choice = process.env.ECLD_ASSUME_YES ? 'yes' : await promptOptions("Do you want to continue by generating the necessary certificates using the Ethernity Cloud public certificate extraction services? (yes/no) (default: no): ", opt, "no");
660
657
 
661
658
  if (choice.toLowerCase() !== 'yes') {
662
659
  console.log("Exiting.");
@@ -664,7 +661,7 @@ const main = async () => {
664
661
  } else {
665
662
  console.log("\nGenerating certificates using the Ethernity Cloud signing service...\n");
666
663
  console.log(`ENCLAVE_NAME_SECURELOCK: ${ENCLAVE_NAME_SECURELOCK}`);
667
- execSync(`node ./public_key_service.js --enclave_name "${securelock}" --protocol_version "v3" --network "${process.env.BLOCKCHAIN_NETWORK}" --template_version "${process.env.VERSION}"`, { stdio: 'inherit' });
664
+ execSync(`node ./public_key_service.js --enclave_name "${securelock}" --protocol_version "v3" --network "${process.env.BLOCKCHAIN_NETWORK}" --template_version "${process.env.VERSION}"${registryV2 ? ' --chain-registered' : ''}`, { stdio: 'inherit' });
668
665
  PUBLIC_KEY_SECURELOCK_RES = fs.readFileSync('PUBLIC_KEY.txt', 'utf8').trim();
669
666
  console.log(`PUBLIC_KEY_SECURELOCK_RES: ${PUBLIC_KEY_SECURELOCK_RES}`);
670
667
  if (!PUBLIC_KEY_SECURELOCK_RES || PUBLIC_KEY_SECURELOCK_RES === '-1') {
@@ -731,7 +728,7 @@ const main = async () => {
731
728
  fs.unlinkSync('IPFS_HASH.ipfs');
732
729
  }
733
730
  console.log('Upload docker registry to IPFS');
734
- ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
731
+ ipfsCli(['--action', 'upload', '--folderPath', registryPath], ['ignore', 'inherit', 'inherit']);
735
732
  if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
736
733
  console.error("Error: Could not upload registry to IPFS, please try again!");
737
734
  process.exit(1);
package/run.js CHANGED
@@ -90,7 +90,7 @@ usage: ecld-run 'hello("World")' | ecld-run --file payload.js | ecld-run --input
90
90
  payload: a code string, or --file <path>; input via --input <file> / --input-text <str>
91
91
  network: --network BLOXBERG_TESTNET (default: BLOCKCHAIN_NETWORK from .env)
92
92
  --unsafe: the network's -unsafe variant, which runs without a CAS
93
- (BLOXBERG_TESTNET_UNSAFE, LITVM_LITEFORGE_UNSAFE)
93
+ (BLOXBERG_TESTNET_UNSAFE, LITVM_LITEFORGE_UNSAFE, ETHEREUM_SEPOLIA_UNSAFE)
94
94
  enclaves: --securelock <name> (default PROJECT_NAME), --trustedzone <name> (default TRUSTED_ZONE_IMAGE);
95
95
  on an -unsafe network the default is each one's -unsafe variant
96
96
  key: PRIVATE_KEY (a funded 0x key) from .env / env, or ECLD_PRIVATE_KEY
@@ -178,7 +178,7 @@ async function main() {
178
178
  // ---- drive the runner ----
179
179
  let runner;
180
180
  try {
181
- runner = new EthernityCloudRunner(network.address, { privateKey }, network.chainId);
181
+ runner = new EthernityCloudRunner(network.address, network.rpcUrl ? { privateKey, rpcUrl: network.rpcUrl } : { privateKey }, network.chainId);
182
182
  if (network.unsafe) await runner.setNetwork(...network.unsafe);
183
183
  } catch (e) {
184
184
  console.error('ecld-run: could not initialise the runner: ' + e.message);