From 01fbf03a4c15a88b4f433170d42d4c3a4828bd0f Mon Sep 17 00:00:00 2001 From: blockchainluffy Date: Wed, 18 Mar 2026 11:23:02 +0530 Subject: [PATCH 01/11] feat: add dkg injection script for dappnode with guide --- .../dappnode-keypers-dkg-injection-guide.md | 64 ++++ dkg_injection/inject_dkg_result_dappnode.sh | 276 ++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 dkg_injection/dappnode-keypers-dkg-injection-guide.md create mode 100755 dkg_injection/inject_dkg_result_dappnode.sh diff --git a/dkg_injection/dappnode-keypers-dkg-injection-guide.md b/dkg_injection/dappnode-keypers-dkg-injection-guide.md new file mode 100644 index 0000000..5920508 --- /dev/null +++ b/dkg_injection/dappnode-keypers-dkg-injection-guide.md @@ -0,0 +1,64 @@ +# Dappnode Keypers: How to use DKG injection script + +This guide describes the process for how **Dappnode** keypers can use the DKG injection script in the **shutter-api-1002** deployment. + +## Purpose + +To restore key material generated during previous deployment, necessary to fulfill pending decryption tasks. + +--- + +**Initial Keypers**: Keypers who were active during **eon 11**. Timestamp range: Mar-24-2025 01:03:45 PM UTC (1742821425) - Dec-01-2025 11:25:35 AM UTC (1764588335). + +--- + +## Prerequisites + +- Fully synced keyper running the shutter-api-1002 deployment version on Dappnode +- The same signing keys used for initial keyper deployment +- Dappnode backup from the initial keyper +- Access to Dappnode instance via shell + +--- + +## Process Steps + +### 1. Run Keypers with Same Signing Keys + +In the **shutter-api-1002** deployment, run the keypers with the **same signing keys** that were used previously for the initial keypers deployment and wait for them to sync with the network. + +Sync can be confirmed by this log line: + +``` +synced registry contract end-block=20044460 num-discarded-events=0 num-inserted-events=0 start-block=20044460 +``` + +The **end-block** should be (or greater than) the current head of the chain in the explorer. + +### 2. Ensure the backup is copied to the same instance + +Copy the backup to the same instance where the keyper is running. + +### 3. Run DKG Injection Script (Dappnode) + +After a keyperset transition is done, run the Dappnode DKG injection script with the backup path: + +```bash +curl -fsSL https://raw.githubusercontent.com/DAppnodePackage-shutter-api +/dkg-injection/dkg_injection/inject_dkg_result_dappnode.sh | bash -s -- +``` + +Replace `` with the actual path to your Dappnode backup archive. + +Check if there is no error in running the script. + +--- + +## Summary Checklist + +| Step | Action | +|------|--------| +| 1 | Run keypers in shutter-api-1002 with same signing keys as initial keypers and wait for keypers to sync | +| 2 | Ensure the Dappnode backup archive is copied to the same instance | +| 3 | Run the Dappnode DKG injection script with the backup archive path | + diff --git a/dkg_injection/inject_dkg_result_dappnode.sh b/dkg_injection/inject_dkg_result_dappnode.sh new file mode 100755 index 0000000..82b7b27 --- /dev/null +++ b/dkg_injection/inject_dkg_result_dappnode.sh @@ -0,0 +1,276 @@ +#!/usr/bin/env bash + +# This script overrides a selected DKG result in the live keyper database +# with the corresponding data from a Dappnode backup. The following tables are +# affected: +# - dkg_result (columns: success, error, pure_result) +# - keyper_set (columns: keypers, threshold) +# - tendermint_batch_config (columns: keypers, threshold) +# +# Usage: +# ./inject_dkg_result_dappnode.sh +# +# Ensure the node is sufficiently synced before running. If the keyper +# service is running, it will be stopped during the operation and +# restarted afterwards. The database service will be started if not +# already running, and stopped again afterwards if it was not running. + +set -euo pipefail + +EON="11" +KEYPER_CONFIG_INDEX="11" +MIN_TENDERMINT_CURRENT_BLOCK="0" + +BACKUP_CONTAINER="backup-db" +BACKUP_IMAGE="postgres:16" +BACKUP_DB="postgres" +BACKUP_USER="postgres" +BACKUP_PASSWORD="postgres" +KEYPER_DB="keyper" +BACKUP_TABLE_SUFFIX="_backup" + +TMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t inject-dkg-result)" +TABLES=( + "dkg_result:eon:${EON}:success, error, pure_result" + "tendermint_batch_config:keyper_config_index:${KEYPER_CONFIG_INDEX}:keypers, threshold" + "keyper_set:keyper_config_index:${KEYPER_CONFIG_INDEX}:keypers, threshold" +) + +log() { + echo "==> $1" +} + +usage() { + echo "Usage: $(basename "$0") " >&2 + exit 1 +} + +if [[ "$#" -ne 1 ]]; then + usage +fi + +if ! command -v tar >/dev/null 2>&1; then + echo "ERROR: required command 'tar' not found in PATH" >&2 + exit 1 +fi + +BACKUP_TARBALL_PATH="$1" + +if [[ ! -f "$BACKUP_TARBALL_PATH" ]]; then + echo "ERROR: tarball not found: $BACKUP_TARBALL_PATH" >&2 + exit 1 +fi + +if docker ps -a --format '{{.Names}}' | grep -q "^${BACKUP_CONTAINER}\$"; then + echo "ERROR: container '${BACKUP_CONTAINER}' already exists. Aborting." >&2 + exit 1 +fi + +DB_WAS_RUNNING=0 +KEYPER_WAS_RUNNING=0 + +LIVE_DB_CONTAINER="${LIVE_DB_CONTAINER:-DAppNodePackage-db.shutter-api-gnosis.dnp.dappnode.eth}" +if docker ps --format '{{.Names}}' | grep -q "^${LIVE_DB_CONTAINER}\$"; then + DB_WAS_RUNNING=1 +fi + +LIVE_KEYPER_CONTAINER="${LIVE_KEYPER_CONTAINER:-DAppNodePackage-shutter.shutter-api-gnosis.dnp.dappnode.eth}" +if docker ps --format '{{.Names}}' | grep -q "^${LIVE_KEYPER_CONTAINER}\$"; then + KEYPER_WAS_RUNNING=1 +fi + +cleanup() { + rv=$? + if [[ "$rv" -ne 0 ]]; then + echo "Aborting due to error (exit code $rv)" >&2 + fi + + log "Stopping backup container" + docker stop "$BACKUP_CONTAINER" >/dev/null 2>&1 || true + + if [[ "$KEYPER_WAS_RUNNING" -eq 1 ]]; then + log "Restarting keyper service (was running before)" + docker start "$LIVE_KEYPER_CONTAINER" >/dev/null 2>&1 || true + else + log "Leaving keyper service stopped (was not running before)" + fi + + if [[ "$DB_WAS_RUNNING" -eq 0 ]]; then + log "Stopping db service (was not running before)" + docker stop "$LIVE_DB_CONTAINER" >/dev/null 2>&1 || true + else + log "Keeping db service running (was running before)" + fi + + if [[ -d "$TMP_DIR" ]]; then + log "Removing temporary directory ${TMP_DIR}" + rm -rf "$TMP_DIR" + fi + + exit "$rv" +} +trap cleanup EXIT + +if [[ "$DB_WAS_RUNNING" -eq 0 ]]; then + log "Starting db service (was not running)" + docker start "$LIVE_DB_CONTAINER" >/dev/null +fi + +log "Checking shuttermint sync block number >= ${MIN_TENDERMINT_CURRENT_BLOCK}" +CURRENT_BLOCK=$(docker exec -i "$LIVE_DB_CONTAINER" sh -lc \ + "psql -t -A -U postgres -d ${KEYPER_DB} -c \"SELECT current_block FROM tendermint_sync_meta ORDER BY current_block DESC LIMIT 1\"" \ + 2>/dev/null | tr -d '[:space:]') + +if [[ -z "$CURRENT_BLOCK" ]]; then + echo "ERROR: failed to read shuttermint sync block number" >&2 + exit 1 +fi + +if ! [[ "$CURRENT_BLOCK" =~ ^[0-9]+$ ]]; then + echo "ERROR: shuttermint sync block number is not an integer: $CURRENT_BLOCK" >&2 + exit 1 +fi + +if (( CURRENT_BLOCK < MIN_TENDERMINT_CURRENT_BLOCK )); then + echo "ERROR: shuttermint sync block number ($CURRENT_BLOCK) is below MIN_TENDERMINT_CURRENT_BLOCK ($MIN_TENDERMINT_CURRENT_BLOCK); aborting. Please wait until the node is sufficiently synced and try again." >&2 + exit 1 +fi + +log "Stopping keyper service" +docker stop "$LIVE_KEYPER_CONTAINER" >/dev/null 2>&1 || true + +log "Extracting keyper DB from backup" +TAR_WARNING_FLAGS=() +if tar --help 2>/dev/null | grep -q -- '--warning'; then + TAR_WARNING_FLAGS+=(--warning=no-unknown-keyword) +fi + +TAR_ERROR_FILE="${TMP_DIR}/tar-extract.err" +if ! tar "${TAR_WARNING_FLAGS[@]}" -xf "$BACKUP_TARBALL_PATH" -C "$TMP_DIR" 2>"$TAR_ERROR_FILE"; then + if ! tar "${TAR_WARNING_FLAGS[@]}" -xJf "$BACKUP_TARBALL_PATH" -C "$TMP_DIR" 2>"$TAR_ERROR_FILE"; then + echo "ERROR: failed to extract backup tarball: $BACKUP_TARBALL_PATH" >&2 + if [[ -s "$TAR_ERROR_FILE" ]]; then + cat "$TAR_ERROR_FILE" >&2 + fi + exit 1 + fi +fi + +if [[ -z "$(find "$TMP_DIR" -mindepth 1 -print -quit 2>/dev/null)" ]]; then + echo "ERROR: backup tarball extracted no files: $BACKUP_TARBALL_PATH" >&2 + exit 1 +fi + +DB_DATA_DIR="" +while IFS= read -r -d '' d; do + if [[ -d "$d" && -f "$d/PG_VERSION" ]]; then + DB_DATA_DIR="$d" + break + fi +done < <(find "$TMP_DIR" -type d -name "db-data" -print0 2>/dev/null) + +if [[ -z "$DB_DATA_DIR" || ! -d "$DB_DATA_DIR" ]]; then + echo "ERROR: could not find db-data directory (Postgres data) inside backup" >&2 + exit 1 +fi + +log "Starting backup container" +docker run -d --rm \ + --name "$BACKUP_CONTAINER" \ + -e POSTGRES_USER="$BACKUP_USER" \ + -e POSTGRES_PASSWORD="$BACKUP_PASSWORD" \ + -e POSTGRES_DB="$BACKUP_DB" \ + -v "$DB_DATA_DIR:/var/lib/postgresql/data" \ + "$BACKUP_IMAGE" >/dev/null + +log "Waiting for backup DB to become ready" +for i in {1..30}; do + if docker exec "$BACKUP_CONTAINER" pg_isready -U "$BACKUP_USER" -d "$BACKUP_DB" >/dev/null 2>&1; then + break + fi + sleep 1 +done +if ! docker exec "$BACKUP_CONTAINER" pg_isready -U "$BACKUP_USER" -d "$BACKUP_DB" >/dev/null 2>&1; then + echo "ERROR: backup DB did not become ready after 30 seconds" >&2 + exit 1 +fi + +for entry in "${TABLES[@]}"; do + IFS=: read -r TABLE KEY_COLUMN KEY_VALUE SELECT_COLUMNS <<<"$entry" + BACKUP_CSV_FILE="${TMP_DIR}/${TABLE}_backup_${KEY_COLUMN}_${KEY_VALUE}.csv" + LIVE_CSV_FILE="${TMP_DIR}/${TABLE}_live_${KEY_COLUMN}_${KEY_VALUE}.csv" + SELECT_COLUMN_LIST=() + + for col in ${SELECT_COLUMNS//,/ }; do + [[ -z "$col" ]] && continue + if [[ "$col" == "$KEY_COLUMN" ]]; then + echo "ERROR: column list for ${TABLE} must not include key column ${KEY_COLUMN}" >&2 + exit 1 + fi + SELECT_COLUMN_LIST+=("$col") + done + + if [[ "${#SELECT_COLUMN_LIST[@]}" -eq 0 ]]; then + echo "ERROR: no non-key columns specified for update in ${TABLE}" >&2 + exit 1 + fi + + SELECT_COLUMN_LIST_WITH_KEY=("$KEY_COLUMN" "${SELECT_COLUMN_LIST[@]}") + SELECT_COLUMNS_WITH_KEY=$(IFS=', '; echo "${SELECT_COLUMN_LIST_WITH_KEY[*]}") + + log "Extracting ${TABLE} row ${KEY_COLUMN}=${KEY_VALUE} from backup DB" + docker exec "$BACKUP_CONTAINER" bash -lc \ + "psql -v ON_ERROR_STOP=1 -U '$BACKUP_USER' -d '$KEYPER_DB' -c \"COPY (SELECT ${SELECT_COLUMNS_WITH_KEY} FROM ${TABLE} WHERE ${KEY_COLUMN} = '${KEY_VALUE}' LIMIT 1) TO STDOUT WITH CSV\"" \ + >"$BACKUP_CSV_FILE" 2>/dev/null + + if [[ ! -s "$BACKUP_CSV_FILE" ]]; then + echo "ERROR: no data extracted from backup DB (no row with ${KEY_COLUMN}=${KEY_VALUE} in ${TABLE})" >&2 + exit 1 + fi + + log "Extracting ${TABLE} row ${KEY_COLUMN}=${KEY_VALUE} from live DB" + docker exec -i "$LIVE_DB_CONTAINER" sh -lc \ + "psql -v ON_ERROR_STOP=1 -U postgres -d ${KEYPER_DB} -c \"COPY (SELECT ${SELECT_COLUMNS_WITH_KEY} FROM ${TABLE} WHERE ${KEY_COLUMN} = '${KEY_VALUE}' LIMIT 1) TO STDOUT WITH CSV\"" \ + >"$LIVE_CSV_FILE" 2>/dev/null || true + + if [[ ! -s "$LIVE_CSV_FILE" ]]; then + echo "ERROR: no data extracted from live DB (no row with ${KEY_COLUMN}=${KEY_VALUE} in ${TABLE})" >&2 + exit 1 + fi + + if [[ -s "$LIVE_CSV_FILE" && -s "$BACKUP_CSV_FILE" && "$(cat "$LIVE_CSV_FILE")" == "$(cat "$BACKUP_CSV_FILE")" ]]; then + log "Live row for ${TABLE} already matches backup, nothing to do" + continue + fi + + BACKUP_TABLE_NAME="${TABLE}${BACKUP_TABLE_SUFFIX}" + + log "Backing up table ${TABLE} to ${BACKUP_TABLE_NAME} in live DB" + { + echo "CREATE TABLE IF NOT EXISTS ${BACKUP_TABLE_NAME} (LIKE ${TABLE} INCLUDING ALL);" + echo "TRUNCATE ${BACKUP_TABLE_NAME};" + echo "INSERT INTO ${BACKUP_TABLE_NAME} SELECT * FROM ${TABLE};" + } | docker exec -i "$LIVE_DB_CONTAINER" psql -U postgres -d "${KEYPER_DB}" >/dev/null 2>&1 + + UPDATE_SET="" + for col in "${SELECT_COLUMN_LIST[@]}"; do + if [[ -z "$UPDATE_SET" ]]; then + UPDATE_SET="${col} = u.${col}" + else + UPDATE_SET="${UPDATE_SET}, ${col} = u.${col}" + fi + done + + log "Restoring ${TABLE} row ${KEY_COLUMN}=${KEY_VALUE}" + { + echo "BEGIN;" + echo "CREATE TEMP TABLE tmp_update AS SELECT ${SELECT_COLUMNS_WITH_KEY} FROM ${TABLE} WHERE 1=0;" + echo "COPY tmp_update FROM STDIN WITH CSV;" + cat "$BACKUP_CSV_FILE" + echo '\.' + echo "UPDATE ${TABLE} AS t SET ${UPDATE_SET} FROM tmp_update u WHERE t.${KEY_COLUMN} = u.${KEY_COLUMN};" + echo "COMMIT;" + } | docker exec -i "$LIVE_DB_CONTAINER" psql -U postgres -d "${KEYPER_DB}" >/dev/null 2>&1 +done + +log "Done" From d27cb83def0662eb6e75672cda5e9dd3a396e8c2 Mon Sep 17 00:00:00 2001 From: blockchainluffy Date: Tue, 14 Apr 2026 16:12:03 +0530 Subject: [PATCH 02/11] fix: update inject_dkg_result script to check for previous runs --- dkg_injection/inject_dkg_result_dappnode.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/dkg_injection/inject_dkg_result_dappnode.sh b/dkg_injection/inject_dkg_result_dappnode.sh index 82b7b27..6bf7b3e 100755 --- a/dkg_injection/inject_dkg_result_dappnode.sh +++ b/dkg_injection/inject_dkg_result_dappnode.sh @@ -145,9 +145,16 @@ if tar --help 2>/dev/null | grep -q -- '--warning'; then TAR_WARNING_FLAGS+=(--warning=no-unknown-keyword) fi +EXTRACT_DIR="${TMP_DIR}/backup-extract" TAR_ERROR_FILE="${TMP_DIR}/tar-extract.err" -if ! tar "${TAR_WARNING_FLAGS[@]}" -xf "$BACKUP_TARBALL_PATH" -C "$TMP_DIR" 2>"$TAR_ERROR_FILE"; then - if ! tar "${TAR_WARNING_FLAGS[@]}" -xJf "$BACKUP_TARBALL_PATH" -C "$TMP_DIR" 2>"$TAR_ERROR_FILE"; then +extract_backup() { + rm -rf "$EXTRACT_DIR" + mkdir -p "$EXTRACT_DIR" + tar "${TAR_WARNING_FLAGS[@]}" "$1" "$BACKUP_TARBALL_PATH" -C "$EXTRACT_DIR" 2>"$TAR_ERROR_FILE" +} + +if ! extract_backup -xf; then + if ! extract_backup -xJf; then echo "ERROR: failed to extract backup tarball: $BACKUP_TARBALL_PATH" >&2 if [[ -s "$TAR_ERROR_FILE" ]]; then cat "$TAR_ERROR_FILE" >&2 @@ -156,7 +163,7 @@ if ! tar "${TAR_WARNING_FLAGS[@]}" -xf "$BACKUP_TARBALL_PATH" -C "$TMP_DIR" 2>"$ fi fi -if [[ -z "$(find "$TMP_DIR" -mindepth 1 -print -quit 2>/dev/null)" ]]; then +if [[ -z "$(find "$EXTRACT_DIR" -mindepth 1 -print -quit 2>/dev/null)" ]]; then echo "ERROR: backup tarball extracted no files: $BACKUP_TARBALL_PATH" >&2 exit 1 fi @@ -167,7 +174,7 @@ while IFS= read -r -d '' d; do DB_DATA_DIR="$d" break fi -done < <(find "$TMP_DIR" -type d -name "db-data" -print0 2>/dev/null) +done < <(find "$EXTRACT_DIR" -type d -name "db-data" -print0 2>/dev/null) if [[ -z "$DB_DATA_DIR" || ! -d "$DB_DATA_DIR" ]]; then echo "ERROR: could not find db-data directory (Postgres data) inside backup" >&2 From 07b588619a5a695c4294483920ead0e1a713f78b Mon Sep 17 00:00:00 2001 From: Jannik Luhn Date: Fri, 29 May 2026 13:40:40 +0200 Subject: [PATCH 03/11] fix: robustify DKG injection --- dkg_injection/inject_dkg_result_dappnode.sh | 29 ++++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/dkg_injection/inject_dkg_result_dappnode.sh b/dkg_injection/inject_dkg_result_dappnode.sh index 6bf7b3e..a611e6a 100755 --- a/dkg_injection/inject_dkg_result_dappnode.sh +++ b/dkg_injection/inject_dkg_result_dappnode.sh @@ -191,14 +191,18 @@ docker run -d --rm \ "$BACKUP_IMAGE" >/dev/null log "Waiting for backup DB to become ready" -for i in {1..30}; do +_consecutive=0 +for i in {1..60}; do if docker exec "$BACKUP_CONTAINER" pg_isready -U "$BACKUP_USER" -d "$BACKUP_DB" >/dev/null 2>&1; then - break + _consecutive=$(( _consecutive + 1 )) + [ "$_consecutive" -ge 3 ] && break + else + _consecutive=0 fi sleep 1 done -if ! docker exec "$BACKUP_CONTAINER" pg_isready -U "$BACKUP_USER" -d "$BACKUP_DB" >/dev/null 2>&1; then - echo "ERROR: backup DB did not become ready after 30 seconds" >&2 +if [[ "$_consecutive" -lt 3 ]]; then + echo "ERROR: backup DB did not become ready after 60 seconds" >&2 exit 1 fi @@ -241,8 +245,7 @@ for entry in "${TABLES[@]}"; do >"$LIVE_CSV_FILE" 2>/dev/null || true if [[ ! -s "$LIVE_CSV_FILE" ]]; then - echo "ERROR: no data extracted from live DB (no row with ${KEY_COLUMN}=${KEY_VALUE} in ${TABLE})" >&2 - exit 1 + log "No existing row for ${TABLE} ${KEY_COLUMN}=${KEY_VALUE} in live DB, will insert" fi if [[ -s "$LIVE_CSV_FILE" && -s "$BACKUP_CSV_FILE" && "$(cat "$LIVE_CSV_FILE")" == "$(cat "$BACKUP_CSV_FILE")" ]]; then @@ -259,23 +262,23 @@ for entry in "${TABLES[@]}"; do echo "INSERT INTO ${BACKUP_TABLE_NAME} SELECT * FROM ${TABLE};" } | docker exec -i "$LIVE_DB_CONTAINER" psql -U postgres -d "${KEYPER_DB}" >/dev/null 2>&1 - UPDATE_SET="" + UPSERT_SET="" for col in "${SELECT_COLUMN_LIST[@]}"; do - if [[ -z "$UPDATE_SET" ]]; then - UPDATE_SET="${col} = u.${col}" + if [[ -z "$UPSERT_SET" ]]; then + UPSERT_SET="${col} = EXCLUDED.${col}" else - UPDATE_SET="${UPDATE_SET}, ${col} = u.${col}" + UPSERT_SET="${UPSERT_SET}, ${col} = EXCLUDED.${col}" fi done log "Restoring ${TABLE} row ${KEY_COLUMN}=${KEY_VALUE}" { echo "BEGIN;" - echo "CREATE TEMP TABLE tmp_update AS SELECT ${SELECT_COLUMNS_WITH_KEY} FROM ${TABLE} WHERE 1=0;" - echo "COPY tmp_update FROM STDIN WITH CSV;" + echo "CREATE TEMP TABLE tmp_upsert AS SELECT ${SELECT_COLUMNS_WITH_KEY} FROM ${TABLE} WHERE 1=0;" + echo "COPY tmp_upsert FROM STDIN WITH CSV;" cat "$BACKUP_CSV_FILE" echo '\.' - echo "UPDATE ${TABLE} AS t SET ${UPDATE_SET} FROM tmp_update u WHERE t.${KEY_COLUMN} = u.${KEY_COLUMN};" + echo "INSERT INTO ${TABLE} (${SELECT_COLUMNS_WITH_KEY}) SELECT ${SELECT_COLUMNS_WITH_KEY} FROM tmp_upsert ON CONFLICT (${KEY_COLUMN}) DO UPDATE SET ${UPSERT_SET};" echo "COMMIT;" } | docker exec -i "$LIVE_DB_CONTAINER" psql -U postgres -d "${KEYPER_DB}" >/dev/null 2>&1 done From 0827dcb63c483882308aed39da5bbf5228e4d666 Mon Sep 17 00:00:00 2001 From: Jannik Luhn Date: Mon, 8 Jun 2026 01:28:39 +0200 Subject: [PATCH 04/11] feat: align with latest injection script version --- dkg_injection/inject_dkg_result_dappnode.sh | 204 ++++++++++---------- 1 file changed, 106 insertions(+), 98 deletions(-) diff --git a/dkg_injection/inject_dkg_result_dappnode.sh b/dkg_injection/inject_dkg_result_dappnode.sh index a611e6a..a43a709 100755 --- a/dkg_injection/inject_dkg_result_dappnode.sh +++ b/dkg_injection/inject_dkg_result_dappnode.sh @@ -1,45 +1,55 @@ #!/usr/bin/env bash -# This script overrides a selected DKG result in the live keyper database -# with the corresponding data from a Dappnode backup. The following tables are -# affected: -# - dkg_result (columns: success, error, pure_result) -# - keyper_set (columns: keypers, threshold) -# - tendermint_batch_config (columns: keypers, threshold) +# This script injects a DKG result into the keyper database on a DAppnode deployment. +# The following tables are affected: +# - dkg_result: eon, success, error, pure_result — copied from backup +# - keyper_set: keyper_config_index, activation_block_number, keypers, threshold — hardcoded +# - tendermint_batch_config: all columns — hardcoded except eon/success/error/pure_result # -# Usage: -# ./inject_dkg_result_dappnode.sh +# The existing tables are backed up in the same database (with suffix "_backup") +# before applying changes. # -# Ensure the node is sufficiently synced before running. If the keyper -# service is running, it will be stopped during the operation and -# restarted afterwards. The database service will be started if not -# already running, and stopped again afterwards if it was not running. +# Usage: ./inject_dkg_result_dappnode.sh +# +# Ensure the node is sufficiently synced before running. If the keyper service +# is running, it will be stopped during the operation and restarted afterwards. +# The database service will be started if not already running, and stopped again +# afterwards if it was not running before. set -euo pipefail +MIN_TENDERMINT_CURRENT_BLOCK="349800" + EON="11" KEYPER_CONFIG_INDEX="11" -MIN_TENDERMINT_CURRENT_BLOCK="0" +KEYPERS="{0xe03472CCb8e011b7Dfb3343837D75Bf6C9c3324C,0x4B5E2356b666898e101627BdDc518956bcd90a03,0x23d33956940083e0E92Dd608D6E576AfbEcc83a9,0x48A0e1789C82084aE28c179bd5742454f8CD4ed6,0xfc7d75e4bb6D18591cDc1E766CE7cF231bc08fBc,0x00D82BAc88c5E60fDAfac7e534A13D0E7F3e145a,0xcc7cd01106951B4809e640873C15363609d2C58e,0x7Ca18A55b64c1509d34e964a9e323a6c71e905a2,0x0c8f3E3912F35a59ffddc9Ff1ABB8FafC89b29de,0xEbe0BE11161e8aea85733D4ff09De6470E6558Da,0x2AF3d10Ac40737bf38437e96C8EdE308f2C6A3bc,0x4521DC1B2748585E51f8631A0f4c964B6e8BC893}" +THRESHOLD="5" +ACTIVATION_BLOCK_NUMBER="44979852" +TENDERMINT_HEIGHT="723" +TENDERMINT_STARTED="true" BACKUP_CONTAINER="backup-db" -BACKUP_IMAGE="postgres:16" +BACKUP_IMAGE="postgres" BACKUP_DB="postgres" BACKUP_USER="postgres" BACKUP_PASSWORD="postgres" KEYPER_DB="keyper" -BACKUP_TABLE_SUFFIX="_backup" TMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t inject-dkg-result)" -TABLES=( - "dkg_result:eon:${EON}:success, error, pure_result" - "tendermint_batch_config:keyper_config_index:${KEYPER_CONFIG_INDEX}:keypers, threshold" - "keyper_set:keyper_config_index:${KEYPER_CONFIG_INDEX}:keypers, threshold" -) +CMD_LOG="${TMP_DIR}/cmd.log" log() { echo "==> $1" } +run_logged() { + local description="$1"; shift + if ! "$@" >"$CMD_LOG" 2>&1; then + echo "ERROR: ${description} failed" >&2 + exit 1 + fi +} + usage() { echo "Usage: $(basename "$0") " >&2 exit 1 @@ -83,6 +93,10 @@ cleanup() { rv=$? if [[ "$rv" -ne 0 ]]; then echo "Aborting due to error (exit code $rv)" >&2 + if [[ -s "$CMD_LOG" ]]; then + echo "--- Last command output ---" >&2 + cat "$CMD_LOG" >&2 + fi fi log "Stopping backup container" @@ -113,13 +127,17 @@ trap cleanup EXIT if [[ "$DB_WAS_RUNNING" -eq 0 ]]; then log "Starting db service (was not running)" - docker start "$LIVE_DB_CONTAINER" >/dev/null + run_logged "start db service" docker start "$LIVE_DB_CONTAINER" fi log "Checking shuttermint sync block number >= ${MIN_TENDERMINT_CURRENT_BLOCK}" -CURRENT_BLOCK=$(docker exec -i "$LIVE_DB_CONTAINER" sh -lc \ +if ! docker exec -i "$LIVE_DB_CONTAINER" sh -lc \ "psql -t -A -U postgres -d ${KEYPER_DB} -c \"SELECT current_block FROM tendermint_sync_meta ORDER BY current_block DESC LIMIT 1\"" \ - 2>/dev/null | tr -d '[:space:]') + >"$CMD_LOG" 2>&1; then + echo "ERROR: failed to read shuttermint sync block number" >&2 + exit 1 +fi +CURRENT_BLOCK=$(tr -d '[:space:]' <"$CMD_LOG") if [[ -z "$CURRENT_BLOCK" ]]; then echo "ERROR: failed to read shuttermint sync block number" >&2 @@ -182,13 +200,13 @@ if [[ -z "$DB_DATA_DIR" || ! -d "$DB_DATA_DIR" ]]; then fi log "Starting backup container" -docker run -d --rm \ +run_logged "start backup container" docker run -d --rm \ --name "$BACKUP_CONTAINER" \ -e POSTGRES_USER="$BACKUP_USER" \ -e POSTGRES_PASSWORD="$BACKUP_PASSWORD" \ -e POSTGRES_DB="$BACKUP_DB" \ -v "$DB_DATA_DIR:/var/lib/postgresql/data" \ - "$BACKUP_IMAGE" >/dev/null + "$BACKUP_IMAGE" log "Waiting for backup DB to become ready" _consecutive=0 @@ -206,81 +224,71 @@ if [[ "$_consecutive" -lt 3 ]]; then exit 1 fi -for entry in "${TABLES[@]}"; do - IFS=: read -r TABLE KEY_COLUMN KEY_VALUE SELECT_COLUMNS <<<"$entry" - BACKUP_CSV_FILE="${TMP_DIR}/${TABLE}_backup_${KEY_COLUMN}_${KEY_VALUE}.csv" - LIVE_CSV_FILE="${TMP_DIR}/${TABLE}_live_${KEY_COLUMN}_${KEY_VALUE}.csv" - SELECT_COLUMN_LIST=() - - for col in ${SELECT_COLUMNS//,/ }; do - [[ -z "$col" ]] && continue - if [[ "$col" == "$KEY_COLUMN" ]]; then - echo "ERROR: column list for ${TABLE} must not include key column ${KEY_COLUMN}" >&2 - exit 1 - fi - SELECT_COLUMN_LIST+=("$col") - done - - if [[ "${#SELECT_COLUMN_LIST[@]}" -eq 0 ]]; then - echo "ERROR: no non-key columns specified for update in ${TABLE}" >&2 - exit 1 - fi - - SELECT_COLUMN_LIST_WITH_KEY=("$KEY_COLUMN" "${SELECT_COLUMN_LIST[@]}") - SELECT_COLUMNS_WITH_KEY=$(IFS=', '; echo "${SELECT_COLUMN_LIST_WITH_KEY[*]}") - - log "Extracting ${TABLE} row ${KEY_COLUMN}=${KEY_VALUE} from backup DB" - docker exec "$BACKUP_CONTAINER" bash -lc \ - "psql -v ON_ERROR_STOP=1 -U '$BACKUP_USER' -d '$KEYPER_DB' -c \"COPY (SELECT ${SELECT_COLUMNS_WITH_KEY} FROM ${TABLE} WHERE ${KEY_COLUMN} = '${KEY_VALUE}' LIMIT 1) TO STDOUT WITH CSV\"" \ - >"$BACKUP_CSV_FILE" 2>/dev/null - - if [[ ! -s "$BACKUP_CSV_FILE" ]]; then - echo "ERROR: no data extracted from backup DB (no row with ${KEY_COLUMN}=${KEY_VALUE} in ${TABLE})" >&2 - exit 1 - fi - - log "Extracting ${TABLE} row ${KEY_COLUMN}=${KEY_VALUE} from live DB" - docker exec -i "$LIVE_DB_CONTAINER" sh -lc \ - "psql -v ON_ERROR_STOP=1 -U postgres -d ${KEYPER_DB} -c \"COPY (SELECT ${SELECT_COLUMNS_WITH_KEY} FROM ${TABLE} WHERE ${KEY_COLUMN} = '${KEY_VALUE}' LIMIT 1) TO STDOUT WITH CSV\"" \ - >"$LIVE_CSV_FILE" 2>/dev/null || true - - if [[ ! -s "$LIVE_CSV_FILE" ]]; then - log "No existing row for ${TABLE} ${KEY_COLUMN}=${KEY_VALUE} in live DB, will insert" - fi - - if [[ -s "$LIVE_CSV_FILE" && -s "$BACKUP_CSV_FILE" && "$(cat "$LIVE_CSV_FILE")" == "$(cat "$BACKUP_CSV_FILE")" ]]; then - log "Live row for ${TABLE} already matches backup, nothing to do" - continue - fi - - BACKUP_TABLE_NAME="${TABLE}${BACKUP_TABLE_SUFFIX}" +log "Checking dkg_result row exists in backup for eon=${EON}" +if ! docker exec "$BACKUP_CONTAINER" psql -t -A -U "$BACKUP_USER" -d "$KEYPER_DB" \ + -c "SELECT COUNT(*) FROM dkg_result WHERE eon = ${EON}" >"$CMD_LOG" 2>&1; then + echo "ERROR: failed to check dkg_result row in backup DB" >&2 + exit 1 +fi +BACKUP_DKG_COUNT=$(tr -d '[:space:]' <"$CMD_LOG") +if [[ "$BACKUP_DKG_COUNT" == "0" ]]; then + echo "ERROR: no dkg_result row for eon=${EON} in backup DB" >&2 + exit 1 +fi - log "Backing up table ${TABLE} to ${BACKUP_TABLE_NAME} in live DB" +log "Checking if backup tables already exist" +if ! docker exec -i "$LIVE_DB_CONTAINER" psql -t -A -U postgres -d "${KEYPER_DB}" \ + -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('dkg_result_backup', 'keyper_set_backup', 'tendermint_batch_config_backup')" \ + >"$CMD_LOG" 2>&1; then + echo "ERROR: failed to check backup tables" >&2 + exit 1 +fi +BACKUP_EXISTS=$(tr -d '[:space:]' <"$CMD_LOG") +if [[ "$BACKUP_EXISTS" -gt 0 ]]; then + log "Backup tables already exist — skipping backup to preserve original state" +else + log "Backing up tables" { - echo "CREATE TABLE IF NOT EXISTS ${BACKUP_TABLE_NAME} (LIKE ${TABLE} INCLUDING ALL);" - echo "TRUNCATE ${BACKUP_TABLE_NAME};" - echo "INSERT INTO ${BACKUP_TABLE_NAME} SELECT * FROM ${TABLE};" - } | docker exec -i "$LIVE_DB_CONTAINER" psql -U postgres -d "${KEYPER_DB}" >/dev/null 2>&1 - - UPSERT_SET="" - for col in "${SELECT_COLUMN_LIST[@]}"; do - if [[ -z "$UPSERT_SET" ]]; then - UPSERT_SET="${col} = EXCLUDED.${col}" - else - UPSERT_SET="${UPSERT_SET}, ${col} = EXCLUDED.${col}" - fi - done + for TABLE in dkg_result keyper_set tendermint_batch_config; do + echo "CREATE TABLE ${TABLE}_backup (LIKE ${TABLE} INCLUDING ALL);" + echo "INSERT INTO ${TABLE}_backup SELECT * FROM ${TABLE};" + done + } | docker exec -i "$LIVE_DB_CONTAINER" psql -v ON_ERROR_STOP=1 -U postgres -d "${KEYPER_DB}" >"$CMD_LOG" 2>&1 || { + echo "ERROR: failed to back up tables" >&2 + exit 1 + } +fi - log "Restoring ${TABLE} row ${KEY_COLUMN}=${KEY_VALUE}" - { - echo "BEGIN;" - echo "CREATE TEMP TABLE tmp_upsert AS SELECT ${SELECT_COLUMNS_WITH_KEY} FROM ${TABLE} WHERE 1=0;" - echo "COPY tmp_upsert FROM STDIN WITH CSV;" - cat "$BACKUP_CSV_FILE" - echo '\.' - echo "INSERT INTO ${TABLE} (${SELECT_COLUMNS_WITH_KEY}) SELECT ${SELECT_COLUMNS_WITH_KEY} FROM tmp_upsert ON CONFLICT (${KEY_COLUMN}) DO UPDATE SET ${UPSERT_SET};" - echo "COMMIT;" - } | docker exec -i "$LIVE_DB_CONTAINER" psql -U postgres -d "${KEYPER_DB}" >/dev/null 2>&1 -done +log "Injecting DKG result" +{ + echo "BEGIN;" + echo "CREATE TEMP TABLE tmp_dkg_result (eon bigint, success boolean, error text, pure_result bytea);" + echo "COPY tmp_dkg_result FROM STDIN WITH (FORMAT csv);" + docker exec "$BACKUP_CONTAINER" psql -U "$BACKUP_USER" -d "$KEYPER_DB" \ + -c "COPY (SELECT eon, success, error, pure_result FROM dkg_result WHERE eon = ${EON} LIMIT 1) TO STDOUT WITH (FORMAT csv)" + echo '\.' + echo "INSERT INTO dkg_result (eon, success, error, pure_result)" + echo " SELECT eon, success, error, pure_result FROM tmp_dkg_result" + echo " ON CONFLICT (eon) DO UPDATE SET" + echo " success = EXCLUDED.success, error = EXCLUDED.error, pure_result = EXCLUDED.pure_result;" + echo "INSERT INTO keyper_set (keyper_config_index, activation_block_number, keypers, threshold)" + echo " VALUES (${KEYPER_CONFIG_INDEX}, ${ACTIVATION_BLOCK_NUMBER}, '${KEYPERS}', ${THRESHOLD})" + echo " ON CONFLICT (keyper_config_index) DO UPDATE SET" + echo " activation_block_number = EXCLUDED.activation_block_number," + echo " keypers = EXCLUDED.keypers," + echo " threshold = EXCLUDED.threshold;" + echo "INSERT INTO tendermint_batch_config (keyper_config_index, height, keypers, threshold, started, activation_block_number)" + echo " VALUES (${KEYPER_CONFIG_INDEX}, ${TENDERMINT_HEIGHT}, '${KEYPERS}', ${THRESHOLD}, ${TENDERMINT_STARTED}, ${ACTIVATION_BLOCK_NUMBER})" + echo " ON CONFLICT (keyper_config_index) DO UPDATE SET" + echo " height = EXCLUDED.height," + echo " keypers = EXCLUDED.keypers," + echo " threshold = EXCLUDED.threshold," + echo " started = EXCLUDED.started," + echo " activation_block_number = EXCLUDED.activation_block_number;" + echo "COMMIT;" +} | docker exec -i "$LIVE_DB_CONTAINER" psql -v ON_ERROR_STOP=1 -U postgres -d "${KEYPER_DB}" >"$CMD_LOG" 2>&1 || { + echo "ERROR: failed to inject DKG result" >&2 + exit 1 +} log "Done" From 696013dd071111fe5483b428e508dba8abd71b3f Mon Sep 17 00:00:00 2001 From: Jannik Luhn Date: Mon, 8 Jun 2026 22:59:21 +0200 Subject: [PATCH 05/11] fix: prevent stdin inheritance --- dkg_injection/inject_dkg_result_dappnode.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dkg_injection/inject_dkg_result_dappnode.sh b/dkg_injection/inject_dkg_result_dappnode.sh index a43a709..4ca633e 100755 --- a/dkg_injection/inject_dkg_result_dappnode.sh +++ b/dkg_injection/inject_dkg_result_dappnode.sh @@ -133,7 +133,7 @@ fi log "Checking shuttermint sync block number >= ${MIN_TENDERMINT_CURRENT_BLOCK}" if ! docker exec -i "$LIVE_DB_CONTAINER" sh -lc \ "psql -t -A -U postgres -d ${KEYPER_DB} -c \"SELECT current_block FROM tendermint_sync_meta ORDER BY current_block DESC LIMIT 1\"" \ - >"$CMD_LOG" 2>&1; then + "$CMD_LOG" 2>&1; then echo "ERROR: failed to read shuttermint sync block number" >&2 exit 1 fi @@ -239,7 +239,7 @@ fi log "Checking if backup tables already exist" if ! docker exec -i "$LIVE_DB_CONTAINER" psql -t -A -U postgres -d "${KEYPER_DB}" \ -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('dkg_result_backup', 'keyper_set_backup', 'tendermint_batch_config_backup')" \ - >"$CMD_LOG" 2>&1; then + "$CMD_LOG" 2>&1; then echo "ERROR: failed to check backup tables" >&2 exit 1 fi From 13f7f8e7df1d1edc5ecac63f523228fb4d716497 Mon Sep 17 00:00:00 2001 From: Jannik Luhn Date: Thu, 11 Jun 2026 00:38:11 +0200 Subject: [PATCH 06/11] feat: check backup is from correct deployment --- dkg_injection/inject_dkg_result_dappnode.sh | 36 ++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/dkg_injection/inject_dkg_result_dappnode.sh b/dkg_injection/inject_dkg_result_dappnode.sh index 4ca633e..a04091a 100755 --- a/dkg_injection/inject_dkg_result_dappnode.sh +++ b/dkg_injection/inject_dkg_result_dappnode.sh @@ -25,6 +25,7 @@ KEYPER_CONFIG_INDEX="11" KEYPERS="{0xe03472CCb8e011b7Dfb3343837D75Bf6C9c3324C,0x4B5E2356b666898e101627BdDc518956bcd90a03,0x23d33956940083e0E92Dd608D6E576AfbEcc83a9,0x48A0e1789C82084aE28c179bd5742454f8CD4ed6,0xfc7d75e4bb6D18591cDc1E766CE7cF231bc08fBc,0x00D82BAc88c5E60fDAfac7e534A13D0E7F3e145a,0xcc7cd01106951B4809e640873C15363609d2C58e,0x7Ca18A55b64c1509d34e964a9e323a6c71e905a2,0x0c8f3E3912F35a59ffddc9Ff1ABB8FafC89b29de,0xEbe0BE11161e8aea85733D4ff09De6470E6558Da,0x2AF3d10Ac40737bf38437e96C8EdE308f2C6A3bc,0x4521DC1B2748585E51f8631A0f4c964B6e8BC893}" THRESHOLD="5" ACTIVATION_BLOCK_NUMBER="44979852" +BACKUP_ACTIVATION_BLOCK_NUMBER="39200771" TENDERMINT_HEIGHT="723" TENDERMINT_STARTED="true" @@ -224,7 +225,7 @@ if [[ "$_consecutive" -lt 3 ]]; then exit 1 fi -log "Checking dkg_result row exists in backup for eon=${EON}" +log "Checking backup DB state" if ! docker exec "$BACKUP_CONTAINER" psql -t -A -U "$BACKUP_USER" -d "$KEYPER_DB" \ -c "SELECT COUNT(*) FROM dkg_result WHERE eon = ${EON}" >"$CMD_LOG" 2>&1; then echo "ERROR: failed to check dkg_result row in backup DB" >&2 @@ -236,6 +237,39 @@ if [[ "$BACKUP_DKG_COUNT" == "0" ]]; then exit 1 fi +if ! docker exec "$BACKUP_CONTAINER" psql -t -A -U "$BACKUP_USER" -d "$KEYPER_DB" \ + -c "SELECT COUNT(*) FROM dkg_result WHERE eon = ${EON} AND success = true AND pure_result IS NOT NULL" >"$CMD_LOG" 2>&1; then + echo "ERROR: failed to check dkg_result fields in backup DB" >&2 + exit 1 +fi +BACKUP_DKG_VALID=$(tr -d '[:space:]' <"$CMD_LOG") +if [[ "$BACKUP_DKG_VALID" == "0" ]]; then + echo "ERROR: dkg_result row for eon=${EON} in backup does not have success=true and pure_result set" >&2 + exit 1 +fi + +if ! docker exec "$BACKUP_CONTAINER" psql -t -A -U "$BACKUP_USER" -d "$KEYPER_DB" \ + -c "SELECT COUNT(*) FROM keyper_set WHERE keyper_config_index = ${KEYPER_CONFIG_INDEX}" >"$CMD_LOG" 2>&1; then + echo "ERROR: failed to check keyper_set row in backup DB" >&2 + exit 1 +fi +BACKUP_KEYPER_SET_COUNT=$(tr -d '[:space:]' <"$CMD_LOG") +if [[ "$BACKUP_KEYPER_SET_COUNT" == "0" ]]; then + echo "ERROR: no keyper_set row for keyper_config_index=${KEYPER_CONFIG_INDEX} in backup DB" >&2 + exit 1 +fi + +if ! docker exec "$BACKUP_CONTAINER" psql -t -A -U "$BACKUP_USER" -d "$KEYPER_DB" \ + -c "SELECT COUNT(*) FROM keyper_set WHERE keyper_config_index = ${KEYPER_CONFIG_INDEX} AND activation_block_number = ${BACKUP_ACTIVATION_BLOCK_NUMBER} AND keypers = '${KEYPERS}' AND threshold = ${THRESHOLD}" >"$CMD_LOG" 2>&1; then + echo "ERROR: failed to check keyper_set values in backup DB" >&2 + exit 1 +fi +BACKUP_KEYPER_SET_VALID=$(tr -d '[:space:]' <"$CMD_LOG") +if [[ "$BACKUP_KEYPER_SET_VALID" == "0" ]]; then + echo "ERROR: keyper_set row for keyper_config_index=${KEYPER_CONFIG_INDEX} in backup does not match expected values (activation_block_number=${BACKUP_ACTIVATION_BLOCK_NUMBER}, keypers, threshold=${THRESHOLD})" >&2 + exit 1 +fi + log "Checking if backup tables already exist" if ! docker exec -i "$LIVE_DB_CONTAINER" psql -t -A -U postgres -d "${KEYPER_DB}" \ -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('dkg_result_backup', 'keyper_set_backup', 'tendermint_batch_config_backup')" \ From b1a77a03f9a5ffad14e7a3050f79edc49a56a831 Mon Sep 17 00:00:00 2001 From: Jannik Luhn Date: Thu, 11 Jun 2026 00:39:26 +0200 Subject: [PATCH 07/11] fix: pin postgres version --- dkg_injection/inject_dkg_result_dappnode.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dkg_injection/inject_dkg_result_dappnode.sh b/dkg_injection/inject_dkg_result_dappnode.sh index a04091a..6a57a6c 100755 --- a/dkg_injection/inject_dkg_result_dappnode.sh +++ b/dkg_injection/inject_dkg_result_dappnode.sh @@ -30,7 +30,7 @@ TENDERMINT_HEIGHT="723" TENDERMINT_STARTED="true" BACKUP_CONTAINER="backup-db" -BACKUP_IMAGE="postgres" +BACKUP_IMAGE="postgres:16" BACKUP_DB="postgres" BACKUP_USER="postgres" BACKUP_PASSWORD="postgres" From 63fdd845bbd9e5fb62888c5ef90de9226ebb4716 Mon Sep 17 00:00:00 2001 From: Jannik Luhn Date: Thu, 11 Jun 2026 00:43:35 +0200 Subject: [PATCH 08/11] fix: explicitly pull db image This avoids running into a timeout if the image has to be downloaded --- dkg_injection/inject_dkg_result_dappnode.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dkg_injection/inject_dkg_result_dappnode.sh b/dkg_injection/inject_dkg_result_dappnode.sh index 6a57a6c..e3d6d7b 100755 --- a/dkg_injection/inject_dkg_result_dappnode.sh +++ b/dkg_injection/inject_dkg_result_dappnode.sh @@ -201,6 +201,7 @@ if [[ -z "$DB_DATA_DIR" || ! -d "$DB_DATA_DIR" ]]; then fi log "Starting backup container" +run_logged "pull backup image" docker pull "$BACKUP_IMAGE" run_logged "start backup container" docker run -d --rm \ --name "$BACKUP_CONTAINER" \ -e POSTGRES_USER="$BACKUP_USER" \ From 162d24753f94cafc46c50b86a0f30c3baab4dbc3 Mon Sep 17 00:00:00 2001 From: Jannik Luhn Date: Thu, 11 Jun 2026 00:58:43 +0200 Subject: [PATCH 09/11] fix: dkg injection readme --- .../dappnode-keypers-dkg-injection-guide.md | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/dkg_injection/dappnode-keypers-dkg-injection-guide.md b/dkg_injection/dappnode-keypers-dkg-injection-guide.md index 5920508..83fda72 100644 --- a/dkg_injection/dappnode-keypers-dkg-injection-guide.md +++ b/dkg_injection/dappnode-keypers-dkg-injection-guide.md @@ -44,13 +44,29 @@ Copy the backup to the same instance where the keyper is running. After a keyperset transition is done, run the Dappnode DKG injection script with the backup path: ```bash -curl -fsSL https://raw.githubusercontent.com/DAppnodePackage-shutter-api -/dkg-injection/dkg_injection/inject_dkg_result_dappnode.sh | bash -s -- +curl -fsSL https://raw.githubusercontent.com/shutter-network/DAppnodePackage-shutter-api/dkg-injection/dkg_injection/inject_dkg_result_dappnode.sh | bash -s -- ``` Replace `` with the actual path to your Dappnode backup archive. -Check if there is no error in running the script. +Check if there is no error in running the script. The output should look something like this: + +``` +==> Checking shuttermint sync block number >= 349800 +==> Stopping keyper service +==> Extracting keyper DB from backup +==> Starting backup container +==> Waiting for backup DB to become ready +==> Checking backup DB state +==> Checking if backup tables already exist +==> Backing up tables +==> Injecting DKG result +==> Done +==> Stopping backup container +==> Restarting keyper service (was running before) +==> Keeping db service running (was running before) +==> Removing temporary directory /tmp/tmp.7l26Tilq40 +``` --- From 16f476da8b411c61c6b7b819342df824b0aef64a Mon Sep 17 00:00:00 2001 From: Jannik Luhn Date: Thu, 11 Jun 2026 16:56:16 +0200 Subject: [PATCH 10/11] feat: update DKG injection script --- dkg_injection/inject_dkg_result_dappnode.sh | 54 +++++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/dkg_injection/inject_dkg_result_dappnode.sh b/dkg_injection/inject_dkg_result_dappnode.sh index e3d6d7b..151dd7f 100755 --- a/dkg_injection/inject_dkg_result_dappnode.sh +++ b/dkg_injection/inject_dkg_result_dappnode.sh @@ -155,8 +155,10 @@ if (( CURRENT_BLOCK < MIN_TENDERMINT_CURRENT_BLOCK )); then exit 1 fi -log "Stopping keyper service" -docker stop "$LIVE_KEYPER_CONTAINER" >/dev/null 2>&1 || true +if [[ "$KEYPER_WAS_RUNNING" -eq 1 ]]; then + log "Stopping keyper service" + docker stop "$LIVE_KEYPER_CONTAINER" >/dev/null 2>&1 +fi log "Extracting keyper DB from backup" TAR_WARNING_FLAGS=() @@ -278,20 +280,25 @@ if ! docker exec -i "$LIVE_DB_CONTAINER" psql -t -A -U postgres -d "${KEYPER_DB} echo "ERROR: failed to check backup tables" >&2 exit 1 fi -BACKUP_EXISTS=$(tr -d '[:space:]' <"$CMD_LOG") -if [[ "$BACKUP_EXISTS" -gt 0 ]]; then +NUM_EXISTING_BACKUP_TABLES=$(tr -d '[:space:]' <"$CMD_LOG") +if [[ "$NUM_EXISTING_BACKUP_TABLES" -eq 3 ]]; then log "Backup tables already exist — skipping backup to preserve original state" -else +elif [[ "$NUM_EXISTING_BACKUP_TABLES" -eq 0 ]]; then log "Backing up tables" { + echo "BEGIN;" for TABLE in dkg_result keyper_set tendermint_batch_config; do echo "CREATE TABLE ${TABLE}_backup (LIKE ${TABLE} INCLUDING ALL);" echo "INSERT INTO ${TABLE}_backup SELECT * FROM ${TABLE};" done + echo "COMMIT;" } | docker exec -i "$LIVE_DB_CONTAINER" psql -v ON_ERROR_STOP=1 -U postgres -d "${KEYPER_DB}" >"$CMD_LOG" 2>&1 || { echo "ERROR: failed to back up tables" >&2 exit 1 } +else + echo "ERROR: partial backup state — some but not all backup tables exist" >&2 + exit 1 fi log "Injecting DKG result" @@ -326,4 +333,41 @@ log "Injecting DKG result" exit 1 } +log "Verifying injected data" +if ! docker exec -i "$LIVE_DB_CONTAINER" psql -t -A -U postgres -d "${KEYPER_DB}" \ + -c "SELECT COUNT(*) FROM dkg_result WHERE eon = ${EON} AND success = true AND pure_result IS NOT NULL" \ + "$CMD_LOG" 2>&1; then + echo "ERROR: failed to verify dkg_result" >&2 + exit 1 +fi +VERIFY_DKG=$(tr -d '[:space:]' <"$CMD_LOG") +if [[ "$VERIFY_DKG" != "1" ]]; then + echo "ERROR: dkg_result verification failed — expected 1 row with success=true and pure_result set for eon=${EON}, got ${VERIFY_DKG}" >&2 + exit 1 +fi + +if ! docker exec -i "$LIVE_DB_CONTAINER" psql -t -A -U postgres -d "${KEYPER_DB}" \ + -c "SELECT COUNT(*) FROM keyper_set WHERE keyper_config_index = ${KEYPER_CONFIG_INDEX} AND activation_block_number = ${ACTIVATION_BLOCK_NUMBER} AND keypers = '${KEYPERS}' AND threshold = ${THRESHOLD}" \ + "$CMD_LOG" 2>&1; then + echo "ERROR: failed to verify keyper_set" >&2 + exit 1 +fi +VERIFY_KEYPER_SET=$(tr -d '[:space:]' <"$CMD_LOG") +if [[ "$VERIFY_KEYPER_SET" != "1" ]]; then + echo "ERROR: keyper_set verification failed — expected 1 matching row for keyper_config_index=${KEYPER_CONFIG_INDEX}, got ${VERIFY_KEYPER_SET}" >&2 + exit 1 +fi + +if ! docker exec -i "$LIVE_DB_CONTAINER" psql -t -A -U postgres -d "${KEYPER_DB}" \ + -c "SELECT COUNT(*) FROM tendermint_batch_config WHERE keyper_config_index = ${KEYPER_CONFIG_INDEX} AND height = ${TENDERMINT_HEIGHT} AND keypers = '${KEYPERS}' AND threshold = ${THRESHOLD} AND started = ${TENDERMINT_STARTED} AND activation_block_number = ${ACTIVATION_BLOCK_NUMBER}" \ + "$CMD_LOG" 2>&1; then + echo "ERROR: failed to verify tendermint_batch_config" >&2 + exit 1 +fi +VERIFY_BATCH_CONFIG=$(tr -d '[:space:]' <"$CMD_LOG") +if [[ "$VERIFY_BATCH_CONFIG" != "1" ]]; then + echo "ERROR: tendermint_batch_config verification failed — expected 1 matching row for keyper_config_index=${KEYPER_CONFIG_INDEX}, got ${VERIFY_BATCH_CONFIG}" >&2 + exit 1 +fi + log "Done" From 4d44519476704f9c73db6666df28640f8690cc67 Mon Sep 17 00:00:00 2001 From: Jannik Luhn Date: Thu, 11 Jun 2026 17:04:56 +0200 Subject: [PATCH 11/11] docs: update dkg injection guide --- .../dappnode-keypers-dkg-injection-guide.md | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/dkg_injection/dappnode-keypers-dkg-injection-guide.md b/dkg_injection/dappnode-keypers-dkg-injection-guide.md index 83fda72..8f5ee90 100644 --- a/dkg_injection/dappnode-keypers-dkg-injection-guide.md +++ b/dkg_injection/dappnode-keypers-dkg-injection-guide.md @@ -1,55 +1,64 @@ -# Dappnode Keypers: How to use DKG injection script +# How to use the DKG injection script to restore the time capsule key shares into an existing DAppNode instance -This guide describes the process for how **Dappnode** keypers can use the DKG injection script in the **shutter-api-1002** deployment. +This guide describes the process to inject the Ethereum Time Capsule Key shares generated under the initial deployment of the Shutter API Keyper set and backed up under [the initial DAppNode deployment](https://github.com/shutter-network/DAppnodePackage-shutter-api/releases/tag/chiado%400.1.0_gnosis%400.1.5) ([DAppNode Explorer link](https://dappnode.github.io/explorer/#/repo/0x8928c414c10d5eeaf2eea30702b3a0c03d52ff6f/0.1.5)) -## Purpose +This is needed to generate the time capsule decryption keys when the decryption timestamp is reached. -To restore key material generated during previous deployment, necessary to fulfill pending decryption tasks. - ---- - -**Initial Keypers**: Keypers who were active during **eon 11**. Timestamp range: Mar-24-2025 01:03:45 PM UTC (1742821425) - Dec-01-2025 11:25:35 AM UTC (1764588335). - ---- +Initial Keypers refer to the Keypers who were active during eon 11 of the initial API Keyper deployment. Timestamp range: Mar-24-2025 01:03:45 PM UTC (1742821425) - Dec-01-2025 11:25:35 AM UTC (1764588335). ## Prerequisites -- Fully synced keyper running the shutter-api-1002 deployment version on Dappnode -- The same signing keys used for initial keyper deployment -- Dappnode backup from the initial keyper -- Access to Dappnode instance via shell - ---- +- Fully synced Keyper running the latest Shutter API 1002 DAppNode deployment version. [Release](https://github.com/shutter-network/DAppnodePackage-shutter-api/compare/chiado@v0.1.0_gnosis@v0.1.9...chiado@v0.1.0_gnosis@v0.1.10) | [DAppNode Explorer link](https://dappnode.github.io/explorer/#/repo/0x8928c414c10d5eeaf2eea30702b3a0c03d52ff6f/0.1.10) +- The same Ethereum signing key used during the time capsule key collection. +- DAppNode backup of the initial Keyper keys requested in November 2025. +- Access to the DAppNode instance via shell. ## Process Steps -### 1. Run Keypers with Same Signing Keys +### 1. Start a Keyper instance with the correct Ethereum key -In the **shutter-api-1002** deployment, run the keypers with the **same signing keys** that were used previously for the initial keypers deployment and wait for them to sync with the network. +All Keypers have already been requested to start a new instance with the Ethereum signing key they used during the time capsule key generation. -Sync can be confirmed by this log line: +This step has already been performed, and all Keypers are running the latest Shutter API 1002 DAppNode deployment with the Ethereum signing key used during the time capsule collection. + +### 2. Check that your Keyper Gnosis chain is sufficiently synced + +The syncing status can be confirmed if you see the below logs: ``` -synced registry contract end-block=20044460 num-discarded-events=0 num-inserted-events=0 start-block=20044460 +synced registry contract end-block=46640633 num-discarded-events=0 num-inserted-events=0 start-block=46640633 ``` -The **end-block** should be (or greater than) the current head of the chain in the explorer. +The **end-block** should be greater than block 44980000, which corresponds to Mar 4, 2026. + +Note: Some Keypers have been running into rate-limiting issues and are not able to sync fully. This is currently not an issue as long as they are synced past the required activation block number, which they already are. -### 2. Ensure the backup is copied to the same instance +## 3. SSH into DAppNode -Copy the backup to the same instance where the keyper is running. +SSH into the DAppNode machine as described in the DAppNode docs: -### 3. Run DKG Injection Script (Dappnode) +`ssh dappnode@` -After a keyperset transition is done, run the Dappnode DKG injection script with the backup path: +from the same network. Use the password set during onboarding. -```bash +Source: +https://docs.dappnode.io/docs/user/access-your-dappnode/terminal/ + +### 4. Ensure the backup is copied to your DAppNode machine + +Copy the November Time Capsule DAppNode backup to your DAppNode machine, under a designated folder `.` + +### 5. Run the DAppNode DKG injection script + +Run the DAppNode DKG injection script and provide the correct time capsule backup path: + +```shell curl -fsSL https://raw.githubusercontent.com/shutter-network/DAppnodePackage-shutter-api/dkg-injection/dkg_injection/inject_dkg_result_dappnode.sh | bash -s -- ``` -Replace `` with the actual path to your Dappnode backup archive. +Replace `` with the actual path to your DAppNode time capsule backup archive. -Check if there is no error in running the script. The output should look something like this: +Check that there is no error when running the script. The output should look something like this: ``` ==> Checking shuttermint sync block number >= 349800 @@ -61,6 +70,7 @@ Check if there is no error in running the script. The output should look somethi ==> Checking if backup tables already exist ==> Backing up tables ==> Injecting DKG result +==> Verifying injected data ==> Done ==> Stopping backup container ==> Restarting keyper service (was running before) @@ -68,13 +78,6 @@ Check if there is no error in running the script. The output should look somethi ==> Removing temporary directory /tmp/tmp.7l26Tilq40 ``` ---- - -## Summary Checklist - -| Step | Action | -|------|--------| -| 1 | Run keypers in shutter-api-1002 with same signing keys as initial keypers and wait for keypers to sync | -| 2 | Ensure the Dappnode backup archive is copied to the same instance | -| 3 | Run the Dappnode DKG injection script with the backup archive path | +### 6. Report the result of the script +Please report the result of the script to the Shutter team under your individual Keyper group.