@le-space/rootfs 0.4.1 → 0.4.3
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/package.json +1 -1
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-bootstrap-refresh.py +13 -10
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap-deregister.py +6 -12
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap-refresh.py +56 -20
- package/reference/uc-go-peer/rootfs/uc-go-peer-configure.sh +7 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-setup-server.py +250 -2
package/package.json
CHANGED
|
@@ -306,19 +306,22 @@ def sign_aleph_message(unsigned_message: dict[str, object], private_key: str) ->
|
|
|
306
306
|
|
|
307
307
|
def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, dict]:
|
|
308
308
|
url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
{**message, "sync": True},
|
|
312
|
-
dict(message),
|
|
313
|
-
]
|
|
309
|
+
request_body = {"sync": True, "message": message}
|
|
310
|
+
max_attempts = 3
|
|
314
311
|
last: tuple[int, dict] | None = None
|
|
315
|
-
for
|
|
316
|
-
last = post_json(url,
|
|
312
|
+
for index in range(max_attempts):
|
|
313
|
+
last = post_json(url, request_body)
|
|
317
314
|
if 200 <= last[0] < 300:
|
|
318
315
|
return last
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
316
|
+
publication_status = last[1].get("publication_status") if isinstance(last[1], dict) else None
|
|
317
|
+
retryable = last[0] >= 500 or (
|
|
318
|
+
isinstance(publication_status, dict)
|
|
319
|
+
and isinstance(publication_status.get("status"), str)
|
|
320
|
+
and publication_status.get("status").strip().lower() == "error"
|
|
321
|
+
)
|
|
322
|
+
if index >= max_attempts - 1 or not retryable:
|
|
323
|
+
raise RuntimeError(f"Aleph broadcast failed: {last[0]} {json_dumps(last[1])}")
|
|
324
|
+
raise RuntimeError("Aleph broadcast failed: retry budget exhausted")
|
|
322
325
|
|
|
323
326
|
|
|
324
327
|
def parse_post_record(entry: object) -> dict[str, object] | None:
|
|
@@ -155,22 +155,16 @@ def is_retryable_broadcast_failure(http_status: int, payload: object) -> bool:
|
|
|
155
155
|
|
|
156
156
|
def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, object]:
|
|
157
157
|
url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
]
|
|
163
|
-
for index, attempt in enumerate(attempts):
|
|
164
|
-
http_status, payload = post_json(url, attempt)
|
|
158
|
+
request_body = {"sync": True, "message": message}
|
|
159
|
+
max_attempts = 3
|
|
160
|
+
for index in range(max_attempts):
|
|
161
|
+
http_status, payload = post_json(url, request_body)
|
|
165
162
|
if 200 <= http_status < 300:
|
|
166
163
|
return http_status, payload
|
|
167
|
-
can_retry = index <
|
|
168
|
-
is_invalid_message_format(http_status, payload)
|
|
169
|
-
or is_retryable_broadcast_failure(http_status, payload)
|
|
170
|
-
)
|
|
164
|
+
can_retry = index < max_attempts - 1 and is_retryable_broadcast_failure(http_status, payload)
|
|
171
165
|
if not can_retry:
|
|
172
166
|
raise RuntimeError(f"Aleph broadcast failed: {http_status} {json_dumps(payload)}")
|
|
173
|
-
raise RuntimeError("Aleph broadcast failed:
|
|
167
|
+
raise RuntimeError("Aleph broadcast failed: retry budget exhausted")
|
|
174
168
|
|
|
175
169
|
|
|
176
170
|
def parse_post_record(entry: object) -> dict[str, object] | None:
|
|
@@ -129,6 +129,21 @@ def signature_payload(chain: str, sender: str, message_type: str, item_hash: str
|
|
|
129
129
|
return "\n".join([chain, sender, message_type, item_hash])
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
def registration_id_instance_item_hash(registration_id: str | None) -> str | None:
|
|
133
|
+
normalized = str(registration_id or "").strip()
|
|
134
|
+
if not normalized:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
parts = [part.strip() for part in normalized.split(":") if part.strip()]
|
|
138
|
+
if not parts:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
candidate = parts[-1]
|
|
142
|
+
if len(candidate) == 64 and all(char in "0123456789abcdefABCDEF" for char in candidate):
|
|
143
|
+
return candidate
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
132
147
|
def relay_authorization_payload(
|
|
133
148
|
owner_address: str,
|
|
134
149
|
publisher_address: str,
|
|
@@ -136,6 +151,7 @@ def relay_authorization_payload(
|
|
|
136
151
|
registration_id: str | None,
|
|
137
152
|
profile: str | None,
|
|
138
153
|
version: str | None,
|
|
154
|
+
instance_item_hash: str | None,
|
|
139
155
|
issued_at: int,
|
|
140
156
|
) -> dict[str, object]:
|
|
141
157
|
return {
|
|
@@ -145,7 +161,7 @@ def relay_authorization_payload(
|
|
|
145
161
|
"registrationId": registration_id,
|
|
146
162
|
"profile": profile,
|
|
147
163
|
"version": version,
|
|
148
|
-
"instanceItemHash":
|
|
164
|
+
"instanceItemHash": instance_item_hash,
|
|
149
165
|
"issuedAt": issued_at,
|
|
150
166
|
"expiresAt": None,
|
|
151
167
|
}
|
|
@@ -194,6 +210,7 @@ def load_owner_authorization(
|
|
|
194
210
|
return None
|
|
195
211
|
|
|
196
212
|
owner_address = address_from_private_key(owner_private_key)
|
|
213
|
+
instance_item_hash = registration_id_instance_item_hash(registration_id)
|
|
197
214
|
payload = relay_authorization_payload(
|
|
198
215
|
owner_address,
|
|
199
216
|
publisher_address,
|
|
@@ -201,6 +218,7 @@ def load_owner_authorization(
|
|
|
201
218
|
registration_id,
|
|
202
219
|
profile,
|
|
203
220
|
version,
|
|
221
|
+
instance_item_hash,
|
|
204
222
|
issued_at,
|
|
205
223
|
)
|
|
206
224
|
return {
|
|
@@ -210,6 +228,28 @@ def load_owner_authorization(
|
|
|
210
228
|
}
|
|
211
229
|
|
|
212
230
|
|
|
231
|
+
def resolve_owner_address(
|
|
232
|
+
env_values: dict[str, str],
|
|
233
|
+
owner_authorization: dict[str, object] | None,
|
|
234
|
+
) -> str | None:
|
|
235
|
+
if isinstance(owner_authorization, dict):
|
|
236
|
+
payload = owner_authorization.get("payload")
|
|
237
|
+
if isinstance(payload, dict):
|
|
238
|
+
owner_address = str(payload.get("ownerAddress") or "").strip()
|
|
239
|
+
if owner_address:
|
|
240
|
+
return owner_address
|
|
241
|
+
|
|
242
|
+
owner_address = env_values.get("ALEPH_BOOTSTRAP_OWNER_ADDRESS", "").strip()
|
|
243
|
+
if owner_address:
|
|
244
|
+
return owner_address
|
|
245
|
+
|
|
246
|
+
owner_private_key = env_values.get("ALEPH_BOOTSTRAP_OWNER_PRIVATE_KEY", "").strip()
|
|
247
|
+
if owner_private_key:
|
|
248
|
+
return address_from_private_key(owner_private_key)
|
|
249
|
+
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
213
253
|
def post_json(url: str, body: dict[str, object]) -> tuple[int, dict]:
|
|
214
254
|
data = json_dumps(body).encode("utf-8")
|
|
215
255
|
request = urllib.request.Request(
|
|
@@ -243,35 +283,34 @@ def build_post_content(
|
|
|
243
283
|
registration_id: str | None,
|
|
244
284
|
profile: str | None,
|
|
245
285
|
version: str | None,
|
|
286
|
+
owner_address: str | None,
|
|
246
287
|
owner_authorization: dict[str, object] | None,
|
|
247
288
|
relay_proof: dict[str, object],
|
|
248
289
|
now_ms: int,
|
|
290
|
+
now_seconds: float,
|
|
249
291
|
ref: str,
|
|
250
292
|
post_type: str,
|
|
251
293
|
) -> dict[str, object]:
|
|
252
294
|
content = {
|
|
253
295
|
"peerId": peer_id,
|
|
254
296
|
"multiaddrs": multiaddrs,
|
|
255
|
-
"browserMultiaddrs": browser_multiaddrs if browser_multiaddrs else None,
|
|
256
297
|
"registrationId": registration_id,
|
|
257
298
|
"profile": profile,
|
|
258
299
|
"version": version,
|
|
259
|
-
"ownerAddress":
|
|
260
|
-
owner_authorization.get("payload", {}).get("ownerAddress")
|
|
261
|
-
if isinstance(owner_authorization, dict)
|
|
262
|
-
else None
|
|
263
|
-
),
|
|
300
|
+
"ownerAddress": owner_address,
|
|
264
301
|
"publisherAddress": sender,
|
|
265
302
|
"authorization": owner_authorization,
|
|
266
303
|
"relayProof": relay_proof,
|
|
267
304
|
"updatedAt": now_ms,
|
|
268
305
|
}
|
|
306
|
+
if browser_multiaddrs:
|
|
307
|
+
content["browserMultiaddrs"] = browser_multiaddrs
|
|
269
308
|
return {
|
|
270
309
|
"type": post_type,
|
|
271
310
|
"address": sender,
|
|
272
311
|
"ref": ref,
|
|
273
312
|
"content": content,
|
|
274
|
-
"time":
|
|
313
|
+
"time": now_seconds,
|
|
275
314
|
}
|
|
276
315
|
|
|
277
316
|
|
|
@@ -347,22 +386,16 @@ def is_retryable_broadcast_failure(http_status: int, payload: object) -> bool:
|
|
|
347
386
|
|
|
348
387
|
def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, object]:
|
|
349
388
|
url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
]
|
|
355
|
-
for index, attempt in enumerate(attempts):
|
|
356
|
-
http_status, payload = post_json(url, attempt)
|
|
389
|
+
request_body = {"sync": True, "message": message}
|
|
390
|
+
max_attempts = 3
|
|
391
|
+
for index in range(max_attempts):
|
|
392
|
+
http_status, payload = post_json(url, request_body)
|
|
357
393
|
if 200 <= http_status < 300:
|
|
358
394
|
return http_status, payload
|
|
359
|
-
can_retry = index <
|
|
360
|
-
is_invalid_message_format(http_status, payload)
|
|
361
|
-
or is_retryable_broadcast_failure(http_status, payload)
|
|
362
|
-
)
|
|
395
|
+
can_retry = index < max_attempts - 1 and is_retryable_broadcast_failure(http_status, payload)
|
|
363
396
|
if not can_retry:
|
|
364
397
|
raise RuntimeError(f"Aleph broadcast failed: {http_status} {json_dumps(payload)}")
|
|
365
|
-
raise RuntimeError("Aleph broadcast failed:
|
|
398
|
+
raise RuntimeError("Aleph broadcast failed: retry budget exhausted")
|
|
366
399
|
|
|
367
400
|
|
|
368
401
|
def parse_post_record(entry: object) -> dict[str, object] | None:
|
|
@@ -512,6 +545,7 @@ def main() -> None:
|
|
|
512
545
|
version,
|
|
513
546
|
now_ms,
|
|
514
547
|
)
|
|
548
|
+
owner_address = resolve_owner_address(env_values, owner_authorization)
|
|
515
549
|
|
|
516
550
|
proof_payload = relay_proof_payload(
|
|
517
551
|
peer_id,
|
|
@@ -535,9 +569,11 @@ def main() -> None:
|
|
|
535
569
|
registration_id,
|
|
536
570
|
profile,
|
|
537
571
|
version,
|
|
572
|
+
owner_address,
|
|
538
573
|
owner_authorization,
|
|
539
574
|
relay_proof,
|
|
540
575
|
now_ms,
|
|
576
|
+
now_seconds,
|
|
541
577
|
ref,
|
|
542
578
|
post_type,
|
|
543
579
|
)
|
|
@@ -26,6 +26,7 @@ UDP_PORT=""
|
|
|
26
26
|
BOOTSTRAP_PUBLISHER_PRIVATE_KEY=""
|
|
27
27
|
BOOTSTRAP_PUBLISHER_LIBP2P_IDENTITY_B64=""
|
|
28
28
|
BOOTSTRAP_OWNER_PRIVATE_KEY=""
|
|
29
|
+
BOOTSTRAP_OWNER_ADDRESS=""
|
|
29
30
|
BOOTSTRAP_OWNER_AUTHORIZATION_B64=""
|
|
30
31
|
BOOTSTRAP_REGISTRATION_ID=""
|
|
31
32
|
START_SERVICE=1
|
|
@@ -46,6 +47,7 @@ Usage:
|
|
|
46
47
|
[--bootstrap-publisher-private-key <hex>] \
|
|
47
48
|
[--bootstrap-publisher-libp2p-identity-b64 <base64>] \
|
|
48
49
|
[--bootstrap-owner-private-key <hex>] \
|
|
50
|
+
[--bootstrap-owner-address <address>] \
|
|
49
51
|
[--bootstrap-owner-authorization-b64 <base64>] \
|
|
50
52
|
[--bootstrap-registration-id <id>] \
|
|
51
53
|
[--no-start]
|
|
@@ -169,6 +171,10 @@ while [ "$#" -gt 0 ]; do
|
|
|
169
171
|
BOOTSTRAP_OWNER_PRIVATE_KEY="${2:-}"
|
|
170
172
|
shift 2
|
|
171
173
|
;;
|
|
174
|
+
--bootstrap-owner-address)
|
|
175
|
+
BOOTSTRAP_OWNER_ADDRESS="${2:-}"
|
|
176
|
+
shift 2
|
|
177
|
+
;;
|
|
172
178
|
--bootstrap-owner-authorization-b64)
|
|
173
179
|
BOOTSTRAP_OWNER_AUTHORIZATION_B64="${2:-}"
|
|
174
180
|
shift 2
|
|
@@ -275,6 +281,7 @@ fi
|
|
|
275
281
|
if [ -n "${BOOTSTRAP_OWNER_PRIVATE_KEY}" ]; then
|
|
276
282
|
write_env_var "ALEPH_BOOTSTRAP_OWNER_PRIVATE_KEY" "${BOOTSTRAP_OWNER_PRIVATE_KEY}"
|
|
277
283
|
fi
|
|
284
|
+
write_env_var "ALEPH_BOOTSTRAP_OWNER_ADDRESS" "${BOOTSTRAP_OWNER_ADDRESS}"
|
|
278
285
|
if [ -n "${BOOTSTRAP_OWNER_AUTHORIZATION_B64}" ]; then
|
|
279
286
|
write_env_var "ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64" "${BOOTSTRAP_OWNER_AUTHORIZATION_B64}"
|
|
280
287
|
fi
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
2
4
|
import ipaddress
|
|
3
5
|
import json
|
|
4
6
|
import os
|
|
@@ -7,6 +9,8 @@ import threading
|
|
|
7
9
|
import time
|
|
8
10
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
9
11
|
from pathlib import Path
|
|
12
|
+
from urllib import error as urllib_error
|
|
13
|
+
from urllib import request as urllib_request
|
|
10
14
|
from urllib.error import HTTPError, URLError
|
|
11
15
|
from urllib.parse import urlencode, urlsplit
|
|
12
16
|
from urllib.request import urlopen
|
|
@@ -23,12 +27,36 @@ AUTHORIZED_KEYS_FILE = os.environ.get("AUTHORIZED_KEYS_FILE", "/root/.ssh/author
|
|
|
23
27
|
ALEPH_API_HOST = os.environ.get("ALEPH_API_HOST", "https://api2.aleph.im")
|
|
24
28
|
BOOTSTRAP_CONFIG_KEY = os.environ.get("BOOTSTRAP_CONFIG_KEY", "vm-bootstrap-config")
|
|
25
29
|
BOOTSTRAP_CONFIG_POLL_SECONDS = float(os.environ.get("BOOTSTRAP_CONFIG_POLL_SECONDS", "5"))
|
|
30
|
+
BOOTSTRAP_CONFIG_SIGNAL_REF = os.environ.get("BOOTSTRAP_CONFIG_SIGNAL_REF", "vm-bootstrap-config")
|
|
31
|
+
BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = os.environ.get(
|
|
32
|
+
"BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE", "vm-bootstrap-config-status"
|
|
33
|
+
)
|
|
34
|
+
BOOTSTRAP_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
|
|
26
35
|
DESCRIBE_TIMEOUT_SECONDS = int(os.environ.get("DESCRIBE_TIMEOUT_SECONDS", "15"))
|
|
27
36
|
DESCRIBE_INTERVAL_SECONDS = float(os.environ.get("DESCRIBE_INTERVAL_SECONDS", "1"))
|
|
28
37
|
|
|
29
38
|
CONFIGURE_LOCK = threading.Lock()
|
|
30
39
|
|
|
31
40
|
|
|
41
|
+
def _json_dumps(payload: object) -> str:
|
|
42
|
+
return json.dumps(payload, separators=(",", ":"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_env_file(path: str) -> dict[str, str]:
|
|
46
|
+
values: dict[str, str] = {}
|
|
47
|
+
if not os.path.exists(path):
|
|
48
|
+
return values
|
|
49
|
+
|
|
50
|
+
with open(path, encoding="utf-8") as handle:
|
|
51
|
+
for line in handle:
|
|
52
|
+
stripped = line.strip()
|
|
53
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
54
|
+
continue
|
|
55
|
+
key, value = stripped.split("=", 1)
|
|
56
|
+
values[key.strip()] = value.strip()
|
|
57
|
+
return values
|
|
58
|
+
|
|
59
|
+
|
|
32
60
|
def _cors_headers(handler: BaseHTTPRequestHandler) -> None:
|
|
33
61
|
handler.send_header("Access-Control-Allow-Origin", "*")
|
|
34
62
|
handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
@@ -136,6 +164,9 @@ def _build_configure_args(record: dict) -> list[str]:
|
|
|
136
164
|
registration_id = str(bootstrap.get("registrationId") or "").strip()
|
|
137
165
|
if registration_id:
|
|
138
166
|
args.extend(["--bootstrap-registration-id", registration_id])
|
|
167
|
+
owner_address = str(record.get("ownerAddress") or "").strip()
|
|
168
|
+
if owner_address:
|
|
169
|
+
args.extend(["--bootstrap-owner-address", owner_address])
|
|
139
170
|
owner_auth = str(bootstrap.get("ownerAuthorizationBase64") or "").strip()
|
|
140
171
|
if owner_auth:
|
|
141
172
|
args.extend(["--bootstrap-owner-authorization-b64", owner_auth])
|
|
@@ -143,7 +174,7 @@ def _build_configure_args(record: dict) -> list[str]:
|
|
|
143
174
|
return args
|
|
144
175
|
|
|
145
176
|
|
|
146
|
-
def _run_configure_process(args: list[str]) -> tuple[bool, str]:
|
|
177
|
+
def _run_configure_process(args: list[str], bootstrap_record: dict | None = None) -> tuple[bool, str]:
|
|
147
178
|
with CONFIGURE_LOCK:
|
|
148
179
|
if os.path.exists(READY_FILE):
|
|
149
180
|
return True, "ready"
|
|
@@ -153,9 +184,223 @@ def _run_configure_process(args: list[str]) -> tuple[bool, str]:
|
|
|
153
184
|
return False, error.stderr.strip() or error.stdout.strip() or str(error)
|
|
154
185
|
|
|
155
186
|
_start_metadata_generation()
|
|
187
|
+
if isinstance(bootstrap_record, dict):
|
|
188
|
+
try:
|
|
189
|
+
_publish_vm_bootstrap_config_signal(bootstrap_record)
|
|
190
|
+
except Exception as error: # pragma: no cover - runtime error path
|
|
191
|
+
return False, f"configured relay but failed to publish bootstrap config signal: {error}"
|
|
156
192
|
return True, result.stdout.strip()
|
|
157
193
|
|
|
158
194
|
|
|
195
|
+
def _address_from_private_key(private_key: str) -> str:
|
|
196
|
+
try:
|
|
197
|
+
from eth_account import Account
|
|
198
|
+
except ImportError as error: # pragma: no cover - runtime dependency
|
|
199
|
+
raise RuntimeError(
|
|
200
|
+
"eth-account is required for guest-side bootstrap status publishing"
|
|
201
|
+
) from error
|
|
202
|
+
|
|
203
|
+
return Account.from_key(private_key).address
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _sign_personal_message(private_key: str, payload: str) -> str:
|
|
207
|
+
try:
|
|
208
|
+
from eth_account import Account
|
|
209
|
+
from eth_account.messages import encode_defunct
|
|
210
|
+
except ImportError as error: # pragma: no cover - runtime dependency
|
|
211
|
+
raise RuntimeError(
|
|
212
|
+
"eth-account is required for guest-side bootstrap status publishing"
|
|
213
|
+
) from error
|
|
214
|
+
|
|
215
|
+
message = encode_defunct(text=payload)
|
|
216
|
+
signed = Account.sign_message(message, private_key=private_key)
|
|
217
|
+
signature = signed.signature.hex()
|
|
218
|
+
return signature if signature.startswith("0x") else f"0x{signature}"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _signature_payload(chain: str, sender: str, message_type: str, item_hash: str) -> str:
|
|
222
|
+
return "\n".join([chain, sender, message_type, item_hash])
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _iter_validation_errors(payload: object) -> list[dict]:
|
|
226
|
+
if isinstance(payload, list):
|
|
227
|
+
return [entry for entry in payload if isinstance(entry, dict)]
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _is_invalid_message_format(http_status: int, payload: object) -> bool:
|
|
232
|
+
if http_status != 422:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
if isinstance(payload, dict):
|
|
236
|
+
details = payload.get("details")
|
|
237
|
+
if isinstance(details, str) and "InvalidMessageFormat" in details:
|
|
238
|
+
return True
|
|
239
|
+
if isinstance(details, dict):
|
|
240
|
+
message = details.get("message")
|
|
241
|
+
if isinstance(message, str) and "InvalidMessageFormat" in message:
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
for entry in _iter_validation_errors(payload):
|
|
245
|
+
message = entry.get("msg")
|
|
246
|
+
location = entry.get("loc")
|
|
247
|
+
if isinstance(message, str) and "InvalidMessageFormat" in message:
|
|
248
|
+
return True
|
|
249
|
+
if message == "Field required" and location == ["message"]:
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _is_retryable_broadcast_failure(http_status: int, payload: object) -> bool:
|
|
256
|
+
if http_status >= 500:
|
|
257
|
+
return True
|
|
258
|
+
if isinstance(payload, dict):
|
|
259
|
+
publication_status = payload.get("publication_status")
|
|
260
|
+
if isinstance(publication_status, dict):
|
|
261
|
+
status = publication_status.get("status")
|
|
262
|
+
if isinstance(status, str) and status.strip().lower() == "error":
|
|
263
|
+
return True
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _post_json(url: str, body: dict[str, object]) -> tuple[int, object]:
|
|
268
|
+
data = _json_dumps(body).encode("utf-8")
|
|
269
|
+
request = urllib_request.Request(
|
|
270
|
+
url,
|
|
271
|
+
data=data,
|
|
272
|
+
headers={"content-type": "application/json"},
|
|
273
|
+
method="POST",
|
|
274
|
+
)
|
|
275
|
+
try:
|
|
276
|
+
with urllib_request.urlopen(request, timeout=30) as response:
|
|
277
|
+
payload = response.read().decode("utf-8")
|
|
278
|
+
return response.status, json.loads(payload or "{}")
|
|
279
|
+
except urllib_error.HTTPError as error:
|
|
280
|
+
payload = error.read().decode("utf-8")
|
|
281
|
+
try:
|
|
282
|
+
return error.code, json.loads(payload or "{}")
|
|
283
|
+
except json.JSONDecodeError:
|
|
284
|
+
return error.code, {"details": payload}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, object]:
|
|
288
|
+
url = f"{api_host.rstrip('/')}/api/v0/messages"
|
|
289
|
+
request_body = {"sync": True, "message": message}
|
|
290
|
+
max_attempts = 3
|
|
291
|
+
for index in range(max_attempts):
|
|
292
|
+
http_status, payload = _post_json(url, request_body)
|
|
293
|
+
if 200 <= http_status < 300:
|
|
294
|
+
return http_status, payload
|
|
295
|
+
can_retry = index < max_attempts - 1 and _is_retryable_broadcast_failure(http_status, payload)
|
|
296
|
+
if not can_retry:
|
|
297
|
+
raise RuntimeError(
|
|
298
|
+
f"Aleph bootstrap status broadcast failed: {http_status} {_json_dumps(payload)}"
|
|
299
|
+
)
|
|
300
|
+
raise RuntimeError("Aleph bootstrap status broadcast failed: retry budget exhausted")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _publish_vm_bootstrap_config_signal(record: dict) -> None:
|
|
304
|
+
env_values = _parse_env_file(ENV_FILE)
|
|
305
|
+
publisher_private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
|
|
306
|
+
if not publisher_private_key:
|
|
307
|
+
raise RuntimeError("missing ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY")
|
|
308
|
+
|
|
309
|
+
owner_authorization_b64 = env_values.get("ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64", "").strip()
|
|
310
|
+
owner_authorization = None
|
|
311
|
+
if owner_authorization_b64:
|
|
312
|
+
decoded = base64.b64decode(owner_authorization_b64).decode("utf-8")
|
|
313
|
+
parsed = json.loads(decoded)
|
|
314
|
+
if isinstance(parsed, dict):
|
|
315
|
+
owner_authorization = parsed
|
|
316
|
+
|
|
317
|
+
owner_address = ""
|
|
318
|
+
if isinstance(owner_authorization, dict):
|
|
319
|
+
payload = owner_authorization.get("payload")
|
|
320
|
+
if isinstance(payload, dict):
|
|
321
|
+
owner_address = str(payload.get("ownerAddress") or "").strip()
|
|
322
|
+
if not owner_address:
|
|
323
|
+
owner_address = str(record.get("ownerAddress") or "").strip()
|
|
324
|
+
if not owner_address:
|
|
325
|
+
raise RuntimeError("missing ownerAddress for bootstrap config signal")
|
|
326
|
+
|
|
327
|
+
deployment_token = str(record.get("deploymentToken") or "").strip()
|
|
328
|
+
profile = str(record.get("profile") or "").strip()
|
|
329
|
+
instance_item_hash = str(record.get("instanceItemHash") or "").strip()
|
|
330
|
+
if not deployment_token or not profile or not instance_item_hash:
|
|
331
|
+
raise RuntimeError("bootstrap config signal is missing deployment metadata")
|
|
332
|
+
|
|
333
|
+
metadata, metadata_error = _fresh_metadata_payload()
|
|
334
|
+
if not metadata:
|
|
335
|
+
raise RuntimeError(
|
|
336
|
+
f"bootstrap config signal metadata is not ready: {metadata_error or 'metadata unavailable'}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
peer_id = metadata.get("peer_id")
|
|
340
|
+
probe_multiaddrs = metadata.get("probe_multiaddrs")
|
|
341
|
+
browser_bootstrap_multiaddrs = metadata.get("browser_bootstrap_multiaddrs")
|
|
342
|
+
if not isinstance(peer_id, str) or not peer_id.strip():
|
|
343
|
+
raise RuntimeError("bootstrap config signal is missing peer_id")
|
|
344
|
+
if not isinstance(probe_multiaddrs, list) or not any(
|
|
345
|
+
isinstance(entry, str) and entry.strip() for entry in probe_multiaddrs
|
|
346
|
+
):
|
|
347
|
+
raise RuntimeError("bootstrap config signal is missing probe_multiaddrs")
|
|
348
|
+
if not isinstance(browser_bootstrap_multiaddrs, list):
|
|
349
|
+
browser_bootstrap_multiaddrs = []
|
|
350
|
+
|
|
351
|
+
publisher_address = _address_from_private_key(publisher_private_key)
|
|
352
|
+
now_ms = int(time.time() * 1000)
|
|
353
|
+
content = {
|
|
354
|
+
"type": BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE,
|
|
355
|
+
"address": publisher_address,
|
|
356
|
+
"ref": BOOTSTRAP_CONFIG_SIGNAL_REF,
|
|
357
|
+
"content": {
|
|
358
|
+
"deploymentToken": deployment_token,
|
|
359
|
+
"status": "applied",
|
|
360
|
+
"profile": profile,
|
|
361
|
+
"ownerAddress": owner_address,
|
|
362
|
+
"instanceItemHash": instance_item_hash,
|
|
363
|
+
"updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now_ms / 1000)),
|
|
364
|
+
"publisherAddress": publisher_address,
|
|
365
|
+
"authorization": owner_authorization,
|
|
366
|
+
"peerId": peer_id.strip(),
|
|
367
|
+
"probeMultiaddrs": [
|
|
368
|
+
entry.strip()
|
|
369
|
+
for entry in probe_multiaddrs
|
|
370
|
+
if isinstance(entry, str) and entry.strip()
|
|
371
|
+
],
|
|
372
|
+
"browserBootstrapMultiaddrs": [
|
|
373
|
+
entry.strip()
|
|
374
|
+
for entry in browser_bootstrap_multiaddrs
|
|
375
|
+
if isinstance(entry, str) and entry.strip()
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
"time": now_ms,
|
|
379
|
+
}
|
|
380
|
+
item_content = _json_dumps(content)
|
|
381
|
+
unsigned_message = {
|
|
382
|
+
"channel": BOOTSTRAP_CHANNEL,
|
|
383
|
+
"sender": publisher_address,
|
|
384
|
+
"chain": "ETH",
|
|
385
|
+
"type": "POST",
|
|
386
|
+
"time": now_ms / 1000,
|
|
387
|
+
"item_type": "inline",
|
|
388
|
+
"item_content": item_content,
|
|
389
|
+
"item_hash": hashlib.sha256(item_content.encode("utf-8")).hexdigest(),
|
|
390
|
+
}
|
|
391
|
+
signed_message = dict(unsigned_message)
|
|
392
|
+
signed_message["signature"] = _sign_personal_message(
|
|
393
|
+
publisher_private_key,
|
|
394
|
+
_signature_payload(
|
|
395
|
+
str(unsigned_message["chain"]),
|
|
396
|
+
str(unsigned_message["sender"]),
|
|
397
|
+
str(unsigned_message["type"]),
|
|
398
|
+
str(unsigned_message["item_hash"]),
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
_broadcast_aleph_message(ALEPH_API_HOST, signed_message)
|
|
402
|
+
|
|
403
|
+
|
|
159
404
|
def _load_vm_bootstrap_record() -> tuple[str | None, str | None, dict | None]:
|
|
160
405
|
owner_address, deployment_token = _extract_guest_bootstrap_locator()
|
|
161
406
|
if not owner_address or not deployment_token:
|
|
@@ -197,7 +442,7 @@ def _poll_bootstrap_config_loop() -> None:
|
|
|
197
442
|
except ValueError:
|
|
198
443
|
time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
|
|
199
444
|
continue
|
|
200
|
-
ok, _ = _run_configure_process(args)
|
|
445
|
+
ok, _ = _run_configure_process(args, record)
|
|
201
446
|
if ok:
|
|
202
447
|
return
|
|
203
448
|
time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
|
|
@@ -274,6 +519,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
274
519
|
"bootstrap_publisher_libp2p_identity_b64"
|
|
275
520
|
)
|
|
276
521
|
bootstrap_owner_private_key = payload.get("bootstrap_owner_private_key")
|
|
522
|
+
bootstrap_owner_address = payload.get("bootstrap_owner_address")
|
|
277
523
|
bootstrap_owner_authorization_b64 = payload.get("bootstrap_owner_authorization_b64")
|
|
278
524
|
bootstrap_registration_id = payload.get("bootstrap_registration_id")
|
|
279
525
|
no_start = bool(payload.get("no_start"))
|
|
@@ -306,6 +552,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
306
552
|
)
|
|
307
553
|
if bootstrap_owner_private_key is not None:
|
|
308
554
|
args.extend(["--bootstrap-owner-private-key", str(bootstrap_owner_private_key)])
|
|
555
|
+
if bootstrap_owner_address is not None:
|
|
556
|
+
args.extend(["--bootstrap-owner-address", str(bootstrap_owner_address)])
|
|
309
557
|
if bootstrap_owner_authorization_b64 is not None:
|
|
310
558
|
args.extend(
|
|
311
559
|
["--bootstrap-owner-authorization-b64", str(bootstrap_owner_authorization_b64)]
|