@mcsherrylabs/pi-meerkat 0.10.1 → 0.11.0
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/dist/installer/committee-init.sh +17 -33
- package/dist/installer/committee.conf +12 -34
- package/dist/installer/{node.devnet.conf → node-follower.conf} +3 -9
- package/dist/installer/node-join.sh +11 -4
- package/dist/installer/node.conf +4 -234
- package/dist/installer/node.local.conf +8 -0
- package/dist/installer/runtime.json +1 -1
- package/dist/installer-runtime/dist/scripts/recover-owner.js +7 -9
- package/dist/installer-runtime/dist/scripts/subscriptions-init.js +3 -4
- package/dist/installer-runtime/dist/scripts/versions.mjs +5 -10
- package/dist/installer-runtime/dist/src/announcements.js +1 -289
- package/dist/installer-runtime/dist/src/app.js +12 -548
- package/dist/installer-runtime/dist/src/chainfetch.js +0 -64
- package/dist/installer-runtime/dist/src/env.js +35 -32
- package/dist/installer-runtime/dist/src/service.js +151 -0
- package/dist/installer.d.ts +8 -18
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +8 -9
- package/dist/installer.js.map +1 -1
- package/dist/mcp-upstream.d.ts +1 -1
- package/dist/mcp-upstream.js +1 -1
- package/installer/Node.Dockerfile +4 -3
- package/installer/Registry.Dockerfile +34 -9
- package/installer/check-single-sdk.mjs +151 -0
- package/package.json +4 -4
- package/dist/installer-runtime/dist/src/runtime.js +0 -192
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
#
|
|
2
|
+
# committee entrypoint: host keys + genesis on first boot, then exec the node.
|
|
3
3
|
#
|
|
4
|
-
# On FIRST boot it generates this node's key material ON THE HOST (never in the
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# Two secrets, handled identically (spec 101 — both are host-only secrets):
|
|
11
|
-
# - Ed25519 node-identity key -> ce.evolver.node.private-key (EVOLVER_PRIVATE_KEY)
|
|
12
|
-
# - BLS consensus key (IKM) -> ce.evolver.node.bls-secret (EVOLVER_BLS_SEED)
|
|
13
|
-
# Their public keys go into genesis (public-key / bls-public-key).
|
|
4
|
+
# On FIRST boot it generates this node's key material ON THE HOST (never in the image or the repo),
|
|
5
|
+
# renders a one-node genesis committee from the derived public keys, and persists everything into
|
|
6
|
+
# the /data volume. On every subsequent boot it REUSES the same keys + genesis so the chain
|
|
7
|
+
# continues. It also EXPORTS the public chain spec into /public-genesis/genesis.conf so the follower
|
|
8
|
+
# can join the chain from it.
|
|
14
9
|
set -eu
|
|
15
10
|
|
|
16
|
-
# Deliberately NOT derived from the app name. This id is written into the genesis
|
|
17
|
-
# file on the volume at FIRST BOOT, so changing it on an existing chain would launch
|
|
18
|
-
# the node under a name absent from its own genesis. Keeping it neutral means
|
|
19
|
-
# `npm run rename` never has to touch this file and cannot leave a previous project's
|
|
20
|
-
# name behind — the template used to default to `tmplnode0`, so every app built from
|
|
21
|
-
# it had a node introducing itself by the TEMPLATE's name, in its logs and in its
|
|
22
|
-
# published deployment artifact.
|
|
23
11
|
NODE_ID="${NODE_ID:-node0}"
|
|
24
12
|
KEYS=/data/keys
|
|
25
13
|
GENESIS=/data/genesis.gen.conf
|
|
@@ -27,6 +15,8 @@ ED_KEY="$KEYS/ed25519.key" # base64 PKCS#8 Ed25519 private
|
|
|
27
15
|
BLS_KEY="$KEYS/bls.key" # base64 BLS IKM (>=32 bytes)
|
|
28
16
|
|
|
29
17
|
if [ -f "$ED_KEY" ] || [ -f "$BLS_KEY" ] || [ -f "$GENESIS" ]; then
|
|
18
|
+
# PARTIAL STATE REFUSES: an incomplete owned key set is never regenerated (that would fork the
|
|
19
|
+
# chain identity), so a node with one of three and not the other two stops rather than claims on.
|
|
30
20
|
[ -s "$ED_KEY" ] && [ -s "$BLS_KEY" ] && [ -s "$GENESIS" ] || {
|
|
31
21
|
echo '[node-init] incomplete owned keys/genesis; refusing regeneration' >&2; exit 1;
|
|
32
22
|
}
|
|
@@ -56,15 +46,8 @@ if [ ! -f "$ED_KEY" ]; then
|
|
|
56
46
|
printf '%s' "$ED_PRIV" > "$ED_KEY"; chmod 600 "$ED_KEY"
|
|
57
47
|
printf '%s' "$BLS_SECRET" > "$BLS_KEY"; chmod 600 "$BLS_KEY"
|
|
58
48
|
|
|
59
|
-
# Render the chain spec (089): committee PUBLIC keys + the chain-wide consensus
|
|
60
|
-
|
|
61
|
-
# (102 follower-join: the genesis file IS the chain, byte-identical everywhere) and
|
|
62
|
-
# its canonical hash is pinned as the genesis block id (ce.evolver.genesis-file in
|
|
63
|
-
# node.conf). 103: the host is the ROUTABLE public address — followers dial the
|
|
64
|
-
# committee at exactly this host:port. Chain-wide params live HERE, not in node.conf,
|
|
65
|
-
# so joiners inherit them automatically — quiescence especially (FR-019: a joiner
|
|
66
|
-
# with different quiescence would time out the deliberately-lingering leader).
|
|
67
|
-
PUBLIC_HOST="${PUBLIC_HOST:-localhost}"
|
|
49
|
+
# Render the chain spec (089): committee PUBLIC keys + the chain-wide consensus parameters.
|
|
50
|
+
PUBLIC_HOST="${PUBLIC_HOST:-committee}"
|
|
68
51
|
cat > "$GENESIS" <<EOF
|
|
69
52
|
ce.evolver.genesis.nodes = [
|
|
70
53
|
{
|
|
@@ -85,10 +68,7 @@ ce.evolver.consensus {
|
|
|
85
68
|
snapshot-interval-blocks = 0 # single-node: no snapshot cadence; joiners sync from block 0
|
|
86
69
|
identity-sponsorship-cost = 100
|
|
87
70
|
|
|
88
|
-
# Low idle disk-write: after 3 empty blocks the leader lingers on a 5-MINUTE heartbeat
|
|
89
|
-
# instead of the 600ms cadence (#31: idle-heartbeat-ms > block-time-ms activates quiescence);
|
|
90
|
-
# wake-on-work (node-local, always on) still proposes immediately on incoming txs.
|
|
91
|
-
# CHAIN-WIDE — identical on every node by construction.
|
|
71
|
+
# Low idle disk-write: after 3 empty blocks the leader lingers on a 5-MINUTE heartbeat.
|
|
92
72
|
quiescence {
|
|
93
73
|
idle-heartbeat-ms = 300000
|
|
94
74
|
idle-after-empty-rounds = 3
|
|
@@ -96,7 +76,6 @@ ce.evolver.consensus {
|
|
|
96
76
|
}
|
|
97
77
|
EOF
|
|
98
78
|
echo "[node-init] generated keys (secrets in $KEYS, mode 600) + chain spec (host=$PUBLIC_HOST)"
|
|
99
|
-
echo "[node-init] PUBLISH the chain spec (public keys + chain-wide config): docker compose cp node:/data/genesis.gen.conf ."
|
|
100
79
|
else
|
|
101
80
|
echo "[node-init] existing keys found in $KEYS — reusing (chain continues)"
|
|
102
81
|
fi
|
|
@@ -106,7 +85,10 @@ EVOLVER_PRIVATE_KEY="$(cat "$ED_KEY")"; export EVOLVER_PRIVATE_KEY
|
|
|
106
85
|
EVOLVER_BLS_SEED="$(cat "$BLS_KEY")"; export EVOLVER_BLS_SEED
|
|
107
86
|
mkdir -p /data/chain
|
|
108
87
|
|
|
109
|
-
|
|
88
|
+
# Publish the PUBLIC chain spec (keys + consensus) into the shared volume. The follower joins
|
|
89
|
+
# from this copy, never from the committee's private data/keys volume. A differing existing copy
|
|
90
|
+
# refuses replacement — the chain identity is hash-pinned, so a byte-different genesis is a
|
|
91
|
+
# different chain.
|
|
110
92
|
if [ -f /public-genesis/genesis.conf ]; then
|
|
111
93
|
cmp -s "$GENESIS" /public-genesis/genesis.conf || { echo '[node-init] public genesis differs; refusing replacement' >&2; exit 1; }
|
|
112
94
|
else
|
|
@@ -114,4 +96,6 @@ else
|
|
|
114
96
|
chmod 640 /public-genesis/genesis.pending
|
|
115
97
|
mv /public-genesis/genesis.pending /public-genesis/genesis.conf
|
|
116
98
|
fi
|
|
99
|
+
|
|
100
|
+
echo "[node-init] launching evolver node '$NODE_ID'"
|
|
117
101
|
exec /opt/evolver/bin/evolver -Dconfig.file=/config/node.conf "$NODE_ID"
|
|
@@ -17,29 +17,21 @@ ce.evolver.node {
|
|
|
17
17
|
evolver.peer-discovery {
|
|
18
18
|
# What this node ADVERTISES about ITSELF to peers — not somewhere it connects to.
|
|
19
19
|
# This chain is standalone (one node, its own genesis, no seeds) so nothing reads
|
|
20
|
-
# these
|
|
21
|
-
#
|
|
22
|
-
# was copied from that deployment. Joining a real chain means setting them to the
|
|
23
|
-
# ROUTABLE p2p transport (raw TCP mTLS :7000) and HTTPS API your peers can dial.
|
|
24
|
-
# 163 — `host:port`, no scheme: the transport is raw TCP + mTLS and never was a URL.
|
|
20
|
+
# these. Joining a real chain means setting them to the ROUTABLE p2p transport
|
|
21
|
+
# (raw TCP mTLS :7000) and HTTPS API your peers can dial.
|
|
25
22
|
transport-address = "committee:7000"
|
|
26
23
|
# No `peer-url`: this chain is one node, so nothing fetches content from it, and the
|
|
27
24
|
# plain-HTTP :8440 above could not serve a peer anyway — a content fetch carries the
|
|
28
|
-
# caller's client certificate and needs the node's OWN TLS listener.
|
|
29
|
-
# chain means setting `transport-address` to the routable :7000 and `peer-url` to this
|
|
30
|
-
# node's https listener (never a reverse proxy: it presents no client certificate).
|
|
25
|
+
# caller's client certificate and needs the node's OWN TLS listener.
|
|
31
26
|
gossip-interval-seconds = 60
|
|
32
27
|
}
|
|
33
28
|
|
|
34
|
-
# Chain-wide consensus params
|
|
35
|
-
# included chain spec — do NOT set them here, they must stay byte-identical on every
|
|
36
|
-
# node. Only node-local knobs below.
|
|
29
|
+
# Chain-wide consensus params come from the included chain spec — do NOT set them here.
|
|
37
30
|
ce.evolver.consensus {
|
|
38
31
|
validate-chain-on-startup = false
|
|
39
32
|
|
|
40
33
|
# Reactivity (node-local, always on since #28): on incoming work the leader proposes
|
|
41
|
-
# IMMEDIATELY rather than waiting out the (5-minute) idle heartbeat
|
|
42
|
-
# sub-second despite slow idle.
|
|
34
|
+
# IMMEDIATELY rather than waiting out the (5-minute) idle heartbeat.
|
|
43
35
|
wake-on-work {
|
|
44
36
|
forward-watermark = 16
|
|
45
37
|
}
|
|
@@ -47,34 +39,17 @@ ce.evolver.consensus {
|
|
|
47
39
|
|
|
48
40
|
data.base = "/data/chain"
|
|
49
41
|
state.snapshots = "snapshots"
|
|
50
|
-
# PLAIN HTTP on 8440 for everything that TALKS to this node, deliberately.
|
|
51
|
-
#
|
|
52
|
-
# MINUTES, because it is a peer authentication credential and not a web certificate. No
|
|
53
|
-
# browser can be made happy with that: an exception you click through expires before you
|
|
54
|
-
# finish reading the page. Tolerating it in code means NODE_TLS_REJECT_UNAUTHORIZED=0,
|
|
55
|
-
# which disables verification PROCESS-WIDE for every host — a bad habit to ship in a
|
|
56
|
-
# template.
|
|
57
|
-
#
|
|
58
|
-
# TLS on 8441 for PEERS, because the content cache needs somewhere mTLS to live: its peer
|
|
59
|
-
# route identifies callers by client certificate and the node refuses to start content
|
|
60
|
-
# without a TLS listener. Before evolver v0.16.0 those were the same listener, so
|
|
61
|
-
# "hold documents" and "put every client on an unpinnable certificate" were one decision.
|
|
62
|
-
# They are not, and this is what that separation looks like in a deployment.
|
|
42
|
+
# PLAIN HTTP on 8440 for everything that TALKS to this node, deliberately. TLS on 8441 for
|
|
43
|
+
# PEERS (the content cache needs somewhere mTLS to live).
|
|
63
44
|
http.port = 8440
|
|
64
45
|
# ⚠️ REQUIRED since evolver #131: the plain API listener now defaults to 127.0.0.1. The app
|
|
65
46
|
# container reaches this node over the CONTAINER NETWORK, which is not loopback from the
|
|
66
|
-
# node's point of view — without this line the app gets connection refused
|
|
67
|
-
# logs nothing unusual. "0.0.0.0" is what this deployment effectively had before.
|
|
47
|
+
# node's point of view — without this line the app gets connection refused.
|
|
68
48
|
http.bind-host = "0.0.0.0"
|
|
69
49
|
https.port = 8441
|
|
70
50
|
evolver.grpc.port = 9000
|
|
71
51
|
|
|
72
52
|
# 139 — CONTENT, so a detail document announced on this chain can actually be fetched.
|
|
73
|
-
#
|
|
74
|
-
# `bind-host` is what lets a SIBLING CONTAINER reach the publish/read route: it is
|
|
75
|
-
# authorized by what can reach it, and the default (loopback) means "inside this node's
|
|
76
|
-
# container and nowhere else". 0.0.0.0 here says: this compose project's network is the
|
|
77
|
-
# boundary. It holds because the port below is published to 127.0.0.1 only.
|
|
78
53
|
ce.evolver.content {
|
|
79
54
|
enabled = true
|
|
80
55
|
http-port = 8450
|
|
@@ -99,4 +74,7 @@ chainDb {
|
|
|
99
74
|
useShutdownHook = false
|
|
100
75
|
freeBlobsEarly = false
|
|
101
76
|
}
|
|
102
|
-
|
|
77
|
+
|
|
78
|
+
# The committee is CONSENSUS ONLY, never the processing node. Execution is disabled so this node
|
|
79
|
+
# never serves the single-consumer channel cursor; the follower below owns that.
|
|
80
|
+
ce.evolver.execution.enabled = false
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
# This node: a NON-GENESIS FOLLOWER
|
|
2
|
-
#
|
|
3
|
-
# node-local knobs below.
|
|
4
|
-
include file("/config/genesis.devnet.conf")
|
|
5
|
-
|
|
6
|
-
# 089 chain-spec pin: the canonical hash of the genesis file IS the genesis block id.
|
|
7
|
-
# Any drift refuses to boot rather than forking quietly.
|
|
8
|
-
ce.evolver.genesis-file = "/config/genesis.devnet.conf"
|
|
1
|
+
# This node: a NON-GENESIS FOLLOWER. Chain-wide params come from the genesis spec selected by the
|
|
2
|
+
# wrapper that includes this file — never set them here, they must stay byte-identical on every
|
|
3
|
+
# node. Only node-local knobs below.
|
|
9
4
|
|
|
10
5
|
# This node's OWN identity, from the first-boot faucet claim (node-join.sh). REQUIRED
|
|
11
6
|
# substitutions (no `?`): a node booting with missing keys must fail fast, not later and
|
|
@@ -235,4 +230,3 @@ chainDb {
|
|
|
235
230
|
useShutdownHook = false
|
|
236
231
|
freeBlobsEarly = false
|
|
237
232
|
}
|
|
238
|
-
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
# faucet-claim its identity on devnet (the committee node sponsors and funds it — no
|
|
9
9
|
# admin secret exists anywhere in this deployment), persist the result. EVERY boot:
|
|
10
10
|
# source it and start as a non-genesis follower, syncing the chain from block 0.
|
|
11
|
+
#
|
|
12
|
+
# EVOLVER_NODE_CONFIG (optional) selects the HOCON config file the JVM boots with; the default
|
|
13
|
+
# is /config/node.conf (the devnet wrapper). A local-chain deployment sets it to
|
|
14
|
+
# /config/node.local.conf to boot from /public-genesis/genesis.conf instead.
|
|
11
15
|
set -eu
|
|
12
16
|
|
|
13
17
|
# Same read-only guard on normal boot and the owned-fixture preflight. It NEVER initializes,
|
|
@@ -223,13 +227,16 @@ echo "[node-join] pinning ${_peers} as the seed; allowing ${_outbound} outbound
|
|
|
223
227
|
' "$_outbound"
|
|
224
228
|
} > /data/chain/peers.conf
|
|
225
229
|
|
|
226
|
-
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
+
# #21: an unclean exit (OOM kill, power loss, SIGKILL after the stop timeout) leaves the private
|
|
231
|
+
# node-control socket behind, and the node refuses ANY pre-existing entry, so restart: unless-stopped
|
|
232
|
+
# would crash-loop forever. Remove it only if it is a SOCKET: this directory is exclusive to this
|
|
233
|
+
# node (the processor mount is read-only) and this entrypoint runs only while this container's
|
|
234
|
+
# node JVM is not running. Any other entry type is left for the node itself to refuse.
|
|
230
235
|
if [ -S /node-control/node.sock ]; then
|
|
231
236
|
echo "[node-join] removing stale node-control socket left by an unclean previous exit"
|
|
232
237
|
rm -f /node-control/node.sock
|
|
233
238
|
fi
|
|
239
|
+
|
|
240
|
+
echo "[node-join] launching follower '$NODE_ID' — syncing devnet from block 0"
|
|
234
241
|
exec /opt/evolver/bin/evolver -Dconfig.file="${EVOLVER_NODE_CONFIG:-/config/node.conf}" "$NODE_ID"
|
|
235
242
|
|
package/dist/installer/node.conf
CHANGED
|
@@ -1,238 +1,8 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
# node-local knobs below.
|
|
4
|
-
include file("/public-genesis/genesis.conf")
|
|
1
|
+
# DevNet wrapper: selects the devnet genesis, then the follower body.
|
|
2
|
+
include file("/config/genesis.devnet.conf")
|
|
5
3
|
|
|
6
4
|
# 089 chain-spec pin: the canonical hash of the genesis file IS the genesis block id.
|
|
7
5
|
# Any drift refuses to boot rather than forking quietly.
|
|
8
|
-
ce.evolver.genesis-file = "/
|
|
9
|
-
|
|
10
|
-
# This node's OWN identity, from the first-boot faucet claim (node-join.sh). REQUIRED
|
|
11
|
-
# substitutions (no `?`): a node booting with missing keys must fail fast, not later and
|
|
12
|
-
# obscurely at the first TLS handshake.
|
|
13
|
-
ce.evolver.node {
|
|
14
|
-
private-key = ${EVOLVER_PRIVATE_KEY} # Ed25519 PKCS#8 base64
|
|
15
|
-
public-key = ${EVOLVER_NODE_PUBLIC_KEY} # Ed25519 X.509 base64
|
|
16
|
-
bls-secret = ${EVOLVER_BLS_SEED} # BLS IKM base64
|
|
17
|
-
port = ${EVOLVER_NODE_PORT} # p2p transport (raw TCP, mTLS)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
evolver.peer-discovery {
|
|
21
|
-
# THIS NODE ADVERTISES NOTHING, AND MUST NOT. `preferred-url` is the address this node
|
|
22
|
-
# gossips as its OWN (`""` = feature off), and a joined app node behind Docker HAS no
|
|
23
|
-
# routable p2p address: its 7000 is container-local and deliberately unpublished. So
|
|
24
|
-
# advertising one means every node on the chain records `<us> -> https://127.0.0.1:7000`
|
|
25
|
-
# and then dials its own loopback.
|
|
26
|
-
#
|
|
27
|
-
# Observed on devnet, and it is worse than noise: this node CONNECTED TO ITSELF, logged
|
|
28
|
-
# `Peer connected: <self>`, sent its own SyncManager `BlockRangeRequest(4841,4845)`, and
|
|
29
|
-
# TipGossip then reported `rotating to <self> (failed=devnet1-node0)` followed by
|
|
30
|
-
# `stalled (upstream=none)`. After that its submits were never forwarded and every write
|
|
31
|
-
# timed out. The table also keeps the dead: `evolvertemplateappnode965813 at
|
|
32
|
-
# 127.0.0.1:7000` from an earlier deployment was still being dialled hours later.
|
|
33
|
-
# ADVERTISED AGAIN, and ⚠️ ONLY BECAUSE IT IS NOW REAL. This was `""` because the node
|
|
34
|
-
# had been advertising `https://127.0.0.1:7000`: every peer that read the discovery
|
|
35
|
-
# table dialled its OWN loopback, so this node connected to itself, sent its own
|
|
36
|
-
# SyncManager block requests, reported `candidates=<self>` / `stalled (upstream=none)`
|
|
37
|
-
# and stopped forwarding submits. An unroutable value reinstates that exactly — which
|
|
38
|
-
# is why `EVOLVER_TRANSPORT_ADDRESS` is REQUIRED here rather than defaulted, and why the
|
|
39
|
-
# command suite refuses a loopback value.
|
|
40
|
-
# ⚠️ EMPTY, DELIBERATELY. This node is a READER behind NAT: it has no routable p2p address.
|
|
41
|
-
# Advertising one is not merely useless, it is harmful — devnet recorded a node that advertised
|
|
42
|
-
# an unroutable address CONNECTING TO ITSELF, sending its own sync requests, reporting
|
|
43
|
-
# `stalled (upstream=none)`, and leaving dead entries other nodes dialled for hours.
|
|
44
|
-
#
|
|
45
|
-
# It costs this node nothing: advertising is how others DIAL YOU, and nothing needs to. Writing
|
|
46
|
-
# still works — this node dials OUT, discovers the network, learns its distance to the committee
|
|
47
|
-
# and forwards transactions along that path. Being undialable only stops bodies being flooded TO
|
|
48
|
-
# it, which is a DA member's concern and not a reader's.
|
|
49
|
-
transport-address = ""
|
|
50
|
-
peer-url = ""
|
|
51
|
-
gossip-interval-seconds = 60
|
|
52
|
-
|
|
53
|
-
# 16 INBOUND, not 0. The 141 leaf recipe (`in=0`) is right for an app node that only
|
|
54
|
-
# talks upstream — and wrong here: the registry's channel requires announcers' nodes to
|
|
55
|
-
# reach THIS node so their flooded bodies can be attested (research D2). Each announcer
|
|
56
|
-
# holds one inbound slot, so 16 is the number of concurrent announcer nodes supported.
|
|
57
|
-
# Generous on both sides so DISCOVERY actually happens. The seed below is pinned so the node
|
|
58
|
-
# always has a way back to the chain, but `allowed-outbound` is deliberately LARGER than the
|
|
59
|
-
# pinned list — the pin is strict only while the two are equal, and a strict pin here would mean
|
|
60
|
-
# this node never dials anything it discovers.
|
|
61
|
-
allowed-inbound = 16
|
|
62
|
-
# `allowed-outbound` and `preferred-peers` are DELIBERATELY ABSENT HERE — node-join.sh renders
|
|
63
|
-
# both into peers.conf so the two cannot drift apart in two files.
|
|
64
|
-
#
|
|
65
|
-
# ⚠️ THIS COMMENT USED TO CLAIM THE PIN IS STRICT. It is NOT, and the difference matters to
|
|
66
|
-
# anyone reasoning about what this node will dial. It said `|preferred| == allowed-outbound`
|
|
67
|
-
# holds "by construction"; node-join.sh renders ONE pinned seed and an outbound budget of 16,
|
|
68
|
-
# so 1 != 16 and the pin is deliberately LOOSE. That is the intended design — a follower has to
|
|
69
|
-
# dial what it discovers or it is a leaf — but the old wording described the opposite property
|
|
70
|
-
# and named the incidents (a dead address from a torn-down deployment still being dialled, this
|
|
71
|
-
# node dialling its own loopback) that a strict pin was supposed to prevent. Those are the COST
|
|
72
|
-
# of the loose pin, not evidence against it: the seed guarantees a way back to the chain, and
|
|
73
|
-
# discovery does the rest. Which is what keeps a shared chain's discovery table out of the
|
|
74
|
-
# dial queue — it keeps the dead (an address from a torn-down deployment was still
|
|
75
|
-
# being dialled hours later) and once made this node dial its own loopback. Raising
|
|
76
|
-
# the budget without naming a peer would silently unpin it; naming a peer without
|
|
77
|
-
# raising the budget could never reach it. One list, both keys, no drift.
|
|
78
|
-
#
|
|
79
|
-
# ⚠️ THIS USED TO BE `out=1`, copied verbatim from the 141 LEAF recipe, and it decided
|
|
80
|
-
# WHICH CHANNELS THIS NODE COULD READ — silently. `ChannelOverlay.peers(ledgerId)` is
|
|
81
|
-
# the channel's DA group ∩ connected, so with the one slot spent on the upstream every
|
|
82
|
-
# channel whose DA member is somebody else resolved to an EMPTY pull set: the node
|
|
83
|
-
# records the miss, sends no request, and says so at DEBUG only (evolver#89). A node
|
|
84
|
-
# reading a channel it does not itself serve needs the upstream AND one reachable DA
|
|
85
|
-
# member per such channel, so a fixed 1 is only ever right for a node that reads
|
|
86
|
-
# nothing but its own.
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
ce.evolver.consensus {
|
|
90
|
-
validate-chain-on-startup = false
|
|
91
|
-
wake-on-work { forward-watermark = 16 }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
# PROCESSING MODE — off by default, and THE REASON THIS DEPLOYMENT EXISTS once your app
|
|
95
|
-
# reads a channel.
|
|
96
|
-
#
|
|
97
|
-
# `/api/channel/next` is a SINGLE-CONSUMER, FORWARD-ONLY cursor: two readers do not
|
|
98
|
-
# error, they silently split the stream and the message is unrecoverable. So an app that
|
|
99
|
-
# consumes an ordered channel must drain a node NOBODY ELSE drains — which means a node
|
|
100
|
-
# of its own. It cannot share a public one, and a single-node chain (devnet today) IS the
|
|
101
|
-
# committee, so that node is not a processing node and serves no stream at all.
|
|
102
|
-
#
|
|
103
|
-
# And the committee's node must never be put in this mode: processing mode DEFERS apply
|
|
104
|
-
# and paces it from the app's polling, so a committee node in processing mode would
|
|
105
|
-
# couple consensus progress to an HTTP poller. Ordering is its job; give the app its own.
|
|
106
|
-
#
|
|
107
|
-
# Turning it on changes what committed reads MEAN here — they answer "as of MY cursor".
|
|
108
|
-
# Measured in this family (evolver-scheduler): right after a successful faucet claim a
|
|
109
|
-
# quorum node reported `{"balance":5000}` for an identity while a processing node
|
|
110
|
-
# reported `{"error":"not_found"}` for the same one. An app in that mode needs to know
|
|
111
|
-
# which of its questions are "has the chain agreed?" (ask a quorum node) and which are
|
|
112
|
-
# "what have I processed?" (ask this one).
|
|
113
|
-
#
|
|
114
|
-
# ON, because this app owns a channel (`announcements`) and reads its ordered stream —
|
|
115
|
-
# steps 5-7 of src/app.ts. BOTH lines are needed and neither works alone: a node in
|
|
116
|
-
# processing mode with no channels declared has nothing to offer, and a node that
|
|
117
|
-
# declares channels without execution stores bodies it never streams.
|
|
118
|
-
#
|
|
119
|
-
# The channel must be named HERE as well as claimed on chain. A processing node stores
|
|
120
|
-
# bodies only for the channels it lists, so an undeclared channel's message is applied
|
|
121
|
-
# in the slice and never offered — the write looks fine and the stream stays silent,
|
|
122
|
-
# which is the failure the app's step 7 names explicitly.
|
|
123
|
-
# Processing is enabled only by the paired managed composition with its persistent zero-module
|
|
124
|
-
# shared SDK consumer. MCP sessions never drain. Independent remote nodes remain unmanaged.
|
|
125
|
-
ce.evolver.execution.enabled = ${?EVOLVER_EXECUTION_ENABLED}
|
|
126
|
-
# Private node-owned lifecycle capability; only the paired processor receives this volume.
|
|
127
|
-
# No control route is exposed on any TCP listener. 202 is acceptance, not ACTIVE.
|
|
128
|
-
ce.evolver.node-control.socket-path = "/node-control/node.sock"
|
|
129
|
-
# Shared SDK controller is the SOLE writer; node sees the whole config-only directory read-only.
|
|
130
|
-
# Ordinary boot/upgrade cannot merge defaults, resurrect removals or seed a lost declaration.
|
|
131
|
-
include required(file("/subscriptions/channels.conf"))
|
|
132
|
-
# `required` for the same reason: an absent optional include is IGNORED, and a node
|
|
133
|
-
# that silently fell back to the 64/64 defaults would dial the discovery table.
|
|
134
|
-
include required(file("/data/chain/peers.conf"))
|
|
135
|
-
|
|
136
|
-
# ── THE LOCAL ROUTES THIS DEPLOYMENT OPENS TO ITS OWN CONTAINERS ────────────
|
|
137
|
-
#
|
|
138
|
-
# The agent and the registry run BESIDE this node, each in its own container on this compose
|
|
139
|
-
# project's network. These routes carry no token and no client certificate — reaching the address
|
|
140
|
-
# IS the authorization — so the node binds them to loopback unless told otherwise, and loopback
|
|
141
|
-
# inside this container is reachable by nothing else in the stack.
|
|
142
|
-
#
|
|
143
|
-
# `bind-host = 0.0.0.0` here means: THIS COMPOSE NETWORK IS THE BOUNDARY. That claim holds only
|
|
144
|
-
# because none of these ports appear under `ports:` in docker-compose.yml and the stack does not
|
|
145
|
-
# use host networking. Publish one, or run with `--network host`, and the claim is simply false.
|
|
146
|
-
#
|
|
147
|
-
# ⚠️ `ce.evolver.login` is deliberately NOT here and cannot be: that route signs as any identity
|
|
148
|
-
# this host holds a signer for, which on this node is the node's own. Setting `bind-host` under
|
|
149
|
-
# that namespace fails the boot rather than being ignored.
|
|
150
|
-
|
|
151
|
-
# 145 — the agent sends and drains application messages. `/api/p2p/send` relays a message the
|
|
152
|
-
# agent signed ITSELF (its own identity, verified at the far end); `/api/p2p/sign-and-send` makes
|
|
153
|
-
# this node sign as itself. One listener, so opening it grants both.
|
|
154
|
-
ce.evolver.p2p {
|
|
155
|
-
http-port = 8460
|
|
156
|
-
bind-host = "0.0.0.0"
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
# 158 — the agent may ASK an enrolled phone to authorise a transaction. It cannot sign: the device
|
|
160
|
-
# holder approves every one, and that gate does not move when the bind does. Inert until a
|
|
161
|
-
# `mobile-signer` block exists, and the node says so at boot rather than failing.
|
|
162
|
-
ce.evolver.mobile {
|
|
163
|
-
http-port = 8470
|
|
164
|
-
bind-host = "0.0.0.0"
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
# 139 — CONTENT, so the agent can publish a document and the registry can read one back.
|
|
168
|
-
#
|
|
169
|
-
# `POST /content` writes to this node's own store, keyed by the bytes' own digest, and talks to
|
|
170
|
-
# no peer at all (FR-006). `bind-host` below is what lets the agent and the registry reach that
|
|
171
|
-
# route from their own containers. Fetching a document another node holds is the part that uses
|
|
172
|
-
# the overlay, and that is what the TLS listener underneath is for.
|
|
173
|
-
ce.evolver.content {
|
|
174
|
-
enabled = true
|
|
175
|
-
http-port = 8450
|
|
176
|
-
bind-host = "0.0.0.0"
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
data.base = "/data/chain"
|
|
180
|
-
state.snapshots = "snapshots"
|
|
181
|
-
# BOTH LISTENERS, and the pair is the point.
|
|
182
|
-
#
|
|
183
|
-
# 8440 PLAIN is what the agent and the registry talk to. They are ordinary HTTP clients in their
|
|
184
|
-
# own containers, and the alternative is not "use TLS" — this node's HTTPS listener presents its
|
|
185
|
-
# own IDENTITY certificate, self-signed, CN=<nodeId>, ~13 minutes old. It rotates, so it cannot
|
|
186
|
-
# be pinned as a CA, so a client that verifies it must turn verification off process-wide. For
|
|
187
|
-
# the agent that would also stop it verifying GoToSocial, which is not a trade worth making for
|
|
188
|
-
# a hop between two containers on one network.
|
|
189
|
-
#
|
|
190
|
-
# 8441 TLS is what PEERS use, and it exists so `ce.evolver.content` above can: the content peer
|
|
191
|
-
# route identifies its requester by client certificate, and refuses to start without somewhere
|
|
192
|
-
# mTLS to live. NOT PUBLISHED in docker-compose.yml — this node is a reader behind NAT that
|
|
193
|
-
# fetches outbound and serves nobody, so nothing needs to dial it.
|
|
194
|
-
#
|
|
195
|
-
# ⚠️ IF THIS NODE EVER SERVES CONTENT, `evolver.peer-discovery.api-url` must name 8441 and that
|
|
196
|
-
# port must be publicly reachable. It is empty here (see above): a node with no routable address
|
|
197
|
-
# advertises nothing and can only fetch.
|
|
198
|
-
http.port = 8440
|
|
199
|
-
# Only this private Compose network may reach the unsigned processing API.
|
|
200
|
-
http.bind-host = "0.0.0.0"
|
|
201
|
-
# ⚠️ REQUIRED SINCE 131 (#131), and without it this stack breaks. `http.bind-host` now defaults
|
|
202
|
-
# to 127.0.0.1 — the plain listener joined the bind-is-authorization set the four local listeners
|
|
203
|
-
# above already belong to, because it carries the whole API with no token and no client
|
|
204
|
-
# certificate, including (this node runs `ce.evolver.execution.enabled`) the SINGLE-CONSUMER
|
|
205
|
-
# cursor `GET /api/channel/next`, which a stranger can step past the registry's own deliveries.
|
|
206
|
-
#
|
|
207
|
-
# The claim being made here is the same one `ce.evolver.content.bind-host` and the p2p/login ones
|
|
208
|
-
# above make: THIS COMPOSE NETWORK IS THE BOUNDARY. It holds because 8440 is NOT published in
|
|
209
|
-
# docker-compose.yml and nothing uses host networking — the agent and the registry reach this
|
|
210
|
-
# node as `node:8440`, a service name on that network. Publish the port, or run with host
|
|
211
|
-
# networking, and the claim stops being true. The node cannot tell, so it logs a WARN naming it.
|
|
212
|
-
http.bind-host = "0.0.0.0"
|
|
213
|
-
https.port = 8441
|
|
214
|
-
evolver.grpc.port = 9000
|
|
215
|
-
|
|
216
|
-
# REQUIRED, and there is no default: `NodeBootstrap.initChainDb` reads this block
|
|
217
|
-
# directly, so a node without it dies with `No configuration setting found for key
|
|
218
|
-
# 'chainDb'` after the migrations have already run — which is exactly how this omission
|
|
219
|
-
# was found. Same values as the bundled deployment's node.
|
|
220
|
-
chainDb {
|
|
221
|
-
datasource {
|
|
222
|
-
driver = "org.hsqldb.jdbc.JDBCDriver"
|
|
223
|
-
user = "sa"
|
|
224
|
-
pass = ""
|
|
225
|
-
maxPoolSize = 5
|
|
226
|
-
connectionProperties = ""
|
|
227
|
-
cachePrepStmts = true
|
|
228
|
-
prepStmtCacheSize = 250
|
|
229
|
-
prepStmtCacheSqlLimit = 2048
|
|
230
|
-
connectionTimeout = 5000
|
|
231
|
-
useServerPrepStmts = false
|
|
232
|
-
transactionIsolationLevel = "TRANSACTION_READ_COMMITTED"
|
|
233
|
-
}
|
|
234
|
-
viewCachesSize = 50
|
|
235
|
-
useShutdownHook = false
|
|
236
|
-
freeBlobsEarly = false
|
|
237
|
-
}
|
|
6
|
+
ce.evolver.genesis-file = "/config/genesis.devnet.conf"
|
|
238
7
|
|
|
8
|
+
include required(file("/config/node-follower.conf"))
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Local-chain wrapper: selects the committee-exported public genesis, then the follower body.
|
|
2
|
+
include file("/public-genesis/genesis.conf")
|
|
3
|
+
|
|
4
|
+
# 089 chain-spec pin: the canonical hash of the genesis file IS the genesis block id.
|
|
5
|
+
# Any drift refuses to boot rather than forking quietly.
|
|
6
|
+
ce.evolver.genesis-file = "/public-genesis/genesis.conf"
|
|
7
|
+
|
|
8
|
+
include required(file("/config/node-follower.conf"))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"operatorIdentity":"evolvermeerkatA","devnet":{"origin":"https://devnet.mcsherrylabs.com","chainId":"c8925985ae7948d2e608efcf863928d15476c4b561ad773fdca6f0d53d4fea05","channel":"ce.meerkat.announcements","operator":"evolvermeerkatA483344","upstreamNodeId":"evolvermeerkatnode920080","daPeers":"schednode2","allowedOutbound":"2","genesisSha256":"7f6a95cd0fcbe2306d89b2426a9473b1c6537427a82feaffff4783d9cc5dc6b9"},"sdk":"0.
|
|
1
|
+
{"operatorIdentity":"evolvermeerkatA","devnet":{"origin":"https://devnet.mcsherrylabs.com","chainId":"c8925985ae7948d2e608efcf863928d15476c4b561ad773fdca6f0d53d4fea05","channel":"ce.meerkat.announcements","operator":"evolvermeerkatA483344","upstreamNodeId":"evolvermeerkatnode920080","daPeers":"schednode2","allowedOutbound":"2","genesisSha256":"7f6a95cd0fcbe2306d89b2426a9473b1c6537427a82feaffff4783d9cc5dc6b9"},"sdk":"0.24.0","bootstrap":"0.17.0"}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
// Explicit offline operator action. Never imported/called by ordinary startup.
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import {
|
|
4
|
-
import { currentTarget } from "../src/env.js";
|
|
5
|
-
|
|
3
|
+
import { recoverManagedOwner } from "@mcsherrylabs/evolver-client/processor";
|
|
4
|
+
import { currentTarget, registryStatePath } from "../src/env.js";
|
|
5
|
+
/** Offline forced owner-lock recovery for the registry. Delegates to the SDK's
|
|
6
|
+
* `recoverManagedOwner`, which owns the usage error, the `[registry] ` prefix and the
|
|
7
|
+
* explicit-acknowledgement warning; only the state path is Meerkat's. The exported name is
|
|
8
|
+
* kept for evolver-mcp's fault fixture. */
|
|
6
9
|
export function recoverMeerkatOwner(args, target, say = console.error) {
|
|
7
|
-
|
|
8
|
-
throw new Error("usage: recover-owner --force (only after stopping ALL collectors and disabling automatic restarts)");
|
|
9
|
-
say("WARNING: --force acknowledges ALL collectors are stopped and automatic restarts disabled; this command cannot verify that.");
|
|
10
|
-
const result = recoverProcessorOwnerLock({ path: processorStatePath(target), force: true });
|
|
11
|
-
say(`${result.removed ? "Removed owner lock" : "No owner lock present"}: ${result.marker}. Database/WAL, progress, subscriptions and keys unchanged. Start ONE collector normally with the SAME target/state environment to acquire a new lock.`);
|
|
12
|
-
return result;
|
|
10
|
+
return recoverManagedOwner(args, { statePath: registryStatePath(target), service: "registry", say });
|
|
13
11
|
}
|
|
14
12
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
15
13
|
recoverMeerkatOwner(process.argv.slice(2), currentTarget());
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
// Explicit fresh-resource operation; NEVER imported/called by ordinary startup or upgrade.
|
|
2
2
|
import { chown, chmod, mkdir, readdir } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { initializeSubscriptions } from "@mcsherrylabs/evolver-client/processor";
|
|
5
|
-
import {
|
|
6
|
-
import { processorConfigPath } from "../src/runtime.js";
|
|
4
|
+
import { initializeSubscriptions, MANAGED_PROCESSOR_DEFAULTS } from "@mcsherrylabs/evolver-client/processor";
|
|
5
|
+
import { ROOT } from "../src/env.js";
|
|
7
6
|
const raw = process.env.EVOLVER_INITIAL_CHANNELS;
|
|
8
7
|
if (raw === undefined)
|
|
9
8
|
throw new Error("set EVOLVER_INITIAL_CHANNELS to an explicit JSON array (including []); initialization is fresh-only");
|
|
10
9
|
const channels = JSON.parse(raw);
|
|
11
10
|
if (!Array.isArray(channels) || channels.some(id => typeof id !== "string"))
|
|
12
11
|
throw new Error("EVOLVER_INITIAL_CHANNELS must be a JSON string array");
|
|
13
|
-
const path =
|
|
12
|
+
const path = MANAGED_PROCESSOR_DEFAULTS.paths.subscriptions;
|
|
14
13
|
const directory = dirname(path);
|
|
15
14
|
if (process.getuid?.() === 0) {
|
|
16
15
|
const uid = Number(process.env.EVOLVER_OPERATOR_UID), gid = Number(process.env.EVOLVER_OPERATOR_GID);
|
|
@@ -32,7 +32,7 @@ export function readEnv() {
|
|
|
32
32
|
continue; // empty ⇒ treat as missing, so the required-keys check reports it
|
|
33
33
|
out[t.slice(0, i).trim()] = v;
|
|
34
34
|
}
|
|
35
|
-
for (const k of ["EVOLVER_REGISTRY", "REGISTRY", "EVOLVER_VERSION", "APP_NAME", "APP_VERSION"]) {
|
|
35
|
+
for (const k of ["EVOLVER_REGISTRY", "REGISTRY", "EVOLVER_VERSION", "APP_NAME", "APP_VERSION", "EVOLVER_MCP_VERSION"]) {
|
|
36
36
|
if (!out[k])
|
|
37
37
|
throw new Error(`${ENV_PATH}: ${k} is missing — it is required, see the comments in that file`);
|
|
38
38
|
}
|
|
@@ -63,15 +63,10 @@ export function updateEnv(updates) {
|
|
|
63
63
|
}
|
|
64
64
|
export const appImage = (e = readEnv()) => `${e.REGISTRY}/${e.APP_NAME}:${e.APP_VERSION}`;
|
|
65
65
|
export const deployRef = (e = readEnv()) => `${e.REGISTRY}/${e.APP_NAME}-deploy:${e.APP_VERSION}`;
|
|
66
|
-
/** The
|
|
67
|
-
*
|
|
68
|
-
export const
|
|
69
|
-
/** The app PLUS ITS OWN NODE, that node joining devnet — from `docker-compose.devnet.yml`.
|
|
70
|
-
*
|
|
71
|
-
* Three refs because they are three different things to hand someone, and the difference is
|
|
72
|
-
* not cosmetic: one carries a throwaway chain, one borrows somebody else's node on a real
|
|
73
|
-
* chain, and one brings a node of its own onto that real chain (which is the only shape that
|
|
74
|
-
* can read an ordered channel stream or join a DA overlay). */
|
|
66
|
+
/** The evolver-mcp-node image the managed follower runs — a separate variable because it is the
|
|
67
|
+
* evolver-mcp project's registry/version, not this app's node image. */
|
|
68
|
+
export const mcpNodeImage = (e = readEnv()) => `${e.EVOLVER_REGISTRY}/evolver-mcp-node:${e.EVOLVER_MCP_VERSION}`;
|
|
69
|
+
/** The app PLUS ITS OWN NODE, that node joining devnet — from `docker-compose.devnet.yml`. */
|
|
75
70
|
export const devnetDeployRef = (e = readEnv()) => `${e.REGISTRY}/${e.APP_NAME}-deploy-devnet:${e.APP_VERSION}`;
|
|
76
71
|
/** Identity names are `^[A-Za-z0-9]{1,50}$` — no hyphens, dots or underscores — so an app
|
|
77
72
|
* name cannot be used verbatim. Strip it to what a node id may contain and bound it.
|