@ethernity-cloud/sdk-js 1.5.5 → 1.5.9
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 +306 -0
- package/nodenithy/build/securelock/src/ecld_state.js +251 -18
- package/nodenithy/build/securelock/src/etny_crypto.js +211 -211
- package/nodenithy/build/securelock/src/etny_exec.js +13 -2
- package/nodenithy/build/securelock/src/models.js +275 -275
- package/nodenithy/build/securelock/src/securelock.js.tmpl +632 -586
- package/nodenithy/build/securelock/src/task_status.js +4 -0
- package/nodenithy/build.mjs +30 -0
- package/nodenithy/run/__pycache__/swift_stream_service.cpython-312.pyc +0 -0
- package/nodenithy/run/image_registry.js +371 -371
- package/nodenithy/src/preStart.js +44 -44
- package/package.json +7 -4
- package/pynithy/build/docker-compose.yml +67 -67
- package/pynithy/run/certs/cert.pem +31 -31
- package/pynithy/run/certs/challenge.crt +10 -10
- package/pynithy/run/certs/key.pem +52 -52
- package/pynithy/run/docker-compose-final.yml +59 -59
- package/pynithy/run/docker-compose-swift-stream.yml +27 -27
- package/pynithy/run/docker-compose.yml +55 -55
- package/pynithy/run/image_registry.js +371 -371
package/info.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
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|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: '0xF76469A5659670B6ade366dE635e6463aaB8f3D8',
|
|
44
|
+
},
|
|
45
|
+
BLOXBERG_TESTNET: {
|
|
46
|
+
type: 'testnet', chainId: 8995, rpc: 'https://core.bloxberg.org',
|
|
47
|
+
protocol: '0x02882F03097fE8cD31afbdFbB5D72a498B41112c',
|
|
48
|
+
imageRegistry: '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31',
|
|
49
|
+
esr: '0xda5e68Bb5e68ee14D73b8de2a4D3Ca15736fACfb',
|
|
50
|
+
},
|
|
51
|
+
LITVM_LITEFORGE: {
|
|
52
|
+
type: 'testnet', chainId: 0, rpc: 'https://liteforge.rpc.caldera.xyz/infra-partner-http',
|
|
53
|
+
protocol: '', imageRegistry: '', esr: '0xbAa7F9E3287ff95D177104eD469E6d0Fd19dBB0F',
|
|
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
|
+
'function entryCount() view returns (uint256)',
|
|
69
|
+
'function getEntriesFrom(uint256 start, uint256 limit) view returns (address[] enclaves, bytes32[] keys, string[] cids, uint256[] versions, uint64[] updatedAts, uint256 total)',
|
|
70
|
+
'event StateCommitted(address indexed enclave, bytes32 indexed key, string cid, uint256 version, uint256 seq)',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
function resolveNetwork(name) {
|
|
74
|
+
name = (name || process.env.ESR_NETWORK || process.env.BLOCKCHAIN_NETWORK || 'BLOXBERG_TESTNET')
|
|
75
|
+
.toUpperCase();
|
|
76
|
+
const net = NETWORKS[name];
|
|
77
|
+
if (!net) {
|
|
78
|
+
throw new Error(`Unknown network '${name}'. Known: ${Object.keys(NETWORKS).join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
return { name, net };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function provider(rpc) {
|
|
84
|
+
return new ethers.providers.JsonRpcProvider(rpc);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function keyHash(key) {
|
|
88
|
+
return ethers.utils.id(key); // keccak256(utf8(key))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function looksLikeCID(cid) {
|
|
92
|
+
return typeof cid === 'string' && (cid.startsWith('Qm') || cid.startsWith('bafk')) && cid.length > 20;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function decodeDetails(d, fallbackName) {
|
|
96
|
+
return {
|
|
97
|
+
name: d.name || fallbackName,
|
|
98
|
+
published: Boolean(d.published),
|
|
99
|
+
validated: Boolean(d.validated),
|
|
100
|
+
image_ipfs_hash: d.ipfsHash || null,
|
|
101
|
+
image_version: d.version || null,
|
|
102
|
+
reward_address: d.rewardAddress,
|
|
103
|
+
owner: d.owner,
|
|
104
|
+
docker_compose_hash: d.dockerComposeHash || null,
|
|
105
|
+
attestation_session: Boolean(d.session),
|
|
106
|
+
cert_present: Boolean(d.certPublicKey),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function lookupRegistration(reg, kind, ipfs, name) {
|
|
111
|
+
const detailsFn = kind === 'trustedzone' ? 'trustedZoneImageDetails' : 'imageDetails';
|
|
112
|
+
const versionsFn = kind === 'trustedzone' ? 'trustedZoneImageVersions' : 'imageVersions';
|
|
113
|
+
if (ipfs) {
|
|
114
|
+
try { return decodeDetails(await reg[detailsFn](ipfs), ipfs); }
|
|
115
|
+
catch (e) { return { found: false, error: e.message }; }
|
|
116
|
+
}
|
|
117
|
+
if (name) {
|
|
118
|
+
let latest = null;
|
|
119
|
+
for (let i = 0; i < 64; i += 1) {
|
|
120
|
+
let v;
|
|
121
|
+
try { v = await reg[versionsFn](name, i); } catch (e) { break; }
|
|
122
|
+
if (!v) break;
|
|
123
|
+
latest = v;
|
|
124
|
+
}
|
|
125
|
+
if (latest) {
|
|
126
|
+
try { return decodeDetails(await reg[detailsFn](latest), name); }
|
|
127
|
+
catch (e) { return { found: false, error: e.message }; }
|
|
128
|
+
}
|
|
129
|
+
return { found: false, note: `no registered version for name '${name}' -- the Image Registry is keyed by image IPFS hash; pass --ipfs <hash>` };
|
|
130
|
+
}
|
|
131
|
+
return { found: false, note: 'no --ipfs or --name to look up' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function sectionNetwork(prov, name, net) {
|
|
135
|
+
const out = {
|
|
136
|
+
network: name, type: net.type, chain_id: net.chainId, rpc: net.rpc,
|
|
137
|
+
protocol_contract: net.protocol || '(n/a)',
|
|
138
|
+
image_registry: net.imageRegistry || '(n/a)',
|
|
139
|
+
esr_contract: net.esr || '(not deployed)',
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
out.latest_block = await prov.getBlockNumber();
|
|
143
|
+
out.connected = true;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
out.connected = false; out.error = e.message;
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function sectionRegistration(prov, net, kind, ipfs, name) {
|
|
151
|
+
if (!net.imageRegistry) return { note: 'no image registry on this network' };
|
|
152
|
+
const reg = new ethers.Contract(net.imageRegistry, IMAGE_REGISTRY_ABI, prov);
|
|
153
|
+
return lookupRegistration(reg, kind, ipfs, name);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function sectionEsr(prov, net, enclaveWallet, eventsN) {
|
|
157
|
+
if (!net.esr) return { note: 'ESR is not deployed on this network' };
|
|
158
|
+
const esr = new ethers.Contract(net.esr, ESR_ABI, prov);
|
|
159
|
+
const out = { esr_contract: net.esr };
|
|
160
|
+
try { out.total_registry_entries = (await esr.entryCount()).toNumber(); } catch (e) { /* ignore */ }
|
|
161
|
+
if (enclaveWallet) {
|
|
162
|
+
try {
|
|
163
|
+
const latest = await prov.getBlockNumber();
|
|
164
|
+
const from = Math.max(0, latest - 500000);
|
|
165
|
+
const flt = esr.filters.StateCommitted(ethers.utils.getAddress(enclaveWallet));
|
|
166
|
+
let logs = await esr.queryFilter(flt, from, 'latest');
|
|
167
|
+
logs = logs.slice(-eventsN);
|
|
168
|
+
out.recent_commits = logs.map((l) => ({
|
|
169
|
+
key_hash: l.args.key,
|
|
170
|
+
version: l.args.version.toNumber(),
|
|
171
|
+
cid: l.args.cid,
|
|
172
|
+
seq: l.args.seq.toNumber(),
|
|
173
|
+
block: l.blockNumber,
|
|
174
|
+
}));
|
|
175
|
+
} catch (e) {
|
|
176
|
+
out.recent_commits_error = e.message;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
out.recent_commits_note = 'pass --enclave <ESR wallet> to list this enclave\'s state commits';
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function esrQuery(prov, name, net, args) {
|
|
185
|
+
const esrAddr = args.contract || net.esr;
|
|
186
|
+
const sub = args.esrCmd || 'address';
|
|
187
|
+
if (sub === 'address') return { network: name, esr_contract: esrAddr || '(not deployed)', rpc: net.rpc };
|
|
188
|
+
if (!esrAddr) throw new Error(`ESR is not deployed on ${name}.`);
|
|
189
|
+
const c = new ethers.Contract(esrAddr, ESR_ABI, prov);
|
|
190
|
+
if (sub === 'count') return { network: name, entry_count: (await c.entryCount()).toNumber() };
|
|
191
|
+
if (sub === 'version') {
|
|
192
|
+
const v = (await c.getVersion(ethers.utils.getAddress(args.enclave), keyHash(args.key))).toNumber();
|
|
193
|
+
return { enclave: args.enclave, key: args.key, version: v };
|
|
194
|
+
}
|
|
195
|
+
if (sub === 'state') {
|
|
196
|
+
const enclave = ethers.utils.getAddress(args.enclave);
|
|
197
|
+
const kh = keyHash(args.key);
|
|
198
|
+
const [cid, version, updated] = await c.getState(enclave, kh);
|
|
199
|
+
const exists = await c.exists(enclave, kh);
|
|
200
|
+
return {
|
|
201
|
+
network: name, enclave: args.enclave, key: args.key, key_hash: kh,
|
|
202
|
+
exists, version: version.toNumber(), cid: cid || null, cid_valid: looksLikeCID(cid),
|
|
203
|
+
updated_at: updated.toNumber(),
|
|
204
|
+
note: 'cid points at ENCRYPTED state; only the enclave can decrypt it',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (sub === 'list') {
|
|
208
|
+
const start = parseInt(args.start || 0, 10);
|
|
209
|
+
const limit = parseInt(args.limit || 50, 10);
|
|
210
|
+
const [encs, keys, cids, versions, updated, total] = await c.getEntriesFrom(start, limit);
|
|
211
|
+
const entries = encs.map((e, i) => ({
|
|
212
|
+
enclave: e, key_hash: keys[i], cid: cids[i] || null,
|
|
213
|
+
version: versions[i].toNumber(), updated_at: updated[i].toNumber(),
|
|
214
|
+
}));
|
|
215
|
+
return { network: name, total: total.toNumber(), start, returned: entries.length, entries };
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`unknown esr subcommand: ${sub}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- arg parsing (tiny, dependency-free) -----------------------------------
|
|
221
|
+
|
|
222
|
+
function parseArgs(argv) {
|
|
223
|
+
const a = { _: [], json: false };
|
|
224
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
225
|
+
const t = argv[i];
|
|
226
|
+
if (t === '--json') a.json = true;
|
|
227
|
+
else if (t.startsWith('--')) { a[t.slice(2)] = argv[i + 1]; i += 1; }
|
|
228
|
+
else a._.push(t);
|
|
229
|
+
}
|
|
230
|
+
return a;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function emit(obj, asJson) {
|
|
234
|
+
if (asJson) { console.log(JSON.stringify(obj, null, 2)); return; }
|
|
235
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
236
|
+
if (Array.isArray(v)) { console.log(`${k}:`); v.forEach((it) => console.log(' ' + JSON.stringify(it))); }
|
|
237
|
+
else console.log(`${k}: ${v}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function printFull(info) {
|
|
242
|
+
const n = info.network;
|
|
243
|
+
console.log('NETWORK');
|
|
244
|
+
console.log(` ${n.network} (${n.type}, chain ${n.chain_id}) block ${n.latest_block || '?'} ${n.connected ? 'connected' : 'NOT CONNECTED'}`);
|
|
245
|
+
console.log(` rpc: ${n.rpc}`);
|
|
246
|
+
console.log(` protocol: ${n.protocol_contract}`);
|
|
247
|
+
console.log(` image registry: ${n.image_registry}`);
|
|
248
|
+
console.log(` esr: ${n.esr_contract}`);
|
|
249
|
+
for (const [label, key] of [['TRUSTEDZONE', 'trustedzone'], ['SECURELOCK', 'securelock']]) {
|
|
250
|
+
const r = info[key] || {};
|
|
251
|
+
console.log(label);
|
|
252
|
+
if (r.published || r.validated || r.image_ipfs_hash) {
|
|
253
|
+
console.log(` published: ${r.published ? 'yes' : 'no'} validated: ${r.validated ? 'yes' : 'no'}`);
|
|
254
|
+
console.log(` image hash: ${r.image_ipfs_hash}`);
|
|
255
|
+
console.log(` version: ${r.image_version}`);
|
|
256
|
+
console.log(` reward addr: ${r.reward_address}`);
|
|
257
|
+
console.log(` attestation: ${r.attestation_session ? 'MRENCLAVE session present' : 'none'}`);
|
|
258
|
+
} else {
|
|
259
|
+
console.log(` not found (${r.error || r.note || 'not registered'})`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const e = info.esr || {};
|
|
263
|
+
console.log('ESR');
|
|
264
|
+
if (e.note) { console.log(` ${e.note}`); return; }
|
|
265
|
+
console.log(` registry: ${e.esr_contract}`);
|
|
266
|
+
if ('total_registry_entries' in e) console.log(` total keys: ${e.total_registry_entries} (all enclaves)`);
|
|
267
|
+
if (e.recent_commits) {
|
|
268
|
+
console.log(` recent state commits for this enclave: ${e.recent_commits.length}`);
|
|
269
|
+
e.recent_commits.forEach((c) => console.log(` v${c.version} seq ${c.seq} ${c.key_hash} ${c.cid} (block ${c.block})`));
|
|
270
|
+
} else if (e.recent_commits_note) console.log(` ${e.recent_commits_note}`);
|
|
271
|
+
else if (e.recent_commits_error) console.log(` (could not read commits: ${e.recent_commits_error})`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function main() {
|
|
275
|
+
const args = parseArgs(process.argv.slice(2));
|
|
276
|
+
const section = args._[0]; // network | trustedzone | securelock | esr | undefined
|
|
277
|
+
const { name: netName, net } = resolveNetwork(args.network);
|
|
278
|
+
const prov = provider(net.rpc);
|
|
279
|
+
|
|
280
|
+
const ipfs = args.ipfs || process.env.IPFS_HASH || null;
|
|
281
|
+
const projName = args.name || process.env.PROJECT_NAME || null;
|
|
282
|
+
|
|
283
|
+
if (section === 'esr') {
|
|
284
|
+
args.esrCmd = args._[1];
|
|
285
|
+
const obj = await esrQuery(prov, netName, net, args);
|
|
286
|
+
emit(obj, args.json);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (section === 'network') { emit(await sectionNetwork(prov, netName, net), args.json); return; }
|
|
290
|
+
if (section === 'trustedzone') { emit(await sectionRegistration(prov, net, 'trustedzone', ipfs, projName), args.json); return; }
|
|
291
|
+
if (section === 'securelock') { emit(await sectionRegistration(prov, net, 'securelock', ipfs, projName), args.json); return; }
|
|
292
|
+
|
|
293
|
+
const info = {
|
|
294
|
+
project: { name: projName || '(unknown)', dapp_type: process.env.SERVICE_TYPE || process.env.DAPP_TYPE, version: process.env.VERSION },
|
|
295
|
+
network: await sectionNetwork(prov, netName, net),
|
|
296
|
+
trustedzone: await sectionRegistration(prov, net, 'trustedzone', ipfs, projName),
|
|
297
|
+
securelock: await sectionRegistration(prov, net, 'securelock', ipfs, projName),
|
|
298
|
+
esr: await sectionEsr(prov, net, args.enclave || null, parseInt(args.events || 10, 10)),
|
|
299
|
+
};
|
|
300
|
+
if (args.json) { console.log(JSON.stringify(info, null, 2)); return; }
|
|
301
|
+
const pr = info.project;
|
|
302
|
+
console.log(`PROJECT ${pr.name}${pr.dapp_type ? ' type ' + pr.dapp_type : ''}${pr.version != null ? ' version ' + pr.version : ''}`);
|
|
303
|
+
printFull(info);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
main().catch((e) => { console.error(`ecld-info: ${e.message}`); process.exit(1); });
|
|
@@ -54,7 +54,7 @@ let _provider = null;
|
|
|
54
54
|
* Wire the registry to the enclave's identity, storage and chain access.
|
|
55
55
|
* Called by securelock at task start; payload code never calls this.
|
|
56
56
|
*/
|
|
57
|
-
function configure({ identityPriv, swiftStreamService, bucket, contractAddress, provider }) {
|
|
57
|
+
function configure({ identityPriv, swiftStreamService, bucket, contractAddress, provider, caller = null }) {
|
|
58
58
|
_identityPriv = identityPriv;
|
|
59
59
|
_swift = swiftStreamService;
|
|
60
60
|
_bucket = bucket;
|
|
@@ -62,6 +62,104 @@ function configure({ identityPriv, swiftStreamService, bucket, contractAddress,
|
|
|
62
62
|
_provider = provider;
|
|
63
63
|
_esrLedger = [];
|
|
64
64
|
_taskLedger = {};
|
|
65
|
+
_taskCaller = normAddr(caller);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The authenticated task caller: the wallet that placed the DO request, read
|
|
69
|
+
// from the PoX contract by the TRUSTEDZONE and forwarded over its signed
|
|
70
|
+
// handoff (caller.securelock + .sig, verified by securelock before configure).
|
|
71
|
+
// null when the trustedzone did not supply one -- anonymity is never a
|
|
72
|
+
// privilege: null cannot claim ownership or touch owned state.
|
|
73
|
+
let _taskCaller = null;
|
|
74
|
+
|
|
75
|
+
function taskCaller() {
|
|
76
|
+
return _taskCaller;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normAddr(addr) {
|
|
80
|
+
return typeof addr === 'string' && addr ? addr.toLowerCase() : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* State container + ACL. Stored blobs are either legacy (raw state --
|
|
85
|
+
* "unowned") or an owned container:
|
|
86
|
+
* { _ecld_state: 1, acl: {...}, data: <state> }
|
|
87
|
+
* acl = { owner, read: [], write: [], public_read: false }. Enforcement
|
|
88
|
+
* happens HERE, inside the enclave, against the trustedzone-attested caller.
|
|
89
|
+
*/
|
|
90
|
+
const CONTAINER_MARK = '_ecld_state';
|
|
91
|
+
|
|
92
|
+
class StatePermissionError extends Error {}
|
|
93
|
+
|
|
94
|
+
function unwrapStored(stored) {
|
|
95
|
+
if (stored && typeof stored === 'object' && stored[CONTAINER_MARK] === 1) {
|
|
96
|
+
return [stored.acl || null, stored.data];
|
|
97
|
+
}
|
|
98
|
+
return [null, stored];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function wrapStored(acl, data, committedBy = null) {
|
|
102
|
+
// For owned state, bind the authoring caller (committedBy) INTO the blob, so
|
|
103
|
+
// it lands in the CID -- the field the securelock signs -- making the caller
|
|
104
|
+
// cryptographically bound to the commit, not merely a re-stampable sidecar.
|
|
105
|
+
if (!acl) return data; // unowned keeps the legacy shape
|
|
106
|
+
const container = { [CONTAINER_MARK]: 1, acl, data };
|
|
107
|
+
if (committedBy) container.committedBy = committedBy;
|
|
108
|
+
return container;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Post-payload: re-assert the trusted caller across the ESR ledger. Called by
|
|
113
|
+
* the securelock after the payload returns, with the attested caller captured
|
|
114
|
+
* before execution (outside the payload's reach). Overwrites callerUsed (and
|
|
115
|
+
* re-fixes enclave to the real signer) in every owned entry and RE-STAGES the
|
|
116
|
+
* ledger, so a forged callerUsed cannot reach the trustedzone. Returns true if
|
|
117
|
+
* any entry's callerUsed differed from the trusted value (tamper detected).
|
|
118
|
+
* Defence in depth -- the trustedzone and the CID-bound caller are the sound
|
|
119
|
+
* layers; this runs in the same process the payload subverted.
|
|
120
|
+
*/
|
|
121
|
+
async function restampLedgerCaller(trustedCaller) {
|
|
122
|
+
const trusted = normAddr(trustedCaller);
|
|
123
|
+
if (!_esrLedger.length) return false;
|
|
124
|
+
let tampered = false;
|
|
125
|
+
let signerAddr = null;
|
|
126
|
+
try {
|
|
127
|
+
signerAddr = await new StateRegistry()._signer().getAddress();
|
|
128
|
+
} catch (e) { signerAddr = null; }
|
|
129
|
+
for (const entry of _esrLedger) {
|
|
130
|
+
if (!entry.owned) continue;
|
|
131
|
+
if (normAddr(entry.callerUsed) !== trusted) tampered = true;
|
|
132
|
+
entry.callerUsed = trusted || '';
|
|
133
|
+
if (signerAddr) entry.enclave = signerAddr;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await _swift.putFileContent(
|
|
137
|
+
_bucket, 'esr.authorizations.json', '',
|
|
138
|
+
Buffer.from(JSON.stringify(_esrLedger), 'utf8'));
|
|
139
|
+
} catch (e) { /* best-effort restage; trustedzone still adjudicates */ }
|
|
140
|
+
return tampered;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function aclMembers(acl, field) {
|
|
144
|
+
return new Set((acl[field] || []).map(normAddr).filter(Boolean));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function canRead(acl) {
|
|
148
|
+
if (!acl || acl.public_read) return true;
|
|
149
|
+
const c = _taskCaller;
|
|
150
|
+
if (!c) return false;
|
|
151
|
+
return c === normAddr(acl.owner) || aclMembers(acl, 'read').has(c) || aclMembers(acl, 'write').has(c);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function canWrite(acl) {
|
|
155
|
+
if (!acl) return true; // unowned: writable; claimed when a caller exists
|
|
156
|
+
const c = _taskCaller;
|
|
157
|
+
if (!c) return false;
|
|
158
|
+
return c === normAddr(acl.owner) || aclMembers(acl, 'write').has(c);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function newAcl(owner) {
|
|
162
|
+
return { owner, read: [], write: [], public_read: false };
|
|
65
163
|
}
|
|
66
164
|
|
|
67
165
|
// Task-scoped ledger of every state key this execution touched, recorded by
|
|
@@ -247,10 +345,22 @@ class StateRegistry {
|
|
|
247
345
|
* state" would let the next commit overwrite data that is still recoverable.
|
|
248
346
|
*/
|
|
249
347
|
async get(key, fallback = {}) {
|
|
348
|
+
const [acl, data, version, cid] = await this._readContainer(key, fallback);
|
|
349
|
+
if (!canRead(acl)) {
|
|
350
|
+
throw new StatePermissionError(
|
|
351
|
+
`caller ${_taskCaller || '<anonymous>'} has no read permission on ` +
|
|
352
|
+
`state key '${key}' (owner: ${acl.owner})`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
ledgerRecord(key, version || 0, cid, data);
|
|
356
|
+
return data;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** [acl, data, version, cid] for `key`; NO permission check here. */
|
|
360
|
+
async _readContainer(key, fallback = {}) {
|
|
250
361
|
const [cid, version] = await this._contract().getState(this.walletAddress, keyHash(key));
|
|
251
362
|
if (!version || !cid) {
|
|
252
|
-
|
|
253
|
-
return fallback;
|
|
363
|
+
return [null, fallback, 0, null];
|
|
254
364
|
}
|
|
255
365
|
if (!looksLikeCID(cid)) {
|
|
256
366
|
throw new Error(
|
|
@@ -259,9 +369,9 @@ class StateRegistry {
|
|
|
259
369
|
);
|
|
260
370
|
}
|
|
261
371
|
const blob = await this._fetch(key, cid);
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
return
|
|
372
|
+
const stored = JSON.parse(decrypt(blob).toString('utf8'));
|
|
373
|
+
const [acl, data] = unwrapStored(stored);
|
|
374
|
+
return [acl, data, Number(version), cid];
|
|
265
375
|
}
|
|
266
376
|
|
|
267
377
|
/**
|
|
@@ -272,15 +382,39 @@ class StateRegistry {
|
|
|
272
382
|
* retry, so concurrent tasks cannot silently lose updates.
|
|
273
383
|
*/
|
|
274
384
|
async commit(key, mutate, attempts = 3) {
|
|
385
|
+
const transform = async (acl, data) => {
|
|
386
|
+
if (!canWrite(acl)) {
|
|
387
|
+
throw new StatePermissionError(
|
|
388
|
+
`caller ${_taskCaller || '<anonymous>'} has no write permission on ` +
|
|
389
|
+
`state key '${key}' (owner: ${acl.owner})`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
let nextAcl = acl;
|
|
393
|
+
if (!nextAcl && _taskCaller) nextAcl = newAcl(_taskCaller); // first-writer-owns
|
|
394
|
+
return [nextAcl, await mutate(data)];
|
|
395
|
+
};
|
|
396
|
+
const [, newData] = await this._commitTransform(key, transform, attempts);
|
|
397
|
+
return newData;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Optimistic-concurrency commit of transform(acl, data, version) ->
|
|
402
|
+
* [newAcl, newData]. The stored blob is the wrapped container (or the bare
|
|
403
|
+
* data while unowned). On a version race we re-read and retry, so
|
|
404
|
+
* concurrent tasks cannot silently lose updates.
|
|
405
|
+
*/
|
|
406
|
+
async _commitTransform(key, transform, attempts = 3) {
|
|
275
407
|
let lastError = null;
|
|
276
408
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
277
409
|
// eslint-disable-next-line no-await-in-loop
|
|
278
|
-
const currentVersion = await this.
|
|
410
|
+
const [acl, data, currentVersion] = await this._readContainer(key);
|
|
279
411
|
// eslint-disable-next-line no-await-in-loop
|
|
280
|
-
const
|
|
281
|
-
|
|
412
|
+
const [nextAcl, newData] = await transform(acl, data, currentVersion);
|
|
413
|
+
// Bind the authoring caller into the blob (hence the CID/signature) for
|
|
414
|
+
// owned state.
|
|
415
|
+
const stored = wrapStored(nextAcl, newData, nextAcl ? _taskCaller : null);
|
|
282
416
|
|
|
283
|
-
const blob = encrypt(Buffer.from(JSON.stringify(
|
|
417
|
+
const blob = encrypt(Buffer.from(JSON.stringify(stored), 'utf8'));
|
|
284
418
|
// Compute the CID from OUR bytes — never accept one from the node.
|
|
285
419
|
const cid = cidv1Raw(blob);
|
|
286
420
|
|
|
@@ -291,11 +425,12 @@ class StateRegistry {
|
|
|
291
425
|
|
|
292
426
|
try {
|
|
293
427
|
// eslint-disable-next-line no-await-in-loop
|
|
294
|
-
await this._sendCommit(keyHash(key), cid, currentVersion
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
428
|
+
await this._sendCommit(keyHash(key), cid, currentVersion,
|
|
429
|
+
nextAcl ? normAddr(nextAcl.owner) : null);
|
|
430
|
+
// Record the POST-commit values (the DATA -- the ACL never leaves in
|
|
431
|
+
// results): this is what the chain shows once the relay lands.
|
|
432
|
+
ledgerRecord(key, currentVersion + 1, cid, newData);
|
|
433
|
+
return [nextAcl, newData];
|
|
299
434
|
} catch (e) {
|
|
300
435
|
const msg = String(e && e.message ? e.message : e);
|
|
301
436
|
if (/VersionMismatch/i.test(msg) || /version/i.test(msg)) {
|
|
@@ -339,7 +474,7 @@ class StateRegistry {
|
|
|
339
474
|
throw new Error(`Could not read state object for '${key}' (${cid})`);
|
|
340
475
|
}
|
|
341
476
|
|
|
342
|
-
async _sendCommit(keyHashHex, cid, expectedVersion) {
|
|
477
|
+
async _sendCommit(keyHashHex, cid, expectedVersion, ownerAddr = null) {
|
|
343
478
|
// The enclave never pays gas and does NO gas math: it SIGNS the commit
|
|
344
479
|
// (commitFor) and stages the signed authorization for the node, which
|
|
345
480
|
// relays it and pays. The trustedzone independently re-prices the whole
|
|
@@ -362,7 +497,15 @@ class StateRegistry {
|
|
|
362
497
|
cid,
|
|
363
498
|
expectedVersion: Number(expectedVersion),
|
|
364
499
|
relayNonce: Number(relayNonce),
|
|
365
|
-
signature
|
|
500
|
+
signature,
|
|
501
|
+
// IMPERSONATION GUARD: the trustedzone requires callerUsed to equal the
|
|
502
|
+
// DO owner it reads from the PoX contract, so a payload that forged the
|
|
503
|
+
// in-enclave caller to impersonate another user is rejected outside the
|
|
504
|
+
// payload's process. It does NOT verify the caller was a permitted
|
|
505
|
+
// writer (it cannot read the encrypted ACL). Unowned commits carry no
|
|
506
|
+
// constraint.
|
|
507
|
+
callerUsed: _taskCaller || '',
|
|
508
|
+
owned: Boolean(ownerAddr)
|
|
366
509
|
};
|
|
367
510
|
|
|
368
511
|
_esrLedger.push(auth);
|
|
@@ -376,10 +519,100 @@ class StateRegistry {
|
|
|
376
519
|
}
|
|
377
520
|
}
|
|
378
521
|
|
|
522
|
+
/** Owner-only ACL mutation; an unowned key is claimed first (owner = caller). */
|
|
523
|
+
async function ownerOp(key, mutator) {
|
|
524
|
+
if (!_taskCaller) {
|
|
525
|
+
throw new StatePermissionError(
|
|
526
|
+
'state management requires an authenticated caller (the trustedzone supplied none)'
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const reg = new StateRegistry();
|
|
530
|
+
const transform = async (acl, data) => {
|
|
531
|
+
let nextAcl = acl || newAcl(_taskCaller);
|
|
532
|
+
if (normAddr(nextAcl.owner) !== _taskCaller) {
|
|
533
|
+
throw new StatePermissionError(
|
|
534
|
+
`caller ${_taskCaller} is not the owner of state key '${key}' (owner: ${nextAcl.owner})`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
nextAcl = mutator({ ...nextAcl });
|
|
538
|
+
return [nextAcl, data];
|
|
539
|
+
};
|
|
540
|
+
const [acl] = await reg._commitTransform(key, transform);
|
|
541
|
+
return { ...acl };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function assertLevel(level) {
|
|
545
|
+
if (level !== 'read' && level !== 'write') throw new Error("level must be 'read' or 'write'");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function esrGrant(key, address, level = 'read') {
|
|
549
|
+
assertLevel(level);
|
|
550
|
+
const addr = normAddr(address);
|
|
551
|
+
if (!addr) throw new Error('a grantee address is required');
|
|
552
|
+
return ownerOp(key, (acl) => {
|
|
553
|
+
const members = new Set((acl[level] || []).map(normAddr).filter(Boolean));
|
|
554
|
+
members.add(addr);
|
|
555
|
+
acl[level] = [...members].sort();
|
|
556
|
+
return acl;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function esrRevoke(key, address, level = 'read') {
|
|
561
|
+
assertLevel(level);
|
|
562
|
+
const addr = normAddr(address);
|
|
563
|
+
return ownerOp(key, (acl) => {
|
|
564
|
+
acl[level] = (acl[level] || []).map(normAddr).filter((a) => a && a !== addr).sort();
|
|
565
|
+
return acl;
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function esrSetPublicRead(key, enabled = true) {
|
|
570
|
+
return ownerOp(key, (acl) => {
|
|
571
|
+
acl.public_read = Boolean(enabled);
|
|
572
|
+
return acl;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function esrTransfer(key, newOwner) {
|
|
577
|
+
const addr = normAddr(newOwner);
|
|
578
|
+
if (!addr) throw new Error('a new owner address is required');
|
|
579
|
+
return ownerOp(key, (acl) => {
|
|
580
|
+
acl.owner = addr;
|
|
581
|
+
return acl;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Owner address of `key`, or null while unowned. Exposes nothing else. */
|
|
586
|
+
async function esrOwner(key) {
|
|
587
|
+
const [acl] = await new StateRegistry()._readContainer(key);
|
|
588
|
+
return acl ? acl.owner : null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Owner-only: the full ACL of `key`. */
|
|
592
|
+
async function esrAcl(key) {
|
|
593
|
+
const [acl] = await new StateRegistry()._readContainer(key);
|
|
594
|
+
if (!acl) return null;
|
|
595
|
+
if (!_taskCaller || normAddr(acl.owner) !== _taskCaller) {
|
|
596
|
+
throw new StatePermissionError(
|
|
597
|
+
`caller ${_taskCaller || '<anonymous>'} is not the owner of state key '${key}'`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return { ...acl };
|
|
601
|
+
}
|
|
602
|
+
|
|
379
603
|
module.exports = {
|
|
380
604
|
StateRegistry,
|
|
381
605
|
configure,
|
|
382
606
|
cidv1Raw,
|
|
383
607
|
looksLikeCID,
|
|
384
|
-
ledgerSnapshot
|
|
608
|
+
ledgerSnapshot,
|
|
609
|
+
taskCaller,
|
|
610
|
+
restampLedgerCaller,
|
|
611
|
+
StatePermissionError,
|
|
612
|
+
esrGrant,
|
|
613
|
+
esrRevoke,
|
|
614
|
+
esrSetPublicRead,
|
|
615
|
+
esrTransfer,
|
|
616
|
+
esrOwner,
|
|
617
|
+
esrAcl
|
|
385
618
|
};
|