Skip to content

Commit 4cca970

Browse files
committed
fix(dappnode): bridge legacy state through schema v1
1 parent 762fccb commit 4cca970

11 files changed

Lines changed: 147 additions & 67 deletions

‎agent/CRATES_ARCHITECTURE.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ identity pair as fresh. A partial identity, any additional unversioned key, any
249249
log without a marker, upgrades, and downgrades fail closed. This narrow exception is
250250
required because autowallet atomically creates the two bootstrap identities before the
251251
builder can stamp the schema; it does not let protocol state bypass compatibility checks.
252+
The DAppNode v0.2.3 package is the explicit bridge for the previously shipped v0.1.8
253+
state: its entrypoint atomically moves `/data/.enclave` to `/data/.interfold`, and the
254+
v0.2.3 release stamps schema version 1 before a later fail-closed binary is installed.
255+
If both namespace roots exist, the bridge refuses to choose between them.
252256

253257
## Actor and message topology
254258

‎agent/flow-trace/00_INDEX.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,4 @@ _Found during source-code cross-referencing of these trace documents._
203203
| 14 | **Post-startup network lag recovery** | Resolved | `NetEventBuffer` still fails readiness closed if its broadcast input lags during startup buffering, because historical/live reconciliation would be incomplete. Once `SyncEnded` has moved it to `Running`, a `Lagged(n)` signal is warned and skipped, and the ingress receive loop continues from the oldest retained event instead of stopping the buffer and permanently deafening gossip consumers. |
204204
| 15 | **Local/network event redelivery collision** | Resolved | EventStore now treats the same HLC timestamp plus stable event ID and equal payload as an idempotent duplicate even when transport context differs (`Local` versus `Net`). It retains the first stored context and still fails closed when different payloads claim the same timestamp, preserving collision detection without panicking on sync/resync redelivery. |
205205
| 16 | **Crash-torn event-log tail** | Resolved | Startup validates active-segment frames against the commitlog index, truncates only an unindexed CRC/length-invalid physical suffix, and restores complete CRC-valid/decodable records whose tail index entry was lost. Indexed corruption remains fatal. Runtime reads return correlated errors instead of panicking query handlers, and `node validate --repair` exposes the same narrowly bounded recovery explicitly. |
206+
| 17 | **DAppNode v0.1.8 schema upgrade** | Resolved | DAppNode v0.2.3 is shipped as the required compatibility bridge. Its entrypoint atomically renames the legacy `/data/.enclave` state root to `/data/.interfold` (and refuses ambiguous dual roots), preserves existing encrypted identities, and then relies on the v0.2.3 release's one-time schema-1 stamp. Later fail-closed binaries therefore see a proven marker instead of permanently rejecting the shipped v0.1.8 datastore. |

‎agent/flow-trace/06_DEACTIVATION_AND_COMPLETION.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,11 @@ recovery as startup and refuses to remove indexed records. Runtime EventStore qu
287287
returned to the correlated caller rather than panicking the actor; committed corruption remains a
288288
startup/integrity failure.
289289

290+
For DAppNode installations, package v0.2.3 is the mandatory bridge from the shipped v0.1.8 state.
291+
It atomically moves the legacy `.enclave` custom-config root to `.interfold`, preserves the encrypted
292+
operator/libp2p identity, and lets the v0.2.3 binary stamp schema version 1 before later binaries
293+
enforce the marker. An ambiguous volume containing both roots fails closed.
294+
290295
### Restart + Persist State Diagram
291296

292297
```mermaid

‎dappnode/README.md‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,25 +155,30 @@ block heights.
155155

156156
### Credentials file
157157

158-
Create a local JSON file containing all three credential values:
158+
Create a local JSON file containing the password and operator key:
159159

160160
```json
161161
{
162162
"password": "a strong encryption password",
163-
"private_key": "0x<64 hex characters>",
164-
"network_private_key": "0x<64 hex characters>"
163+
"private_key": "0x<64 hex characters>"
165164
}
166165
```
167166

168167
Upload it in the setup wizard as **Ciphernode Credentials JSON**. DAppNode copies it to
169168
`/run/secrets/secrets.json` before starting the container. The entrypoint validates a maximum size
170169
of 16 KiB, required fields, and key encodings, then runs the exact commands supported by the pinned
171-
Interfold v0.1.8 image. Any failed command aborts startup. Wallet and network keys are encrypted in
172-
`/data`; v0.1.8 stores its password key there as a mode-`0400` file. After successful persistence,
170+
Interfold v0.2.3 image. Any failed command aborts startup. The wallet command atomically derives and
171+
stores both the Ethereum and libp2p identities. Both keys are encrypted in `/data`; v0.2.3 stores
172+
its password key there as a mode-`0400` file. After successful persistence,
173173
the entrypoint removes the combined plaintext upload. Provisioning sends the secrets through the
174174
CLI's hidden TTY prompts over stdin; plaintext credentials are never placed in process arguments or
175175
container environment variables.
176176

177+
Legacy three-field files containing `network_private_key` are accepted for upgrade compatibility,
178+
but v0.2.3 ignores that obsolete field on a fresh setup. When encrypted identity state already
179+
exists, a matching upload is removed without rewriting either identity; a password mismatch fails
180+
closed.
181+
177182
The Ethereum key must correspond to `NODE_ADDRESS`. Keep an encrypted offline backup of this JSON;
178183
do not paste its contents into package environment variables, `EXTRA_OPTS`, logs, or support
179184
bundles. On an ordinary restart, persisted credential state is reused from `/data` and the upload is
@@ -208,9 +213,8 @@ At container startup, `entrypoint.sh`:
208213
- network name and RPC URL
209214
- contract addresses and deploy blocks
210215

211-
4. Validates the uploaded credential file and programs password, network key, and wallet key. The
212-
pinned v0.1.8 network-key command is `interfold net keypair set`; an isolated Expect helper feeds
213-
its hidden prompts from stdin and failures stop startup.
216+
4. Validates the uploaded credential file and programs the password plus atomic wallet/network
217+
identity. An isolated Expect helper feeds hidden prompts from stdin and failures stop startup.
214218
5. Builds CLI args, including verbosity and `--peer` flags from `PEERS`.
215219
6. Executes:
216220

@@ -223,12 +227,22 @@ The state and databases live under `/data` inside the container, which is backed
223227
that volume includes the password key, DAppNode backups of `/data` must be encrypted and
224228
access-controlled even though the wallet and network keys inside the volume are encrypted.
225229

230+
### Required v0.1.8 upgrade bridge
231+
232+
DAppNode package v0.2.3 is the required bridge from the shipped v0.1.8 package to later binaries.
233+
On its first start it atomically renames the custom-config state root from `/data/.enclave` to
234+
`/data/.interfold`, refusing to proceed if both roots exist. The v0.2.3 binary then stamps schema
235+
version 1 using its release-era compatibility behavior. Later fail-closed binaries can therefore
236+
verify the marker instead of rejecting the old unversioned datastore. Do not skip this package when
237+
upgrading an existing v0.1.8 node, and keep a verified backup of `/data` until the bridge has started
238+
successfully.
239+
226240
## Health semantics
227241

228-
Interfold v0.1.8 does not expose a local readiness endpoint. The package health check therefore uses
242+
Interfold v0.2.3 does not expose a local readiness endpoint. The package health check therefore uses
229243
the strongest non-invasive local signals available in that release: PID 1 must be the expected
230244
`interfold start` command using `/data/config.yaml`, the protected config/password files must exist,
231-
the v0.1.8 Sled/event-log directories must be initialized, and the configured QUIC UDP listener must
245+
the v0.2.3 Sled/event-log directories must be initialized, and the configured QUIC UDP listener must
232246
be bound. This detects the old false-positive case where an unrelated process matched `pgrep`, as
233247
well as missing credentials, uninitialized persistence, and a dead network listener.
234248

‎dappnode/dappnode_package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "interfold-ciphernode.public.dappnode.eth",
3-
"version": "0.1.8",
4-
"upstreamVersion": "0.1.8",
3+
"version": "0.2.3",
4+
"upstreamVersion": "0.2.3",
55
"upstreamArg": "UPSTREAM_VERSION",
66
"upstreamRepo": "theinterfold/interfold",
77
"shortDescription": "Interfold Ciphernode - Run a node for confidential computing",

‎dappnode/docker-compose.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ services:
44
build:
55
context: .
66
args:
7-
UPSTREAM_VERSION: 0.1.8
8-
image: 'ciphernode.interfold-ciphernode.public.dappnode.eth:0.1.8'
7+
UPSTREAM_VERSION: 0.2.3
8+
image: 'ciphernode.interfold-ciphernode.public.dappnode.eth:0.2.3'
99
restart: unless-stopped
1010
# Allow the node's 30-second durability barrier to finish before Docker sends SIGKILL.
1111
stop_grace_period: 45s

‎dappnode/entrypoint.sh‎

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ CONFIG_FILE="${CONFIG_FILE:-$CONFIG_DIR/config.yaml}"
99
TEMPLATE_FILE="${TEMPLATE_FILE:-/opt/config.template.yaml}"
1010
SECRETS_FILE="${SECRETS_FILE:-/run/secrets/secrets.json}"
1111
CREDENTIAL_PROVISIONER="${CREDENTIAL_PROVISIONER:-/opt/provision-credentials.exp}"
12-
# Interfold v0.1.8 resolves a relative `key_file: key` beside a discovered
12+
LEGACY_STATE_DIR="${LEGACY_STATE_DIR:-$CONFIG_DIR/.enclave}"
13+
CURRENT_STATE_DIR="${CURRENT_STATE_DIR:-$CONFIG_DIR/.interfold}"
14+
# Interfold v0.2.3 resolves a relative `key_file: key` beside a discovered
1315
# /data/config.yaml to this path for the default node profile.
14-
PASSWORD_FILE="${PASSWORD_FILE:-$CONFIG_DIR/.enclave/config/_default/key}"
16+
PASSWORD_FILE="${PASSWORD_FILE:-$CURRENT_STATE_DIR/config/_default/key}"
1517

1618
log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$1"; }
1719
fail() {
@@ -36,6 +38,28 @@ fi
3638
[ -r "$TEMPLATE_FILE" ] || fail "configuration template is not readable: $TEMPLATE_FILE"
3739
mkdir -p "$CONFIG_DIR"
3840

41+
migrate_legacy_state() {
42+
if [ -L "$LEGACY_STATE_DIR" ] || [ -L "$CURRENT_STATE_DIR" ]; then
43+
fail "legacy/current state paths must not be symbolic links"
44+
fi
45+
if [ -e "$LEGACY_STATE_DIR" ] && [ ! -d "$LEGACY_STATE_DIR" ]; then
46+
fail "legacy state path is not a directory: $LEGACY_STATE_DIR"
47+
fi
48+
if [ -e "$CURRENT_STATE_DIR" ] && [ ! -d "$CURRENT_STATE_DIR" ]; then
49+
fail "current state path is not a directory: $CURRENT_STATE_DIR"
50+
fi
51+
if [ -d "$LEGACY_STATE_DIR" ] && [ -e "$CURRENT_STATE_DIR" ]; then
52+
fail "both legacy and current state directories exist; refusing an ambiguous upgrade"
53+
fi
54+
if [ -d "$LEGACY_STATE_DIR" ]; then
55+
log "Migrating the v0.1.8 state namespace to Interfold v0.2.3..."
56+
mv -- "$LEGACY_STATE_DIR" "$CURRENT_STATE_DIR" \
57+
|| fail "could not migrate legacy state into $CURRENT_STATE_DIR"
58+
fi
59+
}
60+
61+
migrate_legacy_state
62+
3963
# Set non-secret defaults.
4064
export NETWORK="${NETWORK:-sepolia}"
4165
export QUIC_PORT="${QUIC_PORT:-37173}"
@@ -64,12 +88,14 @@ validate_secret_file() {
6488

6589
jq -e '
6690
type == "object" and
67-
(keys | sort == ["network_private_key", "password", "private_key"]) and
91+
((keys | sort == ["password", "private_key"]) or
92+
(keys | sort == ["network_private_key", "password", "private_key"])) and
6893
(.password | type == "string" and length > 0 and length <= 1024 and
6994
test("^[^\\r\\n\\u0000]+$") and . == gsub("^\\s+|\\s+$"; "")) and
7095
(.private_key | type == "string" and test("^0x[0-9a-fA-F]{64}$")) and
71-
(.network_private_key | type == "string" and test("^0x[0-9a-fA-F]{64}$"))
72-
' "$SECRETS_FILE" >/dev/null || fail "credentials file must contain valid password, private_key, and network_private_key strings"
96+
((has("network_private_key") | not) or
97+
(.network_private_key | type == "string" and test("^0x[0-9a-fA-F]{64}$")))
98+
' "$SECRETS_FILE" >/dev/null || fail "credentials file must contain valid password and private_key strings"
7399
}
74100

75101
validate_persisted_password_file() {
@@ -82,25 +108,24 @@ validate_persisted_password_file() {
82108
configure_credentials() {
83109
validate_secret_file
84110

85-
[ -r "$CREDENTIAL_PROVISIONER" ] || fail "credential provisioner is not readable: $CREDENTIAL_PROVISIONER"
86-
87-
local provisioning_mode=new
88-
89111
if [ -e "$PASSWORD_FILE" ]; then
90112
validate_persisted_password_file
91113
jq -er '.password' "$SECRETS_FILE" | tr -d '\n' | cmp -s - "$PASSWORD_FILE" \
92114
|| fail "uploaded password does not match the persisted credential key"
93-
provisioning_mode=existing
94115
log "Using the matching persisted encryption password."
116+
rm -f "$SECRETS_FILE"
117+
log "Existing encrypted wallet/network identity was preserved."
118+
return
95119
fi
96120

121+
[ -r "$CREDENTIAL_PROVISIONER" ] || fail "credential provisioner is not readable: $CREDENTIAL_PROVISIONER"
97122
log "Provisioning encrypted credentials through hidden stdin prompts..."
98-
jq -jr '[.password, .network_private_key, .private_key][] | @base64 + "\n"' "$SECRETS_FILE" \
99-
| expect "$CREDENTIAL_PROVISIONER" "$CONFIG_FILE" "$provisioning_mode" \
123+
jq -jr '[.password, .private_key][] | @base64 + "\n"' "$SECRETS_FILE" \
124+
| expect "$CREDENTIAL_PROVISIONER" "$CONFIG_FILE" \
100125
|| fail "one or more credential commands failed"
101126

102127
# DAppNode copies fileUpload content into this container before startup.
103-
# Wallet/network keys are encrypted in /data and v0.1.8 stores the password
128+
# Wallet/network keys are encrypted in /data and v0.2.3 stores the password
104129
# key there with mode 0400. Remove the combined plaintext upload.
105130
rm -f "$SECRETS_FILE"
106131
log "Credential setup completed."

‎dappnode/healthcheck.sh‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/sh
2-
# Best available local health signal for the v0.1.8 image. That release has no
2+
# Best available local health signal for the v0.2.3 image. That release has no
33
# readiness HTTP/control endpoint, so require the expected PID 1, its exact
44
# start/config arguments, protected local credential/config files, and the QUIC
55
# listener. This is stronger than PID-only liveness but is not a chain-sync or
@@ -8,9 +8,9 @@ set -eu
88

99
PROC_ROOT="${PROC_ROOT:-/proc}"
1010
CONFIG_FILE="${CONFIG_FILE:-/data/config.yaml}"
11-
PASSWORD_FILE="${PASSWORD_FILE:-/data/.enclave/config/_default/key}"
12-
DB_PATH="${DB_PATH:-/data/.enclave/data/_default/db}"
13-
EVENT_LOG_PATH="${EVENT_LOG_PATH:-/data/.enclave/data/_default/log.0}"
11+
PASSWORD_FILE="${PASSWORD_FILE:-/data/.interfold/config/_default/key}"
12+
DB_PATH="${DB_PATH:-/data/.interfold/data/_default/db}"
13+
EVENT_LOG_PATH="${EVENT_LOG_PATH:-/data/.interfold/data/_default/log.0}"
1414
QUIC_PORT="${QUIC_PORT:-37173}"
1515
SS_BIN="${SS_BIN:-ss}"
1616
STAT_BIN="${STAT_BIN:-stat}"

‎dappnode/provision-credentials.exp‎

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/expect -f
22
# Feed credentials to the hidden interactive prompts exposed by Interfold
3-
# v0.1.8. Secrets arrive as base64-encoded lines on stdin; neither the
3+
# v0.2.3. Secrets arrive as base64-encoded lines on stdin; neither the
44
# plaintext nor the encoded value is placed in argv or the environment.
55

66
set timeout 30
@@ -55,32 +55,20 @@ proc send_at_prompt {pattern secret description} {
5555
}
5656
}
5757

58-
if {$argc != 2} {
59-
fail "expected CONFIG_FILE and provisioning mode"
58+
if {$argc != 1} {
59+
fail "expected CONFIG_FILE"
6060
}
6161
set config_file [lindex $argv 0]
62-
set mode [lindex $argv 1]
63-
if {$mode ne "new" && $mode ne "existing"} {
64-
fail "provisioning mode must be new or existing"
65-
}
6662

6763
set password [read_secret "password"]
68-
set network_private_key [read_secret "network private key"]
6964
set private_key [read_secret "wallet private key"]
7065

71-
if {$mode eq "new"} {
72-
spawn -noecho interfold password set --config $config_file
73-
send_at_prompt {Please enter a new password} $password "new password"
74-
send_at_prompt {Please confirm your password} $password "password confirmation"
75-
finish_child "password command"
76-
}
66+
spawn -noecho interfold password set --config $config_file
67+
send_at_prompt {Please enter a new password} $password "new password"
68+
send_at_prompt {Please confirm your password} $password "password confirmation"
69+
finish_child "password command"
7770
unset password
7871

79-
spawn -noecho interfold net keypair set --config $config_file
80-
send_at_prompt {Enter your network private key} $network_private_key "network private key"
81-
finish_child "network key command"
82-
unset network_private_key
83-
8472
spawn -noecho interfold wallet set --config $config_file
8573
send_at_prompt {Enter your Ethereum private key} $private_key "wallet private key"
8674
finish_child "wallet command"

‎dappnode/setup-wizard.yml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,7 @@ fields:
135135
```json
136136
{
137137
"password": "a strong encryption password",
138-
"private_key": "0x<64 hex characters>",
139-
"network_private_key": "0x<64 hex characters>"
138+
"private_key": "0x<64 hex characters>"
140139
}
141140
```
142141

0 commit comments

Comments
 (0)