@ethernity-cloud/sdk-js 1.6.1 → 1.7.0

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.
Files changed (52) hide show
  1. package/README.md +44 -6
  2. package/cas/config.js +61 -3
  3. package/info.js +333 -324
  4. package/init.js +20 -4
  5. package/nodenithy/build/securelock/Dockerfile.tmpl +34 -32
  6. package/nodenithy/build/securelock/scripts/binary-fs-build.sh +7 -6
  7. package/nodenithy/build/securelock/src/securelock.js.tmpl +5 -2
  8. package/nodenithy/build.mjs +83 -71
  9. package/nodenithy/run/docker-compose-final.yml.tmpl +36 -31
  10. package/nodenithy/run/docker-compose.yml.tmpl +34 -45
  11. package/nodenithy/run/image_registry.js +396 -380
  12. package/nodenithy/run.js +42 -28
  13. package/package.json +5 -3
  14. package/publish.js +4 -2
  15. package/pynithy/build/securelock/Dockerfile.base.tpl +13 -0
  16. package/pynithy/build/securelock/Dockerfile.tpl +131 -0
  17. package/pynithy/build/securelock/scripts/binary-fs-build.sh +25 -34
  18. package/pynithy/build/securelock/scripts/pyfreeze.sh +32 -0
  19. package/pynithy/build/securelock/src/{pox.abi → abi/etnyImplementationV2.abi} +1489 -1489
  20. package/pynithy/build/securelock/src/abi/image_registry.abi +1 -0
  21. package/pynithy/build/securelock/src/ecld_state.py +906 -0
  22. package/pynithy/build/securelock/src/esr.abi +1 -0
  23. package/pynithy/build/securelock/src/esr_local.py +267 -0
  24. package/pynithy/build/securelock/src/esr_wallet.py +150 -0
  25. package/pynithy/build/securelock/src/etny_crypto.py +208 -167
  26. package/pynithy/build/securelock/src/etny_exec.py +385 -79
  27. package/pynithy/build/securelock/src/etny_exec_flask.py +535 -0
  28. package/pynithy/build/securelock/src/etny_exec_serv.py +353 -0
  29. package/pynithy/build/securelock/src/etny_session_exec.py +245 -0
  30. package/pynithy/build/securelock/src/get_sgx_report.so +0 -0
  31. package/pynithy/build/securelock/src/image_registry.abi +1 -1
  32. package/pynithy/build/securelock/src/key_generation.py +17 -17
  33. package/pynithy/build/securelock/src/local_api.py +189 -0
  34. package/pynithy/build/securelock/src/models.py +200 -176
  35. package/pynithy/build/securelock/src/securelock.py.tmpl +1037 -727
  36. package/pynithy/build/securelock/src/swift_stream_service.py +206 -186
  37. package/pynithy/build.mjs +142 -92
  38. package/pynithy/run/docker-compose-final.yml.tmpl +58 -51
  39. package/pynithy/run/docker-compose.yml.tmpl +62 -48
  40. package/pynithy/run/etny-securelock-test.yaml.tpl +4 -1
  41. package/pynithy/run/image_registry.js +396 -380
  42. package/pynithy/run.js +44 -28
  43. package/pynithy/src/serverless/Dockerfile.serverless +8 -0
  44. package/pynithy/src/serverless/requirements.txt +6 -0
  45. package/run.js +69 -28
  46. package/nodenithy/build/las/Dockerfile +0 -5
  47. package/pynithy/build/docker-compose.yml +0 -70
  48. package/pynithy/build/las/Dockerfile +0 -11
  49. package/pynithy/build/las/src/etny_entrypoint.sh +0 -6
  50. package/pynithy/build/las/src/las_entrypoint.sh +0 -26
  51. package/pynithy/build/scripts/binary-fs-build.sh +0 -28
  52. package/pynithy/build/securelock/Dockerfile.tmpl +0 -95
@@ -0,0 +1 @@
1
+ [{"inputs": [], "name": "BadSignature", "type": "error"}, {"inputs": [], "name": "EmptyCID", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "stored", "type": "uint256"}, {"internalType": "uint256", "name": "given", "type": "uint256"}], "name": "NonceOutOfOrder", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "expected", "type": "uint256"}, {"internalType": "uint256", "name": "actual", "type": "uint256"}], "name": "RelayNonceMismatch", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "expected", "type": "uint256"}, {"internalType": "uint256", "name": "actual", "type": "uint256"}], "name": "VersionMismatch", "type": "error"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "enclave", "type": "address"}, {"indexed": true, "internalType": "bytes32", "name": "key", "type": "bytes32"}, {"indexed": false, "internalType": "string", "name": "cid", "type": "string"}, {"indexed": false, "internalType": "uint256", "name": "version", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "seq", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "nonce", "type": "uint256"}], "name": "StateCommitted", "type": "event"}, {"inputs": [{"internalType": "bytes32", "name": "key", "type": "bytes32"}, {"internalType": "string", "name": "newCID", "type": "string"}, {"internalType": "uint256", "name": "expectedVersion", "type": "uint256"}, {"internalType": "uint256", "name": "nonce", "type": "uint256"}], "name": "commit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}, {"internalType": "string", "name": "newCID", "type": "string"}, {"internalType": "uint256", "name": "expectedVersion", "type": "uint256"}, {"internalType": "uint256", "name": "relayNonce", "type": "uint256"}, {"internalType": "uint256", "name": "nonce", "type": "uint256"}], "name": "commitDigest", "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}, {"internalType": "string", "name": "newCID", "type": "string"}, {"internalType": "uint256", "name": "expectedVersion", "type": "uint256"}, {"internalType": "uint256", "name": "relayNonce", "type": "uint256"}, {"internalType": "uint256", "name": "nonce", "type": "uint256"}, {"internalType": "bytes", "name": "signature", "type": "bytes"}], "name": "commitFor", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "commitSeq", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "index", "type": "uint256"}], "name": "entryAt", "outputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "entryCount", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "exists", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "startIndex", "type": "uint256"}, {"internalType": "uint256", "name": "limit", "type": "uint256"}], "name": "getEntriesFrom", "outputs": [{"internalType": "address[]", "name": "enclaves", "type": "address[]"}, {"internalType": "bytes32[]", "name": "keys", "type": "bytes32[]"}, {"internalType": "string[]", "name": "cids", "type": "string[]"}, {"internalType": "uint256[]", "name": "versions", "type": "uint256[]"}, {"internalType": "uint64[]", "name": "updatedAts", "type": "uint64[]"}, {"internalType": "uint256", "name": "total", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "getNonce", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "getState", "outputs": [{"internalType": "string", "name": "cid", "type": "string"}, {"internalType": "uint256", "name": "version", "type": "uint256"}, {"internalType": "uint64", "name": "updatedAt", "type": "uint64"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "getVersion", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "relayNonce", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}]
@@ -0,0 +1,267 @@
1
+ """Local ESR emulator for `ecld-test` -- run state-using backends with no chain.
2
+
3
+ Provides in-memory stand-ins for the two external dependencies StateRegistry
4
+ talks to, so `get`/`commit`/`esr_grant`/... behave EXACTLY as on-chain but
5
+ in-process and instantly, with zero orders and zero gas:
6
+
7
+ * MemSwift -- the state-blob store (replaces the SwiftStream service):
8
+ put_file_content / get_file_content_bytes over a dict,
9
+ optionally mirrored to a JSON file so state survives across
10
+ `ecld-test` runs (the counter keeps counting).
11
+ * MemRegistry -- the ESR contract (getState / getVersion / relayNonce /
12
+ commitDigest, and the commit APPLY that the node would do
13
+ on-chain), keyed by (enclave, keccak(key)) -> (cid, version).
14
+
15
+ `install(ecld_state, caller=..., file=...)` wires these into the ecld_state
16
+ module and sets the local commit-apply hook, so no chain, node, or SGX is
17
+ needed. Used only by the local test API; the real enclave never imports this.
18
+ """
19
+
20
+ import base64
21
+ import json
22
+ import os
23
+
24
+
25
+ # ---- storage ---------------------------------------------------------------
26
+
27
+ class MemSwift:
28
+ """SwiftStream stand-in: (bucket, object) -> bytes, over a dict.
29
+
30
+ Mirrors to `file` (base64 JSON) when given, so committed state persists
31
+ across separate `ecld-test` invocations.
32
+ """
33
+
34
+ def __init__(self, file=None):
35
+ self._file = file
36
+ self._store = {}
37
+ if file and os.path.exists(file):
38
+ try:
39
+ with open(file, encoding="utf-8") as f:
40
+ raw = json.load(f)
41
+ self._store = {k: base64.b64decode(v) for k, v in raw.items()}
42
+ except Exception:
43
+ self._store = {}
44
+
45
+ def _persist(self):
46
+ if not self._file:
47
+ return
48
+ try:
49
+ enc = {k: base64.b64encode(v).decode("ascii") for k, v in self._store.items()}
50
+ tmp = self._file + ".tmp"
51
+ with open(tmp, "w", encoding="utf-8") as f:
52
+ json.dump(enc, f)
53
+ os.replace(tmp, self._file)
54
+ except Exception:
55
+ pass
56
+
57
+ def put_file_content(self, bucket, object_name, _prefix, io_obj):
58
+ data = io_obj.getvalue() if hasattr(io_obj, "getvalue") else bytes(io_obj)
59
+ self._store["%s/%s" % (bucket, object_name)] = data
60
+ self._persist()
61
+ return True, None
62
+
63
+ def get_file_content_bytes(self, bucket, object_name):
64
+ data = self._store.get("%s/%s" % (bucket, object_name))
65
+ if data is None:
66
+ return False, None
67
+ return True, data
68
+
69
+ # Some code paths use the string variant; provide it too.
70
+ def get_file_content(self, bucket, object_name):
71
+ ok, data = self.get_file_content_bytes(bucket, object_name)
72
+ return (ok, data.decode("utf-8") if ok else None)
73
+
74
+ def create_bucket(self, *a, **k):
75
+ return True
76
+
77
+
78
+ # ---- registry --------------------------------------------------------------
79
+
80
+ class _Callable:
81
+ """A web3-style bound function: .call() returns the precomputed value."""
82
+
83
+ def __init__(self, value):
84
+ self._value = value
85
+
86
+ def call(self, *a, **k):
87
+ return self._value
88
+
89
+
90
+ class _Functions:
91
+ def __init__(self, reg):
92
+ self._reg = reg
93
+
94
+ def getState(self, enclave, key_hash):
95
+ cid, version, updated = self._reg.state(enclave, key_hash)
96
+ return _Callable([cid, version, updated])
97
+
98
+ def getVersion(self, enclave, key_hash):
99
+ _cid, version, _u = self._reg.state(enclave, key_hash)
100
+ return _Callable(version)
101
+
102
+ def relayNonce(self, enclave, key_hash):
103
+ return _Callable(self._reg.relay_nonce(enclave, key_hash))
104
+
105
+ def commitDigest(self, enclave, key_hash, cid, expected_version, relay_nonce,
106
+ nonce=0):
107
+ # Deterministic 32-byte digest over the fields (no real signing needed
108
+ # locally; the securelock signs it with the local test key). The
109
+ # idempotency nonce is part of the digest, like on-chain.
110
+ import hashlib
111
+ m = hashlib.sha256()
112
+ m.update(str((enclave, key_hash, cid, expected_version, relay_nonce,
113
+ nonce)).encode())
114
+ return _Callable(m.digest())
115
+
116
+ def getNonce(self, enclave, key_hash):
117
+ return _Callable(self._reg.idem_nonce(enclave, key_hash))
118
+
119
+ def entryCount(self):
120
+ return _Callable(self._reg.entry_count())
121
+
122
+
123
+ class _Contract:
124
+ def __init__(self, reg):
125
+ self.functions = _Functions(reg)
126
+
127
+
128
+ class MemRegistry:
129
+ """In-memory ESR registry: (enclave, keyHash) -> (cid, version, updatedAt).
130
+
131
+ Applies commits directly (the job the node's commitFor does on-chain),
132
+ enforcing monotonic versions with optimistic concurrency.
133
+ """
134
+
135
+ def __init__(self, file=None):
136
+ self._file = file
137
+ # key: "enclave|0xkeyhash" -> {"cid":..., "version":int, "updatedAt":int}
138
+ self._entries = {}
139
+ self._nonce = {}
140
+ if file and os.path.exists(file):
141
+ try:
142
+ with open(file, encoding="utf-8") as f:
143
+ d = json.load(f)
144
+ self._entries = d.get("entries", {})
145
+ self._nonce = d.get("nonce", {})
146
+ except Exception:
147
+ pass
148
+
149
+ def _persist(self):
150
+ if not self._file:
151
+ return
152
+ try:
153
+ tmp = self._file + ".tmp"
154
+ with open(tmp, "w", encoding="utf-8") as f:
155
+ json.dump({"entries": self._entries, "nonce": self._nonce}, f)
156
+ os.replace(tmp, self._file)
157
+ except Exception:
158
+ pass
159
+
160
+ @staticmethod
161
+ def _k(enclave, key_hash):
162
+ kh = key_hash.hex() if isinstance(key_hash, (bytes, bytearray)) else str(key_hash)
163
+ return "%s|%s" % (str(enclave).lower(), kh)
164
+
165
+ def state(self, enclave, key_hash):
166
+ e = self._entries.get(self._k(enclave, key_hash))
167
+ if not e:
168
+ return ("", 0, 0)
169
+ return (e["cid"], int(e["version"]), int(e.get("updatedAt", 0)))
170
+
171
+ def idem_nonce(self, enclave, key_hash):
172
+ """Last accepted idempotency nonce for (enclave, key) -- public data,
173
+ mirroring the on-chain getNonce view."""
174
+ e = self._entries.get(self._k(enclave, key_hash))
175
+ return int(e.get("nonce", 0)) if e else 0
176
+
177
+ def relay_nonce(self, enclave, key_hash):
178
+ # Per (enclave, key), like the contract: same-key commits serialize,
179
+ # different keys are independent.
180
+ return int(self._nonce.get(self._k(enclave, key_hash), 0))
181
+
182
+ def entry_count(self):
183
+ return len(self._entries)
184
+
185
+ def apply_commit(self, enclave, key_hash, cid, expected_version, nonce=0):
186
+ """The on-chain commitFor: bump version if expected matches current.
187
+ Raises 'VersionMismatch' (string in message) on a race, exactly like
188
+ the contract, so StateRegistry.commit's retry loop behaves the same.
189
+
190
+ The per-key nonce advances by EXACTLY 1 on every commit, like the
191
+ contract: nonce == 0 ("omitted") gets the next value assigned by the
192
+ registry itself; a non-zero nonce is a client-pinned guard and must
193
+ be EXACTLY the stored value + 1 (no gaps, no reuse) or
194
+ 'NonceOutOfOrder' is raised."""
195
+ k = self._k(enclave, key_hash)
196
+ entry = self._entries.get(k, {})
197
+ cur = int(entry.get("version", 0))
198
+ if int(expected_version) != cur:
199
+ raise RuntimeError("VersionMismatch: expected %s but current is %s"
200
+ % (expected_version, cur))
201
+ stored_nonce = int(entry.get("nonce", 0))
202
+ n = int(nonce or 0)
203
+ if n == 0:
204
+ stored_nonce += 1
205
+ else:
206
+ if n != stored_nonce + 1:
207
+ raise RuntimeError("NonceOutOfOrder: stored %s, given %s"
208
+ % (stored_nonce, n))
209
+ stored_nonce = n
210
+ self._entries[k] = {"cid": cid, "version": cur + 1, "updatedAt": 0,
211
+ "nonce": stored_nonce}
212
+ self._nonce[k] = self.relay_nonce(enclave, key_hash) + 1
213
+ self._persist()
214
+
215
+
216
+ # ---- web3 stand-in ---------------------------------------------------------
217
+
218
+ class MemWeb3:
219
+ """Just enough of a web3 to satisfy ecld_state._contract()."""
220
+
221
+ def __init__(self, reg):
222
+ self._reg = reg
223
+
224
+ class _Eth:
225
+ def contract(_self, address=None, abi=None):
226
+ return _Contract(reg)
227
+ self.eth = _Eth()
228
+
229
+ @staticmethod
230
+ def to_checksum_address(addr):
231
+ return addr
232
+
233
+
234
+ # ---- installer -------------------------------------------------------------
235
+
236
+ def install(ecld_state, caller=None, identity_priv=None, bucket="ecld-local",
237
+ contract_address="0xLOCAL", file_prefix=".ecld-esr-local"):
238
+ """Wire the emulator into the ecld_state module for local testing.
239
+
240
+ After this, StateRegistry() works fully in-process. `file_prefix` gives two
241
+ small JSON files (state blobs + registry) so state persists across runs;
242
+ pass None to keep everything in-memory only.
243
+ """
244
+ swift_file = (file_prefix + ".swift.json") if file_prefix else None
245
+ reg_file = (file_prefix + ".registry.json") if file_prefix else None
246
+ swift = MemSwift(file=swift_file)
247
+ reg = MemRegistry(file=reg_file)
248
+ web3 = MemWeb3(reg)
249
+
250
+ if identity_priv is None:
251
+ # A fixed, well-known LOCAL test identity -- stable wallet_address across
252
+ # runs. NOT a real key; local testing only.
253
+ identity_priv = b"ecld-local-test-identity-key-0001"
254
+
255
+ ecld_state.configure(
256
+ identity_priv=identity_priv,
257
+ swift_stream_service=swift,
258
+ bucket=bucket,
259
+ contract_address=contract_address,
260
+ web3=web3,
261
+ caller=caller,
262
+ )
263
+ # The node applies commitFor on-chain in real mode; locally, apply it here.
264
+ ecld_state.set_local_commit_apply(
265
+ lambda enclave, key_hash, cid, expected_version, nonce=0:
266
+ reg.apply_commit(enclave, key_hash, cid, expected_version, nonce))
267
+ return {"swift": swift, "registry": reg, "web3": web3}
@@ -0,0 +1,150 @@
1
+ """ESR identity wallet derivation (ESR RFC §5.1).
2
+
3
+ The enclave's Ethereum wallet is derived from the enclave's IDENTITY PRIVATE
4
+ KEY (the cert key), never from MR_ENCLAVE:
5
+
6
+ eth_priv = keccak256(DOMAIN_SEP || identity_priv_der) # secp256k1 scalar
7
+ eth_address = address(secp256k1(eth_priv))
8
+
9
+ Why derive instead of reusing the cert key: the cert is secp384r1 (P-384) and
10
+ Ethereum signing requires secp256k1, so a P-384 key can never sign a
11
+ transaction. Deriving from the identity key means the wallet inherits the
12
+ identity's security properties automatically:
13
+
14
+ * mainnet — identity key is the CAS-provisioned secret, so the wallet is
15
+ enclave-only and stable across every node running this MRENCLAVE.
16
+ * testnet — identity key is derived from MR_SIGNER/MR_ENCLAVE, both PUBLIC,
17
+ so the wallet is public too. Functional testing only; is_secret_identity()
18
+ reports False and callers must warn loudly (RFC §7).
19
+
20
+ DOMAIN_SEP keeps this key from colliding with any other use of the identity
21
+ key; a second separator derives the state-encryption key in a later phase.
22
+
23
+ SECURITY: nothing here may serialize, log or return the private key. The only
24
+ value that ever leaves the enclave is the ADDRESS (RFC §5.2) — that is what
25
+ makes the wallet fundable and readable without weakening secrecy.
26
+ """
27
+
28
+ import hashlib
29
+
30
+ # The wallet PRIVATE key derivation is deliberately not part of the public
31
+ # surface: the client payload is exec'd inside this image and can import any
32
+ # module here, so an exported key-derivation helper would hand untrusted code a
33
+ # ready-made way to materialize the ESR private key. Only the ADDRESS leaves
34
+ # this module (RFC §5.2); _derive_wallet_private_key stays an implementation
35
+ # detail of derive_wallet_address.
36
+ __all__ = [
37
+ "DOMAIN_SEP",
38
+ "derive_wallet_address",
39
+ "is_secret_identity",
40
+ "INSECURE_IDENTITY_WARNING",
41
+ ]
42
+
43
+ DOMAIN_SEP = b"ethernity-cloud/esr-wallet/v1"
44
+
45
+ # secp256k1 group order; a valid private scalar is in [1, N-1].
46
+ _SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
47
+
48
+
49
+ def _keccak256(data: bytes) -> bytes:
50
+ """Keccak-256 (Ethereum's hash), not SHA3-256."""
51
+ try:
52
+ from Crypto.Hash import keccak # pycryptodome
53
+
54
+ h = keccak.new(digest_bits=256)
55
+ h.update(data)
56
+ return h.digest()
57
+ except ImportError:
58
+ from eth_hash.auto import keccak as eth_keccak # web3 dependency
59
+
60
+ return eth_keccak(data)
61
+
62
+
63
+ def _derive_wallet_private_key(identity_priv_der: bytes) -> bytes:
64
+ """32-byte secp256k1 private key derived from the enclave identity key.
65
+
66
+ Kept private to this module's callers; never log or transmit the result.
67
+ """
68
+ if not identity_priv_der:
69
+ raise ValueError("empty enclave identity key; cannot derive ESR wallet")
70
+ digest = _keccak256(DOMAIN_SEP + identity_priv_der)
71
+ # Astronomically unlikely, but a scalar of 0 or >= N is invalid: re-hash
72
+ # until valid so derivation is total rather than probabilistically broken.
73
+ while True:
74
+ scalar = int.from_bytes(digest, "big")
75
+ if 0 < scalar < _SECP256K1_N:
76
+ return digest
77
+ digest = _keccak256(DOMAIN_SEP + digest)
78
+
79
+
80
+ def derive_wallet_address(identity_priv_der: bytes) -> str:
81
+ """Checksummed 0x address of the enclave's ESR wallet (safe to publish)."""
82
+ priv = _derive_wallet_private_key(identity_priv_der)
83
+ try:
84
+ from eth_keys import keys
85
+
86
+ return keys.PrivateKey(priv).public_key.to_checksum_address()
87
+ except ImportError:
88
+ # eth_account ships with the runner dependency chain and is always
89
+ # present in the enclave image; use it when eth_keys is unavailable.
90
+ from eth_account import Account
91
+
92
+ return Account.from_key(priv).address
93
+
94
+
95
+ def is_secret_identity(network_type: str, attested: bool = False) -> bool:
96
+ """True only when the identity key is genuinely unpredictable to outsiders.
97
+
98
+ `attested` is whether this build is provisioned by a CAS (its key comes
99
+ from the CAS session): true on mainnet and on a testnet whose enclaves are
100
+ attested by the ethernity-cas validator set.
101
+
102
+ This is NOT "is the enclave genuine" — SGX protects enclave memory on every
103
+ network. It is the narrower question the ESR wallet depends on: could
104
+ someone outside the enclave reproduce the private key?
105
+
106
+ The distinction is ATTESTATION, not SGX. Both networks run the identical
107
+ in-enclave generator (get_sgx_report.c) on real SGX hardware, so at runtime
108
+ a node operator cannot read the key out of enclave memory on either.
109
+
110
+ * mainnet — CAS attests the enclave (DCAP quote verified) before the
111
+ identity is trusted, so a key is only accepted from a genuine, measured
112
+ enclave. The private key is effectively enclave-only: reproducing the
113
+ derivation outside a real attested enclave yields a key nothing accepts.
114
+ Intended posture; needs nothing from ESR.
115
+ * testnet — NO attestation. The enclave still runs on SGX and still
116
+ generates the keypair with the same algorithm, but nothing verifies the
117
+ MRENCLAVE, and the derivation input (MR_ENCLAVE) is public. So anyone
118
+ who rebuilds the image can reproduce the keypair. This is a deliberate
119
+ testnet tradeoff (real enclave execution for functional testing without
120
+ the CAS dependency) — not a bug. Treat testnet ESR wallets as
121
+ disposable; do not fund them with real value.
122
+
123
+ What would make the testnet key genuinely exclusive is a secret INPUT that
124
+ never leaves the CPU (EGETKEY / sealing key) OR requiring attestation on
125
+ testnet too. Encrypting or obfuscating the generator does NOT achieve this:
126
+ without attestation, an attacker doesn't reverse the code — they run the
127
+ published image (it is on IPFS, MRENCLAVE on-chain) and observe the key.
128
+ """
129
+ import os
130
+
131
+ override = os.getenv("ESR_IDENTITY_SECRET", "").strip().lower()
132
+ if override in ("1", "true", "yes"):
133
+ return True
134
+ if override in ("0", "false", "no"):
135
+ return False
136
+ # Secrecy tracks attestation, not the network name: mainnet is attested,
137
+ # and so is a testnet whose securelock is provisioned by a CAS.
138
+ return attested or str(network_type).strip().lower() == "mainnet"
139
+
140
+
141
+ INSECURE_IDENTITY_WARNING = (
142
+ "[TESTNET-INSECURE] This network runs the enclave on SGX but WITHOUT "
143
+ "attestation, so the ESR wallet's keypair — though generated inside the "
144
+ "enclave — is reproducible by anyone who rebuilds the image (its derivation "
145
+ "input, MR_ENCLAVE, is public and nothing verifies the enclave). This is a "
146
+ "deliberate testnet tradeoff, not a defect. Treat the wallet as disposable "
147
+ "and do not fund it with real value. (An attested testnet build can set "
148
+ "ESR_IDENTITY_SECRET=1 to "
149
+ "suppress this warning.)"
150
+ )