@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 +324 -323
- package/nodenithy/build/securelock/.ecld-test-stage/ecld_state.js +40 -19
- package/nodenithy/build/securelock/.ecld-test-stage/esr_local.js +8 -5
- package/nodenithy/build/securelock/.ecld-test-stage/nonce_ladder.js +42 -22
- package/nodenithy/build/securelock/src/ecld_state.js +40 -19
- package/nodenithy/build/securelock/src/esr_local.js +8 -5
- package/package.json +1 -1
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: '
|
|
44
|
-
},
|
|
45
|
-
BLOXBERG_TESTNET: {
|
|
46
|
-
type: 'testnet', chainId: 8995, rpc: 'https://core.bloxberg.org',
|
|
47
|
-
protocol: '0x02882F03097fE8cD31afbdFbB5D72a498B41112c',
|
|
48
|
-
imageRegistry: '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31',
|
|
49
|
-
esr: '
|
|
50
|
-
},
|
|
51
|
-
LITVM_LITEFORGE: {
|
|
52
|
-
type: 'testnet', chainId: 0, rpc: 'https://liteforge.rpc.caldera.xyz/infra-partner-http',
|
|
53
|
-
protocol: '', imageRegistry: '', esr: '
|
|
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
|
|
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: '
|
|
205
|
-
+ '0 =
|
|
206
|
-
+ '
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
nonce
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
else a.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
console.log(
|
|
262
|
-
console.log(`
|
|
263
|
-
console.log(`
|
|
264
|
-
console.log(`
|
|
265
|
-
console.log(`
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
console.log(`
|
|
272
|
-
console.log(`
|
|
273
|
-
console.log(`
|
|
274
|
-
console.log(`
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
console.log(`
|
|
283
|
-
|
|
284
|
-
if (e
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
else if (e.
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (section === '
|
|
308
|
-
if (section === '
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
439
|
-
* (
|
|
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.
|
|
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 }.
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
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
|
-
|
|
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 (
|
|
518
|
+
if (finalNonce !== storedNonce + 1) {
|
|
506
519
|
throw new StateNonceError(
|
|
507
|
-
`nonce ${
|
|
508
|
-
`${storedNonce});
|
|
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
|
|
515
|
-
// this key after this commit
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
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
|
|
144
|
-
|
|
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.
|
|
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 !==
|
|
46
|
-
|
|
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
|
|
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')) !==
|
|
51
|
-
console.log('4. nonce-less commit
|
|
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.
|
|
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:
|
|
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
|
-
|
|
71
|
+
await r.commit('counter', (s) => s, { nonce: 0 });
|
|
71
72
|
} catch (e) {
|
|
72
|
-
if (!/
|
|
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('
|
|
77
|
-
|
|
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 !==
|
|
86
|
-
console.log('7. chain-first getNonce()==
|
|
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
|
|
439
|
-
* (
|
|
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.
|
|
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 }.
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
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
|
-
|
|
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 (
|
|
518
|
+
if (finalNonce !== storedNonce + 1) {
|
|
506
519
|
throw new StateNonceError(
|
|
507
|
-
`nonce ${
|
|
508
|
-
`${storedNonce});
|
|
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
|
|
515
|
-
// this key after this commit
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
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
|
|
144
|
-
|
|
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