@ethernity-cloud/sdk-js 1.7.17 → 1.7.28
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 +6 -1
- package/cas/config.js +9 -0
- package/cas/session_registry.js +16 -5
- package/info.js +9 -0
- package/init.js +2 -0
- package/network.js +2 -0
- package/nodenithy/build/securelock/Dockerfile.tmpl +6 -3
- package/nodenithy/build/securelock/src/securelock.js.tmpl +12 -0
- package/nodenithy/build/securelock/src/swift_stream_service.js +6 -0
- package/nodenithy/build.mjs +20 -5
- package/nodenithy/nodeFlags.js +24 -0
- package/nodenithy/run/docker-compose-final.yml.tmpl +8 -7
- package/nodenithy/run/docker-compose.yml.tmpl +10 -9
- package/nodenithy/run/etny-securelock-test.yaml.tpl +3 -1
- package/nodenithy/run/image_registry.js +5 -3
- package/nodenithy/run/public_key_service.js +33 -12
- package/nodenithy/run.js +27 -5
- package/package.json +1 -1
- package/pynithy/build.mjs +6 -4
- package/pynithy/run/image_registry.js +5 -3
- package/pynithy/run/public_key_service.js +33 -12
package/README.md
CHANGED
|
@@ -98,7 +98,12 @@ The Pynithy securelock is built exactly as `ethernity-cloud-sdk-py` builds it
|
|
|
98
98
|
your backend is frozen into. `src/serverless/requirements.txt` lists the
|
|
99
99
|
Python packages your backend imports, and `src/serverless/Dockerfile.serverless`
|
|
100
100
|
adds system packages to the securelock base image. `ECLD_MEMORY_TO_ALLOCATE`
|
|
101
|
-
sets the enclave heap (default `1024M`).
|
|
101
|
+
sets the enclave heap (default `1024M`). A Nodenithy securelock built with a
|
|
102
|
+
heap under `1024M` (`ECLD_MEMORY_TO_ALLOCATE=256M`, for nodes with little
|
|
103
|
+
memory) runs node with `--jitless`, since V8's JIT reserves a 512 MB code
|
|
104
|
+
range that fits no smaller heap; the backend's JavaScript then runs
|
|
105
|
+
interpreted, about ten times slower in its crypto. The default `1024M` keeps
|
|
106
|
+
the JIT. `ECLD_NODE_JITLESS=0` or `1` overrides the choice.
|
|
102
107
|
|
|
103
108
|
### Scripts
|
|
104
109
|
|
package/cas/config.js
CHANGED
|
@@ -21,6 +21,9 @@ const VALIDATOR_REGISTRY = {
|
|
|
21
21
|
'ecld-nodenithy-litvm-testnet': '0xbE3759f327e8643fC5A1717fb2072A0dAcBAD29E',
|
|
22
22
|
'ecld-pynithy-ethereum-sepolia': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
|
|
23
23
|
'ecld-nodenithy-ethereum-sepolia': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
|
|
24
|
+
// Amoy's ValidatorRegistry of 2026-10-28 (ethernity-cas CAS-SETUP.md).
|
|
25
|
+
'ecld-pynithy-amoy': '0x70bfdf41D4061592DF509b9b46F91a8457F88483',
|
|
26
|
+
'ecld-nodenithy-amoy': '0x70bfdf41D4061592DF509b9b46F91a8457F88483',
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
const SESSION_REGISTRY = {
|
|
@@ -33,6 +36,9 @@ const SESSION_REGISTRY = {
|
|
|
33
36
|
// Bound to Sepolia's ECImageRegistryV3 of 2026-10-14.
|
|
34
37
|
'ecld-pynithy-ethereum-sepolia': '0xDbB249E108d19fC1691483Bf897bA8BA0dA99c06',
|
|
35
38
|
'ecld-nodenithy-ethereum-sepolia': '0xDbB249E108d19fC1691483Bf897bA8BA0dA99c06',
|
|
39
|
+
// Bound to Amoy's ECImageRegistryV3 of 2026-10-28.
|
|
40
|
+
'ecld-pynithy-amoy': '0x059662030fEeB4Ea8Fc7b05951A578d527a827e5',
|
|
41
|
+
'ecld-nodenithy-amoy': '0x059662030fEeB4Ea8Fc7b05951A578d527a827e5',
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
// The chain the registries above live on. Sepolia's RPC is Tenderly's gateway:
|
|
@@ -44,6 +50,8 @@ const CHAIN = {
|
|
|
44
50
|
'ecld-nodenithy-litvm-testnet': { rpcUrl: 'https://liteforge.rpc.caldera.xyz/infra-partner-http', chainId: 4441 },
|
|
45
51
|
'ecld-pynithy-ethereum-sepolia': { rpcUrl: 'https://sepolia.gateway.tenderly.co', chainId: 11155111 },
|
|
46
52
|
'ecld-nodenithy-ethereum-sepolia': { rpcUrl: 'https://sepolia.gateway.tenderly.co', chainId: 11155111 },
|
|
53
|
+
'ecld-pynithy-amoy': { rpcUrl: 'https://polygon-amoy-bor-rpc.publicnode.com', chainId: 80002 },
|
|
54
|
+
'ecld-nodenithy-amoy': { rpcUrl: 'https://polygon-amoy-bor-rpc.publicnode.com', chainId: 80002 },
|
|
47
55
|
};
|
|
48
56
|
|
|
49
57
|
// Whether the securelock takes its certificate from a CAS session: mainnet
|
|
@@ -67,6 +75,7 @@ const UNSAFE_NETWORKS = {
|
|
|
67
75
|
Bloxberg_Testnet_Unsafe: 'Bloxberg_Testnet',
|
|
68
76
|
LitVM_LiteForge_Unsafe: 'LitVM_LiteForge',
|
|
69
77
|
Ethereum_Sepolia_Unsafe: 'Ethereum_Sepolia',
|
|
78
|
+
Polygon_Amoy_Testnet_Unsafe: 'Polygon_Amoy_Testnet',
|
|
70
79
|
};
|
|
71
80
|
|
|
72
81
|
// Case-insensitive: ecld-init writes Bloxberg_Testnet_Unsafe, a --network
|
package/cas/session_registry.js
CHANGED
|
@@ -98,12 +98,23 @@ async function latestRecord(reg, name) {
|
|
|
98
98
|
return { id, hash: rec.sessionHash.toLowerCase(), creator: rec.creator };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// The legacy gas price a registry write is sent with: the chain's eth_gasPrice
|
|
102
|
+
// plus 10 %, and 1 mwei at the least. The bloxberg testnet reports 1 mwei;
|
|
103
|
+
// Amoy's Bor reports about 30 gwei and refuses a price under its 25 gwei
|
|
104
|
+
// minimum tip; LitVM refuses a price under its base fee.
|
|
105
|
+
const MIN_GAS_PRICE = ethers.utils.parseUnits('1', 'mwei');
|
|
106
|
+
async function gasPrice(provider) {
|
|
107
|
+
const price = (await provider.getGasPrice()).mul(110).div(100);
|
|
108
|
+
return price.gt(MIN_GAS_PRICE) ? price : MIN_GAS_PRICE;
|
|
109
|
+
}
|
|
110
|
+
|
|
101
111
|
// Send `method(...args)` to the registry. It is simulated first, so a call
|
|
102
112
|
// the registry refuses stops with the registry's reason (`refused` names the
|
|
103
|
-
// call), then sent with its gas estimate plus 30
|
|
104
|
-
// grows with the length of the name
|
|
105
|
-
// serves (ethers takes no chainId override on a
|
|
106
|
-
// serving another chain than `chainId` is refused
|
|
113
|
+
// call), then sent with its gas estimate plus 30% (a registration's cost
|
|
114
|
+
// grows with the length of the name) at the chain's gas price. The wallet
|
|
115
|
+
// signs for the chain its RPC serves (ethers takes no chainId override on a
|
|
116
|
+
// contract call), so an RPC serving another chain than `chainId` is refused
|
|
117
|
+
// before anything is sent.
|
|
107
118
|
async function send(reg, chainId, refused, method, ...args) {
|
|
108
119
|
const { chainId: served } = await reg.provider.getNetwork();
|
|
109
120
|
if (served !== chainId) {
|
|
@@ -116,7 +127,7 @@ async function send(reg, chainId, refused, method, ...args) {
|
|
|
116
127
|
}
|
|
117
128
|
const gas = await reg.estimateGas[method](...args);
|
|
118
129
|
const tx = await reg[method](...args, {
|
|
119
|
-
gasLimit: gas.mul(13).div(10), gasPrice:
|
|
130
|
+
gasLimit: gas.mul(13).div(10), gasPrice: await gasPrice(reg.provider),
|
|
120
131
|
});
|
|
121
132
|
const rcpt = await tx.wait();
|
|
122
133
|
if (rcpt.status !== 1) throw new Error(`SessionRegistry.${method} reverted for ${refused} (tx ${tx.hash})`);
|
package/info.js
CHANGED
|
@@ -55,6 +55,15 @@ const NETWORKS = {
|
|
|
55
55
|
imageRegistry: '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D',
|
|
56
56
|
esr: '0x709052Fe77Af543d3d9FE2Ac06a15c635c8D4Be5',
|
|
57
57
|
},
|
|
58
|
+
// The separate-ERC20 implementation 1.1 of 2026-10-28 on the storage and
|
|
59
|
+
// token of the previous implementation 0x1579b37C...; the V3 registry and
|
|
60
|
+
// the ESR of the same day.
|
|
61
|
+
POLYGON_AMOY_TESTNET: {
|
|
62
|
+
type: 'testnet', chainId: 80002, rpc: 'https://polygon-amoy-bor-rpc.publicnode.com',
|
|
63
|
+
protocol: '0xB8f46D2B943fFbe2A061Ae8be087ef5055AD96A3',
|
|
64
|
+
imageRegistry: '0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706',
|
|
65
|
+
esr: '0x1DC58496ed1cA66FD1E0b51fA28A9E7451970843',
|
|
66
|
+
},
|
|
58
67
|
// The family of 2026-10-14, owned by 0xA247...; the previous PoX 0x29D3eC87...
|
|
59
68
|
// and V1 registry 0x55e0ad45... are abandoned.
|
|
60
69
|
ETHEREUM_SEPOLIA: {
|
package/init.js
CHANGED
|
@@ -215,6 +215,7 @@ const main = async () => {
|
|
|
215
215
|
"Bloxberg Testnet Unsafe",
|
|
216
216
|
"LitVM LiteForge Unsafe",
|
|
217
217
|
"Ethereum Sepolia Unsafe",
|
|
218
|
+
"Polygon Amoy Testnet Unsafe",
|
|
218
219
|
];
|
|
219
220
|
const blockchainNetwork = await promptOptions(
|
|
220
221
|
"On which Blockchain network do you want to have the app set up, as a starting point? (default is Bloxberg Testnet): ",
|
|
@@ -392,6 +393,7 @@ const main = async () => {
|
|
|
392
393
|
"Bloxberg Testnet Unsafe": `etny-${svc}-testnet-unsafe`,
|
|
393
394
|
"LitVM LiteForge Unsafe": `ecld-${svc}-litvm-testnet-unsafe`,
|
|
394
395
|
"Ethereum Sepolia Unsafe": `ecld-${svc}-ethereum-sepolia-unsafe`,
|
|
396
|
+
"Polygon Amoy Testnet Unsafe": `ecld-${svc}-amoy-unsafe`,
|
|
395
397
|
};
|
|
396
398
|
const trustedZoneImage = imageByNetwork[blockchainNetwork] || `etny-${svc}-testnet`;
|
|
397
399
|
writeEnv("TRUSTED_ZONE_IMAGE", trustedZoneImage);
|
package/network.js
CHANGED
|
@@ -29,6 +29,8 @@ function networkFor({ ECAddress, ECNetworkByChainId }, raw) {
|
|
|
29
29
|
POLYGON_MAINNET: { address: ECAddress.POLYGON.MAINNET_ADDRESS },
|
|
30
30
|
POLYGON_AMOY: amoy,
|
|
31
31
|
POLYGON_AMOY_TESTNET: amoy,
|
|
32
|
+
POLYGON_AMOY_UNSAFE: { ...amoy, unsafe: ['POLYGON', 'TESTNET_UNSAFE'] },
|
|
33
|
+
POLYGON_AMOY_TESTNET_UNSAFE: { ...amoy, unsafe: ['POLYGON', 'TESTNET_UNSAFE'] },
|
|
32
34
|
IOTEX_TESTNET: { address: ECAddress.IOTEX.TESTNET_ADDRESS, chainId: ECNetworkByChainId.IOTEX.TESTNET, rpcUrl: 'https://babel-api.testnet.iotex.io' },
|
|
33
35
|
ETHEREUM_SEPOLIA: sepolia,
|
|
34
36
|
SEPOLIA: sepolia,
|
|
@@ -63,8 +63,9 @@ RUN openssl genrsa -3 -out /enclave-key.pem 3072
|
|
|
63
63
|
# scone-signer below (SCONE embeds defaults for anything not signed explicitly;
|
|
64
64
|
# any drift makes SCONE recompute + debug re-sign the enclave at load, which CAS
|
|
65
65
|
# rejects on mainnet). Values mirror the publish/run docker-compose securelock
|
|
66
|
-
# service (SCONE_HEAP
|
|
67
|
-
|
|
66
|
+
# service (SCONE_HEAP as build.mjs's MEMORY_TO_ALLOCATE, SCONE_STACK=4M,
|
|
67
|
+
# dlopen=1, extensions=libbinary-fs).
|
|
68
|
+
ENV SCONE_HEAP=__MEMORY_TO_ALLOCATE__
|
|
68
69
|
ENV SCONE_STACK=4M
|
|
69
70
|
ENV SCONE_ALLOW_DLOPEN=1
|
|
70
71
|
ENV SCONE_EXTENSIONS_PATH=/lib/libbinary-fs.so
|
|
@@ -86,4 +87,6 @@ __SIGNED_MRENCLAVE__
|
|
|
86
87
|
|
|
87
88
|
RUN rm -rf /enclave-key.pem
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
# The node flags (build.mjs NODE_FLAGS) are also the CAS session's command and
|
|
91
|
+
# the published .env's SECURELOCK_NODE_FLAGS; the composes run this ENTRYPOINT.
|
|
92
|
+
ENTRYPOINT ["/usr/local/bin/node"__NODE_FLAGS__, "/etny-securelock/securelock.js"]
|
|
@@ -21,6 +21,18 @@ const {executeTask} = require("./etny_exec");
|
|
|
21
21
|
const {TaskStatus} = require("./task_status");
|
|
22
22
|
const SwiftStreamService = require("./swift_stream_service");
|
|
23
23
|
|
|
24
|
+
// An error a network socket emits after its request's caller has moved on
|
|
25
|
+
// (a connection the server closed, ECONNRESET) reaches no handler of the
|
|
26
|
+
// request; without these the node runtime exits on it, and the task is lost.
|
|
27
|
+
// Logged and the run goes on: every step awaits its own result and treats a
|
|
28
|
+
// failed one.
|
|
29
|
+
process.on('uncaughtException', (e) => {
|
|
30
|
+
console.log('uncaught exception:', e && (e.stack || e.message) ? (e.stack || e.message) : e);
|
|
31
|
+
});
|
|
32
|
+
process.on('unhandledRejection', (e) => {
|
|
33
|
+
console.log('unhandled rejection:', e && (e.stack || e.message) ? (e.stack || e.message) : e);
|
|
34
|
+
});
|
|
35
|
+
|
|
24
36
|
const contract_abi = require('./pox_abi');
|
|
25
37
|
const image_registry_abi = require('./image_registry_abi');
|
|
26
38
|
const {sha256} = require("./etny_crypto");
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
const Minio = require('minio');
|
|
2
|
+
const http = require('http');
|
|
2
3
|
const fs = require("fs");
|
|
3
4
|
class SwiftStreamService {
|
|
4
5
|
constructor(endpoint, port, accessKey, secretKey) {
|
|
6
|
+
// One connection per request: the minio client otherwise uses node's
|
|
7
|
+
// global agent, which keeps connections alive, and a connection the
|
|
8
|
+
// server has closed while this process computed between two requests
|
|
9
|
+
// fails the next request with ECONNRESET ("socket hang up").
|
|
5
10
|
this.client = new Minio.Client({
|
|
6
11
|
endPoint: endpoint,
|
|
7
12
|
port: port,
|
|
8
13
|
accessKey: accessKey,
|
|
9
14
|
secretKey: secretKey,
|
|
10
15
|
useSSL: false,
|
|
16
|
+
transportAgent: new http.Agent({ keepAlive: false }),
|
|
11
17
|
});
|
|
12
18
|
}
|
|
13
19
|
|
package/nodenithy/build.mjs
CHANGED
|
@@ -39,9 +39,11 @@ export const ECRunner = {
|
|
|
39
39
|
// --- polygon mainnet ---
|
|
40
40
|
'ecld-pynithy': ['0x439945BE73fD86fcC172179021991E96Beff3Cc4', '0x689f3806874d3c8A973f419a4eB24e6fBA7E830F', 'https://polygon-bor-rpc.publicnode.com', 137],
|
|
41
41
|
'ecld-nodenithy': ['0x439945BE73fD86fcC172179021991E96Beff3Cc4', '0x689f3806874d3c8A973f419a4eB24e6fBA7E830F', 'https://polygon-bor-rpc.publicnode.com', 137],
|
|
42
|
-
// --- polygon amoy testnet
|
|
43
|
-
'ecld-pynithy-amoy': ['
|
|
44
|
-
'ecld-nodenithy-amoy': ['
|
|
42
|
+
// --- polygon amoy testnet: the separate-ERC20 PoX EthernityImplementationV2 1.1 and the ECImageRegistryV3 of 2026-10-28 ---
|
|
43
|
+
'ecld-pynithy-amoy': ['0xB8f46D2B943fFbe2A061Ae8be087ef5055AD96A3', '0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706', 'https://polygon-amoy-bor-rpc.publicnode.com', 80002],
|
|
44
|
+
'ecld-nodenithy-amoy': ['0xB8f46D2B943fFbe2A061Ae8be087ef5055AD96A3', '0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706', 'https://polygon-amoy-bor-rpc.publicnode.com', 80002],
|
|
45
|
+
'ecld-pynithy-amoy-unsafe': ['0xB8f46D2B943fFbe2A061Ae8be087ef5055AD96A3', '0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706', 'https://polygon-amoy-bor-rpc.publicnode.com', 80002],
|
|
46
|
+
'ecld-nodenithy-amoy-unsafe': ['0xB8f46D2B943fFbe2A061Ae8be087ef5055AD96A3', '0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706', 'https://polygon-amoy-bor-rpc.publicnode.com', 80002],
|
|
45
47
|
// --- iotex testnet ---
|
|
46
48
|
'ecld-pynithy-iotex-testnet': ['0xD56385A97413Ed80E28B1b54A193b98F2C49c975', '0xa7467A6391816be9367a1cC52E0ef0c15FfE3cCC', 'https://babel-api.testnet.iotex.io', 4690],
|
|
47
49
|
'ecld-nodenithy-iotex-testnet': ['0xD56385A97413Ed80E28B1b54A193b98F2C49c975', '0xa7467A6391816be9367a1cC52E0ef0c15FfE3cCC', 'https://babel-api.testnet.iotex.io', 4690],
|
|
@@ -68,6 +70,7 @@ export const ECRunner = {
|
|
|
68
70
|
// matching SessionRegistry.
|
|
69
71
|
const require = createRequire(import.meta.url);
|
|
70
72
|
const casConfig = require('../cas/config.js');
|
|
73
|
+
const nodeFlags = require('./nodeFlags.js');
|
|
71
74
|
export const VALIDATOR_REGISTRY = casConfig.VALIDATOR_REGISTRY;
|
|
72
75
|
|
|
73
76
|
// An -unsafe network runs an -unsafe trustedzone and nothing else, and an
|
|
@@ -215,6 +218,8 @@ const trustedZoneNetByBlockchain = {
|
|
|
215
218
|
LitVM_LiteForge: 'litvm_liteforge',
|
|
216
219
|
Bloxberg_Testnet_Unsafe: 'bloxberg_testnet_unsafe',
|
|
217
220
|
LitVM_LiteForge_Unsafe: 'litvm_liteforge_unsafe',
|
|
221
|
+
Ethereum_Sepolia_Unsafe: 'ethereum_sepolia_unsafe',
|
|
222
|
+
Polygon_Amoy_Testnet_Unsafe: 'amoy_unsafe',
|
|
218
223
|
};
|
|
219
224
|
const trustedZoneNet = trustedZoneNetByBlockchain[process.env.BLOCKCHAIN_NETWORK];
|
|
220
225
|
if (!trustedZoneNet) {
|
|
@@ -263,9 +268,17 @@ const esrEnvBlock = ESR_ENABLED ? `ENV ESR_CONTRACT_ADDRESS=${ESR_CONTRACT_ADDRE
|
|
|
263
268
|
const dockerfileSecureTemplate = fs.readFileSync('Dockerfile.tmpl', 'utf8');
|
|
264
269
|
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` : '');
|
|
265
270
|
|
|
266
|
-
//
|
|
267
|
-
// run/docker-compose securelock service;
|
|
271
|
+
// The securelock's protected heap (SCONE_HEAP), signed into the enclave and
|
|
272
|
+
// set on the run/docker-compose securelock service; ECLD_MEMORY_TO_ALLOCATE
|
|
273
|
+
// overrides the default. The default 1024M keeps V8's JIT, whose code range
|
|
274
|
+
// (512 MB, reserved at start) fits no smaller heap; a heap under 1024M runs
|
|
275
|
+
// node --jitless (nodeFlags.js), the backend's JavaScript interpreted.
|
|
268
276
|
const MEMORY_TO_ALLOCATE = (process.env.ECLD_MEMORY_TO_ALLOCATE || '1024M').trim();
|
|
277
|
+
const NODE_FLAGS = nodeFlags.securelockNodeFlags(MEMORY_TO_ALLOCATE);
|
|
278
|
+
// Read back by ecld-publish (run.js), which renders them into the composes'
|
|
279
|
+
// securelock service and the CAS session's command.
|
|
280
|
+
writeEnv('MEMORY_TO_ALLOCATE', MEMORY_TO_ALLOCATE);
|
|
281
|
+
writeEnv('SECURELOCK_NODE_FLAGS', NODE_FLAGS.join(' '));
|
|
269
282
|
|
|
270
283
|
// CRITICAL (mainnet DCAP): sign /usr/local/bin/node -- the binary the enclave
|
|
271
284
|
// actually EXECUTES (ENTRYPOINT + the run/publish compose command both run
|
|
@@ -297,6 +310,8 @@ const signedMrenclaveStep =
|
|
|
297
310
|
// securelock is signed debug with the same explicit params.
|
|
298
311
|
const cas = casConfig.casProvisioned(templateName, isMainnet);
|
|
299
312
|
dockerfileSecureContent = dockerfileSecureContent
|
|
313
|
+
.replace(/__MEMORY_TO_ALLOCATE__/g, MEMORY_TO_ALLOCATE)
|
|
314
|
+
.replace(/__NODE_FLAGS__/g, NODE_FLAGS.map((flag) => `, "${flag}"`).join(''))
|
|
300
315
|
.replace('__SCONE_SIGN__', `RUN scone-signer sign ${signFlags}${cas ? ' --production' : ''} /usr/local/bin/node`)
|
|
301
316
|
.replace('__SIGNED_MRENCLAVE__', signedMrenclaveStep);
|
|
302
317
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The node flags the securelock is built and run with, derived from its
|
|
2
|
+
// protected heap (SCONE_HEAP). V8 reserves a 512 MB code range for its JIT at
|
|
3
|
+
// start, which fits a 1024M heap and not a smaller one; a smaller heap runs
|
|
4
|
+
// node --jitless, which reserves no code range and runs JavaScript
|
|
5
|
+
// interpreted. ECLD_NODE_JITLESS=1 or 0 overrides the derivation.
|
|
6
|
+
const MEBIBYTE = 1024 * 1024;
|
|
7
|
+
const JIT_HEAP_BYTES = 1024 * MEBIBYTE;
|
|
8
|
+
|
|
9
|
+
// Bytes of a SCONE_HEAP value: a whole number with an optional K, M or G.
|
|
10
|
+
const heapBytes = (heap) => {
|
|
11
|
+
const m = /^(\d+)([KMG])?$/i.exec(String(heap).trim());
|
|
12
|
+
if (!m) throw new Error(`heap ${JSON.stringify(heap)}: expected <n>, <n>K, <n>M or <n>G`);
|
|
13
|
+
const unit = { K: 1024, M: MEBIBYTE, G: 1024 * MEBIBYTE }[(m[2] || '').toUpperCase()] || 1;
|
|
14
|
+
return Number(m[1]) * unit;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const securelockNodeFlags = (heap, override = process.env.ECLD_NODE_JITLESS) => {
|
|
18
|
+
const jitless = override === undefined || String(override).trim() === ''
|
|
19
|
+
? heapBytes(heap) < JIT_HEAP_BYTES
|
|
20
|
+
: /^(1|true|yes)$/i.test(String(override).trim());
|
|
21
|
+
return jitless ? ['--jitless'] : [];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
module.exports = { securelockNodeFlags, heapBytes, JIT_HEAP_BYTES };
|
|
@@ -30,15 +30,16 @@ services:
|
|
|
30
30
|
container_name: etny-securelock
|
|
31
31
|
privileged: true
|
|
32
32
|
image: localhost:5000/etny-securelock
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
# No command: the image's ENTRYPOINT runs node with the flags the
|
|
34
|
+
# securelock was built and measured with (build.mjs NODE_FLAGS).
|
|
35
35
|
environment:
|
|
36
36
|
- SCONE_CAS_ADDR=scone-cas.cf # __CAS_ONLY__
|
|
37
37
|
- SCONE_LAS_ADDR=las # __CAS_ONLY__
|
|
38
38
|
- SCONE_CONFIG_ID=__ENCLAVE_NAME_SECURELOCK__/application # __CAS_ONLY__
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
#
|
|
39
|
+
# Must match the --heap and --stack the securelock binary was signed with
|
|
40
|
+
# (build.mjs), otherwise SCONE recomputes MRENCLAVE at load and re-signs
|
|
41
|
+
# as debug. The heap is the build's MEMORY_TO_ALLOCATE (.env).
|
|
42
|
+
- SCONE_HEAP=__MEMORY_TO_ALLOCATE__
|
|
42
43
|
- SCONE_STACK=4M
|
|
43
44
|
- SCONE_LOG=DEBUG
|
|
44
45
|
- SCONE_ALLOW_DLOPEN=1
|
|
@@ -55,8 +56,8 @@ services:
|
|
|
55
56
|
container_name: etny-trustedzone
|
|
56
57
|
privileged: true
|
|
57
58
|
image: localhost:5000/etny-trustedzone
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
# No command: the image's ENTRYPOINT runs node with the flags the
|
|
60
|
+
# trustedzone was built and measured with (etny-nodenithy Dockerfile.tmpl).
|
|
60
61
|
environment:
|
|
61
62
|
- SCONE_CAS_ADDR=scone-cas.cf # __CAS_ONLY__
|
|
62
63
|
- SCONE_LAS_ADDR=las # __CAS_ONLY__
|
|
@@ -33,15 +33,16 @@ services:
|
|
|
33
33
|
container_name: etny-securelock
|
|
34
34
|
privileged: true
|
|
35
35
|
image: localhost:5000/etny-securelock
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# No command: the image's ENTRYPOINT runs node with the flags the
|
|
37
|
+
# securelock was built and measured with (build.mjs NODE_FLAGS).
|
|
38
38
|
environment:
|
|
39
39
|
- SCONE_CAS_ADDR=scone-cas.cf # __CAS_ONLY__
|
|
40
40
|
- SCONE_LAS_ADDR=las # __CAS_ONLY__
|
|
41
41
|
- SCONE_CONFIG_ID=__ENCLAVE_NAME_SECURELOCK__/application # __CAS_ONLY__
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
#
|
|
42
|
+
# Must match the --heap and --stack the securelock binary was signed with
|
|
43
|
+
# (build.mjs), otherwise SCONE recomputes MRENCLAVE at load and re-signs
|
|
44
|
+
# as debug. The heap is the build's MEMORY_TO_ALLOCATE (.env).
|
|
45
|
+
- SCONE_HEAP=__MEMORY_TO_ALLOCATE__
|
|
45
46
|
- SCONE_STACK=4M
|
|
46
47
|
- SCONE_LOG=DEBUG
|
|
47
48
|
- SCONE_ALLOW_DLOPEN=1
|
|
@@ -54,14 +55,14 @@ services:
|
|
|
54
55
|
container_name: etny-trustedzone
|
|
55
56
|
privileged: true
|
|
56
57
|
image: localhost:5000/etny-trustedzone
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
# No command: the image's ENTRYPOINT runs node with the flags the
|
|
59
|
+
# trustedzone was built and measured with (etny-nodenithy Dockerfile.tmpl).
|
|
59
60
|
environment:
|
|
60
61
|
- SCONE_CAS_ADDR=scone-cas.cf # __CAS_ONLY__
|
|
61
62
|
- SCONE_LAS_ADDR=las # __CAS_ONLY__
|
|
62
63
|
- SCONE_CONFIG_ID=__ENCLAVE_NAME_TRUSTEDZONE__/application # __CAS_ONLY__
|
|
63
|
-
# The heap the trustedzone was
|
|
64
|
-
#
|
|
64
|
+
# The heap the trustedzone was signed with: the SCONE_HEAP of the
|
|
65
|
+
# bundled trustedzone image (run.js reads it from the image).
|
|
65
66
|
- SCONE_HEAP=__TRUSTEDZONE_HEAP__
|
|
66
67
|
- SCONE_LOG=DEBUG
|
|
67
68
|
- SCONE_ALLOW_DLOPEN=1
|
|
@@ -16,7 +16,9 @@ services:
|
|
|
16
16
|
- name: application
|
|
17
17
|
image_name: application_image
|
|
18
18
|
mrenclaves: [ "__MRENCLAVE__" ]
|
|
19
|
-
command
|
|
19
|
+
# The command the enclave runs, applied from the session at attestation:
|
|
20
|
+
# the image ENTRYPOINT's, with the build's node flags (run.js NODE_FLAGS).
|
|
21
|
+
command: /usr/local/bin/node __NODE_FLAGS__/etny-securelock/securelock.js
|
|
20
22
|
pwd: /
|
|
21
23
|
environment:
|
|
22
24
|
GREETING: hello from ETNY SECURELOCK!!!!
|
|
@@ -25,17 +25,19 @@ function setVars(network = "") {
|
|
|
25
25
|
: "0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31";
|
|
26
26
|
} else if (BLOCKCHAIN_NETWORK.includes("Polygon")) {
|
|
27
27
|
if (BLOCKCHAIN_NETWORK.includes("Mainnet")) {
|
|
28
|
-
NETWORK_RPC = "https://polygon-rpc.com";
|
|
28
|
+
NETWORK_RPC = "https://polygon-bor-rpc.publicnode.com";
|
|
29
29
|
IMAGE_REGISTRY_ADDRESS = "0x689f3806874d3c8A973f419a4eB24e6fBA7E830F";
|
|
30
30
|
CHAIN_ID = 137;
|
|
31
31
|
GAS = 20000000;
|
|
32
32
|
GAS_PRICE = 40500500010;
|
|
33
33
|
} else {
|
|
34
|
+
// Amoy's ECImageRegistryV3 of 2026-10-28; the chain's base fee is
|
|
35
|
+
// about 30 gwei.
|
|
34
36
|
NETWORK_RPC = "https://polygon-amoy-bor-rpc.publicnode.com";
|
|
35
|
-
IMAGE_REGISTRY_ADDRESS = "
|
|
37
|
+
IMAGE_REGISTRY_ADDRESS = "0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706";
|
|
36
38
|
CHAIN_ID = 80002;
|
|
37
39
|
GAS = 20000000;
|
|
38
|
-
GAS_PRICE =
|
|
40
|
+
GAS_PRICE = 40000000010;
|
|
39
41
|
}
|
|
40
42
|
} else if (BLOCKCHAIN_NETWORK.includes("LitVM")) {
|
|
41
43
|
NETWORK_RPC = "https://liteforge.rpc.caldera.xyz/infra-partner-http";
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// registries) is queued by the service's registry watcher from the
|
|
6
6
|
// ImageRegistered event, with this publish's IPFS peer; the hash is posted to
|
|
7
7
|
// /api/addHash only when that row has not appeared within
|
|
8
|
-
// CHAIN_ROW_GRACE_SECONDS
|
|
8
|
+
// CHAIN_ROW_GRACE_SECONDS, or when the service holds a failed extraction of
|
|
9
|
+
// the image. Other images are posted at once.
|
|
9
10
|
const axios = require('axios');
|
|
10
11
|
const fs = require('fs');
|
|
11
12
|
const { Command } = require('commander');
|
|
@@ -14,6 +15,14 @@ const BASE_URL = "https://publickey.ethernity.cloud";
|
|
|
14
15
|
// The watcher polls the registries every 30 s.
|
|
15
16
|
const CHAIN_ROW_GRACE_SECONDS = 180;
|
|
16
17
|
const POLL_SECONDS = 10;
|
|
18
|
+
// A failed extraction is recorded against the hash with publicKey -1 and a
|
|
19
|
+
// reason, and a new submission of the hash runs it again. The service answers
|
|
20
|
+
// the old record for a moment after a submission, so a failed answer is taken
|
|
21
|
+
// as the outcome only once this long has passed since this publish submitted.
|
|
22
|
+
const RESUBMIT_GRACE_SECONDS = 60;
|
|
23
|
+
|
|
24
|
+
// The service's record of an extraction that failed.
|
|
25
|
+
const failed = (answer) => answer !== null && answer.publicKey === -1;
|
|
17
26
|
|
|
18
27
|
async function submitIpfsHash(hash, enclaveName, protocolVersion, network, templateVersion, dockerComposerHash) {
|
|
19
28
|
const url = `${BASE_URL}/api/addHash`;
|
|
@@ -48,9 +57,10 @@ async function checkIpfsHashStatus(hash) {
|
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
// Queues the image as described above and returns the service's first
|
|
52
|
-
//
|
|
53
|
-
//
|
|
60
|
+
// Queues the image as described above and returns the service's first answer
|
|
61
|
+
// for it (`answer`, null right after a submission) and whether this publish
|
|
62
|
+
// posted the hash (`submitted`). `submit`, `check`, `sleep` and `now` are the
|
|
63
|
+
// functions above and the clock, replaceable for tests.
|
|
54
64
|
async function queueImage({ hash, chainRegistered, submitArgs, submit = submitIpfsHash, check = checkIpfsHashStatus,
|
|
55
65
|
sleep = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000)), now = () => Date.now() / 1000,
|
|
56
66
|
log = console.log, graceSeconds = CHAIN_ROW_GRACE_SECONDS, pollSeconds = POLL_SECONDS }) {
|
|
@@ -58,7 +68,11 @@ async function queueImage({ hash, chainRegistered, submitArgs, submit = submitIp
|
|
|
58
68
|
const deadline = now() + graceSeconds;
|
|
59
69
|
while (true) {
|
|
60
70
|
const answer = await check(hash);
|
|
61
|
-
if (answer
|
|
71
|
+
if (failed(answer)) {
|
|
72
|
+
log(`The service's previous extraction of this image failed (${answer.reason || "no reason reported"}); submitting the hash again`);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (answer !== null) return { answer, submitted: false };
|
|
62
76
|
if (now() >= deadline) {
|
|
63
77
|
log(`The certificate service has not picked the registration up from the chain in ${graceSeconds} s; submitting the hash`);
|
|
64
78
|
break;
|
|
@@ -69,7 +83,7 @@ async function queueImage({ hash, chainRegistered, submitArgs, submit = submitIp
|
|
|
69
83
|
}
|
|
70
84
|
const submitResponse = await submit(hash, ...submitArgs);
|
|
71
85
|
log("Submit IPFS Hash Response:", submitResponse);
|
|
72
|
-
return null;
|
|
86
|
+
return { answer: null, submitted: true };
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
async function main() {
|
|
@@ -98,10 +112,12 @@ async function main() {
|
|
|
98
112
|
console.log("Template Version:", templateVersion);
|
|
99
113
|
console.log("Docker Composer Hash:", dockerComposerHash);
|
|
100
114
|
|
|
101
|
-
|
|
115
|
+
const queued = await queueImage({
|
|
102
116
|
hash: hhash, chainRegistered: Boolean(options.chainRegistered),
|
|
103
117
|
submitArgs: [enclaveName, protocolVersion, network, templateVersion, dockerComposerHash],
|
|
104
118
|
});
|
|
119
|
+
let checkResponse = queued.answer;
|
|
120
|
+
const submittedAt = queued.submitted ? Date.now() / 1000 : null;
|
|
105
121
|
|
|
106
122
|
// The service answers publicKey 0 while the image is queued or running
|
|
107
123
|
// (with status, percent and queuePosition), -1 when the extraction failed
|
|
@@ -119,10 +135,15 @@ async function main() {
|
|
|
119
135
|
} else {
|
|
120
136
|
console.log(`Public key not available yet. Queue position: ${checkResponse.queuePosition ?? 'Unknown'}`);
|
|
121
137
|
}
|
|
122
|
-
} else if (checkResponse
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
} else if (failed(checkResponse)) {
|
|
139
|
+
if (submittedAt !== null && Date.now() / 1000 - submittedAt < RESUBMIT_GRACE_SECONDS) {
|
|
140
|
+
// The record of the run before this publish's submission.
|
|
141
|
+
console.log("The service still answers its previous record; waiting for the new run");
|
|
142
|
+
} else {
|
|
143
|
+
console.log(`Public key extraction failed: ${checkResponse.reason || "reason not reported by the service"}`);
|
|
144
|
+
console.log("Fix the cause (keep the image pinned, build with the current SDK) and publish again: a new submission of the same hash runs the extraction again.");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
126
147
|
} else {
|
|
127
148
|
console.log("Public Key:", checkResponse.publicKey);
|
|
128
149
|
// Save public key to file
|
|
@@ -152,4 +173,4 @@ async function main() {
|
|
|
152
173
|
|
|
153
174
|
if (require.main === module) main();
|
|
154
175
|
|
|
155
|
-
module.exports = { queueImage, CHAIN_ROW_GRACE_SECONDS, POLL_SECONDS };
|
|
176
|
+
module.exports = { queueImage, CHAIN_ROW_GRACE_SECONDS, POLL_SECONDS, RESUBMIT_GRACE_SECONDS };
|
package/nodenithy/run.js
CHANGED
|
@@ -113,12 +113,28 @@ const extractSignedMrenclave = (service) => {
|
|
|
113
113
|
}
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
+
// The heap the trustedzone was signed with: the SCONE_HEAP of the image the
|
|
117
|
+
// build bundled (etny-nodenithy's Dockerfile sets it to the --heap it signs
|
|
118
|
+
// with). The compose must name the same value, or SCONE recomputes the
|
|
119
|
+
// measurement at load and the CAS refuses the enclave.
|
|
120
|
+
const trustedZoneHeapOfImage = (image) => {
|
|
121
|
+
const env = execSync(`docker image inspect ${image} --format "{{range .Config.Env}}{{println .}}{{end}}"`,
|
|
122
|
+
{ stdio: ['pipe', 'pipe', 'pipe'] }).toString();
|
|
123
|
+
const m = env.match(/^SCONE_HEAP=(\S+)$/m);
|
|
124
|
+
if (!m) throw new Error(`${image} carries no SCONE_HEAP in its environment`);
|
|
125
|
+
return m[1];
|
|
126
|
+
};
|
|
127
|
+
|
|
116
128
|
const main = async () => {
|
|
117
129
|
process.env.NODE_NO_WARNINGS = 1
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
const
|
|
130
|
+
const trustedZoneHeap = trustedZoneHeapOfImage('localhost:5000/etny-trustedzone');
|
|
131
|
+
console.log(`Trustedzone heap: ${trustedZoneHeap}`);
|
|
132
|
+
// The securelock heap ecld-build signed it with (part of MRENCLAVE).
|
|
133
|
+
const memory = (process.env.MEMORY_TO_ALLOCATE || '').trim();
|
|
134
|
+
if (!memory) {
|
|
135
|
+
console.error("Error: MEMORY_TO_ALLOCATE is not set in .env; run ecld-build first.");
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
122
138
|
|
|
123
139
|
// Where the image goes. An IPFS API of the application's own
|
|
124
140
|
// (IPFS_ENDPOINT, with IPFS_TOKEN as its Authorization header when set)
|
|
@@ -208,7 +224,8 @@ const main = async () => {
|
|
|
208
224
|
return;
|
|
209
225
|
}
|
|
210
226
|
const content = casConfig.renderCompose(fs.readFileSync(template, 'utf8'), cas, unsafe)
|
|
211
|
-
.replace(/__TRUSTEDZONE_HEAP__/g, trustedZoneHeap)
|
|
227
|
+
.replace(/__TRUSTEDZONE_HEAP__/g, trustedZoneHeap)
|
|
228
|
+
.replace(/__MEMORY_TO_ALLOCATE__/g, memory);
|
|
212
229
|
fs.writeFileSync(file, content, 'utf8');
|
|
213
230
|
});
|
|
214
231
|
|
|
@@ -308,10 +325,15 @@ const main = async () => {
|
|
|
308
325
|
PREDECESSOR_HASH_SECURELOCK = 'EMPTY';
|
|
309
326
|
}
|
|
310
327
|
|
|
328
|
+
// The node flags the build gave the securelock (.env SECURELOCK_NODE_FLAGS,
|
|
329
|
+
// build.mjs): the session's command runs node with the same flags as the
|
|
330
|
+
// image ENTRYPOINT. A .env without the key is a build without flags.
|
|
331
|
+
const nodeFlags = (process.env.SECURELOCK_NODE_FLAGS || '').trim();
|
|
311
332
|
const replacementsSecurelock = {
|
|
312
333
|
PREDECESSOR: PREDECESSOR_HASH_SECURELOCK === 'EMPTY' ? `# predecessor: ${PREDECESSOR_HASH_SECURELOCK}` : `predecessor: ${PREDECESSOR_HASH_SECURELOCK}`,
|
|
313
334
|
MRENCLAVE: process.env.MRENCLAVE_SECURELOCK,
|
|
314
335
|
ENCLAVE_NAME: ENCLAVE_NAME_SECURELOCK,
|
|
336
|
+
NODE_FLAGS: nodeFlags ? `${nodeFlags} ` : '',
|
|
315
337
|
ESR_ENVIRONMENT: casConfig.esrSessionEnvironment(process.env)
|
|
316
338
|
};
|
|
317
339
|
if (cas) {
|
package/package.json
CHANGED
package/pynithy/build.mjs
CHANGED
|
@@ -39,9 +39,11 @@ export const ECRunner = {
|
|
|
39
39
|
// --- polygon mainnet ---
|
|
40
40
|
'ecld-pynithy': ['0x439945BE73fD86fcC172179021991E96Beff3Cc4', '0x689f3806874d3c8A973f419a4eB24e6fBA7E830F', 'https://polygon-bor-rpc.publicnode.com', 137],
|
|
41
41
|
'ecld-nodenithy': ['0x439945BE73fD86fcC172179021991E96Beff3Cc4', '0x689f3806874d3c8A973f419a4eB24e6fBA7E830F', 'https://polygon-bor-rpc.publicnode.com', 137],
|
|
42
|
-
// --- polygon amoy testnet
|
|
43
|
-
'ecld-pynithy-amoy': ['
|
|
44
|
-
'ecld-nodenithy-amoy': ['
|
|
42
|
+
// --- polygon amoy testnet: the separate-ERC20 PoX EthernityImplementationV2 1.1 and the ECImageRegistryV3 of 2026-10-28 ---
|
|
43
|
+
'ecld-pynithy-amoy': ['0xB8f46D2B943fFbe2A061Ae8be087ef5055AD96A3', '0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706', 'https://polygon-amoy-bor-rpc.publicnode.com', 80002],
|
|
44
|
+
'ecld-nodenithy-amoy': ['0xB8f46D2B943fFbe2A061Ae8be087ef5055AD96A3', '0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706', 'https://polygon-amoy-bor-rpc.publicnode.com', 80002],
|
|
45
|
+
'ecld-pynithy-amoy-unsafe': ['0xB8f46D2B943fFbe2A061Ae8be087ef5055AD96A3', '0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706', 'https://polygon-amoy-bor-rpc.publicnode.com', 80002],
|
|
46
|
+
'ecld-nodenithy-amoy-unsafe': ['0xB8f46D2B943fFbe2A061Ae8be087ef5055AD96A3', '0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706', 'https://polygon-amoy-bor-rpc.publicnode.com', 80002],
|
|
45
47
|
// --- iotex testnet ---
|
|
46
48
|
'ecld-pynithy-iotex-testnet': ['0xD56385A97413Ed80E28B1b54A193b98F2C49c975', '0xa7467A6391816be9367a1cC52E0ef0c15FfE3cCC', 'https://babel-api.testnet.iotex.io', 4690],
|
|
47
49
|
'ecld-nodenithy-iotex-testnet': ['0xD56385A97413Ed80E28B1b54A193b98F2C49c975', '0xa7467A6391816be9367a1cC52E0ef0c15FfE3cCC', 'https://babel-api.testnet.iotex.io', 4690],
|
|
@@ -156,7 +158,7 @@ const casProvisioned = casConfig.casProvisioned(templateName, isMainnet);
|
|
|
156
158
|
|
|
157
159
|
// Enclave heap (SCONE_HEAP). Part of the measurement: publish runs the
|
|
158
160
|
// securelock with the value recorded here.
|
|
159
|
-
const MEMORY_TO_ALLOCATE = (process.env.ECLD_MEMORY_TO_ALLOCATE || process.env.MEMORY_TO_ALLOCATE || '
|
|
161
|
+
const MEMORY_TO_ALLOCATE = (process.env.ECLD_MEMORY_TO_ALLOCATE || process.env.MEMORY_TO_ALLOCATE || '256M').trim();
|
|
160
162
|
writeEnv('MEMORY_TO_ALLOCATE', MEMORY_TO_ALLOCATE);
|
|
161
163
|
|
|
162
164
|
runCommand('docker pull registry:2');
|
|
@@ -25,17 +25,19 @@ function setVars(network = "") {
|
|
|
25
25
|
: "0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31";
|
|
26
26
|
} else if (BLOCKCHAIN_NETWORK.includes("Polygon")) {
|
|
27
27
|
if (BLOCKCHAIN_NETWORK.includes("Mainnet")) {
|
|
28
|
-
NETWORK_RPC = "https://polygon-rpc.com";
|
|
28
|
+
NETWORK_RPC = "https://polygon-bor-rpc.publicnode.com";
|
|
29
29
|
IMAGE_REGISTRY_ADDRESS = "0x689f3806874d3c8A973f419a4eB24e6fBA7E830F";
|
|
30
30
|
CHAIN_ID = 137;
|
|
31
31
|
GAS = 20000000;
|
|
32
32
|
GAS_PRICE = 40500500010;
|
|
33
33
|
} else {
|
|
34
|
+
// Amoy's ECImageRegistryV3 of 2026-10-28; the chain's base fee is
|
|
35
|
+
// about 30 gwei.
|
|
34
36
|
NETWORK_RPC = "https://polygon-amoy-bor-rpc.publicnode.com";
|
|
35
|
-
IMAGE_REGISTRY_ADDRESS = "
|
|
37
|
+
IMAGE_REGISTRY_ADDRESS = "0x0C0f52ebd72cd2d2cBe94F385c9e23cec528b706";
|
|
36
38
|
CHAIN_ID = 80002;
|
|
37
39
|
GAS = 20000000;
|
|
38
|
-
GAS_PRICE =
|
|
40
|
+
GAS_PRICE = 40000000010;
|
|
39
41
|
}
|
|
40
42
|
} else if (BLOCKCHAIN_NETWORK.includes("LitVM")) {
|
|
41
43
|
NETWORK_RPC = "https://liteforge.rpc.caldera.xyz/infra-partner-http";
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// registries) is queued by the service's registry watcher from the
|
|
6
6
|
// ImageRegistered event, with this publish's IPFS peer; the hash is posted to
|
|
7
7
|
// /api/addHash only when that row has not appeared within
|
|
8
|
-
// CHAIN_ROW_GRACE_SECONDS
|
|
8
|
+
// CHAIN_ROW_GRACE_SECONDS, or when the service holds a failed extraction of
|
|
9
|
+
// the image. Other images are posted at once.
|
|
9
10
|
const axios = require('axios');
|
|
10
11
|
const fs = require('fs');
|
|
11
12
|
const { Command } = require('commander');
|
|
@@ -14,6 +15,14 @@ const BASE_URL = "https://publickey.ethernity.cloud";
|
|
|
14
15
|
// The watcher polls the registries every 30 s.
|
|
15
16
|
const CHAIN_ROW_GRACE_SECONDS = 180;
|
|
16
17
|
const POLL_SECONDS = 10;
|
|
18
|
+
// A failed extraction is recorded against the hash with publicKey -1 and a
|
|
19
|
+
// reason, and a new submission of the hash runs it again. The service answers
|
|
20
|
+
// the old record for a moment after a submission, so a failed answer is taken
|
|
21
|
+
// as the outcome only once this long has passed since this publish submitted.
|
|
22
|
+
const RESUBMIT_GRACE_SECONDS = 60;
|
|
23
|
+
|
|
24
|
+
// The service's record of an extraction that failed.
|
|
25
|
+
const failed = (answer) => answer !== null && answer.publicKey === -1;
|
|
17
26
|
|
|
18
27
|
async function submitIpfsHash(hash, enclaveName, protocolVersion, network, templateVersion, dockerComposerHash) {
|
|
19
28
|
const url = `${BASE_URL}/api/addHash`;
|
|
@@ -48,9 +57,10 @@ async function checkIpfsHashStatus(hash) {
|
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
// Queues the image as described above and returns the service's first
|
|
52
|
-
//
|
|
53
|
-
//
|
|
60
|
+
// Queues the image as described above and returns the service's first answer
|
|
61
|
+
// for it (`answer`, null right after a submission) and whether this publish
|
|
62
|
+
// posted the hash (`submitted`). `submit`, `check`, `sleep` and `now` are the
|
|
63
|
+
// functions above and the clock, replaceable for tests.
|
|
54
64
|
async function queueImage({ hash, chainRegistered, submitArgs, submit = submitIpfsHash, check = checkIpfsHashStatus,
|
|
55
65
|
sleep = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000)), now = () => Date.now() / 1000,
|
|
56
66
|
log = console.log, graceSeconds = CHAIN_ROW_GRACE_SECONDS, pollSeconds = POLL_SECONDS }) {
|
|
@@ -58,7 +68,11 @@ async function queueImage({ hash, chainRegistered, submitArgs, submit = submitIp
|
|
|
58
68
|
const deadline = now() + graceSeconds;
|
|
59
69
|
while (true) {
|
|
60
70
|
const answer = await check(hash);
|
|
61
|
-
if (answer
|
|
71
|
+
if (failed(answer)) {
|
|
72
|
+
log(`The service's previous extraction of this image failed (${answer.reason || "no reason reported"}); submitting the hash again`);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (answer !== null) return { answer, submitted: false };
|
|
62
76
|
if (now() >= deadline) {
|
|
63
77
|
log(`The certificate service has not picked the registration up from the chain in ${graceSeconds} s; submitting the hash`);
|
|
64
78
|
break;
|
|
@@ -69,7 +83,7 @@ async function queueImage({ hash, chainRegistered, submitArgs, submit = submitIp
|
|
|
69
83
|
}
|
|
70
84
|
const submitResponse = await submit(hash, ...submitArgs);
|
|
71
85
|
log("Submit IPFS Hash Response:", submitResponse);
|
|
72
|
-
return null;
|
|
86
|
+
return { answer: null, submitted: true };
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
async function main() {
|
|
@@ -98,10 +112,12 @@ async function main() {
|
|
|
98
112
|
console.log("Template Version:", templateVersion);
|
|
99
113
|
console.log("Docker Composer Hash:", dockerComposerHash);
|
|
100
114
|
|
|
101
|
-
|
|
115
|
+
const queued = await queueImage({
|
|
102
116
|
hash: hhash, chainRegistered: Boolean(options.chainRegistered),
|
|
103
117
|
submitArgs: [enclaveName, protocolVersion, network, templateVersion, dockerComposerHash],
|
|
104
118
|
});
|
|
119
|
+
let checkResponse = queued.answer;
|
|
120
|
+
const submittedAt = queued.submitted ? Date.now() / 1000 : null;
|
|
105
121
|
|
|
106
122
|
// The service answers publicKey 0 while the image is queued or running
|
|
107
123
|
// (with status, percent and queuePosition), -1 when the extraction failed
|
|
@@ -119,10 +135,15 @@ async function main() {
|
|
|
119
135
|
} else {
|
|
120
136
|
console.log(`Public key not available yet. Queue position: ${checkResponse.queuePosition ?? 'Unknown'}`);
|
|
121
137
|
}
|
|
122
|
-
} else if (checkResponse
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
} else if (failed(checkResponse)) {
|
|
139
|
+
if (submittedAt !== null && Date.now() / 1000 - submittedAt < RESUBMIT_GRACE_SECONDS) {
|
|
140
|
+
// The record of the run before this publish's submission.
|
|
141
|
+
console.log("The service still answers its previous record; waiting for the new run");
|
|
142
|
+
} else {
|
|
143
|
+
console.log(`Public key extraction failed: ${checkResponse.reason || "reason not reported by the service"}`);
|
|
144
|
+
console.log("Fix the cause (keep the image pinned, build with the current SDK) and publish again: a new submission of the same hash runs the extraction again.");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
126
147
|
} else {
|
|
127
148
|
console.log("Public Key:", checkResponse.publicKey);
|
|
128
149
|
// Save public key to file
|
|
@@ -152,4 +173,4 @@ async function main() {
|
|
|
152
173
|
|
|
153
174
|
if (require.main === module) main();
|
|
154
175
|
|
|
155
|
-
module.exports = { queueImage, CHAIN_ROW_GRACE_SECONDS, POLL_SECONDS };
|
|
176
|
+
module.exports = { queueImage, CHAIN_ROW_GRACE_SECONDS, POLL_SECONDS, RESUBMIT_GRACE_SECONDS };
|