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