@ethernity-cloud/sdk-js 1.5.15 → 1.5.16

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/info.js CHANGED
@@ -1,324 +1,324 @@
1
- #!/usr/bin/env node
2
- /**
3
- * ecld-info: everything a developer needs to read or troubleshoot an enclave.
4
- *
5
- * Read-only and free (eth_call + event logs -- no task, no gas, no private
6
- * key). Reads your project's identity from .env (PROJECT_NAME / IPFS_HASH /
7
- * BLOCKCHAIN_NETWORK), then reports:
8
- *
9
- * NETWORK name, type, chain id, RPC, contract addresses, live block
10
- * TRUSTEDZONE the gatekeeper enclave's Image Registry record
11
- * SECURELOCK the executor enclave's Image Registry record
12
- * ESR registry address, total keys, and -- with --enclave <wallet>
13
- * -- this enclave's recent StateCommitted events
14
- *
15
- * Subcommands (default: the full summary):
16
- * ecld-info full summary
17
- * ecld-info network just the network section
18
- * ecld-info trustedzone just the trustedzone registration
19
- * ecld-info securelock just the securelock registration
20
- * ecld-info esr address|count|state|version|nonce|list ESR inspection
21
- *
22
- * Registration is keyed by the image's IPFS hash: pass --ipfs <hash> for an
23
- * exact record, or --name <n> to resolve the latest registered version of a
24
- * name. With neither, .env IPFS_HASH / PROJECT_NAME are used.
25
- *
26
- * Network: --network NAME, else ESR_NETWORK env, else .env BLOCKCHAIN_NETWORK,
27
- * else BLOXBERG_TESTNET.
28
- */
29
-
30
- 'use strict';
31
-
32
- const fs = require('fs');
33
- const path = require('path');
34
- const { ethers } = require('ethers');
35
- try { require('dotenv').config(); } catch (e) { /* dotenv optional */ }
36
-
37
- // Authoritative per-network values (mirror of the Python SDK's network table).
38
- const NETWORKS = {
39
- BLOXBERG_MAINNET: {
40
- type: 'mainnet', chainId: 8995, rpc: 'https://core.bloxberg.org',
41
- protocol: '0x549A6E06BB2084100148D50F51CF77a3436C3Ae7',
42
- imageRegistry: '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31',
43
- esr: '0xDaFa1e3CAF370765275d853cd86dDEd671Ce29Dd',
44
- },
45
- BLOXBERG_TESTNET: {
46
- type: 'testnet', chainId: 8995, rpc: 'https://core.bloxberg.org',
47
- protocol: '0x02882F03097fE8cD31afbdFbB5D72a498B41112c',
48
- imageRegistry: '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31',
49
- esr: '0xD7a7Cb9cbb0Ca1adFb2B8405382f299EA1c6132f',
50
- },
51
- LITVM_LITEFORGE: {
52
- type: 'testnet', chainId: 0, rpc: 'https://liteforge.rpc.caldera.xyz/infra-partner-http',
53
- protocol: '', imageRegistry: '', esr: '0x213aA794F29EA717B9226dF81F7317334Ac36169',
54
- },
55
- };
56
-
57
- const IMAGE_REGISTRY_ABI = [
58
- 'function imageDetails(string) view returns (address owner, string ipfsHash, string version, string session, uint256 fee, address rewardAddress, bool validated, bool published, string certPublicKey, string dockerComposeHash, string name)',
59
- 'function trustedZoneImageDetails(string) view returns (address owner, string ipfsHash, string version, string session, uint256 fee, address rewardAddress, bool validated, bool published, string certPublicKey, string dockerComposeHash, string name)',
60
- 'function imageVersions(string, uint256) view returns (string)',
61
- 'function trustedZoneImageVersions(string, uint256) view returns (string)',
62
- ];
63
-
64
- const ESR_ABI = [
65
- 'function getState(address enclave, bytes32 key) view returns (string cid, uint256 version, uint64 updatedAt)',
66
- 'function getVersion(address enclave, bytes32 key) view returns (uint256)',
67
- 'function exists(address enclave, bytes32 key) view returns (bool)',
68
- // The idempotency nonce is PUBLIC on-chain data, recorded next to the
69
- // version and enforced strictly in-order per (enclave, key).
70
- 'function getNonce(address enclave, bytes32 key) view returns (uint256)',
71
- 'function entryCount() view returns (uint256)',
72
- 'function getEntriesFrom(uint256 start, uint256 limit) view returns (address[] enclaves, bytes32[] keys, string[] cids, uint256[] versions, uint64[] updatedAts, uint256 total)',
73
- 'event StateCommitted(address indexed enclave, bytes32 indexed key, string cid, uint256 version, uint256 seq, uint256 nonce)',
74
- ];
75
-
76
- function resolveNetwork(name) {
77
- name = (name || process.env.ESR_NETWORK || process.env.BLOCKCHAIN_NETWORK || 'BLOXBERG_TESTNET')
78
- .toUpperCase();
79
- const net = NETWORKS[name];
80
- if (!net) {
81
- throw new Error(`Unknown network '${name}'. Known: ${Object.keys(NETWORKS).join(', ')}`);
82
- }
83
- return { name, net };
84
- }
85
-
86
- function provider(rpc) {
87
- return new ethers.providers.JsonRpcProvider(rpc);
88
- }
89
-
90
- function keyHash(key) {
91
- return ethers.utils.id(key); // keccak256(utf8(key))
92
- }
93
-
94
- function looksLikeCID(cid) {
95
- return typeof cid === 'string' && (cid.startsWith('Qm') || cid.startsWith('bafk')) && cid.length > 20;
96
- }
97
-
98
- function decodeDetails(d, fallbackName) {
99
- return {
100
- name: d.name || fallbackName,
101
- published: Boolean(d.published),
102
- validated: Boolean(d.validated),
103
- image_ipfs_hash: d.ipfsHash || null,
104
- image_version: d.version || null,
105
- reward_address: d.rewardAddress,
106
- owner: d.owner,
107
- docker_compose_hash: d.dockerComposeHash || null,
108
- attestation_session: Boolean(d.session),
109
- cert_present: Boolean(d.certPublicKey),
110
- };
111
- }
112
-
113
- async function lookupRegistration(reg, kind, ipfs, name) {
114
- const detailsFn = kind === 'trustedzone' ? 'trustedZoneImageDetails' : 'imageDetails';
115
- const versionsFn = kind === 'trustedzone' ? 'trustedZoneImageVersions' : 'imageVersions';
116
- if (ipfs) {
117
- try { return decodeDetails(await reg[detailsFn](ipfs), ipfs); }
118
- catch (e) { return { found: false, error: e.message }; }
119
- }
120
- if (name) {
121
- let latest = null;
122
- for (let i = 0; i < 64; i += 1) {
123
- let v;
124
- try { v = await reg[versionsFn](name, i); } catch (e) { break; }
125
- if (!v) break;
126
- latest = v;
127
- }
128
- if (latest) {
129
- try { return decodeDetails(await reg[detailsFn](latest), name); }
130
- catch (e) { return { found: false, error: e.message }; }
131
- }
132
- return { found: false, note: `no registered version for name '${name}' -- the Image Registry is keyed by image IPFS hash; pass --ipfs <hash>` };
133
- }
134
- return { found: false, note: 'no --ipfs or --name to look up' };
135
- }
136
-
137
- async function sectionNetwork(prov, name, net) {
138
- const out = {
139
- network: name, type: net.type, chain_id: net.chainId, rpc: net.rpc,
140
- protocol_contract: net.protocol || '(n/a)',
141
- image_registry: net.imageRegistry || '(n/a)',
142
- esr_contract: net.esr || '(not deployed)',
143
- };
144
- try {
145
- out.latest_block = await prov.getBlockNumber();
146
- out.connected = true;
147
- } catch (e) {
148
- out.connected = false; out.error = e.message;
149
- }
150
- return out;
151
- }
152
-
153
- async function sectionRegistration(prov, net, kind, ipfs, name) {
154
- if (!net.imageRegistry) return { note: 'no image registry on this network' };
155
- const reg = new ethers.Contract(net.imageRegistry, IMAGE_REGISTRY_ABI, prov);
156
- return lookupRegistration(reg, kind, ipfs, name);
157
- }
158
-
159
- async function sectionEsr(prov, net, enclaveWallet, eventsN) {
160
- if (!net.esr) return { note: 'ESR is not deployed on this network' };
161
- const esr = new ethers.Contract(net.esr, ESR_ABI, prov);
162
- const out = { esr_contract: net.esr };
163
- try { out.total_registry_entries = (await esr.entryCount()).toNumber(); } catch (e) { /* ignore */ }
164
- if (enclaveWallet) {
165
- try {
166
- const latest = await prov.getBlockNumber();
167
- const from = Math.max(0, latest - 500000);
168
- const flt = esr.filters.StateCommitted(ethers.utils.getAddress(enclaveWallet));
169
- let logs = await esr.queryFilter(flt, from, 'latest');
170
- logs = logs.slice(-eventsN);
171
- out.recent_commits = logs.map((l) => ({
172
- key_hash: l.args.key,
173
- version: l.args.version.toNumber(),
174
- cid: l.args.cid,
175
- seq: l.args.seq.toNumber(),
176
- // PUBLIC idempotency nonce; 0 = the commit carried no guard.
177
- nonce: l.args.nonce.toNumber(),
178
- block: l.blockNumber,
179
- }));
180
- } catch (e) {
181
- out.recent_commits_error = e.message;
182
- }
183
- } else {
184
- out.recent_commits_note = 'pass --enclave <ESR wallet> to list this enclave\'s state commits';
185
- }
186
- return out;
187
- }
188
-
189
- async function esrQuery(prov, name, net, args) {
190
- const esrAddr = args.contract || net.esr;
191
- const sub = args.esrCmd || 'address';
192
- if (sub === 'address') return { network: name, esr_contract: esrAddr || '(not deployed)', rpc: net.rpc };
193
- if (!esrAddr) throw new Error(`ESR is not deployed on ${name}.`);
194
- const c = new ethers.Contract(esrAddr, ESR_ABI, prov);
195
- if (sub === 'count') return { network: name, entry_count: (await c.entryCount()).toNumber() };
196
- if (sub === 'version') {
197
- const v = (await c.getVersion(ethers.utils.getAddress(args.enclave), keyHash(args.key))).toNumber();
198
- return { enclave: args.enclave, key: args.key, version: v };
199
- }
200
- if (sub === 'nonce') {
201
- const n = (await c.getNonce(ethers.utils.getAddress(args.enclave), keyHash(args.key))).toNumber();
202
- return {
203
- enclave: args.enclave, key: args.key, nonce: n,
204
- note: 'last accepted idempotency nonce (PUBLIC data, free eth_call); '
205
- + '0 = no guarded commit yet. Pass EXACTLY nonce + 1 to commit() to '
206
- + 'guard against duplicates; the contract enforces the sequence '
207
- + 'strictly per key (no gaps, no reuse)',
208
- };
209
- }
210
- if (sub === 'state') {
211
- const enclave = ethers.utils.getAddress(args.enclave);
212
- const kh = keyHash(args.key);
213
- const [cid, version, updated] = await c.getState(enclave, kh);
214
- const exists = await c.exists(enclave, kh);
215
- return {
216
- network: name, enclave: args.enclave, key: args.key, key_hash: kh,
217
- exists, version: version.toNumber(),
218
- // PUBLIC idempotency nonce; 0 = no guarded commit yet.
219
- nonce: (await c.getNonce(enclave, kh)).toNumber(),
220
- cid: cid || null, cid_valid: looksLikeCID(cid),
221
- updated_at: updated.toNumber(),
222
- note: 'cid points at ENCRYPTED state; only the enclave can decrypt it',
223
- };
224
- }
225
- if (sub === 'list') {
226
- const start = parseInt(args.start || 0, 10);
227
- const limit = parseInt(args.limit || 50, 10);
228
- const [encs, keys, cids, versions, updated, total] = await c.getEntriesFrom(start, limit);
229
- const entries = encs.map((e, i) => ({
230
- enclave: e, key_hash: keys[i], cid: cids[i] || null,
231
- version: versions[i].toNumber(), updated_at: updated[i].toNumber(),
232
- }));
233
- return { network: name, total: total.toNumber(), start, returned: entries.length, entries };
234
- }
235
- throw new Error(`unknown esr subcommand: ${sub}`);
236
- }
237
-
238
- // ---- arg parsing (tiny, dependency-free) -----------------------------------
239
-
240
- function parseArgs(argv) {
241
- const a = { _: [], json: false };
242
- for (let i = 0; i < argv.length; i += 1) {
243
- const t = argv[i];
244
- if (t === '--json') a.json = true;
245
- else if (t.startsWith('--')) { a[t.slice(2)] = argv[i + 1]; i += 1; }
246
- else a._.push(t);
247
- }
248
- return a;
249
- }
250
-
251
- function emit(obj, asJson) {
252
- if (asJson) { console.log(JSON.stringify(obj, null, 2)); return; }
253
- for (const [k, v] of Object.entries(obj)) {
254
- if (Array.isArray(v)) { console.log(`${k}:`); v.forEach((it) => console.log(' ' + JSON.stringify(it))); }
255
- else console.log(`${k}: ${v}`);
256
- }
257
- }
258
-
259
- function printFull(info) {
260
- const n = info.network;
261
- console.log('NETWORK');
262
- console.log(` ${n.network} (${n.type}, chain ${n.chain_id}) block ${n.latest_block || '?'} ${n.connected ? 'connected' : 'NOT CONNECTED'}`);
263
- console.log(` rpc: ${n.rpc}`);
264
- console.log(` protocol: ${n.protocol_contract}`);
265
- console.log(` image registry: ${n.image_registry}`);
266
- console.log(` esr: ${n.esr_contract}`);
267
- for (const [label, key] of [['TRUSTEDZONE', 'trustedzone'], ['SECURELOCK', 'securelock']]) {
268
- const r = info[key] || {};
269
- console.log(label);
270
- if (r.published || r.validated || r.image_ipfs_hash) {
271
- console.log(` published: ${r.published ? 'yes' : 'no'} validated: ${r.validated ? 'yes' : 'no'}`);
272
- console.log(` image hash: ${r.image_ipfs_hash}`);
273
- console.log(` version: ${r.image_version}`);
274
- console.log(` reward addr: ${r.reward_address}`);
275
- console.log(` attestation: ${r.attestation_session ? 'MRENCLAVE session present' : 'none'}`);
276
- } else {
277
- console.log(` not found (${r.error || r.note || 'not registered'})`);
278
- }
279
- }
280
- const e = info.esr || {};
281
- console.log('ESR');
282
- if (e.note) { console.log(` ${e.note}`); return; }
283
- console.log(` registry: ${e.esr_contract}`);
284
- if ('total_registry_entries' in e) console.log(` total keys: ${e.total_registry_entries} (all enclaves)`);
285
- if (e.recent_commits) {
286
- console.log(` recent state commits for this enclave: ${e.recent_commits.length}`);
287
- e.recent_commits.forEach((c) => console.log(` v${c.version} seq ${c.seq} nonce ${c.nonce} ${c.key_hash} ${c.cid} (block ${c.block})`));
288
- } else if (e.recent_commits_note) console.log(` ${e.recent_commits_note}`);
289
- else if (e.recent_commits_error) console.log(` (could not read commits: ${e.recent_commits_error})`);
290
- }
291
-
292
- async function main() {
293
- const args = parseArgs(process.argv.slice(2));
294
- const section = args._[0]; // network | trustedzone | securelock | esr | undefined
295
- const { name: netName, net } = resolveNetwork(args.network);
296
- const prov = provider(net.rpc);
297
-
298
- const ipfs = args.ipfs || process.env.IPFS_HASH || null;
299
- const projName = args.name || process.env.PROJECT_NAME || null;
300
-
301
- if (section === 'esr') {
302
- args.esrCmd = args._[1];
303
- const obj = await esrQuery(prov, netName, net, args);
304
- emit(obj, args.json);
305
- return;
306
- }
307
- if (section === 'network') { emit(await sectionNetwork(prov, netName, net), args.json); return; }
308
- if (section === 'trustedzone') { emit(await sectionRegistration(prov, net, 'trustedzone', ipfs, projName), args.json); return; }
309
- if (section === 'securelock') { emit(await sectionRegistration(prov, net, 'securelock', ipfs, projName), args.json); return; }
310
-
311
- const info = {
312
- project: { name: projName || '(unknown)', dapp_type: process.env.SERVICE_TYPE || process.env.DAPP_TYPE, version: process.env.VERSION },
313
- network: await sectionNetwork(prov, netName, net),
314
- trustedzone: await sectionRegistration(prov, net, 'trustedzone', ipfs, projName),
315
- securelock: await sectionRegistration(prov, net, 'securelock', ipfs, projName),
316
- esr: await sectionEsr(prov, net, args.enclave || null, parseInt(args.events || 10, 10)),
317
- };
318
- if (args.json) { console.log(JSON.stringify(info, null, 2)); return; }
319
- const pr = info.project;
320
- console.log(`PROJECT ${pr.name}${pr.dapp_type ? ' type ' + pr.dapp_type : ''}${pr.version != null ? ' version ' + pr.version : ''}`);
321
- printFull(info);
322
- }
323
-
324
- main().catch((e) => { console.error(`ecld-info: ${e.message}`); process.exit(1); });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ecld-info: everything a developer needs to read or troubleshoot an enclave.
4
+ *
5
+ * Read-only and free (eth_call + event logs -- no task, no gas, no private
6
+ * key). Reads your project's identity from .env (PROJECT_NAME / IPFS_HASH /
7
+ * BLOCKCHAIN_NETWORK), then reports:
8
+ *
9
+ * NETWORK name, type, chain id, RPC, contract addresses, live block
10
+ * TRUSTEDZONE the gatekeeper enclave's Image Registry record
11
+ * SECURELOCK the executor enclave's Image Registry record
12
+ * ESR registry address, total keys, and -- with --enclave <wallet>
13
+ * -- this enclave's recent StateCommitted events
14
+ *
15
+ * Subcommands (default: the full summary):
16
+ * ecld-info full summary
17
+ * ecld-info network just the network section
18
+ * ecld-info trustedzone just the trustedzone registration
19
+ * ecld-info securelock just the securelock registration
20
+ * ecld-info esr address|count|state|version|nonce|list ESR inspection
21
+ *
22
+ * Registration is keyed by the image's IPFS hash: pass --ipfs <hash> for an
23
+ * exact record, or --name <n> to resolve the latest registered version of a
24
+ * name. With neither, .env IPFS_HASH / PROJECT_NAME are used.
25
+ *
26
+ * Network: --network NAME, else ESR_NETWORK env, else .env BLOCKCHAIN_NETWORK,
27
+ * else BLOXBERG_TESTNET.
28
+ */
29
+
30
+ 'use strict';
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+ const { ethers } = require('ethers');
35
+ try { require('dotenv').config(); } catch (e) { /* dotenv optional */ }
36
+
37
+ // Authoritative per-network values (mirror of the Python SDK's network table).
38
+ const NETWORKS = {
39
+ BLOXBERG_MAINNET: {
40
+ type: 'mainnet', chainId: 8995, rpc: 'https://core.bloxberg.org',
41
+ protocol: '0x549A6E06BB2084100148D50F51CF77a3436C3Ae7',
42
+ imageRegistry: '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31',
43
+ esr: '0x54e0dD4201F530703c7988427d3b7c70c1dCeC94',
44
+ },
45
+ BLOXBERG_TESTNET: {
46
+ type: 'testnet', chainId: 8995, rpc: 'https://core.bloxberg.org',
47
+ protocol: '0x02882F03097fE8cD31afbdFbB5D72a498B41112c',
48
+ imageRegistry: '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31',
49
+ esr: '0x421E216087eEc1e27b82188C23E490b0E2cA384d',
50
+ },
51
+ LITVM_LITEFORGE: {
52
+ type: 'testnet', chainId: 0, rpc: 'https://liteforge.rpc.caldera.xyz/infra-partner-http',
53
+ protocol: '', imageRegistry: '', esr: '0x5f427A78A0f2Bd7379b99A09C7D8fE51DD7E54D7',
54
+ },
55
+ };
56
+
57
+ const IMAGE_REGISTRY_ABI = [
58
+ 'function imageDetails(string) view returns (address owner, string ipfsHash, string version, string session, uint256 fee, address rewardAddress, bool validated, bool published, string certPublicKey, string dockerComposeHash, string name)',
59
+ 'function trustedZoneImageDetails(string) view returns (address owner, string ipfsHash, string version, string session, uint256 fee, address rewardAddress, bool validated, bool published, string certPublicKey, string dockerComposeHash, string name)',
60
+ 'function imageVersions(string, uint256) view returns (string)',
61
+ 'function trustedZoneImageVersions(string, uint256) view returns (string)',
62
+ ];
63
+
64
+ const ESR_ABI = [
65
+ 'function getState(address enclave, bytes32 key) view returns (string cid, uint256 version, uint64 updatedAt)',
66
+ 'function getVersion(address enclave, bytes32 key) view returns (uint256)',
67
+ 'function exists(address enclave, bytes32 key) view returns (bool)',
68
+ // The idempotency nonce is PUBLIC on-chain data, recorded next to the
69
+ // version and enforced strictly in-order per (enclave, key).
70
+ 'function getNonce(address enclave, bytes32 key) view returns (uint256)',
71
+ 'function entryCount() view returns (uint256)',
72
+ 'function getEntriesFrom(uint256 start, uint256 limit) view returns (address[] enclaves, bytes32[] keys, string[] cids, uint256[] versions, uint64[] updatedAts, uint256 total)',
73
+ 'event StateCommitted(address indexed enclave, bytes32 indexed key, string cid, uint256 version, uint256 seq, uint256 nonce)',
74
+ ];
75
+
76
+ function resolveNetwork(name) {
77
+ name = (name || process.env.ESR_NETWORK || process.env.BLOCKCHAIN_NETWORK || 'BLOXBERG_TESTNET')
78
+ .toUpperCase();
79
+ const net = NETWORKS[name];
80
+ if (!net) {
81
+ throw new Error(`Unknown network '${name}'. Known: ${Object.keys(NETWORKS).join(', ')}`);
82
+ }
83
+ return { name, net };
84
+ }
85
+
86
+ function provider(rpc) {
87
+ return new ethers.providers.JsonRpcProvider(rpc);
88
+ }
89
+
90
+ function keyHash(key) {
91
+ return ethers.utils.id(key); // keccak256(utf8(key))
92
+ }
93
+
94
+ function looksLikeCID(cid) {
95
+ return typeof cid === 'string' && (cid.startsWith('Qm') || cid.startsWith('bafk')) && cid.length > 20;
96
+ }
97
+
98
+ function decodeDetails(d, fallbackName) {
99
+ return {
100
+ name: d.name || fallbackName,
101
+ published: Boolean(d.published),
102
+ validated: Boolean(d.validated),
103
+ image_ipfs_hash: d.ipfsHash || null,
104
+ image_version: d.version || null,
105
+ reward_address: d.rewardAddress,
106
+ owner: d.owner,
107
+ docker_compose_hash: d.dockerComposeHash || null,
108
+ attestation_session: Boolean(d.session),
109
+ cert_present: Boolean(d.certPublicKey),
110
+ };
111
+ }
112
+
113
+ async function lookupRegistration(reg, kind, ipfs, name) {
114
+ const detailsFn = kind === 'trustedzone' ? 'trustedZoneImageDetails' : 'imageDetails';
115
+ const versionsFn = kind === 'trustedzone' ? 'trustedZoneImageVersions' : 'imageVersions';
116
+ if (ipfs) {
117
+ try { return decodeDetails(await reg[detailsFn](ipfs), ipfs); }
118
+ catch (e) { return { found: false, error: e.message }; }
119
+ }
120
+ if (name) {
121
+ let latest = null;
122
+ for (let i = 0; i < 64; i += 1) {
123
+ let v;
124
+ try { v = await reg[versionsFn](name, i); } catch (e) { break; }
125
+ if (!v) break;
126
+ latest = v;
127
+ }
128
+ if (latest) {
129
+ try { return decodeDetails(await reg[detailsFn](latest), name); }
130
+ catch (e) { return { found: false, error: e.message }; }
131
+ }
132
+ return { found: false, note: `no registered version for name '${name}' -- the Image Registry is keyed by image IPFS hash; pass --ipfs <hash>` };
133
+ }
134
+ return { found: false, note: 'no --ipfs or --name to look up' };
135
+ }
136
+
137
+ async function sectionNetwork(prov, name, net) {
138
+ const out = {
139
+ network: name, type: net.type, chain_id: net.chainId, rpc: net.rpc,
140
+ protocol_contract: net.protocol || '(n/a)',
141
+ image_registry: net.imageRegistry || '(n/a)',
142
+ esr_contract: net.esr || '(not deployed)',
143
+ };
144
+ try {
145
+ out.latest_block = await prov.getBlockNumber();
146
+ out.connected = true;
147
+ } catch (e) {
148
+ out.connected = false; out.error = e.message;
149
+ }
150
+ return out;
151
+ }
152
+
153
+ async function sectionRegistration(prov, net, kind, ipfs, name) {
154
+ if (!net.imageRegistry) return { note: 'no image registry on this network' };
155
+ const reg = new ethers.Contract(net.imageRegistry, IMAGE_REGISTRY_ABI, prov);
156
+ return lookupRegistration(reg, kind, ipfs, name);
157
+ }
158
+
159
+ async function sectionEsr(prov, net, enclaveWallet, eventsN) {
160
+ if (!net.esr) return { note: 'ESR is not deployed on this network' };
161
+ const esr = new ethers.Contract(net.esr, ESR_ABI, prov);
162
+ const out = { esr_contract: net.esr };
163
+ try { out.total_registry_entries = (await esr.entryCount()).toNumber(); } catch (e) { /* ignore */ }
164
+ if (enclaveWallet) {
165
+ try {
166
+ const latest = await prov.getBlockNumber();
167
+ const from = Math.max(0, latest - 500000);
168
+ const flt = esr.filters.StateCommitted(ethers.utils.getAddress(enclaveWallet));
169
+ let logs = await esr.queryFilter(flt, from, 'latest');
170
+ logs = logs.slice(-eventsN);
171
+ out.recent_commits = logs.map((l) => ({
172
+ key_hash: l.args.key,
173
+ version: l.args.version.toNumber(),
174
+ cid: l.args.cid,
175
+ seq: l.args.seq.toNumber(),
176
+ // PUBLIC per-key nonce (advances by 1 on every commit).
177
+ nonce: l.args.nonce.toNumber(),
178
+ block: l.blockNumber,
179
+ }));
180
+ } catch (e) {
181
+ out.recent_commits_error = e.message;
182
+ }
183
+ } else {
184
+ out.recent_commits_note = 'pass --enclave <ESR wallet> to list this enclave\'s state commits';
185
+ }
186
+ return out;
187
+ }
188
+
189
+ async function esrQuery(prov, name, net, args) {
190
+ const esrAddr = args.contract || net.esr;
191
+ const sub = args.esrCmd || 'address';
192
+ if (sub === 'address') return { network: name, esr_contract: esrAddr || '(not deployed)', rpc: net.rpc };
193
+ if (!esrAddr) throw new Error(`ESR is not deployed on ${name}.`);
194
+ const c = new ethers.Contract(esrAddr, ESR_ABI, prov);
195
+ if (sub === 'count') return { network: name, entry_count: (await c.entryCount()).toNumber() };
196
+ if (sub === 'version') {
197
+ const v = (await c.getVersion(ethers.utils.getAddress(args.enclave), keyHash(args.key))).toNumber();
198
+ return { enclave: args.enclave, key: args.key, version: v };
199
+ }
200
+ if (sub === 'nonce') {
201
+ const n = (await c.getNonce(ethers.utils.getAddress(args.enclave), keyHash(args.key))).toNumber();
202
+ return {
203
+ enclave: args.enclave, key: args.key, nonce: n,
204
+ note: 'per-key nonce, PUBLIC data (free eth_call); advances by 1 on '
205
+ + 'every commit, 0 = key never committed. For exactly-once, pass '
206
+ + 'EXACTLY nonce + 1 to commit(); an omitted nonce is auto-assigned '
207
+ + 'the next value by the registry',
208
+ };
209
+ }
210
+ if (sub === 'state') {
211
+ const enclave = ethers.utils.getAddress(args.enclave);
212
+ const kh = keyHash(args.key);
213
+ const [cid, version, updated] = await c.getState(enclave, kh);
214
+ const exists = await c.exists(enclave, kh);
215
+ return {
216
+ network: name, enclave: args.enclave, key: args.key, key_hash: kh,
217
+ exists, version: version.toNumber(),
218
+ // PUBLIC per-key nonce; 0 = key never committed.
219
+ nonce: (await c.getNonce(enclave, kh)).toNumber(),
220
+ cid: cid || null, cid_valid: looksLikeCID(cid),
221
+ updated_at: updated.toNumber(),
222
+ note: 'cid points at ENCRYPTED state; only the enclave can decrypt it',
223
+ };
224
+ }
225
+ if (sub === 'list') {
226
+ const start = parseInt(args.start || 0, 10);
227
+ const limit = parseInt(args.limit || 50, 10);
228
+ const [encs, keys, cids, versions, updated, total] = await c.getEntriesFrom(start, limit);
229
+ const entries = encs.map((e, i) => ({
230
+ enclave: e, key_hash: keys[i], cid: cids[i] || null,
231
+ version: versions[i].toNumber(), updated_at: updated[i].toNumber(),
232
+ }));
233
+ return { network: name, total: total.toNumber(), start, returned: entries.length, entries };
234
+ }
235
+ throw new Error(`unknown esr subcommand: ${sub}`);
236
+ }
237
+
238
+ // ---- arg parsing (tiny, dependency-free) -----------------------------------
239
+
240
+ function parseArgs(argv) {
241
+ const a = { _: [], json: false };
242
+ for (let i = 0; i < argv.length; i += 1) {
243
+ const t = argv[i];
244
+ if (t === '--json') a.json = true;
245
+ else if (t.startsWith('--')) { a[t.slice(2)] = argv[i + 1]; i += 1; }
246
+ else a._.push(t);
247
+ }
248
+ return a;
249
+ }
250
+
251
+ function emit(obj, asJson) {
252
+ if (asJson) { console.log(JSON.stringify(obj, null, 2)); return; }
253
+ for (const [k, v] of Object.entries(obj)) {
254
+ if (Array.isArray(v)) { console.log(`${k}:`); v.forEach((it) => console.log(' ' + JSON.stringify(it))); }
255
+ else console.log(`${k}: ${v}`);
256
+ }
257
+ }
258
+
259
+ function printFull(info) {
260
+ const n = info.network;
261
+ console.log('NETWORK');
262
+ console.log(` ${n.network} (${n.type}, chain ${n.chain_id}) block ${n.latest_block || '?'} ${n.connected ? 'connected' : 'NOT CONNECTED'}`);
263
+ console.log(` rpc: ${n.rpc}`);
264
+ console.log(` protocol: ${n.protocol_contract}`);
265
+ console.log(` image registry: ${n.image_registry}`);
266
+ console.log(` esr: ${n.esr_contract}`);
267
+ for (const [label, key] of [['TRUSTEDZONE', 'trustedzone'], ['SECURELOCK', 'securelock']]) {
268
+ const r = info[key] || {};
269
+ console.log(label);
270
+ if (r.published || r.validated || r.image_ipfs_hash) {
271
+ console.log(` published: ${r.published ? 'yes' : 'no'} validated: ${r.validated ? 'yes' : 'no'}`);
272
+ console.log(` image hash: ${r.image_ipfs_hash}`);
273
+ console.log(` version: ${r.image_version}`);
274
+ console.log(` reward addr: ${r.reward_address}`);
275
+ console.log(` attestation: ${r.attestation_session ? 'MRENCLAVE session present' : 'none'}`);
276
+ } else {
277
+ console.log(` not found (${r.error || r.note || 'not registered'})`);
278
+ }
279
+ }
280
+ const e = info.esr || {};
281
+ console.log('ESR');
282
+ if (e.note) { console.log(` ${e.note}`); return; }
283
+ console.log(` registry: ${e.esr_contract}`);
284
+ if ('total_registry_entries' in e) console.log(` total keys: ${e.total_registry_entries} (all enclaves)`);
285
+ if (e.recent_commits) {
286
+ console.log(` recent state commits for this enclave: ${e.recent_commits.length}`);
287
+ e.recent_commits.forEach((c) => console.log(` v${c.version} seq ${c.seq} nonce ${c.nonce} ${c.key_hash} ${c.cid} (block ${c.block})`));
288
+ } else if (e.recent_commits_note) console.log(` ${e.recent_commits_note}`);
289
+ else if (e.recent_commits_error) console.log(` (could not read commits: ${e.recent_commits_error})`);
290
+ }
291
+
292
+ async function main() {
293
+ const args = parseArgs(process.argv.slice(2));
294
+ const section = args._[0]; // network | trustedzone | securelock | esr | undefined
295
+ const { name: netName, net } = resolveNetwork(args.network);
296
+ const prov = provider(net.rpc);
297
+
298
+ const ipfs = args.ipfs || process.env.IPFS_HASH || null;
299
+ const projName = args.name || process.env.PROJECT_NAME || null;
300
+
301
+ if (section === 'esr') {
302
+ args.esrCmd = args._[1];
303
+ const obj = await esrQuery(prov, netName, net, args);
304
+ emit(obj, args.json);
305
+ return;
306
+ }
307
+ if (section === 'network') { emit(await sectionNetwork(prov, netName, net), args.json); return; }
308
+ if (section === 'trustedzone') { emit(await sectionRegistration(prov, net, 'trustedzone', ipfs, projName), args.json); return; }
309
+ if (section === 'securelock') { emit(await sectionRegistration(prov, net, 'securelock', ipfs, projName), args.json); return; }
310
+
311
+ const info = {
312
+ project: { name: projName || '(unknown)', dapp_type: process.env.SERVICE_TYPE || process.env.DAPP_TYPE, version: process.env.VERSION },
313
+ network: await sectionNetwork(prov, netName, net),
314
+ trustedzone: await sectionRegistration(prov, net, 'trustedzone', ipfs, projName),
315
+ securelock: await sectionRegistration(prov, net, 'securelock', ipfs, projName),
316
+ esr: await sectionEsr(prov, net, args.enclave || null, parseInt(args.events || 10, 10)),
317
+ };
318
+ if (args.json) { console.log(JSON.stringify(info, null, 2)); return; }
319
+ const pr = info.project;
320
+ console.log(`PROJECT ${pr.name}${pr.dapp_type ? ' type ' + pr.dapp_type : ''}${pr.version != null ? ' version ' + pr.version : ''}`);
321
+ printFull(info);
322
+ }
323
+
324
+ main().catch((e) => { console.error(`ecld-info: ${e.message}`); process.exit(1); });
@@ -462,11 +462,15 @@ class StateRegistry {
462
462
  */
463
463
  async commit(key, mutate, opts = 3) {
464
464
  // Back-compat: the third arg used to be `attempts` (a number); it may now
465
- // also be { attempts, nonce }. `nonce` is an idempotency guard the dApp
466
- // controls: it must be EXACTLY the last accepted nonce for the key + 1
467
- // (see getNonce; the sequence is 1, 2, 3, ... with no gaps and no
468
- // reuse). Anything else throws StateNonceError and the state is NOT
469
- // changed.
465
+ // also be { attempts, nonce }. The per-key nonce sequence advances by
466
+ // exactly 1 on EVERY commit (1, 2, 3, ... -- no gaps, no reuse; see
467
+ // getNonce).
468
+ // - Omitted: the SDK takes the next value in sequence automatically.
469
+ // Always succeeds (subject to permissions/versioning) but carries NO
470
+ // exactly-once guarantee -- a resubmitted task picks a fresh number.
471
+ // - Explicit: must be EXACTLY the last accepted nonce + 1, pinned by
472
+ // the CLIENT before submitting. Anything else throws StateNonceError
473
+ // (task code 36) and the state is NOT changed.
470
474
  const { attempts = 3, nonce = null } =
471
475
  typeof opts === 'number' ? { attempts: opts } : (opts || {});
472
476
  const transform = async (acl, data) => {
@@ -499,26 +503,40 @@ class StateRegistry {
499
503
  // nonce (chain merged with this task's own accepted commits), so a
500
504
  // duplicate fails in-enclave with task code 36 instead of as a failed
501
505
  // relay. The contract re-enforces the same rule on-chain either way.
506
+ let finalNonce;
502
507
  if (nonce != null) {
503
- const n = Math.trunc(nonce);
508
+ finalNonce = Math.trunc(nonce);
509
+ if (finalNonce === 0) {
510
+ throw new StateNonceError(
511
+ `nonce=0 is reserved for 'not set' (the registry then assigns the ` +
512
+ `next value itself) -- omit the nonce on commit('${key}', ...) ` +
513
+ 'instead of passing 0. Pinned nonces start at 1: use getNonce() + 1.'
514
+ );
515
+ }
504
516
  // eslint-disable-next-line no-await-in-loop
505
517
  const storedNonce = await this.getNonce(key);
506
- if (n !== storedNonce + 1) {
518
+ if (finalNonce !== storedNonce + 1) {
507
519
  throw new StateNonceError(
508
- `nonce ${n} is out of sequence for state key '${key}' (last accepted: ` +
520
+ `nonce ${finalNonce} is out of sequence for state key '${key}' (last accepted: ` +
509
521
  `${storedNonce}, expected: ${storedNonce + 1}); commit suppressed, ` +
510
522
  'state unchanged'
511
523
  );
512
524
  }
525
+ } else {
526
+ // No explicit nonce: the CONTRACT assigns the next value in sequence
527
+ // itself (wire nonce 0 = "omitted"); this predicted value feeds the
528
+ // blob's reporting copy and the task memo. The sequence advances on
529
+ // EVERY commit; only an EXPLICIT nonce carries the exactly-once
530
+ // guarantee (an omitted one is auto-assigned a fresh number, so it
531
+ // cannot deduplicate -- by design).
532
+ // eslint-disable-next-line no-await-in-loop
533
+ finalNonce = (await this.getNonce(key)) + 1;
513
534
  }
514
535
  // eslint-disable-next-line no-await-in-loop
515
536
  const [nextAcl, newData] = await transform(acl, data, currentVersion);
516
- // The blob carries a REPORTING COPY of what the registry will store for
517
- // this key after this commit: the supplied nonce, or the preserved
518
- // current value for a nonce-less commit. Chain stays primary; reads
537
+ // The blob carries a REPORTING COPY of the nonce the registry will
538
+ // store for this key after this commit. Chain stays primary; reads
519
539
  // verify the two match.
520
- // eslint-disable-next-line no-await-in-loop
521
- const finalNonce = nonce != null ? Math.trunc(nonce) : ((await this.getNonce(key)) || null);
522
540
  // Bind the authoring caller into the blob (hence the CID/signature) for
523
541
  // owned state.
524
542
  const stored = wrapStored(nextAcl, newData, nextAcl ? _taskCaller : null, finalNonce);
@@ -533,7 +551,8 @@ class StateRegistry {
533
551
  await this._publish(key, blob, cid);
534
552
 
535
553
  try {
536
- // On-chain, 0 means "no guard: preserve the stored nonce".
554
+ // Wire value: the pinned nonce, or 0 = "omitted" -- the contract
555
+ // then assigns the next value atomically on-chain.
537
556
  // eslint-disable-next-line no-await-in-loop
538
557
  await this._sendCommit(keyHash(key), cid, currentVersion,
539
558
  nextAcl ? normAddr(nextAcl.owner) : null,
@@ -541,7 +560,7 @@ class StateRegistry {
541
560
  // Record the POST-commit values (the DATA -- the ACL never leaves in
542
561
  // results): this is what the chain shows once the relay lands.
543
562
  ledgerRecord(key, currentVersion + 1, cid, newData);
544
- if (nonce != null) _taskNonces[key] = Math.trunc(nonce);
563
+ _taskNonces[key] = finalNonce;
545
564
  return [nextAcl, newData];
546
565
  } catch (e) {
547
566
  const msg = String(e && e.message ? e.message : e);
@@ -128,9 +128,10 @@ class MemContract {
128
128
  }
129
129
 
130
130
  // ---- the on-chain commitFor, applied locally ----
131
- // Idempotency nonces are enforced strictly sequentially per (enclave, key),
132
- // exactly like the contract: nonce != 0 must be EXACTLY the stored value + 1
133
- // (no gaps, no reuse) or NonceOutOfOrder is thrown; nonce == 0 preserves it.
131
+ // The per-key nonce advances by EXACTLY 1 on every commit, like the
132
+ // contract: nonce == 0 ("omitted") gets the next value assigned by the
133
+ // registry itself; a non-zero nonce is a client-pinned guard and must be
134
+ // EXACTLY the stored value + 1 (no gaps, no reuse) or NonceOutOfOrder.
134
135
  applyCommit(enclave, keyHash, cid, expectedVersion, nonce = 0) {
135
136
  const k = key(enclave, keyHash);
136
137
  const entry = this._entries[k] || {};
@@ -140,7 +141,9 @@ class MemContract {
140
141
  }
141
142
  let storedNonce = Number(entry.nonce || 0);
142
143
  const n = Number(nonce || 0);
143
- if (n !== 0) {
144
+ if (n === 0) {
145
+ storedNonce += 1;
146
+ } else {
144
147
  if (n !== storedNonce + 1) {
145
148
  throw new Error(`NonceOutOfOrder: stored ${storedNonce}, given ${n}`);
146
149
  }
@@ -52,10 +52,10 @@ async function main() {
52
52
  if (!threw) throw new Error('gap accepted');
53
53
  console.log('3. nonce=2 (+1) accepted, gap nonce=10 rejected ok');
54
54
 
55
- // 4. nonce-less commit preserves the stored nonce
55
+ // 4. nonce-less commit AUTO-takes the next in sequence (2 -> 3)
56
56
  await r.commit('counter', (s) => ({ ...s, note: 'free' }));
57
- if ((await r.getNonce('counter')) !== 2) throw new Error('step4');
58
- console.log('4. nonce-less commit preserves stored nonce ok');
57
+ if ((await r.getNonce('counter')) !== 3) throw new Error('step4');
58
+ console.log('4. nonce-less commit auto-advanced the sequence to 3 ok');
59
59
 
60
60
  // 5. stale (reused) nonce rejected; the exact next one still works
61
61
  threw = false;
@@ -66,9 +66,17 @@ async function main() {
66
66
  threw = true;
67
67
  }
68
68
  if (!threw) throw new Error('stale accepted');
69
- await r.commit('counter', (s) => ({ ...s, step: 3 }), { nonce: 3 });
70
- if ((await r.getNonce('counter')) !== 3) throw new Error('step5');
71
- console.log('5. stale nonce=1 rejected, next nonce=3 accepted ok');
69
+ threw = false;
70
+ try {
71
+ await r.commit('counter', (s) => s, { nonce: 0 });
72
+ } catch (e) {
73
+ if (!/reserved/.test(String(e.message))) throw e;
74
+ threw = true;
75
+ }
76
+ if (!threw) throw new Error('explicit 0 accepted');
77
+ await r.commit('counter', (s) => ({ ...s, step: 4 }), { nonce: 4 });
78
+ if ((await r.getNonce('counter')) !== 4) throw new Error('step5');
79
+ console.log('5. stale nonce=1 rejected, explicit nonce=0 refused, next nonce=4 accepted ok');
72
80
 
73
81
  // 6. contract-side (registry) enforcement, bypassing the SDK pre-check
74
82
  const mc = new MemContract(null);
@@ -94,8 +102,8 @@ async function main() {
94
102
  esrLocal.install(ecld2, { caller: A, filePrefix: 'st' });
95
103
  const r2 = new ecld2.StateRegistry();
96
104
  const n2 = await r2.getNonce('counter');
97
- if (n2 !== 3) throw new Error('step7 got ' + n2);
98
- console.log('7. chain-first getNonce()==3 after reload (public read, no memo) ok');
105
+ if (n2 !== 4) throw new Error('step7 got ' + n2);
106
+ console.log('7. chain-first getNonce()==4 after reload (public read, no memo) ok');
99
107
 
100
108
  // 8. public: nonce read needs no ACL -- content read stays denied for B
101
109
  delete require.cache[require.resolve(path.join(__dirname, 'ecld_state.js'))];
@@ -462,11 +462,15 @@ class StateRegistry {
462
462
  */
463
463
  async commit(key, mutate, opts = 3) {
464
464
  // Back-compat: the third arg used to be `attempts` (a number); it may now
465
- // also be { attempts, nonce }. `nonce` is an idempotency guard the dApp
466
- // controls: it must be EXACTLY the last accepted nonce for the key + 1
467
- // (see getNonce; the sequence is 1, 2, 3, ... with no gaps and no
468
- // reuse). Anything else throws StateNonceError and the state is NOT
469
- // changed.
465
+ // also be { attempts, nonce }. The per-key nonce sequence advances by
466
+ // exactly 1 on EVERY commit (1, 2, 3, ... -- no gaps, no reuse; see
467
+ // getNonce).
468
+ // - Omitted: the SDK takes the next value in sequence automatically.
469
+ // Always succeeds (subject to permissions/versioning) but carries NO
470
+ // exactly-once guarantee -- a resubmitted task picks a fresh number.
471
+ // - Explicit: must be EXACTLY the last accepted nonce + 1, pinned by
472
+ // the CLIENT before submitting. Anything else throws StateNonceError
473
+ // (task code 36) and the state is NOT changed.
470
474
  const { attempts = 3, nonce = null } =
471
475
  typeof opts === 'number' ? { attempts: opts } : (opts || {});
472
476
  const transform = async (acl, data) => {
@@ -499,26 +503,40 @@ class StateRegistry {
499
503
  // nonce (chain merged with this task's own accepted commits), so a
500
504
  // duplicate fails in-enclave with task code 36 instead of as a failed
501
505
  // relay. The contract re-enforces the same rule on-chain either way.
506
+ let finalNonce;
502
507
  if (nonce != null) {
503
- const n = Math.trunc(nonce);
508
+ finalNonce = Math.trunc(nonce);
509
+ if (finalNonce === 0) {
510
+ throw new StateNonceError(
511
+ `nonce=0 is reserved for 'not set' (the registry then assigns the ` +
512
+ `next value itself) -- omit the nonce on commit('${key}', ...) ` +
513
+ 'instead of passing 0. Pinned nonces start at 1: use getNonce() + 1.'
514
+ );
515
+ }
504
516
  // eslint-disable-next-line no-await-in-loop
505
517
  const storedNonce = await this.getNonce(key);
506
- if (n !== storedNonce + 1) {
518
+ if (finalNonce !== storedNonce + 1) {
507
519
  throw new StateNonceError(
508
- `nonce ${n} is out of sequence for state key '${key}' (last accepted: ` +
520
+ `nonce ${finalNonce} is out of sequence for state key '${key}' (last accepted: ` +
509
521
  `${storedNonce}, expected: ${storedNonce + 1}); commit suppressed, ` +
510
522
  'state unchanged'
511
523
  );
512
524
  }
525
+ } else {
526
+ // No explicit nonce: the CONTRACT assigns the next value in sequence
527
+ // itself (wire nonce 0 = "omitted"); this predicted value feeds the
528
+ // blob's reporting copy and the task memo. The sequence advances on
529
+ // EVERY commit; only an EXPLICIT nonce carries the exactly-once
530
+ // guarantee (an omitted one is auto-assigned a fresh number, so it
531
+ // cannot deduplicate -- by design).
532
+ // eslint-disable-next-line no-await-in-loop
533
+ finalNonce = (await this.getNonce(key)) + 1;
513
534
  }
514
535
  // eslint-disable-next-line no-await-in-loop
515
536
  const [nextAcl, newData] = await transform(acl, data, currentVersion);
516
- // The blob carries a REPORTING COPY of what the registry will store for
517
- // this key after this commit: the supplied nonce, or the preserved
518
- // current value for a nonce-less commit. Chain stays primary; reads
537
+ // The blob carries a REPORTING COPY of the nonce the registry will
538
+ // store for this key after this commit. Chain stays primary; reads
519
539
  // verify the two match.
520
- // eslint-disable-next-line no-await-in-loop
521
- const finalNonce = nonce != null ? Math.trunc(nonce) : ((await this.getNonce(key)) || null);
522
540
  // Bind the authoring caller into the blob (hence the CID/signature) for
523
541
  // owned state.
524
542
  const stored = wrapStored(nextAcl, newData, nextAcl ? _taskCaller : null, finalNonce);
@@ -533,7 +551,8 @@ class StateRegistry {
533
551
  await this._publish(key, blob, cid);
534
552
 
535
553
  try {
536
- // On-chain, 0 means "no guard: preserve the stored nonce".
554
+ // Wire value: the pinned nonce, or 0 = "omitted" -- the contract
555
+ // then assigns the next value atomically on-chain.
537
556
  // eslint-disable-next-line no-await-in-loop
538
557
  await this._sendCommit(keyHash(key), cid, currentVersion,
539
558
  nextAcl ? normAddr(nextAcl.owner) : null,
@@ -541,7 +560,7 @@ class StateRegistry {
541
560
  // Record the POST-commit values (the DATA -- the ACL never leaves in
542
561
  // results): this is what the chain shows once the relay lands.
543
562
  ledgerRecord(key, currentVersion + 1, cid, newData);
544
- if (nonce != null) _taskNonces[key] = Math.trunc(nonce);
563
+ _taskNonces[key] = finalNonce;
545
564
  return [nextAcl, newData];
546
565
  } catch (e) {
547
566
  const msg = String(e && e.message ? e.message : e);
@@ -128,9 +128,10 @@ class MemContract {
128
128
  }
129
129
 
130
130
  // ---- the on-chain commitFor, applied locally ----
131
- // Idempotency nonces are enforced strictly sequentially per (enclave, key),
132
- // exactly like the contract: nonce != 0 must be EXACTLY the stored value + 1
133
- // (no gaps, no reuse) or NonceOutOfOrder is thrown; nonce == 0 preserves it.
131
+ // The per-key nonce advances by EXACTLY 1 on every commit, like the
132
+ // contract: nonce == 0 ("omitted") gets the next value assigned by the
133
+ // registry itself; a non-zero nonce is a client-pinned guard and must be
134
+ // EXACTLY the stored value + 1 (no gaps, no reuse) or NonceOutOfOrder.
134
135
  applyCommit(enclave, keyHash, cid, expectedVersion, nonce = 0) {
135
136
  const k = key(enclave, keyHash);
136
137
  const entry = this._entries[k] || {};
@@ -140,7 +141,9 @@ class MemContract {
140
141
  }
141
142
  let storedNonce = Number(entry.nonce || 0);
142
143
  const n = Number(nonce || 0);
143
- if (n !== 0) {
144
+ if (n === 0) {
145
+ storedNonce += 1;
146
+ } else {
144
147
  if (n !== storedNonce + 1) {
145
148
  throw new Error(`NonceOutOfOrder: stored ${storedNonce}, given ${n}`);
146
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.5.15",
3
+ "version": "1.5.16",
4
4
  "description": "A package to run ethernity cloud build scripts",
5
5
  "scripts": {
6
6
  "ecld-build": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/build.js",