@ethernity-cloud/sdk-js 1.5.14 → 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,323 +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: '0x39d6f2A98f7FFF9866AAaa0EAa9aEd8bcf6f728E',
44
- },
45
- BLOXBERG_TESTNET: {
46
- type: 'testnet', chainId: 8995, rpc: 'https://core.bloxberg.org',
47
- protocol: '0x02882F03097fE8cD31afbdFbB5D72a498B41112c',
48
- imageRegistry: '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31',
49
- esr: '0xdfDD088b9cB998280685aF4E93DC0b37952aB08e',
50
- },
51
- LITVM_LITEFORGE: {
52
- type: 'testnet', chainId: 0, rpc: 'https://liteforge.rpc.caldera.xyz/infra-partner-http',
53
- protocol: '', imageRegistry: '', esr: '0x71a4bb5AC4c8F37ea0d9394fB29bBC06eEC28000',
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 any greater value to commit() to '
206
- + 'guard against duplicates; the contract enforces in-order per key',
207
- };
208
- }
209
- if (sub === 'state') {
210
- const enclave = ethers.utils.getAddress(args.enclave);
211
- const kh = keyHash(args.key);
212
- const [cid, version, updated] = await c.getState(enclave, kh);
213
- const exists = await c.exists(enclave, kh);
214
- return {
215
- network: name, enclave: args.enclave, key: args.key, key_hash: kh,
216
- exists, version: version.toNumber(),
217
- // PUBLIC idempotency nonce; 0 = no guarded commit yet.
218
- nonce: (await c.getNonce(enclave, kh)).toNumber(),
219
- cid: cid || null, cid_valid: looksLikeCID(cid),
220
- updated_at: updated.toNumber(),
221
- note: 'cid points at ENCRYPTED state; only the enclave can decrypt it',
222
- };
223
- }
224
- if (sub === 'list') {
225
- const start = parseInt(args.start || 0, 10);
226
- const limit = parseInt(args.limit || 50, 10);
227
- const [encs, keys, cids, versions, updated, total] = await c.getEntriesFrom(start, limit);
228
- const entries = encs.map((e, i) => ({
229
- enclave: e, key_hash: keys[i], cid: cids[i] || null,
230
- version: versions[i].toNumber(), updated_at: updated[i].toNumber(),
231
- }));
232
- return { network: name, total: total.toNumber(), start, returned: entries.length, entries };
233
- }
234
- throw new Error(`unknown esr subcommand: ${sub}`);
235
- }
236
-
237
- // ---- arg parsing (tiny, dependency-free) -----------------------------------
238
-
239
- function parseArgs(argv) {
240
- const a = { _: [], json: false };
241
- for (let i = 0; i < argv.length; i += 1) {
242
- const t = argv[i];
243
- if (t === '--json') a.json = true;
244
- else if (t.startsWith('--')) { a[t.slice(2)] = argv[i + 1]; i += 1; }
245
- else a._.push(t);
246
- }
247
- return a;
248
- }
249
-
250
- function emit(obj, asJson) {
251
- if (asJson) { console.log(JSON.stringify(obj, null, 2)); return; }
252
- for (const [k, v] of Object.entries(obj)) {
253
- if (Array.isArray(v)) { console.log(`${k}:`); v.forEach((it) => console.log(' ' + JSON.stringify(it))); }
254
- else console.log(`${k}: ${v}`);
255
- }
256
- }
257
-
258
- function printFull(info) {
259
- const n = info.network;
260
- console.log('NETWORK');
261
- console.log(` ${n.network} (${n.type}, chain ${n.chain_id}) block ${n.latest_block || '?'} ${n.connected ? 'connected' : 'NOT CONNECTED'}`);
262
- console.log(` rpc: ${n.rpc}`);
263
- console.log(` protocol: ${n.protocol_contract}`);
264
- console.log(` image registry: ${n.image_registry}`);
265
- console.log(` esr: ${n.esr_contract}`);
266
- for (const [label, key] of [['TRUSTEDZONE', 'trustedzone'], ['SECURELOCK', 'securelock']]) {
267
- const r = info[key] || {};
268
- console.log(label);
269
- if (r.published || r.validated || r.image_ipfs_hash) {
270
- console.log(` published: ${r.published ? 'yes' : 'no'} validated: ${r.validated ? 'yes' : 'no'}`);
271
- console.log(` image hash: ${r.image_ipfs_hash}`);
272
- console.log(` version: ${r.image_version}`);
273
- console.log(` reward addr: ${r.reward_address}`);
274
- console.log(` attestation: ${r.attestation_session ? 'MRENCLAVE session present' : 'none'}`);
275
- } else {
276
- console.log(` not found (${r.error || r.note || 'not registered'})`);
277
- }
278
- }
279
- const e = info.esr || {};
280
- console.log('ESR');
281
- if (e.note) { console.log(` ${e.note}`); return; }
282
- console.log(` registry: ${e.esr_contract}`);
283
- if ('total_registry_entries' in e) console.log(` total keys: ${e.total_registry_entries} (all enclaves)`);
284
- if (e.recent_commits) {
285
- console.log(` recent state commits for this enclave: ${e.recent_commits.length}`);
286
- e.recent_commits.forEach((c) => console.log(` v${c.version} seq ${c.seq} nonce ${c.nonce} ${c.key_hash} ${c.cid} (block ${c.block})`));
287
- } else if (e.recent_commits_note) console.log(` ${e.recent_commits_note}`);
288
- else if (e.recent_commits_error) console.log(` (could not read commits: ${e.recent_commits_error})`);
289
- }
290
-
291
- async function main() {
292
- const args = parseArgs(process.argv.slice(2));
293
- const section = args._[0]; // network | trustedzone | securelock | esr | undefined
294
- const { name: netName, net } = resolveNetwork(args.network);
295
- const prov = provider(net.rpc);
296
-
297
- const ipfs = args.ipfs || process.env.IPFS_HASH || null;
298
- const projName = args.name || process.env.PROJECT_NAME || null;
299
-
300
- if (section === 'esr') {
301
- args.esrCmd = args._[1];
302
- const obj = await esrQuery(prov, netName, net, args);
303
- emit(obj, args.json);
304
- return;
305
- }
306
- if (section === 'network') { emit(await sectionNetwork(prov, netName, net), args.json); return; }
307
- if (section === 'trustedzone') { emit(await sectionRegistration(prov, net, 'trustedzone', ipfs, projName), args.json); return; }
308
- if (section === 'securelock') { emit(await sectionRegistration(prov, net, 'securelock', ipfs, projName), args.json); return; }
309
-
310
- const info = {
311
- project: { name: projName || '(unknown)', dapp_type: process.env.SERVICE_TYPE || process.env.DAPP_TYPE, version: process.env.VERSION },
312
- network: await sectionNetwork(prov, netName, net),
313
- trustedzone: await sectionRegistration(prov, net, 'trustedzone', ipfs, projName),
314
- securelock: await sectionRegistration(prov, net, 'securelock', ipfs, projName),
315
- esr: await sectionEsr(prov, net, args.enclave || null, parseInt(args.events || 10, 10)),
316
- };
317
- if (args.json) { console.log(JSON.stringify(info, null, 2)); return; }
318
- const pr = info.project;
319
- console.log(`PROJECT ${pr.name}${pr.dapp_type ? ' type ' + pr.dapp_type : ''}${pr.version != null ? ' version ' + pr.version : ''}`);
320
- printFull(info);
321
- }
322
-
323
- 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); });
@@ -435,14 +435,14 @@ class StateRegistry {
435
435
 
436
436
  /**
437
437
  * The last accepted idempotency nonce for `key` (0 if none was ever used).
438
- * A dApp that wants duplicate suppression reads this, picks a greater value
439
- * (e.g. +1, or a timestamp), and passes it to commit(..., { nonce }).
438
+ * A dApp that wants duplicate suppression reads this and passes
439
+ * getNonce(key) + 1 to commit(..., { nonce }) -- the contract accepts
440
+ * EXACTLY the next value in sequence, nothing else.
440
441
  *
441
442
  * The nonce is PUBLIC data: the registry records it on-chain next to the
442
443
  * version, and anyone can read it with a free eth_call (getNonce) -- so no
443
444
  * read-ACL applies here, and web3 clients see the same value via the
444
- * runner's esrNonce. Use opaque monotonic values (a counter or a
445
- * timestamp), never secret-derived ones.
445
+ * runner's esrNonce.
446
446
  *
447
447
  * Reads the chain (authoritative, post-relay), merged with this task's
448
448
  * own accepted commits so `commit(..., { nonce: N }); getNonce()` returns N
@@ -462,10 +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 STRICTLY GREATER than the last accepted nonce for
467
- // the key (see getNonce). A duplicate or stale nonce throws
468
- // StateNonceError and the state is NOT 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.
469
474
  const { attempts = 3, nonce = null } =
470
475
  typeof opts === 'number' ? { attempts: opts } : (opts || {});
471
476
  const transform = async (acl, data) => {
@@ -498,25 +503,40 @@ class StateRegistry {
498
503
  // nonce (chain merged with this task's own accepted commits), so a
499
504
  // duplicate fails in-enclave with task code 36 instead of as a failed
500
505
  // relay. The contract re-enforces the same rule on-chain either way.
506
+ let finalNonce;
501
507
  if (nonce != null) {
502
- 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
+ }
503
516
  // eslint-disable-next-line no-await-in-loop
504
517
  const storedNonce = await this.getNonce(key);
505
- if (n <= storedNonce) {
518
+ if (finalNonce !== storedNonce + 1) {
506
519
  throw new StateNonceError(
507
- `nonce ${n} was already used for state key '${key}' (last accepted: ` +
508
- `${storedNonce}); duplicate commit suppressed, state unchanged`
520
+ `nonce ${finalNonce} is out of sequence for state key '${key}' (last accepted: ` +
521
+ `${storedNonce}, expected: ${storedNonce + 1}); commit suppressed, ` +
522
+ 'state unchanged'
509
523
  );
510
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;
511
534
  }
512
535
  // eslint-disable-next-line no-await-in-loop
513
536
  const [nextAcl, newData] = await transform(acl, data, currentVersion);
514
- // The blob carries a REPORTING COPY of what the registry will store for
515
- // this key after this commit: the supplied nonce, or the preserved
516
- // 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
517
539
  // verify the two match.
518
- // eslint-disable-next-line no-await-in-loop
519
- const finalNonce = nonce != null ? Math.trunc(nonce) : ((await this.getNonce(key)) || null);
520
540
  // Bind the authoring caller into the blob (hence the CID/signature) for
521
541
  // owned state.
522
542
  const stored = wrapStored(nextAcl, newData, nextAcl ? _taskCaller : null, finalNonce);
@@ -531,7 +551,8 @@ class StateRegistry {
531
551
  await this._publish(key, blob, cid);
532
552
 
533
553
  try {
534
- // 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.
535
556
  // eslint-disable-next-line no-await-in-loop
536
557
  await this._sendCommit(keyHash(key), cid, currentVersion,
537
558
  nextAcl ? normAddr(nextAcl.owner) : null,
@@ -539,7 +560,7 @@ class StateRegistry {
539
560
  // Record the POST-commit values (the DATA -- the ACL never leaves in
540
561
  // results): this is what the chain shows once the relay lands.
541
562
  ledgerRecord(key, currentVersion + 1, cid, newData);
542
- if (nonce != null) _taskNonces[key] = Math.trunc(nonce);
563
+ _taskNonces[key] = finalNonce;
543
564
  return [nextAcl, newData];
544
565
  } catch (e) {
545
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 IN ORDER per (enclave, key), exactly like
132
- // the contract: nonce != 0 must be strictly greater than the stored value
133
- // (gaps allowed) 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,8 +141,10 @@ 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 <= storedNonce) {
144
+ if (n === 0) {
145
+ storedNonce += 1;
146
+ } else {
147
+ if (n !== storedNonce + 1) {
145
148
  throw new Error(`NonceOutOfOrder: stored ${storedNonce}, given ${n}`);
146
149
  }
147
150
  storedNonce = n;
@@ -38,43 +38,63 @@ async function main() {
38
38
  st = await r.get('counter');
39
39
  if (st.n !== 1) throw new Error('duplicate mutated state');
40
40
 
41
- // 3. next nonce and a gap both accepted (in order, gaps allowed)
41
+ // 3. exactly +1 accepted; a gap (skip-ahead) is rejected
42
42
  await r.commit('counter', (s) => ({ n: s.n + 1 }), { nonce: 2 });
43
- await r.commit('counter', (s) => ({ n: s.n + 1 }), { nonce: 10 });
44
43
  st = await r.get('counter');
45
- if (st.n !== 3 || (await r.getNonce('counter')) !== 10) throw new Error('step3');
46
- console.log('3. in-order nonces 2 then 10 (gap) ok, getNonce()==10');
44
+ if (st.n !== 2 || (await r.getNonce('counter')) !== 2) throw new Error('step3a');
45
+ threw = false;
46
+ try {
47
+ await r.commit('counter', (s) => ({ n: s.n + 1 }), { nonce: 10 });
48
+ } catch (e) {
49
+ if (!(e instanceof StateNonceError)) throw e;
50
+ threw = true;
51
+ }
52
+ if (!threw) throw new Error('gap accepted');
53
+ console.log('3. nonce=2 (+1) accepted, gap nonce=10 rejected ok');
47
54
 
48
- // 4. nonce-less commit preserves the stored nonce
55
+ // 4. nonce-less commit AUTO-takes the next in sequence (2 -> 3)
49
56
  await r.commit('counter', (s) => ({ ...s, note: 'free' }));
50
- if ((await r.getNonce('counter')) !== 10) throw new Error('step4');
51
- 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');
52
59
 
53
- // 5. out-of-order (lower, non-zero) nonce rejected
60
+ // 5. stale (reused) nonce rejected; the exact next one still works
54
61
  threw = false;
55
62
  try {
56
- await r.commit('counter', (s) => s, { nonce: 5 });
63
+ await r.commit('counter', (s) => s, { nonce: 1 });
57
64
  } catch (e) {
58
65
  if (!(e instanceof StateNonceError)) throw e;
59
66
  threw = true;
60
67
  }
61
68
  if (!threw) throw new Error('stale accepted');
62
- console.log('5. out-of-order nonce=5 rejected ok');
63
-
64
- // 6. contract-side (registry) enforcement, bypassing the SDK pre-check
65
- const mc = new MemContract(null);
66
- const kh = '0x' + '11'.repeat(32);
67
- mc.applyCommit('0xE', kh, 'cid1', 0, 7);
68
69
  threw = false;
69
70
  try {
70
- mc.applyCommit('0xE', kh, 'cid2', 1, 7);
71
+ await r.commit('counter', (s) => s, { nonce: 0 });
71
72
  } catch (e) {
72
- if (!/NonceOutOfOrder/.test(String(e.message))) throw e;
73
+ if (!/reserved/.test(String(e.message))) throw e;
73
74
  threw = true;
74
- console.log('6. contract-side in-order enforcement ok:', e.message);
75
75
  }
76
- if (!threw) throw new Error('registry accepted stale nonce');
77
- if (Number(await mc.getNonce('0xE', kh)) !== 7) throw new Error('getNonce view');
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');
80
+
81
+ // 6. contract-side (registry) enforcement, bypassing the SDK pre-check
82
+ const mc = new MemContract(null);
83
+ const kh = '0x' + '11'.repeat(32);
84
+ mc.applyCommit('0xE', kh, 'cid1', 0, 1);
85
+ for (const bad of [1, 3]) { // reuse and gap both violate the +1 rule
86
+ threw = false;
87
+ try {
88
+ mc.applyCommit('0xE', kh, 'cid2', 1, bad);
89
+ } catch (e) {
90
+ if (!/NonceOutOfOrder/.test(String(e.message))) throw e;
91
+ threw = true;
92
+ }
93
+ if (!threw) throw new Error('registry accepted nonce ' + bad);
94
+ }
95
+ mc.applyCommit('0xE', kh, 'cid2', 1, 2);
96
+ if (Number(await mc.getNonce('0xE', kh)) !== 2) throw new Error('getNonce view');
97
+ console.log('6. contract-side +1 enforcement ok (reuse and gap both revert)');
78
98
 
79
99
  // 7. chain-first getNonce after a task-memo wipe (fresh process/other task)
80
100
  delete require.cache[require.resolve(path.join(__dirname, 'ecld_state.js'))];
@@ -82,8 +102,8 @@ async function main() {
82
102
  esrLocal.install(ecld2, { caller: A, filePrefix: 'st' });
83
103
  const r2 = new ecld2.StateRegistry();
84
104
  const n2 = await r2.getNonce('counter');
85
- if (n2 !== 10) throw new Error('step7 got ' + n2);
86
- console.log('7. chain-first getNonce()==10 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');
87
107
 
88
108
  // 8. public: nonce read needs no ACL -- content read stays denied for B
89
109
  delete require.cache[require.resolve(path.join(__dirname, 'ecld_state.js'))];
@@ -435,14 +435,14 @@ class StateRegistry {
435
435
 
436
436
  /**
437
437
  * The last accepted idempotency nonce for `key` (0 if none was ever used).
438
- * A dApp that wants duplicate suppression reads this, picks a greater value
439
- * (e.g. +1, or a timestamp), and passes it to commit(..., { nonce }).
438
+ * A dApp that wants duplicate suppression reads this and passes
439
+ * getNonce(key) + 1 to commit(..., { nonce }) -- the contract accepts
440
+ * EXACTLY the next value in sequence, nothing else.
440
441
  *
441
442
  * The nonce is PUBLIC data: the registry records it on-chain next to the
442
443
  * version, and anyone can read it with a free eth_call (getNonce) -- so no
443
444
  * read-ACL applies here, and web3 clients see the same value via the
444
- * runner's esrNonce. Use opaque monotonic values (a counter or a
445
- * timestamp), never secret-derived ones.
445
+ * runner's esrNonce.
446
446
  *
447
447
  * Reads the chain (authoritative, post-relay), merged with this task's
448
448
  * own accepted commits so `commit(..., { nonce: N }); getNonce()` returns N
@@ -462,10 +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 STRICTLY GREATER than the last accepted nonce for
467
- // the key (see getNonce). A duplicate or stale nonce throws
468
- // StateNonceError and the state is NOT 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.
469
474
  const { attempts = 3, nonce = null } =
470
475
  typeof opts === 'number' ? { attempts: opts } : (opts || {});
471
476
  const transform = async (acl, data) => {
@@ -498,25 +503,40 @@ class StateRegistry {
498
503
  // nonce (chain merged with this task's own accepted commits), so a
499
504
  // duplicate fails in-enclave with task code 36 instead of as a failed
500
505
  // relay. The contract re-enforces the same rule on-chain either way.
506
+ let finalNonce;
501
507
  if (nonce != null) {
502
- 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
+ }
503
516
  // eslint-disable-next-line no-await-in-loop
504
517
  const storedNonce = await this.getNonce(key);
505
- if (n <= storedNonce) {
518
+ if (finalNonce !== storedNonce + 1) {
506
519
  throw new StateNonceError(
507
- `nonce ${n} was already used for state key '${key}' (last accepted: ` +
508
- `${storedNonce}); duplicate commit suppressed, state unchanged`
520
+ `nonce ${finalNonce} is out of sequence for state key '${key}' (last accepted: ` +
521
+ `${storedNonce}, expected: ${storedNonce + 1}); commit suppressed, ` +
522
+ 'state unchanged'
509
523
  );
510
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;
511
534
  }
512
535
  // eslint-disable-next-line no-await-in-loop
513
536
  const [nextAcl, newData] = await transform(acl, data, currentVersion);
514
- // The blob carries a REPORTING COPY of what the registry will store for
515
- // this key after this commit: the supplied nonce, or the preserved
516
- // 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
517
539
  // verify the two match.
518
- // eslint-disable-next-line no-await-in-loop
519
- const finalNonce = nonce != null ? Math.trunc(nonce) : ((await this.getNonce(key)) || null);
520
540
  // Bind the authoring caller into the blob (hence the CID/signature) for
521
541
  // owned state.
522
542
  const stored = wrapStored(nextAcl, newData, nextAcl ? _taskCaller : null, finalNonce);
@@ -531,7 +551,8 @@ class StateRegistry {
531
551
  await this._publish(key, blob, cid);
532
552
 
533
553
  try {
534
- // 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.
535
556
  // eslint-disable-next-line no-await-in-loop
536
557
  await this._sendCommit(keyHash(key), cid, currentVersion,
537
558
  nextAcl ? normAddr(nextAcl.owner) : null,
@@ -539,7 +560,7 @@ class StateRegistry {
539
560
  // Record the POST-commit values (the DATA -- the ACL never leaves in
540
561
  // results): this is what the chain shows once the relay lands.
541
562
  ledgerRecord(key, currentVersion + 1, cid, newData);
542
- if (nonce != null) _taskNonces[key] = Math.trunc(nonce);
563
+ _taskNonces[key] = finalNonce;
543
564
  return [nextAcl, newData];
544
565
  } catch (e) {
545
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 IN ORDER per (enclave, key), exactly like
132
- // the contract: nonce != 0 must be strictly greater than the stored value
133
- // (gaps allowed) 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,8 +141,10 @@ 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 <= storedNonce) {
144
+ if (n === 0) {
145
+ storedNonce += 1;
146
+ } else {
147
+ if (n !== storedNonce + 1) {
145
148
  throw new Error(`NonceOutOfOrder: stored ${storedNonce}, given ${n}`);
146
149
  }
147
150
  storedNonce = n;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.5.14",
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",