@ethernity-cloud/sdk-js 1.6.2 → 1.7.1
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/README.md +86 -48
- package/cas/config.js +61 -3
- package/info.js +14 -5
- package/init.js +24 -6
- package/network.js +51 -0
- package/nodenithy/build/securelock/Dockerfile.tmpl +34 -32
- package/nodenithy/build/securelock/scripts/binary-fs-build.sh +7 -6
- package/nodenithy/build.mjs +80 -68
- package/nodenithy/run/docker-compose-final.yml.tmpl +36 -31
- package/nodenithy/run/docker-compose.yml.tmpl +34 -45
- package/nodenithy/run/image_registry.js +17 -4
- package/nodenithy/run.js +42 -28
- package/nodenithy/src/ec_helloworld_example.js +46 -42
- package/nodenithy/src/preStart.mjs +41 -51
- package/package.json +6 -3
- package/publish.js +4 -2
- package/pynithy/build/securelock/Dockerfile.base.tpl +13 -0
- package/pynithy/build/securelock/Dockerfile.tpl +131 -0
- package/pynithy/build/securelock/scripts/binary-fs-build.sh +25 -34
- package/pynithy/build/securelock/scripts/pyfreeze.sh +32 -0
- package/pynithy/build/securelock/src/{pox.abi → abi/etnyImplementationV2.abi} +1489 -1489
- package/pynithy/build/securelock/src/abi/image_registry.abi +1 -0
- package/pynithy/build/securelock/src/ecld_state.py +906 -0
- package/pynithy/build/securelock/src/esr.abi +1 -0
- package/pynithy/build/securelock/src/esr_local.py +267 -0
- package/pynithy/build/securelock/src/esr_wallet.py +150 -0
- package/pynithy/build/securelock/src/etny_crypto.py +208 -167
- package/pynithy/build/securelock/src/etny_exec.py +385 -79
- package/pynithy/build/securelock/src/etny_exec_flask.py +535 -0
- package/pynithy/build/securelock/src/etny_exec_serv.py +353 -0
- package/pynithy/build/securelock/src/etny_session_exec.py +245 -0
- package/pynithy/build/securelock/src/get_sgx_report.so +0 -0
- package/pynithy/build/securelock/src/image_registry.abi +1 -1
- package/pynithy/build/securelock/src/key_generation.py +17 -17
- package/pynithy/build/securelock/src/local_api.py +189 -0
- package/pynithy/build/securelock/src/models.py +200 -176
- package/pynithy/build/securelock/src/securelock.py.tmpl +1037 -731
- package/pynithy/build/securelock/src/swift_stream_service.py +206 -186
- package/pynithy/build.mjs +139 -89
- package/pynithy/run/docker-compose-final.yml.tmpl +58 -51
- package/pynithy/run/docker-compose.yml.tmpl +62 -48
- package/pynithy/run/etny-securelock-test.yaml.tpl +4 -1
- package/pynithy/run/image_registry.js +17 -4
- package/pynithy/run.js +44 -28
- package/pynithy/src/ec_helloworld_example.js +46 -42
- package/pynithy/src/preStart.mjs +41 -51
- package/pynithy/src/serverless/Dockerfile.serverless +8 -0
- package/pynithy/src/serverless/requirements.txt +6 -0
- package/run.js +37 -34
- package/nodenithy/build/las/Dockerfile +0 -5
- package/nodenithy/src/preStart.js +0 -45
- package/pynithy/build/docker-compose.yml +0 -70
- package/pynithy/build/las/Dockerfile +0 -11
- package/pynithy/build/las/src/etny_entrypoint.sh +0 -6
- package/pynithy/build/las/src/las_entrypoint.sh +0 -26
- package/pynithy/build/scripts/binary-fs-build.sh +0 -28
- package/pynithy/build/securelock/Dockerfile.tmpl +0 -95
|
@@ -1,79 +1,385 @@
|
|
|
1
|
-
import os.path
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
1
|
+
import os.path
|
|
2
|
+
import ast
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
7
|
+
# If the serverless backend fails to import, remember WHY. Previously any
|
|
8
|
+
# import failure was silently swallowed (backend = None), so every task then
|
|
9
|
+
# died later with a misleading "name 'X' is not defined" — a missing module in
|
|
10
|
+
# requirements.txt or a top-level-style import inside backend.py (the enclave
|
|
11
|
+
# loads it as the package `serverless.backend`) took many builds to diagnose.
|
|
12
|
+
# Only a genuinely absent backend (no serverless module at all) stays silent:
|
|
13
|
+
# that is the stock-enclave configuration, where payloads are self-contained.
|
|
14
|
+
_backend_import_error = None
|
|
15
|
+
try:
|
|
16
|
+
import serverless.backend as backend
|
|
17
|
+
except ModuleNotFoundError as e:
|
|
18
|
+
backend = None
|
|
19
|
+
if e.name not in ("serverless", "serverless.backend"):
|
|
20
|
+
_backend_import_error = f"{type(e).__name__}: {e}"
|
|
21
|
+
except BaseException as e: # SyntaxError, ValueError at import time, etc.
|
|
22
|
+
backend = None
|
|
23
|
+
_backend_import_error = f"{type(e).__name__}: {e}"
|
|
24
|
+
|
|
25
|
+
sdkFunctions = {}
|
|
26
|
+
if backend is not None:
|
|
27
|
+
for func in backend.__dict__.keys():
|
|
28
|
+
if func not in backend.__builtins__.keys() and func not in [
|
|
29
|
+
"__file__",
|
|
30
|
+
"__cached__",
|
|
31
|
+
"__builtins__",
|
|
32
|
+
]:
|
|
33
|
+
sdkFunctions.update({func: backend.__dict__[func]})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _encode_result_data(data):
|
|
37
|
+
"""Automatic encoding for task results (single code path for all results).
|
|
38
|
+
|
|
39
|
+
Returns (type, encoded): JSON-serializable values ride as JSON, strings as
|
|
40
|
+
text, bytes-like as base64. Anything else is JSON-dumped with a stringify
|
|
41
|
+
fallback -- a task result never fails on encoding.
|
|
42
|
+
"""
|
|
43
|
+
import base64 as _b64
|
|
44
|
+
if isinstance(data, (bytes, bytearray, memoryview)):
|
|
45
|
+
return "base64", _b64.b64encode(bytes(data)).decode("ascii")
|
|
46
|
+
if isinstance(data, str):
|
|
47
|
+
return "text", data
|
|
48
|
+
try:
|
|
49
|
+
json.dumps(data)
|
|
50
|
+
return "json", data
|
|
51
|
+
except (TypeError, ValueError):
|
|
52
|
+
try:
|
|
53
|
+
return "json", json.loads(json.dumps(data, default=str))
|
|
54
|
+
except Exception:
|
|
55
|
+
return "text", str(data)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ecld_result(data=None, *, state=True, keys=None):
|
|
59
|
+
"""Return a task result (THE way to end a task).
|
|
60
|
+
|
|
61
|
+
Builds the structured result envelope and terminates execution with it:
|
|
62
|
+
|
|
63
|
+
{"ecld": 1, "type": "json"|"text"|"base64", "data": ..., "esr": ...}
|
|
64
|
+
|
|
65
|
+
- `data` is encoded automatically (see _encode_result_data).
|
|
66
|
+
- `state=True` (default) attaches the ESR state of every key this task
|
|
67
|
+
touched; `state="meta"` attaches {key, version, cid} without the state
|
|
68
|
+
payload; `state=False` attaches nothing.
|
|
69
|
+
- `keys=[...]` restricts the attachment to those keys, force-reading any
|
|
70
|
+
the task did not touch.
|
|
71
|
+
|
|
72
|
+
ESR attachment is best-effort: an ESR-disabled build (or any attachment
|
|
73
|
+
error) yields `esr: null`, never a failed task.
|
|
74
|
+
"""
|
|
75
|
+
rtype, rdata = _encode_result_data(data)
|
|
76
|
+
envelope = {"ecld": 1, "type": rtype, "data": rdata, "esr": None}
|
|
77
|
+
if state or keys:
|
|
78
|
+
try:
|
|
79
|
+
import ecld_state
|
|
80
|
+
envelope["esr"] = ecld_state.ledger_snapshot(
|
|
81
|
+
include_state=(state is True), keys=keys)
|
|
82
|
+
except Exception:
|
|
83
|
+
envelope["esr"] = None
|
|
84
|
+
quit([0, json.dumps(envelope, default=str)])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ___etny_result___(data):
|
|
88
|
+
"""Legacy alias: same builder, ESR attachment off -- today's behavior."""
|
|
89
|
+
return ecld_result(data, state=False)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def esr_fetch(*keys):
|
|
93
|
+
"""Standard state-fetch task body: no computation, just attach state.
|
|
94
|
+
|
|
95
|
+
The runner's cache-gated read submits this on a cache miss, so dApps never
|
|
96
|
+
write their own read function: `esr_fetch("profile-7")`.
|
|
97
|
+
"""
|
|
98
|
+
return ecld_result(None, keys=list(keys) or None)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TaskStatus:
|
|
102
|
+
SUCCESS = 0
|
|
103
|
+
SYSTEM_ERROR = 1
|
|
104
|
+
KEY_ERROR = 2
|
|
105
|
+
SYNTAX_WARNING = 3
|
|
106
|
+
BASE_EXCEPTION = 4
|
|
107
|
+
PAYLOAD_NOT_DEFINED = 5
|
|
108
|
+
PAYLOAD_CHECKSUM_ERROR = 6
|
|
109
|
+
INPUT_CHECKSUM_ERROR = 7
|
|
110
|
+
EXECVE = 8
|
|
111
|
+
# Extended diagnostics (match the trustedzone's extended enum): the
|
|
112
|
+
# serverless backend failed to import inside the enclave, so none of its
|
|
113
|
+
# functions exist. Reported eagerly with the original import error instead
|
|
114
|
+
# of letting every call die as "name 'X' is not defined".
|
|
115
|
+
IMPORT_ERROR = 28
|
|
116
|
+
# A required enclave config value is present but EMPTY (e.g. an ESR
|
|
117
|
+
# address that was never baked into the sealed image). Reported eagerly
|
|
118
|
+
# with the variable named, instead of a confusing downstream crash.
|
|
119
|
+
CONFIG_ERROR = 32
|
|
120
|
+
EXECUTION_TIMEOUT = 33 # Started but produced no result within the order duration.
|
|
121
|
+
ESR_GAS_LIMIT_EXCEEDED = 34 # ESR state commits would exceed the per-order relayed-gas budget.
|
|
122
|
+
ESR_NONCE_VIOLATION = 36 # A commit's idempotency nonce was already used -- duplicate
|
|
123
|
+
# suppressed, state unchanged (StateNonceError).
|
|
124
|
+
ESR_RELAY_TIMEOUT = 37 # The enclave signed state commits but they did not land on
|
|
125
|
+
# the registry within 5 blocks -- the node did not relay them.
|
|
126
|
+
# The result is NOT trustworthy as persisted state; the
|
|
127
|
+
# validator refunds the order.
|
|
128
|
+
ESR_COMMIT_LIMIT_EXCEEDED = 38 # More than 100 state commits in one run -- the per-run cap
|
|
129
|
+
# that bounds relayed transactions and the result size.
|
|
130
|
+
SECURITY_VIOLATION = 35 # A state commit was authorized under a caller other than the
|
|
131
|
+
# task's submitter (the in-enclave ownership check was bypassed).
|
|
132
|
+
# Set by the securelock when it detects a forged commit caller,
|
|
133
|
+
# and independently by the trustedzone re-adjudication.
|
|
134
|
+
CAS_ATTESTATION_FAULT = 40 # The CAS that provisioned this enclave presented an
|
|
135
|
+
# INVALID self-attestation quote (ECAS_CAS_QUOTE failing
|
|
136
|
+
# the ValidatorRegistry checks). A missing quote is a
|
|
137
|
+
# rollout gap and only logs; an INCORRECT one means the
|
|
138
|
+
# operator provisioned the task through an impostor CAS.
|
|
139
|
+
# Operator fault; the order terminates and is refunded.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _empty_required_config():
|
|
143
|
+
"""Names of *_ADDRESS-style enclave config vars that are set but empty.
|
|
144
|
+
|
|
145
|
+
The enclave is sealed: env can only come from the image, so a present-but-
|
|
146
|
+
empty required value is always a build/render defect, never a runtime
|
|
147
|
+
choice. ESR_* vars only exist when the project enabled ESR at build time.
|
|
148
|
+
"""
|
|
149
|
+
required = ("ESR_CONTRACT_ADDRESS",)
|
|
150
|
+
return [name for name in required
|
|
151
|
+
if name in os.environ and not os.environ[name].strip()]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def execute_task_v3(payload_data, input_data, extra_globals=None):
|
|
155
|
+
missing = _empty_required_config()
|
|
156
|
+
if missing:
|
|
157
|
+
return (
|
|
158
|
+
TaskStatus.CONFIG_ERROR,
|
|
159
|
+
"ENCLAVE CONFIG ERROR: required value(s) empty inside the enclave: "
|
|
160
|
+
+ ", ".join(missing)
|
|
161
|
+
+ " | The enclave is sealed, so this value had to be baked at build"
|
|
162
|
+
+ " time and was not. Re-run ecld-build (it validates ESR config)"
|
|
163
|
+
+ " and republish.",
|
|
164
|
+
)
|
|
165
|
+
if _backend_import_error is not None:
|
|
166
|
+
return (
|
|
167
|
+
TaskStatus.IMPORT_ERROR,
|
|
168
|
+
"BACKEND IMPORT ERROR: " + _backend_import_error
|
|
169
|
+
+ " | The serverless backend failed to import inside the enclave, so"
|
|
170
|
+
+ " none of its functions are available. Common causes: a module"
|
|
171
|
+
+ " missing from src/serverless/requirements.txt, or a top-level"
|
|
172
|
+
+ " import inside backend.py (the enclave loads it as the package"
|
|
173
|
+
+ " 'serverless.backend' - use relative imports for sibling modules).",
|
|
174
|
+
)
|
|
175
|
+
base_globals = session_base_globals()
|
|
176
|
+
if extra_globals:
|
|
177
|
+
base_globals.update(extra_globals)
|
|
178
|
+
return Exec(payload_data, input_data, globals=base_globals)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class _Ecld:
|
|
182
|
+
"""The single dApp-facing handle injected into every payload as `ecld`.
|
|
183
|
+
|
|
184
|
+
A namespaced front door over the same primitives a payload used to reach
|
|
185
|
+
by bare magic names, chosen to read like a contract object to Web3 devs:
|
|
186
|
+
|
|
187
|
+
ecld.result(value) # end the task with a result (+ESR state)
|
|
188
|
+
ecld.input # the request payload (was ___etny_data_set___)
|
|
189
|
+
ecld.caller # the data owner's wallet (msg.sender-style)
|
|
190
|
+
ecld.on_input = fn # session input handler (was ___etny_on_input___)
|
|
191
|
+
ecld.state.get(k) / .commit(k, v) / .grant(...) / ... # ESR
|
|
192
|
+
ecld.fetch("k1", "k2") # standard state-fetch task body
|
|
193
|
+
|
|
194
|
+
Every legacy name (___etny_result___, ___etny_data_set___,
|
|
195
|
+
___etny_on_input___, task_caller, esr_*, ...) still works unchanged; this
|
|
196
|
+
object adds a coherent surface without removing anything. `input` and the
|
|
197
|
+
session handler are populated per-run by the executor.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self):
|
|
201
|
+
self.result = ecld_result
|
|
202
|
+
self.fetch = esr_fetch
|
|
203
|
+
self.input = None # set by Exec() when input_data is present
|
|
204
|
+
self.on_input = None # session handler slot; payload assigns it
|
|
205
|
+
self.state = None # StateRegistry-style handle if ESR present
|
|
206
|
+
self._caller_fn = None # bound to ecld_state.task_caller if present
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def caller(self):
|
|
210
|
+
"""The verified wallet that submitted this task (msg.sender-style),
|
|
211
|
+
or None in a non-ESR build / local test. Live: reads the attested
|
|
212
|
+
caller at access time, not at scope-build time."""
|
|
213
|
+
fn = self._caller_fn
|
|
214
|
+
try:
|
|
215
|
+
return fn() if callable(fn) else None
|
|
216
|
+
except Exception:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def session_base_globals():
|
|
221
|
+
"""The exec globals every payload run receives. Public so the session
|
|
222
|
+
executor (etny_session_exec) builds ONE persistent dict and keeps payload
|
|
223
|
+
state alive across streamed inputs."""
|
|
224
|
+
ecld = _Ecld()
|
|
225
|
+
base_globals = {
|
|
226
|
+
"___etny_result___": ___etny_result___, # legacy alias (no ESR)
|
|
227
|
+
"ecld_result": ecld_result, # the result API (bare)
|
|
228
|
+
"esr_fetch": esr_fetch, # standard state-fetch task
|
|
229
|
+
"ecld": ecld, # the namespaced handle
|
|
230
|
+
**sdkFunctions,
|
|
231
|
+
}
|
|
232
|
+
# State ownership / ACL API (present only in ESR-enabled builds). Enforced
|
|
233
|
+
# inside ecld_state against the trustedzone-attested caller.
|
|
234
|
+
try:
|
|
235
|
+
import ecld_state as _ecld_state
|
|
236
|
+
base_globals.setdefault("task_caller", _ecld_state.task_caller)
|
|
237
|
+
for _name in ("esr_grant", "esr_revoke", "esr_set_public_read",
|
|
238
|
+
"esr_transfer", "esr_owner", "esr_acl", "esr_nonce"):
|
|
239
|
+
base_globals.setdefault(_name, getattr(_ecld_state, _name))
|
|
240
|
+
# Mirror the ESR surface onto the namespaced handle. `caller` is a live
|
|
241
|
+
# property that calls task_caller() at access time (see _Ecld.caller).
|
|
242
|
+
ecld._caller_fn = _ecld_state.task_caller
|
|
243
|
+
try:
|
|
244
|
+
ecld.state = _ecld_state.StateRegistry()
|
|
245
|
+
except Exception:
|
|
246
|
+
ecld.state = None
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
return base_globals
|
|
250
|
+
|
|
251
|
+
def Exec(payload_data, input_data, globals=None, locals=None):
|
|
252
|
+
try:
|
|
253
|
+
if globals is None:
|
|
254
|
+
globals = {}
|
|
255
|
+
if locals is None:
|
|
256
|
+
locals = globals
|
|
257
|
+
|
|
258
|
+
print("Globals keys:", list(globals.keys()))
|
|
259
|
+
print("Locals keys:", list(locals.keys()))
|
|
260
|
+
print("execve in globals:", "execve" in globals)
|
|
261
|
+
print("execve in locals:", "execve" in locals)
|
|
262
|
+
|
|
263
|
+
if payload_data is not None:
|
|
264
|
+
if input_data is not None:
|
|
265
|
+
globals["___etny_data_set___"] = input_data # legacy name
|
|
266
|
+
_ecld = globals.get("ecld")
|
|
267
|
+
if _ecld is not None:
|
|
268
|
+
_ecld.input = input_data # ecld.input
|
|
269
|
+
module = ast.parse(payload_data)
|
|
270
|
+
outputs = []
|
|
271
|
+
for node in module.body:
|
|
272
|
+
if isinstance(node, ast.Expr):
|
|
273
|
+
expr_code = compile(
|
|
274
|
+
ast.Expression(node.value), filename="<ast>", mode="eval"
|
|
275
|
+
)
|
|
276
|
+
result = eval(expr_code, globals, locals)
|
|
277
|
+
outputs.append(result)
|
|
278
|
+
else:
|
|
279
|
+
# Handle statements if needed
|
|
280
|
+
exec(
|
|
281
|
+
compile(ast.Module([node], type_ignores=[]), filename="<ast>", mode="exec"),
|
|
282
|
+
globals,
|
|
283
|
+
locals,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Function results are arbitrary Python values -- the shipped
|
|
287
|
+
# esr-counter example returns dicts -- so serialize anything that
|
|
288
|
+
# is not already a str instead of letting join() raise. None (an
|
|
289
|
+
# expression statement with no value, e.g. a bare print()) is
|
|
290
|
+
# dropped, matching REPL semantics. NOTE: this is the executor the
|
|
291
|
+
# serverless securelock actually runs (local_api.py imports
|
|
292
|
+
# etny_exec); the 0.8.3 fix landed only in etny_exec_serv.py,
|
|
293
|
+
# which nothing imports.
|
|
294
|
+
rendered = [
|
|
295
|
+
out if isinstance(out, str) else json.dumps(out, default=str)
|
|
296
|
+
for out in outputs
|
|
297
|
+
if out is not None
|
|
298
|
+
]
|
|
299
|
+
return ___etny_result___("\n".join(rendered))
|
|
300
|
+
else:
|
|
301
|
+
return (
|
|
302
|
+
TaskStatus.PAYLOAD_NOT_DEFINED,
|
|
303
|
+
"Could not find the source file to execute",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return TaskStatus.SUCCESS, "TASK EXECUTED SUCCESSFULLY"
|
|
307
|
+
except SystemExit as e:
|
|
308
|
+
# A task returns its result ONLY through ___etny_result___(data) ->
|
|
309
|
+
# quit([code, data]), which raises SystemExit carrying [code, data].
|
|
310
|
+
# Decode and honor that embedded code -- it is the authoritative outcome.
|
|
311
|
+
#
|
|
312
|
+
# Handled BEFORE `except SystemError` below on purpose. The read path runs
|
|
313
|
+
# web3/crypto C-extensions before returning; those intermittently raise a
|
|
314
|
+
# spurious SystemError ("error return without exception set") during the
|
|
315
|
+
# SystemExit unwind. With SystemError caught first, that spurious error
|
|
316
|
+
# stamped SYSTEM_ERROR onto a task that had already computed the correct
|
|
317
|
+
# value -- the ~50% false failure seen only on read tasks. Honoring the
|
|
318
|
+
# real result first fixes it.
|
|
319
|
+
try:
|
|
320
|
+
code, data = e.args[0][0], e.args[0][1]
|
|
321
|
+
except Exception:
|
|
322
|
+
tb = traceback.format_exc()
|
|
323
|
+
logging.error(
|
|
324
|
+
"SystemExit without a [code, data] result payload: %r (args=%r)"
|
|
325
|
+
" -- full traceback follows:\n%s", e, getattr(e, "args", None), tb)
|
|
326
|
+
return TaskStatus.SYSTEM_ERROR, f"SYSTEM_ERROR: {e!r}\n{tb}"
|
|
327
|
+
if code == 0:
|
|
328
|
+
return TaskStatus.SUCCESS, data
|
|
329
|
+
return int(code), data
|
|
330
|
+
except SystemError as e:
|
|
331
|
+
# A spurious native/interpreter SystemError can surface DURING the
|
|
332
|
+
# SystemExit unwind and REPLACE it, so the exception reaching here is a
|
|
333
|
+
# SystemError even though the task already produced its result via
|
|
334
|
+
# quit([code, data]). The real SystemExit is still on the chain as
|
|
335
|
+
# e.__context__ / e.__cause__ -- recover the result from there before
|
|
336
|
+
# treating this as a failure.
|
|
337
|
+
for chained in (getattr(e, "__context__", None), getattr(e, "__cause__", None)):
|
|
338
|
+
if isinstance(chained, SystemExit):
|
|
339
|
+
try:
|
|
340
|
+
code, data = chained.args[0][0], chained.args[0][1]
|
|
341
|
+
except Exception:
|
|
342
|
+
continue
|
|
343
|
+
logging.warning(
|
|
344
|
+
"SystemError raised during result unwind; recovered task "
|
|
345
|
+
"result from chained SystemExit (code=%r). Native error was: "
|
|
346
|
+
"%r", code, e)
|
|
347
|
+
if code == 0:
|
|
348
|
+
return TaskStatus.SUCCESS, data
|
|
349
|
+
return int(code), data
|
|
350
|
+
# No recoverable result -> genuine internal error. Deliver the FULL
|
|
351
|
+
# traceback to the data owner AS the result (visible in their runner
|
|
352
|
+
# output) and never crash the enclave.
|
|
353
|
+
tb = traceback.format_exc()
|
|
354
|
+
logging.error(
|
|
355
|
+
"SystemError in payload execution: %r (args=%r) -- full traceback "
|
|
356
|
+
"follows:\n%s", e, getattr(e, "args", None), tb)
|
|
357
|
+
return TaskStatus.SYSTEM_ERROR, f"SYSTEM_ERROR: {e!r}\n{tb}"
|
|
358
|
+
except KeyError as e:
|
|
359
|
+
return TaskStatus.KEY_ERROR, e.args[0]
|
|
360
|
+
except SyntaxWarning as e:
|
|
361
|
+
return TaskStatus.SYNTAX_WARNING, e.args[0]
|
|
362
|
+
except BaseException as e:
|
|
363
|
+
# A duplicate-suppressed commit (StateNonceError) gets its own task
|
|
364
|
+
# code so the dApp can distinguish "already applied" from a failure.
|
|
365
|
+
# Matched by name so non-ESR builds need no import.
|
|
366
|
+
if type(e).__name__ == "StateNonceError":
|
|
367
|
+
return TaskStatus.ESR_NONCE_VIOLATION, f"ESR_NONCE_VIOLATION: {e}"
|
|
368
|
+
# Over the per-run commit cap (100): the over-limit commit was NOT
|
|
369
|
+
# applied; the earlier commits stand and will be relayed.
|
|
370
|
+
if type(e).__name__ == "StateLimitError":
|
|
371
|
+
return (TaskStatus.ESR_COMMIT_LIMIT_EXCEEDED,
|
|
372
|
+
f"ESR_COMMIT_LIMIT_EXCEEDED: {e}")
|
|
373
|
+
# Deliver the traceback to the data owner as the result and never crash;
|
|
374
|
+
# preserve the embedded-result SUCCESS path (code 0).
|
|
375
|
+
try:
|
|
376
|
+
if e.args[0][0] == 0:
|
|
377
|
+
return TaskStatus.SUCCESS, e.args[0][1]
|
|
378
|
+
except Exception:
|
|
379
|
+
pass
|
|
380
|
+
tb = traceback.format_exc()
|
|
381
|
+
logging.error(
|
|
382
|
+
"BaseException in payload execution: %r -- full traceback follows:\n%s",
|
|
383
|
+
e, tb)
|
|
384
|
+
return TaskStatus.BASE_EXCEPTION, f"BASE_EXCEPTION: {e!r}\n{tb}"
|
|
385
|
+
|