Reference

Upgrade Guide

Upgrade Guide

How to update your eIOU node to the latest version while preserving your wallet, transaction history, and configuration.

Table of Contents

  1. Overview
  2. What Gets Preserved
  3. How It Works
  4. Upgrade Procedures
  5. Verification
  6. Rollback
  7. Troubleshooting

Overview

eIOU uses Docker named volumes to separate user data from application code. Source code lives at /app/eiou/ (baked into the image), while user data is persisted on seven named volumes. When you upgrade to a new image, the container is recreated with updated code while those volumes are reattached, preserving wallet data, plugin state, backups, backup coordination state, and TLS material.

The short version:

# 1. Create a backup
docker exec <container> eiou backup create

# 2. Pull/build the new image, recreate the container
docker-compose -f <compose-file>.yml up -d --build

# 3. Verify
docker exec <container> eiou info

Your wallet, contacts, transaction history, and settings are preserved automatically.


What Gets Preserved

Preserved (stored on named volumes)

Data Volume Container Path Notes
Database (transactions, contacts, balances) {node}-mysql-data /var/lib/mysql All structured data (encrypted at rest via MariaDB TDE)
Wallet keys (encrypted) {node}-config /etc/eiou/config/userconfig.json Public key, encrypted private key, mnemonic
Master encryption key {node}-config /etc/eiou/config/.master.key Derived from seed phrase; recoverable via restore. Optionally encrypted with a volume passphrase (stored as .master.key.enc)
Database credentials (encrypted) {node}-config /etc/eiou/config/dbconfig.json Auto-generated; encrypted at rest with AES-256-GCM
User settings {node}-config /etc/eiou/config/defaultconfig.json Fee preferences, transport mode, update check preference, etc.
Installed plugins {node}-plugins /etc/eiou/plugins Operator-installed plugin directories; bundled plugins are re-seeded from the image on first boot
Plugin runtime state {node}-plugin-scratch /var/lib/eiou/plugin-scratch Each sandboxed plugin’s private state (keys, balances, ledgers, file-backed stores, caches) under its own 0700 subdir
Encrypted backups {node}-backups /var/lib/eiou/backups/*.eiou.enc AES-256-GCM encrypted database dumps
Backup coordination {node}-backup-locks /var/lib/eiou/backup-locks Lifecycle lock and durable pending archive-snapshot obligation
SSL certificates and certbot state {node}-ssl-cert /var/lib/eiou/ssl One volume with ssl/letsencrypt/ (certbot bookkeeping) and ssl/nginx/ (cert nginx serves on :443) subdirectories; startup.sh symlinks /etc/letsencrypt and /etc/nginx/ssl into them. Self-signed cert fingerprint stable across rebuilds

Updated automatically (overwritten by new image)

Data Container Path Notes
PHP source code /app/eiou/src/ Baked into image at build time
Web GUI files /app/eiou/www/ Baked into image at build time
API/CLI entry points /app/eiou/api/, /app/eiou/cli/ Baked into image at build time
Background processors /app/eiou/processors/ Baked into image at build time
Composer autoloader /app/eiou/vendor/ Baked into image at build time

Regenerated (not persisted across container recreation)

Data Container Path Notes
Tor hidden service keys /var/lib/tor/hidden_service/ Deterministically derived from wallet seed phrase
MariaDB TDE config /etc/mysql/conf.d/encryption.cnf Generated on first boot; recreated from master key on subsequent boots
MariaDB TDE key file /dev/shm/.mariadb-encryption-key RAM-backed; re-derived from master key on every boot

Tor address is stable: The .onion address is derived deterministically from your BIP39 seed phrase. A new container will produce the same Tor address as long as the wallet data (in the {node}-config volume) is present.


How It Works

The upgrade mechanism relies on several components working together:

1. Named Volumes Survive Container Recreation

Docker named volumes persist independently of containers. When docker-compose up recreates a container, the named volumes are reattached to the new container at the same mount points. The data on the volumes is untouched.

2. Config File Migration

Older images stored config files at /etc/eiou/ (root level). Current images expect them at /etc/eiou/config/. On startup, startup.sh detects config files at the legacy location and migrates them to /etc/eiou/config/ automatically. This ensures upgrades from any prior version work without manual intervention.

3. Source/Data Separation

Source code and user data are stored in separate locations:

  • Source code (/app/eiou/) is baked into the image at build time. When a new image is built, it contains the updated code. No runtime sync is needed.
  • User data (/etc/eiou/config/) is stored on the {node}-config named volume and is never overwritten by image updates.
  • Composer dependencies are installed during image build (composer install --no-dev --optimize-autoloader), not at runtime. The vendor directory at /app/eiou/vendor/ is baked into the image.

4. Database Migrations on Application Init

Application.php calls DatabaseSetup::runMigrations() on every startup. This adds any new tables or columns required by the updated code without affecting existing data.

5. Automatic Pre-Shutdown Backup

When the container receives SIGTERM (from docker compose up -d --build or docker compose down), graceful_shutdown() creates an encrypted database backup before stopping any processors or services. This ensures a recent backup exists even if the user forgot to run eiou backup create manually. The backup is stored on the {node}-backups volume, which is preserved across container recreations.

6. Maintenance Mode During Startup

On startup, startup.sh creates a lockfile (/tmp/eiou_maintenance.lock) before beginning database migrations. While this lockfile exists, all HTTP entry points (API, GUI, P2P transport) return 503 Service Unavailable with a Retry-After: 30 header. This prevents:

  • Requests hitting a mid-migration database schema
  • Incoming P2P messages being processed before the node is fully initialized

The response body is content-negotiated from the request’s Accept header:

  • Browsers (Accept: text/html...) receive a styled, self-contained HTML page (“Under Maintenance — Your eIOU node is starting up or going through an update. Check back in a little bit.”) with a <meta http-equiv="refresh" content="15"> so it auto-recovers without requiring JavaScript (Tor Browser friendly). The page is fully inlined — no external CSS/JS/fonts — since /app/eiou/ may be mid-rebuild and none of the app’s own assets are guaranteed resolvable.
  • API clients (Accept: application/json, */*, or missing) receive the existing JSON response ({"success": false, "status": "maintenance", "error": {"message": "...", "code": "maintenance_mode"}}).

Both paths set HTTP 503 and Retry-After: 30.

The lockfile is removed after all initialization is complete (composer install, migrations, processor startup).

7. Data-at-Rest Encryption (MariaDB TDE)

On first startup after wallet creation, MariaDB Transparent Data Encryption (TDE) is enabled automatically. The TDE key is derived from the master encryption key via HMAC-SHA256 and written to /dev/shm (RAM-backed, lost on restart). MariaDB is restarted once to load the file_key_management plugin, then all existing tables are encrypted. On subsequent boots, the TDE key is re-derived before MariaDB starts — no user action is needed.

Database credentials in dbconfig.json are also encrypted at rest with AES-256-GCM on first boot after the master key becomes available.

8. Update Version Check

A daily cron job (2 AM UTC) checks Docker Hub for newer image tags and caches the result in /etc/eiou/config/update-check.json. If Docker Hub is unreachable, it falls back to GitHub Releases. The check is read-only, cached for 24 hours, and respects the user’s updateCheckEnabled setting (enabled by default). Tor-only nodes silently skip the check. When an update is available, a notification is shown in the GUI dashboard.

Visual Flow

Old Container (running v1) — receives SIGTERM:
  1. Pre-shutdown backup   — encrypted backup saved to {node}-backups volume
  2. Processor shutdown    — SIGTERM to all PHP processors, wait for completion
  3. Service shutdown      — web server, MariaDB, Tor, cron stopped in order
  4. Lockfile cleanup      — processor lockfiles and shutdown flag removed

         │  container removed, volumes kept, new image built
         ▼

New Container (running v2) — startup.sh runs:
  1. Maintenance mode ON   — /tmp/eiou_maintenance.lock created (HTTP → 503)
  2. Config migration      — moves legacy config files to /etc/eiou/config/ if needed
  3. Volume decryption     — if volume passphrase active, decrypt master key to /dev/shm
  4. TDE key setup         — derive MariaDB TDE key from master key, write to /dev/shm;
                             if encryption.cnf lost (container rebuild), recreate it
                             from master key so MariaDB can read encrypted data
  5. MariaDB version check — compare binary version to stored version on volume;
                             if mismatch, use force-recovery to regenerate redo logs
  5b. Missing redo log check — if ibdata1 exists but ib_logfile0 does not (broken
                             prior container), move broken data aside, reinitialize
                             MariaDB, recreate database + tables from config, enable
                             TDE, and auto-restore from latest backup
  6. Services start        — web server, MariaDB, Tor, cron
  7. MariaDB upgrade       — if version changed, run mariadb-upgrade + store new version
  8. Database migrations   — adds new tables/columns as needed
  9. TDE first-time setup  — if new, encrypt existing tables (MariaDB restarts once)
  10. Credential encryption — encrypt dbconfig.json credentials if not already encrypted
  11. Cron jobs            — install update check, analytics, backup cron entries
  12. Maintenance mode OFF — lockfile removed, HTTP requests accepted
  13. Processors start     — P2P, Transaction, Cleanup, ContactStatus

Result:
  ├── /var/lib/mysql             ← same volume reattached (data intact)
  ├── /app/eiou/                 ← new source code (baked into image)
  ├── /etc/eiou/config/          ← same volume reattached (wallet configuration)
  ├── /etc/eiou/plugins/         ← same volume reattached (installed plugins)
  ├── /var/lib/eiou/plugin-scratch ← same volume reattached (plugin state)
  ├── /var/lib/eiou/backups      ← same volume reattached (encrypted backups)
  ├── /var/lib/eiou/backup-locks ← same volume reattached (backup coordination)
  └── /var/lib/eiou/ssl          ← same volume reattached (TLS state)

Upgrade Procedures

Method 1: Local Build (from source)

Use this when you have the repository cloned and want to build from the latest source.

cd /path/to/eiou-docker

# Pull the latest source
git pull origin main

# Create a pre-upgrade backup
docker exec <container-name> eiou backup create

# Rebuild image and recreate container (volumes preserved)
docker-compose -f <compose-file>.yml up -d --build

What happens:

  1. Docker builds a new image from the updated source
  2. Docker Compose sends SIGTERM to the running container
  3. Automatic pre-shutdown backup is created (encrypted, stored on {node}-backups volume)
  4. PHP processors receive SIGTERM and finish their current work gracefully
  5. Services stop in reverse order (web server, MariaDB, Tor, cron)
  6. Container is removed, named volumes are kept
  7. New container starts with maintenance mode enabled (HTTP requests return 503)
  8. New source code is available at /app/eiou/ (baked into the image, including updated vendor dependencies)
  9. Database migrations run if needed (idempotent — only new tables/columns added)
  10. Maintenance mode is released — HTTP requests are accepted again
  11. Background processors start normally

Method 2: Docker Hub Pull

Use this when pulling a pre-built image from Docker Hub.

# Create a pre-upgrade backup
docker exec <container-name> eiou backup create

# Pull the latest image
docker pull eiou/eiou:latest

# Recreate container with the new image
docker-compose -f <compose-file>.yml up -d

Note: If your docker-compose file uses build: instead of image:, you need to either change it to image: eiou/eiou:latest or use Method 1 instead.

Method 3: Multi-Node Upgrade (4-line, 10-line, cluster)

For multi-node topologies, all nodes are upgraded together since they share the same image.

cd /path/to/eiou-docker

# Pull latest source
git pull origin main

# Create backups for all nodes
docker-compose -f docker-compose-4line.yml exec alice eiou backup create
docker-compose -f docker-compose-4line.yml exec bob eiou backup create
docker-compose -f docker-compose-4line.yml exec carol eiou backup create
docker-compose -f docker-compose-4line.yml exec daniel eiou backup create

# Rebuild and recreate all containers
docker-compose -f docker-compose-4line.yml up -d --build

Each node’s named volumes (alice-mysql-data, alice-config, alice-backups, alice-backup-locks, etc.) are reattached to their respective new containers.


Verification

After upgrading, verify the node is running correctly:

# Check container is healthy
docker ps | grep eiou

# Check node information (wallet address, hostname, etc.)
docker exec <container-name> eiou info --show-auth

# Check backup list (should include pre-upgrade backup)
docker exec <container-name> eiou backup list

# View startup logs for any errors
docker logs <container-name> 2>&1 | head -100

What to look for in the logs:

  • Wallet already configured or eIOU has been initiated — existing wallet detected on volume
  • MariaDB TDE: key file ready — TDE key derived from master key successfully
  • Enabling MariaDB data-at-rest encryption... — first-time TDE setup (normal on first upgrade to a TDE-enabled version)
  • MariaDB version change detected: X.Y.Z -> A.B.C — version mismatch detected, redo logs will be cleaned (normal after image rebuild with a different MariaDB patch)
  • MariaDB upgrade completed successfully — mariadb-upgrade ran after version change
  • MariaDB: Adding version tracking — first boot with version tracking enabled (normal on first upgrade to v0.1.6+)
  • Update check cron job installed (daily at 2 AM UTC) — version check active
  • Analytics cron job installed (daily at 3 AM UTC) — analytics cron active
  • eIOU Node started successfully! — all processors running, ready to receive

Rollback

If the new version has issues and you need to go back:

Option A: Rebuild from an Older Commit

cd /path/to/eiou-docker

# Check out the previous working version
git log --oneline -10       # find the commit hash
git checkout <commit-hash>

# Rebuild with the old code
docker-compose -f <compose-file>.yml up -d --build

The older image contains the previous source code at /app/eiou/. Your data volumes remain untouched.

Option B: Restore from Backup

If the database was affected by a migration issue:

# List available backups
docker exec <container-name> eiou backup list

# Stop the original node. Do not remove its volumes.
docker compose down

# Save this as recovery-candidate.yml. It replaces the MySQL and backup-locks
# mounts as a paired generation; config, backups, plugins, and certificates
# remain the original node's volumes.
cat > recovery-candidate.yml <<'YAML'
services:
  eiou:
    environment:
      EIOU_BOOT_RECOVERY_BACKUP: "${RECOVERY_BACKUP:?set RECOVERY_BACKUP}"
    volumes:
      - recovery-mysql:/var/lib/mysql
      - recovery-backup-locks:/var/lib/eiou/backup-locks
volumes:
  recovery-mysql:
    # Replace this with a unique name that has never held database data.
    name: replace-with-a-new-empty-candidate-volume-name
  recovery-backup-locks:
    # Keep this unique volume paired with the candidate MySQL generation.
    name: replace-with-a-new-empty-candidate-lock-volume-name
YAML

# Restore into the empty candidate and follow startup to completion.
export RECOVERY_BACKUP=<backup-filename>
docker compose -f docker-compose.yml -f recovery-candidate.yml up -d
docker compose -f docker-compose.yml -f recovery-candidate.yml logs -f eiou

# Validate the candidate before promotion.
docker compose -f docker-compose.yml -f recovery-candidate.yml exec eiou eiou info
docker compose -f docker-compose.yml -f recovery-candidate.yml exec eiou eiou verify-chain

The wallet GUI can create and verify backups, but online restore is disabled: mysqldump DDL is not atomic against a serving database. Restore into an isolated new-volume candidate during pre-service boot, validate it, then promote the candidate. To promote this candidate, first remove the entire environment: block from recovery-candidate.yml; it is a one-boot recovery request. Keep the remaining override in every future Compose command so the service continues to mount the candidate MySQL and backup-locks volumes as one generation. Retain the original MySQL and backup-locks pair until the restored node has been validated and backed up; the candidate never mutates their recovery journal. Do not copy lifecycle.lock or the recovery/ directory between lock volumes. If an archive-backup-pending obligation must follow the promoted candidate, copy only that marker while both nodes are stopped. After removing the environment block, run unset RECOVERY_BACKUP.

v0.1.18-alpha backup cutoff: earlier encrypted artifacts are intentionally rejected. Restore one with the earlier image that created it, then immediately create a v3 full snapshot. Both scheduled and archive-triggered v3 snapshots contain the complete database and an authenticated schema/table manifest. The encrypted record stream is chunked so memory stays bounded as history grows. This is a hard legacy cutoff.


Troubleshooting

Container starts but wallet is gone

The named volumes were removed (likely by docker-compose down -v). The -v flag deletes all named volumes.

Recovery options:

  1. From backup: If the {node}-backups volume still exists, or you have a backup file, restore from it
  2. From seed phrase: If you have your 24-word mnemonic, restore the wallet:
    environment:
      - RESTORE=word1 word2 word3 ... word24
    The master key is derived deterministically from the seed phrase, so restoring from seed recovers the same master key. Old encrypted backups remain decryptable.

Database connection error after upgrade

The dbconfig.json file may have been accidentally removed or corrupted. Check that it exists:

docker exec <container-name> cat /etc/eiou/config/dbconfig.json

If missing, the database credentials are lost. You would need to restore from backup with a fresh wallet setup.

SSL certificate warnings after upgrade

Self-signed SSL certificates are regenerated when a container is recreated. This is normal. If you use external certificates, ensure your bind mount is configured:

volumes:
  - /path/to/certs:/ssl-certs:ro

MariaDB fails to start after upgrade

Version mismatch (most common after image rebuild): If MariaDB was upgraded to a different patch version between image builds (e.g., 10.11.6 → 10.11.14), the InnoDB redo logs on the persistent volume are incompatible with the new binary. The error log shows: Reading log encryption info failed; the log was created with MariaDB X.Y.Z. Starting with v0.1.8-alpha, startup.sh handles this automatically:

  1. Before starting MariaDB, it compares the binary version against /var/lib/mysql/.mariadb_version
  2. On mismatch, it starts MariaDB with innodb_force_recovery=1 to bypass stale redo logs
  3. It performs a clean shutdown to regenerate redo logs in the new version’s format
  4. It restarts MariaDB normally, then runs mariadb-upgrade and stores the new version

If the proactive check is bypassed (e.g., first boot with version tracking), a reactive fallback applies the same force-recovery when the normal startup times out. If all recovery fails, the container exits with a FATAL message instead of looping forever.

Missing redo log (ib_logfile0): If the prior container crashed during initialization or was otherwise broken, the persistent volume may have ibdata1 (InnoDB system tablespace) but no ib_logfile0 (redo log). MariaDB refuses to start without this file, and no innodb_force_recovery level bypasses this. Starting with v0.1.8-alpha, startup.sh detects this condition and performs automatic recovery: moves broken data to /tmp/mysql-broken-<timestamp>/, reinitializes MariaDB with mysql_install_db, recreates the database and tables from the config volume, enables TDE encryption, and auto-restores from the latest backup. Wallet identity is preserved because userconfig.json (keys, .onion address) on the config volume is never modified. The recovery is crash-safe — if the process dies mid-recovery, the next boot retriggers the same flow. The error log shows InnoDB: File ./ib_logfile0 was not found followed by Plugin 'InnoDB' registration as a STORAGE ENGINE failed.

TDE encryption config lost after container rebuild: The encryption.cnf file lives in the container filesystem (/etc/mysql/conf.d/), not on a volume. When the container is recreated (docker compose up -d --build), this file is lost, but the mysql-data volume still has TDE-encrypted redo logs and tablespace files. MariaDB fails with: Obtaining redo log encryption key version 1 failed. Starting with v0.1.8-alpha, the pre-MariaDB TDE key setup detects this condition: if the master key is available and a database exists on the volume, it recreates encryption.cnf and the TDE key file automatically. No manual action needed.

Missing TDE key file: If the TDE encryption plugin was enabled on a previous boot but the TDE key file is missing, MariaDB cannot read its encrypted tables. Check the logs for WARNING: Failed to prepare TDE key file. This can happen if the master key is unavailable (e.g., volume passphrase not provided). Ensure the {node}-config volume has .master.key (or .master.key.enc with the correct EIOU_VOLUME_KEY_FILE).

Volume passphrase lost

If you enabled volume passphrase encryption (EIOU_VOLUME_KEY_FILE) and lost the passphrase, the encrypted master key cannot be decrypted. Recovery requires restoring from your 24-word seed phrase, which re-derives the master key from scratch. Old encrypted backups remain decryptable after seed restore.

Before starting seed recovery, either mount a readable replacement at the configured EIOU_VOLUME_KEY_FILE path or remove that setting to explicitly return to plaintext master-key storage. Recovery fails closed when the configured file is missing so the node cannot complete once and then become unbootable again at the next container restart.

Recovery logs a RECOVERY POINT line before the first database mutation. It contains the authenticated backup filename, backup id, artifact modification time, and selected key path. Treat that timestamp as an operator-visible estimate: transactions committed after the snapshot are not present in the recovered database.

The recovery journal uses same-filesystem temporary files, atomic renames, and directory synchronization. Keep the MySQL and backup-locks paths on durable Docker volumes backed by a filesystem that honors fsync and atomic rename. Network filesystems, copy-on-write volume exporters, Docker Desktop/WSL2, and storage restored from snapshots should be validated with the recovery crash-test suite before being relied upon for disaster recovery.

Persistent “Chain Gap” on a contact after upgrading from a pre-fix version

If a transaction you sent to a contact was cancelled while still pending (before being signed and delivered) on a version before the cancelled/rejected chain-link fix, any transaction you sent to that contact afterwards was signed with its previous_txid pointing at that unsigned cancelled row. The peer never received the cancelled row (sync excludes cancelled/rejected) so they can’t verify the successor, and the contact’s chain status gets stuck as Action Required / Chain Gap.

New transactions created after the upgrade will chain correctly — the fixed getPreviousTxid skips cancelled/rejected rows when picking the predecessor. Pre-upgrade broken chains are not automatically migrated because re-signing a peer-received transaction requires the sender’s private key; the receiver can’t fix it locally. Resolve a pre-existing broken chain by accepting the tx-drop proposal the sender will auto-propose on the next sync, or manually from the contact’s detail panel. This is exactly what the tx-drop flow was originally designed for.

Permissions errors in logs

The source sync may have set incorrect permissions. This is usually handled automatically, but if errors persist:

docker exec <container-name> bash -c "
  chown -R www-data:www-data /etc/eiou/config/
  chmod 600 /etc/eiou/config/.master.key
  chmod 600 /etc/eiou/config/userconfig.json
  chmod 600 /etc/eiou/config/dbconfig.json
"

Important Notes

Do NOT use docker-compose down -v

The -v flag removes named volumes, which permanently deletes all wallet data, transaction history, and backups. Only use -v if you intentionally want to start fresh.

Command Volumes Safe for upgrade?
docker-compose down Preserved Yes
docker-compose up -d --build Preserved Yes
docker-compose down -v Deleted No – data loss

Back Up Your Seed Phrase

Your 24-word BIP39 mnemonic is displayed only once during initial wallet generation. Store it securely offline. It is your last-resort recovery method if all volumes are lost.

Back Up Before Every Upgrade

Always run eiou backup create before upgrading. The encrypted backup file can be used to restore your database if anything goes wrong. Backup files are stored on the {node}-backups volume, which is preserved across upgrades.

Schema Migrations by Version

Database migrations run automatically on startup (step 8 above) and are idempotent — safe to run multiple times. No manual SQL is ever needed.

Schema version Change Released
v26 Added payback_method_answer_log on fresh installs and self-healed it on upgraded nodes, so responder-side payback-method request limits use the same schema on every installation v0.1.17-alpha (2026-07-27)
v10 Added transactions_archive table — cold storage for completed transactions older than transactionsArchiveRetentionDays, schema mirrors transactions + an archived_at timestamp. Added transaction_chain_checkpoints table — records gap-free-at-archival proof per bilateral pair (archived_count, archived_txid_hash [SHA-256 over sorted archived txids], highest_archived_timestamp, highest_archived_time, last_verified_gap_free_at). Populated by the nightly archival cron (transaction-archive-cron.php at 01:30 UTC). The checkpoint is consumed by verifyChainIntegrity() on every outbound send so the send hot-path stays O(recent tail); eiou verify-chain does the full O(all history) walk on demand v0.1.13-alpha (2026-04-21)
v9 Added payment_requests_archive table — cold storage for resolved (non-pending) payment requests older than paymentRequestsArchiveRetentionDays. Schema mirrors payment_requests + an archived_at timestamp. Populated by the nightly archival cron (payment-request-archive-cron.php at 01:00 UTC). Read paths UNION across live + archive, so archived rows stay queryable via GUI/CLI v0.1.13-alpha (2026-04-21)
v5 Added payment_requests table — stores both outgoing requests you sent and incoming requests from contacts, with direction, status, amount, currency, description, requester address, timestamps, and resulting txid on approval v0.1.10-alpha (2026-04-08)
v4 and earlier Prior tables (transactions, contacts, balances, P2P, DLQ, etc.) —

If you are upgrading from any version with schema ≤ v4, the payment_requests table is created automatically on first boot. No data is lost. Upgrading from schema ≤ v8 adds the new payment_requests_archive table (empty on first boot) — the archival cron starts moving rows once any resolved request is older than the configured retention window. Upgrading from schema ≤ v9 additionally adds transactions_archive and transaction_chain_checkpoints (both empty on first boot) — the transaction archival cron starts moving completed rows once any bilateral pair has rows older than the retention window AND the pair verifies gap-free at that moment.

Version Compatibility

Starting with v0.1.5-alpha, nodes enforce version compatibility. Nodes running versions below 0.1.3-alpha are rejected because earlier versions use an incompatible amount format (cents-based integers vs SplitAmount) that causes data corruption.

What happens if you don’t upgrade:

  • Other nodes running v0.1.5+ will reject your contact requests, transactions, and sync messages
  • You will see rejection responses with a message indicating the minimum required version
  • Existing contacts running newer versions will be unable to send to you

What happens when you upgrade:

  • Your node automatically includes its version in all outgoing messages and contact responses
  • Contacts learn your new version via the next message, ping, or contact handshake
  • Any previously blocked communication auto-heals — no manual action needed

Checking a contact’s version:

  • Each contact’s remote_version is stored in the database and updated automatically
  • The version is exchanged during: contact acceptance (mutual acceptance response), ping/pong, and any incoming message envelope
  • Version is NOT exposed before trust is established — the initial contact request and “received” response intentionally omit the version to prevent untrusted nodes from fingerprinting your software version. Version is only shared after contact acceptance, through message envelopes (outside the signed content), ping/pong responses, and mutual acceptance payloads

Master Key Is Derived from Seed

The AES-256 master encryption key (/etc/eiou/config/.master.key) is deterministically derived from the BIP39 seed phrase. All other encryption keys (MariaDB TDE, database credential encryption, backup encryption) are derived from this single master key. If the {node}-config volume is lost:

  • Wallet keys, Tor address, auth code, and master key are all recoverable from the seed phrase
  • Encrypted backups remain decryptable after a seed restore (the same master key is re-derived)
  • MariaDB TDE and credential encryption are re-established automatically on next boot

The seed phrase is the single recovery secret for the entire node.