Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
9a82c09
chore: scaffold DKG e2e-tests harness with shared polling helper
May 18, 2026
67c4bcb
test(e2e): add DKG happy-path task across four keyper set transitions
May 18, 2026
079dafe
test(e2e): add DKG offline-recovery task
May 18, 2026
57321f1
test(e2e): add `test` runner task for DKG e2e suite
May 18, 2026
ce53c90
chore: allow overriding env vars in test setup
May 18, 2026
f83d945
fix(e2e): use --keyper-index flag so non-member keyper sets work
May 18, 2026
1825f03
fixup! test(e2e): add DKG offline-recovery task
May 19, 2026
9263c1f
fix: use keyper config index instead of eon
May 19, 2026
bed7cfb
feat(contract): generate Go bindings for DKGContract and ECIESKeyRegi…
May 20, 2026
8dda3f7
feat(keyper/gnosis): sync ECIES Key Registry and auto-register on join
May 20, 2026
3c27849
feat(keyper/gnosis): drive DKG via on-chain phase and submit transact…
May 20, 2026
c6ee996
remove(keyper): retire Shuttermint and EonKeyPublish in favor of cont…
May 20, 2026
742f1db
feat(keyper): pass polyEvals as bytes[] via regenerated DKG binding
May 20, 2026
a948b1e
feat(keyper/gnosis): store per-keyper-set DKG params on the eons row
May 20, 2026
920f721
fix(e2e-tests): set NUM_KEYPERS=6 in e2e-tests/mise.toml [env]
May 20, 2026
63d1b32
refactor(keyper/gnosis): rename InstanceStart to DKGStart
May 20, 2026
e9cef18
feat(keyper): add tx_outbox table and TxSender service
May 20, 2026
f9d36cc
refactor(chainsync): ECIESKeySyncer iterates registry directly
May 20, 2026
58e9220
refactor(chainsync): DKGEvent as interface with concrete per-event types
May 20, 2026
a5b52ae
refactor(keyper/gnosis): drop dkgManager cache; rebuild puredkg from DB
May 20, 2026
6314f2b
refactor(keyper/gnosis): enqueue DKG submissions via tx_outbox
May 20, 2026
c0bf76a
dkg: extract DKG participation into shared package
May 20, 2026
a911446
feat(keyper/shutterservice): integrate DKG module via HandleBlock
May 20, 2026
1e5cf15
refactor(keyper/gnosis): align newkeyperset eon-exists check with shu…
May 20, 2026
dcf3009
refactor(txsender): merge submit and confirm loops into a single poll…
May 21, 2026
a9d7b26
fix(txsender): mark row submitted before broadcasting
May 21, 2026
1444e3a
feat(txsender): add caller-supplied label column and thread to logs
May 21, 2026
02424af
feat(keyper/db): rename DKG tables to plural and add dkg_initial_states
May 21, 2026
0fdccb2
refactor(dkg): rename startDealing to maybeDeal, persist initial pure…
May 21, 2026
586f533
fix(dkg): make buildPureDKG phase-aware; rename start{Accusing,Apolog…
May 21, 2026
e7d59e3
refactor(dkg): single tx per eon in HandleBlock; phase-gated dispatch
May 21, 2026
ccca130
refactor(dkg): export MaybeRegisterECIESKey; call from KeyperSetAdded…
May 21, 2026
31cd484
feat(txsender): switch outbox sender to EIP-1559 (DynamicFeeTx)
May 21, 2026
d2d9da3
refactor(dkg): strict per-eon config; remove DKGContractAddr fallback
May 21, 2026
0750341
dkg: check membership before alreadySucceeded in handleEon
May 21, 2026
cf7da1c
refactor(dkg): split handleEon into narrow read tx + narrow write tx
May 21, 2026
0069922
feat(dkg): add dkg_sent_actions table as idempotency store for reactors
May 21, 2026
d29ccd9
refactor(dkg): remove own-message writes from shared chain-event tables
May 21, 2026
8fd6cd1
feat(dkg): chain event is sole source of truth for dkg_result
May 21, 2026
405101d
fix(e2e): replace dkg_result poll with on-chain DKG failure detection
May 21, 2026
0ccbed7
fix(dkg): tolerate missing ECIES key for one receiver during dealing
May 21, 2026
9ee373c
feat(mise): add fund-keypers task
May 21, 2026
62ded35
feat(mise-test-setup): rewrite wait-for-dkg to be contract-based
May 21, 2026
59aa02d
feat(mise-test-setup): add show-dkg-status task
May 21, 2026
b8f36a6
feat(e2e): add fund-keypers to test setup
May 21, 2026
c3641fb
feat(mise-test-setup): add check-dkg task
May 21, 2026
309f344
feat(mise-test-setup): add watch-dkg-events task
May 21, 2026
99a8848
chore(mise-test-setup): drop shuttermint-era artefacts
May 21, 2026
039f198
feat(dkg): improve Info-level observability across DKG phases
May 21, 2026
385d8dd
fix(dkg): set sent-action guard even when no tx is enqueued
May 21, 2026
1bc9896
fix(mise-test-setup): cap show-dkg-status retry range at DKGSucceeded
May 21, 2026
edeafa6
fix(mise-test-setup): auto-stop watch-dkg-events on terminal DKGSucce…
May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions contracts/scripts/abigen_dkg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/bash
#
# Generate Go bindings for the DKGContract and ECIESKeyRegistry contracts.
#
# The contracts live in the top-level foundry-based contracts repo
# (../../../contracts relative to this script). We build them with forge and
# then run abigen against the resulting artifacts, producing a single
# binding_dkg.abigen.gen.go file in the contract package.

set -Eeuo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONTRACTS_DIR="$(cd "$SCRIPT_DIR/../../../contracts" && pwd)"
OUT_GO="$(cd "$SCRIPT_DIR/../../rolling-shutter/contract" && pwd)/binding_dkg.abigen.gen.go"

(cd "$CONTRACTS_DIR" && forge build --offline)

TMP_COMBINED="$(mktemp --suffix=.json)"
trap 'rm -f "$TMP_COMBINED"' EXIT

jq -n \
--slurpfile dkg "$CONTRACTS_DIR/out/DKGContract.sol/DKGContract.json" \
--slurpfile ecies "$CONTRACTS_DIR/out/ECIESKeyRegistry.sol/ECIESKeyRegistry.json" \
'{
contracts: {
"src/common/DKGContract.sol:DKGContract": {
abi: ($dkg[0].abi | tostring),
bin: $dkg[0].bytecode.object,
"bin-runtime": $dkg[0].deployedBytecode.object,
userdoc: "{}",
devdoc: "{}"
},
"src/common/ECIESKeyRegistry.sol:ECIESKeyRegistry": {
abi: ($ecies[0].abi | tostring),
bin: $ecies[0].bytecode.object,
"bin-runtime": $ecies[0].deployedBytecode.object,
userdoc: "{}",
devdoc: "{}"
}
},
version: "foundry"
}' > "$TMP_COMBINED"

abigen \
--combined-json "$TMP_COMBINED" \
--pkg contract \
--out "$OUT_GO"
26 changes: 7 additions & 19 deletions mise-test-setup/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Mise Test Setup

`mise test setup` is a local mise-driven setup for running the shutter service
flow with Ethereum, shuttermint, keypers, and supporting infrastructure.
flow with Ethereum, keypers, and supporting infrastructure.

For the normal happy path, the two main commands are:

Expand All @@ -21,14 +21,13 @@ mise run test-decryption

- Add a new keyper set on-chain for the selected keypers.

- `mise run wait-for-dkg --keyper-set-index 2`
- `mise run wait-for-dkg --ksi 2 --success`

- Wait until a given keyper set finishes DKG successfully.

- `mise run wait-for-dkg --eon 5`

- Wait until a specific DKG finishes. This exits with a nonzero status if that
eon completes with failure.
- Wait until the DKG for a given Keyper Set Index succeeds on-chain. Without
`--ksi`, defaults to the latest registered keyper set. Without `--success`
/ `--failure`, exits 0 on any completion. With `--retry N`, pins the watch
to a specific retry counter; with `--success`/`--failure`, asserts that
retry's outcome.

- `mise run test-decryption`

Expand Down Expand Up @@ -58,9 +57,6 @@ can still run them directly if you want to test specific parts of the system:
- `up-ethereum`
- `deploy`
- `gen-keyper-configs`
- `init-chain-seed`
- `init-chain-nodes`
- `patch-genesis`
- `init-keyper-dbs`
- `up`
- `down`
Expand All @@ -72,14 +68,6 @@ Dependency flow for `wait-for-initial-dkg`:
```text
- `wait-for-initial-dkg`
- `up`
- `patch-genesis`
- `init-chain-nodes`
- `init-chain-seed`
- `gen-compose`
- `gen-keyper-configs`
- `deploy`
- `up-ethereum`
- `gen-compose`
- `init-keyper-dbs`
- `up-db`
- `gen-compose`
Expand Down
15 changes: 0 additions & 15 deletions mise-test-setup/compose.common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,6 @@ services:
timeout: 1s
retries: 30

chain-seed:
image: rolling-shutter
entrypoint: /rolling-shutter
command:
- chain
- --config
- /chain/config/config.toml
volumes:
- ${DATA_DIR}/chain-seed:/chain
healthcheck:
test: curl -sf http://127.0.0.1:26657/status >/dev/null
interval: 1s
timeout: 1s
retries: 10

bootnode:
image: rolling-shutter
entrypoint: /rolling-shutter
Expand Down
Empty file removed mise-test-setup/compose.gnosis.yml
Empty file.
24 changes: 0 additions & 24 deletions mise-test-setup/compose.keypers.yml.j2
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
x-chain-base: &chain_base
image: rolling-shutter
entrypoint:
- /rolling-shutter
command:
- chain
- --config
- /chain/config/config.toml
healthcheck:
test: curl -sf http://127.0.0.1:26657/status >/dev/null
interval: 1s
timeout: 1s
retries: 10
depends_on:
chain-seed:
condition: service_healthy

x-keyper-base: &keyper_base
image: rolling-shutter
entrypoint:
Expand All @@ -26,18 +9,11 @@ x-keyper-base: &keyper_base

services:
{%- for i in range(num_keypers|int) %}
chain-{{ i }}:
<<: *chain_base
volumes:
- ../${DATA_DIR}/chain-{{ i }}/:/chain

keyper-{{ i }}:
<<: *keyper_base
depends_on:
bootnode:
condition: service_started
chain-{{ i }}:
condition: service_healthy
volumes:
- ../${DATA_DIR}/keyper-{{ i }}.toml:/config.toml
{%- if not loop.last %}
Expand Down
Empty file.
3 changes: 1 addition & 2 deletions mise-test-setup/compose.yml.j2
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
include:
- ../compose.common.yml
- ./compose.keypers.yml
- ../compose.{{ deployment_type }}.yml
- ./compose.keypers.yml
72 changes: 72 additions & 0 deletions mise-test-setup/e2e-tests/mise-tasks/e2e_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Shared helpers for DKG e2e tests.

Importable from `mise run` task scripts in this directory: the file is colocated
with the task scripts, so uv adds its directory to sys.path automatically.

Both helpers delegate to `mise run wait-for-dkg`, which polls the DKG Contract
directly. They wrap the subprocess in a timeout so that a hung DKG fails the
e2e test promptly rather than blocking the whole suite.
"""

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path


_PARENT_TASKS_DIR = Path(__file__).resolve().parents[2] / "mise-tasks"
if str(_PARENT_TASKS_DIR) not in sys.path:
sys.path.insert(0, str(_PARENT_TASKS_DIR))

import utils # noqa: E402 (re-exported for test scripts)


DEFAULT_SUCCESS_TIMEOUT = 120.0
DEFAULT_FAILURE_TIMEOUT = 240.0


def wait_for_dkg_success(
*,
keyper_set_index: int,
timeout: float = DEFAULT_SUCCESS_TIMEOUT,
) -> None:
"""Wait for `succeeded(keyper_set_index)` on the DKG Contract."""
cmd = [
"mise", "run", "wait-for-dkg",
"--ksi", str(keyper_set_index),
"--success",
]
try:
subprocess.run(cmd, check=True, timeout=timeout)
except subprocess.TimeoutExpired:
raise SystemExit(
f"Timed out after {timeout:.0f}s waiting for DKG success "
f"for keyper set {keyper_set_index}"
)


def wait_for_dkg_failure(
*,
keyper_set_index: int,
retry_counter: int = 0,
timeout: float = DEFAULT_FAILURE_TIMEOUT,
) -> None:
"""Wait for the (keyper_set_index, retry_counter) cycle to elapse without success.

Raises SystemExit if the DKG unexpectedly succeeded or on timeout.
"""
cmd = [
"mise", "run", "wait-for-dkg",
"--ksi", str(keyper_set_index),
"--retry", str(retry_counter),
"--failure",
]
try:
subprocess.run(cmd, check=True, timeout=timeout)
except subprocess.TimeoutExpired:
raise SystemExit(
f"Timed out after {timeout:.0f}s waiting for DKG failure "
f"for keyper set {keyper_set_index} retry {retry_counter}"
)
8 changes: 8 additions & 0 deletions mise-test-setup/e2e-tests/mise-tasks/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
#MISE description="Run all DKG e2e tests in sequence (happy path, then offline recovery)"
#MISE depends=["test-dkg-happy-path", "test-dkg-offline-recovery"]

# Sequential execution and fail-fast are enforced by `jobs = 1` in the
# sibling mise.toml; each depended-on test also calls `mise run clean` at
# startup, so no cleanup between them is needed.
echo "all DKG e2e tests passed"
37 changes: 37 additions & 0 deletions mise-test-setup/e2e-tests/mise-tasks/test-dkg-happy-path
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env -S uv run --script
#MISE description="DKG e2e: initial set + three keyper-set transitions, with decryption after each"
#MISE env={NUM_KEYPERS = "6", INITIAL_KEYPER_SET_INDICES = "0,1,2", INITIAL_THRESHOLD = "2"}

import os

from e2e_utils import utils, wait_for_dkg_success


def add_set_and_verify(
indices: str, threshold: str, keyper_set_index: int, keyper_index: int = 0
) -> None:
utils.run(["mise", "run", "add-keyper-set", "--indices", indices, "--threshold", threshold])
wait_for_dkg_success(keyper_set_index=keyper_set_index)
utils.run(["mise", "run", "check-dkg", "--ksi", str(keyper_set_index)])
cmd = ["mise", "run", "test-decryption"]
if keyper_index != 0:
cmd += ["--keyper-index", str(keyper_index)]
utils.run(cmd)


utils.run(["mise", "run", "clean"])
utils.run(["mise", "run", "fund-keypers"])

# Keyper set 1 (initial): 0,1,2 / threshold 2.
utils.run(["mise", "run", "wait-for-initial-dkg"])
utils.run(["mise", "run", "check-dkg", "--ksi", "1"])
utils.run(["mise", "run", "test-decryption"])

# Keyper set 2: grow to 0,1,2,3 / threshold 3.
add_set_and_verify("0,1,2,3", "3", 2)

# Keyper set 3: zero-overlap replacement 3,4,5 / threshold 2.
add_set_and_verify("3,4,5", "2", 3, keyper_index=3)

# Keyper set 4: return to original 0,1,2 / threshold 2.
add_set_and_verify("0,1,2", "2", 4)
31 changes: 31 additions & 0 deletions mise-test-setup/e2e-tests/mise-tasks/test-dkg-offline-recovery
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env -S uv run --script
#MISE description="DKG e2e: DKG fails below threshold, then succeeds once a third keyper comes online"
#MISE env={NUM_KEYPERS = "4", INITIAL_KEYPER_SET_INDICES = "0,1,2,3", INITIAL_THRESHOLD = "2"}

from e2e_utils import utils, wait_for_dkg_failure, wait_for_dkg_success


utils.run(["mise", "run", "clean"])
utils.run(["mise", "run", "fund-keypers"])

# Bootstrap: 2-of-4 initial DKG succeeds. This also establishes a previous
# keyper set with threshold 2, which gates the next config vote.
utils.run(["mise", "run", "wait-for-initial-dkg"])

# Stop keypers 2 and 3 before adding the 3-of-4 set.
# The new set's config vote only needs 2 votes (previous threshold),
# so keypers 0 and 1 can push it through despite the higher new threshold.
utils.run(["docker", "compose", "stop", "keyper-2", "keyper-3"])

utils.run(["mise", "run", "add-keyper-set", "--indices", "0,1,2,3", "--threshold", "3"])

# Only 2 of 3 required keypers online → DKG must fail.
wait_for_dkg_failure(keyper_set_index=2)

# Bring keyper-2 online to reach threshold; the keyper logic retries DKG.
utils.run(["docker", "compose", "up", "-d", "keyper-2"])

# Use keyper_set_index=2 to avoid a false positive from eon 1's success row.
wait_for_dkg_success(keyper_set_index=2)

utils.run(["mise", "run", "test-decryption"])
20 changes: 20 additions & 0 deletions mise-test-setup/e2e-tests/mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# DKG e2e tests harness. Env vars and parent tasks (e.g. `clean`,
# `wait-for-dkg`, `test-decryption`) are inherited from ../mise.toml via mise's
# directory-walking config discovery. Tests scripts live in `mise-tasks/`.

# jobs = 1 forces mise to run task dependencies serially so the `test`
# runner's two depends execute in order and fail-fast (offline-recovery is
# skipped if happy-path fails). Mise defaults to parallel (jobs = 4) which
# would otherwise let both tests `mise run clean` simultaneously and clobber
# each other's docker state.
[settings]
jobs = 1

[env]
# The happy-path test needs keypers 3, 4, and 5 for the zero-overlap
# replacement set; bump the pool to 6. Mise's directory-walking config
# discovery means this override is applied to every `mise run` invocation
# made from inside `e2e-tests/`, including child `mise run` subprocesses
# spawned by test scripts — which need the larger pool to find
# `keyper-4.toml`/`keyper-5.toml` when the test adds the 3,4,5 set.
NUM_KEYPERS = "6"
3 changes: 3 additions & 0 deletions mise-test-setup/mise-tasks/add-keyper-set
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ run_path = (
deployment_run = json.loads(run_path.read_text())
keyper_set_manager = utils.get_created_contract_address(deployment_run, "KeyperSetManager")
key_broadcast_contract = utils.get_created_contract_address(deployment_run, "KeyBroadcastContract")
dkg_contract = utils.get_created_contract_address(deployment_run, "DKGContract") or ""
keyper_addresses = ",".join(
utils.keyper_address(data_dir / f"keyper-{index}.toml") for index in indices
)
Expand All @@ -47,6 +48,8 @@ utils.run(
"--env",
f"KEYBROADCAST_ADDRESS={key_broadcast_contract}",
"--env",
f"DKG_CONTRACT_ADDRESS={dkg_contract}",
"--env",
f"KEYPER_ADDRESSES={keyper_addresses}",
"--env",
f"THRESHOLD={threshold}",
Expand Down
Loading