@le-space/rootfs 0.3.18 → 0.3.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/rootfs",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -86,11 +86,13 @@ virt-customize \
86
86
  --copy-in "${APP_BINARY}:${GUEST_APP_DIR}" \
87
87
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap.sh:/usr/local/sbin" \
88
88
  --copy-in "${SCRIPT_DIR}/uc-go-peer-configure.sh:/usr/local/sbin" \
89
+ --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-deregister.py:/usr/local/sbin" \
89
90
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-refresh.py:/usr/local/sbin" \
90
91
  --copy-in "${SCRIPT_DIR}/uc-go-peer-autotls-refresh.py:/usr/local/sbin" \
91
92
  --copy-in "${SCRIPT_DIR}/uc-go-peer-describe.py:/usr/local/sbin" \
92
93
  --copy-in "${SCRIPT_DIR}/uc-go-peer-setup-server.py:/usr/local/sbin" \
93
94
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap.service:/etc/systemd/system" \
95
+ --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-deregister.service:/etc/systemd/system" \
94
96
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-refresh.service:/etc/systemd/system" \
95
97
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-refresh.timer:/etc/systemd/system" \
96
98
  --copy-in "${SCRIPT_DIR}/uc-go-peer-autotls-refresh.service:/etc/systemd/system" \
@@ -98,6 +100,7 @@ virt-customize \
98
100
  --run-command "chmod 0755 ${ROOTFS_CONTRACT_BINARY_PATH}" \
99
101
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-bootstrap.sh" \
100
102
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-configure.sh" \
103
+ --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-bootstrap-deregister.py" \
101
104
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-bootstrap-refresh.py" \
102
105
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-autotls-refresh.py" \
103
106
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-describe.py" \
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env python3
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+
12
+ try:
13
+ from eth_account import Account
14
+ from eth_account.messages import encode_defunct
15
+ except ImportError as error: # pragma: no cover - runtime dependency
16
+ raise SystemExit(
17
+ "eth-account is required for guest-side bootstrap deregistration"
18
+ ) from error
19
+
20
+
21
+ ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/uc-go-peer")
22
+ DESCRIBE_SCRIPT = os.environ.get("DESCRIBE_SCRIPT", "/usr/local/sbin/uc-go-peer-describe.py")
23
+ DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api2.aleph.im")
24
+ DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
25
+ DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
26
+ DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", "relay-bootstrap")
27
+ MAX_PREVIOUS_PAGES = int(os.environ.get("ALEPH_BOOTSTRAP_MAX_PREVIOUS_PAGES", "5"))
28
+ PAGINATION = int(os.environ.get("ALEPH_BOOTSTRAP_PAGINATION", "50"))
29
+
30
+
31
+ def parse_env_file(path: str) -> dict[str, str]:
32
+ values: dict[str, str] = {}
33
+ if not os.path.exists(path):
34
+ return values
35
+
36
+ with open(path, encoding="utf-8") as handle:
37
+ for line in handle:
38
+ stripped = line.strip()
39
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
40
+ continue
41
+ key, value = stripped.split("=", 1)
42
+ values[key.strip()] = value.strip()
43
+ return values
44
+
45
+
46
+ def json_dumps(payload: object) -> str:
47
+ return json.dumps(payload, separators=(",", ":"))
48
+
49
+
50
+ def dedupe(values: list[str]) -> list[str]:
51
+ seen: set[str] = set()
52
+ result: list[str] = []
53
+ for value in values:
54
+ if value and value not in seen:
55
+ seen.add(value)
56
+ result.append(value)
57
+ return result
58
+
59
+
60
+ def sign_personal_message(private_key: str, payload: str) -> str:
61
+ message = encode_defunct(text=payload)
62
+ signed = Account.sign_message(message, private_key=private_key)
63
+ return signed.signature.hex()
64
+
65
+
66
+ def address_from_private_key(private_key: str) -> str:
67
+ return Account.from_key(private_key).address
68
+
69
+
70
+ def signature_payload(chain: str, sender: str, message_type: str, item_hash: str) -> str:
71
+ return "\n".join([chain, sender, message_type, item_hash])
72
+
73
+
74
+ def post_json(url: str, body: dict[str, object]) -> tuple[int, dict]:
75
+ data = json_dumps(body).encode("utf-8")
76
+ request = urllib.request.Request(
77
+ url,
78
+ data=data,
79
+ headers={"content-type": "application/json"},
80
+ method="POST",
81
+ )
82
+ try:
83
+ with urllib.request.urlopen(request, timeout=30) as response:
84
+ payload = response.read().decode("utf-8")
85
+ return response.status, json.loads(payload or "{}")
86
+ except urllib.error.HTTPError as error:
87
+ payload = error.read().decode("utf-8")
88
+ try:
89
+ return error.code, json.loads(payload or "{}")
90
+ except json.JSONDecodeError:
91
+ return error.code, {"details": payload}
92
+
93
+
94
+ def get_json(url: str) -> dict:
95
+ with urllib.request.urlopen(url, timeout=30) as response:
96
+ return json.loads(response.read().decode("utf-8") or "{}")
97
+
98
+
99
+ def sign_aleph_message(unsigned_message: dict[str, object], private_key: str) -> dict[str, object]:
100
+ signed = dict(unsigned_message)
101
+ signed["signature"] = sign_personal_message(
102
+ private_key,
103
+ signature_payload(
104
+ str(unsigned_message["chain"]),
105
+ str(unsigned_message["sender"]),
106
+ str(unsigned_message["type"]),
107
+ str(unsigned_message["item_hash"]),
108
+ ),
109
+ )
110
+ return signed
111
+
112
+
113
+ def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, dict]:
114
+ url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
115
+ attempts = [
116
+ {"sync": True, "message": message},
117
+ {**message, "sync": True},
118
+ dict(message),
119
+ ]
120
+ last: tuple[int, dict] | None = None
121
+ for attempt in attempts:
122
+ last = post_json(url, attempt)
123
+ if 200 <= last[0] < 300:
124
+ return last
125
+ if last is None:
126
+ raise RuntimeError("Aleph broadcast failed before any attempt was made")
127
+ raise RuntimeError(f"Aleph broadcast failed: {last[0]} {json_dumps(last[1])}")
128
+
129
+
130
+ def parse_post_record(entry: object) -> dict[str, object] | None:
131
+ if not isinstance(entry, dict):
132
+ return None
133
+ item_hash = entry.get("item_hash") or entry.get("hash")
134
+ if not isinstance(item_hash, str) or not item_hash:
135
+ return None
136
+
137
+ sender = entry.get("address") or entry.get("sender")
138
+ item_content = entry.get("item_content")
139
+ if isinstance(item_content, str):
140
+ try:
141
+ item_content = json.loads(item_content)
142
+ except json.JSONDecodeError:
143
+ item_content = None
144
+
145
+ if not isinstance(item_content, dict):
146
+ return None
147
+
148
+ content = item_content.get("content")
149
+ if not isinstance(content, dict):
150
+ return None
151
+
152
+ return {
153
+ "item_hash": item_hash,
154
+ "sender": sender,
155
+ "registration_id": content.get("registrationId"),
156
+ "peer_id": content.get("peerId"),
157
+ }
158
+
159
+
160
+ def load_peer_id() -> str | None:
161
+ describe = subprocess.run([DESCRIBE_SCRIPT], check=False, capture_output=True, text=True)
162
+ if describe.returncode != 0:
163
+ return None
164
+ try:
165
+ metadata = json.loads(describe.stdout.strip() or "{}")
166
+ except json.JSONDecodeError:
167
+ return None
168
+ peer_id = metadata.get("peer_id")
169
+ if isinstance(peer_id, str) and peer_id.strip():
170
+ return peer_id.strip()
171
+ return None
172
+
173
+
174
+ def fetch_current_hashes(
175
+ api_host: str,
176
+ channel: str,
177
+ ref: str,
178
+ post_type: str,
179
+ sender: str,
180
+ registration_id: str | None,
181
+ peer_id: str | None,
182
+ ) -> list[str]:
183
+ found: list[str] = []
184
+ for page in range(1, MAX_PREVIOUS_PAGES + 1):
185
+ url = (
186
+ f"{api_host.rstrip('/')}/api/v0/posts.json?"
187
+ f"channels={urllib.parse.quote(channel)}&"
188
+ f"refs={urllib.parse.quote(ref)}&"
189
+ f"types={urllib.parse.quote(post_type)}&"
190
+ f"pagination={PAGINATION}&page={page}"
191
+ )
192
+ payload = get_json(url)
193
+ posts = payload.get("posts")
194
+ if not isinstance(posts, list):
195
+ break
196
+
197
+ for entry in posts:
198
+ parsed = parse_post_record(entry)
199
+ if parsed is None:
200
+ continue
201
+ if str(parsed["sender"]).lower() != sender.lower():
202
+ continue
203
+ if registration_id and parsed["registration_id"] == registration_id:
204
+ found.append(str(parsed["item_hash"]))
205
+ continue
206
+ if peer_id and parsed["peer_id"] == peer_id:
207
+ found.append(str(parsed["item_hash"]))
208
+
209
+ if len(posts) < PAGINATION:
210
+ break
211
+
212
+ return dedupe(found)
213
+
214
+
215
+ def broadcast_forget(
216
+ api_host: str,
217
+ sender: str,
218
+ private_key: str,
219
+ hashes: list[str],
220
+ channel: str,
221
+ ) -> tuple[int, dict] | None:
222
+ if not hashes:
223
+ return None
224
+
225
+ now_seconds = time.time()
226
+ item_content = json_dumps(
227
+ {
228
+ "address": sender,
229
+ "time": now_seconds,
230
+ "hashes": hashes,
231
+ "aggregates": [],
232
+ "reason": f"Deregister relay bootstrap records for {sender}",
233
+ }
234
+ )
235
+ unsigned = {
236
+ "sender": sender,
237
+ "chain": "ETH",
238
+ "type": "FORGET",
239
+ "item_hash": hashlib.sha256(item_content.encode("utf-8")).hexdigest(),
240
+ "item_type": "inline",
241
+ "item_content": item_content,
242
+ "time": now_seconds,
243
+ "channel": channel,
244
+ }
245
+ return broadcast_aleph_message(api_host, sign_aleph_message(unsigned, private_key))
246
+
247
+
248
+ def main() -> None:
249
+ env_values = parse_env_file(ENV_FILE)
250
+ publisher_private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
251
+ registration_id = env_values.get("ALEPH_BOOTSTRAP_REGISTRATION_ID", "").strip() or None
252
+ if not publisher_private_key:
253
+ print(json_dumps({"status": "skipped", "reason": "missing publisher key"}))
254
+ return
255
+
256
+ publisher_address = address_from_private_key(publisher_private_key)
257
+ peer_id = load_peer_id()
258
+ channel = env_values.get("ALEPH_BOOTSTRAP_CHANNEL", DEFAULT_CHANNEL).strip() or DEFAULT_CHANNEL
259
+ ref = env_values.get("ALEPH_BOOTSTRAP_REF", DEFAULT_REF).strip() or DEFAULT_REF
260
+ post_type = env_values.get("ALEPH_BOOTSTRAP_POST_TYPE", DEFAULT_POST_TYPE).strip() or DEFAULT_POST_TYPE
261
+ api_host = env_values.get("ALEPH_BOOTSTRAP_API_HOST", DEFAULT_API_HOST).strip() or DEFAULT_API_HOST
262
+
263
+ hashes = fetch_current_hashes(
264
+ api_host,
265
+ channel,
266
+ ref,
267
+ post_type,
268
+ publisher_address,
269
+ registration_id,
270
+ peer_id,
271
+ )
272
+ if not hashes:
273
+ print(json_dumps({"status": "skipped", "reason": "no matching bootstrap records"}))
274
+ return
275
+
276
+ http_status, response = broadcast_forget(
277
+ api_host, publisher_address, publisher_private_key, hashes, channel
278
+ ) or (0, {})
279
+ print(
280
+ json_dumps(
281
+ {
282
+ "status": "forgotten",
283
+ "httpStatus": http_status,
284
+ "sender": publisher_address,
285
+ "registrationId": registration_id,
286
+ "peerId": peer_id,
287
+ "forgottenHashes": hashes,
288
+ "response": response,
289
+ }
290
+ )
291
+ )
292
+
293
+
294
+ if __name__ == "__main__":
295
+ try:
296
+ main()
297
+ except Exception as error: # pragma: no cover - runtime error path
298
+ print(json_dumps({"status": "error", "error": str(error)}), file=sys.stderr)
299
+ raise
@@ -0,0 +1,19 @@
1
+ [Unit]
2
+ Description=Best-effort Aleph bootstrap deregistration on shutdown for uc-go-peer
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+ ConditionPathExists=/etc/default/uc-go-peer.ready
6
+
7
+ [Service]
8
+ Type=oneshot
9
+ RemainAfterExit=yes
10
+ Environment=ENV_FILE=/etc/default/uc-go-peer
11
+ Environment=DESCRIBE_SCRIPT=/usr/local/sbin/uc-go-peer-describe.py
12
+ ExecStart=/bin/true
13
+ ExecStop=/usr/bin/python3 /usr/local/sbin/uc-go-peer-bootstrap-deregister.py
14
+ StandardOutput=journal
15
+ StandardError=journal
16
+ TimeoutStopSec=25
17
+
18
+ [Install]
19
+ WantedBy=multi-user.target
@@ -466,9 +466,6 @@ def main() -> None:
466
466
  version,
467
467
  now_ms,
468
468
  )
469
- if owner_authorization is None:
470
- print(json_dumps({"status": "skipped", "reason": "missing owner authorization"}))
471
- return
472
469
 
473
470
  proof_payload = relay_proof_payload(
474
471
  peer_id,
@@ -529,6 +526,7 @@ def main() -> None:
529
526
  "publishedMultiaddrs": multiaddrs,
530
527
  "publishedBrowserMultiaddrs": browser_multiaddrs,
531
528
  "forgottenHashes": previous_hashes,
529
+ "ownerAuthorizationPresent": owner_authorization is not None,
532
530
  "forgetResponse": forget_response[1] if forget_response else None,
533
531
  "response": response,
534
532
  }
@@ -11,6 +11,7 @@ SERVICE_NAME="${SERVICE_NAME:-uc-go-peer.service}"
11
11
  AUTOTLS_REFRESH_SERVICE="${AUTOTLS_REFRESH_SERVICE:-uc-go-peer-autotls-refresh.service}"
12
12
  BOOTSTRAP_REFRESH_TIMER="${BOOTSTRAP_REFRESH_TIMER:-uc-go-peer-bootstrap-refresh.timer}"
13
13
  BOOTSTRAP_REFRESH_SERVICE="${BOOTSTRAP_REFRESH_SERVICE:-uc-go-peer-bootstrap-refresh.service}"
14
+ BOOTSTRAP_DEREGISTER_SERVICE="${BOOTSTRAP_DEREGISTER_SERVICE:-uc-go-peer-bootstrap-deregister.service}"
14
15
  CADDY_SERVICE="${CADDY_SERVICE:-caddy.service}"
15
16
  CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}"
16
17
  BOOTSTRAP_SERVICE="${BOOTSTRAP_SERVICE:-uc-go-peer-bootstrap.service}"
@@ -97,6 +98,27 @@ current_identity_path() {
97
98
  printf '/var/lib/uc-go-peer/identity.key\n'
98
99
  }
99
100
 
101
+ current_bootstrap_publisher_private_key() {
102
+ local existing
103
+ existing="$(grep -E '^[#[:space:]]*ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY=' "${ENV_FILE}" 2>/dev/null | tail -n1 | cut -d= -f2- || true)"
104
+ if [ -n "${existing}" ]; then
105
+ printf '%s\n' "${existing}"
106
+ return
107
+ fi
108
+ printf '\n'
109
+ }
110
+
111
+ generate_bootstrap_publisher_private_key() {
112
+ while true; do
113
+ local candidate
114
+ candidate="$(openssl rand -hex 32)"
115
+ if [ -n "${candidate}" ] && [ "${candidate}" != "0000000000000000000000000000000000000000000000000000000000000000" ]; then
116
+ printf '0x%s\n' "${candidate}"
117
+ return
118
+ fi
119
+ done
120
+ }
121
+
100
122
  while [ "$#" -gt 0 ]; do
101
123
  case "$1" in
102
124
  --public-ipv4)
@@ -179,6 +201,13 @@ fi
179
201
  touch "${ENV_FILE}"
180
202
  rm -f "${AUTOTLS_READY_FILE}" "${AUTOTLS_ZONE_FILE}" "${AUTOTLS_HOSTS_FILE}" "${AUTOTLS_CADDY_READY_FILE}"
181
203
 
204
+ if [ -z "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
205
+ BOOTSTRAP_PUBLISHER_PRIVATE_KEY="$(current_bootstrap_publisher_private_key)"
206
+ fi
207
+ if [ -z "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
208
+ BOOTSTRAP_PUBLISHER_PRIVATE_KEY="$(generate_bootstrap_publisher_private_key)"
209
+ fi
210
+
182
211
  announce=(
183
212
  "/ip4/${PUBLIC_IPV4}/tcp/${TCP_PORT}"
184
213
  )
@@ -234,6 +263,7 @@ if [ -n "${UDP_PORT}" ]; then
234
263
  fi
235
264
  if [ -n "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
236
265
  write_env_var "ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY" "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}"
266
+ write_env_var "GO_PEER_IDENTITY_EVM_PRIVATE_KEY" "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}"
237
267
  fi
238
268
  if [ -n "${BOOTSTRAP_PUBLISHER_LIBP2P_IDENTITY_B64}" ]; then
239
269
  identity_path="$(current_identity_path)"
@@ -261,6 +291,8 @@ if [ "${START_SERVICE}" -eq 1 ]; then
261
291
  systemctl restart "${SERVICE_NAME}"
262
292
  systemctl enable "${AUTOTLS_REFRESH_SERVICE}"
263
293
  systemctl enable "${BOOTSTRAP_REFRESH_TIMER}"
294
+ systemctl enable "${BOOTSTRAP_DEREGISTER_SERVICE}"
295
+ systemctl restart "${BOOTSTRAP_DEREGISTER_SERVICE}"
264
296
  systemctl start "${BOOTSTRAP_REFRESH_TIMER}"
265
297
  systemctl restart --no-block "${BOOTSTRAP_REFRESH_SERVICE}" || true
266
298
  if [ -n "${PROXY_HOSTNAME}" ]; then