@le-space/rootfs 0.3.0 → 0.3.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/build-rootfs-image.sh +10 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-bootstrap-refresh.py +546 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-bootstrap-refresh.service +13 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-bootstrap-refresh.timer +12 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-bootstrap.sh +3 -1
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-configure.sh +49 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-setup-server.py +27 -0
- package/reference/uc-go-peer/rootfs/build-rootfs-image.sh +4 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap-refresh.py +544 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap-refresh.service +13 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap-refresh.timer +12 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap.sh +2 -1
- package/reference/uc-go-peer/rootfs/uc-go-peer-configure.sh +63 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-setup-server.py +27 -0
- package/le-space-rootfs-0.3.0.tgz +0 -0
package/package.json
CHANGED
|
@@ -11,6 +11,8 @@ PY_LIBP2P_DIR="${PY_LIBP2P_DIR:-${REPO_DIR}/py-libp2p}"
|
|
|
11
11
|
ORBITDB_RELAY_PINNER_DIR="${ORBITDB_RELAY_PINNER_DIR:-}"
|
|
12
12
|
UNIVERSAL_CONNECTIVITY_DIR="${UNIVERSAL_CONNECTIVITY_DIR:-}"
|
|
13
13
|
OUT_DIR="${OUT_DIR:-${APP_DIR}/dist-rootfs}"
|
|
14
|
+
HOST_UID="${HOST_UID:-}"
|
|
15
|
+
HOST_GID="${HOST_GID:-}"
|
|
14
16
|
BASE_URL="${BASE_URL:-https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2}"
|
|
15
17
|
BASE_IMAGE="${OUT_DIR}/debian-12-genericcloud-amd64.qcow2"
|
|
16
18
|
IMAGE_SIZE="${IMAGE_SIZE:-${ROOTFS_IMAGE_SIZE:-20G}}"
|
|
@@ -224,15 +226,19 @@ case "${ROOTFS_PROFILE}" in
|
|
|
224
226
|
--copy-in "${APP_TAR}:/opt"
|
|
225
227
|
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap.sh:/usr/local/sbin"
|
|
226
228
|
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-configure.sh:/usr/local/sbin"
|
|
229
|
+
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap-refresh.py:/usr/local/sbin"
|
|
227
230
|
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-autotls-refresh.py:/usr/local/sbin"
|
|
228
231
|
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-setup-server.py:/usr/local/sbin"
|
|
229
232
|
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-describe.py:/usr/local/sbin"
|
|
230
233
|
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap.service:/etc/systemd/system"
|
|
234
|
+
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap-refresh.service:/etc/systemd/system"
|
|
235
|
+
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap-refresh.timer:/etc/systemd/system"
|
|
231
236
|
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-autotls-refresh.service:/etc/systemd/system"
|
|
232
237
|
--copy-in "${SCRIPT_DIR}/orbitdb-relay-pinner-bootstrap.conf:/etc/systemd/system/orbitdb-relay-pinner.service.d"
|
|
233
238
|
--run-command "tar -xf /opt/$(basename "${APP_TAR}") -C /opt/orbitdb-relay-pinner"
|
|
234
239
|
--run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-bootstrap.sh"
|
|
235
240
|
--run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-configure.sh"
|
|
241
|
+
--run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-bootstrap-refresh.py"
|
|
236
242
|
--run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-autotls-refresh.py"
|
|
237
243
|
--run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-setup-server.py"
|
|
238
244
|
--run-command "chmod 0755 /usr/local/sbin/orbitdb-relay-pinner-describe.py"
|
|
@@ -333,3 +339,7 @@ if [ "${ROOTFS_SPARSIFY}" = "1" ] && command -v virt-sparsify >/dev/null 2>&1; t
|
|
|
333
339
|
fi
|
|
334
340
|
|
|
335
341
|
echo "Rootfs image ready at ${IMAGE}"
|
|
342
|
+
|
|
343
|
+
if [ -n "${HOST_UID}" ] && [ -n "${HOST_GID}" ]; then
|
|
344
|
+
chown -R "${HOST_UID}:${HOST_GID}" "${OUT_DIR}"
|
|
345
|
+
fi
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
import ipaddress
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from eth_account import Account
|
|
16
|
+
from eth_account.messages import encode_defunct
|
|
17
|
+
except ImportError as error: # pragma: no cover - runtime dependency
|
|
18
|
+
raise SystemExit(
|
|
19
|
+
"eth-account is required for guest-side bootstrap refresh publishing"
|
|
20
|
+
) from error
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/orbitdb-relay-pinner")
|
|
24
|
+
DESCRIBE_SCRIPT = os.environ.get(
|
|
25
|
+
"DESCRIBE_SCRIPT", "/usr/local/sbin/orbitdb-relay-pinner-describe.py"
|
|
26
|
+
)
|
|
27
|
+
DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api2.aleph.im")
|
|
28
|
+
DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
|
|
29
|
+
DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
|
|
30
|
+
DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", "relay-bootstrap")
|
|
31
|
+
DEFAULT_PROFILE = os.environ.get("ALEPH_BOOTSTRAP_PROFILE", "orbitdb-relay-pinner")
|
|
32
|
+
MAX_PREVIOUS_PAGES = int(os.environ.get("ALEPH_BOOTSTRAP_MAX_PREVIOUS_PAGES", "5"))
|
|
33
|
+
PAGINATION = int(os.environ.get("ALEPH_BOOTSTRAP_PAGINATION", "50"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_env_file(path: str) -> dict[str, str]:
|
|
37
|
+
values: dict[str, str] = {}
|
|
38
|
+
if not os.path.exists(path):
|
|
39
|
+
return values
|
|
40
|
+
|
|
41
|
+
with open(path, encoding="utf-8") as handle:
|
|
42
|
+
for line in handle:
|
|
43
|
+
stripped = line.strip()
|
|
44
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
45
|
+
continue
|
|
46
|
+
key, value = stripped.split("=", 1)
|
|
47
|
+
values[key.strip()] = value.strip()
|
|
48
|
+
return values
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def dedupe(values: list[str]) -> list[str]:
|
|
52
|
+
seen: set[str] = set()
|
|
53
|
+
result: list[str] = []
|
|
54
|
+
for value in values:
|
|
55
|
+
if value and value not in seen:
|
|
56
|
+
seen.add(value)
|
|
57
|
+
result.append(value)
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def split_multiaddr(addr: str) -> list[str]:
|
|
62
|
+
return [part for part in addr.split("/") if part]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def has_peer_id(addr: str) -> bool:
|
|
66
|
+
return "p2p" in split_multiaddr(addr)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_browser_dialable(addr: str) -> bool:
|
|
70
|
+
normalized = addr.lower()
|
|
71
|
+
return any(
|
|
72
|
+
token in normalized
|
|
73
|
+
for token in ("/ws", "/wss", "/webtransport", "/webrtc-direct")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_public_multiaddr(addr: str) -> bool:
|
|
78
|
+
parts = split_multiaddr(addr)
|
|
79
|
+
if not parts:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
for index, token in enumerate(parts[:-1]):
|
|
84
|
+
value = parts[index + 1]
|
|
85
|
+
if token == "ip4" and ipaddress.ip_address(value).is_private:
|
|
86
|
+
return False
|
|
87
|
+
if token == "ip6":
|
|
88
|
+
ip = ipaddress.ip_address(value)
|
|
89
|
+
if ip.is_loopback or ip.is_private or ip.is_link_local:
|
|
90
|
+
return False
|
|
91
|
+
if token in ("dns", "dns4", "dns6"):
|
|
92
|
+
host = value.strip().lower()
|
|
93
|
+
if host == "localhost" or host.endswith(".localhost") or host.endswith(".local"):
|
|
94
|
+
return False
|
|
95
|
+
except ValueError:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
return has_peer_id(addr)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def filter_public_multiaddrs(values: list[str], browser_only: bool = False) -> list[str]:
|
|
102
|
+
filtered: list[str] = []
|
|
103
|
+
for value in values:
|
|
104
|
+
if not isinstance(value, str):
|
|
105
|
+
continue
|
|
106
|
+
candidate = value.strip()
|
|
107
|
+
if not candidate or not is_public_multiaddr(candidate):
|
|
108
|
+
continue
|
|
109
|
+
if browser_only and not is_browser_dialable(candidate):
|
|
110
|
+
continue
|
|
111
|
+
filtered.append(candidate)
|
|
112
|
+
return dedupe(filtered)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def json_dumps(payload: object) -> str:
|
|
116
|
+
return json.dumps(payload, separators=(",", ":"))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def sign_personal_message(private_key: str, payload: str) -> str:
|
|
120
|
+
message = encode_defunct(text=payload)
|
|
121
|
+
signed = Account.sign_message(message, private_key=private_key)
|
|
122
|
+
return signed.signature.hex()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def address_from_private_key(private_key: str) -> str:
|
|
126
|
+
return Account.from_key(private_key).address
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def signature_payload(chain: str, sender: str, message_type: str, item_hash: str) -> str:
|
|
130
|
+
return "\n".join([chain, sender, message_type, item_hash])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def relay_authorization_payload(
|
|
134
|
+
owner_address: str,
|
|
135
|
+
publisher_address: str,
|
|
136
|
+
peer_id: str,
|
|
137
|
+
registration_id: str | None,
|
|
138
|
+
profile: str | None,
|
|
139
|
+
version: str | None,
|
|
140
|
+
issued_at: int,
|
|
141
|
+
) -> dict[str, object]:
|
|
142
|
+
return {
|
|
143
|
+
"ownerAddress": owner_address,
|
|
144
|
+
"publisherAddress": publisher_address,
|
|
145
|
+
"peerId": peer_id,
|
|
146
|
+
"registrationId": registration_id,
|
|
147
|
+
"profile": profile,
|
|
148
|
+
"version": version,
|
|
149
|
+
"instanceItemHash": None,
|
|
150
|
+
"issuedAt": issued_at,
|
|
151
|
+
"expiresAt": None,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def relay_proof_payload(
|
|
156
|
+
peer_id: str,
|
|
157
|
+
multiaddrs: list[str],
|
|
158
|
+
browser_multiaddrs: list[str],
|
|
159
|
+
registration_id: str | None,
|
|
160
|
+
profile: str | None,
|
|
161
|
+
version: str | None,
|
|
162
|
+
updated_at: int,
|
|
163
|
+
) -> dict[str, object]:
|
|
164
|
+
payload: dict[str, object] = {
|
|
165
|
+
"peerId": peer_id,
|
|
166
|
+
"multiaddrs": dedupe(multiaddrs),
|
|
167
|
+
"registrationId": registration_id,
|
|
168
|
+
"profile": profile,
|
|
169
|
+
"version": version,
|
|
170
|
+
"updatedAt": updated_at,
|
|
171
|
+
}
|
|
172
|
+
if browser_multiaddrs:
|
|
173
|
+
payload["browserMultiaddrs"] = dedupe(browser_multiaddrs)
|
|
174
|
+
return payload
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def load_owner_authorization(
|
|
178
|
+
env_values: dict[str, str],
|
|
179
|
+
publisher_address: str,
|
|
180
|
+
peer_id: str,
|
|
181
|
+
registration_id: str | None,
|
|
182
|
+
profile: str | None,
|
|
183
|
+
version: str | None,
|
|
184
|
+
issued_at: int,
|
|
185
|
+
) -> dict[str, object] | None:
|
|
186
|
+
encoded = env_values.get("ALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64", "").strip()
|
|
187
|
+
if encoded:
|
|
188
|
+
decoded = base64.b64decode(encoded).decode("utf-8")
|
|
189
|
+
payload = json.loads(decoded)
|
|
190
|
+
if isinstance(payload, dict):
|
|
191
|
+
return payload
|
|
192
|
+
|
|
193
|
+
owner_private_key = env_values.get("ALEPH_BOOTSTRAP_OWNER_PRIVATE_KEY", "").strip()
|
|
194
|
+
if not owner_private_key:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
owner_address = address_from_private_key(owner_private_key)
|
|
198
|
+
payload = relay_authorization_payload(
|
|
199
|
+
owner_address,
|
|
200
|
+
publisher_address,
|
|
201
|
+
peer_id,
|
|
202
|
+
registration_id,
|
|
203
|
+
profile,
|
|
204
|
+
version,
|
|
205
|
+
issued_at,
|
|
206
|
+
)
|
|
207
|
+
return {
|
|
208
|
+
"scheme": "personal_sign",
|
|
209
|
+
"payload": payload,
|
|
210
|
+
"signature": sign_personal_message(owner_private_key, json_dumps(payload)),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def post_json(url: str, body: dict[str, object]) -> tuple[int, dict]:
|
|
215
|
+
data = json_dumps(body).encode("utf-8")
|
|
216
|
+
request = urllib.request.Request(
|
|
217
|
+
url,
|
|
218
|
+
data=data,
|
|
219
|
+
headers={"content-type": "application/json"},
|
|
220
|
+
method="POST",
|
|
221
|
+
)
|
|
222
|
+
try:
|
|
223
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
224
|
+
payload = response.read().decode("utf-8")
|
|
225
|
+
return response.status, json.loads(payload or "{}")
|
|
226
|
+
except urllib.error.HTTPError as error:
|
|
227
|
+
payload = error.read().decode("utf-8")
|
|
228
|
+
try:
|
|
229
|
+
return error.code, json.loads(payload or "{}")
|
|
230
|
+
except json.JSONDecodeError:
|
|
231
|
+
return error.code, {"details": payload}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_json(url: str) -> dict:
|
|
235
|
+
with urllib.request.urlopen(url, timeout=30) as response:
|
|
236
|
+
return json.loads(response.read().decode("utf-8") or "{}")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def build_post_content(
|
|
240
|
+
sender: str,
|
|
241
|
+
peer_id: str,
|
|
242
|
+
multiaddrs: list[str],
|
|
243
|
+
browser_multiaddrs: list[str],
|
|
244
|
+
registration_id: str | None,
|
|
245
|
+
profile: str | None,
|
|
246
|
+
version: str | None,
|
|
247
|
+
owner_authorization: dict[str, object] | None,
|
|
248
|
+
relay_proof: dict[str, object],
|
|
249
|
+
now_ms: int,
|
|
250
|
+
ref: str,
|
|
251
|
+
post_type: str,
|
|
252
|
+
) -> dict[str, object]:
|
|
253
|
+
content = {
|
|
254
|
+
"peerId": peer_id,
|
|
255
|
+
"multiaddrs": multiaddrs,
|
|
256
|
+
"browserMultiaddrs": browser_multiaddrs if browser_multiaddrs else None,
|
|
257
|
+
"registrationId": registration_id,
|
|
258
|
+
"profile": profile,
|
|
259
|
+
"version": version,
|
|
260
|
+
"ownerAddress": (
|
|
261
|
+
owner_authorization.get("payload", {}).get("ownerAddress")
|
|
262
|
+
if isinstance(owner_authorization, dict)
|
|
263
|
+
else None
|
|
264
|
+
),
|
|
265
|
+
"publisherAddress": sender,
|
|
266
|
+
"authorization": owner_authorization,
|
|
267
|
+
"relayProof": relay_proof,
|
|
268
|
+
"updatedAt": now_ms,
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
"type": post_type,
|
|
272
|
+
"address": sender,
|
|
273
|
+
"ref": ref,
|
|
274
|
+
"content": content,
|
|
275
|
+
"time": now_ms,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def build_unsigned_message(sender: str, item_content: str, now_seconds: float, channel: str) -> dict[str, object]:
|
|
280
|
+
item_hash = hashlib.sha256(item_content.encode("utf-8")).hexdigest()
|
|
281
|
+
return {
|
|
282
|
+
"channel": channel,
|
|
283
|
+
"sender": sender,
|
|
284
|
+
"chain": "ETH",
|
|
285
|
+
"type": "POST",
|
|
286
|
+
"time": now_seconds,
|
|
287
|
+
"item_type": "inline",
|
|
288
|
+
"item_content": item_content,
|
|
289
|
+
"item_hash": item_hash,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def sign_aleph_message(unsigned_message: dict[str, object], private_key: str) -> dict[str, object]:
|
|
294
|
+
signed = dict(unsigned_message)
|
|
295
|
+
signed["signature"] = sign_personal_message(
|
|
296
|
+
private_key,
|
|
297
|
+
signature_payload(
|
|
298
|
+
str(unsigned_message["chain"]),
|
|
299
|
+
str(unsigned_message["sender"]),
|
|
300
|
+
str(unsigned_message["type"]),
|
|
301
|
+
str(unsigned_message["item_hash"]),
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
return signed
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, dict]:
|
|
308
|
+
url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
|
|
309
|
+
attempts = [
|
|
310
|
+
{"sync": True, "message": message},
|
|
311
|
+
{**message, "sync": True},
|
|
312
|
+
dict(message),
|
|
313
|
+
]
|
|
314
|
+
last: tuple[int, dict] | None = None
|
|
315
|
+
for attempt in attempts:
|
|
316
|
+
last = post_json(url, attempt)
|
|
317
|
+
if 200 <= last[0] < 300:
|
|
318
|
+
return last
|
|
319
|
+
if last is None:
|
|
320
|
+
raise RuntimeError("Aleph broadcast failed before any attempt was made")
|
|
321
|
+
raise RuntimeError(f"Aleph broadcast failed: {last[0]} {json_dumps(last[1])}")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def parse_post_record(entry: object) -> dict[str, object] | None:
|
|
325
|
+
if not isinstance(entry, dict):
|
|
326
|
+
return None
|
|
327
|
+
item_hash = entry.get("item_hash") or entry.get("hash")
|
|
328
|
+
if not isinstance(item_hash, str) or not item_hash:
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
sender = entry.get("address") or entry.get("sender")
|
|
332
|
+
item_content = entry.get("item_content")
|
|
333
|
+
if isinstance(item_content, str):
|
|
334
|
+
try:
|
|
335
|
+
item_content = json.loads(item_content)
|
|
336
|
+
except json.JSONDecodeError:
|
|
337
|
+
item_content = None
|
|
338
|
+
|
|
339
|
+
if not isinstance(item_content, dict):
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
content = item_content.get("content")
|
|
343
|
+
if not isinstance(content, dict):
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
"item_hash": item_hash,
|
|
348
|
+
"sender": sender,
|
|
349
|
+
"registration_id": content.get("registrationId"),
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def fetch_previous_hashes(
|
|
354
|
+
api_host: str,
|
|
355
|
+
channel: str,
|
|
356
|
+
ref: str,
|
|
357
|
+
post_type: str,
|
|
358
|
+
sender: str,
|
|
359
|
+
registration_id: str | None,
|
|
360
|
+
current_item_hash: str,
|
|
361
|
+
) -> list[str]:
|
|
362
|
+
if not registration_id:
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
found: list[str] = []
|
|
366
|
+
for page in range(1, MAX_PREVIOUS_PAGES + 1):
|
|
367
|
+
url = (
|
|
368
|
+
f"{api_host.rstrip('/')}/api/v0/posts.json?"
|
|
369
|
+
f"channels={urllib.parse.quote(channel)}&"
|
|
370
|
+
f"refs={urllib.parse.quote(ref)}&"
|
|
371
|
+
f"types={urllib.parse.quote(post_type)}&"
|
|
372
|
+
f"pagination={PAGINATION}&page={page}"
|
|
373
|
+
)
|
|
374
|
+
payload = get_json(url)
|
|
375
|
+
posts = payload.get("posts")
|
|
376
|
+
if not isinstance(posts, list):
|
|
377
|
+
break
|
|
378
|
+
|
|
379
|
+
for entry in posts:
|
|
380
|
+
parsed = parse_post_record(entry)
|
|
381
|
+
if parsed is None:
|
|
382
|
+
continue
|
|
383
|
+
if str(parsed["sender"]).lower() != sender.lower():
|
|
384
|
+
continue
|
|
385
|
+
if parsed["registration_id"] != registration_id:
|
|
386
|
+
continue
|
|
387
|
+
if parsed["item_hash"] == current_item_hash:
|
|
388
|
+
continue
|
|
389
|
+
found.append(str(parsed["item_hash"]))
|
|
390
|
+
|
|
391
|
+
if len(posts) < PAGINATION:
|
|
392
|
+
break
|
|
393
|
+
|
|
394
|
+
return dedupe(found)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def broadcast_forget(
|
|
398
|
+
api_host: str,
|
|
399
|
+
sender: str,
|
|
400
|
+
private_key: str,
|
|
401
|
+
hashes: list[str],
|
|
402
|
+
channel: str,
|
|
403
|
+
) -> tuple[int, dict] | None:
|
|
404
|
+
if not hashes:
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
now_seconds = time.time()
|
|
408
|
+
item_content = json_dumps(
|
|
409
|
+
{
|
|
410
|
+
"address": sender,
|
|
411
|
+
"time": now_seconds,
|
|
412
|
+
"hashes": hashes,
|
|
413
|
+
"aggregates": [],
|
|
414
|
+
"reason": f"Replace older relay bootstrap records for {sender}",
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
unsigned = {
|
|
418
|
+
"sender": sender,
|
|
419
|
+
"chain": "ETH",
|
|
420
|
+
"type": "FORGET",
|
|
421
|
+
"item_hash": hashlib.sha256(item_content.encode("utf-8")).hexdigest(),
|
|
422
|
+
"item_type": "inline",
|
|
423
|
+
"item_content": item_content,
|
|
424
|
+
"time": now_seconds,
|
|
425
|
+
"channel": channel,
|
|
426
|
+
}
|
|
427
|
+
return broadcast_aleph_message(api_host, sign_aleph_message(unsigned, private_key))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def main() -> None:
|
|
431
|
+
env_values = parse_env_file(ENV_FILE)
|
|
432
|
+
publisher_private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
|
|
433
|
+
registration_id = env_values.get("ALEPH_BOOTSTRAP_REGISTRATION_ID", "").strip() or None
|
|
434
|
+
if not publisher_private_key:
|
|
435
|
+
print(json_dumps({"status": "skipped", "reason": "missing publisher key"}))
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
describe = subprocess.run([DESCRIBE_SCRIPT], check=True, capture_output=True, text=True)
|
|
439
|
+
metadata = json.loads(describe.stdout.strip() or "{}")
|
|
440
|
+
peer_id = metadata.get("peer_id")
|
|
441
|
+
if not isinstance(peer_id, str) or not peer_id.strip():
|
|
442
|
+
raise SystemExit("unable to discover relay peer ID from describe script")
|
|
443
|
+
|
|
444
|
+
multiaddrs = filter_public_multiaddrs(metadata.get("probe_multiaddrs") or [])
|
|
445
|
+
browser_multiaddrs = filter_public_multiaddrs(
|
|
446
|
+
metadata.get("browser_bootstrap_multiaddrs") or [], browser_only=True
|
|
447
|
+
)
|
|
448
|
+
if not multiaddrs:
|
|
449
|
+
print(json_dumps({"status": "skipped", "reason": "no public multiaddrs"}))
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
publisher_address = address_from_private_key(publisher_private_key)
|
|
453
|
+
now_ms = int(time.time() * 1000)
|
|
454
|
+
now_seconds = now_ms / 1000
|
|
455
|
+
profile = env_values.get("ALEPH_BOOTSTRAP_PROFILE", DEFAULT_PROFILE).strip() or DEFAULT_PROFILE
|
|
456
|
+
version = env_values.get("ALEPH_BOOTSTRAP_VERSION", "").strip() or None
|
|
457
|
+
channel = env_values.get("ALEPH_BOOTSTRAP_CHANNEL", DEFAULT_CHANNEL).strip() or DEFAULT_CHANNEL
|
|
458
|
+
ref = env_values.get("ALEPH_BOOTSTRAP_REF", DEFAULT_REF).strip() or DEFAULT_REF
|
|
459
|
+
post_type = env_values.get("ALEPH_BOOTSTRAP_POST_TYPE", DEFAULT_POST_TYPE).strip() or DEFAULT_POST_TYPE
|
|
460
|
+
api_host = env_values.get("ALEPH_BOOTSTRAP_API_HOST", DEFAULT_API_HOST).strip() or DEFAULT_API_HOST
|
|
461
|
+
|
|
462
|
+
owner_authorization = load_owner_authorization(
|
|
463
|
+
env_values,
|
|
464
|
+
publisher_address,
|
|
465
|
+
peer_id,
|
|
466
|
+
registration_id,
|
|
467
|
+
profile,
|
|
468
|
+
version,
|
|
469
|
+
now_ms,
|
|
470
|
+
)
|
|
471
|
+
if owner_authorization is None:
|
|
472
|
+
print(json_dumps({"status": "skipped", "reason": "missing owner authorization"}))
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
proof_payload = relay_proof_payload(
|
|
476
|
+
peer_id,
|
|
477
|
+
multiaddrs,
|
|
478
|
+
browser_multiaddrs,
|
|
479
|
+
registration_id,
|
|
480
|
+
profile,
|
|
481
|
+
version,
|
|
482
|
+
now_ms,
|
|
483
|
+
)
|
|
484
|
+
relay_proof = {
|
|
485
|
+
"scheme": "personal_sign",
|
|
486
|
+
"payload": proof_payload,
|
|
487
|
+
"signature": sign_personal_message(publisher_private_key, json_dumps(proof_payload)),
|
|
488
|
+
}
|
|
489
|
+
post_content = build_post_content(
|
|
490
|
+
publisher_address,
|
|
491
|
+
peer_id,
|
|
492
|
+
multiaddrs,
|
|
493
|
+
browser_multiaddrs,
|
|
494
|
+
registration_id,
|
|
495
|
+
profile,
|
|
496
|
+
version,
|
|
497
|
+
owner_authorization,
|
|
498
|
+
relay_proof,
|
|
499
|
+
now_ms,
|
|
500
|
+
ref,
|
|
501
|
+
post_type,
|
|
502
|
+
)
|
|
503
|
+
item_content = json_dumps(post_content)
|
|
504
|
+
unsigned_message = build_unsigned_message(
|
|
505
|
+
publisher_address, item_content, now_seconds, channel
|
|
506
|
+
)
|
|
507
|
+
signed_message = sign_aleph_message(unsigned_message, publisher_private_key)
|
|
508
|
+
http_status, response = broadcast_aleph_message(api_host, signed_message)
|
|
509
|
+
|
|
510
|
+
previous_hashes = fetch_previous_hashes(
|
|
511
|
+
api_host,
|
|
512
|
+
channel,
|
|
513
|
+
ref,
|
|
514
|
+
post_type,
|
|
515
|
+
publisher_address,
|
|
516
|
+
registration_id,
|
|
517
|
+
str(unsigned_message["item_hash"]),
|
|
518
|
+
)
|
|
519
|
+
forget_response = broadcast_forget(
|
|
520
|
+
api_host, publisher_address, publisher_private_key, previous_hashes, channel
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
print(
|
|
524
|
+
json_dumps(
|
|
525
|
+
{
|
|
526
|
+
"status": "published",
|
|
527
|
+
"httpStatus": http_status,
|
|
528
|
+
"itemHash": unsigned_message["item_hash"],
|
|
529
|
+
"sender": publisher_address,
|
|
530
|
+
"peerId": peer_id,
|
|
531
|
+
"publishedMultiaddrs": multiaddrs,
|
|
532
|
+
"publishedBrowserMultiaddrs": browser_multiaddrs,
|
|
533
|
+
"forgottenHashes": previous_hashes,
|
|
534
|
+
"forgetResponse": forget_response[1] if forget_response else None,
|
|
535
|
+
"response": response,
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
if __name__ == "__main__":
|
|
542
|
+
try:
|
|
543
|
+
main()
|
|
544
|
+
except Exception as error: # pragma: no cover - runtime error path
|
|
545
|
+
print(json_dumps({"status": "error", "error": str(error)}), file=sys.stderr)
|
|
546
|
+
raise
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Refresh Aleph bootstrap registration for orbitdb-relay-pinner
|
|
3
|
+
Wants=network-online.target
|
|
4
|
+
After=network-online.target orbitdb-relay-pinner.service
|
|
5
|
+
ConditionPathExists=/etc/default/orbitdb-relay-pinner.ready
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=oneshot
|
|
9
|
+
Environment=ENV_FILE=/etc/default/orbitdb-relay-pinner
|
|
10
|
+
Environment=DESCRIBE_SCRIPT=/usr/local/sbin/orbitdb-relay-pinner-describe.py
|
|
11
|
+
ExecStart=/usr/bin/python3 /usr/local/sbin/orbitdb-relay-pinner-bootstrap-refresh.py
|
|
12
|
+
StandardOutput=journal
|
|
13
|
+
StandardError=journal
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Periodic Aleph bootstrap refresh for orbitdb-relay-pinner
|
|
3
|
+
|
|
4
|
+
[Timer]
|
|
5
|
+
OnBootSec=20m
|
|
6
|
+
OnUnitActiveSec=6h
|
|
7
|
+
RandomizedDelaySec=15m
|
|
8
|
+
Persistent=true
|
|
9
|
+
Unit=orbitdb-relay-pinner-bootstrap-refresh.service
|
|
10
|
+
|
|
11
|
+
[Install]
|
|
12
|
+
WantedBy=timers.target
|
|
@@ -15,7 +15,7 @@ fi
|
|
|
15
15
|
|
|
16
16
|
export DEBIAN_FRONTEND=noninteractive
|
|
17
17
|
apt-get update
|
|
18
|
-
apt-get install -y ca-certificates curl gnupg python3 build-essential caddy
|
|
18
|
+
apt-get install -y ca-certificates curl gnupg python3 python3-pip build-essential caddy
|
|
19
19
|
|
|
20
20
|
if ! command -v node >/dev/null 2>&1 || [ "$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || echo 0)" -lt "${NODE_MIN_MAJOR}" ]; then
|
|
21
21
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
@@ -34,6 +34,8 @@ if ! command -v npm >/dev/null 2>&1; then
|
|
|
34
34
|
exit 1
|
|
35
35
|
fi
|
|
36
36
|
|
|
37
|
+
python3 -m pip install --break-system-packages --no-cache-dir eth-account
|
|
38
|
+
|
|
37
39
|
mkdir -p "${DATA_DIR}" "$(dirname "${ENV_FILE}")"
|
|
38
40
|
|
|
39
41
|
if ! id "${SERVICE_USER}" >/dev/null 2>&1; then
|