diff --git a/docs/adr-0001-dkg-on-chain-replace-shuttermint.md b/docs/adr-0001-dkg-on-chain-replace-shuttermint.md new file mode 100644 index 000000000..4f40e0fe8 --- /dev/null +++ b/docs/adr-0001-dkg-on-chain-replace-shuttermint.md @@ -0,0 +1,103 @@ +# ADR 0008: Replace Shuttermint with an Ethereum DKG Contract + +## Status + +Accepted + +## Context + +Keypers previously coordinated Distributed Key Generation via Shuttermint — a +dedicated Tendermint sidechain. Running Shuttermint required operating a +separate process alongside the keyper, executing a bootstrapping ceremony to +initialise the chain, maintaining a P2P network between keyper nodes, and +continuously synchronising state between Shuttermint and the management +contracts on Gnosis Chain. The management contracts already lived on Gnosis +Chain; DKG coordination could move there too. + +## Decision + +Replace Shuttermint entirely with a single Ethereum smart contract (the **DKG +Contract**) on Gnosis Chain. The contract acts as a bulletin board: Keypers +submit DKG messages as transactions, the contract emits them as events, and +Keypers read back what their peers submitted. The Shuttermint binary, validator +key management, Tendermint P2P network, bootstrapping ceremony, and all +synchronisation code between Shuttermint and the management contracts are +removed. + +## Design decisions + +**No on-chain cryptographic verification.** The DKG Contract enforces only that +the sender is a Keyper Set member and that the message type matches the current +phase. It performs no BLS signature checks or polynomial commitment +verification. The cryptographic primitives required (BLS12-381 pairings) are not +available as EVM precompiles, and gas costs would be prohibitive on any +plausible execution model. Keypers detect misbehaviour — invalid or missing +evaluations — off-chain and exclude bad actors from their local DKG computation. + +**Stateless, block-number-derived phase windows.** Phase boundaries are computed +from the DKG Contract parameters (`PHASE_LENGTH`, `DKG_LEAD_LENGTH`) and the +Keyper Set's activation block: +`activation_block - dkg_lead_length + r * cycle_length`. No start block is +recorded when a DKG Instance begins. A Keyper that receives a `KeyperSetAdded` +event has everything it needs to compute the full phase schedule without any +further on-chain or off-chain state, which eliminates a whole class of sync +problems. + +**Per-Keyper-Set DKG Contract address.** The DKG Contract hardcodes +`PHASE_LENGTH` and `DKG_LEAD_LENGTH`. When those parameters need to change, a +new contract is deployed and the new Keyper Set points to it; when they stay the +same, multiple Keyper Sets share one contract. The address is stored in +`KeyperSet.sol` (`dkgContract` field) and read by the keyper when it processes a +`KeyperSetAdded` event. This follows the same pattern as the `publisher` field +already in `KeyperSet.sol`. + +**DKG module as a DB-driven reactor.** The `rolling-shutter/dkg` package +contains all DKG participation logic but owns no chain subscriptions. On each +new block the host keyper calls `HandleBlock`; the module reads current state +from the database and writes back any required actions (message rows, +transaction outbox entries). The host keyper is responsible for delivering chain +events into the database. This reuses the existing chainsync infrastructure +rather than introducing a second subscription stack. + +**Transaction outbox for decoupled sending.** DKG messages are not submitted to +the chain inside open database transactions. Instead, the DKG module writes a +row to `tx_outbox` as part of the same transaction as the associated message +rows; a separate `TxSender` service reads pending rows and handles signing, +nonce management, gas estimation, and resubmission. This separates DKG +participation logic from transaction lifecycle concerns (retries, gas price +bumps, stuck-tx handling), keeps database transactions short — inline RPC calls +hold locks for an unbounded duration and create goroutine contention — and +allows the outbox to serve ECIES key registration and success votes as well as +DKG messages. + +**ECIES keys persist across Keyper Set transitions.** Keypers register their +ECIES encryption public key once in a standalone ECIES Key Registry contract; +they do not re-register when joining a new Keyper Set. ECIES keys are long-lived +identity keys that Keypers are not expected to rotate. + +**DKG events delivered via chainsync subscriptions.** DKG Contract events and +ECIES Registry events use the same `eth_subscribe` pattern already in place for +`KeyperSetSyncer`, rather than block-range polling with reorg handling. Events +emitted while a keyper is offline are not replayed on reconnect. This is +consistent with the existing event syncing mechanism; block-range polling can be +added later without changing the DKG module. + +## Consequences + +**Positive:** + +- Eliminates the Shuttermint binary, validator key management, Tendermint P2P + network, bootstrapping ceremony, and all synchronisation code between + Shuttermint and the management contracts. +- All DKG coordination is observable on the same chain as the rest of the + protocol. +- Phase windows are fully auditable from on-chain parameters alone. + +**Negative:** + +- DKG messages are Ethereum transactions, incurring gas costs per message. The + number of messages scales quadratically with Keyper Set size (each Keyper + sends one PolyEval per peer), which limits the maximum viable Keyper Set size + more than the per-message cost alone would suggest. +- A keyper that is offline during a DKG phase will miss events and be unable to + participate in that instance. It must wait for the next retry. diff --git a/docs/dkg-architecture.md b/docs/dkg-architecture.md new file mode 100644 index 000000000..606f9265c --- /dev/null +++ b/docs/dkg-architecture.md @@ -0,0 +1,223 @@ +# DKG Architecture + +Shuttermint — the Tendermint sidechain — is gone. DKG coordination now runs +entirely on Gnosis Chain via an Ethereum smart contract. This document explains +the new code for someone familiar with the old architecture who wants to review +the implementation. + +## What was removed + +- `rolling-shutter/app/` — the Tendermint ABCI application +- `rolling-shutter/shmsg/` — Shuttermint protobuf messages +- `rolling-shutter/keyper/shutterevents/` — Shuttermint event parsing +- `rolling-shutter/keyper/smobserver/` — Shuttermint state machine observer +- `rolling-shutter/keyper/fx/` — Shuttermint message sender +- `rolling-shutter/eonkeypublisher/` — EonKeyPublish contract interaction +- `rolling-shutter/cmd/chain/`, `cmd/bootstrap/` — chain and bootstrapping + commands + +## What was added + +| Package | Role | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `rolling-shutter/dkg/` | All DKG participation logic (dealing, accusing, apologizing, finalizing) | +| `rolling-shutter/txsender/` | Generic transaction outbox sender | +| `medley/chainsync/syncer/dkg.go` | Discovers DKG contracts from KeyperSetManager | +| `medley/chainsync/syncer/dkg_contract.go` | Subscribes to events on one DKG contract | +| `medley/chainsync/syncer/ecies.go` | Subscribes to ECIES Key Registry events | +| `rolling-shutter/contract/binding_dkg.abigen.gen.go` | Generated Go bindings for DKG and ECIES contracts (temporarily vendored here; will be imported from an external package like `github.com/shutter-network/shop-contracts/bindings` provides `KeyperSetManager`) | + +The host keyper implementations (`keyperimpl/gnosis` and +`keyperimpl/shutterservice`) wire these together; changes are structurally +identical in both. + +--- + +## Database schema changes + +The migration files (`keyper/database/sql/migrations/`) tell the full story. Key +changes: + +**Dropped (Shuttermint remnants):** + +- `tendermint_batch_config`, `tendermint_encryption_key`, + `tendermint_outgoing_messages`, `tendermint_sync_meta` +- `poly_evals`, `puredkg` — old blob-based DKG storage +- `outgoing_eon_keys`, `last_batch_config_sent`, `last_block_seen` + +**Added:** + +| Table | Key columns | Purpose | +| ---------------------- | -------------------------------------------------- | ---------------------------------------------------------------- | +| `ecies_keys` | `keyper_address`, `ecies_public_key` | Keyper encryption keys, written by `ECIESKeySyncer` | +| `dkg_poly_commitments` | `(ksi, retry, keyper_index)` | Feldman commitments received from chain | +| `dkg_poly_evals` | `(ksi, retry, sender_index, receiver_index)` | Encrypted evaluations received from chain | +| `dkg_accusations` | `(ksi, retry, accuser_index, accused_index)` | Accusations received from chain | +| `dkg_apologies` | `(ksi, retry, apologizer_index, accuser_index)` | Apologies received from chain | +| `dkg_initial_states` | `(ksi, retry)`, `puredkg_bytes` | Serialized puredkg state after polynomial generation (see below) | +| `dkg_sent_actions` | `(ksi, retry, action)`, `tx_outbox_id` | Idempotency log for phase handler submissions | +| `tx_outbox` | `to_address`, `data`, `value`, `status`, `tx_hash` | Pending outbound transactions | + +**Modified:** + +- `eons` — extended with `dkg_contract` (address), `phase_length`, + `lead_length`; `height` column dropped + +`ksi` = `keyper_config_index` throughout. + +--- + +## Data flow + +### 1. Keyper Set registration + +When `DKGSyncer` receives a `KeyperSetAdded` event, +`keyperimpl/*/newkeyperset.go:processNewKeyperSet` runs: + +1. Inserts the keyper set into `observer.keyper_set`. +2. If this keyper is a member, calls the DKG contract to read `PHASE_LENGTH` and + `DKG_LEAD_LENGTH`, then inserts a row into `eons` with those parameters and + the DKG contract address. +3. Calls `dkg.MaybeRegisterECIESKey` — if this keyper has no ECIES key + registered on-chain, enqueues a `registerKey` transaction via `tx_outbox`. + +The `eons` row is the sole source of phase parameters for the DKG module. It +never calls the chain again. + +### 2. Chain event ingestion + +`DKGSyncer` (`medley/chainsync/syncer/dkg.go`) watches `KeyperSetManager` for +new keyper sets. For each unique DKG contract address it discovers, it starts a +`DKGContractSyncer` (`syncer/dkg_contract.go`) as a service. `DKGContractSyncer` +subscribes to five event types on that contract and forwards them to the host +keyper's handler: + +| Event | Handler | Written to | +| ---------------------- | ---------------- | ---------------------------------------- | +| `DealingSubmitted` | `newdkgevent.go` | `dkg_poly_commitments`, `dkg_poly_evals` | +| `AccusationSubmitted` | `newdkgevent.go` | `dkg_accusations` | +| `ApologySubmitted` | `newdkgevent.go` | `dkg_apologies` | +| `SuccessVoteSubmitted` | `newdkgevent.go` | `dkg_success_votes` | +| `DKGSucceeded` | `newdkgevent.go` | triggers `HandleDKGSuccess` | + +`ECIESKeySyncer` (`syncer/ecies.go`) watches the ECIES Key Registry and writes +registered keys to `ecies_keys`. + +All event handlers write to the database and return. The DKG module never +receives events directly. + +### 3. Per-block participation + +On every new block, the host keyper calls +`dkg.Manager.HandleBlock(ctx, db, blockNumber)` (`dkg/manager.go`). + +`HandleBlock` iterates every eon the keyper is a member of that has not yet +succeeded: + +1. Opens a **read-only transaction** to reconstruct the in-memory `puredkg` + state by replaying all stored messages for this + `(keyperConfigIndex, retryCounter)`. +2. Calls `PhaseAt(blockNumber)` (`dkg/phase.go`) to determine the current phase + from pure arithmetic — no stored state. +3. Opens a **write transaction** and dispatches to the phase handler. + +The two-transaction split is intentional: the read phase can be long (replaying +all messages); the write transaction is kept as short as possible. + +### 4. Phase handlers + +Each handler in `dkg/dealing.go`, `dkg/accusing.go`, `dkg/apologizing.go`, +`dkg/finalizing.go` follows the same pattern: + +1. **Check idempotency** via `dkg_sent_actions`. If a row exists for + `(keyperConfigIndex, retryCounter, action)`, return immediately — this block + is a replay. +2. **Compute the action** using the in-memory `puredkg`. +3. **Enqueue to `tx_outbox`** (or skip if no action is needed, e.g. no + accusations to make). +4. **Insert into `dkg_sent_actions`** with the outbox row id (or NULL if no tx + was enqueued). + +The `dkg_sent_actions` table is what prevents the same transaction from being +submitted on every block while the phase is active. + +**Dealing** generates the keyper's random polynomial and serialises the +resulting `puredkg` state into `dkg_initial_states` before enqueuing. This is a +cryptographic necessity: the random polynomial cannot be re-derived from the +messages stored on-chain. Subsequent handlers load this snapshot and replay +received messages on top of it rather than trying to reconstruct the polynomial +from scratch. + +### 5. Transaction submission + +`TxSender` (`txsender/txsender.go`) runs as an independent service polling +`tx_outbox` in two passes per tick: + +**Submit pass** — rows with `status = 'pending'`: + +- Fetches pending nonce, estimates gas, computes EIP-1559 fee cap. +- Marks the row `submitted` with the tx hash **before** broadcasting. This + ensures a crash between mark and send leaves a recoverable row rather than a + phantom. +- Calls `SendTransaction`. + +**Confirm pass** — rows with `status = 'submitted'`: + +- Calls `TransactionReceipt` with the stored hash. +- Marks `confirmed` on success, leaves unchanged if not yet mined. + +`TxSender` is intentionally generic — it knows nothing about DKG. Retry logic, +gas price bumps, and stuck-tx handling belong here, not in the DKG module. + +### 6. DKG success + +When `DKGContractSyncer` receives a `DKGSucceeded` event, +`Manager.HandleDKGSuccess` runs: + +- Inserts a `dkg_result` row marking the eon as succeeded. +- Rebuilds `puredkg` from stored messages at the Finalizing phase to compute the + eon secret key share. +- `HandleBlock` skips this eon from the next block onward. + +The `KeyBroadcastContract.broadcastEonKey` call is made by the DKG Contract +itself when the success-vote threshold is reached — not by the keyper. + +--- + +## Key invariants + +**The DKG module never calls the chain.** All phase parameters, ECIES keys, and +DKG messages arrive via the database. If a required row is missing (e.g. a +peer's ECIES key), the handler skips rather than fetching it. + +**`puredkg` is ephemeral.** It is rebuilt from database rows on every +`HandleBlock` call (or loaded from the `dkg_initial_states` snapshot for +post-Dealing phases). There is no long-lived in-memory DKG state to get out of +sync. + +**Phase windows are pure arithmetic.** `PhaseAt(block)` has no side effects and +needs no stored state beyond the `eons` row. The full retry schedule is +determined the moment `KeyperSetAdded` is processed. + +**`dkg_sent_actions` is the idempotency boundary.** Any code path that enqueues +a transaction must insert a `dkg_sent_actions` row in the same database +transaction. Without this, a crash between submit and confirm would cause the +handler to enqueue a duplicate on restart. + +--- + +## Database tables + +| Table | Written by | Read by | +| ---------------------- | ------------------------------ | ---------------------------------------- | +| `eons` | `newkeyperset` handler | DKG module (phase params, contract addr) | +| `ecies_keys` | `ECIESKeySyncer` | DKG module (encrypt PolyEvals) | +| `dkg_poly_commitments` | `newdkgevent` handler | DKG module (rebuild puredkg) | +| `dkg_poly_evals` | `newdkgevent` handler | DKG module (rebuild puredkg) | +| `dkg_accusations` | `newdkgevent` handler | DKG module (rebuild puredkg) | +| `dkg_apologies` | `newdkgevent` handler | DKG module (rebuild puredkg) | +| `dkg_success_votes` | `newdkgevent` handler | DKG module (rebuild puredkg) | +| `dkg_initial_states` | Dealing handler | Accusing/Apologizing/Finalizing handlers | +| `dkg_sent_actions` | Phase handlers | Phase handlers (idempotency check) | +| `tx_outbox` | Phase handlers, `newkeyperset` | `TxSender` | +| `dkg_result` | `HandleDKGSuccess` | `HandleBlock` (skip check) | diff --git a/mise-test-setup/README.md b/mise-test-setup/README.md index 7e66f87d3..17edd4ad8 100644 --- a/mise-test-setup/README.md +++ b/mise-test-setup/README.md @@ -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: @@ -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` @@ -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` @@ -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` diff --git a/mise-test-setup/compose.common.yml b/mise-test-setup/compose.common.yml index ef1b23d19..6fc1b9ecf 100644 --- a/mise-test-setup/compose.common.yml +++ b/mise-test-setup/compose.common.yml @@ -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 @@ -61,7 +46,7 @@ services: contracts: build: - context: https://github.com/shutter-network/contracts.git#docker + context: https://github.com/shutter-network/contracts.git#dkg-contract profiles: - tools environment: diff --git a/mise-test-setup/compose.gnosis.yml b/mise-test-setup/compose.gnosis.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/mise-test-setup/compose.keypers.yml.j2 b/mise-test-setup/compose.keypers.yml.j2 index 45fdf0da2..03b796f7f 100644 --- a/mise-test-setup/compose.keypers.yml.j2 +++ b/mise-test-setup/compose.keypers.yml.j2 @@ -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: @@ -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 %} diff --git a/mise-test-setup/compose.service.yml b/mise-test-setup/compose.service.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/mise-test-setup/compose.yml.j2 b/mise-test-setup/compose.yml.j2 index c5fcdc483..891c5adf7 100644 --- a/mise-test-setup/compose.yml.j2 +++ b/mise-test-setup/compose.yml.j2 @@ -1,4 +1,3 @@ include: - ../compose.common.yml - - ./compose.keypers.yml - - ../compose.{{ deployment_type }}.yml \ No newline at end of file + - ./compose.keypers.yml \ No newline at end of file diff --git a/mise-test-setup/e2e-tests/mise-tasks/e2e_utils.py b/mise-test-setup/e2e-tests/mise-tasks/e2e_utils.py new file mode 100644 index 000000000..1893d0570 --- /dev/null +++ b/mise-test-setup/e2e-tests/mise-tasks/e2e_utils.py @@ -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}" + ) diff --git a/mise-test-setup/e2e-tests/mise-tasks/test b/mise-test-setup/e2e-tests/mise-tasks/test new file mode 100755 index 000000000..ce4f32ef2 --- /dev/null +++ b/mise-test-setup/e2e-tests/mise-tasks/test @@ -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" diff --git a/mise-test-setup/e2e-tests/mise-tasks/test-dkg-happy-path b/mise-test-setup/e2e-tests/mise-tasks/test-dkg-happy-path new file mode 100755 index 000000000..f4ee841a5 --- /dev/null +++ b/mise-test-setup/e2e-tests/mise-tasks/test-dkg-happy-path @@ -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) diff --git a/mise-test-setup/e2e-tests/mise-tasks/test-dkg-offline-recovery b/mise-test-setup/e2e-tests/mise-tasks/test-dkg-offline-recovery new file mode 100755 index 000000000..6287f42c2 --- /dev/null +++ b/mise-test-setup/e2e-tests/mise-tasks/test-dkg-offline-recovery @@ -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"]) diff --git a/mise-test-setup/e2e-tests/mise.toml b/mise-test-setup/e2e-tests/mise.toml new file mode 100644 index 000000000..3fcc71818 --- /dev/null +++ b/mise-test-setup/e2e-tests/mise.toml @@ -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" diff --git a/mise-test-setup/mise-tasks/add-keyper-set b/mise-test-setup/mise-tasks/add-keyper-set index ec5991066..631ac53a0 100755 --- a/mise-test-setup/mise-tasks/add-keyper-set +++ b/mise-test-setup/mise-tasks/add-keyper-set @@ -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 ) @@ -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}", diff --git a/mise-test-setup/mise-tasks/check-dkg b/mise-test-setup/mise-tasks/check-dkg new file mode 100755 index 000000000..dd1e93ec1 --- /dev/null +++ b/mise-test-setup/mise-tasks/check-dkg @@ -0,0 +1,142 @@ +#!/usr/bin/env -S uv run --script +#MISE description="Cross-reference DKG outcome across DKG Contract, KeyBroadcastContract, and keyper DBs" +#USAGE flag "--ksi " help="Keyper Set Index to check (default: latest registered)" + +import os +from pathlib import Path + +import utils + + +def get_keyper_set_members(ksi: int) -> list[str]: + """Return the keyper member addresses (lowercase) for the given KSI.""" + ksm = utils.get_deployed_address("KeyperSetManager") + keyper_set_addr = utils.cast_call(ksm, "getKeyperSetAddress(uint64)(address)", str(ksi)) + raw = utils.cast_call(keyper_set_addr, "getMembers()(address[])") + inner = raw.strip().lstrip("[").rstrip("]") + if not inner: + return [] + return [token.strip().lower() for token in inner.split(",") if token.strip()] + + +def build_address_to_index() -> dict[str, int]: + """Map keyper Ethereum address → keyper-{i} index using local config files.""" + data_dir = Path(os.environ["DATA_DIR"]) + num_keypers = int(os.environ["NUM_KEYPERS"]) + mapping: dict[str, int] = {} + for index in range(num_keypers): + config_path = data_dir / f"keyper-{index}.toml" + if not config_path.exists(): + continue + mapping[utils.keyper_address(config_path).lower()] = index + return mapping + + +def get_eon_for_ksi(keyper_index: int, ksi: int) -> str | None: + """Return the local `eon` for a KSI in keyper-{i}'s DB, or None if absent.""" + result = utils.query_keyper_db( + keyper_index, + f"SELECT eon FROM eons WHERE keyper_config_index = {ksi} LIMIT 1", + ) + return result if result else None + + +def get_dkg_result_success(keyper_index: int, eon: str) -> str | None: + """Return 't'/'f' for dkg_result.success, or None if no row exists.""" + result = utils.query_keyper_db( + keyper_index, + f"SELECT success FROM dkg_result WHERE eon = {int(eon)} LIMIT 1", + ) + return result if result else None + + +def get_eon_key(ksi: int) -> str: + """Return the raw eon key bytes hex (`0x...`) from KeyBroadcastContract.""" + kbc = utils.get_deployed_address("KeyBroadcastContract") + return utils.cast_call(kbc, "getEonKey(uint64)(bytes)", str(ksi)) + + +def eon_key_present(raw: str) -> bool: + stripped = raw.strip().lower() + return stripped not in ("", "0x") + + +ksi_raw = os.environ.get("usage_ksi", "").strip() +ksi = int(ksi_raw) if ksi_raw else utils.get_latest_keyper_set_index() + +print(f"Checking DKG outcome for ksi={ksi}") + +contract_succeeded = utils.get_succeeded(ksi) +raw_eon_key = get_eon_key(ksi) +key_present = eon_key_present(raw_eon_key) + +members = get_keyper_set_members(ksi) +if not members: + raise SystemExit(f"KeyperSet for ksi {ksi} has no members") + +address_to_index = build_address_to_index() +unmatched: list[str] = [] +keyper_indices: list[int] = [] +for addr in members: + if addr in address_to_index: + keyper_indices.append(address_to_index[addr]) + else: + unmatched.append(addr) +keyper_indices.sort() + +per_keyper: list[tuple[int, str | None, str | None]] = [] +for k_idx in keyper_indices: + eon = get_eon_for_ksi(k_idx, ksi) + success = get_dkg_result_success(k_idx, eon) if eon else None + per_keyper.append((k_idx, eon, success)) + +print(f" DKG Contract succeeded({ksi}): {contract_succeeded}") +print(f" KeyBroadcastContract eon key set: {key_present}") +for k_idx, eon, success in per_keyper: + eon_display = eon if eon is not None else "(no row)" + success_display = success if success is not None else "(no row)" + print(f" keyper-{k_idx} eons.eon={eon_display}, dkg_result.success={success_display}") +if unmatched: + for addr in unmatched: + print(f" WARNING: member {addr} has no matching keyper-{{i}} config") + +disagreements: list[str] = [] + +if contract_succeeded != key_present: + disagreements.append( + f"DKG Contract succeeded({ksi})={contract_succeeded} but " + f"KeyBroadcastContract eon key present={key_present}" + ) + +for k_idx, eon, success in per_keyper: + if eon is None: + disagreements.append(f"keyper-{k_idx} has no eons row for ksi {ksi}") + continue + if success is None: + disagreements.append(f"keyper-{k_idx} has no dkg_result row for eon {eon}") + continue + keyper_succeeded = success == "t" + if keyper_succeeded != contract_succeeded: + disagreements.append( + f"keyper-{k_idx} dkg_result.success={success} disagrees with " + f"DKG Contract succeeded({ksi})={contract_succeeded}" + ) + +if unmatched: + disagreements.append( + f"{len(unmatched)} keyper set member(s) not mapped to a local keyper-{{i}} config" + ) + +if disagreements: + print() + print(f"DKG check FAILED for ksi {ksi}:") + for msg in disagreements: + print(f" - {msg}") + raise SystemExit(1) + +outcome = "succeeded" if contract_succeeded else "failed" +print() +print( + f"DKG check OK for ksi {ksi}: all sources agree DKG {outcome} " + f"(checked {len(per_keyper)} keyper DB(s))" +) diff --git a/mise-test-setup/mise-tasks/deploy b/mise-test-setup/mise-tasks/deploy index 3d117b2ba..691e6547b 100755 --- a/mise-test-setup/mise-tasks/deploy +++ b/mise-test-setup/mise-tasks/deploy @@ -13,8 +13,16 @@ REQUIRED_CONTRACTS: dict[str, list[str]] = { "KeyperSetManager", "Sequencer", "ValidatorRegistry", + "ECIESKeyRegistry", + "DKGContract", + ], + "service": [ + "KeyBroadcastContract", + "KeyperSetManager", + "ShutterRegistry", + "ECIESKeyRegistry", + "DKGContract", ], - "service": ["KeyBroadcastContract", "KeyperSetManager", "ShutterRegistry"], } diff --git a/mise-test-setup/mise-tasks/fund-keypers b/mise-test-setup/mise-tasks/fund-keypers new file mode 100755 index 000000000..f0c6d9926 --- /dev/null +++ b/mise-test-setup/mise-tasks/fund-keypers @@ -0,0 +1,69 @@ +#!/usr/bin/env -S uv run --script +#MISE description="Fund keyper Ethereum addresses with ETH from ANVIL_KEY_0 if balance is zero" +#MISE depends=["up-ethereum", "gen-keyper-configs"] + +import os +from pathlib import Path + +import utils + + +FUND_VALUE = "1000ether" +ANVIL_RPC_URL = "http://ethereum:8545" + + +def cast_balance(address: str) -> int: + result = utils.run( + [ + "docker", + "compose", + "run", + "--rm", + "--entrypoint", + "cast", + "contracts", + "balance", + "--rpc-url", + ANVIL_RPC_URL, + address, + ], + capture_output=True, + ) + return int(result.stdout.strip()) + + +def cast_send_fund(address: str, value: str, private_key: str) -> None: + utils.run( + [ + "docker", + "compose", + "run", + "--rm", + "--entrypoint", + "cast", + "contracts", + "send", + "--rpc-url", + ANVIL_RPC_URL, + "--private-key", + private_key, + "--value", + value, + address, + ] + ) + + +data_dir = Path(os.environ["DATA_DIR"]) +num_keypers = int(os.environ["NUM_KEYPERS"]) +private_key = os.environ["ANVIL_KEY_0"] + +for index in range(num_keypers): + config_path = data_dir / f"keyper-{index}.toml" + address = utils.keyper_address(config_path) + balance = cast_balance(address) + if balance > 0: + print(f"keyper-{index} ({address}) balance={balance} wei; skipping") + continue + print(f"keyper-{index} ({address}) balance=0; funding with {FUND_VALUE}") + cast_send_fund(address, FUND_VALUE, private_key) diff --git a/mise-test-setup/mise-tasks/gen-compose b/mise-test-setup/mise-tasks/gen-compose index 6c3f3078a..062b95a40 100755 --- a/mise-test-setup/mise-tasks/gen-compose +++ b/mise-test-setup/mise-tasks/gen-compose @@ -23,7 +23,5 @@ compose_keypers = environment.get_template("compose.keypers.yml.j2").render( ) (generated_dir / "compose.keypers.yml").write_text(compose_keypers) -compose = environment.get_template("compose.yml.j2").render( - deployment_type=deployment_type, -) +compose = environment.get_template("compose.yml.j2").render() (generated_dir / "compose.yml").write_text(compose) diff --git a/mise-test-setup/mise-tasks/gen-keyper-configs b/mise-test-setup/mise-tasks/gen-keyper-configs index 27961297e..71fdfefad 100755 --- a/mise-test-setup/mise-tasks/gen-keyper-configs +++ b/mise-test-setup/mise-tasks/gen-keyper-configs @@ -51,22 +51,25 @@ for index in range(int(os.environ["NUM_KEYPERS"])): ] ) + # The TOML section that holds the chain-specific config and contracts. + # The gnosis keyper config nests them under [Gnosis], while the + # shutterservice keyper uses [Chain]. + chain_section = "Gnosis" if deployment_type == "gnosis" else "Chain" + document = tomlkit.parse(config_path.read_text()) utils.set_toml_path(document, ["DatabaseURL"], f"postgresql://postgres@db:5432/keyper-{index}") - utils.set_toml_path(document, ["Shuttermint", "ShuttermintURL"], f"http://chain-{index}:{os.environ['SHUTTERMINT_RPC_PORT']}") - utils.set_toml_path(document, ["Shuttermint", "DKGPhaseLength"], int(os.environ["SHUTTERMINT_DKG_PHASE_LENGTH"])) - utils.set_toml_path(document, ["Shuttermint", "StartBlockDelta"], int(os.environ["SHUTTERMINT_START_BLOCK_DELTA"])) utils.set_toml_path(document, ["P2P", "CustomBootstrapAddresses"], bootstrap_addresses) - utils.set_toml_path(document, ["Chain", "Contracts", "KeyBroadcastContract"], utils.get_created_contract_address(deployment_run, "KeyBroadcastContract")) - utils.set_toml_path(document, ["Chain", "Contracts", "KeyperSetManager"], utils.get_created_contract_address(deployment_run, "KeyperSetManager")) + utils.set_toml_path(document, [chain_section, "Contracts", "KeyBroadcastContract"], utils.get_created_contract_address(deployment_run, "KeyBroadcastContract")) + utils.set_toml_path(document, [chain_section, "Contracts", "KeyperSetManager"], utils.get_created_contract_address(deployment_run, "KeyperSetManager")) + utils.set_toml_path(document, [chain_section, "Node", "EthereumURL"], "ws://ethereum:8545") if deployment_type == "gnosis": - utils.set_toml_path(document, ["Gnosis", "Node", "EthereumURL"], "ws://ethereum:8545") - utils.set_toml_path(document, ["Chain", "Contracts", "Sequencer"], utils.get_created_contract_address(deployment_run, "Sequencer")) - utils.set_toml_path(document, ["Chain", "Contracts", "ValidatorRegistry"], utils.get_created_contract_address(deployment_run, "ValidatorRegistry")) + utils.set_toml_path(document, [chain_section, "Contracts", "Sequencer"], utils.get_created_contract_address(deployment_run, "Sequencer")) + utils.set_toml_path(document, [chain_section, "Contracts", "ValidatorRegistry"], utils.get_created_contract_address(deployment_run, "ValidatorRegistry")) + utils.set_toml_path(document, [chain_section, "Contracts", "ECIESKeyRegistry"], utils.get_created_contract_address(deployment_run, "ECIESKeyRegistry")) elif deployment_type == "service": - utils.set_toml_path(document, ["Chain", "Node", "EthereumURL"], "ws://ethereum:8545") - utils.set_toml_path(document, ["Chain", "Contracts", "ShutterRegistry"], utils.get_created_contract_address(deployment_run, "ShutterRegistry")) + utils.set_toml_path(document, [chain_section, "Contracts", "ShutterRegistry"], utils.get_created_contract_address(deployment_run, "ShutterRegistry")) + utils.set_toml_path(document, [chain_section, "Contracts", "ECIESKeyRegistry"], utils.get_created_contract_address(deployment_run, "ECIESKeyRegistry")) else: raise SystemExit(f"Unsupported DEPLOYMENT_TYPE for config generation: {deployment_type}") diff --git a/mise-test-setup/mise-tasks/init-chain-nodes b/mise-test-setup/mise-tasks/init-chain-nodes deleted file mode 100755 index 61d42f7cd..000000000 --- a/mise-test-setup/mise-tasks/init-chain-nodes +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env -S uv run --script -#MISE description="Initialize shuttermint chain nodes for all keypers" -#MISE depends=["init-chain-seed", "gen-keyper-configs"] -# /// script -# dependencies = ["tomlkit"] -# /// - -import os -from pathlib import Path - -import tomlkit -import utils - - -data_dir = Path(os.environ["DATA_DIR"]) -seed_node_id_path = data_dir / "chain-seed" / "config" / "node_key.json.id" -seed_node_id = seed_node_id_path.read_text().strip() - -for index in range(int(os.environ["NUM_KEYPERS"])): - config_path = data_dir / f"chain-{index}" / "config" / "config.toml" - validator_pubkey_path = data_dir / f"chain-{index}" / "config" / "priv_validator_pubkey.hex" - keyper_config_path = data_dir / f"keyper-{index}.toml" - - if not config_path.exists(): - utils.run( - [ - "docker", - "compose", - "run", - "--rm", - "--no-deps", - f"chain-{index}", - "chain", - "init", - "--root", - "/chain", - "--genesis-keyper", - "0x0000000000000000000000000000000000000000", - "--blocktime", - os.environ["SHUTTERMINT_BLOCK_TIME"], - "--listen-address", - f"tcp://0.0.0.0:{os.environ['SHUTTERMINT_RPC_PORT']}", - "--role", - "validator", - ] - ) - - seed_node = f"{seed_node_id}@chain-seed:{os.environ['SHUTTERMINT_P2P_PORT']}" - external_address = f"chain-{index}:{os.environ['SHUTTERMINT_P2P_PORT']}" - - document = tomlkit.parse(config_path.read_text()) - utils.set_toml_path(document, ["p2p", "seeds"], seed_node) - utils.set_toml_path(document, ["p2p", "external_address"], external_address) - document["moniker"] = f"chain-{index}" - config_path.write_text(tomlkit.dumps(document)) - - validator_pubkey = validator_pubkey_path.read_text().strip() - keyper_document = tomlkit.parse(keyper_config_path.read_text()) - utils.set_toml_path(keyper_document, ["Shuttermint", "ValidatorPublicKey"], validator_pubkey) - keyper_config_path.write_text(tomlkit.dumps(keyper_document)) diff --git a/mise-test-setup/mise-tasks/init-chain-seed b/mise-test-setup/mise-tasks/init-chain-seed deleted file mode 100755 index 27cf88486..000000000 --- a/mise-test-setup/mise-tasks/init-chain-seed +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env -S uv run --script -#MISE description="Initialize the shuttermint seed node" -#MISE depends=["gen-compose"] -#MISE outputs=["{{env.DATA_DIR}}/chain-seed/config/config.toml"] -# /// script -# dependencies = ["tomlkit"] -# /// - -import os -from pathlib import Path - -import tomlkit -import utils - - -data_dir = Path(os.environ["DATA_DIR"]) -config_path = data_dir / "chain-seed" / "config" / "config.toml" - -if not config_path.exists(): - utils.run( - [ - "docker", - "compose", - "run", - "--rm", - "--no-deps", - "chain-seed", - "chain", - "init", - "--root", - "/chain", - "--genesis-keyper", - "0x0000000000000000000000000000000000000000", - "--blocktime", - os.environ["SHUTTERMINT_BLOCK_TIME"], - "--listen-address", - f"tcp://0.0.0.0:{os.environ['SHUTTERMINT_RPC_PORT']}", - "--role", - "seed", - ] - ) - -document = tomlkit.parse(config_path.read_text()) -document["moniker"] = "chain-seed" -config_path.write_text(tomlkit.dumps(document)) diff --git a/mise-test-setup/mise-tasks/patch-genesis b/mise-test-setup/mise-tasks/patch-genesis deleted file mode 100755 index 1bd6856dc..000000000 --- a/mise-test-setup/mise-tasks/patch-genesis +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env -S uv run --script -#MISE description="Patch and distribute the shuttermint genesis" -#MISE depends=["init-chain-nodes"] - -import json -import os -from pathlib import Path - -import utils - -data_dir = Path(os.environ["DATA_DIR"]) -num_keypers = int(os.environ["NUM_KEYPERS"]) -initial_keyper_set_indices = utils.parse_indices(os.environ["INITIAL_KEYPER_SET_INDICES"]) -source_path = data_dir / "chain-seed" / "config" / "genesis.json" -genesis = json.loads(source_path.read_text()) - -validators: list[dict[str, object]] = [] -keypers: list[str] = [] -for index in initial_keyper_set_indices: - validator_key_path = ( - data_dir / f"chain-{index}" / "config" / "priv_validator_key.json" - ) - validator_key = json.loads(validator_key_path.read_text()) - validators.append( - { - "address": validator_key["address"], - "pub_key": validator_key["pub_key"], - "power": "10", - "name": f"chain-{index}", - } - ) - - keyper_config_path = data_dir / f"keyper-{index}.toml" - keypers.append(utils.keyper_address(keyper_config_path)) - -genesis["validators"] = validators -genesis["app_state"]["keypers"] = keypers -genesis["app_state"]["threshold"] = os.environ["INITIAL_THRESHOLD"] - -rendered_genesis = json.dumps(genesis, indent=2) + "\n" -targets: list[Path] = [source_path] -for index in range(num_keypers): - targets.append(data_dir / f"chain-{index}" / "config" / "genesis.json") - -for target in targets: - target.write_text(rendered_genesis) diff --git a/mise-test-setup/mise-tasks/show-dkg-status b/mise-test-setup/mise-tasks/show-dkg-status new file mode 100755 index 000000000..5b662929c --- /dev/null +++ b/mise-test-setup/mise-tasks/show-dkg-status @@ -0,0 +1,140 @@ +#!/usr/bin/env -S uv run --script +#MISE description="Print on-chain DKG state for a Keyper Set Index" +#USAGE flag "--ksi " help="Keyper Set Index to inspect (default: latest registered)" +#USAGE flag "--retry " help="Limit output to a single retry counter (default: all retries up to current)" + +import os + +import utils + + +# Event signature hashes (keccak256 of the canonical event signatures from DKGContract.sol). +TOPIC_DEALING = "0xa5074728b92791250d48ccdc55266aca7b1cca22f89e1ef2ab91ccd7ae0f337d" +TOPIC_ACCUSATION = "0xb5cc05e740a503c13e1b7ac3ad2d5abf41b91d2e5fca79455d34e515fb70849a" +TOPIC_APOLOGY = "0x62729f0820cf3ab60fbd63a51cdeae0554d0ab24f843b86bd0d2591594d69136" +TOPIC_VOTE = "0xacfc0aa3f6ba36ac0ffda53e00d2cdace2fd4a9e41e2fe3214032794332c37c3" +TOPIC_SUCCEEDED = "0xe6c9bd3daa50b9218615605cdf333410d405b7f3b8dbeee5a2373769679aa9f5" + +SUBMITTER_EVENTS = { + TOPIC_DEALING: "Dealings", + TOPIC_ACCUSATION: "Accusations", + TOPIC_APOLOGY: "Apologies", + TOPIC_VOTE: "Votes", +} + +PHASE_NAMES = ["None", "Dealing", "Accusing", "Apologizing", "Finalizing"] + + +def phase_for_offset(offset: int, phase_len: int) -> int: + if offset < 0: + return 0 + if offset < phase_len: + return 1 + if offset < 2 * phase_len: + return 2 + if offset < 3 * phase_len: + return 3 + if offset < 4 * phase_len: + return 4 + return 0 + + +def find_succeeded_retry(events: list[dict], ksi: int) -> int | None: + """Return the retry counter from the DKGSucceeded event for ksi, or None.""" + for ev in events: + topics = ev.get("topics", []) + if len(topics) < 3: + continue + if topics[0].lower() != TOPIC_SUCCEEDED: + continue + if utils.topic_to_uint(topics[1]) != ksi: + continue + return utils.topic_to_uint(topics[2]) + return None + + +def collect_submitters(events: list[dict], topic0: str, ksi: int, retry: int) -> list[int]: + indices: set[int] = set() + for ev in events: + topics = ev.get("topics", []) + if len(topics) < 4: + continue + if topics[0].lower() != topic0: + continue + if utils.topic_to_uint(topics[1]) != ksi: + continue + if utils.topic_to_uint(topics[2]) != retry: + continue + indices.add(utils.topic_to_uint(topics[3])) + return sorted(indices) + + +def print_instance( + ksi: int, + retry: int, + current_block: int, + phase_len: int, + cycle_len: int, + succeeded: bool, + events: list[dict], +) -> None: + start = utils.get_dkg_start(ksi, retry) + offset = current_block - start + phase_idx = phase_for_offset(offset, phase_len) + phase_name = PHASE_NAMES[phase_idx] + + print(f"=== DKG Instance (ksi={ksi}, retry={retry}) ===") + print(f" current block: {current_block}") + print(f" dkg start: {start}") + print( + f" phase windows: Dealing [{start}-{start + phase_len - 1}], " + f"Accusing [{start + phase_len}-{start + 2 * phase_len - 1}], " + f"Apologizing [{start + 2 * phase_len}-{start + 3 * phase_len - 1}], " + f"Finalizing [{start + 3 * phase_len}-{start + 4 * phase_len - 1}]" + ) + if phase_idx == 0: + if offset < 0: + print(f" current phase: None (starts in {-offset} blocks)") + else: + print(f" current phase: None (cycle ended {offset - cycle_len} blocks ago)") + else: + phase_end = start + phase_idx * phase_len - 1 + remaining = phase_end - current_block + print(f" current phase: {phase_name} ({remaining} blocks remaining)") + print(f" ksi succeeded on-chain: {'yes' if succeeded else 'no'}") + + for topic, label in SUBMITTER_EVENTS.items(): + submitters = collect_submitters(events, topic, ksi, retry) + if submitters: + indices_str = ", ".join(str(i) for i in submitters) + else: + indices_str = "(none)" + print(f" {label:<13} keyper indices: {indices_str}") + print() + + +ksi_raw = os.environ.get("usage_ksi", "").strip() +retry_raw = os.environ.get("usage_retry", "").strip() + +ksi = int(ksi_raw) if ksi_raw else utils.get_latest_keyper_set_index() + +phase_len = utils.get_phase_length() +cycle_len = utils.get_cycle_length() +current_block = utils.cast_block_number() +succeeded = utils.get_succeeded(ksi) + +dkg_address = utils.get_deployed_address("DKGContract") +events = utils.cast_logs_at_address(dkg_address) + +if retry_raw: + retries = [int(retry_raw)] +else: + upper = utils.derive_retry_counter(ksi) + if succeeded: + succeeded_retry = find_succeeded_retry(events, ksi) + if succeeded_retry is not None: + upper = succeeded_retry + retries = list(range(upper + 1)) + +for retry in retries: + print_instance(ksi, retry, current_block, phase_len, cycle_len, succeeded, events) diff --git a/mise-test-setup/mise-tasks/submit-identity-registration b/mise-test-setup/mise-tasks/submit-identity-registration index 892caa7c3..a6a79cb31 100755 --- a/mise-test-setup/mise-tasks/submit-identity-registration +++ b/mise-test-setup/mise-tasks/submit-identity-registration @@ -35,7 +35,10 @@ registry_address = utils.get_created_contract_address(deployment_run, "ShutterRe eon = os.environ.get("usage_eon", "").strip() if not eon: - eon = utils.query_keyper_db(0, "SELECT max(eon) FROM eons") + eon = utils.query_keyper_db( + 0, + "SELECT keyper_config_index FROM eons ORDER BY eon DESC LIMIT 1", + ) if not eon: raise SystemExit("No eon found") diff --git a/mise-test-setup/mise-tasks/test-decryption b/mise-test-setup/mise-tasks/test-decryption index 4b7c474e9..9525b40c5 100755 --- a/mise-test-setup/mise-tasks/test-decryption +++ b/mise-test-setup/mise-tasks/test-decryption @@ -1,25 +1,26 @@ #!/usr/bin/env -S uv run --script #MISE description="Submit a decryption trigger and wait for the key" +#USAGE flag "--keyper-index " help="Keyper index to query for decryption key (default 0; use a member of the target set)" import json +import os import utils +keyper_index = os.environ.get("usage_keyper_index", "").strip() + result = utils.run( ["mise", "run", "submit-identity-registration"], capture_output=True, ) context = json.loads(result.stdout.strip().splitlines()[-1]) -utils.run( - [ - "mise", - "run", - "wait-for-decryption-key", - "--eon", - context["eon"], - "--identity-prefix", - context["identity_prefix"], - ] -) +cmd = [ + "mise", "run", "wait-for-decryption-key", + "--eon", context["eon"], + "--identity-prefix", context["identity_prefix"], +] +if keyper_index: + cmd += ["--keyper-index", keyper_index] +utils.run(cmd) diff --git a/mise-test-setup/mise-tasks/up b/mise-test-setup/mise-tasks/up index 3cda696a3..fc79ab78d 100755 --- a/mise-test-setup/mise-tasks/up +++ b/mise-test-setup/mise-tasks/up @@ -1,8 +1,7 @@ #!/usr/bin/env -S uv run --script #MISE description="Start the implemented docker3 stack" -#MISE depends=["patch-genesis", "init-keyper-dbs"] +#MISE depends=["init-keyper-dbs"] -import os import utils @@ -10,7 +9,3 @@ utils.run(["docker", "compose", "up", "-d"]) utils.wait_for_service_health("ethereum") utils.wait_for_service_health("db") -utils.wait_for_service_health("chain-seed") -num_keypers = int(os.environ["NUM_KEYPERS"]) -for index in range(num_keypers): - utils.wait_for_service_health(f"chain-{index}") diff --git a/mise-test-setup/mise-tasks/utils.py b/mise-test-setup/mise-tasks/utils.py index 589107eae..cb951e443 100644 --- a/mise-test-setup/mise-tasks/utils.py +++ b/mise-test-setup/mise-tasks/utils.py @@ -1,5 +1,7 @@ from __future__ import annotations +import json +import os import subprocess import time from pathlib import Path @@ -115,3 +117,134 @@ def get_created_contract_address( if isinstance(address, str) and address: return address return None + + +def load_deployment_run() -> dict[str, object]: + deployment_type = resolve_deployment_type(os.environ.get("DEPLOYMENT_TYPE", "")) + data_dir = Path(os.environ["DATA_DIR"]) + run_path = ( + data_dir + / "contracts" + / "broadcast" + / DEPLOYMENT_SCRIPTS[deployment_type] + / os.environ["ETHEREUM_CHAIN_ID"] + / "run-latest.json" + ) + return json.loads(run_path.read_text()) + + +def get_deployed_address(contract_name: str) -> str: + address = get_created_contract_address(load_deployment_run(), contract_name) + if not address: + raise SystemExit(f"{contract_name} address not found in deployment broadcast") + return address + + +def cast_call(address: str, selector: str, *args: str) -> str: + return run( + [ + "docker", "compose", "run", "--rm", "--entrypoint", "cast", + "contracts", "call", + "--rpc-url", "http://ethereum:8545", + address, selector, *args, + ], + capture_output=True, + ).stdout.strip() + + +def cast_block_number() -> int: + return int( + run( + [ + "docker", "compose", "run", "--rm", "--entrypoint", "cast", + "contracts", "block-number", + "--rpc-url", "http://ethereum:8545", + ], + capture_output=True, + ).stdout.strip() + ) + + +def get_latest_keyper_set_index() -> int: + """Return the highest Keyper Set Index registered on-chain. + + Index 0 is the bootstrap guard keyper set; real sets start at 1. + """ + ksm = get_deployed_address("KeyperSetManager") + count = int(cast_call(ksm, "getNumKeyperSets()(uint64)")) + if count == 0: + raise SystemExit("No keyper sets exist on-chain") + return count - 1 + + +def get_dkg_start(ksi: int, retry: int) -> int: + dkg = get_deployed_address("DKGContract") + return int(cast_call(dkg, "dkgStart(uint64,uint64)(int256)", str(ksi), str(retry))) + + +def get_cycle_length() -> int: + dkg = get_deployed_address("DKGContract") + return int(cast_call(dkg, "cycleLength()(uint64)")) + + +def get_phase_length() -> int: + dkg = get_deployed_address("DKGContract") + return int(cast_call(dkg, "PHASE_LENGTH()(uint64)")) + + +def cast_logs_at_address( + address: str, + *, + from_block: str = "earliest", + to_block: str | None = None, +) -> list[dict]: + """Fetch all logs for the given contract address as parsed JSON entries. + + A single `cast logs` invocation; no topic filtering — callers narrow + results client-side via topic0 (event signature) and topic1..3. + """ + cmd = [ + "docker", "compose", "run", "--rm", "--entrypoint", "cast", + "contracts", "logs", + "--rpc-url", "http://ethereum:8545", + "--address", address, + "--from-block", from_block, + ] + if to_block is not None: + cmd += ["--to-block", to_block] + cmd.append("--json") + result = run(cmd, capture_output=True).stdout.strip() + if not result: + return [] + return json.loads(result) + + +def topic_to_uint(topic: str) -> int: + return int(topic, 16) + + +def get_succeeded(ksi: int) -> bool: + dkg = get_deployed_address("DKGContract") + result = cast_call(dkg, "succeeded(uint64)(bool)", str(ksi)) + return result.lower() == "true" + + +def derive_retry_counter(ksi: int) -> int: + """Derive the active retry counter for a Keyper Set Index from the current block. + + Returns the largest n such that dkgStart(ksi, n) <= currentBlock, or 0 if + no retry has started yet. + """ + current = cast_block_number() + start_0 = get_dkg_start(ksi, 0) + cycle = get_cycle_length() + if current < start_0: + return 0 + return (current - start_0) // cycle + + +def retry_window_elapsed(ksi: int, retry: int) -> bool: + """Return True once the full cycle window for (ksi, retry) has passed.""" + start = get_dkg_start(ksi, retry) + cycle = get_cycle_length() + return cast_block_number() > start + cycle diff --git a/mise-test-setup/mise-tasks/wait-for-decryption-key b/mise-test-setup/mise-tasks/wait-for-decryption-key index 20af1ea5f..d999a161e 100755 --- a/mise-test-setup/mise-tasks/wait-for-decryption-key +++ b/mise-test-setup/mise-tasks/wait-for-decryption-key @@ -2,6 +2,7 @@ #MISE description="Wait for a decryption key" #USAGE flag "--eon " help="Target eon" #USAGE flag "--identity-prefix " help="Identity prefix hex string" +#USAGE flag "--keyper-index " help="Keyper index whose DB to query (default 0; use a member of the target set)" import os import time @@ -12,6 +13,7 @@ import utils requested_eon = os.environ.get("usage_eon", "").strip() identity_prefix = os.environ.get("usage_identity_prefix", "").strip() poll_interval = float(os.environ["DECRYPTION_KEY_POLL_INTERVAL"]) +keyper_index = int(os.environ.get("usage_keyper_index") or "0") if not requested_eon or not identity_prefix: raise SystemExit("Provide both --eon and --identity-prefix") @@ -37,7 +39,7 @@ while True: continue exists = utils.query_keyper_db( - 0, + keyper_index, "SELECT EXISTS (" "SELECT 1 FROM decryption_key " f"WHERE eon = {int(keyper_set_index)} AND encode(epoch_id, 'hex') = '{identity}'" diff --git a/mise-test-setup/mise-tasks/wait-for-dkg b/mise-test-setup/mise-tasks/wait-for-dkg index b06995267..b662f3f59 100755 --- a/mise-test-setup/mise-tasks/wait-for-dkg +++ b/mise-test-setup/mise-tasks/wait-for-dkg @@ -1,7 +1,9 @@ #!/usr/bin/env -S uv run --script -#MISE description="Wait for DKG completion" -#USAGE flag "--keyper-set-index " help="Wait for a successful DKG for this keyper set index" -#USAGE flag "--eon " help="Wait for completion of this eon" +#MISE description="Wait for DKG completion on the DKG Contract" +#USAGE flag "--ksi " help="Keyper Set Index to watch (default: latest registered)" +#USAGE flag "--retry " help="Retry counter to watch (default: derived from current block)" +#USAGE flag "--success" help="Require eventual DKG success; exit nonzero on failure" +#USAGE flag "--failure" help="Require DKG failure; exit nonzero on success" import os import time @@ -9,45 +11,50 @@ import time import utils -keyper_set_index = os.environ.get("usage_keyper_set_index", "").strip() -eon = os.environ.get("usage_eon", "").strip() +require_success = os.environ.get("usage_success", "").lower() == "true" +require_failure = os.environ.get("usage_failure", "").lower() == "true" +if require_success and require_failure: + raise SystemExit("--success and --failure are mutually exclusive") + +ksi_raw = os.environ.get("usage_ksi", "").strip() +retry_raw = os.environ.get("usage_retry", "").strip() poll_interval = float(os.environ["DKG_RESULT_POLL_INTERVAL"]) -if bool(keyper_set_index) == bool(eon): - raise SystemExit("Provide exactly one of --keyper-set-index or --eon") +ksi = int(ksi_raw) if ksi_raw else utils.get_latest_keyper_set_index() +retry_fixed: int | None = int(retry_raw) if retry_raw else None -last_reported_failed_eon: str | None = None +# Which retry's timeout do we treat as a "failure" signal? +# - With --retry N: that retry. +# - Without --retry: retry 0 (first one that can time out). Once retry 0's +# window has elapsed without succeeded[ksi], at least one DKG Instance has +# completed-as-failure — enough for "neither" and "--failure" to exit. +# "--success" without --retry ignores this and keeps polling. +target_retry = retry_fixed if retry_fixed is not None else 0 while True: - if keyper_set_index: - target_eon = utils.query_keyper_db( - 0, - f"SELECT max(eon) FROM eons WHERE keyper_config_index = {int(keyper_set_index)}", - ) - if not target_eon: - time.sleep(poll_interval) - continue + succeeded = utils.get_succeeded(ksi) + window_elapsed = utils.retry_window_elapsed(ksi, target_retry) + + if require_success: + if succeeded: + print(f"DKG succeeded for ksi {ksi}") + raise SystemExit(0) + if retry_fixed is not None and window_elapsed: + print(f"DKG retry {retry_fixed} failed for ksi {ksi}; required success") + raise SystemExit(1) + elif require_failure: + if window_elapsed: + print(f"DKG retry {target_retry} failed for ksi {ksi}") + raise SystemExit(0) + if succeeded: + print(f"DKG succeeded for ksi {ksi}; required failure") + raise SystemExit(1) else: - target_eon = str(int(eon)) - - success = utils.query_keyper_db( - 0, - f"SELECT success FROM dkg_result WHERE eon = {target_eon}", - ) - if not success: - time.sleep(poll_interval) - continue - - if success == "t": - print(f"DKG finished successfully for eon {target_eon}") - raise SystemExit(0) - - if keyper_set_index: - if target_eon != last_reported_failed_eon: - print(f"DKG failed for eon {target_eon}") - last_reported_failed_eon = target_eon - time.sleep(poll_interval) - continue - - print(f"DKG failed for eon {target_eon}") - raise SystemExit(1) + if succeeded: + print(f"DKG succeeded for ksi {ksi}") + raise SystemExit(0) + if window_elapsed: + print(f"DKG retry {target_retry} failed for ksi {ksi}") + raise SystemExit(0) + + time.sleep(poll_interval) diff --git a/mise-test-setup/mise-tasks/wait-for-initial-dkg b/mise-test-setup/mise-tasks/wait-for-initial-dkg index f56da9423..0faabf37f 100755 --- a/mise-test-setup/mise-tasks/wait-for-initial-dkg +++ b/mise-test-setup/mise-tasks/wait-for-initial-dkg @@ -4,4 +4,4 @@ import utils -utils.run(["mise", "run", "wait-for-dkg", "--keyper-set-index", "1"]) +utils.run(["mise", "run", "wait-for-dkg", "--ksi", "1", "--success"]) diff --git a/mise-test-setup/mise-tasks/watch-dkg-events b/mise-test-setup/mise-tasks/watch-dkg-events new file mode 100755 index 000000000..c43c5fad6 --- /dev/null +++ b/mise-test-setup/mise-tasks/watch-dkg-events @@ -0,0 +1,167 @@ +#!/usr/bin/env -S uv run --script +#MISE description="Stream DKG Contract events in real time" +#USAGE flag "--ksi " help="Filter the stream to a single Keyper Set Index" +#USAGE flag "--retry " help="Filter the stream to a single retry counter" + +import os +import time + +import utils + + +# Event signature hashes match those in show-dkg-status. +DKG_SUCCEEDED_TOPIC = "0xe6c9bd3daa50b9218615605cdf333410d405b7f3b8dbeee5a2373769679aa9f5" +EVENT_NAMES = { + "0xa5074728b92791250d48ccdc55266aca7b1cca22f89e1ef2ab91ccd7ae0f337d": "DealingSubmitted", + "0xb5cc05e740a503c13e1b7ac3ad2d5abf41b91d2e5fca79455d34e515fb70849a": "AccusationSubmitted", + "0x62729f0820cf3ab60fbd63a51cdeae0554d0ab24f843b86bd0d2591594d69136": "ApologySubmitted", + "0xacfc0aa3f6ba36ac0ffda53e00d2cdace2fd4a9e41e2fe3214032794332c37c3": "SuccessVoteSubmitted", + DKG_SUCCEEDED_TOPIC: "DKGSucceeded", +} + +# AccusationSubmitted and ApologySubmitted both carry a uint64[] of indices as +# the first dynamic ABI argument in event data; the others have no useful +# non-indexed indices for diagnostics. +INDICES_LABEL = { + "0xb5cc05e740a503c13e1b7ac3ad2d5abf41b91d2e5fca79455d34e515fb70849a": "accused", + "0x62729f0820cf3ab60fbd63a51cdeae0554d0ab24f843b86bd0d2591594d69136": "accusers", +} + + +def parse_first_uint64_array(data_hex: str) -> list[int]: + """Decode the first dynamic uint64[] parameter in event data. + + Both AccusationSubmitted and ApologySubmitted start with a uint64[] in + non-indexed args; for other events this returns an empty list rather than + raising, so the watcher tolerates malformed or unfamiliar data fields. + """ + if not data_hex: + return [] + data = data_hex[2:] if data_hex.startswith("0x") else data_hex + if len(data) < 64: + return [] + try: + offset_chars = int(data[:64], 16) * 2 + if len(data) < offset_chars + 64: + return [] + length = int(data[offset_chars : offset_chars + 64], 16) + result: list[int] = [] + for i in range(length): + start = offset_chars + 64 + i * 64 + end = start + 64 + if len(data) < end: + break + result.append(int(data[start:end], 16)) + return result + except ValueError: + return [] + + +def block_number_of(ev: dict) -> int: + raw = ev.get("blockNumber") + if isinstance(raw, int): + return raw + if isinstance(raw, str): + return int(raw, 16) if raw.startswith("0x") else int(raw) + return 0 + + +def format_event(ev: dict) -> str | None: + topics = ev.get("topics", []) + if not topics: + return None + topic0 = topics[0].lower() + name = EVENT_NAMES.get(topic0) + if name is None: + return None + block = block_number_of(ev) + ksi = utils.topic_to_uint(topics[1]) if len(topics) >= 2 else None + retry = utils.topic_to_uint(topics[2]) if len(topics) >= 3 else None + keyper = utils.topic_to_uint(topics[3]) if len(topics) >= 4 else None + parts = [ + f"block={block}", + f"event={name}", + f"ksi={ksi}", + f"retry={retry}", + ] + if keyper is not None: + parts.append(f"keyper={keyper}") + indices_label = INDICES_LABEL.get(topic0) + if indices_label is not None: + data = ev.get("data", "") or "" + indices = parse_first_uint64_array(data) + parts.append(f"{indices_label}=[{','.join(str(i) for i in indices)}]") + return " ".join(parts) + + +def passes_filter(ev: dict, ksi_filter: int | None, retry_filter: int | None) -> bool: + topics = ev.get("topics", []) + if ksi_filter is not None: + if len(topics) < 2 or utils.topic_to_uint(topics[1]) != ksi_filter: + return False + if retry_filter is not None: + if len(topics) < 3 or utils.topic_to_uint(topics[2]) != retry_filter: + return False + return True + + +def is_terminal_dkg_succeeded(ev: dict, ksi_filter: int | None) -> bool: + """True when this event should auto-stop the watcher. + + Only fires when a ksi filter is active and the event is a `DKGSucceeded` + for that ksi. The retry filter is intentionally ignored here: a + `DKGSucceeded` log terminates the entire DKG cycle for the ksi, so even a + watcher pinned to a non-final retry should be notified and exit. + """ + if ksi_filter is None: + return False + topics = ev.get("topics", []) + if not topics or topics[0].lower() != DKG_SUCCEEDED_TOPIC: + return False + if len(topics) < 2 or utils.topic_to_uint(topics[1]) != ksi_filter: + return False + return True + + +ksi_raw = os.environ.get("usage_ksi", "").strip() +retry_raw = os.environ.get("usage_retry", "").strip() +ksi_filter = int(ksi_raw) if ksi_raw else None +retry_filter = int(retry_raw) if retry_raw else None + +poll_interval = float(os.environ.get("DKG_RESULT_POLL_INTERVAL", "1")) + +dkg_address = utils.get_deployed_address("DKGContract") + +# Tail semantics: start from the next block, not history. +next_block = utils.cast_block_number() + 1 + +try: + while True: + head = utils.cast_block_number() + if head >= next_block: + events = utils.cast_logs_at_address( + dkg_address, + from_block=str(next_block), + to_block=str(head), + ) + events.sort( + key=lambda e: ( + block_number_of(e), + int(e.get("logIndex", "0x0"), 16) + if isinstance(e.get("logIndex"), str) + else int(e.get("logIndex") or 0), + ) + ) + for ev in events: + auto_stop = is_terminal_dkg_succeeded(ev, ksi_filter) + if not auto_stop and not passes_filter(ev, ksi_filter, retry_filter): + continue + line = format_event(ev) + if line is not None: + print(line, flush=True) + if auto_stop: + raise SystemExit(0) + next_block = head + 1 + time.sleep(poll_interval) +except KeyboardInterrupt: + raise SystemExit(0) diff --git a/mise-test-setup/mise.toml b/mise-test-setup/mise.toml index c6be5a0c9..18dbe4f11 100644 --- a/mise-test-setup/mise.toml +++ b/mise-test-setup/mise.toml @@ -7,26 +7,21 @@ ANVIL_ADDRESS_1 = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" ANVIL_KEY_2 = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" ANVIL_ADDRESS_2 = "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" -DATA_DIR = "./data/" -GENERATED_DIR = "./generated/" -STATIC_DIR = "./static/" +DATA_DIR = "{{ get_env(name='DATA_DIR', default='./data/') }}" +GENERATED_DIR = "{{ get_env(name='GENERATED_DIR', default='./generated/') }}" +STATIC_DIR = "{{ get_env(name='STATIC_DIR', default='./static/') }}" DEPLOY_KEY = "{{env.ANVIL_KEY_0}}" -ETHEREUM_BLOCK_TIME = "1" -ETHEREUM_CHAIN_ID = "31337" -NUM_KEYPERS = "4" -INITIAL_KEYPER_SET_INDICES = "0,1,2" -INITIAL_THRESHOLD = "2" -ACTIVATION_DELTA = "10" -DKG_RESULT_POLL_INTERVAL = "1" -DECRYPTION_KEY_POLL_INTERVAL = "1" -DECRYPTION_TRIGGER_TIMESTAMP_DELTA = "10" -SHUTTERMINT_BLOCK_TIME = "1" -SHUTTERMINT_DKG_PHASE_LENGTH = "10" -SHUTTERMINT_START_BLOCK_DELTA = "200" -SHUTTERMINT_RPC_PORT = "26657" -SHUTTERMINT_P2P_PORT = "26656" -DEPLOYMENT_TYPE = "service" # "gnosis" or "service" +ETHEREUM_BLOCK_TIME = "{{ get_env(name='ETHEREUM_BLOCK_TIME', default='1') }}" +ETHEREUM_CHAIN_ID = "{{ get_env(name='ETHEREUM_CHAIN_ID', default='31337') }}" +NUM_KEYPERS = "{{ get_env(name='NUM_KEYPERS', default='4') }}" +INITIAL_KEYPER_SET_INDICES = "{{ get_env(name='INITIAL_KEYPER_SET_INDICES', default='0,1,2') }}" +INITIAL_THRESHOLD = "{{ get_env(name='INITIAL_THRESHOLD', default='2') }}" +ACTIVATION_DELTA = "{{ get_env(name='ACTIVATION_DELTA', default='10') }}" +DKG_RESULT_POLL_INTERVAL = "{{ get_env(name='DKG_RESULT_POLL_INTERVAL', default='1') }}" +DECRYPTION_KEY_POLL_INTERVAL = "{{ get_env(name='DECRYPTION_KEY_POLL_INTERVAL', default='1') }}" +DECRYPTION_TRIGGER_TIMESTAMP_DELTA = "{{ get_env(name='DECRYPTION_TRIGGER_TIMESTAMP_DELTA', default='10') }}" +DEPLOYMENT_TYPE = "{{ get_env(name='DEPLOYMENT_TYPE', default='service') }}" # "gnosis" or "service" # Load optional overrides after defaults so .env wins when present. mise.file = ".env" diff --git a/rolling-shutter/app/app.go b/rolling-shutter/app/app.go deleted file mode 100644 index a0162d86e..000000000 --- a/rolling-shutter/app/app.go +++ /dev/null @@ -1,891 +0,0 @@ -package app - -import ( - "encoding/base64" - "encoding/gob" - stderrors "errors" - "fmt" - "os" - "reflect" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/tendermint/go-amino" - abcitypes "github.com/tendermint/tendermint/abci/types" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/shutterevents" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/shutterevents/shtxresp" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -var ( - // PersistMinDuration is the minimum duration between two calls to persistToDisk - // TODO we should probably increase the default here and we should have a way to collect - // garbage to keep the persisted state small enough. - // The variable is declared here, because we do not want to persist it as part of the - // application. The same could be said about the Gobpath field though, which we persist as - // part of the application. - // If we set this to zero, the state will get saved on every call to Commit. - PersistMinDuration time.Duration = 30 * time.Second - - // NonExistentValidator is an artificial key used to replace the voting power of validators - // that haven't sent their CheckIn message yet. - NonExistentValidator ValidatorPubkey -) - -func init() { - gob.Register(crypto.S256()) // Allow gob to serialize ecsda.PrivateKey - - var err error - k := [32]byte{'n', 'o', 'v', 'a', 'l', 'i', 'd', 'a', 't', 'o', 'r'} - NonExistentValidator, err = NewValidatorPubkey(k[:]) - if err != nil { - panic(err) - } -} - -// Visit https://github.com/tendermint/spec/blob/master/spec/abci/abci.md for more information on -// the application interface we're implementing here. -// https://docs.tendermint.com/master/spec/abci/apps.html also provides some useful information - -// CheckTx checks if a transaction is valid. If return Code != 0, it will be rejected from the -// mempool and hence not broadcasted to other peers and not included in a proposal block. -func (app *ShutterApp) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { - signer, msg, err := app.decodeTx(req.Tx) - if err != nil { - return abcitypes.ResponseCheckTx{Code: 1, Log: "cannot decode message"} - } - if string(msg.ChainId) != app.ChainID { - return abcitypes.ResponseCheckTx{Code: 1, Log: "wrong chain"} - } - - // Check that the message's nonce has not been used by the sender yet. Note that - // `app.NonceTracker` keeps track of nonces of transactions included in the chain, while - // `app.CheckTxState` keeps track of all transactions in the mempool checked since the last - // `Commit`. - if !app.NonceTracker.Check(signer, msg.RandomNonce) { - return abcitypes.ResponseCheckTx{Code: 1, Log: "nonce already used"} - } - if !app.CheckTxState.AddTx(signer, msg) { - return abcitypes.ResponseCheckTx{Code: 1, Log: "not a keyper set member"} - } - return abcitypes.ResponseCheckTx{Code: 0, GasWanted: 1} -} - -// NewShutterApp creates a new ShutterApp. -func NewShutterApp() *ShutterApp { - return &ShutterApp{ - Configs: []*BatchConfig{{}}, - DKGMap: make(map[uint64]*DKGInstance), - ConfigVoting: NewConfigVoting(), - Identities: make(map[common.Address]ValidatorPubkey), - BlocksSeen: make(map[common.Address]uint64), - CheckTxState: NewCheckTxState(), - NonceTracker: NewNonceTracker(), - ChainID: "", // will be set in InitChain - ForkHeights: nil, // will be set in InitChain - } -} - -// LoadShutterAppFromFile loads a shutter app from a file. -func LoadShutterAppFromFile(gobpath string) (ShutterApp, error) { - var shapp ShutterApp - gobfile, err := os.Open(gobpath) - if os.IsNotExist(err) { - shapp = *NewShutterApp() - } else if err != nil { - return shapp, err - } else { - defer gobfile.Close() - dec := gob.NewDecoder(gobfile) - err = dec.Decode(&shapp) - if err != nil { - return shapp, err - } - shapp.ForkHeights = migrateForkHeights(shapp.ForkHeights) - log.Info(). - Str("file", gobpath). - Time("last-saved", shapp.LastSaved). - Int64("last-block-height", shapp.LastBlockHeight). - Bool("devmode", shapp.DevMode). - Msg("Loaded shutter app from file") - } - - shapp.Gobpath = gobpath - shapp.LastSaved = time.Now() // Do not persist immediately after starting - return shapp, nil -} - -// migrateForkHeights applies backwards-compatibility migrations for fork -// height state loaded from persisted app state or genesis. -func migrateForkHeights(forkHeights *ForkHeights) *ForkHeights { - if forkHeights == nil { - return NewForkHeightsAllDisabled() - } - if forkHeights.CheckInUpdateNew == (ForkHeight{}) && forkHeights.CheckInUpdate != nil { - log.Info(). - Int64("check-in-update-height", *forkHeights.CheckInUpdate). - Msg("migrating legacy fork height checkInUpdate to checkInUpdateNew") - forkHeights.CheckInUpdateNew = ForkHeight{ - Enabled: true, - Height: *forkHeights.CheckInUpdate, - } - } - if forkHeights.CheckInUpdate != nil { - log.Info().Msg("clearing legacy fork height checkInUpdate after migration") - forkHeights.CheckInUpdate = nil - } - return forkHeights -} - -// checkConfig checks if the given BatchConfig could be added. -func (app *ShutterApp) checkConfig(cfg BatchConfig) error { - err := cfg.EnsureValid() - if err != nil { - return err - } - lastConfig := app.LastConfig() - if cfg.ActivationBlockNumber < lastConfig.ActivationBlockNumber { - return errors.Errorf( - "start activation block number of next config (%d) lower than current one (%d)", - cfg.ActivationBlockNumber, - lastConfig.ActivationBlockNumber, - ) - } - if cfg.KeyperConfigIndex <= lastConfig.KeyperConfigIndex { - return errors.Errorf( - "config index of next config (%d) not greater than current one (%d)", - cfg.KeyperConfigIndex, - lastConfig.KeyperConfigIndex, - ) - } - return nil -} - -func (app *ShutterApp) addConfig(cfg BatchConfig) error { - err := app.checkConfig(cfg) - if err != nil { - return err - } - log.Info().Uint64("config-index", cfg.KeyperConfigIndex). - Msg("adding keyper config") - app.Configs = append(app.Configs, &cfg) - app.updateCheckTxMembers() - return nil -} - -// updateCheckTxMembers sets the member set of the check tx state to the set of known keypers. -// This should be called whenever a new config is added. -func (app *ShutterApp) updateCheckTxMembers() { - // This potentially double counts some keypers, but that's ok as CheckTxState.SetMembers - // ignores duplicates. - members := []common.Address{} - for _, c := range app.Configs { - members = append(members, c.Keypers...) - } - app.CheckTxState.SetMembers(members) -} - -func (app *ShutterApp) Query(_ abcitypes.RequestQuery) abcitypes.ResponseQuery { - return abcitypes.ResponseQuery{ - Code: 1, - Log: "query not implemented", - } -} - -// Info should return the latest committed state of the app. On startup, tendermint calls the Info -// method and will replay blocks that are not yet committed. -// See https://github.com/tendermint/spec/blob/master/spec/abci/apps.md#crash-recovery -func (app *ShutterApp) Info(_ abcitypes.RequestInfo) abcitypes.ResponseInfo { - return abcitypes.ResponseInfo{ - LastBlockHeight: app.LastBlockHeight, - LastBlockAppHash: []byte(""), - } -} - -func (ShutterApp) ListSnapshots(abcitypes.RequestListSnapshots) abcitypes.ResponseListSnapshots { - return abcitypes.ResponseListSnapshots{} -} - -func (ShutterApp) LoadSnapshotChunk(abcitypes.RequestLoadSnapshotChunk) abcitypes.ResponseLoadSnapshotChunk { - return abcitypes.ResponseLoadSnapshotChunk{} -} - -func (ShutterApp) ApplySnapshotChunk(abcitypes.RequestApplySnapshotChunk) abcitypes.ResponseApplySnapshotChunk { - return abcitypes.ResponseApplySnapshotChunk{} -} - -func (ShutterApp) OfferSnapshot(abcitypes.RequestOfferSnapshot) abcitypes.ResponseOfferSnapshot { - return abcitypes.ResponseOfferSnapshot{} -} - -/* - BlockExecution - -The first time a new blockchain is started, Tendermint calls InitChain. From then on, the following -sequence of methods is executed for each block: - -BeginBlock, [DeliverTx], EndBlock, Commit - -where one DeliverTx is called for each transaction in the block. The result is an updated -application state. Cryptographic commitments to the results of DeliverTx, EndBlock, and Commit are -included in the header of the next block. -*/ -func (app *ShutterApp) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain { //nolint:unparam - genesisState := GenesisAppState{} - err := amino.NewCodec().UnmarshalJSON(req.AppStateBytes, &genesisState) - if err != nil { - log.Fatal().Err(err).Msg("cannot unmarshal genesis app state") - } - - app.ForkHeights = migrateForkHeights(genesisState.ForkHeights) - - bc := BatchConfig{ - ActivationBlockNumber: 0, - Keypers: genesisState.GetKeypers(), - Threshold: genesisState.Threshold, - } - err = bc.EnsureValid() - if err != nil { - log.Fatal().Err(err).Msg("invalid genesis app state") - } - - if len(app.Configs) == 1 && len(app.Configs[0].Keypers) == 0 { - log.Info(). - Uint64("initial-eon", genesisState.InitialEon). - Str("chain-id", req.ChainId). - Msg("initializing new chain") - for i, k := range genesisState.Keypers { - log.Info().Int("index", i).Str("keyper", k.String()).Msg("initial keyper") - } - validators, err := MakePowermap(req.Validators) - if err != nil { - log.Fatal().Err(err).Msg("cannot handle validator keys") - } - app.Validators = validators - app.Configs = []*BatchConfig{&bc} - app.EONCounter = genesisState.InitialEon - app.CheckTxState = NewCheckTxState() - app.updateCheckTxMembers() - } else { - // XXX This else block is not executed anymore. Maybe we should remove it. - // Ensure that our app state matches the genesis config - if !reflect.DeepEqual(bc, *app.Configs[0]) { - log.Fatal(). - Interface("initial-state", bc). - Interface("stored-state", app.Configs[0]). - Msg("mismatch between stored app state and initial app state") - } - if app.EONCounter < genesisState.InitialEon { - log.Fatal(). - Uint64("EonCounter", app.EONCounter). - Uint64("InitialEon", genesisState.InitialEon). - Msg("mismatch between stored app state and initial app state") - } - } - - app.ChainID = req.ChainId - - return abcitypes.ResponseInitChain{} -} - -func (app *ShutterApp) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { - var events []abcitypes.Event - if req.Header.Height == 1 { - events = append(events, app.Configs[0].MakeABCIEvent()) - } - return abcitypes.ResponseBeginBlock{Events: events} -} - -func (app *ShutterApp) PrepareProposal(req abcitypes.RequestPrepareProposal) abcitypes.ResponsePrepareProposal { - txs := make([][]byte, 0, len(req.Txs)) - var totalBytes int64 - for _, tx := range req.Txs { - totalBytes += int64(len(tx)) - if totalBytes > req.MaxTxBytes { - break - } - txs = append(txs, tx) - } - return abcitypes.ResponsePrepareProposal{Txs: txs} -} - -func (app *ShutterApp) ProcessProposal(_ abcitypes.RequestProcessProposal) abcitypes.ResponseProcessProposal { - return abcitypes.ResponseProcessProposal{ - Status: abcitypes.ResponseProcessProposal_ACCEPT, - } -} - -// decodeTx decodes the given transaction. It's kind of strange that we have do URL decode the -// message outselves instead of tendermint doing it for us. -func (ShutterApp) decodeTx(tx []byte) (signer common.Address, msg *shmsg.MessageWithNonce, err error) { - var signedMsg []byte - signedMsg, err = base64.RawURLEncoding.DecodeString(string(tx)) - if err != nil { - return - } - signer, err = shmsg.GetSigner(signedMsg) - if err != nil { - return - } - - msg, err = shmsg.GetMessage(signedMsg) - if err != nil { - return - } - return -} - -func (app *ShutterApp) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { - signer, msg, err := app.decodeTx(req.Tx) - if err != nil { - msg := fmt.Sprintf("Error while decoding transaction: %s", err) - log.Print(msg) - return makeErrorResponse(msg) - } - if string(msg.ChainId) != app.ChainID { - return makeErrorResponse(fmt.Sprintf("wrong chain id (expected %s, got %s)", app.ChainID, msg.ChainId)) - } - if !app.NonceTracker.Check(signer, msg.RandomNonce) { - msg := fmt.Sprintf("Nonce %d of %s already used", msg.RandomNonce, signer.Hex()) - return makeErrorResponse(msg) - } - app.NonceTracker.Add(signer, msg.RandomNonce) - return app.deliverMessage(msg.Msg, signer) -} - -func makeErrorResponse(msg string) abcitypes.ResponseDeliverTx { - return abcitypes.ResponseDeliverTx{ - Code: shtxresp.Error, - Log: msg, - Events: []abcitypes.Event{}, - } -} - -func makeAlreadySeenResponse(msg string) abcitypes.ResponseDeliverTx { - return abcitypes.ResponseDeliverTx{ - Code: shtxresp.Seen, - Log: msg, - Events: []abcitypes.Event{}, - } -} - -func notAKeyper(sender common.Address) abcitypes.ResponseDeliverTx { - return makeErrorResponse(fmt.Sprintf( - "sender %s is not a keyper", sender.Hex())) -} - -func (app *ShutterApp) allowedToVoteOnConfigChanges(sender common.Address) bool { - lastConfig := app.LastConfig() - _, ok := lastConfig.KeyperIndex(sender) - return ok -} - -func (app *ShutterApp) deliverBatchConfig(msg *shmsg.BatchConfig, sender common.Address) abcitypes.ResponseDeliverTx { - bc, err := shutterevents.BatchConfigFromMessage(msg) - if err != nil { - return makeErrorResponse(fmt.Sprintf("Malformed BatchConfig message: %s", err)) - } - - if reflect.DeepEqual(*app.LastConfig(), bc) { - // The config has already been accepted. So, let's just return success - // XXX We do not check if we're allowed to vote on config changes here - log.Info().Uint64("config-index", bc.KeyperConfigIndex).Msg("keyper config already accepted") - return abcitypes.ResponseDeliverTx{ - Code: shtxresp.Seen, - } - } - err = app.checkConfig(bc) - if err != nil { - return makeErrorResponse(fmt.Sprintf("checkConfig: %s", err)) - } - - if !app.allowedToVoteOnConfigChanges(sender) { - return makeErrorResponse("not allowed to vote on config changes") - } - - var events []abcitypes.Event - - err = app.ConfigVoting.AddVote(sender, bc) - if err != nil { - return makeErrorResponse(fmt.Sprintf("Error adding vote: %s", err)) - } - - _, ok := app.ConfigVoting.Outcome(int(app.LastConfig().Threshold)) - if ok { - app.ConfigVoting = NewConfigVoting() - err = app.addConfig(bc) - if err != nil { - return makeErrorResponse(fmt.Sprintf("Error in addConfig: %s", err)) - } - - events = append(events, bc.MakeABCIEvent()) - dkg := app.StartDKG(bc) - events = append(events, shutterevents.EonStarted{ - Eon: dkg.Eon, - ActivationBlockNumber: bc.ActivationBlockNumber, - KeyperConfigIndex: bc.KeyperConfigIndex, - }.MakeABCIEvent()) - } - - return abcitypes.ResponseDeliverTx{ - Code: 0, - Events: events, - } -} - -// isKeyper checks if the given address is a keyper in any config (current and previous ones). -func (app *ShutterApp) isKeyper(a common.Address) bool { - for _, cfg := range app.Configs { - _, ok := cfg.KeyperIndex(a) - if ok { - return true - } - } - return false -} - -func (app *ShutterApp) deliverCheckIn(msg *shmsg.CheckIn, sender common.Address) abcitypes.ResponseDeliverTx { - // Before the check in update fork, only the first check-in of a keyper - // is accepted, any subsequent check-in is ignored. After the fork, check-ins - // of already checked-in keypers are accepted and update their validator - // identity and encryption key. - if !app.IsCheckInUpdateForkActive() { - _, ok := app.Identities[sender] - if ok { - return makeAlreadySeenResponse(fmt.Sprintf( - "sender %s already checked in", sender.Hex())) - } - } - if !app.isKeyper(sender) { - return notAKeyper(sender) - } - - validatorPublicKey, err := NewValidatorPubkey(msg.ValidatorPublicKey) - if err != nil { - return makeErrorResponse(fmt.Sprintf( - "malformed validator public key: %s", err)) - } - encryptionPublicKeyECDSA, err := crypto.DecompressPubkey(msg.EncryptionPublicKey) - if err != nil { - return makeErrorResponse(fmt.Sprintf("malformed encryption public key: %s", err)) - } - encryptionPublicKey := ecies.ImportECDSAPublic(encryptionPublicKeyECDSA) - - app.Identities[sender] = validatorPublicKey - return abcitypes.ResponseDeliverTx{ - Code: 0, - Events: []abcitypes.Event{ - shutterevents.CheckIn{ - Sender: sender, - EncryptionPublicKey: encryptionPublicKey, - }.MakeABCIEvent(), - }, - } -} - -func (app *ShutterApp) deliverBlockSeen( - msg *shmsg.BlockSeen, - sender common.Address, -) abcitypes.ResponseDeliverTx { - if msg.BlockNumber > app.BlocksSeen[sender] { - app.BlocksSeen[sender] = msg.BlockNumber - } - return abcitypes.ResponseDeliverTx{ - Code: 0, - Events: []abcitypes.Event{}, - } -} - -func (app *ShutterApp) deliverDKGResult(msg *shmsg.DKGResult, sender common.Address) abcitypes.ResponseDeliverTx { - dkginstance, ok := app.DKGMap[msg.Eon] - if !ok { - return makeErrorResponse(fmt.Sprintf( - "cannot handle DKGResult message for eon %d", msg.Eon), - ) - } - config := dkginstance.Config - if !config.IsKeyper(sender) { - return notAKeyper(sender) - } - - err := dkginstance.SuccessVoting.AddVote(sender, msg.Success) - if err != nil { - return makeAlreadySeenResponse("already voted on dkg result") - } - - dkg, started := app.maybeStartEon(msg.Eon) - if !started { - return abcitypes.ResponseDeliverTx{ - Code: 0, - Events: []abcitypes.Event{}, - } - } - return abcitypes.ResponseDeliverTx{ - Code: 0, - Events: []abcitypes.Event{ - shutterevents.EonStarted{ - Eon: dkg.Eon, - ActivationBlockNumber: config.ActivationBlockNumber, - KeyperConfigIndex: config.KeyperConfigIndex, - }.MakeABCIEvent(), - }, - } -} - -func (app *ShutterApp) maybeStartEon(eon uint64) (*DKGInstance, bool) { - dkg, ok := app.DKGMap[eon] - if !ok { - return nil, false - } - - threshold := int(dkg.Config.Threshold) - success, ok := dkg.SuccessVoting.Outcome(threshold) - // dismiss votes for Eon that was voted on successfully already - outdatedEon := app.EONCounter > eon - if !ok || success || outdatedEon { - return nil, false - } - return app.StartDKG(dkg.Config), true -} - -func (app *ShutterApp) handlePolyEvalMsg(msg *shmsg.PolyEval, sender common.Address) abcitypes.ResponseDeliverTx { - appMsg, err := ParsePolyEvalMsg(msg, sender) - if err != nil { - msg := fmt.Sprintf("Error: Failed to parse PolyEval message: %+v", err) - log.Print(msg) - return makeErrorResponse(msg) - } - - dkg := app.DKGMap[appMsg.Eon] - if dkg == nil { - msg := "Error: Received PolyEval message while DKG is not active" - log.Print(msg) - return makeErrorResponse(msg) - } - - err = dkg.RegisterPolyEvalMsg(*appMsg) - if err != nil { - if stderrors.Is(err, ErrorPolyEvalAlreadyPresent) { - return makeAlreadySeenResponse("PolyEval message already present") - } - msg := fmt.Sprintf("Error: Failed to register PolyEval message: %+v", err) - log.Print(msg) - return makeErrorResponse(msg) - } - - event := appMsg.MakeABCIEvent() - return abcitypes.ResponseDeliverTx{ - Code: 0, - Events: []abcitypes.Event{event}, - } -} - -func (app *ShutterApp) handlePolyCommitmentMsg(msg *shmsg.PolyCommitment, sender common.Address) abcitypes.ResponseDeliverTx { - appMsg, err := ParsePolyCommitmentMsg(msg, sender) - if err != nil { - msg := fmt.Sprintf("Error: Failed to parse PolyCommitment message: %+v", err) - log.Print(msg) - return makeErrorResponse(msg) - } - - dkg := app.DKGMap[appMsg.Eon] - if dkg == nil { - msg := "Error: Received PolyCommitment message while DKG is not active" - log.Print(msg) - return makeErrorResponse(msg) - } - - err = dkg.RegisterPolyCommitmentMsg(*appMsg) - if err != nil { - if stderrors.Is(err, ErrorPolyCommitAlreadyPresent) { - return makeAlreadySeenResponse("PolyCommitment message already present") - } - msg := fmt.Sprintf("Error: Failed to register PolyCommitment message: %+v", err) - log.Print(msg) - return makeErrorResponse(msg) - } - - event := appMsg.MakeABCIEvent() - return abcitypes.ResponseDeliverTx{ - Code: 0, - Events: []abcitypes.Event{event}, - } -} - -func (app *ShutterApp) handleAccusationMsg(msg *shmsg.Accusation, sender common.Address) abcitypes.ResponseDeliverTx { - appMsg, err := ParseAccusationMsg(msg, sender) - if err != nil { - msg := fmt.Sprintf("Error: Failed to parse Accusation message: %+v", err) - log.Print(msg) - return makeErrorResponse(msg) - } - - dkg := app.DKGMap[appMsg.Eon] - if dkg == nil { - msg := "Error: Received Accusation message while DKG is not active" - log.Print(msg) - return makeErrorResponse(msg) - } - - err = dkg.RegisterAccusationMsg(*appMsg) - if err != nil { - if stderrors.Is(err, ErrorAccusationAlreadyPresent) { - return makeAlreadySeenResponse("Accusation message already present") - } - msg := fmt.Sprintf("Error: Failed to register Accusation message: %+v", err) - log.Print(msg) - return makeErrorResponse(msg) - } - - event := appMsg.MakeABCIEvent() - return abcitypes.ResponseDeliverTx{ - Code: 0, - Events: []abcitypes.Event{event}, - } -} - -func (app *ShutterApp) handleApologyMsg(msg *shmsg.Apology, sender common.Address) abcitypes.ResponseDeliverTx { - appMsg, err := ParseApologyMsg(msg, sender) - if err != nil { - msg := fmt.Sprintf("Error: Failed to parse Apology message: %+v", err) - log.Print(msg) - return makeErrorResponse(msg) - } - - dkg := app.DKGMap[appMsg.Eon] - if dkg == nil { - msg := "Error: Received Apology message while DKG is not active" - log.Print(msg) - return makeErrorResponse(msg) - } - - err = dkg.RegisterApologyMsg(*appMsg) - if err != nil { - if stderrors.Is(err, ErrorApologyAlreadyPresent) { - return makeAlreadySeenResponse("Apology message already present") - } - msg := fmt.Sprintf("Error: Failed to register Apology message: %+v", err) - log.Print(msg) - return makeErrorResponse(msg) - } - - event := appMsg.MakeABCIEvent() - return abcitypes.ResponseDeliverTx{ - Code: 0, - Events: []abcitypes.Event{event}, - } -} - -func (app *ShutterApp) deliverMessage(msg *shmsg.Message, sender common.Address) abcitypes.ResponseDeliverTx { - if msg.GetBatchConfig() != nil { - return app.deliverBatchConfig(msg.GetBatchConfig(), sender) - } - if msg.GetBlockSeen() != nil { - return app.deliverBlockSeen(msg.GetBlockSeen(), sender) - } - if msg.GetCheckIn() != nil { - return app.deliverCheckIn(msg.GetCheckIn(), sender) - } - if msg.GetDkgResult() != nil { - return app.deliverDKGResult(msg.GetDkgResult(), sender) - } - - if msg.GetPolyEval() != nil { - return app.handlePolyEvalMsg(msg.GetPolyEval(), sender) - } - if msg.GetPolyCommitment() != nil { - return app.handlePolyCommitmentMsg(msg.GetPolyCommitment(), sender) - } - if msg.GetAccusation() != nil { - return app.handleAccusationMsg(msg.GetAccusation(), sender) - } - if msg.GetApology() != nil { - return app.handleApologyMsg(msg.GetApology(), sender) - } - log.Print("Error: cannot deliver message: ", msg) - return makeErrorResponse("cannot deliver message") -} - -func (app *ShutterApp) StartDKG(config BatchConfig) *DKGInstance { - app.EONCounter++ - dkg := NewDKGInstance(config, app.EONCounter) - app.DKGMap[dkg.Eon] = &dkg - return &dkg -} - -// LastConfig returns the config with the highest known index. -func (app *ShutterApp) LastConfig() *BatchConfig { - if len(app.Configs) == 0 { - panic("internal error: app.Configs is empty") - } - return app.Configs[len(app.Configs)-1] -} - -// makePowermap creates a power map for the given slice of keypers. The voting power of each keyper -// that hasn't registered yet, is given to the NonExistentValidator key. -func (app *ShutterApp) makePowermap(keypers []common.Address) Powermap { - pm := make(Powermap) - for _, k := range keypers { - pubkey, ok := app.Identities[k] - if ok { - pm[pubkey] += 10 - } else { - pm[NonExistentValidator] += 10 - } - } - return pm -} - -// CurrentValidators returns a powermap of current validators. -func (app *ShutterApp) CurrentValidators() Powermap { - for i := len(app.Configs) - 1; i >= 0; i-- { - if app.Configs[i].Started && app.Configs[i].ValidatorsUpdated { - return app.makePowermap(app.Configs[i].Keypers) - } - } - return app.Validators -} - -// countCheckedInKeypers counts the number of keypers that have already checked in in the given slice. -func (app *ShutterApp) countCheckedInKeypers(keypers []common.Address) uint64 { - var numCheckedIn uint64 - for _, k := range keypers { - _, ok := app.Identities[k] - if ok { - numCheckedIn++ - } - } - return numCheckedIn -} - -func (app *ShutterApp) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { - var events []abcitypes.Event - - for i, config := range app.Configs { - if !config.Started { - var allowanceConfigIndex int - if i > 0 { - allowanceConfigIndex = i - 1 - } - numRequiredVotes := app.Configs[allowanceConfigIndex].Threshold - - var numVotes uint64 - for _, k := range app.Configs[allowanceConfigIndex].Keypers { - b, ok := app.BlocksSeen[k] - if ok && b >= config.ActivationBlockNumber { - numVotes++ - } - } - if numVotes >= numRequiredVotes { - log.Info().Uint64("config-index", config.KeyperConfigIndex).Msg("starting keyper config") - config.Started = true - events = append(events, shutterevents.BatchConfigStarted{ - KeyperConfigIndex: config.KeyperConfigIndex, - }.MakeABCIEvent()) - } - } - if config.Started && !config.ValidatorsUpdated && app.countCheckedInKeypers(config.Keypers) >= numRequiredTransitionValidators(config) { - config.ValidatorsUpdated = true - } - } - - newValidators := app.CurrentValidators() - validatorUpdates := DiffPowermaps(app.Validators, newValidators).ValidatorUpdates() - app.Validators = newValidators - app.LastBlockHeight = req.Height - if app.DevMode { - if len(validatorUpdates) > 0 { - log.Info().Int("count", len(validatorUpdates)).Msg("ignoring validator updates in dev mode") - } - return abcitypes.ResponseEndBlock{Events: events} - } - if len(validatorUpdates) > 0 { - log.Info().Int("count", len(validatorUpdates)).Interface("validator-updates", validatorUpdates). - Msg("applying validator updates") - } - return abcitypes.ResponseEndBlock{ - ValidatorUpdates: validatorUpdates, - Events: events, - } -} - -// numRequiredTransitionValidators returns the number of validators required to be online before -// transitioning to a new config. This number is either the threshold value in the config or 2/3 -// of the validator set, whatever is greater. This makes sure that both the security assumption -// defined by the threshold is met and that the chain is able to make progress. -func numRequiredTransitionValidators(config *BatchConfig) uint64 { - n := len(config.Keypers) - if n == 0 { - // this case doesn't make much sense, but the normal path would return 1 which makes even - // less sense - return 0 - } - defenders := uint64(n - (n+2)/3 + 1) - if config.Threshold >= defenders { - return config.Threshold - } - return defenders -} - -// persistToDisk stores the ShutterApp on disk. This method first writes to a temporary file and -// renames the file later. Most probably this will not work on windows! -func (app *ShutterApp) PersistToDisk() error { - log.Info().Int64("height", app.LastBlockHeight).Msg("persisting state to disk") - tmppath := app.Gobpath + ".tmp" - file, err := os.Create(tmppath) - if err != nil { - return err - } - defer func() { - err = file.Close() - if err != nil { - log.Error().Err(err).Str("path", tmppath).Msg("failed to close file") - return - } - }() - - app.LastSaved = time.Now() - enc := gob.NewEncoder(file) - err = enc.Encode(app) - if err != nil { - return err - } - err = file.Sync() - if err != nil { - return err - } - err = os.Rename(tmppath, app.Gobpath) - return err -} - -func (app *ShutterApp) maybePersistToDisk() error { - if app.Gobpath == "" { - return nil - } - if time.Since(app.LastSaved) <= PersistMinDuration { - return nil - } - return app.PersistToDisk() -} - -func (app *ShutterApp) Commit() abcitypes.ResponseCommit { //nolint:unparam - app.CheckTxState.Reset() - - err := app.maybePersistToDisk() - if err != nil { - log.Error().Err(err).Msg("cannot persist state to disk") - } - - return abcitypes.ResponseCommit{} -} - -// CurrentBlockHeight returns the height of the block being processed between -// BeginBlock and EndBlock. -func (app *ShutterApp) CurrentBlockHeight() int64 { - return app.LastBlockHeight + 1 -} diff --git a/rolling-shutter/app/app_test.go b/rolling-shutter/app/app_test.go deleted file mode 100644 index dec419263..000000000 --- a/rolling-shutter/app/app_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package app - -import ( - "testing" - - "github.com/ethereum/go-ethereum/common" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" - - "github.com/shutter-network/shutter/shlib/shtest" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/testlog" -) - -func init() { - testlog.Setup() -} - -func TestNewShutterApp(t *testing.T) { - app := NewShutterApp() - assert.Equal(t, len(app.Configs), 1, "Configs should contain exactly one guard element") - assert.Assert(t, is.DeepEqual(app.Configs[0], &BatchConfig{}), "Bad guard element") -} - -func TestAddConfig(t *testing.T) { - app := NewShutterApp() - - err := app.addConfig(BatchConfig{ - KeyperConfigIndex: 1, - ActivationBlockNumber: 100, - Threshold: 1, - Keypers: addr, - }) - assert.NilError(t, err) - - err = app.addConfig(BatchConfig{ - KeyperConfigIndex: 2, - ActivationBlockNumber: 99, - Threshold: 1, - Keypers: addr, - }) - assert.Assert(t, err != nil, "Expected error, ActivationBlockNumber must not decrease") - - err = app.addConfig(BatchConfig{ - KeyperConfigIndex: 1, - ActivationBlockNumber: 100, - Threshold: 1, - Keypers: addr, - }) - assert.Assert(t, err != nil, "Expected error, ConfigIndex must increase") - - err = app.addConfig(BatchConfig{ - KeyperConfigIndex: 2, - ActivationBlockNumber: 100, - Threshold: 2, - Keypers: addr, - }) - assert.NilError(t, err) -} - -func TestGobDKG(t *testing.T) { - var eon uint64 = 201 - var err error - keypers := addr - dkg := NewDKGInstance(BatchConfig{ - KeyperConfigIndex: 1, - ActivationBlockNumber: 100, - Threshold: 1, - Keypers: keypers, - }, eon) - - err = dkg.RegisterAccusationMsg(Accusation{ - Sender: keypers[0], - Eon: eon, - Accused: []common.Address{keypers[1]}, - }) - assert.NilError(t, err) - - err = dkg.RegisterApologyMsg(Apology{ - Sender: keypers[0], - Eon: eon, - Accusers: []common.Address{keypers[1]}, - }) - assert.NilError(t, err) - - err = dkg.RegisterPolyCommitmentMsg(PolyCommitment{ - Sender: keypers[0], - Eon: eon, - }) - assert.NilError(t, err) - - err = dkg.RegisterPolyEvalMsg(PolyEval{ - Sender: keypers[0], - Eon: eon, - Receivers: []common.Address{keypers[1]}, - EncryptedEvals: [][]byte{{}}, - }) - assert.NilError(t, err) - - shtest.EnsureGobable(t, &dkg, new(DKGInstance)) -} - -func TestMigrateForkHeights(t *testing.T) { - t.Run("fork heights nil defaults to all disabled", func(t *testing.T) { - got := migrateForkHeights(nil) - assert.Assert(t, is.DeepEqual(got, NewForkHeightsAllDisabled())) - }) - - t.Run("legacy CheckInUpdate migrates to CheckInUpdateNew", func(t *testing.T) { - legacyHeight := int64(123) - got := migrateForkHeights(&ForkHeights{CheckInUpdate: &legacyHeight}) - - assert.Assert(t, got.CheckInUpdate == nil) - assert.Assert(t, is.DeepEqual(got.CheckInUpdateNew, ForkHeight{Enabled: true, Height: 123})) - }) - - t.Run("legacy CheckInUpdate zero migrates to enabled genesis fork", func(t *testing.T) { - legacyHeight := int64(0) - got := migrateForkHeights(&ForkHeights{CheckInUpdate: &legacyHeight}) - - assert.Assert(t, got.CheckInUpdate == nil) - assert.Assert(t, is.DeepEqual(got.CheckInUpdateNew, ForkHeight{Enabled: true, Height: 0})) - }) - - t.Run("CheckInUpdateNew is preserved and CheckInUpdate is unset", func(t *testing.T) { - legacyHeight := int64(999) - expected := ForkHeight{Enabled: true, Height: 42} - got := migrateForkHeights(&ForkHeights{ - CheckInUpdate: &legacyHeight, - CheckInUpdateNew: expected, - }) - - assert.Assert(t, got.CheckInUpdate == nil) - assert.Assert(t, is.DeepEqual(got.CheckInUpdateNew, expected)) - }) - - t.Run("idempotent when CheckInUpdate is unset", func(t *testing.T) { - initial := &ForkHeights{CheckInUpdateNew: ForkHeight{Enabled: true, Height: 42}} - - first := migrateForkHeights(initial) - second := migrateForkHeights(first) - - assert.Assert(t, is.DeepEqual(second, first)) - }) -} diff --git a/rolling-shutter/app/checktx.go b/rolling-shutter/app/checktx.go deleted file mode 100644 index bd7a48069..000000000 --- a/rolling-shutter/app/checktx.go +++ /dev/null @@ -1,51 +0,0 @@ -package app - -import ( - "github.com/ethereum/go-ethereum/common" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -// MaxTxsPerBlock is the maximum number of txs by a single sender per block. -const MaxTxsPerBlock = 10 - -// NewCheckTxState returns a new check tx state. -func NewCheckTxState() *CheckTxState { - s := CheckTxState{} - s.Reset() - return &s -} - -// Reset should be called at every block commit so that all nodes get the same value no matter -// their view on the network. -func (s *CheckTxState) Reset() { - s.TxCounts = make(map[common.Address]int) - s.NonceTracker = NewNonceTracker() -} - -// SetMembers sets the member set allowed to send txs. Duplicate addresses are ignored. -func (s *CheckTxState) SetMembers(members []common.Address) { - s.Members = make(map[common.Address]bool) - for _, address := range members { - s.Members[address] = true - } -} - -// AddTx checks if a tx can be added and updates the internal state accordingly. -// Returns true if the sender is a member (or the member set is empty), has not exceeded their -// tx limit yet, and has not used the message's nonce since the last call to `Reset`. -func (s *CheckTxState) AddTx(sender common.Address, msg *shmsg.MessageWithNonce) bool { - if len(s.Members) > 0 && !s.Members[sender] { - return false - } - if s.TxCounts[sender] >= MaxTxsPerBlock { - return false - } - if !s.NonceTracker.Check(sender, msg.RandomNonce) { - return false - } - - s.TxCounts[sender]++ - s.NonceTracker.Add(sender, msg.RandomNonce) - return true -} diff --git a/rolling-shutter/app/checktx_test.go b/rolling-shutter/app/checktx_test.go deleted file mode 100644 index b901b17b2..000000000 --- a/rolling-shutter/app/checktx_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package app - -import ( - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "gotest.tools/v3/assert" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -func TestCheckTxStateCouting(t *testing.T) { - a1 := common.BigToAddress(big.NewInt(0)) - a2 := common.BigToAddress(big.NewInt(1)) - n := uint64(0) - - makeMsg := func() *shmsg.MessageWithNonce { - n++ - return &shmsg.MessageWithNonce{ - RandomNonce: n, - Msg: nil, - } - } - - s := NewCheckTxState() - assert.Assert(t, len(s.Members) == 0) - assert.Assert(t, len(s.TxCounts) == 0) - - assert.Assert(t, s.AddTx(a1, makeMsg())) // no members set yet, so anyone can send - s.Reset() - - s.SetMembers([]common.Address{a1}) - assert.Assert(t, !s.AddTx(a2, makeMsg())) // not a member - - for i := 0; i < MaxTxsPerBlock; i++ { - assert.Assert(t, s.AddTx(a1, makeMsg())) - } - assert.Assert(t, !s.AddTx(a1, makeMsg())) // too many txs - s.Reset() - assert.Assert(t, s.AddTx(a1, makeMsg())) -} - -func TestCheckTxStateNonce(t *testing.T) { - a1 := common.BigToAddress(big.NewInt(0)) - a2 := common.BigToAddress(big.NewInt(1)) - - msg1 := &shmsg.MessageWithNonce{ - RandomNonce: uint64(10), - Msg: nil, - } - msg2 := &shmsg.MessageWithNonce{ - RandomNonce: uint64(20), - Msg: nil, - } - - s := NewCheckTxState() - s.SetMembers([]common.Address{a1, a2}) - - assert.Assert(t, s.AddTx(a1, msg1)) - assert.Assert(t, !s.AddTx(a1, msg1)) - assert.Assert(t, s.AddTx(a1, msg2)) - assert.Assert(t, s.AddTx(a2, msg1)) - s.Reset() - assert.Assert(t, s.AddTx(a1, msg1)) -} diff --git a/rolling-shutter/app/configvoting_test.go b/rolling-shutter/app/configvoting_test.go deleted file mode 100644 index 93a017c84..000000000 --- a/rolling-shutter/app/configvoting_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package app - -import ( - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "gotest.tools/v3/assert" - - "github.com/shutter-network/shutter/shlib/shtest" -) - -var ( - addr []common.Address - votes []BatchConfig -) - -func init() { - for i := 0; i < 10; i++ { - addr = append(addr, common.BigToAddress(big.NewInt(int64(i)))) - votes = append(votes, BatchConfig{ActivationBlockNumber: uint64(i)}) - } -} - -func TestConfigVoting(t *testing.T) { - cfgv := NewConfigVoting() - // Make sure we don't have an outcome yet. - _, ok := cfgv.Outcome(0) - assert.Assert(t, !ok) - _, ok = cfgv.Outcome(1) - assert.Assert(t, !ok) - - err := cfgv.AddVote(addr[0], votes[0]) - assert.NilError(t, err) - - outcome, ok := cfgv.Outcome(0) - assert.Assert(t, ok) - assert.DeepEqual(t, votes[0], outcome) - - outcome, ok = cfgv.Outcome(1) - assert.Assert(t, ok) - assert.DeepEqual(t, votes[0], outcome) - - _, ok = cfgv.Outcome(2) - assert.Assert(t, !ok) - - err = cfgv.AddVote(addr[0], votes[0]) // duplicate vote, same vote - assert.Assert(t, err != nil, "voting two times should be prohibited") - - err = cfgv.AddVote(addr[0], votes[1]) // duplicate vote, different vote - assert.Assert(t, err != nil, "voting two times should be prohibited") - - err = cfgv.AddVote(addr[1], votes[2]) - assert.NilError(t, err) - - _, ok = cfgv.Outcome(2) - assert.Assert(t, !ok) - - err = cfgv.AddVote(addr[2], votes[2]) - assert.NilError(t, err) - - outcome, ok = cfgv.Outcome(2) - assert.Assert(t, ok) - assert.DeepEqual(t, votes[2], outcome) - - var dst ConfigVoting - shtest.EnsureGobable(t, &cfgv, &dst) -} diff --git a/rolling-shutter/app/dkg.go b/rolling-shutter/app/dkg.go deleted file mode 100644 index db1a1584b..000000000 --- a/rolling-shutter/app/dkg.go +++ /dev/null @@ -1,129 +0,0 @@ -package app - -import ( - stderrors "errors" - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" -) - -var ( - ErrorPolyEvalAlreadyPresent = stderrors.New("poly evaluation already present") - ErrorPolyCommitAlreadyPresent = stderrors.New("poly commitment already present") - ErrorAccusationAlreadyPresent = stderrors.New("accusation already present") - ErrorApologyAlreadyPresent = stderrors.New("apology already present") -) - -// NewDKGInstance creates a new DKGInstance. -func NewDKGInstance(config BatchConfig, eon uint64) DKGInstance { - return DKGInstance{ - Config: config, - Eon: eon, - SuccessVoting: NewVoting[bool, ComparableEquals[bool]](), - PolyEvalsSeen: make(map[SenderReceiverPair]struct{}), - PolyCommitmentsSeen: make(map[common.Address]struct{}), - AccusationsSeen: make(map[common.Address]struct{}), - ApologiesSeen: make(map[common.Address]struct{}), - } -} - -// RegisterPolyEvalMsg adds a polynomial evaluation message to the instance. It makes sure the -// message meets the basic requirements, i.e. the sender and receivers are keypers and we do not -// send multiple messages from one sender to one receiver. -func (dkg *DKGInstance) RegisterPolyEvalMsg(msg PolyEval) error { - if msg.Eon != dkg.Eon { - return errors.Errorf("msg is from eon %d, not %d", msg.Eon, dkg.Eon) - } - - sender := msg.Sender - if !dkg.Config.IsKeyper(sender) { - return errors.Errorf("sender %s is not a keyper", sender.Hex()) - } - - for _, receiver := range msg.Receivers { - if !dkg.Config.IsKeyper(receiver) { - return errors.Errorf("receiver %s is not a keyper", msg.Sender.Hex()) - } - if receiver == sender { - return errors.Errorf("receiver %s is also the sender", msg.Sender.Hex()) - } - _, ok := dkg.PolyEvalsSeen[SenderReceiverPair{sender, receiver}] - if ok { - return fmt.Errorf("%w: from keyper %s for receiver %s", ErrorPolyEvalAlreadyPresent, sender.Hex(), receiver.Hex()) - } - } - - for _, receiver := range msg.Receivers { - dkg.PolyEvalsSeen[SenderReceiverPair{sender, receiver}] = struct{}{} - } - - return nil -} - -// RegisterPolyCommitmentMsg adds a polynomial commitment message to the instance. -func (dkg *DKGInstance) RegisterPolyCommitmentMsg(msg PolyCommitment) error { - if msg.Eon != dkg.Eon { - return errors.Errorf("msg is from eon %d, not %d", msg.Eon, dkg.Eon) - } - if !dkg.Config.IsKeyper(msg.Sender) { - return errors.Errorf("sender %s is not a keyper", msg.Sender.Hex()) - } - - if _, ok := dkg.PolyCommitmentsSeen[msg.Sender]; ok { - return fmt.Errorf("%w from keyper %s", ErrorPolyCommitAlreadyPresent, msg.Sender.Hex()) - } - dkg.PolyCommitmentsSeen[msg.Sender] = struct{}{} - - return nil -} - -// RegisterAccusationMsg adds an accusation message to the instance. -func (dkg *DKGInstance) RegisterAccusationMsg(msg Accusation) error { - if msg.Eon != dkg.Eon { - return errors.Errorf("msg is from eon %d, not %d", msg.Eon, dkg.Eon) - } - if !dkg.Config.IsKeyper(msg.Sender) { - return errors.Errorf("sender %s is not a keyper", msg.Sender.Hex()) - } - for _, accused := range msg.Accused { - if !dkg.Config.IsKeyper(accused) { - return errors.Errorf("accused %s is not a keyper", accused.Hex()) - } - if msg.Sender == accused { - return errors.Errorf("sender %s is accusing themselves", msg.Sender.Hex()) - } - } - - if _, ok := dkg.AccusationsSeen[msg.Sender]; ok { - return fmt.Errorf("%w from keyper %s", ErrorAccusationAlreadyPresent, msg.Sender.Hex()) - } - dkg.AccusationsSeen[msg.Sender] = struct{}{} - - return nil -} - -// RegisterApologyMsg adds an apology message to the instance. -func (dkg *DKGInstance) RegisterApologyMsg(msg Apology) error { - if msg.Eon != dkg.Eon { - return errors.Errorf("msg is from eon %d, not %d", msg.Eon, dkg.Eon) - } - if !dkg.Config.IsKeyper(msg.Sender) { - return errors.Errorf("sender %s is not a keyper", msg.Sender.Hex()) - } - for _, accuser := range msg.Accusers { - if !dkg.Config.IsKeyper(accuser) { - return errors.Errorf("accuser %s is not a keyper", msg.Sender.Hex()) - } - if msg.Sender == accuser { - return errors.Errorf("sender %s sends apology for accusation against themselves", msg.Sender.Hex()) - } - } - - if _, ok := dkg.ApologiesSeen[msg.Sender]; ok { - return fmt.Errorf("%w from keyper %s already present", ErrorApologyAlreadyPresent, msg.Sender.Hex()) - } - dkg.ApologiesSeen[msg.Sender] = struct{}{} - - return nil -} diff --git a/rolling-shutter/app/dkg_test.go b/rolling-shutter/app/dkg_test.go deleted file mode 100644 index 84aa78263..000000000 --- a/rolling-shutter/app/dkg_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package app - -import ( - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "gotest.tools/v3/assert" -) - -var polyEval = []*big.Int{new(big.Int).SetBytes([]byte{})} - -func TestRegisterMsgs(t *testing.T) { //nolint:funlen - eon := uint64(10) - keypers := []common.Address{} - for i := 0; i < 3; i++ { - keypers = append(keypers, common.BigToAddress(big.NewInt(int64(i+10)))) - } - nonKeyper := common.BigToAddress(big.NewInt(666)) - config := BatchConfig{ - Keypers: keypers, - } - - t.Run("RegisterPolyEvalMsg", func(t *testing.T) { - dkg := NewDKGInstance(config, eon) - - // fail if wrong eon - msg := PolyEval{ - Sender: keypers[0], - Eon: eon + 1, - Receivers: []common.Address{keypers[1]}, - EncryptedEvals: [][]byte{{}}, - } - err := dkg.RegisterPolyEvalMsg(msg) - assert.Assert(t, err != nil) - - // fail if sender is not a keyper - msg = PolyEval{ - Sender: nonKeyper, - Eon: eon, - Receivers: []common.Address{keypers[0]}, - EncryptedEvals: [][]byte{{}}, - } - err = dkg.RegisterPolyEvalMsg(msg) - assert.Assert(t, err != nil) - - // fail if receiver is not a keyper - msg = PolyEval{ - Sender: keypers[0], - Eon: eon, - Receivers: []common.Address{nonKeyper}, - EncryptedEvals: [][]byte{{}}, - } - err = dkg.RegisterPolyEvalMsg(msg) - assert.Assert(t, err != nil) - - // fail if sender and receiver are equal - msg = PolyEval{ - Sender: keypers[0], - Eon: eon, - Receivers: []common.Address{keypers[0]}, - EncryptedEvals: [][]byte{{}}, - } - err = dkg.RegisterPolyEvalMsg(msg) - assert.Assert(t, err != nil) - - // adding should work - msg = PolyEval{ - Sender: keypers[0], - Eon: eon, - Receivers: []common.Address{keypers[1]}, - EncryptedEvals: [][]byte{{}}, - } - err = dkg.RegisterPolyEvalMsg(msg) - assert.NilError(t, err) - - // adding twice should fail - msg = PolyEval{ - Sender: keypers[0], - Eon: eon, - Receivers: []common.Address{keypers[1]}, - EncryptedEvals: [][]byte{{}}, - } - err = dkg.RegisterPolyEvalMsg(msg) - assert.Assert(t, err != nil) - }) - - t.Run("RegisterPolyCommitmentMsg", func(t *testing.T) { - dkg := NewDKGInstance(config, eon) - - // fail if wrong eon - msg := PolyCommitment{ - Sender: keypers[0], - Eon: eon + 1, - } - err := dkg.RegisterPolyCommitmentMsg(msg) - assert.Assert(t, err != nil) - _, ok := dkg.PolyCommitmentsSeen[nonKeyper] - assert.Assert(t, !ok) - - // fail if sender is not a keyper - msg = PolyCommitment{ - Sender: nonKeyper, - Eon: eon, - } - err = dkg.RegisterPolyCommitmentMsg(msg) - assert.Assert(t, err != nil) - _, ok = dkg.PolyCommitmentsSeen[nonKeyper] - assert.Assert(t, !ok) - - // adding should work - msg = PolyCommitment{ - Sender: keypers[0], - Eon: eon, - } - err = dkg.RegisterPolyCommitmentMsg(msg) - assert.NilError(t, err) - _, ok = dkg.PolyCommitmentsSeen[keypers[0]] - assert.Assert(t, ok) - - // adding twice should fail - msg = PolyCommitment{ - Sender: keypers[0], - Eon: eon, - } - err = dkg.RegisterPolyCommitmentMsg(msg) - assert.Assert(t, err != nil) - }) - - t.Run("RegisterAccusationMsg", func(t *testing.T) { - dkg := NewDKGInstance(config, eon) - - // fail if wrong eon - msg := Accusation{ - Sender: keypers[0], - Eon: eon + 1, - Accused: []common.Address{keypers[1]}, - } - err := dkg.RegisterAccusationMsg(msg) - assert.Assert(t, err != nil) - _, ok := dkg.AccusationsSeen[nonKeyper] - assert.Assert(t, !ok) - - // fail if sender is not a keyper - msg = Accusation{ - Sender: nonKeyper, - Eon: eon, - Accused: []common.Address{keypers[0]}, - } - err = dkg.RegisterAccusationMsg(msg) - assert.Assert(t, err != nil) - _, ok = dkg.AccusationsSeen[nonKeyper] - assert.Assert(t, !ok) - - // fail if accused is not a keyper - msg = Accusation{ - Sender: keypers[0], - Eon: eon, - Accused: []common.Address{nonKeyper}, - } - err = dkg.RegisterAccusationMsg(msg) - assert.Assert(t, err != nil) - _, ok = dkg.AccusationsSeen[keypers[0]] - assert.Assert(t, !ok) - - // fail if sender and accused are equal - msg = Accusation{ - Sender: keypers[0], - Eon: eon, - Accused: []common.Address{keypers[0]}, - } - err = dkg.RegisterAccusationMsg(msg) - assert.Assert(t, err != nil) - _, ok = dkg.AccusationsSeen[keypers[0]] - assert.Assert(t, !ok) - - // adding should work - msg = Accusation{ - Sender: keypers[0], - Eon: eon, - Accused: []common.Address{keypers[1]}, - } - err = dkg.RegisterAccusationMsg(msg) - assert.NilError(t, err) - _, ok = dkg.AccusationsSeen[keypers[0]] - assert.Assert(t, ok) - - // adding twice should fail - msg = Accusation{ - Sender: keypers[0], - Eon: eon, - Accused: []common.Address{keypers[1]}, - } - err = dkg.RegisterAccusationMsg(msg) - assert.Assert(t, err != nil) - }) - - t.Run("RegisterApologyMsg", func(t *testing.T) { - dkg := NewDKGInstance(config, eon) - - // fail if wrong eon - msg := Apology{ - Sender: keypers[0], - Eon: eon + 1, - Accusers: []common.Address{keypers[1]}, - PolyEval: polyEval, - } - err := dkg.RegisterApologyMsg(msg) - assert.Assert(t, err != nil) - _, ok := dkg.AccusationsSeen[nonKeyper] - assert.Assert(t, !ok) - - // fail if sender is not a keyper - msg = Apology{ - Sender: nonKeyper, - Eon: eon, - Accusers: []common.Address{keypers[0]}, - PolyEval: polyEval, - } - err = dkg.RegisterApologyMsg(msg) - assert.Assert(t, err != nil) - _, ok = dkg.AccusationsSeen[nonKeyper] - assert.Assert(t, !ok) - - // fail if accuser is not a keyper - msg = Apology{ - Sender: keypers[0], - Eon: eon, - Accusers: []common.Address{nonKeyper}, - PolyEval: polyEval, - } - err = dkg.RegisterApologyMsg(msg) - assert.Assert(t, err != nil) - _, ok = dkg.ApologiesSeen[keypers[0]] - assert.Assert(t, !ok) - - // fail if sender and accused are equal - msg = Apology{ - Sender: keypers[0], - Eon: eon, - Accusers: []common.Address{keypers[0]}, - PolyEval: polyEval, - } - err = dkg.RegisterApologyMsg(msg) - assert.Assert(t, err != nil) - _, ok = dkg.ApologiesSeen[keypers[0]] - assert.Assert(t, !ok) - - // adding should work - msg = Apology{ - Sender: keypers[0], - Eon: eon, - Accusers: []common.Address{keypers[1]}, - PolyEval: polyEval, - } - err = dkg.RegisterApologyMsg(msg) - assert.NilError(t, err) - _, ok = dkg.ApologiesSeen[keypers[0]] - assert.Assert(t, ok) - - // adding twice should fail - msg = Apology{ - Sender: keypers[0], - Eon: eon, - Accusers: []common.Address{keypers[1]}, - PolyEval: polyEval, - } - err = dkg.RegisterApologyMsg(msg) - assert.Assert(t, err != nil) - }) -} diff --git a/rolling-shutter/app/forks.go b/rolling-shutter/app/forks.go deleted file mode 100644 index 29fb8c9ce..000000000 --- a/rolling-shutter/app/forks.go +++ /dev/null @@ -1,102 +0,0 @@ -package app - -import "github.com/rs/zerolog/log" - -var forkHeightOverrides = map[string]ForkHeightOverrides{ - "shutter-gnosis-1000": {CheckInUpdate: &ForkHeightOverride{ - Eon: uint64Ptr(9), - }}, - "shutter-chiado-102000": {CheckInUpdate: &ForkHeightOverride{ - Eon: uint64Ptr(13), - }}, - "shutter-api-gnosis-1001": {CheckInUpdate: &ForkHeightOverride{ - Eon: uint64Ptr(13), - }}, - "shutter-service-chiado-1000": {CheckInUpdate: &ForkHeightOverride{ - Eon: uint64Ptr(9), - }}, - "shutter-api-gnosis-1002": {CheckInUpdate: &ForkHeightOverride{ - Eon: uint64Ptr(0), - }}, -} - -func int64Ptr(value int64) *int64 { - return &value -} - -func uint64Ptr(value uint64) *uint64 { - return &value -} - -// NewForkHeightsAllEnabled creates a ForkHeights struct, activating all forks -// at genesis. -func NewForkHeightsAllEnabled() *ForkHeights { - zero := int64(0) - return &ForkHeights{ - CheckInUpdateNew: ForkHeight{ - Enabled: true, - Height: zero, - }, - } -} - -// NewForkHeightsAllDisabled creates a ForkHeights struct in which all forks -// are set to disabled. -func NewForkHeightsAllDisabled() *ForkHeights { - return &ForkHeights{ - CheckInUpdateNew: ForkHeight{ - Enabled: false, - Height: 0, - }, - } -} - -func (app *ShutterApp) IsCheckInUpdateForkActive() bool { - var override *ForkHeightOverride - if overrides, ok := forkHeightOverrides[app.ChainID]; ok { - override = overrides.CheckInUpdate - } - if app.ForkHeights == nil { - log.Warn().Msg("ForkHeights is nil, assuming all forks disabled") - return false - } - return app.ForkHeights.CheckInUpdateNew.IsForkActive(override, app.CurrentBlockHeight(), app.EONCounter) -} - -// IsForkActive checks whether a fork is active. -// -// A fork is active in either of the following cases: -// - an override is set and the override condition is met -// - no override is set, the fork height enabled flag is set, and the current -// block height is greater than or equal to the fork height -// -// Otherwise, the fork is not active, i.e., in any of the following cases: -// - an override is set but the override condition is not met -// - no override is set, the fork height flag is enabled, but the current -// block height is less than the fork height -// - no override is set and the fork height enabled flag is not set -// -// An override condition is met if -// - an override height is set and the current block height is greater than or -// equal to the override height -// - an override eon is set and the current eon is greater than or equal to -// the override eon -// -// If both override height and override eon are set, the height takes -// precedence. If neither is set, the fork is not active, regardless of the -// fork height. -func (fh ForkHeight) IsForkActive(override *ForkHeightOverride, currentBlockHeight int64, currentEon uint64) bool { - if override != nil { - if override.Height != nil { - return currentBlockHeight >= *override.Height - } - if override.Eon != nil { - return currentEon >= *override.Eon - } - return false - } - if !fh.Enabled { - return false - } - return currentBlockHeight >= fh.Height -} diff --git a/rolling-shutter/app/forks_test.go b/rolling-shutter/app/forks_test.go deleted file mode 100644 index 6d936bba9..000000000 --- a/rolling-shutter/app/forks_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package app - -import "testing" - -func TestIsForkActive(t *testing.T) { - testcases := []struct { - name string - forkHeight ForkHeight - override *ForkHeightOverride - currentBlockHeight int64 - currentEon uint64 - want bool - }{ - { - name: "override height met", - override: &ForkHeightOverride{Height: int64Ptr(10)}, - currentBlockHeight: 10, - want: true, - }, - { - name: "override height not met", - override: &ForkHeightOverride{Height: int64Ptr(10)}, - currentBlockHeight: 9, - want: false, - }, - { - name: "override eon met", - override: &ForkHeightOverride{Eon: uint64Ptr(5)}, - currentEon: 5, - want: true, - }, - { - name: "override eon not met", - override: &ForkHeightOverride{Eon: uint64Ptr(5)}, - currentEon: 4, - want: false, - }, - { - name: "override height takes precedence over eon", - override: &ForkHeightOverride{Height: int64Ptr(10), Eon: uint64Ptr(2)}, - currentBlockHeight: 9, - currentEon: 5, - want: false, - }, - { - name: "override without fields never activates fork", - override: &ForkHeightOverride{}, - currentBlockHeight: 100, - currentEon: 100, - want: false, - }, - { - name: "genesis height met without override", - forkHeight: ForkHeight{Enabled: true, Height: 7}, - currentBlockHeight: 7, - want: true, - }, - { - name: "genesis height not met without override", - forkHeight: ForkHeight{Enabled: true, Height: 7}, - currentBlockHeight: 6, - want: false, - }, - { - name: "enabled zero height activates at genesis", - forkHeight: ForkHeight{Enabled: true, Height: 0}, - currentBlockHeight: 0, - want: true, - }, - { - name: "disabled fork with non-zero height stays inactive", - forkHeight: ForkHeight{Enabled: false, Height: 7}, - currentBlockHeight: 100, - want: false, - }, - { - name: "zero-value fork height is disabled", - forkHeight: ForkHeight{}, - currentBlockHeight: 100, - want: false, - }, - { - name: "nil override uses fork height", - forkHeight: ForkHeight{Enabled: false, Height: 0}, - currentBlockHeight: 100, - want: false, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - got := tc.forkHeight.IsForkActive(tc.override, tc.currentBlockHeight, tc.currentEon) - if got != tc.want { - t.Fatalf("%s: IsForkActive() = %t, want %t (test case: %+v)", tc.name, got, tc.want, tc) - } - }) - } -} diff --git a/rolling-shutter/app/messages.go b/rolling-shutter/app/messages.go deleted file mode 100644 index 74289b9eb..000000000 --- a/rolling-shutter/app/messages.go +++ /dev/null @@ -1,132 +0,0 @@ -package app - -import ( - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" - blst "github.com/supranational/blst/bindings/go" - - "github.com/shutter-network/shutter/shlib/shcrypto" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -func validateAddress(address []byte) (common.Address, error) { - if len(address) != common.AddressLength { - return common.Address{}, errors.Errorf( - "address has invalid length (%d instead of %d bytes)", - len(address), - common.AddressLength, - ) - } - - return common.BytesToAddress(address), nil -} - -// ParsePolyEvalMsg converts a shmsg.PolyEvalMsg to an app.PolyEvalMsg. -func ParsePolyEvalMsg(msg *shmsg.PolyEval, sender common.Address) (*PolyEval, error) { - if len(msg.Receivers) != len(msg.EncryptedEvals) { - return nil, errors.Errorf("number of receivers %d does not match number of evals %d", len(msg.Receivers), len(msg.EncryptedEvals)) - } - - receivers := []common.Address{} - for _, receiver := range msg.Receivers { - address, err := validateAddress(receiver) - if err != nil { - return nil, err - } - receivers = append(receivers, address) - } - - if err := medley.EnsureUniqueAddresses(receivers); err != nil { - return nil, err - } - - return &PolyEval{ - Sender: sender, - Eon: msg.Eon, - Receivers: receivers, - EncryptedEvals: msg.EncryptedEvals, - }, nil -} - -// ParsePolyCommitmentMsg converts a shmsg.PolyCommitmentMsg to an app.PolyCommitmentMsg. -func ParsePolyCommitmentMsg(msg *shmsg.PolyCommitment, sender common.Address) (*PolyCommitment, error) { - gammas := shcrypto.Gammas{} - for _, g := range msg.Gammas { - p := new(blst.P2Affine) - p = p.Uncompress(g) - if p == nil { - return nil, errors.Errorf("invalid gamma value %x", g) - } - if !p.InG2() { - return nil, errors.Errorf("invalid gamma value %x", g) - } - gammas = append(gammas, p) - } - return &PolyCommitment{ - Sender: sender, - Eon: msg.Eon, - Gammas: &gammas, - }, nil -} - -// ParseAccusationMsg converts a shmsg.AccusationMsg to an app.AccusationMsg. -func ParseAccusationMsg(msg *shmsg.Accusation, sender common.Address) (*Accusation, error) { - accused := []common.Address{} - for _, acc := range msg.Accused { - address, err := validateAddress(acc) - if err != nil { - return nil, err - } - accused = append(accused, address) - } - - if err := medley.EnsureUniqueAddresses(accused); err != nil { - return nil, err - } - - return &Accusation{ - Sender: sender, - Eon: msg.Eon, - Accused: accused, - }, nil -} - -// ParseApologyMsg converts a shmsg.ApologyMsg to an app.ApologyMsg. -func ParseApologyMsg(msg *shmsg.Apology, sender common.Address) (*Apology, error) { - if len(msg.Accusers) != len(msg.PolyEvals) { - return nil, errors.Errorf("number of accusers %d and apology evals %d not equal", len(msg.Accusers), len(msg.PolyEvals)) - } - - accusers := []common.Address{} - - for _, acc := range msg.Accusers { - accuser, err := validateAddress(acc) - if err != nil { - return nil, err - } - - accusers = append(accusers, accuser) - } - - if err := medley.EnsureUniqueAddresses(accusers); err != nil { - return nil, err - } - - var polyEval []*big.Int - for _, b := range msg.PolyEvals { - e := new(big.Int) - e.SetBytes(b) - polyEval = append(polyEval, e) - } - - return &Apology{ - Sender: sender, - Eon: msg.Eon, - Accusers: accusers, - PolyEval: polyEval, - }, nil -} diff --git a/rolling-shutter/app/messages_test.go b/rolling-shutter/app/messages_test.go deleted file mode 100644 index e1a503141..000000000 --- a/rolling-shutter/app/messages_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package app - -import ( - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "gotest.tools/v3/assert" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -func TestMessageParsing(t *testing.T) { - eon := uint64(5) - sender := common.BigToAddress(new(big.Int).SetUint64(123)) - anotherAddress := common.BigToAddress(new(big.Int).SetUint64(456)) - anotherAddressBytes := anotherAddress.Bytes() - badAddressBytes := []byte("only nineteen bytes") - data := []byte("some data") - - t.Run("ParsePolyEvalMsg", func(t *testing.T) { - smsg := shmsg.PolyEval{ - Eon: eon, - Receivers: [][]byte{anotherAddressBytes}, - EncryptedEvals: [][]byte{data}, - } - msg, err := ParsePolyEvalMsg(&smsg, sender) - assert.NilError(t, err) - assert.DeepEqual(t, sender, msg.Sender) - assert.Equal(t, eon, msg.Eon) - assert.DeepEqual(t, anotherAddress, msg.Receivers[0]) - assert.DeepEqual(t, data, msg.EncryptedEvals[0]) - - // invalid receiver - smsg = shmsg.PolyEval{ - Eon: eon, - Receivers: [][]byte{badAddressBytes}, - EncryptedEvals: [][]byte{data}, - } - _, err = ParsePolyEvalMsg(&smsg, sender) - assert.Assert(t, err != nil) - }) - - t.Run("ParsePolyCommitmentMsg", func(t *testing.T) { - smsg := shmsg.PolyCommitment{ - Eon: eon, - Gammas: [][]byte{}, - } - msg, err := ParsePolyCommitmentMsg(&smsg, sender) - assert.NilError(t, err) - assert.DeepEqual(t, sender, msg.Sender) - assert.Equal(t, eon, msg.Eon) - }) - - t.Run("ParseAccusationMsg", func(t *testing.T) { - smsg := shmsg.Accusation{ - Eon: eon, - Accused: [][]byte{anotherAddressBytes}, - } - msg, err := ParseAccusationMsg(&smsg, sender) - assert.NilError(t, err) - assert.DeepEqual(t, sender, msg.Sender) - assert.Equal(t, eon, msg.Eon) - assert.DeepEqual(t, anotherAddress, msg.Accused[0]) - - // invalid accused - smsg = shmsg.Accusation{ - Eon: eon, - Accused: [][]byte{badAddressBytes}, - } - _, err = ParseAccusationMsg(&smsg, sender) - assert.Assert(t, err != nil) - }) - - t.Run("ParseApologyMsg", func(t *testing.T) { - smsg := shmsg.Apology{ - Eon: eon, - Accusers: [][]byte{anotherAddressBytes}, - PolyEvals: [][]byte{{}}, - } - msg, err := ParseApologyMsg(&smsg, sender) - assert.NilError(t, err) - assert.DeepEqual(t, sender, msg.Sender) - assert.Equal(t, eon, msg.Eon) - assert.DeepEqual(t, anotherAddress, msg.Accusers[0]) - - // invalid accuser - smsg = shmsg.Apology{ - Eon: eon, - Accusers: [][]byte{badAddressBytes}, - PolyEvals: [][]byte{{}}, - } - _, err = ParseApologyMsg(&smsg, sender) - assert.Assert(t, err != nil) - }) -} diff --git a/rolling-shutter/app/noncetracker.go b/rolling-shutter/app/noncetracker.go deleted file mode 100644 index 87a5dce11..000000000 --- a/rolling-shutter/app/noncetracker.go +++ /dev/null @@ -1,29 +0,0 @@ -package app - -import "github.com/ethereum/go-ethereum/common" - -// NewNonceTracker creates a new NonceTracker. -func NewNonceTracker() *NonceTracker { - return &NonceTracker{ - RandomNonces: make(map[common.Address]map[uint64]bool), - } -} - -// Check returns true if the given nonce is free and false if it has been added already. -func (t *NonceTracker) Check(sender common.Address, randomNonce uint64) bool { - m, ok := t.RandomNonces[sender] - if !ok { - return true - } - return !m[randomNonce] -} - -// Add adds the given nonce if it hasn't already. -func (t *NonceTracker) Add(sender common.Address, randomNonce uint64) { - m, ok := t.RandomNonces[sender] - if !ok { - m = make(map[uint64]bool) - t.RandomNonces[sender] = m - } - m[randomNonce] = true -} diff --git a/rolling-shutter/app/noncetracker_test.go b/rolling-shutter/app/noncetracker_test.go deleted file mode 100644 index 30e3e03ba..000000000 --- a/rolling-shutter/app/noncetracker_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package app - -import ( - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "gotest.tools/v3/assert" -) - -func TestNonceTracker(t *testing.T) { - tracker := NewNonceTracker() - a1 := common.BigToAddress(big.NewInt(0)) - a2 := common.BigToAddress(big.NewInt(1)) - r1 := uint64(10) - r2 := uint64(20) - - assert.Assert(t, tracker.Check(a1, r1)) - tracker.Add(a1, r1) - assert.Assert(t, !tracker.Check(a1, r1)) - assert.Assert(t, tracker.Check(a1, r2)) - assert.Assert(t, tracker.Check(a2, r1)) -} diff --git a/rolling-shutter/app/powermap.go b/rolling-shutter/app/powermap.go deleted file mode 100644 index f6f389541..000000000 --- a/rolling-shutter/app/powermap.go +++ /dev/null @@ -1,71 +0,0 @@ -package app - -import ( - "bytes" - "sort" - - "github.com/pkg/errors" - abcitypes "github.com/tendermint/tendermint/abci/types" - tmcrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" -) - -// MakePowermap creates a new Powermap with voting powers as specified in validators. -func MakePowermap(validators []abcitypes.ValidatorUpdate) (Powermap, error) { - res := make(Powermap) - for _, v := range validators { - data := v.PubKey.GetEd25519() - if data == nil { - return res, errors.Errorf("cannot handle key %s", v.PubKey) - } - pubkey, err := NewValidatorPubkey(data) - if err != nil { - return res, err - } - res[pubkey] += v.Power - } - return res, nil -} - -// SortValidators sorts a slice of ValidatorUpdates in a determistic way suitable for updating the -// validators in tendermint. -func SortValidators(validators []abcitypes.ValidatorUpdate) { - sort.Slice(validators, func(i, j int) bool { - return bytes.Compare(validators[i].PubKey.GetEd25519(), validators[j].PubKey.GetEd25519()) < 0 - }) -} - -// DiffPowermaps computes the diff to be applied by tendermint to change the old validators into -// the new validators. -func DiffPowermaps(oldpm, newpm Powermap) Powermap { - res := make(Powermap) - - // Remove old keys - for v := range oldpm { - _, ok := newpm[v] - if !ok { - res[v] = 0 - } - } - - // Update new keys - for v, p := range newpm { - if oldpm[v] != p { - res[v] = p - } - } - - return res -} - -// ValidatorUpdates computes a deterministic slice of ValidatorUpdate structs. -func (pm Powermap) ValidatorUpdates() []abcitypes.ValidatorUpdate { - var res []abcitypes.ValidatorUpdate - for k, p := range pm { - res = append(res, abcitypes.ValidatorUpdate{ - Power: p, - PubKey: tmcrypto.PublicKey{Sum: &tmcrypto.PublicKey_Ed25519{Ed25519: []byte(k.Ed25519pubkey)}}, - }) - } - SortValidators(res) - return res -} diff --git a/rolling-shutter/app/powermap_test.go b/rolling-shutter/app/powermap_test.go deleted file mode 100644 index 399c09aae..000000000 --- a/rolling-shutter/app/powermap_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package app - -import ( - "fmt" - "testing" - - abcitypes "github.com/tendermint/tendermint/abci/types" - tmcrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" - "gotest.tools/v3/assert" -) - -func makeKey(n int) []byte { - return []byte(fmt.Sprintf("%32d", n)) -} - -func newPubkey(n int) ValidatorPubkey { - res, err := NewValidatorPubkey(makeKey(n)) - if err != nil { - panic(err) - } - return res -} - -func TestMakePowermapEmpty(t *testing.T) { - pm, err := MakePowermap([]abcitypes.ValidatorUpdate{}) - assert.NilError(t, err) - assert.Equal(t, 0, len(pm)) -} - -func TestMakePowermapBadType(t *testing.T) { - _, err := MakePowermap([]abcitypes.ValidatorUpdate{ - { - Power: 10, - PubKey: tmcrypto.PublicKey{Sum: &tmcrypto.PublicKey_Ed25519{Ed25519: []byte("xxx")}}, - }, - }) - assert.Assert(t, err != nil) -} - -func TestMakePowermap(t *testing.T) { - var power int64 = 42 - pm, err := MakePowermap([]abcitypes.ValidatorUpdate{ - {Power: power, PubKey: tmcrypto.PublicKey{Sum: &tmcrypto.PublicKey_Ed25519{Ed25519: makeKey(1)}}}, - }) - assert.NilError(t, err) - assert.Equal(t, 1, len(pm)) - assert.Equal(t, power, pm[newPubkey(1)]) -} - -func TestDiffPowermaps(t *testing.T) { - oldpm := make(Powermap) - for i := 0; i < 4; i++ { - oldpm[newPubkey(i)] = int64(i) - } - - newpm := make(Powermap) - // drop 0,1 - newpm[newPubkey(2)] = oldpm[newPubkey(2)] // keep 2 - newpm[newPubkey(3)] = 15 // change 3 - newpm[newPubkey(4)] = 20 // add 4 - - assert.Equal(t, 0, len(DiffPowermaps(oldpm, oldpm))) - - diff := DiffPowermaps(oldpm, newpm) - - expected := Powermap{ - newPubkey(0): 0, - newPubkey(1): 0, - newPubkey(3): 15, - newPubkey(4): 20, - } - fmt.Println("DIFF", diff) - - assert.DeepEqual(t, expected, diff) -} diff --git a/rolling-shutter/app/types.go b/rolling-shutter/app/types.go deleted file mode 100644 index fa75e46ce..000000000 --- a/rolling-shutter/app/types.go +++ /dev/null @@ -1,175 +0,0 @@ -package app - -import ( - "crypto/ed25519" - "encoding/hex" - "fmt" - "reflect" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/shutterevents" -) - -// GenesisAppState is used to hold the initial list of keypers, who will bootstrap the system by -// providing the first real BatchConfig to be used. We use common.MixedcaseAddress to hold the list -// of keypers as that one serializes as checksum address. -type GenesisAppState struct { - Keypers []common.MixedcaseAddress `json:"keypers"` - Threshold uint64 `json:"threshold"` - InitialEon uint64 `json:"initialEon"` - ForkHeights *ForkHeights `json:"forkHeights"` -} - -func NewGenesisAppState(keypers []common.Address, threshold int, initialEon uint64, forkHeights *ForkHeights) GenesisAppState { - appState := GenesisAppState{ - Threshold: uint64(threshold), //nolint:gosec // G115 - InitialEon: initialEon, - ForkHeights: forkHeights, - } - for _, k := range keypers { - appState.Keypers = append(appState.Keypers, common.NewMixedcaseAddress(k)) - } - return appState -} - -// GetKeypers returns the keypers defined in the GenesisAppState. -func (appState *GenesisAppState) GetKeypers() []common.Address { - var res []common.Address - for _, k := range appState.Keypers { - res = append(res, k.Address()) - } - return res -} - -// ReflectDeepEquals is used to compare any two objects by the Voting generics type using -// reflect.DeepEqual. -type ReflectDeepEquals[T any] struct{} - -func (ReflectDeepEquals[T]) Equals(a, b T) bool { - return reflect.DeepEqual(a, b) -} - -// BatchConfigEquals is used to compare two BatchConfig structs by the Voting generics type. -type BatchConfigEquals = ReflectDeepEquals[BatchConfig] - -// ComparableEquals is used to compare two Comparables by the Voting generics type. -type ComparableEquals[T comparable] struct{} - -func (ComparableEquals[T]) Equals(a, b T) bool { - return a == b -} - -type DKGSuccessVoting = Voting[bool, ComparableEquals[bool]] - -// ConfigVoting is used to let the keypers vote on new BatchConfigs to be added. -type ConfigVoting = Voting[BatchConfig, BatchConfigEquals] - -// NewConfigVoting creates a ConfigVoting struct. -func NewConfigVoting() ConfigVoting { - return NewVoting[BatchConfig, BatchConfigEquals]() -} - -// ValidatorPubkey holds the raw 32 byte ed25519 public key to be used as tendermint validator key -// We use this is a map key, so don't use a byte slice. -type ValidatorPubkey struct { - Ed25519pubkey string -} - -func (vp ValidatorPubkey) String() string { - return fmt.Sprintf("ed25519:%s", hex.EncodeToString([]byte(vp.Ed25519pubkey))) -} - -// Powermap maps a ValidatorPubkey to the validators voting power. -type Powermap map[ValidatorPubkey]int64 - -// NewValidatorPubkey creates a new ValidatorPubkey from a 32 byte ed25519 raw pubkey. See -// https://docs.tendermint.com/master/spec/abci/apps.html#validator-updates for more information -func NewValidatorPubkey(pubkey []byte) (ValidatorPubkey, error) { - if len(pubkey) != ed25519.PublicKeySize { - return ValidatorPubkey{}, errors.Errorf("pubkey must be 32 bytes") - } - return ValidatorPubkey{Ed25519pubkey: string(pubkey)}, nil -} - -// ShutterApp holds our data structures used for the tendermint app. -type ShutterApp struct { - Configs []*BatchConfig - DKGMap map[uint64]*DKGInstance // map eon to DKGInstance - ConfigVoting ConfigVoting - // EonStartVotings map[uint64]*EonStartVoting - Gobpath string - LastSaved time.Time - LastBlockHeight int64 - Identities map[common.Address]ValidatorPubkey - BlocksSeen map[common.Address]uint64 - Validators Powermap - EONCounter uint64 - DevMode bool - CheckTxState *CheckTxState - NonceTracker *NonceTracker - ChainID string - ForkHeights *ForkHeights -} - -// CheckTxState is a part of the state used by CheckTx calls that is reset at every commit. -type CheckTxState struct { - Members map[common.Address]bool - TxCounts map[common.Address]int - NonceTracker *NonceTracker -} - -// NonceTracker tracks which nonces have been used and which have not. -type NonceTracker struct { - RandomNonces map[common.Address]map[uint64]bool -} - -type SenderReceiverPair struct { - Sender, Receiver common.Address -} - -// DKGInstance manages the state of one eon key generation instance. -type DKGInstance struct { - Config BatchConfig - Eon uint64 - SuccessVoting DKGSuccessVoting - PolyEvalsSeen map[SenderReceiverPair]struct{} - PolyCommitmentsSeen map[common.Address]struct{} - AccusationsSeen map[common.Address]struct{} - ApologiesSeen map[common.Address]struct{} -} - -// ForkHeights stores the configuration that controls when protocol forks -// become active. A fork activates if the `Enabled` flag is true and the -// current block height reaches `Height`, unless overridden in code for -// specific chain IDs. -type ForkHeights struct { - // legacy and unused, only present for backwards compatible deserialization - CheckInUpdate *int64 `json:"checkInUpdate"` - - CheckInUpdateNew ForkHeight `json:"checkInUpdateNew"` -} - -type ForkHeight struct { - Enabled bool - Height int64 -} - -type ForkHeightOverrides struct { - CheckInUpdate *ForkHeightOverride -} - -type ForkHeightOverride struct { - Height *int64 - Eon *uint64 -} - -type ( - Accusation = shutterevents.Accusation - Apology = shutterevents.Apology - BatchConfig = shutterevents.BatchConfig - PolyCommitment = shutterevents.PolyCommitment - PolyEval = shutterevents.PolyEval -) diff --git a/rolling-shutter/app/voting.go b/rolling-shutter/app/voting.go deleted file mode 100644 index 8758a62ff..000000000 --- a/rolling-shutter/app/voting.go +++ /dev/null @@ -1,83 +0,0 @@ -package app - -import ( - "errors" - - "github.com/ethereum/go-ethereum/common" -) - -var errAlreadyVoted = errors.New("sender already voted") - -// Equals is used to parametrize equality comparison in the Voting type. We need this because we -// cannot use the default comparison operator, because BatchConfig objects are not comparable. Also -// we cannot define a custom comparison method on the type, because we want to be able to also vote -// on basic types like uint64. This is being used in SetVote below, by using the Equals method on -// the null value of the given Equals type. -type Equals[T any] interface { - Equals(a, b T) bool -} - -// Voting is used to store votes. Each ethereum address can vote on one 'candidate'. -type Voting[T any, E Equals[T]] struct { - Votes map[common.Address]int - Candidates []T -} - -// NewVoting creates an empty Voting struct, where ethereum addresses can vote on candidates of -// type T. -func NewVoting[T any, E Equals[T]]() Voting[T, E] { - return Voting[T, E]{ - Votes: make(map[common.Address]int), - Candidates: nil, - } -} - -// SetVote registers the given vote of the given address. Only the last vote of a given ethereum -// address is stored. -func (v *Voting[T, E]) SetVote(sender common.Address, candidate T) { - var eq E - for i, c := range v.Candidates { - if eq.Equals(candidate, c) { - v.Votes[sender] = i - return - } - } - v.Candidates = append(v.Candidates, candidate) - v.Votes[sender] = len(v.Candidates) - 1 -} - -// AddVote registers the given vote of the given address. This function returns an error if the -// address already voted. -func (v *Voting[T, _]) AddVote(sender common.Address, candidate T) error { - _, ok := v.Votes[sender] - if ok { - return errAlreadyVoted - } - v.SetVote(sender, candidate) - return nil -} - -// outcomeIndex checks if one of the candidate indices has more than numRequiredVotes. -func (v *Voting[_, _]) outcomeIndex(numRequiredVotes int) (int, bool) { - numVotes := make(map[int]int) - - for _, vote := range v.Votes { - numVotes[vote]++ - } - for index, votes := range numVotes { - if votes >= numRequiredVotes { - return index, true - } - } - return -1, false -} - -// Outcome checks if one of the votes has received at least numRequiredVotes votes and returns it. -func (v *Voting[T, _]) Outcome(numRequiredVotes int) (T, bool) { - idx, ok := v.outcomeIndex(numRequiredVotes) - if !ok { - var n T - return n, false - } - return v.Candidates[idx], true -} diff --git a/rolling-shutter/cmd/bootstrap/bootstrap.go b/rolling-shutter/cmd/bootstrap/bootstrap.go deleted file mode 100644 index 8b4d6dcb3..000000000 --- a/rolling-shutter/cmd/bootstrap/bootstrap.go +++ /dev/null @@ -1,161 +0,0 @@ -package bootstrap - -import ( - "context" - "math/big" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/tendermint/tendermint/rpc/client/http" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/contract" - "github.com/shutter-network/rolling-shutter/rolling-shutter/contract/deployment" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/fx" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -type Config struct { - ShuttermintURL string `mapstructure:"shuttermint-url"` - EthereumURL string `mapstructure:"ethereum-url"` - DeploymentDir string `mapstructure:"deployment-dir"` - KeyperConfigIndex int `mapstructure:"index"` - SigningKey string `mapstructure:"signing-key"` - Keypers []string `mapstructure:"ethereum-url"` -} - -func Cmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "bootstrap", - Short: "Bootstrap Shuttermint by submitting the initial batch config", - Long: `This command sends a batch config to the Shuttermint chain in a message signed -with the given private key. This will instruct a newly created chain to update -its validator set according to the keyper set defined in the batch config. The -private key must correspond to the initial validator address as defined in the -chain's genesis config.`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - config := &Config{} - if err := viper.Unmarshal(config); err != nil { - return err - } - return bootstrap(config) - }, - } - - cmd.PersistentFlags().StringP( - "ethereum-url", - "", - "http://localhost:8545", - "Ethereum URL", - ) - - cmd.PersistentFlags().StringP( - "deployment-dir", - "", - "./deployments/localhost", - "Deployment directory", - ) - - cmd.PersistentFlags().StringP( - "shuttermint-url", - "s", - "http://localhost:26657", - "Shuttermint RPC URL", - ) - cmd.PersistentFlags().IntP( - "index", - "i", - 1, - "keyper config index to bootstrap with (use latest if negative)", - ) - - cmd.PersistentFlags().StringP( - "signing-key", - "k", - "", - "private key of the keyper to send the message with", - ) - - return cmd -} - -// TODO: deprecate this command and split this up in 2 stages: -// 1. gather initial keyperset information e.g. from contracts -// (could be different from optimism, rollupshutter,snapshot) -// 2. send the batch-config / new block seen message based on that info. -func bootstrap(config *Config) error { - ctx := context.Background() - ethereumClient, err := ethclient.DialContext(ctx, config.EthereumURL) - if err != nil { - return err - } - contracts, err := deployment.NewContracts(ethereumClient, config.DeploymentDir) - if err != nil { - return err - } - - shmcl, err := http.New(config.ShuttermintURL, "/websocket") - if err != nil { - log.Fatal().Err(err).Msg("failed to connect to Shuttermint node") - } - - signingKey, err := crypto.HexToECDSA(config.SigningKey) - if err != nil { - log.Fatal().Err(err).Msg("failed to parse signing key") - } - - keyperConfigIndex := uint64(config.KeyperConfigIndex) - cfg, err := contracts.KeypersConfigsList.KeypersConfigs(nil, big.NewInt(int64(keyperConfigIndex))) - if err != nil { - return err - } - addr, err := contracts.KeypersConfigsList.AddrsSeq(nil) - if err != nil { - return err - } - seq, err := contract.NewAddrsSeq(addr, ethereumClient) - if err != nil { - return err - } - keypers, err := seq.GetAddrs(nil, cfg.SetIndex) - if err != nil { - return err - } - - log.Info().Interface("config", cfg).Interface("keypers", keypers). - Msg("using configuration") - - threshold := cfg.Threshold - - ms := fx.NewRPCMessageSender(shmcl, signingKey) - activationBlockNumber := cfg.ActivationBlockNumber - batchConfigMsg := shmsg.NewBatchConfig( - activationBlockNumber, - keypers, - threshold, - keyperConfigIndex, - ) - - err = ms.SendMessage(ctx, batchConfigMsg) - if err != nil { - return errors.Errorf("Failed to send batch config message: %v", err) - } - - blockSeenMsg := shmsg.NewBlockSeen(activationBlockNumber) - err = ms.SendMessage(ctx, blockSeenMsg) - if err != nil { - return errors.Errorf("Failed to send start message: %v", err) - } - - log.Info(). - Uint64("keyper-config-index", keyperConfigIndex). - Uint64("activation-block-number", activationBlockNumber). - Uint64("threshold", threshold). - Int("num-keypers", len(keypers)). - Msg("submitted bootstrapping transaction") - return nil -} diff --git a/rolling-shutter/cmd/chain/chain.go b/rolling-shutter/cmd/chain/chain.go deleted file mode 100644 index 6fe1595be..000000000 --- a/rolling-shutter/cmd/chain/chain.go +++ /dev/null @@ -1,127 +0,0 @@ -package chain - -import ( - "context" - "os" - "path/filepath" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" - cfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/node" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/privval" - "github.com/tendermint/tendermint/proxy" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/app" - "github.com/shutter-network/rolling-shutter/rolling-shutter/cmd/shversion" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" -) - -var cfgFile string - -func Cmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "chain", - Short: "Run a node for Shutter's Tendermint chain", - Long: `This command runs a node that will connect to Shutter's Tendermint chain.`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - chainMain() - }, - } - cmd.Flags().StringVar(&cfgFile, "config", "", "config file (required)") - cmd.MarkFlagRequired("config") - cmd.AddCommand(initCmd()) - return cmd -} - -func chainMain() { - log.Info().Str("version", shversion.Version()).Msg("starting shuttermint") - config, err := readConfig(cfgFile) - if err != nil { - log.Error().Err(err).Msg("could not read config file") - os.Exit(2) - } - - err = service.RunWithSighandler(context.Background(), &appService{config: config}) - if err != nil { - log.Error().Err(err).Msg("service failed") - os.Exit(1) - } -} - -type appService struct { - config *cfg.Config -} - -func (as *appService) Start(ctx context.Context, runner service.Runner) error { - logger, err := newLogger(as.config.LogLevel) - if err != nil { - return errors.Wrap(err, "failed to create tendermint logger") - } - - shapp, err := app.LoadShutterAppFromFile( - filepath.Join(as.config.DBDir(), "shutter.gob"), - ) - if err != nil { - return err - } - nodeKey, err := p2p.LoadNodeKey(as.config.NodeKeyFile()) - if err != nil { - return err - } - log.Info().Str("node-id", string(nodeKey.ID())).Msg("loaded node-id") - - tmNode, err := node.NewNode( - as.config, - privval.LoadFilePV(as.config.PrivValidatorKeyFile(), as.config.PrivValidatorStateFile()), - nodeKey, - proxy.NewLocalClientCreator(&shapp), - node.DefaultGenesisDocProviderFunc(as.config), - node.DefaultDBProvider, - node.DefaultMetricsProvider(as.config.Instrumentation), - logger, - ) - if err != nil { - return errors.Wrap(err, "failed to create new Tendermint node") - } - err = tmNode.Start() - if err != nil { - return errors.Wrap(err, "failed to start Tendermint node") - } - runner.Go(func() error { - tmNode.Wait() - log.Debug().Msg("Node stopped") - return nil - }) - runner.Go(func() error { - <-ctx.Done() - log.Debug().Msg("Stopping node") - err := tmNode.Stop() - if err != nil { - log.Error().Err(err).Msg("failed to stop Tendermint node") - } - return nil - }) - return nil -} - -func readConfig(configFile string) (*cfg.Config, error) { - config := cfg.DefaultConfig() - config.RootDir = filepath.Dir(filepath.Dir(configFile)) - config.SetRoot(config.RootDir) - viper.SetConfigFile(configFile) - if err := viper.ReadInConfig(); err != nil { - return nil, errors.Wrap(err, "viper failed to read config file") - } - if err := viper.Unmarshal(config); err != nil { - return nil, errors.Wrap(err, "viper failed to unmarshal config") - } - if err := config.ValidateBasic(); err != nil { - return nil, errors.Wrap(err, "config is invalid") - } - return config, nil -} diff --git a/rolling-shutter/cmd/chain/init.go b/rolling-shutter/cmd/chain/init.go deleted file mode 100644 index 173105b0a..000000000 --- a/rolling-shutter/cmd/chain/init.go +++ /dev/null @@ -1,302 +0,0 @@ -package chain - -// This has been copied from tendermint's own init command - -import ( - "encoding/hex" - "fmt" - "os" - "path/filepath" - "slices" - "strconv" - "strings" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/tendermint/go-amino" - cfg "github.com/tendermint/tendermint/config" - tmos "github.com/tendermint/tendermint/libs/os" - tmrand "github.com/tendermint/tendermint/libs/rand" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/privval" - "github.com/tendermint/tendermint/types" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/app" -) - -const ( - VALIDATOR = "validator" - ISOLATEDVALIDATOR = "isolated-validator" - SENTRY = "sentry" - SEED = "seed" -) - -type Config struct { - RootDir string `mapstructure:"root"` - DevMode bool `mapstructure:"dev"` - Index int `mapstructure:"index"` - BlockTime float64 `mapstructure:"blocktime"` - GenesisKeyper []string `mapstructure:"genesis-keyper"` - ListenAddress string `mapstructure:"listen-address"` - Role string `mapstructure:"role"` - InitialEon uint64 `mapstructure:"initial-eon"` - Forks ForkConfig `mapstructure:"forks"` -} - -type ForkConfig struct { - CheckInUpdate Fork `mapstructure:"check-in-update"` -} - -type Fork struct { - Height int64 `mapstructure:"height"` - Disabled bool `mapstructure:"disabled"` -} - -func initCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "init", - Short: "Create a config file for a Shuttermint node", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - config := &Config{} - if err := viper.Unmarshal(config); err != nil { - return err - } - if len(config.GenesisKeyper) == 0 { - return errors.New("required argument `genesis-keyper` not specified") - } - if config.RootDir == "" { - return errors.New("required argument `root` not specified") - } - return initFiles(cmd, config, args) - }, - } - cmd.PersistentFlags().String("root", "", "root directory") - cmd.PersistentFlags().Bool("dev", false, "turn on devmode (disables validator set changes)") - cmd.PersistentFlags().Int("index", 0, "keyper index") - cmd.PersistentFlags().Float64("blocktime", 1.0, "block time in seconds") - cmd.PersistentFlags().StringSlice("genesis-keyper", nil, "genesis keyper address") - cmd.PersistentFlags().String("listen-address", "tcp://127.0.0.1:26657", "tendermint RPC listen address") - cmd.PersistentFlags().String("role", "validator", "tendermint node role (validator, isolated-validator, sentry, seed)") - cmd.PersistentFlags().Uint64("initial-eon", 0, "initial eon") - cmd.PersistentFlags().Int64("forks.check-in-update.height", 0, "block height at which to activate the check-in update fork") - cmd.PersistentFlags().Bool("forks.check-in-update.disabled", false, "whether the check-in update fork is disabled") - return cmd -} - -func scaleToBlockTime(config *cfg.Config, blockTime float64) { - f := blockTime * float64(time.Second) / float64(config.Consensus.TimeoutCommit) - scale := func(d *time.Duration) { - *d = time.Duration(float64(*d) * f) - } - scale(&config.Consensus.TimeoutPropose) - scale(&config.Consensus.TimeoutProposeDelta) - scale(&config.Consensus.TimeoutPrevote) - scale(&config.Consensus.TimeoutPrecommit) - scale(&config.Consensus.TimeoutPrecommitDelta) - scale(&config.Consensus.TimeoutCommit) - scale(&config.RPC.TimeoutBroadcastTxCommit) -} - -func getArgFromViper[T interface{}](getter func(string) T, name string, required bool) (T, error) { - if !viper.IsSet(name) && required { - var nullVal T - return nullVal, errors.Errorf("required argument `%s` not set", name) - } - return getter(name), nil -} - -func initFiles(_ *cobra.Command, config *Config, _ []string) error { - keypers := []common.Address{} - - if slices.Contains([]string{VALIDATOR, ISOLATEDVALIDATOR}, config.Role) { - for _, a := range config.GenesisKeyper { - if !common.IsHexAddress(a) { - return errors.Errorf("--genesis-keyper argument '%s' is not an address", a) - } - keypers = append(keypers, common.HexToAddress(a)) - } - } - - tendermintCfg := cfg.DefaultConfig() - tendermintCfg.BaseConfig.LogLevel = "error" - tendermintCfg.RPC.ListenAddress = config.ListenAddress - - scaleToBlockTime(tendermintCfg, config.BlockTime) - keyper0RPCAddress := tendermintCfg.RPC.ListenAddress - rpcAddress, err := adjustPort(keyper0RPCAddress, config.Index) - if err != nil { - return err - } - tendermintCfg.RPC.ListenAddress = rpcAddress - - keyper0P2PAddress := tendermintCfg.P2P.ListenAddress - p2pAddress, err := adjustPort(keyper0P2PAddress, config.Index) - if err != nil { - return err - } - tendermintCfg.P2P.ListenAddress = p2pAddress - - tendermintCfg.P2P.AllowDuplicateIP = true - - tendermintCfg.SetRoot(config.RootDir) - if err := tendermintCfg.ValidateBasic(); err != nil { - return errors.Wrap(err, "error in config file") - } - cfg.EnsureRoot(tendermintCfg.RootDir) - // set up according to the network role: https://docs.tendermint.com/v0.34/tendermint-core/validators.html - switch config.Role { - case VALIDATOR: // standard validator mode, network exposed - tendermintCfg.P2P.PexReactor = true - tendermintCfg.P2P.AddrBookStrict = true - case ISOLATEDVALIDATOR: // validator mode behind a sentry node - tendermintCfg.P2P.PexReactor = false - tendermintCfg.P2P.AddrBookStrict = false - case SENTRY: - tendermintCfg.P2P.PexReactor = true - tendermintCfg.P2P.AddrBookStrict = false - case SEED: - tendermintCfg.P2P.PexReactor = true - tendermintCfg.P2P.AddrBookStrict = false - default: - return errors.Errorf("illegal value for --role: %s", config.Role) - } - // EnsureRoot also write the config file but with the default config. We want our own, so - // let's overwrite it. - cfg.WriteConfigFile(config.RootDir+"/config/config.toml", tendermintCfg) - // Initialize fork heights according to config. - forkHeights := app.NewForkHeightsAllDisabled() - if !config.Forks.CheckInUpdate.Disabled { - forkHeights.CheckInUpdateNew = app.ForkHeight{ - Enabled: true, - Height: config.Forks.CheckInUpdate.Height, - } - } - appState := app.NewGenesisAppState(keypers, (2*len(keypers)+2)/3, config.InitialEon, forkHeights) - - return initFilesWithConfig(tendermintCfg, config, appState) -} - -func adjustPort(address string, keyperIndex int) (string, error) { - substrings := strings.Split(address, ":") - if len(substrings) < 2 { - return "", errors.Errorf("address %s does not contain port", address) - } - portStr := substrings[len(substrings)-1] - portInt, err := strconv.Atoi(portStr) - if err != nil { - return "", errors.Errorf("port %s is not an integer", portStr) - } - portIntAdjusted := portInt + keyperIndex*2 - portStrAdjusted := strconv.Itoa(portIntAdjusted) - return strings.Join(substrings[:len(substrings)-1], ":") + ":" + portStrAdjusted, nil -} - -func initFilesWithConfig(tendermintConfig *cfg.Config, config *Config, appState app.GenesisAppState) error { - var err error - if slices.Contains([]string{VALIDATOR, ISOLATEDVALIDATOR, SEED}, config.Role) { - // private validator - privValKeyFile := tendermintConfig.PrivValidatorKeyFile() - privValStateFile := tendermintConfig.PrivValidatorStateFile() - var pv *privval.FilePV - if tmos.FileExists(privValKeyFile) { - if tmos.FileExists(privValStateFile) { - pv = privval.LoadFilePV(privValKeyFile, privValStateFile) - log.Info(). - Str("privValKeyFile", privValKeyFile). - Str("stateFile", privValStateFile). - Msg("Found private validator") - } else { - pv = privval.LoadFilePVEmptyState(privValKeyFile, privValStateFile) - pv.Save() - log.Info(). - Str("privValKeyFile", privValKeyFile). - Str("stateFile", privValStateFile). - Msg("Found private validator but no state file") - } - } else { - pv = privval.GenFilePV(privValKeyFile, privValStateFile) - pv.Save() - log.Info(). - Str("privValKeyFile", privValKeyFile). - Str("stateFile", privValStateFile). - Msg("Generated private validator") - } - - validatorPubKeyPath := filepath.Join(tendermintConfig.RootDir, "config", "priv_validator_pubkey.hex") - validatorPublicKeyHex := hex.EncodeToString(pv.Key.PubKey.Bytes()) - err = os.WriteFile(validatorPubKeyPath, []byte(validatorPublicKeyHex), 0o644) - if err != nil { - return errors.Wrapf(err, "Could not write to %s", validatorPubKeyPath) - } - log.Info().Str("path", validatorPubKeyPath).Str("validatorPublicKey", validatorPublicKeyHex).Msg("Saved private validator publickey") - - // genesis file - genFile := tendermintConfig.GenesisFile() - if tmos.FileExists(genFile) { - log.Info().Str("path", genFile).Msg("Found genesis file") - } else { - appStateBytes, err := amino.NewCodec().MarshalJSONIndent(appState, "", " ") - if err != nil { - return err - } - genDoc := types.GenesisDoc{ - ChainID: fmt.Sprintf("shutter-test-chain-%v", tmrand.Str(6)), - GenesisTime: time.Now(), - ConsensusParams: types.DefaultConsensusParams(), - AppState: appStateBytes, - } - pubKey, err := pv.GetPubKey() - if err != nil { - return errors.Wrap(err, "can't get pubkey") - } - genDoc.Validators = []types.GenesisValidator{{ - Address: pubKey.Address(), - PubKey: pubKey, - Power: 10, - }} - - if err := genDoc.SaveAs(genFile); err != nil { - return err - } - log.Info().Str("path", genFile).Msg("Generated genesis file") - } - } - - nodeKeyFile := tendermintConfig.NodeKeyFile() - if tmos.FileExists(nodeKeyFile) { - log.Info().Str("path", nodeKeyFile).Msg("Found node key") - } else { - nodeKey, err := p2p.LoadOrGenNodeKey(nodeKeyFile) - if err != nil { - return err - } - nodeid := nodeKey.ID() - idpath := nodeKeyFile + ".id" - err = os.WriteFile(idpath, []byte(nodeid), 0o644) - if err != nil { - return errors.Wrapf(err, "Could not write to %s", idpath) - } - log.Info().Str("path", nodeKeyFile).Str("id", string(nodeid)).Msg("Generated node key") - } - - gobPath := filepath.Join(tendermintConfig.DBDir(), "shutter.gob") - - if tmos.FileExists(gobPath) { - log.Warn().Str("path", gobPath).Msg("Shutter app state file already exists") - } else { - a := app.NewShutterApp() - a.Gobpath = gobPath - a.DevMode = config.DevMode - err = a.PersistToDisk() - if err != nil { - return err - } - } - - return nil -} diff --git a/rolling-shutter/cmd/chain/log.go b/rolling-shutter/cmd/chain/log.go deleted file mode 100644 index 91713374b..000000000 --- a/rolling-shutter/cmd/chain/log.go +++ /dev/null @@ -1,45 +0,0 @@ -package chain - -import ( - "fmt" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - tenderlog "github.com/tendermint/tendermint/libs/log" -) - -type tendermintLogger struct { - zerolog.Logger -} - -func (l tendermintLogger) Info(msg string, keyVals ...interface{}) { - l.Logger.Info().Fields(keyVals).Msg(msg) -} - -func (l tendermintLogger) Error(msg string, keyVals ...interface{}) { - e := l.Logger.Error() - e.Fields(keyVals).Msg(msg) -} - -func (l tendermintLogger) Debug(msg string, keyVals ...interface{}) { - l.Logger.Debug().Fields(keyVals).Msg(msg) -} - -func (l tendermintLogger) With(keyVals ...interface{}) tenderlog.Logger { - return tendermintLogger{ - Logger: l.Logger.With().Fields(keyVals).Logger(), - } -} - -// newLogger sets up a new tendermint logger adapted from the global logger. This will be -// compatible with the logger we configure in cmd/root.go. We use this as a replacement for -// tenderlog.NewDefaultLogger. -func newLogger(level string) (tenderlog.Logger, error) { - logLevel, err := zerolog.ParseLevel(level) - if err != nil { - return nil, fmt.Errorf("failed to parse log level (%s): %w", level, err) - } - logger := log.Logger.Level(logLevel) - logger = logger.With().CallerWithSkipFrameCount(zerolog.CallerSkipFrameCount + 1).Logger() - return tendermintLogger{Logger: logger}, nil -} diff --git a/rolling-shutter/cmd/command.go b/rolling-shutter/cmd/command.go index 1b66d53ad..46cd98bdf 100644 --- a/rolling-shutter/cmd/command.go +++ b/rolling-shutter/cmd/command.go @@ -3,8 +3,6 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/shutter-network/rolling-shutter/rolling-shutter/cmd/bootstrap" - "github.com/shutter-network/rolling-shutter/rolling-shutter/cmd/chain" "github.com/shutter-network/rolling-shutter/rolling-shutter/cmd/cryptocmd" "github.com/shutter-network/rolling-shutter/rolling-shutter/cmd/gnosisaccessnode" "github.com/shutter-network/rolling-shutter/rolling-shutter/cmd/gnosiskeyper" @@ -19,10 +17,7 @@ import ( func Subcommands() []*cobra.Command { return []*cobra.Command{ - bootstrap.Cmd(), - chain.Cmd(), optimism.Cmd(), - optimism.OPBootstrapCmd(), snapshot.Cmd(), snapshotkeyper.Cmd(), gnosiskeyper.Cmd(), diff --git a/rolling-shutter/cmd/gnosiskeyper/gnosiskeyper.go b/rolling-shutter/cmd/gnosiskeyper/gnosiskeyper.go index 2c6fa02e1..9777d1e7f 100644 --- a/rolling-shutter/cmd/gnosiskeyper/gnosiskeyper.go +++ b/rolling-shutter/cmd/gnosiskeyper/gnosiskeyper.go @@ -22,8 +22,7 @@ func Cmd() *cobra.Command { main, command.Usage( "Run a Shutter keyper for Gnosis Chain", - `This command runs a keyper node. It will connect to both a Gnosis and a -Shuttermint node which have to be started separately in advance.`, + "This command runs a keyper node. It will connect to a Gnosis execution node which has to be started separately in advance.", ), command.WithGenerateConfigSubcommand(), command.WithDumpConfigSubcommand(), @@ -42,7 +41,6 @@ func main(config *keyper.Config) error { log.Info(). Str("version", shversion.Version()). Str("address", config.GetAddress().Hex()). - Str("shuttermint", config.Shuttermint.ShuttermintURL). Msg("starting gnosis keyper") kpr := keyper.New(config) diff --git a/rolling-shutter/cmd/optimism/bootstrap.go b/rolling-shutter/cmd/optimism/bootstrap.go deleted file mode 100644 index 45e616309..000000000 --- a/rolling-shutter/cmd/optimism/bootstrap.go +++ /dev/null @@ -1,42 +0,0 @@ -package optimism - -import ( - "context" - - "github.com/spf13/cobra" - - boot "github.com/shutter-network/rolling-shutter/rolling-shutter/keyperimpl/optimism/bootstrap" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/configuration/command" -) - -// TODO: use this to replace the old bootstrap command. -// First writing the keyperset and then bootstrapping allows -// to support different contracts etc. -func OPBootstrapCmd() *cobra.Command { - builder := command.Build( - bootstrap, - command.Usage( - "Bootstrap validator utility functions for a shuttermint chain", - ``, - ), - command.WithGenerateConfigSubcommand(), - ) - - bootstrapCmd := &cobra.Command{ - Use: "fetch-keyperset", - Short: "fetch-keyperset", - Args: cobra.NoArgs, - RunE: builder.WrapFuncParseConfig(keyperSet), - } - builder.Command().AddCommand(bootstrapCmd) - return builder.Command() -} - -func keyperSet(cfg *boot.Config) error { - ctx := context.Background() - return boot.GetKeyperSet(ctx, cfg) -} - -func bootstrap(cfg *boot.Config) error { - return boot.BootstrapValidators(cfg) -} diff --git a/rolling-shutter/cmd/optimism/keyper.go b/rolling-shutter/cmd/optimism/keyper.go index f23d091be..04003907e 100644 --- a/rolling-shutter/cmd/optimism/keyper.go +++ b/rolling-shutter/cmd/optimism/keyper.go @@ -22,8 +22,7 @@ func Cmd() *cobra.Command { main, command.Usage( "Run a Shutter optimism keyper node", - `This command runs a keyper node. It will connect to both an Optimism and a -Shuttermint node which have to be started separately in advance.`, + "This command runs a keyper node. It will connect to an Optimism execution node which has to be started separately in advance.", ), command.WithGenerateConfigSubcommand(), ) @@ -35,7 +34,6 @@ func main(cfg *config.Config) error { log.Info(). Str("version", shversion.Version()). Str("address", cfg.GetAddress().Hex()). - Str("shuttermint", cfg.Shuttermint.ShuttermintURL). Msg("starting keyper") kpr, err := keyper.New(cfg) if err != nil { diff --git a/rolling-shutter/cmd/primevkeyper/keyper.go b/rolling-shutter/cmd/primevkeyper/keyper.go index eea90084d..a06b7ee98 100644 --- a/rolling-shutter/cmd/primevkeyper/keyper.go +++ b/rolling-shutter/cmd/primevkeyper/keyper.go @@ -21,8 +21,7 @@ func Cmd() *cobra.Command { main, command.Usage( "Run a Shutter keyper for PrimeV POC", - `This command runs a keyper node. It will connect to both a PrimeV and a -Shuttermint node which have to be started separately in advance.`, + "This command runs a keyper node. It will connect to a PrimeV execution node which has to be started separately in advance.", ), command.WithGenerateConfigSubcommand(), command.WithDumpConfigSubcommand(), @@ -35,7 +34,6 @@ func main(config *keyper.Config) error { log.Info(). Str("version", shversion.Version()). Str("address", config.GetAddress().Hex()). - Str("shuttermint", config.Shuttermint.ShuttermintURL). Msg("starting primev keyper") kpr := keyper.New(config) diff --git a/rolling-shutter/cmd/shutterservicekeyper/shutterservicekeyper.go b/rolling-shutter/cmd/shutterservicekeyper/shutterservicekeyper.go index c81210353..b544700ad 100644 --- a/rolling-shutter/cmd/shutterservicekeyper/shutterservicekeyper.go +++ b/rolling-shutter/cmd/shutterservicekeyper/shutterservicekeyper.go @@ -21,8 +21,7 @@ func Cmd() *cobra.Command { main, command.Usage( "Run a Shutter keyper for Shutter Service", - `This command runs a keyper node. It will connect to both a Shutter service and a -Shuttermint node which have to be started separately in advance.`, + "This command runs a keyper node. It will connect to a Shutter service execution node which has to be started separately in advance.", ), command.WithGenerateConfigSubcommand(), command.WithDumpConfigSubcommand(), @@ -35,7 +34,6 @@ func main(config *keyper.Config) error { log.Info(). Str("version", shversion.Version()). Str("address", config.GetAddress().Hex()). - Str("shuttermint", config.Shuttermint.ShuttermintURL). Msg("starting shutter service keyper") kpr := keyper.New(config) diff --git a/rolling-shutter/cmd/shversion/shversion.go b/rolling-shutter/cmd/shversion/shversion.go index 46838c289..e162bc4bb 100644 --- a/rolling-shutter/cmd/shversion/shversion.go +++ b/rolling-shutter/cmd/shversion/shversion.go @@ -11,7 +11,7 @@ import ( // This gets set via ldflags when building via the Makefile. var version string -// Version returns shuttermint's version string. +// Version returns the rolling-shutter version string. func Version() string { var raceinfo string if raceDetectorEnabled { diff --git a/rolling-shutter/cmd/snapshotkeyper/snapshotkeyper.go b/rolling-shutter/cmd/snapshotkeyper/snapshotkeyper.go index a6ed5e42b..1eb2c9cab 100644 --- a/rolling-shutter/cmd/snapshotkeyper/snapshotkeyper.go +++ b/rolling-shutter/cmd/snapshotkeyper/snapshotkeyper.go @@ -22,8 +22,7 @@ func Cmd() *cobra.Command { command.CommandName("snapshotkeyper"), command.Usage( "Run a Shutter snapshotkeyper node", - `This command runs a keyper node. It will connect to both an Ethereum and a -Shuttermint node which have to be started separately in advance.`, + "This command runs a keyper node. It will connect to an Ethereum execution node which has to be started separately in advance.", ), command.WithGenerateConfigSubcommand(), command.WithDumpConfigSubcommand(), @@ -36,7 +35,6 @@ func main(config *keyper.Config) error { log.Info(). Str("version", shversion.Version()). Str("address", config.GetAddress().Hex()). - Str("shuttermint", config.Shuttermint.ShuttermintURL). Msg("starting snapshotkeyper") kpr := keyper.New(config) diff --git a/rolling-shutter/dkg/accusing.go b/rolling-shutter/dkg/accusing.go new file mode 100644 index 000000000..b071ab067 --- /dev/null +++ b/rolling-shutter/dkg/accusing.go @@ -0,0 +1,120 @@ +package dkg + +import ( + "context" + "database/sql" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/jackc/pgx/v4" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + + "github.com/shutter-network/shutter/shlib/puredkg" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/txsender" +) + +// maybeAccuse is the per-block reactor for the Accusing phase. It enqueues a +// `submitAccusation` row when our locally-rebuilt puredkg (loaded from +// `dkg_initial_states` and replayed with received dealings) yields at least +// one accusation. The function is safely re-invokable: presence of a +// `dkg_sent_actions` row for `(k, r, "accusing")` is the idempotency marker. +// +// Our own accusation rows are NOT written to the shared `dkg_accusations` +// table — the chain syncer is the sole writer to it, so every keyper has +// the same view at each block height. Replay in `buildPureDKG` picks up +// our own accusations only after the on-chain submitAccusation event is +// indexed. +// +// The caller passes a `pure` returned by `buildPureDKG(PhaseAccusing)` — +// Phase=Dealing with commitments + evals applied; here we call +// StartPhase2Accusing, which advances the phase and emits accusations for +// dealers whose PolyEval is missing or fails verification. +func (m *Manager) maybeAccuse( + ctx context.Context, + tx pgx.Tx, + dkgAddr common.Address, + keyperSetIndex, retryCounter int64, + pure *puredkg.PureDKG, + keypers []common.Address, + ownIndex uint64, +) error { + queries := corekeyperdb.New(tx) + alreadySent, err := queries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionAccusing, + }) + if err != nil { + return errors.Wrap(err, "check dkg sent action existence") + } + if alreadySent { + return nil + } + + accusations := pure.StartPhase2Accusing() + if len(accusations) == 0 { + log.Info(). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Msg("Not sending accusations, no misbehavior detected") + // Mark the phase as resolved with a NULL tx_outbox_id row so the + // log line above runs at most once per DKG Instance. The set of + // accusations is fixed before the Apologizing phase begins, so + // re-evaluating on every block is redundant work. + if err := queries.InsertDKGSentAction(ctx, corekeyperdb.InsertDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionAccusing, + TxOutboxID: sql.NullInt64{}, + }); err != nil { + return errors.Wrap(err, "store accusing sent action marker (no accusations)") + } + return nil + } + + accusedIndices := make([]uint64, 0, len(accusations)) + accusedDescriptions := make([]string, 0, len(accusations)) + for _, a := range accusations { + accusedIndices = append(accusedIndices, a.Accused) + accusedDescriptions = append(accusedDescriptions, fmt.Sprintf("%d (%s)", a.Accused, keypers[a.Accused].Hex())) + } + + abi, err := dkgcontract.DkgcontractMetaData.GetAbi() + if err != nil { + return errors.Wrap(err, "load DKG contract ABI") + } + data, err := abi.Pack( + "submitAccusation", + uint64(keyperSetIndex), //nolint:gosec // G115: keyper set index is bounded by the on-chain contract + uint64(retryCounter), //nolint:gosec // G115: retry counter is bounded by the on-chain contract + ownIndex, + accusedIndices, + ) + if err != nil { + return errors.Wrap(err, "pack submitAccusation calldata") + } + label := fmt.Sprintf("submitAccusation ksi=%d retry=%d", keyperSetIndex, retryCounter) + outboxID, err := txsender.EnqueueTx(ctx, tx, dkgAddr, data, nil, label) + if err != nil { + return errors.Wrap(err, "enqueue submitAccusation tx") + } + if err := queries.InsertDKGSentAction(ctx, corekeyperdb.InsertDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionAccusing, + TxOutboxID: sql.NullInt64{Int64: outboxID, Valid: true}, + }); err != nil { + return errors.Wrap(err, "store accusing sent action marker") + } + log.Info(). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Int("count", len(accusedIndices)). + Strs("accused", accusedDescriptions). + Msg("submitting DKG accusations") + return nil +} diff --git a/rolling-shutter/dkg/accusing_test.go b/rolling-shutter/dkg/accusing_test.go new file mode 100644 index 000000000..5a79959d7 --- /dev/null +++ b/rolling-shutter/dkg/accusing_test.go @@ -0,0 +1,401 @@ +package dkg + +import ( + "context" + "crypto/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" + "gotest.tools/v3/assert" + + "github.com/shutter-network/shutter/shlib/puredkg" + + obskeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/testsetup" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" +) + +// dkgTestEnv bundles the artifacts a maybeAccuse / maybeApologize test +// needs: a three-keyper set with the local Manager at index 1, all three +// ECIES keys registered, and a ready-to-call `mgr.maybeDeal` / +// `mgr.maybeAccuse` etc. +type dkgTestEnv struct { + dbpool *pgxpool.Pool + mgr *Manager + dkgAddr common.Address + ownECIES *ecies.PrivateKey +} + +const ( + testKsi int64 = 7 + testRetry int64 = 0 +) + +func setupDKGTestEnv(ctx context.Context, t *testing.T) *dkgTestEnv { + t.Helper() + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + other0, err := crypto.GenerateKey() + assert.NilError(t, err) + other2, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + other0Addr := crypto.PubkeyToAddress(other0.PublicKey) + other2Addr := crypto.PubkeyToAddress(other2.PublicKey) + keypers := []common.Address{other0Addr, ownAddr, other2Addr} + + obsQueries := obskeyperdb.New(dbpool) + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: testKsi, + ActivationBlockNumber: 0, + Keypers: shdb.EncodeAddresses(keypers), + Threshold: 2, + }) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(dbpool) + for _, kp := range []struct { + addr common.Address + key *ecies.PrivateKey + }{ + {addr: other0Addr, key: ecies.ImportECDSA(other0)}, + {addr: ownAddr, key: ecies.ImportECDSA(ownECDSA)}, + {addr: other2Addr, key: ecies.ImportECDSA(other2)}, + } { + err = coreQueries.UpsertECIESKey(ctx, corekeyperdb.UpsertECIESKeyParams{ + KeyperAddress: shdb.EncodeAddress(kp.addr), + EciesPublicKey: shdb.EncodeEciesPublicKey(&kp.key.PublicKey), + }) + assert.NilError(t, err) + } + + dkgAddr := common.HexToAddress("0xd0000000000000000000000000000000000000aa") + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + return &dkgTestEnv{ + dbpool: dbpool, + mgr: mgr, + dkgAddr: dkgAddr, + ownECIES: ecies.ImportECDSA(ownECDSA), + } +} + +// runMaybe wraps a single dispatch the way `processDKG` does: look up the +// keyper set, open a read transaction for `buildPureDKG`, then open a +// separate write transaction for the matching maybe-function. Returns nil +// silently if the manager would not participate (non-member or no initial +// state for non-Dealing phases). +func (env *dkgTestEnv) runMaybe(ctx context.Context, phase Phase) error { + obsQueries := obskeyperdb.New(env.dbpool) + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, testKsi) + if err != nil { + return err + } + ownIndex, err := keyperSet.GetIndex(env.mgr.cfg.OwnAddress) + if err != nil { + return nil + } + keypers, err := shdb.DecodeAddresses(keyperSet.Keypers) + if err != nil { + return err + } + threshold := uint64(keyperSet.Threshold) //nolint:gosec // G115: threshold is a small positive integer + + var pure *puredkg.PureDKG + err = env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + p, err := env.mgr.buildPureDKG(ctx, tx, testKsi, testRetry, phase, keypers, ownIndex, threshold) + if err != nil { + return err + } + pure = p + return nil + }) + if err != nil { + return err + } + if pure == nil { + return nil + } + return env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + switch phase { + case PhaseDealing: + return env.mgr.maybeDeal(ctx, tx, env.dkgAddr, testKsi, testRetry, pure, keypers, ownIndex) + case PhaseAccusing: + return env.mgr.maybeAccuse(ctx, tx, env.dkgAddr, testKsi, testRetry, pure, keypers, ownIndex) + case PhaseApologizing: + return env.mgr.maybeApologize(ctx, tx, env.dkgAddr, testKsi, testRetry, pure, keypers, ownIndex) + case PhaseFinalizing: + return env.mgr.maybeFinalize(ctx, tx, env.dkgAddr, testKsi, testRetry, pure, ownIndex) + case PhaseNone: + return nil + } + return nil + }) +} + +// runMaybeRetry is like runMaybe but uses an explicit retryCounter instead of +// testRetry. Useful for multi-retry tests. +func (env *dkgTestEnv) runMaybeRetry(ctx context.Context, phase Phase, retry int64) error { + obsQueries := obskeyperdb.New(env.dbpool) + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, testKsi) + if err != nil { + return err + } + ownIndex, err := keyperSet.GetIndex(env.mgr.cfg.OwnAddress) + if err != nil { + return nil + } + keypers, err := shdb.DecodeAddresses(keyperSet.Keypers) + if err != nil { + return err + } + threshold := uint64(keyperSet.Threshold) //nolint:gosec // G115: threshold is a small positive integer + + var pure *puredkg.PureDKG + err = env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + p, err := env.mgr.buildPureDKG(ctx, tx, testKsi, retry, phase, keypers, ownIndex, threshold) + if err != nil { + return err + } + pure = p + return nil + }) + if err != nil { + return err + } + if pure == nil { + return nil + } + return env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + switch phase { + case PhaseDealing: + return env.mgr.maybeDeal(ctx, tx, env.dkgAddr, testKsi, retry, pure, keypers, ownIndex) + case PhaseAccusing: + return env.mgr.maybeAccuse(ctx, tx, env.dkgAddr, testKsi, retry, pure, keypers, ownIndex) + case PhaseApologizing: + return env.mgr.maybeApologize(ctx, tx, env.dkgAddr, testKsi, retry, pure, keypers, ownIndex) + case PhaseFinalizing: + return env.mgr.maybeFinalize(ctx, tx, env.dkgAddr, testKsi, retry, pure, ownIndex) + case PhaseNone: + return nil + } + return nil + }) +} + +// insertForeignDealingRetry is like insertForeignDealing but accepts an +// explicit retryCounter for multi-retry tests. +func (env *dkgTestEnv) insertForeignDealingRetry(ctx context.Context, t *testing.T, dealerIdx uint64, retry int64) { + t.Helper() + const eon = uint64(testKsi) + p := puredkg.NewPureDKG(eon, 3, 2, dealerIdx) + commit, evals, err := p.StartPhase1Dealing() + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(env.dbpool) + err = coreQueries.InsertDKGPolyCommitment(ctx, corekeyperdb.InsertDKGPolyCommitmentParams{ + KeyperSetIndex: testKsi, + RetryCounter: retry, + KeyperIndex: int64(dealerIdx), //nolint:gosec // G115: keyper index is bounded by the keyper set size + Commitment: commit.Gammas.Marshal(), + }) + assert.NilError(t, err) + + for _, ev := range evals { + if ev.Receiver != 1 { + continue + } + ciphertext, err := ecies.Encrypt(rand.Reader, &env.ownECIES.PublicKey, ev.Eval.Bytes(), nil, nil) + assert.NilError(t, err) + err = coreQueries.InsertDKGPolyEval(ctx, corekeyperdb.InsertDKGPolyEvalParams{ + KeyperSetIndex: testKsi, + RetryCounter: retry, + SenderIndex: int64(dealerIdx), //nolint:gosec // G115: keyper index is bounded by the keyper set size + ReceiverIndex: 1, + EncryptedEval: ciphertext, + }) + assert.NilError(t, err) + } +} + +// insertForeignDealing simulates `dealerIdx` having dealt: insert their poly +// commitment and the encrypted-for-us PolyEval row. Both are derived from a +// fresh puredkg run by the test in-process. +func (env *dkgTestEnv) insertForeignDealing(ctx context.Context, t *testing.T, dealerIdx uint64) { + t.Helper() + const eon = uint64(testKsi) + p := puredkg.NewPureDKG(eon, 3, 2, dealerIdx) + commit, evals, err := p.StartPhase1Dealing() + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(env.dbpool) + err = coreQueries.InsertDKGPolyCommitment(ctx, corekeyperdb.InsertDKGPolyCommitmentParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + KeyperIndex: int64(dealerIdx), //nolint:gosec // G115: keyper index is bounded by the keyper set size + Commitment: commit.Gammas.Marshal(), + }) + assert.NilError(t, err) + + for _, ev := range evals { + if ev.Receiver != 1 { + continue + } + ciphertext, err := ecies.Encrypt(rand.Reader, &env.ownECIES.PublicKey, ev.Eval.Bytes(), nil, nil) + assert.NilError(t, err) + err = coreQueries.InsertDKGPolyEval(ctx, corekeyperdb.InsertDKGPolyEvalParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + SenderIndex: int64(dealerIdx), //nolint:gosec // G115: keyper index is bounded by the keyper set size + ReceiverIndex: 1, + EncryptedEval: ciphertext, + }) + assert.NilError(t, err) + } +} + +// TestMaybeAccuseEnqueuesAccusationForMissingDealing asserts that maybeAccuse +// enqueues one submitAccusation tx when at least one dealer's PolyEval is +// missing — the bug-fix path. The function must NOT write to the shared +// `dkg_accusations` table (the chain syncer owns it); the test checks +// presence of the tx_outbox row and the `dkg_sent_actions` marker instead. +func TestMaybeAccuseEnqueuesAccusationForMissingDealing(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + // Local keyper deals — populates dkg_initial_states. + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + + // Neither keyper 0 nor keyper 2 deals: their commitment+eval rows are + // absent. maybeAccuse must enqueue a submitAccusation for both. + err = env.runMaybe(ctx, PhaseAccusing) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(env.dbpool) + accusations, err := coreQueries.GetDKGAccusations(ctx, corekeyperdb.GetDKGAccusationsParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(accusations), "maybeAccuse must not write to dkg_accusations — chain syncer owns it") + + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + Action: ActionAccusing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should mark the accusing action as enqueued") + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + // One submitDealing entry from maybeDeal, plus one submitAccusation. + assert.Equal(t, 2, len(pending)) +} + +// TestMaybeAccuseNoopWhenAllDealersHonest asserts the silent-success path: +// when every dealer's commitment + eval is present and valid, maybeAccuse +// enqueues no submitAccusation tx but still writes a `dkg_sent_actions` row +// so the reactor short-circuits on subsequent blocks (no log spam, no +// repeated puredkg replay). A second call is a no-op. +func TestMaybeAccuseNoopWhenAllDealersHonest(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + env.insertForeignDealing(ctx, t, 0) + env.insertForeignDealing(ctx, t, 2) + + err = env.runMaybe(ctx, PhaseAccusing) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(env.dbpool) + accusations, err := coreQueries.GetDKGAccusations(ctx, corekeyperdb.GetDKGAccusationsParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(accusations)) + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + // Only the submitDealing entry from maybeDeal — no submitAccusation. + assert.Equal(t, 1, len(pending)) + + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + Action: ActionAccusing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should be written even when no accusations are sent") + + // Second invocation: short-circuits on the existing dkg_sent_actions + // row, returns without error, and writes nothing new. + err = env.runMaybe(ctx, PhaseAccusing) + assert.NilError(t, err) + pendingAfter, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, len(pending), len(pendingAfter), "no extra tx_outbox rows on idempotent no-accusations call") +} + +// TestMaybeAccuseIdempotent asserts that a second invocation does not enqueue +// an additional submitAccusation tx_outbox row. The shared `dkg_accusations` +// table is never written by the reactor in either invocation. +func TestMaybeAccuseIdempotent(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + err = env.runMaybe(ctx, PhaseAccusing) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(env.dbpool) + firstPending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + Action: ActionAccusing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should exist for the accusing action") + + err = env.runMaybe(ctx, PhaseAccusing) + assert.NilError(t, err) + + accusations, err := coreQueries.GetDKGAccusations(ctx, corekeyperdb.GetDKGAccusationsParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(accusations), "maybeAccuse must not write to dkg_accusations on any invocation") + secondPending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + + assert.Equal(t, len(firstPending), len(secondPending), "no extra tx_outbox rows on idempotent call") +} diff --git a/rolling-shutter/dkg/active_dkgs_test.go b/rolling-shutter/dkg/active_dkgs_test.go new file mode 100644 index 000000000..38dfa4d11 --- /dev/null +++ b/rolling-shutter/dkg/active_dkgs_test.go @@ -0,0 +1,232 @@ +package dkg + +import ( + "context" + "database/sql" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/jackc/pgx/v4/pgxpool" + "gotest.tools/v3/assert" + + obskeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/testsetup" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" +) + +// activeDKGsSetFixture describes a Keyper Set + eons row pair inserted by +// the table-driven activeDKGs tests. `maxRetries` is zero-defaulted to +// `fixtureMaxRetries` so cases that don't care about the retry ceiling can +// omit it. +type activeDKGsSetFixture struct { + index int64 + activationBlock int64 + includeOwn bool + insertResult bool // pre-seed a dkg_result success row for this ksi. + maxRetries int64 +} + +// The same phase params are used for every fixture row unless a case +// overrides them per-set. Cycle length = 4 * phaseLength = 40 blocks; +// retry 0 dealing window opens at activationBlock - leadLength. +const ( + fixturePhaseLength int64 = 10 + fixtureLeadLength int64 = 2 + fixtureMaxRetries int64 = 10 +) + +func insertActiveDKGsFixture( + ctx context.Context, + t *testing.T, + dbpool *pgxpool.Pool, + ownAddr common.Address, + sets []activeDKGsSetFixture, +) { + t.Helper() + + // One extra non-member address to pad keyper sets when `includeOwn` is + // false. Regenerated per call so no cross-case leakage is possible. + otherECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + otherAddr := crypto.PubkeyToAddress(otherECDSA.PublicKey) + + coreQueries := corekeyperdb.New(dbpool) + obsQueries := obskeyperdb.New(dbpool) + + for _, s := range sets { + var keypers []common.Address + if s.includeOwn { + keypers = []common.Address{ownAddr, otherAddr} + } else { + keypers = []common.Address{otherAddr} + } + err := obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: s.index, + ActivationBlockNumber: s.activationBlock, + Keypers: shdb.EncodeAddresses(keypers), + Threshold: 1, + }) + assert.NilError(t, err) + + maxRetries := s.maxRetries + if maxRetries == 0 { + maxRetries = fixtureMaxRetries + } + err = coreQueries.InsertEon(ctx, corekeyperdb.InsertEonParams{ + Eon: s.index, + ActivationBlockNumber: s.activationBlock, + KeyperConfigIndex: s.index, + DkgContract: sql.NullString{String: "0xd0000000000000000000000000000000000000aa", Valid: true}, + PhaseLength: sql.NullInt64{Int64: fixturePhaseLength, Valid: true}, + LeadLength: sql.NullInt64{Int64: fixtureLeadLength, Valid: true}, + MaxRetries: maxRetries, + }) + assert.NilError(t, err) + + if s.insertResult { + err := coreQueries.InsertDKGResult(ctx, corekeyperdb.InsertDKGResultParams{ + Eon: s.index, + Success: true, + }) + assert.NilError(t, err) + } + } +} + +// TestActiveDKGsFilters is a table-driven test that walks the four filters — +// membership, prior success, supersession, and retry ceiling — through every +// case listed in docs/dkg-supersede/02-skip-superseded-keyper-sets.md's +// "Seam 1" checklist. Each row builds a fresh test database, inserts the +// declared keyper sets and eons, and asserts activeDKGs returns exactly the +// expected keyper-set indices. +// +//nolint:funlen // table body inlined for readability; every case is one clause of the ticket-02 checklist. +func TestActiveDKGsFilters(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + // Test-cases derive from the ticket-02 test checklist. `blockNumber` is + // picked so retry counter arithmetic never trips the retry ceiling unless + // a case sets a low `maxRetries` on its fixture set. + cases := []struct { + name string + sets []activeDKGsSetFixture + blockNumber uint64 + wantIndices []int64 + }{ + { + name: "single set member: included", + sets: []activeDKGsSetFixture{ + {index: 1, activationBlock: 100, includeOwn: true}, + }, + blockNumber: 105, + wantIndices: []int64{1}, + }, + { + name: "single set non-member: excluded", + sets: []activeDKGsSetFixture{ + {index: 1, activationBlock: 100, includeOwn: false}, + }, + blockNumber: 105, + wantIndices: nil, + }, + { + name: "two sets both live: only higher-index included", + sets: []activeDKGsSetFixture{ + {index: 1, activationBlock: 100, includeOwn: true}, + {index: 2, activationBlock: 120, includeOwn: true}, + }, + blockNumber: 130, + wantIndices: []int64{2}, + }, + { + name: "successor scheduled but not yet live: both included", + sets: []activeDKGsSetFixture{ + {index: 1, activationBlock: 100, includeOwn: true}, + {index: 2, activationBlock: 200, includeOwn: true}, + }, + blockNumber: 110, + wantIndices: []int64{1, 2}, + }, + { + name: "boundary: activation_M == currentBlock excludes older", + sets: []activeDKGsSetFixture{ + {index: 1, activationBlock: 100, includeOwn: true}, + {index: 2, activationBlock: 120, includeOwn: true}, + }, + blockNumber: 120, + wantIndices: []int64{2}, + }, + { + name: "superseded set with prior success row: still excluded", + sets: []activeDKGsSetFixture{ + {index: 1, activationBlock: 100, includeOwn: true, insertResult: true}, + {index: 2, activationBlock: 120, includeOwn: true}, + }, + blockNumber: 130, + wantIndices: []int64{2}, + }, + { + name: "member of older set but not successor: older still excluded", + sets: []activeDKGsSetFixture{ + {index: 1, activationBlock: 100, includeOwn: true}, + {index: 2, activationBlock: 120, includeOwn: false}, + }, + blockNumber: 130, + wantIndices: nil, + }, + { + name: "retry counter past MAX_RETRIES with no successor: excluded", + sets: []activeDKGsSetFixture{ + // activation=100, lead=2, phase=10, cycle=40, maxRetries=2. + // Block 185 lands in retry 3's dealing window (>= max), so + // the retry-ceiling filter excludes this set. + {index: 1, activationBlock: 100, includeOwn: true, maxRetries: 2}, + }, + blockNumber: 185, + wantIndices: nil, + }, + { + name: "no live keyper set: future scheduled set included", + sets: []activeDKGsSetFixture{ + {index: 1, activationBlock: 500, includeOwn: true}, + }, + blockNumber: 100, + wantIndices: []int64{1}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + + insertActiveDKGsFixture(ctx, t, dbpool, ownAddr, tc.sets) + + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + active, _, err := mgr.activeDKGs(ctx, tc.blockNumber) + assert.NilError(t, err) + + var gotIndices []int64 + for _, e := range active { + gotIndices = append(gotIndices, e.KeyperConfigIndex) + } + assert.DeepEqual(t, tc.wantIndices, gotIndices) + }) + } +} diff --git a/rolling-shutter/dkg/apologizing.go b/rolling-shutter/dkg/apologizing.go new file mode 100644 index 000000000..d4273115b --- /dev/null +++ b/rolling-shutter/dkg/apologizing.go @@ -0,0 +1,122 @@ +package dkg + +import ( + "context" + "database/sql" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/jackc/pgx/v4" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + + "github.com/shutter-network/shutter/shlib/puredkg" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/txsender" +) + +// maybeApologize is the per-block reactor for the Apologizing phase. It +// enqueues an apology tx when our locally-rebuilt puredkg has at least one +// accusation against us. Presence of a `dkg_sent_actions` row for +// `(k, r, "apologizing")` is the idempotency marker. +// +// Our own apology rows are NOT written to the shared `dkg_apologies` table +// — the chain syncer is the sole writer to it, so every keyper has the +// same view at each block height. Replay in `buildPureDKG` picks up our +// own apologies only after the on-chain submitApology event is indexed. +// +// The caller passes a `pure` returned by `buildPureDKG(PhaseApologizing)` — +// Phase=Accusing with commitments + evals + accusations replayed. We call +// StartPhase3Apologizing here, which advances the phase and emits apology +// messages with the polynomial loaded from `dkg_initial_states`. +func (m *Manager) maybeApologize( + ctx context.Context, + tx pgx.Tx, + dkgAddr common.Address, + keyperSetIndex, retryCounter int64, + pure *puredkg.PureDKG, + keypers []common.Address, + ownIndex uint64, +) error { + queries := corekeyperdb.New(tx) + alreadySent, err := queries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionApologizing, + }) + if err != nil { + return errors.Wrap(err, "check dkg sent action existence") + } + if alreadySent { + return nil + } + + apologies := pure.StartPhase3Apologizing() + if len(apologies) == 0 { + log.Info(). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Msg("Not sending apologies, nobody accused us") + // Mark the phase as resolved with a NULL tx_outbox_id row so the + // log line above runs at most once per DKG Instance. The set of + // on-chain accusations against us is fixed before the Apologizing + // phase begins, so re-evaluating on every block is redundant work. + if err := queries.InsertDKGSentAction(ctx, corekeyperdb.InsertDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionApologizing, + TxOutboxID: sql.NullInt64{}, + }); err != nil { + return errors.Wrap(err, "store apologizing sent action marker (no apologies)") + } + return nil + } + + accuserIndices := make([]uint64, 0, len(apologies)) + accuserDescriptions := make([]string, 0, len(apologies)) + polyEvalData := make([][]byte, 0, len(apologies)) + for _, ap := range apologies { + evalBytes := ap.Eval.Bytes() + accuserIndices = append(accuserIndices, ap.Accuser) + accuserDescriptions = append(accuserDescriptions, fmt.Sprintf("%d (%s)", ap.Accuser, keypers[ap.Accuser].Hex())) + polyEvalData = append(polyEvalData, evalBytes) + } + + abi, err := dkgcontract.DkgcontractMetaData.GetAbi() + if err != nil { + return errors.Wrap(err, "load DKG contract ABI") + } + data, err := abi.Pack( + "submitApology", + uint64(keyperSetIndex), //nolint:gosec // G115: keyper set index is bounded by the on-chain contract + uint64(retryCounter), //nolint:gosec // G115: retry counter is bounded by the on-chain contract + ownIndex, + accuserIndices, + polyEvalData, + ) + if err != nil { + return errors.Wrap(err, "pack submitApology calldata") + } + label := fmt.Sprintf("submitApology ksi=%d retry=%d", keyperSetIndex, retryCounter) + outboxID, err := txsender.EnqueueTx(ctx, tx, dkgAddr, data, nil, label) + if err != nil { + return errors.Wrap(err, "enqueue submitApology tx") + } + if err := queries.InsertDKGSentAction(ctx, corekeyperdb.InsertDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionApologizing, + TxOutboxID: sql.NullInt64{Int64: outboxID, Valid: true}, + }); err != nil { + return errors.Wrap(err, "store apologizing sent action marker") + } + log.Info(). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Int("count", len(accuserIndices)). + Strs("accusers", accuserDescriptions). + Msg("submitting DKG apologies") + return nil +} diff --git a/rolling-shutter/dkg/apologizing_test.go b/rolling-shutter/dkg/apologizing_test.go new file mode 100644 index 000000000..9a051e5ce --- /dev/null +++ b/rolling-shutter/dkg/apologizing_test.go @@ -0,0 +1,276 @@ +package dkg + +import ( + "context" + "testing" + + "github.com/jackc/pgx/v4" + "gotest.tools/v3/assert" + + "github.com/shutter-network/shutter/shlib/puredkg" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" +) + +// TestMaybeApologizeEnqueuesApologyWhenAccused asserts that maybeApologize +// enqueues a submitApology tx_outbox entry when an accusation against us is +// present — the bug-fix path. The function must NOT write to the shared +// `dkg_apologies` table (the chain syncer owns it); the test checks the +// tx_outbox row and `dkg_sent_actions` marker instead. +func TestMaybeApologizeEnqueuesApologyWhenAccused(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + + // Keyper 0 accuses us (keyper 1). The accusation row drives StartPhase3 + // to emit one apology addressed back to keyper 0. + coreQueries := corekeyperdb.New(env.dbpool) + err = coreQueries.InsertDKGAccusation(ctx, corekeyperdb.InsertDKGAccusationParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + AccuserIndex: 0, + AccusedIndex: 1, + }) + assert.NilError(t, err) + + err = env.runMaybe(ctx, PhaseApologizing) + assert.NilError(t, err) + + apologies, err := coreQueries.GetDKGApologies(ctx, corekeyperdb.GetDKGApologiesParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(apologies), "maybeApologize must not write to dkg_apologies — chain syncer owns it") + + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + Action: ActionApologizing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should mark the apologizing action as enqueued") + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + // submitDealing + submitApology. + assert.Equal(t, 2, len(pending)) +} + +// TestMaybeApologizeNoopWhenNotAccused asserts that maybeApologize enqueues +// no submitApology tx when no accusation against us is on file but still +// writes a `dkg_sent_actions` row so the reactor short-circuits on +// subsequent blocks (no log spam, no repeated puredkg replay). A second +// call is a no-op. +func TestMaybeApologizeNoopWhenNotAccused(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + + // Accusation against another keyper (0 → 2), not us. + coreQueries := corekeyperdb.New(env.dbpool) + err = coreQueries.InsertDKGAccusation(ctx, corekeyperdb.InsertDKGAccusationParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + AccuserIndex: 0, + AccusedIndex: 2, + }) + assert.NilError(t, err) + + err = env.runMaybe(ctx, PhaseApologizing) + assert.NilError(t, err) + + apologies, err := coreQueries.GetDKGApologies(ctx, corekeyperdb.GetDKGApologiesParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(apologies)) + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 1, len(pending)) // only the submitDealing entry + + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + Action: ActionApologizing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should be written even when no apologies are sent") + + // Second invocation: short-circuits on the existing dkg_sent_actions + // row, returns without error, and writes nothing new. + err = env.runMaybe(ctx, PhaseApologizing) + assert.NilError(t, err) + pendingAfter, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, len(pending), len(pendingAfter), "no extra tx_outbox rows on idempotent no-apologies call") +} + +// TestMaybeApologizeToleratesAccusationBetweenReadAndWriteTx asserts the +// split-transaction acceptance criterion from the read/write tx slice: +// when a new Accusation row is inserted between the buildPureDKG read tx +// closing and the write tx opening, maybeApologize must not error. The +// apology produced reflects the snapshot the read tx saw — the late +// accusation is left for the next block to cover. +func TestMaybeApologizeToleratesAccusationBetweenReadAndWriteTx(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + + // Keyper 0's accusation is the row the read tx will see. + coreQueries := corekeyperdb.New(env.dbpool) + err = coreQueries.InsertDKGAccusation(ctx, corekeyperdb.InsertDKGAccusationParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + AccuserIndex: 0, + AccusedIndex: 1, + }) + assert.NilError(t, err) + + keypers, ownIndex, threshold := loadKeyperSetParams(ctx, t, env) + + // Read tx: rebuild puredkg with the single accusation visible. + var pure *puredkg.PureDKG + err = env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + p, err := env.mgr.buildPureDKG(ctx, tx, testKsi, testRetry, PhaseApologizing, keypers, ownIndex, threshold) + pure = p + return err + }) + assert.NilError(t, err) + assert.Assert(t, pure != nil) + + // Race: keyper 2 accuses us after the read tx closed. The write tx + // will see this row in the DB but maybeApologize works off the stale + // puredkg snapshot returned above — it must not error. + err = coreQueries.InsertDKGAccusation(ctx, corekeyperdb.InsertDKGAccusationParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + AccuserIndex: 2, + AccusedIndex: 1, + }) + assert.NilError(t, err) + + // Write tx: dispatch maybeApologize with the snapshot from the read tx. + err = env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + return env.mgr.maybeApologize(ctx, tx, env.dkgAddr, testKsi, testRetry, pure, keypers, ownIndex) + }) + assert.NilError(t, err, "maybeApologize must tolerate a late accusation arriving between read tx and write tx") + + apologies, err := coreQueries.GetDKGApologies(ctx, corekeyperdb.GetDKGApologiesParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + }) + assert.NilError(t, err) + // maybeApologize no longer writes its own apology row — the chain + // syncer owns dkg_apologies. The acceptance signal that the call + // processed exactly the read-tx snapshot is the single tx_outbox entry + // recorded against the apologizing sent-action marker; the late + // accusation from keyper 2 is left for the next block within the + // Apologizing phase window. + assert.Equal(t, 0, len(apologies), "maybeApologize must not write to dkg_apologies") + + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + Action: ActionApologizing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should mark the apologizing action as enqueued") +} + +// TestMaybeApologizeIdempotent asserts that a second invocation does not +// enqueue an additional submitApology tx_outbox row. The dkg_sent_actions +// row written on the first invocation is the idempotency marker. The +// shared `dkg_apologies` table is never written by the reactor in either +// invocation. +func TestMaybeApologizeIdempotent(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(env.dbpool) + err = coreQueries.InsertDKGAccusation(ctx, corekeyperdb.InsertDKGAccusationParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + AccuserIndex: 0, + AccusedIndex: 1, + }) + assert.NilError(t, err) + + err = env.runMaybe(ctx, PhaseApologizing) + assert.NilError(t, err) + + firstPending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + Action: ActionApologizing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should exist for the apologizing action") + + err = env.runMaybe(ctx, PhaseApologizing) + assert.NilError(t, err) + + apologies, err := coreQueries.GetDKGApologies(ctx, corekeyperdb.GetDKGApologiesParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(apologies), "maybeApologize must not write to dkg_apologies on any invocation") + secondPending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, len(firstPending), len(secondPending), "no extra tx_outbox rows on idempotent call") +} + +// TestMaybeApologizeNoopWithoutInitialState asserts that maybeApologize +// returns silently when `dkg_initial_states` has no row — buildPureDKG +// returns nil and the action is skipped. +func TestMaybeApologizeNoopWithoutInitialState(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + coreQueries := corekeyperdb.New(env.dbpool) + err := coreQueries.InsertDKGAccusation(ctx, corekeyperdb.InsertDKGAccusationParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + AccuserIndex: 0, + AccusedIndex: 1, + }) + assert.NilError(t, err) + + err = env.runMaybe(ctx, PhaseApologizing) + assert.NilError(t, err) + + apologies, err := coreQueries.GetDKGApologies(ctx, corekeyperdb.GetDKGApologiesParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(apologies)) +} diff --git a/rolling-shutter/dkg/build_puredkg_test.go b/rolling-shutter/dkg/build_puredkg_test.go new file mode 100644 index 000000000..f4efaa440 --- /dev/null +++ b/rolling-shutter/dkg/build_puredkg_test.go @@ -0,0 +1,128 @@ +package dkg + +import ( + "context" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/jackc/pgx/v4" + "gotest.tools/v3/assert" + + "github.com/shutter-network/shutter/shlib/puredkg" + + obskeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" +) + +// loadKeyperSetParams looks up keypers/ownIndex/threshold for testKsi via the +// observer-db, matching the pool query processDKG runs before opening the +// buildPureDKG read tx. Tests call this once and pass the values into +// buildPureDKG so the function itself performs no observer-db reads. +func loadKeyperSetParams(ctx context.Context, t *testing.T, env *dkgTestEnv) ([]common.Address, uint64, uint64) { + t.Helper() + obsQueries := obskeyperdb.New(env.dbpool) + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, testKsi) + assert.NilError(t, err) + ownIndex, err := keyperSet.GetIndex(env.mgr.cfg.OwnAddress) + assert.NilError(t, err) + keypers, err := shdb.DecodeAddresses(keyperSet.Keypers) + assert.NilError(t, err) + return keypers, ownIndex, uint64(keyperSet.Threshold) //nolint:gosec // G115: threshold is a small positive integer +} + +// TestBuildPureDKGReturnsExpectedPhasePerBlockPhase asserts the per-phase +// reconstruction contract: each `blockPhase` input must leave the returned +// puredkg one step short of the phase the corresponding maybe-function will +// transition into (so `StartPhaseN` advances cleanly without tripping +// puredkg's `setPhase` invariant). +func TestBuildPureDKGReturnsExpectedPhasePerBlockPhase(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + keypers, ownIndex, threshold := loadKeyperSetParams(ctx, t, env) + + cases := []struct { + name string + phase Phase + wantPhase puredkg.Phase + }{ + {"PhaseDealing", PhaseDealing, puredkg.Off}, + {"PhaseAccusing", PhaseAccusing, puredkg.Dealing}, + {"PhaseApologizing", PhaseApologizing, puredkg.Accusing}, + {"PhaseFinalizing", PhaseFinalizing, puredkg.Apologizing}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var pure *puredkg.PureDKG + err := env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + p, err := env.mgr.buildPureDKG(ctx, tx, testKsi, testRetry, tc.phase, keypers, ownIndex, threshold) + pure = p + return err + }) + assert.NilError(t, err) + assert.Assert(t, pure != nil, "puredkg should be returned for member at phase %s", tc.name) + assert.Equal(t, tc.wantPhase, pure.Phase) + }) + } +} + +// TestBuildPureDKGNilWithoutInitialStateForNonDealingPhases asserts the +// "Keyper never dealt" branch: without a `dkg_initial_states` row, +// non-Dealing phases return nil so the caller skips silently. +func TestBuildPureDKGNilWithoutInitialStateForNonDealingPhases(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + keypers, ownIndex, threshold := loadKeyperSetParams(ctx, t, env) + + for _, p := range []Phase{PhaseAccusing, PhaseApologizing, PhaseFinalizing} { + t.Run(p.String(), func(t *testing.T) { + var pure *puredkg.PureDKG + err := env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + got, err := env.mgr.buildPureDKG(ctx, tx, testKsi, testRetry, p, keypers, ownIndex, threshold) + pure = got + return err + }) + assert.NilError(t, err) + assert.Assert(t, pure == nil, "expected nil puredkg for %s without initial state", p) + }) + } +} + +// TestBuildPureDKGDealingDoesNotRequireInitialState asserts the Dealing +// branch builds a fresh PureDKG regardless of whether `dkg_initial_states` +// has a row — `maybeDeal` is what populates it. +func TestBuildPureDKGDealingDoesNotRequireInitialState(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + // Sanity: no initial state row exists yet. + coreQueries := corekeyperdb.New(env.dbpool) + _, err := coreQueries.GetDKGInitialState(ctx, corekeyperdb.GetDKGInitialStateParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + }) + assert.Assert(t, err != nil) + + keypers, ownIndex, threshold := loadKeyperSetParams(ctx, t, env) + var pure *puredkg.PureDKG + err = env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + p, err := env.mgr.buildPureDKG(ctx, tx, testKsi, testRetry, PhaseDealing, keypers, ownIndex, threshold) + pure = p + return err + }) + assert.NilError(t, err) + assert.Assert(t, pure != nil) + assert.Equal(t, puredkg.Off, pure.Phase) +} diff --git a/rolling-shutter/dkg/dealing.go b/rolling-shutter/dkg/dealing.go new file mode 100644 index 000000000..4add62953 --- /dev/null +++ b/rolling-shutter/dkg/dealing.go @@ -0,0 +1,159 @@ +package dkg + +import ( + "context" + "database/sql" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/jackc/pgx/v4" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + + "github.com/shutter-network/shutter/shlib/puredkg" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" + "github.com/shutter-network/rolling-shutter/rolling-shutter/txsender" +) + +// maybeDeal is the per-block reactor for the Dealing phase. Idempotency is +// keyed on the presence of a `dkg_sent_actions` row for +// `(keyperSetIndex, retryCounter, "dealing")`; if one exists the call is a +// no-op. Otherwise we sample a polynomial via `puredkg.StartPhase1Dealing`, +// persist the resulting puredkg state (so accusations/apologies can be +// produced on later blocks even after a process restart), enqueue a +// `submitDealing` row in `tx_outbox` for `TxSender` to sign and submit, and +// insert the `dkg_sent_actions` marker atomically with the outbox row. +// +// Our own PolyCommitment and PolyEval rows are NOT written to the shared +// `dkg_poly_commitments` / `dkg_poly_evals` tables — the chain syncer is +// the sole writer to those, so every keyper has the same view at each +// block height. Replay in `buildPureDKG` re-derives our own commitment and +// self-eval from the polynomial in `dkg_initial_states`. +// +// The caller owns the (write) transaction and is responsible for committing +// or rolling back. `pure`, `keypers`, and `ownIndex` come from a prior read +// transaction's `buildPureDKG` call at the `processDKG` level. +// +//nolint:gocyclo,funlen // linear flow: check idempotency, deal, encrypt per receiver, enqueue. +func (m *Manager) maybeDeal( + ctx context.Context, + tx pgx.Tx, + dkgAddr common.Address, + keyperSetIndex, retryCounter int64, + pure *puredkg.PureDKG, + keypers []common.Address, + ownIndex uint64, +) error { + queries := corekeyperdb.New(tx) + alreadySent, err := queries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionDealing, + }) + if err != nil { + return errors.Wrap(err, "check dkg sent action existence") + } + if alreadySent { + return nil + } + + commitmentMsg, polyEvalMsgs, err := pure.StartPhase1Dealing() + if err != nil { + return errors.Wrap(err, "puredkg StartPhase1Dealing") + } + + // Persist the initial puredkg state — Phase=Dealing, polynomial set, + // `Evals[ownIndex]` populated from the self-eval — before writing any + // downstream rows. Subsequent blocks (and post-restart invocations) + // can rebuild from this blob to drive Accusing/Apologizing. + pureBytes, err := shdb.EncodePureDKG(pure) + if err != nil { + return errors.Wrap(err, "encode initial puredkg state") + } + if err := queries.InsertDKGInitialState(ctx, corekeyperdb.InsertDKGInitialStateParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + PuredkgBytes: pureBytes, + }); err != nil { + return errors.Wrap(err, "store initial puredkg state") + } + + commitmentBytes := commitmentMsg.Gammas.Marshal() + missingECIESCount := 0 + receivers := ReceiverIndicesForSender(uint64(len(keypers)), ownIndex) + encryptedEvals := make([][]byte, 0, len(receivers)) + for _, recvIdx := range receivers { + var evalMsg *puredkg.PolyEvalMsg + for i := range polyEvalMsgs { + if polyEvalMsgs[i].Receiver == recvIdx { + evalMsg = &polyEvalMsgs[i] + break + } + } + if evalMsg == nil { + return errors.Errorf("no poly eval for receiver %d", recvIdx) + } + recvAddr := keypers[recvIdx] + ciphertext, err := m.encryptPolyEvalFor(ctx, queries, recvAddr, evalMsg.Eval) + if err != nil { + // A missing ECIES key for one receiver is a recoverable + // condition: substitute empty bytes at that slot so the + // positional N−1 layout is preserved for all other receivers, + // and continue. The receiver will fail to decrypt their slot + // and accuse us through the normal Accusation flow. + if errors.Is(err, pgx.ErrNoRows) { + log.Warn(). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Uint64("receiver-index", recvIdx). + Str("receiver-address", recvAddr.Hex()). + Msg("no ECIES key registered for receiver; substituting empty eval and continuing") + missingECIESCount++ + encryptedEvals = append(encryptedEvals, []byte{}) + continue + } + return errors.Wrapf(err, "encrypt poly eval for receiver %d (%s)", recvIdx, recvAddr.Hex()) + } + encryptedEvals = append(encryptedEvals, ciphertext) + } + + abi, err := dkgcontract.DkgcontractMetaData.GetAbi() + if err != nil { + return errors.Wrap(err, "load DKG contract ABI") + } + data, err := abi.Pack( + "submitDealing", + uint64(keyperSetIndex), //nolint:gosec // G115: keyper set index is bounded by the on-chain contract + uint64(retryCounter), //nolint:gosec // G115: retry counter is bounded by the on-chain contract + ownIndex, + commitmentBytes, + encryptedEvals, + ) + if err != nil { + return errors.Wrap(err, "pack submitDealing calldata") + } + label := fmt.Sprintf("submitDealing ksi=%d retry=%d", keyperSetIndex, retryCounter) + outboxID, err := txsender.EnqueueTx(ctx, tx, dkgAddr, data, nil, label) + if err != nil { + return errors.Wrap(err, "enqueue submitDealing tx") + } + if err := queries.InsertDKGSentAction(ctx, corekeyperdb.InsertDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionDealing, + TxOutboxID: sql.NullInt64{Int64: outboxID, Valid: true}, + }); err != nil { + return errors.Wrap(err, "store dealing sent action marker") + } + log.Info(). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Uint64("keyper-index", ownIndex). + Int("keyper-count", len(keypers)). + Int("missing-ecies-count", missingECIESCount). + Msg("submitting DKG dealing") + return nil +} diff --git a/rolling-shutter/dkg/dealing_test.go b/rolling-shutter/dkg/dealing_test.go new file mode 100644 index 000000000..6cc1bbf33 --- /dev/null +++ b/rolling-shutter/dkg/dealing_test.go @@ -0,0 +1,407 @@ +package dkg + +import ( + "context" + "database/sql" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + "gotest.tools/v3/assert" + + "github.com/shutter-network/shutter/shlib/puredkg" + + obskeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/testsetup" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" + "github.com/shutter-network/rolling-shutter/rolling-shutter/txsender" +) + +// runMaybeDealLocal mirrors the dispatch processDKG performs: look up the +// keyper set, build the puredkg in a read tx, then call maybeDeal in a +// separate write tx. Used by dealing_test.go tests that wire a Manager by +// hand instead of going through dkgTestEnv. +func runMaybeDealLocal( + ctx context.Context, + dbpool *pgxpool.Pool, + mgr *Manager, + dkgAddr common.Address, + keyperConfigIndex int64, +) error { + const retryCounter int64 = 0 + obsQueries := obskeyperdb.New(dbpool) + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, keyperConfigIndex) + if err != nil { + return err + } + ownIndex, err := keyperSet.GetIndex(mgr.cfg.OwnAddress) + if err != nil { + return nil + } + keypers, err := shdb.DecodeAddresses(keyperSet.Keypers) + if err != nil { + return err + } + threshold := uint64(keyperSet.Threshold) //nolint:gosec // G115: threshold is a small positive integer + + var pure *puredkg.PureDKG + err = dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + p, err := mgr.buildPureDKG(ctx, tx, keyperConfigIndex, retryCounter, PhaseDealing, keypers, ownIndex, threshold) + if err != nil { + return err + } + pure = p + return nil + }) + if err != nil { + return err + } + if pure == nil { + return nil + } + return dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + return mgr.maybeDeal(ctx, tx, dkgAddr, keyperConfigIndex, retryCounter, pure, keypers, ownIndex) + }) +} + +// TestMaybeDealPersistsInitialStateAndIsIdempotent exercises the two +// acceptance criteria for the maybeDeal refactor: +// +// 1. First invocation writes a dkg_initial_states row, a tx_outbox row for +// submitDealing, and a dkg_sent_actions row marking the dealing action +// as enqueued. It must NOT write to the shared `dkg_poly_commitments` +// or `dkg_poly_evals` tables — those are populated exclusively by the +// chain syncer from indexed events. +// 2. Second invocation is a no-op — no new rows are inserted in any of those +// tables, because the dkg_sent_actions row already exists. +// +//nolint:funlen // fixture-heavy integration test; the "first-call writes, second is no-op" narrative is clearer inline. +func TestMaybeDealPersistsInitialStateAndIsIdempotent(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const ( + keyperConfigIndex int64 = 7 + retryCounter int64 = 0 + ) + + // Build a 3-keyper set where we are the keyper at index 1. + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + other0, err := crypto.GenerateKey() + assert.NilError(t, err) + other0Addr := crypto.PubkeyToAddress(other0.PublicKey) + other2, err := crypto.GenerateKey() + assert.NilError(t, err) + other2Addr := crypto.PubkeyToAddress(other2.PublicKey) + keypers := []common.Address{other0Addr, ownAddr, other2Addr} + + obsQueries := obskeyperdb.New(dbpool) + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: keyperConfigIndex, + ActivationBlockNumber: 0, + Keypers: shdb.EncodeAddresses(keypers), + Threshold: 2, + }) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(dbpool) + // All three keypers must have an ECIES public key registered — encryption + // of per-receiver evals (and the self-eval) reads this table. + for _, kp := range []struct { + addr common.Address + key *ecies.PrivateKey + }{ + {addr: other0Addr, key: ecies.ImportECDSA(other0)}, + {addr: ownAddr, key: ecies.ImportECDSA(ownECDSA)}, + {addr: other2Addr, key: ecies.ImportECDSA(other2)}, + } { + err = coreQueries.UpsertECIESKey(ctx, corekeyperdb.UpsertECIESKeyParams{ + KeyperAddress: shdb.EncodeAddress(kp.addr), + EciesPublicKey: shdb.EncodeEciesPublicKey(&kp.key.PublicKey), + }) + assert.NilError(t, err) + } + + dkgAddr := common.HexToAddress("0xd0000000000000000000000000000000000000aa") + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + // Sanity: nothing exists yet. + _, err = coreQueries.GetDKGInitialState(ctx, corekeyperdb.GetDKGInitialStateParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + }) + assert.Assert(t, err != nil, "no initial state row expected before maybeDeal") + + // First invocation: writes rows. + err = runMaybeDealLocal(ctx, dbpool, mgr, dkgAddr, keyperConfigIndex) + assert.NilError(t, err) + + initial, err := coreQueries.GetDKGInitialState(ctx, corekeyperdb.GetDKGInitialStateParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + }) + assert.NilError(t, err) + assert.Assert(t, len(initial.PuredkgBytes) > 0, "initial state blob should not be empty") + // Blob must decode back to a usable PureDKG with the self-eval populated. + roundtrip, err := shdb.DecodePureDKG(initial.PuredkgBytes) + assert.NilError(t, err) + assert.Equal(t, uint64(1), roundtrip.Keyper) + assert.Assert(t, roundtrip.Evals[1] != nil, "self-eval should be set on persisted state") + + commitments, err := coreQueries.GetDKGPolyCommitments(ctx, corekeyperdb.GetDKGPolyCommitmentsParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(commitments), "maybeDeal must not write to dkg_poly_commitments — chain syncer owns it") + + evals, err := coreQueries.GetDKGPolyEvals(ctx, corekeyperdb.GetDKGPolyEvalsParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(evals), "maybeDeal must not write to dkg_poly_evals — chain syncer owns it") + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 1, len(pending)) + assert.Equal(t, dkgAddr.Hex(), pending[0].ToAddress) + firstOutboxID := pending[0].ID + + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + Action: ActionDealing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should exist for the dealing action") + + // Second invocation: idempotent — no new rows in any tracked table. + err = runMaybeDealLocal(ctx, dbpool, mgr, dkgAddr, keyperConfigIndex) + assert.NilError(t, err) + + commitmentsAfter, err := coreQueries.GetDKGPolyCommitments(ctx, corekeyperdb.GetDKGPolyCommitmentsParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(commitmentsAfter), "dkg_poly_commitments still empty on idempotent call") + + evalsAfter, err := coreQueries.GetDKGPolyEvals(ctx, corekeyperdb.GetDKGPolyEvalsParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(evalsAfter), "dkg_poly_evals still empty on idempotent call") + + pendingAfter, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 1, len(pendingAfter), "no extra tx_outbox rows on idempotent call") + assert.Equal(t, firstOutboxID, pendingAfter[0].ID, "same outbox row as before") +} + +// TestMaybeDealNoopWhenSentActionExists asserts that a pre-seeded +// dkg_sent_actions row for the dealing action short-circuits maybeDeal — no +// commitment, eval, or outbox row is written. This is the new idempotency +// signal that replaces the previous dkg_initial_states check. +func TestMaybeDealNoopWhenSentActionExists(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const ( + keyperConfigIndex int64 = 9 + retryCounter int64 = 0 + ) + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + obsQueries := obskeyperdb.New(dbpool) + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: keyperConfigIndex, + ActivationBlockNumber: 0, + Keypers: shdb.EncodeAddresses([]common.Address{ownAddr}), + Threshold: 1, + }) + assert.NilError(t, err) + + // Pre-seed a tx_outbox row (via EnqueueTx) so the dkg_sent_actions FK is + // satisfied, then the dkg_sent_actions marker for the dealing action. + coreQueries := corekeyperdb.New(dbpool) + var outboxID int64 + err = dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + id, inner := txsender.EnqueueTx(ctx, tx, common.HexToAddress("0xd0000000000000000000000000000000000000aa"), []byte{0x01}, nil, "preseed") + if inner != nil { + return inner + } + outboxID = id + return corekeyperdb.New(tx).InsertDKGSentAction(ctx, corekeyperdb.InsertDKGSentActionParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + Action: ActionDealing, + TxOutboxID: sql.NullInt64{Int64: id, Valid: true}, + }) + }) + assert.NilError(t, err) + + dkgAddr := common.HexToAddress("0xd0000000000000000000000000000000000000aa") + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + err = runMaybeDealLocal(ctx, dbpool, mgr, dkgAddr, keyperConfigIndex) + assert.NilError(t, err) + + commitments, err := coreQueries.GetDKGPolyCommitments(ctx, corekeyperdb.GetDKGPolyCommitmentsParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + }) + assert.NilError(t, err) + assert.Equal(t, 0, len(commitments), "no commitment row should be written when sent action exists") + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 1, len(pending), "only the pre-seeded outbox row should remain") + assert.Equal(t, outboxID, pending[0].ID) + _, err = coreQueries.GetDKGInitialState(ctx, corekeyperdb.GetDKGInitialStateParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + }) + assert.Assert(t, err != nil, "no initial state row should be written when sent action exists") +} + +// TestMaybeDealSubstitutesEmptyEvalForMissingECIESKey asserts the graceful- +// fallback behavior added for the "single misconfigured keyper must not block +// the entire DKG" problem: when one receiver has no `ecies_keys` row, the +// dealing is still enqueued, the `polyEvals` array still has length N−1, and +// the slot for the missing receiver is empty bytes (positional semantics +// preserved). Other receivers' slots remain populated with valid ciphertexts. +func TestMaybeDealSubstitutesEmptyEvalForMissingECIESKey(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const ( + keyperConfigIndex int64 = 11 + retryCounter int64 = 0 + ) + + // Build a 3-keyper set where we are the keyper at index 1, the receiver + // at index 0 has no ECIES key, and the receiver at index 2 does. + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + other0, err := crypto.GenerateKey() + assert.NilError(t, err) + other0Addr := crypto.PubkeyToAddress(other0.PublicKey) + other2, err := crypto.GenerateKey() + assert.NilError(t, err) + other2Addr := crypto.PubkeyToAddress(other2.PublicKey) + keypers := []common.Address{other0Addr, ownAddr, other2Addr} + + obsQueries := obskeyperdb.New(dbpool) + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: keyperConfigIndex, + ActivationBlockNumber: 0, + Keypers: shdb.EncodeAddresses(keypers), + Threshold: 2, + }) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(dbpool) + // Register ECIES keys for self and `other2` only; `other0` is intentionally + // absent — simulating a keyper that has not (yet) registered. + for _, kp := range []struct { + addr common.Address + key *ecies.PrivateKey + }{ + {addr: ownAddr, key: ecies.ImportECDSA(ownECDSA)}, + {addr: other2Addr, key: ecies.ImportECDSA(other2)}, + } { + err = coreQueries.UpsertECIESKey(ctx, corekeyperdb.UpsertECIESKeyParams{ + KeyperAddress: shdb.EncodeAddress(kp.addr), + EciesPublicKey: shdb.EncodeEciesPublicKey(&kp.key.PublicKey), + }) + assert.NilError(t, err) + } + + dkgAddr := common.HexToAddress("0xd0000000000000000000000000000000000000aa") + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + err = runMaybeDealLocal(ctx, dbpool, mgr, dkgAddr, keyperConfigIndex) + assert.NilError(t, err, "missing ECIES key for one receiver must not abort dealing") + + // Dealing was enqueued — initial state, sent-action marker, and tx_outbox + // row must all be present. + _, err = coreQueries.GetDKGInitialState(ctx, corekeyperdb.GetDKGInitialStateParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + }) + assert.NilError(t, err, "initial state row should be written despite missing ECIES key") + + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: retryCounter, + Action: ActionDealing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should be written for the dealing action") + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 1, len(pending), "one submitDealing outbox row expected") + assert.Equal(t, dkgAddr.Hex(), pending[0].ToAddress) + + // Decode the submitDealing calldata to inspect the polyEvals payload. + // Calldata layout is [4-byte selector][ABI-encoded args]. + abi, err := dkgcontract.DkgcontractMetaData.GetAbi() + assert.NilError(t, err) + method, ok := abi.Methods["submitDealing"] + assert.Assert(t, ok, "submitDealing method must be present on the ABI") + assert.Assert(t, len(pending[0].Data) >= 4, "calldata must contain the 4-byte selector") + args, err := method.Inputs.Unpack(pending[0].Data[4:]) + assert.NilError(t, err) + polyEvals, ok := args[4].([][]byte) + assert.Assert(t, ok, "polyEvals arg must decode to [][]byte") + + // N=3, sender index 1 → receivers [0, 2] → polyEvals length 2. + // Slot 0 corresponds to receiver 0 (missing key, empty); slot 1 to + // receiver 2 (present, non-empty ciphertext). + assert.Equal(t, 2, len(polyEvals), "polyEvals length must equal N-1") + assert.Equal(t, 0, len(polyEvals[0]), "missing-key slot must be empty bytes") + assert.Assert(t, len(polyEvals[1]) > 0, "present-key slot must contain ciphertext") +} diff --git a/rolling-shutter/dkg/ecies.go b/rolling-shutter/dkg/ecies.go new file mode 100644 index 000000000..b2b0ef51b --- /dev/null +++ b/rolling-shutter/dkg/ecies.go @@ -0,0 +1,92 @@ +package dkg + +import ( + "context" + "fmt" + + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/jackc/pgx/v4" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/shutter-network/contracts/v2/bindings/ecieskeyregistry" + + obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" + "github.com/shutter-network/rolling-shutter/rolling-shutter/txsender" +) + +// MaybeRegisterECIESKey enqueues a `registerKey` outbox row for the ECIES +// key registry if we are a member of the given keyper set and our public +// key is not yet present in `ecies_keys`. The DB cache is populated by the +// host keyper's ECIES syncer (both initial poll and live events) and is the +// source of truth for prior registrations. +// +// Per ADR 0004 this writes to `tx_outbox` instead of calling the registry +// directly; `TxSender` handles signing and submission. The check is +// idempotent — once the registry event is indexed back into `ecies_keys`, +// subsequent calls become no-ops. +// +// The destination address is the manager's configured `ECIESRegistryAddr` +// (a single registry serves all keyper sets). This is intended to be called +// by the host keyper's Keyper Set Syncer handler once per discovered keyper +// set, at the same architectural level as `HandleBlock`. +func (m *Manager) MaybeRegisterECIESKey(ctx context.Context, keyperSetIndex int64) error { + return m.cfg.DBPool.BeginFunc(ctx, func(tx pgx.Tx) error { + obsQueries := obskeyper.New(tx) + coreQueries := corekeyperdb.New(tx) + + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, keyperSetIndex) + if err != nil { + return errors.Wrapf(err, "fetch keyper set %d", keyperSetIndex) + } + ownIndex, err := keyperSet.GetIndex(m.cfg.OwnAddress) + if err != nil { + // Not a member. + return nil + } + + exists, err := coreQueries.ExistsECIESKey(ctx, shdb.EncodeAddress(m.cfg.OwnAddress)) + if err != nil { + return errors.Wrap(err, "query ecies_keys for own address") + } + if exists { + log.Debug(). + Int64("keyper-set-index", keyperSetIndex). + Uint64("keyper-index", ownIndex). + Msg("ECIES key already registered for keyper set, skipping") + return nil + } + + // `crypto/ecies.PrivateKey` embeds an ECDSA public key on its + // `PublicKey.ExportECDSA()` accessor. We need raw secp256k1 public + // key bytes (the on-chain format) for the registry. + ecdsaPub := m.cfg.ECIESPrivateKey.ExportECDSA().PublicKey + pubKey := ethcrypto.FromECDSAPub(&ecdsaPub) + + abi, err := ecieskeyregistry.EcieskeyregistryMetaData.GetAbi() + if err != nil { + return errors.Wrap(err, "load ECIESKeyRegistry ABI") + } + data, err := abi.Pack( + "registerKey", + uint64(keyperSetIndex), //nolint:gosec // G115: keyper set index is bounded by the on-chain contract + ownIndex, + pubKey, + ) + if err != nil { + return errors.Wrap(err, "pack registerKey calldata") + } + label := fmt.Sprintf("registerKey ksi=%d", keyperSetIndex) + outboxID, err := txsender.EnqueueTx(ctx, tx, m.cfg.ECIESRegistryAddr, data, nil, label) + if err != nil { + return errors.Wrap(err, "enqueue registerKey tx") + } + log.Info(). + Int64("keyper-set-index", keyperSetIndex). + Uint64("keyper-index", ownIndex). + Int64("tx-outbox-id", outboxID). + Msg("enqueued ECIES key registration") + return nil + }) +} diff --git a/rolling-shutter/dkg/ecies_test.go b/rolling-shutter/dkg/ecies_test.go new file mode 100644 index 000000000..b8f797985 --- /dev/null +++ b/rolling-shutter/dkg/ecies_test.go @@ -0,0 +1,142 @@ +package dkg + +import ( + "context" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "gotest.tools/v3/assert" + + obskeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/testsetup" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" +) + +// TestMaybeRegisterECIESKeyIsIdempotent exercises the two acceptance +// criteria of MaybeRegisterECIESKey: +// +// 1. First invocation for a keyper set we are a member of writes a single +// tx_outbox row (a `registerKey` enqueue against the configured registry +// address). +// 2. Second invocation is a no-op once the `ecies_keys` row exists — the +// DB cache populated by the host keyper's ECIES syncer is the source of +// truth for prior registrations. +func TestMaybeRegisterECIESKeyIsIdempotent(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const keyperConfigIndex int64 = 11 + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + otherECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + otherAddr := crypto.PubkeyToAddress(otherECDSA.PublicKey) + + obsQueries := obskeyperdb.New(dbpool) + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: keyperConfigIndex, + ActivationBlockNumber: 0, + // We are index 1 in this keyper set; the test asserts on `pending[0].ID` + // rather than the encoded calldata, so the specific index does not + // matter for the acceptance criteria. + Keypers: shdb.EncodeAddresses([]common.Address{otherAddr, ownAddr}), + Threshold: 2, + }) + assert.NilError(t, err) + + registryAddr := common.HexToAddress("0xe0000000000000000000000000000000000000bb") + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: registryAddr, + }) + + coreQueries := corekeyperdb.New(dbpool) + + // First invocation: writes exactly one outbox row targeting the registry. + err = mgr.MaybeRegisterECIESKey(ctx, keyperConfigIndex) + assert.NilError(t, err) + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 1, len(pending), "first call must enqueue exactly one registerKey tx") + assert.Equal(t, registryAddr.Hex(), pending[0].ToAddress) + firstOutboxID := pending[0].ID + + // Second invocation (before the ECIES syncer has indexed the registry + // event back into ecies_keys): still a no-op, because membership and + // the ECIES-key check are the only gates. The first run's enqueue is + // still in the outbox, but that does not affect ExistsECIESKey, so a + // raw second call would actually re-enqueue. The contract is that the + // caller's ECIES syncer populates ecies_keys before the next call — + // emulate that here, then verify the no-op. + err = coreQueries.UpsertECIESKey(ctx, corekeyperdb.UpsertECIESKeyParams{ + KeyperAddress: shdb.EncodeAddress(ownAddr), + EciesPublicKey: shdb.EncodeEciesPublicKey(&ecies.ImportECDSA(ownECDSA).PublicKey), + }) + assert.NilError(t, err) + + err = mgr.MaybeRegisterECIESKey(ctx, keyperConfigIndex) + assert.NilError(t, err) + + pendingAfter, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 1, len(pendingAfter), "second call must not enqueue another registerKey tx") + assert.Equal(t, firstOutboxID, pendingAfter[0].ID, "same outbox row as before") +} + +// TestMaybeRegisterECIESKeyNotMemberIsNoop asserts that a keyper who is +// not a member of the given keyper set never enqueues a registerKey tx. +func TestMaybeRegisterECIESKeyNotMemberIsNoop(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const keyperConfigIndex int64 = 12 + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + otherECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + otherAddr := crypto.PubkeyToAddress(otherECDSA.PublicKey) + + obsQueries := obskeyperdb.New(dbpool) + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: keyperConfigIndex, + ActivationBlockNumber: 0, + Keypers: shdb.EncodeAddresses([]common.Address{otherAddr}), + Threshold: 1, + }) + assert.NilError(t, err) + + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + err = mgr.MaybeRegisterECIESKey(ctx, keyperConfigIndex) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(dbpool) + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 0, len(pending), "non-member must not enqueue any registerKey tx") +} diff --git a/rolling-shutter/dkg/finalizing.go b/rolling-shutter/dkg/finalizing.go new file mode 100644 index 000000000..69d1c29a1 --- /dev/null +++ b/rolling-shutter/dkg/finalizing.go @@ -0,0 +1,119 @@ +package dkg + +import ( + "context" + "database/sql" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/jackc/pgx/v4" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + + "github.com/shutter-network/shutter/shlib/puredkg" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/txsender" +) + +// maybeFinalize is the per-block reactor for the Finalizing phase. It +// enqueues a `submitSuccessVote` tx with the locally-computed eon public key. +// +// Idempotency uses `ExistsDKGSentAction` keyed on (keyperSetIndex, +// retryCounter, ActionFinalizing). Using the retry-scoped sent-actions row +// rather than `ExistsDKGResultSuccess` means a keyper that voted for retry 0 +// is not blocked from voting again when retry 1 starts: a different +// retryCounter produces a different row. `dkg_result` is written only when +// the chain emits a DKGSucceeded event (via `Manager.HandleDKGSuccess`), so +// it is never present here. +// +// The caller passes a `pure` returned by `buildPureDKG(PhaseFinalizing)` — +// Phase=Apologizing with all stored apologies applied. We bypass puredkg's +// phase machinery by setting `pure.Phase = Finalized` directly and call +// `ComputeResult`. +func (m *Manager) maybeFinalize( + ctx context.Context, + tx pgx.Tx, + dkgAddr common.Address, + keyperSetIndex, retryCounter int64, + pure *puredkg.PureDKG, + ownIndex uint64, +) error { + queries := corekeyperdb.New(tx) + alreadySent, err := queries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionFinalizing, + }) + if err != nil { + return errors.Wrap(err, "check finalizing sent action") + } + if alreadySent { + return nil + } + + // buildPureDKG returns the puredkg at Phase=Apologizing with all + // stored apologies applied. Finalize() advances it to Finalized so + // ComputeResult can run. + pure.Finalize() + result, err := pure.ComputeResult() + if err != nil { + log.Warn().Err(err). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Msg("cannot compute DKG result; skipping success vote") + // Mark the phase as resolved with a NULL tx_outbox_id row so the + // warning above runs at most once per DKG Instance. The local + // puredkg state is stable once all on-chain messages have been + // indexed; re-running ComputeResult on every block is redundant. + if insertErr := queries.InsertDKGSentAction(ctx, corekeyperdb.InsertDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionFinalizing, + TxOutboxID: sql.NullInt64{}, + }); insertErr != nil { + return errors.Wrap(insertErr, "store finalizing sent action marker (compute result failed)") + } + return nil + } + + eonPubKeyBytes, err := result.PublicKey.GobEncode() + if err != nil { + return errors.Wrap(err, "encode eon public key") + } + + abi, err := dkgcontract.DkgcontractMetaData.GetAbi() + if err != nil { + return errors.Wrap(err, "load DKG contract ABI") + } + data, err := abi.Pack( + "submitSuccessVote", + uint64(keyperSetIndex), //nolint:gosec // G115: keyper set index is bounded by the on-chain contract + uint64(retryCounter), //nolint:gosec // G115: retry counter is bounded by the on-chain contract + ownIndex, + eonPubKeyBytes, + ) + if err != nil { + return errors.Wrap(err, "pack submitSuccessVote calldata") + } + label := fmt.Sprintf("submitSuccessVote ksi=%d retry=%d", keyperSetIndex, retryCounter) + outboxID, err := txsender.EnqueueTx(ctx, tx, dkgAddr, data, nil, label) + if err != nil { + return errors.Wrap(err, "enqueue submitSuccessVote tx") + } + if err := queries.InsertDKGSentAction(ctx, corekeyperdb.InsertDKGSentActionParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + Action: ActionFinalizing, + TxOutboxID: sql.NullInt64{Int64: outboxID, Valid: true}, + }); err != nil { + return errors.Wrap(err, "store finalizing sent action marker") + } + log.Info(). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Int64("tx-outbox-id", outboxID). + Msg("enqueued DKG success vote") + return nil +} diff --git a/rolling-shutter/dkg/finalizing_test.go b/rolling-shutter/dkg/finalizing_test.go new file mode 100644 index 000000000..770230877 --- /dev/null +++ b/rolling-shutter/dkg/finalizing_test.go @@ -0,0 +1,180 @@ +package dkg + +import ( + "context" + "testing" + + "github.com/jackc/pgx/v4" + "gotest.tools/v3/assert" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" +) + +// TestMaybeFinalizeWritesSentActionNotDKGResult exercises the success path of +// maybeFinalize when all three dealers are honest: +// +// 1. First invocation enqueues a submitSuccessVote tx_outbox row and writes a +// dkg_sent_actions row for ActionFinalizing. It must NOT write dkg_result — +// that row is the exclusive responsibility of HandleDKGSuccess. +// 2. Second invocation is a no-op — ExistsDKGSentAction short-circuits so no +// new rows are inserted. +func TestMaybeFinalizeWritesSentActionNotDKGResult(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + env.insertForeignDealing(ctx, t, 0) + env.insertForeignDealing(ctx, t, 2) + + err = env.runMaybe(ctx, PhaseFinalizing) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(env.dbpool) + + // dkg_result must NOT be present — HandleDKGSuccess owns that write. + _, err = coreQueries.GetDKGResult(ctx, testKsi) + assert.ErrorContains(t, err, "no rows", "maybeFinalize must not write dkg_result") + + firstPending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + // submitDealing + submitSuccessVote. + assert.Equal(t, 2, len(firstPending)) + + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + Action: ActionFinalizing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should exist for the finalizing action") + + // Second invocation: ExistsDKGSentAction short-circuits; no new rows. + err = env.runMaybe(ctx, PhaseFinalizing) + assert.NilError(t, err) + + secondPending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, len(firstPending), len(secondPending), "no extra tx_outbox rows on idempotent call") +} + +// TestMaybeFinalizeWritesSentActionOnComputeResultFailure asserts that when +// ComputeResult fails (e.g. a dealer is missing without an accusation +// against them) maybeFinalize still writes a `dkg_sent_actions` row so the +// "cannot compute DKG result" warning runs at most once per DKG Instance. A +// second invocation is a no-op. +func TestMaybeFinalizeWritesSentActionOnComputeResultFailure(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + // Local keyper deals — populates dkg_initial_states. Neither keyper 0 + // nor keyper 2's PolyCommitment+PolyEval ever arrives, and we do NOT + // record an accusation against them, so isCorrupt(0)/isCorrupt(2) is + // false at Finalize time. ComputeResult errors out because a "corrupt + // keyper is not considered corrupt". + err := env.runMaybe(ctx, PhaseDealing) + assert.NilError(t, err) + + err = env.runMaybe(ctx, PhaseFinalizing) + assert.NilError(t, err, "maybeFinalize must not surface ComputeResult errors") + + coreQueries := corekeyperdb.New(env.dbpool) + + // No submitSuccessVote was enqueued — only the submitDealing entry. + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 1, len(pending), "no submitSuccessVote when ComputeResult fails") + + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: testRetry, + Action: ActionFinalizing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction, "dkg_sent_actions row should be written even when ComputeResult fails") + + // Second invocation: short-circuits on the existing dkg_sent_actions + // row, returns without error, and writes nothing new. + err = env.runMaybe(ctx, PhaseFinalizing) + assert.NilError(t, err) + pendingAfter, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, len(pending), len(pendingAfter), "no extra tx_outbox rows on idempotent compute-result-failed call") +} + +// TestHandleDKGSuccessRetry1AfterRetry0Failed asserts the key correctness +// property: a keyper that locally finalized retry 0 (sent a success vote) is +// not blocked from participating in retry 1 when retry 0 fails on chain, and +// HandleDKGSuccess for retry 1 populates dkg_result.pure_result. +func TestHandleDKGSuccessRetry1AfterRetry0Failed(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + env := setupDKGTestEnv(ctx, t) + + const retry0 int64 = 0 + const retry1 int64 = 1 + + // --- Retry 0 --- + // Local keyper deals and all three keypers' data is available. + err := env.runMaybeRetry(ctx, PhaseDealing, retry0) + assert.NilError(t, err) + env.insertForeignDealingRetry(ctx, t, 0, retry0) + env.insertForeignDealingRetry(ctx, t, 2, retry0) + + // maybeFinalize enqueues the success vote for retry 0. + err = env.runMaybeRetry(ctx, PhaseFinalizing, retry0) + assert.NilError(t, err) + + coreQueries := corekeyperdb.New(env.dbpool) + + // Retry 0 failed on chain: no dkg_result row exists yet. + _, err = coreQueries.GetDKGResult(ctx, testKsi) + assert.ErrorContains(t, err, "no rows", "no dkg_result row before HandleDKGSuccess") + + // --- Retry 1 --- + // Local keyper deals again (new dkg_initial_states row for retry 1). + err = env.runMaybeRetry(ctx, PhaseDealing, retry1) + assert.NilError(t, err) + env.insertForeignDealingRetry(ctx, t, 0, retry1) + env.insertForeignDealingRetry(ctx, t, 2, retry1) + + // maybeFinalize for retry 1 must succeed — ExistsDKGSentAction checks + // (ksi, retry=1, "finalizing") which has no row yet. + err = env.runMaybeRetry(ctx, PhaseFinalizing, retry1) + assert.NilError(t, err) + + sentAction1, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: testKsi, + RetryCounter: retry1, + Action: ActionFinalizing, + }) + assert.NilError(t, err) + assert.Assert(t, sentAction1, "dkg_sent_actions row should exist for retry 1 finalizing") + + // Chain emits DKGSucceeded for retry 1: HandleDKGSuccess must write + // dkg_result with a non-nil pure_result. + err = env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + return env.mgr.HandleDKGSuccess(ctx, tx, testKsi, retry1) + }) + assert.NilError(t, err) + + result, err := coreQueries.GetDKGResult(ctx, testKsi) + assert.NilError(t, err) + assert.Assert(t, result.Success, "dkg_result row must mark success") + assert.Assert(t, len(result.PureResult) > 0, "pure_result must be populated for a participating keyper") + assert.Assert(t, !result.Error.Valid, "error must be NULL when result was computed successfully") + + // Idempotency: second HandleDKGSuccess call is a no-op. + err = env.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + return env.mgr.HandleDKGSuccess(ctx, tx, testKsi, retry1) + }) + assert.NilError(t, err) +} diff --git a/rolling-shutter/dkg/manager.go b/rolling-shutter/dkg/manager.go new file mode 100644 index 000000000..edc4166da --- /dev/null +++ b/rolling-shutter/dkg/manager.go @@ -0,0 +1,431 @@ +package dkg + +import ( + "context" + "crypto/ecdsa" + "database/sql" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/shutter-network/shutter/shlib/puredkg" + + obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" +) + +// Action names recorded in the `dkg_sent_actions` table by each reactor on +// the success path. The table is the uniform idempotency store across all +// four reactors. +const ( + ActionDealing = "dealing" + ActionAccusing = "accusing" + ActionApologizing = "apologizing" + ActionFinalizing = "finalizing" +) + +// Config carries the dependencies needed to run a DKG Manager. The manager +// reads chain state exclusively through the database (populated by the host +// keyper's chain syncers) and writes both message rows and outbox tx intents +// to the same database. It never holds a chain client. +type Config struct { + // DBPool is the database connection pool. Required. + DBPool *pgxpool.Pool + // OwnAddress identifies the keyper for DKG membership checks. Required. + OwnAddress common.Address + // ECIESPrivateKey is the keyper's ECIES private key used to decrypt + // PolyEval messages addressed to us. + ECIESPrivateKey *ecies.PrivateKey + // ECIESRegistryAddr is the ECIES key registry contract address. Used as + // the destination for the `registerKey` outbox entry on first + // participation in a keyper set. + ECIESRegistryAddr common.Address +} + +// NewConfigFromECDSA is a small convenience wrapper that builds a Config +// from a standard ECDSA private key without the caller needing to import the +// `crypto/ecies` package itself. +func NewConfigFromECDSA( + dbPool *pgxpool.Pool, + ownAddress common.Address, + eciesECDSAPrivateKey *ecdsa.PrivateKey, + eciesRegistryAddr common.Address, +) Config { + return Config{ + DBPool: dbPool, + OwnAddress: ownAddress, + ECIESPrivateKey: eciesPrivateKeyFromECDSA(eciesECDSAPrivateKey), + ECIESRegistryAddr: eciesRegistryAddr, + } +} + +// Manager owns no goroutines, no chain subscriptions, and no in-memory +// caches. HandleBlock is the only entry point; it is called by the host +// keyper on each new block. +type Manager struct { + cfg Config +} + +// New constructs a Manager with the given configuration. +func New(cfg Config) *Manager { + return &Manager{cfg: cfg} +} + +// HandleBlock is called once per new block by the host keyper. It asks +// `activeDKGs` which eons this keyper must still consider at this block, then +// dispatches each one to `processDKG`. Errors from individual eons are logged +// but do not abort the loop — a per-eon failure must not stop the others. +func (m *Manager) HandleBlock(ctx context.Context, blockNumber uint64) error { + if blockNumber == 0 { + return nil + } + eons, summary, err := m.activeDKGs(ctx, blockNumber) + if err != nil { + return errors.Wrap(err, "list active DKGs") + } + log.Debug(). + Uint64("block-number", blockNumber). + Int("active", len(eons)). + Int("filtered-not-member", summary.notMember). + Int("filtered-succeeded", summary.succeeded). + Int("filtered-superseded", summary.superseded). + Int("filtered-retries-exhausted", summary.retriesExhausted). + Msg("DKG manager: active DKGs") + for _, eon := range eons { + if err := m.processDKG(ctx, eon, blockNumber); err != nil { + log.Error().Err(err). + Int64("keyper-set-index", eon.KeyperConfigIndex). + Uint64("block-number", blockNumber). + Msg("DKG manager: per-eon handler failed") + } + } + return nil +} + +// activeDKGsSummary counts how many eons were filtered out by each reason. +// Populated by `activeDKGs` and logged by `HandleBlock` as a single per-block +// summary line — there is no per-skip log line. +type activeDKGsSummary struct { + notMember int + succeeded int + superseded int + retriesExhausted int +} + +// activeDKGs returns the eons this keyper must still consider at `blockNumber`. +// It applies the "over or not mine" filters in one place so per-eon processing +// can focus on phase math. Filters, in the order applied — membership first +// because it is the most selective, then prior success, then supersession, +// then the retry ceiling: +// +// 1. Local keyper is a member of the eon's Keyper Set. +// 2. No `dkg_result` success row exists for the Keyper Set Index. +// 3. The Keyper Set is not superseded at this block (i.e. no later Keyper +// Set has already activated). See CONTEXT.md#superseded-keyper-set. +// 4. The current retry counter is below `MAX_RETRIES`. +// +// The supersession predicate reuses the observer's existing "latest keyper +// set with activation block <= N" query (`GetKeyperSet`). `pgx.ErrNoRows` from +// that query means "no Keyper Set is live yet" and no set is considered +// superseded — future scheduled sets remain active. +// +// Eons whose configuration is too incomplete to evaluate a filter (e.g. NULL +// `phase_length`) are passed through: `processDKG` will surface the config +// error, HandleBlock will log it. A single query joining eons + keyper sets + +// dkg_result would be more efficient, but keyper sets live in the observer +// schema (separate connection pool) so cross-schema joins are not possible. +// +// The returned summary counts, one per filter, are populated so HandleBlock +// can emit a single per-block Debug summary line without any per-skip log +// entries. On error the returned summary is zero-valued. +func (m *Manager) activeDKGs(ctx context.Context, blockNumber uint64) ([]corekeyperdb.Eon, activeDKGsSummary, error) { + queries := corekeyperdb.New(m.cfg.DBPool) + obsQueries := obskeyper.New(m.cfg.DBPool) + allEons, err := queries.GetAllEons(ctx) + if err != nil { + return nil, activeDKGsSummary{}, errors.Wrap(err, "list eons") + } + + // Resolve the "latest keyper set at this block" once. Its index is the + // supersession threshold: any Keyper Set with a strictly smaller index is + // superseded. `pgx.ErrNoRows` means no set is live yet, so nothing is + // superseded (future scheduled sets stay active). + var latestActiveIndex int64 + haveLatestActive := false + //nolint:gosec // G115: block numbers fit in int64 in practice. + latestActive, err := obsQueries.GetKeyperSet(ctx, int64(blockNumber)) + switch { + case err == nil: + latestActiveIndex = latestActive.KeyperConfigIndex + haveLatestActive = true + case errors.Is(err, pgx.ErrNoRows): + // No Keyper Set is live yet — no supersession possible. + default: + return nil, activeDKGsSummary{}, errors.Wrap(err, "fetch latest active keyper set") + } + + summary := activeDKGsSummary{} + active := make([]corekeyperdb.Eon, 0, len(allEons)) + for _, eon := range allEons { + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, eon.KeyperConfigIndex) + if err != nil { + return nil, activeDKGsSummary{}, errors.Wrapf(err, "fetch keyper set %d", eon.KeyperConfigIndex) + } + if _, err := keyperSet.GetIndex(m.cfg.OwnAddress); err != nil { + summary.notMember++ + continue + } + alreadySucceeded, err := queries.ExistsDKGResultSuccess(ctx, eon.KeyperConfigIndex) + if err != nil { + return nil, activeDKGsSummary{}, errors.Wrap(err, "check existing dkg_result") + } + if alreadySucceeded { + summary.succeeded++ + continue + } + if haveLatestActive && eon.KeyperConfigIndex < latestActiveIndex { + summary.superseded++ + continue + } + if eon.PhaseLength.Valid && eon.LeadLength.Valid { + activationBlock, err := medley.Int64ToUint64Safe(eon.ActivationBlockNumber) + if err != nil { + return nil, activeDKGsSummary{}, errors.Wrap(err, "convert activation block") + } + //nolint:gosec // G115: phase params come from the on-chain contract and are non-negative + phaseLength, leadLength, maxRetries := uint64(eon.PhaseLength.Int64), uint64(eon.LeadLength.Int64), uint64(eon.MaxRetries) + retry := CurrentRetryCounter(activationBlock, leadLength, phaseLength, blockNumber) + if retry >= maxRetries { + summary.retriesExhausted++ + continue + } + } + active = append(active, eon) + } + return active, summary, nil +} + +// processDKG runs the per-block phase math and dispatches at most one action +// for a single eon. It assumes `activeDKGs` has already filtered out eons +// that this keyper is not a member of, that have already succeeded, or whose +// retry counter has reached MAX_RETRIES. +// +// Pool queries (no transaction) fire first: phase params and the keyper-set +// lookup needed for `ownIndex`. Once dispatch is required, two narrow +// transactions are opened in sequence: a read-only one for `buildPureDKG`, +// then a separate write one for the maybe-function. Splitting the transactions +// keeps the read window short and lets the write transaction commit +// independently. A new chain event arriving between the two transactions is +// acceptable — the maybe-function will see the stale snapshot for one block +// and pick up the new state on the next dispatch. +// +// Returns nil for "nothing to do" (no active phase at this block, no initial +// state for non-Dealing phases). Returns an error for missing per-eon +// configuration (NULL `dkg_contract` or NULL `phase_length`/`lead_length`). +// The caller logs but does not abort on error. +func (m *Manager) processDKG(ctx context.Context, eon corekeyperdb.Eon, blockNumber uint64) error { + activationBlock, err := medley.Int64ToUint64Safe(eon.ActivationBlockNumber) + if err != nil { + return errors.Wrap(err, "convert activation block") + } + phaseLength, leadLength, maxRetries, err := m.phaseParamsForEon(eon) + if err != nil { + return err + } + + obsQueries := obskeyper.New(m.cfg.DBPool) + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, eon.KeyperConfigIndex) + if err != nil { + return errors.Wrapf(err, "fetch keyper set %d", eon.KeyperConfigIndex) + } + ownIndex, err := keyperSet.GetIndex(m.cfg.OwnAddress) + if err != nil { + // Belt-and-braces: activeDKGs already filters non-members, but if a + // caller invokes processDKG directly (tests, future diagnostics) we + // exit silently rather than surface a misleading error. + return nil + } + keypers, err := shdb.DecodeAddresses(keyperSet.Keypers) + if err != nil { + return errors.Wrap(err, "decode keyper addresses") + } + threshold, err := medley.Int32ToUint64Safe(keyperSet.Threshold) + if err != nil { + return errors.Wrap(err, "convert keyper set threshold") + } + + retry := CurrentRetryCounter(activationBlock, leadLength, phaseLength, blockNumber) + retryInt64 := int64(retry) //nolint:gosec // G115: retry counter is bounded by the on-chain contract + blockPhase := PhaseAt(activationBlock, leadLength, phaseLength, maxRetries, retry, blockNumber) + if blockPhase == PhaseNone { + return nil + } + dkgAddr, err := m.dkgContractAddrForEon(eon) + if err != nil { + return err + } + + // Read transaction: rebuild the puredkg snapshot. Reads only; commit and + // rollback are equivalent here, so we let BeginFunc commit on nil return. + var pure *puredkg.PureDKG + err = m.cfg.DBPool.BeginFunc(ctx, func(tx pgx.Tx) error { + p, err := m.buildPureDKG(ctx, tx, eon.KeyperConfigIndex, retryInt64, blockPhase, keypers, ownIndex, threshold) + if err != nil { + return errors.Wrap(err, "build puredkg") + } + pure = p + return nil + }) + if err != nil { + return err + } + if pure == nil { + return nil + } + + // Write transaction: dispatch to the maybe-function. Scope is narrow — + // only the outbox row, idempotency rows, and `dkg_initial_states` / + // `dkg_result` writes happen here. + return m.cfg.DBPool.BeginFunc(ctx, func(tx pgx.Tx) error { + switch blockPhase { + case PhaseDealing: + return m.maybeDeal(ctx, tx, dkgAddr, eon.KeyperConfigIndex, retryInt64, pure, keypers, ownIndex) + case PhaseAccusing: + return m.maybeAccuse(ctx, tx, dkgAddr, eon.KeyperConfigIndex, retryInt64, pure, keypers, ownIndex) + case PhaseApologizing: + return m.maybeApologize(ctx, tx, dkgAddr, eon.KeyperConfigIndex, retryInt64, pure, keypers, ownIndex) + case PhaseFinalizing: + return m.maybeFinalize(ctx, tx, dkgAddr, eon.KeyperConfigIndex, retryInt64, pure, ownIndex) + case PhaseNone: + return nil + default: + return nil + } + }) +} + +// phaseParamsForEon returns the DKG phase length, lead length, and retry +// ceiling (max_retries) for the given eon. Rows populated by +// `processNewKeyperSet` carry the values read from the keyper-set-specific +// DKG contract; rows with NULL phase_length or lead_length are a fatal +// configuration error — the module owns no chain client and there is no +// fallback to fetch them from. `max_retries` is a NOT NULL column, so it is +// always present; see the `MAX_RETRIES` glossary entry for its semantics. +func (m *Manager) phaseParamsForEon(eon corekeyperdb.Eon) (phaseLength, leadLength, maxRetries uint64, err error) { + if !eon.PhaseLength.Valid || !eon.LeadLength.Valid { + return 0, 0, 0, errors.Errorf( + "eons row %d missing DKG phase params (phase_length and/or lead_length is NULL)", + eon.KeyperConfigIndex, + ) + } + //nolint:gosec // G115: phase length, lead length, and max retries come from the on-chain contract and are non-negative + return uint64(eon.PhaseLength.Int64), uint64(eon.LeadLength.Int64), uint64(eon.MaxRetries), nil +} + +// dkgContractAddrForEon returns the on-chain DKG contract address responsible +// for the given eon. A NULL `dkg_contract` column is a fatal configuration +// error — there is no global fallback that could mask a misconfigured keyper +// set. +func (m *Manager) dkgContractAddrForEon(eon corekeyperdb.Eon) (common.Address, error) { + if !eon.DkgContract.Valid { + return common.Address{}, errors.Errorf( + "eons row %d missing DKG contract address (dkg_contract is NULL)", + eon.KeyperConfigIndex, + ) + } + return common.HexToAddress(eon.DkgContract.String), nil +} + +// HandleDKGSuccess is called by the chain-event handler when a DKGSucceeded +// event arrives on chain. It is the single writer of `dkg_result` rows: the +// chain event is the source of truth for which retry actually won. +// +// If this keyper participated in `retryCounter` it rebuilds the puredkg state +// and stores the computed result in `pure_result`; otherwise `pure_result` is +// nil. Both outcomes produce a `dkg_result` row with `success=true`. +func (m *Manager) HandleDKGSuccess(ctx context.Context, tx pgx.Tx, keyperSetIndex, retryCounter int64) error { + queries := corekeyperdb.New(tx) + exists, err := queries.ExistsDKGResultSuccess(ctx, keyperSetIndex) + if err != nil { + return errors.Wrap(err, "check existing dkg_result") + } + if exists { + return nil + } + + obsQueries := obskeyper.New(tx) + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, keyperSetIndex) + if err != nil { + return errors.Wrapf(err, "fetch keyper set %d", keyperSetIndex) + } + + var pureBytes []byte + var resultNumKeypers, resultThreshold uint64 + var accusationCount, apologyCount int + var hasResult bool + var localErr sql.NullString + + ownIndex, memberErr := keyperSet.GetIndex(m.cfg.OwnAddress) + if memberErr == nil { + keypers, err := shdb.DecodeAddresses(keyperSet.Keypers) + if err != nil { + return errors.Wrap(err, "decode keyper addresses") + } + threshold, err := medley.Int32ToUint64Safe(keyperSet.Threshold) + if err != nil { + return errors.Wrap(err, "convert threshold") + } + pure, err := m.buildPureDKG(ctx, tx, keyperSetIndex, retryCounter, PhaseFinalizing, keypers, ownIndex, threshold) + if err != nil { + return errors.Wrap(err, "rebuild puredkg for success") + } + if pure == nil { + localErr = sql.NullString{String: "local: buildPureDKG returned nil", Valid: true} + } else { + pure.Finalize() + result, err := pure.ComputeResult() + if err != nil { + localErr = sql.NullString{String: fmt.Sprintf("local: compute result failed: %s", err), Valid: true} + log.Warn().Err(err). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Msg("cannot compute DKG result on success event; storing nil") + } else { + hasResult = true + resultNumKeypers = result.NumKeypers + resultThreshold = result.Threshold + accusationCount = len(pure.Accusations) + apologyCount = len(pure.Apologies) + pureBytes, err = shdb.EncodePureDKGResult(&result) + if err != nil { + return errors.Wrap(err, "encode pure DKG result") + } + } + } + } + + event := log.Info(). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter) + if hasResult { + event = event. + Uint64("keyper-count", resultNumKeypers). + Uint64("threshold", resultThreshold). + Int("accusation-count", accusationCount). + Int("apology-count", apologyCount) + } + event.Msg("DKG succeeded") + return queries.InsertDKGResult(ctx, corekeyperdb.InsertDKGResultParams{ + Eon: keyperSetIndex, + Success: true, + Error: localErr, + PureResult: pureBytes, + }) +} diff --git a/rolling-shutter/dkg/manager_test.go b/rolling-shutter/dkg/manager_test.go new file mode 100644 index 000000000..6983941ab --- /dev/null +++ b/rolling-shutter/dkg/manager_test.go @@ -0,0 +1,512 @@ +package dkg + +import ( + "context" + "database/sql" + "fmt" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/jackc/pgx/v4" + "gotest.tools/v3/assert" + + obskeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/testsetup" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" +) + +// TestProcessDKGReturnsErrorWhenDkgContractIsNull asserts the strict-config +// acceptance criterion: an eons row with NULL `dkg_contract` causes +// processDKG to return a non-nil error and write no tx_outbox row. +func TestProcessDKGReturnsErrorWhenDkgContractIsNull(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const ( + keyperConfigIndex int64 = 7 + phaseLength int64 = 10 + leadLength int64 = 2 + blockNumber uint64 = 5 // within Dealing phase + ) + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + + coreQueries := corekeyperdb.New(dbpool) + err = coreQueries.InsertEon(ctx, corekeyperdb.InsertEonParams{ + Eon: keyperConfigIndex, + ActivationBlockNumber: 0, + KeyperConfigIndex: keyperConfigIndex, + // DkgContract intentionally NULL. + PhaseLength: sql.NullInt64{Int64: phaseLength, Valid: true}, + LeadLength: sql.NullInt64{Int64: leadLength, Valid: true}, + MaxRetries: 10, + }) + assert.NilError(t, err) + + // Membership check fires before dkg_contract validation, so the keyper + // set must include our address for the dkg_contract error to surface. + obsQueries := obskeyperdb.New(dbpool) + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: keyperConfigIndex, + ActivationBlockNumber: 0, + Keypers: shdb.EncodeAddresses([]common.Address{ownAddr}), + Threshold: 1, + }) + assert.NilError(t, err) + + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + eon, err := coreQueries.GetEon(ctx, keyperConfigIndex) + assert.NilError(t, err) + + err = mgr.processDKG(ctx, eon, blockNumber) + assert.Assert(t, err != nil, "expected error when dkg_contract is NULL") + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 0, len(pending), "no tx_outbox row may be written when dkg_contract is NULL") +} + +// TestProcessDKGReturnsErrorWhenPhaseParamsAreNull asserts the strict-config +// acceptance criterion: an eons row with NULL `phase_length` and/or NULL +// `lead_length` causes processDKG to return a non-nil error and write no +// tx_outbox row. +func TestProcessDKGReturnsErrorWhenPhaseParamsAreNull(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const ( + keyperConfigIndex int64 = 8 + blockNumber uint64 = 5 + ) + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + + coreQueries := corekeyperdb.New(dbpool) + err = coreQueries.InsertEon(ctx, corekeyperdb.InsertEonParams{ + Eon: keyperConfigIndex, + ActivationBlockNumber: 0, + KeyperConfigIndex: keyperConfigIndex, + DkgContract: sql.NullString{String: "0xd0000000000000000000000000000000000000aa", Valid: true}, + // PhaseLength and LeadLength intentionally NULL. + MaxRetries: 10, + }) + assert.NilError(t, err) + + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + eon, err := coreQueries.GetEon(ctx, keyperConfigIndex) + assert.NilError(t, err) + + err = mgr.processDKG(ctx, eon, blockNumber) + assert.Assert(t, err != nil, "expected error when phase_length/lead_length is NULL") + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 0, len(pending), "no tx_outbox row may be written when phase params are NULL") +} + +// TestHandleBlockSkipsEonWhenNotAMember asserts the membership-filter +// acceptance criterion: HandleBlock returns nil and writes no tx_outbox row +// when the manager's address is not present in the keyper set, even though +// the eon row is otherwise fully configured (dkg_contract + phase params +// set) and the block falls inside the Dealing phase. The filter now lives in +// `activeDKGs`, so we exercise the full `HandleBlock` path. +func TestHandleBlockSkipsEonWhenNotAMember(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const ( + keyperConfigIndex int64 = 11 + phaseLength int64 = 10 + leadLength int64 = 2 + blockNumber uint64 = 5 // within Dealing phase + ) + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + + // Two other keypers — deliberately NOT including ownAddr. + other0, err := crypto.GenerateKey() + assert.NilError(t, err) + other0Addr := crypto.PubkeyToAddress(other0.PublicKey) + other1, err := crypto.GenerateKey() + assert.NilError(t, err) + other1Addr := crypto.PubkeyToAddress(other1.PublicKey) + + coreQueries := corekeyperdb.New(dbpool) + err = coreQueries.InsertEon(ctx, corekeyperdb.InsertEonParams{ + Eon: keyperConfigIndex, + ActivationBlockNumber: 0, + KeyperConfigIndex: keyperConfigIndex, + DkgContract: sql.NullString{String: "0xd0000000000000000000000000000000000000aa", Valid: true}, + PhaseLength: sql.NullInt64{Int64: phaseLength, Valid: true}, + LeadLength: sql.NullInt64{Int64: leadLength, Valid: true}, + MaxRetries: 10, + }) + assert.NilError(t, err) + + obsQueries := obskeyperdb.New(dbpool) + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: keyperConfigIndex, + ActivationBlockNumber: 0, + Keypers: shdb.EncodeAddresses([]common.Address{other0Addr, other1Addr}), + Threshold: 1, + }) + assert.NilError(t, err) + + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + err = mgr.HandleBlock(ctx, blockNumber) + assert.NilError(t, err, "HandleBlock must return nil when the keyper is filtered out") + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 0, len(pending), "no tx_outbox row may be written when not a member") +} + +// TestProcessDKGWritesNothingPastMaxRetries is the end-to-end wire-through +// acceptance test for the MAX_RETRIES ceiling: an eons row with +// `max_retries = 2` plus a block advanced into what would be retry 3's +// Dealing window must produce zero `tx_outbox` and zero `dkg_sent_actions` +// rows. The retry-ceiling filter now lives in `activeDKGs`, and `PhaseAt` +// retains its `retryCounter >= maxRetries` guard as belt-and-braces — either +// gate suffices on its own, so exercising `processDKG` directly still yields +// a no-op. +func TestProcessDKGWritesNothingPastMaxRetries(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const ( + keyperConfigIndex int64 = 13 + activationBlock int64 = 100 + phaseLength int64 = 10 + leadLength int64 = 40 + maxRetries int64 = 2 + // Cycle length = 4 * phaseLength = 40. DKG start for retry 0 is + // activationBlock - leadLength = 60. Block 185 lands in retry 3's + // Dealing window ([180, 190)) — retry counter 3 >= maxRetries 2, so + // PhaseAt must return PhaseNone and processDKG must be a no-op. + blockNumber uint64 = 185 + ) + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + + coreQueries := corekeyperdb.New(dbpool) + err = coreQueries.InsertEon(ctx, corekeyperdb.InsertEonParams{ + Eon: keyperConfigIndex, + ActivationBlockNumber: activationBlock, + KeyperConfigIndex: keyperConfigIndex, + DkgContract: sql.NullString{String: "0xd0000000000000000000000000000000000000aa", Valid: true}, + PhaseLength: sql.NullInt64{Int64: phaseLength, Valid: true}, + LeadLength: sql.NullInt64{Int64: leadLength, Valid: true}, + MaxRetries: maxRetries, + }) + assert.NilError(t, err) + + obsQueries := obskeyperdb.New(dbpool) + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: keyperConfigIndex, + ActivationBlockNumber: activationBlock, + Keypers: shdb.EncodeAddresses([]common.Address{ownAddr}), + Threshold: 1, + }) + assert.NilError(t, err) + + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + eon, err := coreQueries.GetEon(ctx, keyperConfigIndex) + assert.NilError(t, err) + + // Sanity: without the max-retries gate, retry 3's Dealing window would + // otherwise trigger a dispatch. Verify the retry counter arithmetic is + // what the test intends. + retry := CurrentRetryCounter(uint64(activationBlock), uint64(leadLength), uint64(phaseLength), blockNumber) + assert.Equal(t, uint64(3), retry, "test setup expects retry counter 3 at this block") + + err = mgr.processDKG(ctx, eon, blockNumber) + assert.NilError(t, err, "processDKG must return nil (no error) once past max_retries") + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + assert.Equal(t, 0, len(pending), "no tx_outbox row may be written once retry counter >= max_retries") + + for _, action := range []string{ActionDealing, ActionAccusing, ActionApologizing, ActionFinalizing} { + //nolint:gosec // G115: retry counter fits well within int64 + sentAction, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: keyperConfigIndex, + RetryCounter: int64(retry), + Action: action, + }) + assert.NilError(t, err) + assert.Assert(t, !sentAction, + "no dkg_sent_actions row may be written for action=%s once past max_retries", action) + } +} + +// TestHandleBlockSkipsSupersededKeyperSet is the end-to-end wire-through for +// the supersession filter (Seam 2 case a): two Keyper Sets with activation +// blocks 100 and 120; advancing to a block past the successor's activation +// must leave the older set silent — no `tx_outbox` row and no +// `dkg_sent_actions` row for any of the four phases. +func TestHandleBlockSkipsSupersededKeyperSet(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const ( + olderIndex int64 = 1 + newerIndex int64 = 2 + olderActivation int64 = 100 + newerActivation int64 = 120 + phaseLength int64 = 10 + leadLength int64 = 2 + maxRetries int64 = 10 + // blockNumber lands in what would be the older set's retry-1 Dealing + // window (retry 0 dealing at [98, 108), retry 1 dealing at [138, 148)). + // 130 is past the successor's activation (120), so supersession must + // keep the older set silent even though a naive read of the phase + // arithmetic would place the older set outside PhaseNone at 138+. + blockNumber uint64 = 138 + ) + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + + coreQueries := corekeyperdb.New(dbpool) + for _, e := range []struct { + idx int64 + activation int64 + }{ + {olderIndex, olderActivation}, + {newerIndex, newerActivation}, + } { + err = coreQueries.InsertEon(ctx, corekeyperdb.InsertEonParams{ + Eon: e.idx, + ActivationBlockNumber: e.activation, + KeyperConfigIndex: e.idx, + DkgContract: sql.NullString{String: "0xd0000000000000000000000000000000000000aa", Valid: true}, + PhaseLength: sql.NullInt64{Int64: phaseLength, Valid: true}, + LeadLength: sql.NullInt64{Int64: leadLength, Valid: true}, + MaxRetries: maxRetries, + }) + assert.NilError(t, err) + } + + obsQueries := obskeyperdb.New(dbpool) + for _, e := range []struct { + idx int64 + activation int64 + }{ + {olderIndex, olderActivation}, + {newerIndex, newerActivation}, + } { + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: e.idx, + ActivationBlockNumber: e.activation, + Keypers: shdb.EncodeAddresses([]common.Address{ownAddr}), + Threshold: 1, + }) + assert.NilError(t, err) + } + + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + err = mgr.HandleBlock(ctx, blockNumber) + assert.NilError(t, err) + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + olderLabelMarker := fmt.Sprintf("ksi=%d ", olderIndex) + for _, tx := range pending { + assert.Assert(t, !strings.Contains(tx.Label, olderLabelMarker), + "no tx_outbox row may be written for the superseded older set (label=%q)", tx.Label) + } + + for retry := int64(0); retry < maxRetries; retry++ { + for _, action := range []string{ActionDealing, ActionAccusing, ActionApologizing, ActionFinalizing} { + sent, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: olderIndex, + RetryCounter: retry, + Action: action, + }) + assert.NilError(t, err) + assert.Assert(t, !sent, + "no dkg_sent_actions row may be written for superseded set (retry=%d, action=%s)", retry, action) + } + } +} + +// TestHandleDKGSuccessOnSupersededSetStillWritesResult is the end-to-end +// wire-through for the peer-driven success case (Seam 2 case b): after the +// successor has activated, a `DKGSucceeded` event arriving for the older +// (superseded) set must still write the `dkg_result` row exactly as it does +// today, and the next HandleBlock must still produce no submissions for the +// older set. +func TestHandleDKGSuccessOnSupersededSetStillWritesResult(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const ( + olderIndex int64 = 1 + newerIndex int64 = 2 + olderActivation int64 = 100 + newerActivation int64 = 120 + phaseLength int64 = 10 + leadLength int64 = 2 + maxRetries int64 = 10 + blockNumber uint64 = 138 + retryCounter int64 = 0 + ) + + ownECDSA, err := crypto.GenerateKey() + assert.NilError(t, err) + ownAddr := crypto.PubkeyToAddress(ownECDSA.PublicKey) + + coreQueries := corekeyperdb.New(dbpool) + for _, e := range []struct { + idx int64 + activation int64 + }{ + {olderIndex, olderActivation}, + {newerIndex, newerActivation}, + } { + err = coreQueries.InsertEon(ctx, corekeyperdb.InsertEonParams{ + Eon: e.idx, + ActivationBlockNumber: e.activation, + KeyperConfigIndex: e.idx, + DkgContract: sql.NullString{String: "0xd0000000000000000000000000000000000000aa", Valid: true}, + PhaseLength: sql.NullInt64{Int64: phaseLength, Valid: true}, + LeadLength: sql.NullInt64{Int64: leadLength, Valid: true}, + MaxRetries: maxRetries, + }) + assert.NilError(t, err) + } + + obsQueries := obskeyperdb.New(dbpool) + for _, e := range []struct { + idx int64 + activation int64 + }{ + {olderIndex, olderActivation}, + {newerIndex, newerActivation}, + } { + err = obsQueries.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: e.idx, + ActivationBlockNumber: e.activation, + Keypers: shdb.EncodeAddresses([]common.Address{ownAddr}), + Threshold: 1, + }) + assert.NilError(t, err) + } + + mgr := New(Config{ + DBPool: dbpool, + OwnAddress: ownAddr, + ECIESPrivateKey: ecies.ImportECDSA(ownECDSA), + ECIESRegistryAddr: common.HexToAddress("0xe0000000000000000000000000000000000000bb"), + }) + + // Peer-driven success on the (already superseded) older set. + err = dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + return mgr.HandleDKGSuccess(ctx, tx, olderIndex, retryCounter) + }) + assert.NilError(t, err) + + succeeded, err := coreQueries.ExistsDKGResultSuccess(ctx, olderIndex) + assert.NilError(t, err) + assert.Assert(t, succeeded, "HandleDKGSuccess must write dkg_result row even for a superseded set") + + // Next HandleBlock must still be a no-op for the older set: the + // "already-succeeded" filter now also excludes it, but the supersession + // filter alone was already enough. + err = mgr.HandleBlock(ctx, blockNumber) + assert.NilError(t, err) + + pending, err := coreQueries.GetPendingTxs(ctx) + assert.NilError(t, err) + olderLabelMarker := fmt.Sprintf("ksi=%d ", olderIndex) + for _, tx := range pending { + assert.Assert(t, !strings.Contains(tx.Label, olderLabelMarker), + "no tx_outbox row may be written for the superseded older set after peer-driven success (label=%q)", tx.Label) + } + + for retry := int64(0); retry < maxRetries; retry++ { + for _, action := range []string{ActionDealing, ActionAccusing, ActionApologizing, ActionFinalizing} { + sent, err := coreQueries.ExistsDKGSentAction(ctx, corekeyperdb.ExistsDKGSentActionParams{ + KeyperSetIndex: olderIndex, + RetryCounter: retry, + Action: action, + }) + assert.NilError(t, err) + assert.Assert(t, !sent, + "no dkg_sent_actions row may be written for superseded set (retry=%d, action=%s)", retry, action) + } + } +} diff --git a/rolling-shutter/dkg/phase.go b/rolling-shutter/dkg/phase.go new file mode 100644 index 000000000..354b03d17 --- /dev/null +++ b/rolling-shutter/dkg/phase.go @@ -0,0 +1,109 @@ +// Package dkg implements the DKG participation logic as a database-driven +// reactor. The host keyper calls HandleBlock on every new block; the manager +// iterates all active eons and writes any required actions (DKG message rows +// and tx_outbox entries) back to the database. The package owns no chainsync +// subscriptions and makes no live chain calls — see ADR 0004. +package dkg + +// Phase mirrors the on-chain enum defined in DKGContract.sol. The numeric +// values match the contract so that values returned by `DKGContract.currentPhase` +// can be compared directly. PhaseNone is used for blocks before the dealing +// window starts (negative offset) and for blocks past the finalizing window. +type Phase uint8 + +const ( + PhaseNone Phase = iota + PhaseDealing + PhaseAccusing + PhaseApologizing + PhaseFinalizing +) + +func (p Phase) String() string { + switch p { + case PhaseNone: + return "None" + case PhaseDealing: + return "Dealing" + case PhaseAccusing: + return "Accusing" + case PhaseApologizing: + return "Apologizing" + case PhaseFinalizing: + return "Finalizing" + default: + return "Unknown" + } +} + +// DKGStart returns the first block number at which the DKG instance +// `(keyperSetIndex, retryCounter)` enters its Dealing phase. The formula +// matches `DKGContract.dkgStart` in DKGContract.sol; using int64 internally +// allows the result to be negative when the activation block is smaller than +// the lead length (which the on-chain contract handles via int256). +func DKGStart(activationBlock, dkgLeadLength, phaseLength uint64, retryCounter uint64) int64 { + cycle := CycleLength(phaseLength) + //nolint:gosec // G115: block numbers and DKG params fit well within int64 + return int64(activationBlock) - int64(dkgLeadLength) + int64(retryCounter)*int64(cycle) +} + +// CycleLength returns the length of a full DKG cycle (one attempt across all +// four phases) for the given per-phase length. +func CycleLength(phaseLength uint64) uint64 { + return 4 * phaseLength +} + +// PhaseAt returns the DKG phase of the instance `(activationBlock, retryCounter)` +// at the given block number. Blocks before the dealing window or past the +// finalizing window return PhaseNone. The boundaries are half-open: a phase +// covers `[start + n*phaseLength, start + (n+1)*phaseLength)` for n = 0..3. +// +// `maxRetries` is the on-chain retry ceiling for this keyper set (see the +// `MAX_RETRIES` glossary entry): the valid retry-counter range is +// `{0, ..., maxRetries - 1}`. Any `retryCounter >= maxRetries` returns +// PhaseNone before any window arithmetic. `maxRetries` is positioned before +// `retryCounter` to mirror the DKG contract's argument ordering. +func PhaseAt(activationBlock, dkgLeadLength, phaseLength, maxRetries, retryCounter, currentBlock uint64) Phase { + if retryCounter >= maxRetries { + return PhaseNone + } + if phaseLength == 0 { + return PhaseNone + } + start := DKGStart(activationBlock, dkgLeadLength, phaseLength, retryCounter) + offset := int64(currentBlock) - start //nolint:gosec // G115: block number fits well within int64 + if offset < 0 { + return PhaseNone + } + pl := int64(phaseLength) //nolint:gosec // G115: phase length fits well within int64 + switch { + case offset < pl: + return PhaseDealing + case offset < 2*pl: + return PhaseAccusing + case offset < 3*pl: + return PhaseApologizing + case offset < 4*pl: + return PhaseFinalizing + default: + return PhaseNone + } +} + +// CurrentRetryCounter derives the active retry counter from block arithmetic. +// Each failed cycle advances the counter by one; the counter is never stored +// in the database. A block before `DKGStart(..., 0)` returns 0 since the +// loop has not begun yet (matching the contract's behavior of treating early +// blocks as "Phase.None" within retry 0). +func CurrentRetryCounter(activationBlock, dkgLeadLength, phaseLength, currentBlock uint64) uint64 { + cycle := CycleLength(phaseLength) + if cycle == 0 { + return 0 + } + start := DKGStart(activationBlock, dkgLeadLength, phaseLength, 0) + offset := int64(currentBlock) - start //nolint:gosec // G115: block number fits well within int64 + if offset < 0 { + return 0 + } + return uint64(offset) / cycle +} diff --git a/rolling-shutter/dkg/phase_test.go b/rolling-shutter/dkg/phase_test.go new file mode 100644 index 000000000..b143b251d --- /dev/null +++ b/rolling-shutter/dkg/phase_test.go @@ -0,0 +1,162 @@ +package dkg + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +// maxRetriesForTests is a generous ceiling used by tests that do not +// themselves exercise the retry-limit gate. It matches the deploy-time +// default (`DKG_MAX_RETRIES = 10`) and keeps existing per-window assertions +// unaffected by the `retryCounter >= maxRetries` guard added in ticket 02. +const maxRetriesForTests uint64 = 10 + +func TestPhaseAtBoundaries(t *testing.T) { + // Choose parameters where the lead length leaves room for a positive + // dealing-start block so the boundaries are easy to reason about. + const ( + activationBlock uint64 = 1000 + dkgLeadLength uint64 = 40 + phaseLength uint64 = 10 + ) + // Retry 0: dealing starts at block 960 (1000 - 40). + start := DKGStart(activationBlock, dkgLeadLength, phaseLength, 0) + assert.Equal(t, int64(960), start) + + cases := []struct { + name string + block uint64 + want Phase + }{ + {"before dealing start", 959, PhaseNone}, + {"dealing first block", 960, PhaseDealing}, + {"dealing last block", 969, PhaseDealing}, + {"accusing first block", 970, PhaseAccusing}, + {"accusing last block", 979, PhaseAccusing}, + {"apologizing first block", 980, PhaseApologizing}, + {"apologizing last block", 989, PhaseApologizing}, + {"finalizing first block", 990, PhaseFinalizing}, + {"finalizing last block", 999, PhaseFinalizing}, + {"activation block (already past finalizing of retry 0)", 1000, PhaseDealing}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + retry := CurrentRetryCounter(activationBlock, dkgLeadLength, phaseLength, tc.block) + got := PhaseAt(activationBlock, dkgLeadLength, phaseLength, maxRetriesForTests, retry, tc.block) + assert.Equal(t, tc.want, got, "block=%d, retry=%d", tc.block, retry) + }) + } +} + +func TestPhaseAtAcrossRetries(t *testing.T) { + const ( + activationBlock uint64 = 1000 + dkgLeadLength uint64 = 40 + phaseLength uint64 = 10 + ) + cycle := CycleLength(phaseLength) // 40 + + // Retry 1 starts at block 1000 (960 + 40). + assert.Equal(t, int64(1000), DKGStart(activationBlock, dkgLeadLength, phaseLength, 1)) + + r := CurrentRetryCounter(activationBlock, dkgLeadLength, phaseLength, 1000) + assert.Equal(t, uint64(1), r) + assert.Equal(t, PhaseDealing, PhaseAt(activationBlock, dkgLeadLength, phaseLength, maxRetriesForTests, r, 1000)) + + r = CurrentRetryCounter(activationBlock, dkgLeadLength, phaseLength, 1039) + assert.Equal(t, uint64(1), r) + assert.Equal(t, PhaseFinalizing, PhaseAt(activationBlock, dkgLeadLength, phaseLength, maxRetriesForTests, r, 1039)) + + r = CurrentRetryCounter(activationBlock, dkgLeadLength, phaseLength, 1040) + assert.Equal(t, uint64(2), r) + assert.Equal(t, PhaseDealing, PhaseAt(activationBlock, dkgLeadLength, phaseLength, maxRetriesForTests, r, 1040)) + + assert.Equal(t, + DKGStart(activationBlock, dkgLeadLength, phaseLength, 0)+2*int64(cycle), //nolint:gosec // G115: cycle length fits well within int64 + DKGStart(activationBlock, dkgLeadLength, phaseLength, 2), + ) +} + +func TestPhaseAtPastInstance(t *testing.T) { + const ( + activationBlock uint64 = 1000 + dkgLeadLength uint64 = 40 + phaseLength uint64 = 10 + ) + got := PhaseAt(activationBlock, dkgLeadLength, phaseLength, maxRetriesForTests, 0, 1000) + assert.Equal(t, PhaseNone, got) +} + +func TestCurrentRetryCounterBeforeStart(t *testing.T) { + const ( + activationBlock uint64 = 1000 + dkgLeadLength uint64 = 40 + phaseLength uint64 = 10 + ) + for _, block := range []uint64{0, 500, 959} { + r := CurrentRetryCounter(activationBlock, dkgLeadLength, phaseLength, block) + assert.Equal(t, uint64(0), r, "block=%d", block) + } +} + +func TestPhaseAtZeroPhaseLength(t *testing.T) { + assert.Equal(t, PhaseNone, PhaseAt(1000, 40, 0, maxRetriesForTests, 0, 1000)) + assert.Equal(t, uint64(0), CurrentRetryCounter(1000, 40, 0, 1000)) +} + +// TestPhaseAtRetryCounterAtOrAboveMaxRetries covers the new retry-ceiling +// gate: PhaseAt returns PhaseNone for any retryCounter >= maxRetries, across +// representative block numbers inside each of the four sub-windows. This +// mirrors the on-chain rule enforced by ticket 01. +func TestPhaseAtRetryCounterAtOrAboveMaxRetries(t *testing.T) { + const ( + activationBlock uint64 = 1000 + dkgLeadLength uint64 = 40 + phaseLength uint64 = 10 + maxRetries uint64 = 2 + ) + // Retry 2 starts at block 1040 (960 + 2*40). Sample one block per phase + // inside retry 2's cycle and one well past it, plus a retry > maxRetries. + sampleBlocks := []uint64{1040, 1050, 1060, 1070, 5000} + for _, retryCounter := range []uint64{maxRetries, maxRetries + 1, maxRetries + 5, 100} { + for _, block := range sampleBlocks { + got := PhaseAt(activationBlock, dkgLeadLength, phaseLength, maxRetries, retryCounter, block) + assert.Equal(t, PhaseNone, got, + "retryCounter=%d, maxRetries=%d, block=%d", retryCounter, maxRetries, block) + } + } +} + +// TestPhaseAtRetryCounterBelowMaxRetries verifies that the existing per-window +// behavior is preserved for any retryCounter < maxRetries. This is the +// mirror-case of TestPhaseAtRetryCounterAtOrAboveMaxRetries. +func TestPhaseAtRetryCounterBelowMaxRetries(t *testing.T) { + const ( + activationBlock uint64 = 1000 + dkgLeadLength uint64 = 40 + phaseLength uint64 = 10 + maxRetries uint64 = 3 + ) + cases := []struct { + name string + retryCounter uint64 + block uint64 + want Phase + }{ + {"retry 0 dealing", 0, 960, PhaseDealing}, + {"retry 0 finalizing", 0, 990, PhaseFinalizing}, + {"retry 1 dealing", 1, 1000, PhaseDealing}, + {"retry 1 apologizing", 1, 1020, PhaseApologizing}, + {"retry 2 dealing", 2, 1040, PhaseDealing}, + {"retry 2 accusing", 2, 1050, PhaseAccusing}, + {"retry 2 finalizing", 2, 1070, PhaseFinalizing}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := PhaseAt(activationBlock, dkgLeadLength, phaseLength, maxRetries, tc.retryCounter, tc.block) + assert.Equal(t, tc.want, got, + "retryCounter=%d, maxRetries=%d, block=%d", tc.retryCounter, maxRetries, tc.block) + }) + } +} diff --git a/rolling-shutter/dkg/puredkg.go b/rolling-shutter/dkg/puredkg.go new file mode 100644 index 000000000..3df0ccef1 --- /dev/null +++ b/rolling-shutter/dkg/puredkg.go @@ -0,0 +1,338 @@ +package dkg + +import ( + "context" + "crypto/ecdsa" + "crypto/rand" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/jackc/pgx/v4" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/shutter-network/shutter/shlib/puredkg" + "github.com/shutter-network/shutter/shlib/shcrypto" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" +) + +// ReceiverIndicesForSender returns the keyper indices that should be receivers +// of polyEvals submitted by `senderIndex` within a keyper set of size `n`: +// all indices in [0, n) except `senderIndex`, in ascending order. +func ReceiverIndicesForSender(n, senderIndex uint64) []uint64 { + out := make([]uint64, 0, n-1) + for i := uint64(0); i < n; i++ { + if i == senderIndex { + continue + } + out = append(out, i) + } + return out +} + +// buildPureDKG reconstructs the in-memory puredkg state for a single DKG +// attempt at the phase the corresponding maybe-function expects to operate +// in. The DB is the source of truth; nothing is cached between invocations. +// +// The caller is responsible for the keyper-set lookup and membership check +// (both run as pool queries inside `processDKG` before this function is +// entered). `keypers`, `ownIndex`, and `threshold` are passed in as +// parameters so this function performs no observer-db reads. +// +// Returns (nil, nil) when this keyper has no `dkg_initial_states` row for a +// non-Dealing phase (Keyper crashed before dealing — they cannot +// accuse/apologize/finalize via local replay because the polynomial is +// unrecoverable). +// +// Phase-by-phase reconstruction: +// +// - PhaseDealing: fresh PureDKG (Phase=Off). Apply already-received +// PolyCommitment + PolyEval messages. The caller (`maybeDeal`) decides +// whether to call StartPhase1Dealing based on the dkg_initial_states +// idempotency check. +// - PhaseAccusing: load from dkg_initial_states (Phase=Dealing, polynomial +// set, self-eval populated). Apply PolyCommitment + PolyEval messages. +// The caller (`maybeAccuse`) calls StartPhase2Accusing. +// - PhaseApologizing: load from dkg_initial_states. Apply PolyCommitment + +// PolyEval. Fast-forward via StartPhase2Accusing (output discarded — +// accusations were already committed to chain). Apply Accusation +// messages. The caller (`maybeApologize`) calls StartPhase3Apologizing. +// - PhaseFinalizing: load from dkg_initial_states. Apply PolyCommitment + +// PolyEval. Fast-forward via StartPhase2Accusing + StartPhase3Apologizing +// (polynomial alive from the initial state makes Phase3 a no-op for +// accusations not addressed to us). Apply Apology messages. The caller +// sets Phase=Finalized directly and calls ComputeResult. +// +// Messages are applied in phase order: each `Start*` call is interleaved +// before the next message type so puredkg's per-handler phase guards are +// satisfied (`HandleAccusationMsg` requires Phase ≤ Accusing, +// `HandleApologyMsg` requires Phase ≤ Apologizing). +func (m *Manager) buildPureDKG( + ctx context.Context, + tx pgx.Tx, + keyperSetIndex, retryCounter int64, + blockPhase Phase, + keypers []common.Address, + ownIndex uint64, + threshold uint64, +) (*puredkg.PureDKG, error) { + queries := corekeyperdb.New(tx) + + var pure *puredkg.PureDKG + switch blockPhase { + case PhaseDealing: + p := puredkg.NewPureDKG( + uint64(keyperSetIndex), //nolint:gosec // G115: keyper set index is bounded by the on-chain contract + uint64(len(keypers)), + threshold, + ownIndex, + ) + pure = &p + case PhaseAccusing, PhaseApologizing, PhaseFinalizing: + row, err := queries.GetDKGInitialState(ctx, corekeyperdb.GetDKGInitialStateParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, nil + } + return nil, errors.Wrap(err, "load dkg initial state") + } + pure, err = shdb.DecodePureDKG(row.PuredkgBytes) + if err != nil { + return nil, errors.Wrap(err, "decode dkg initial state") + } + // gob converts nil pointers in `[]*shcrypto.Gammas` / `[]*big.Int` + // slices into non-nil zero-value pointers on decode (via the + // elements' GobDecoder), which would defeat puredkg's + // "duplicate msg" check during the subsequent DB replay. Reset + // the slices, then re-derive the self-eval and self-commitment + // from the loaded polynomial — both are fully determined by the + // polynomial, so we do not depend on chain-syncer indexing of + // our own submitDealing event to populate these slots. + pure.Commitments = make([]*shcrypto.Gammas, pure.NumKeypers) + pure.Evals = make([]*big.Int, pure.NumKeypers) + //nolint:gosec // G115: keyper index is bounded by the keyper set size + pure.Evals[pure.Keyper] = pure.Polynomial.EvalForKeyper(int(pure.Keyper)) + pure.Commitments[pure.Keyper] = pure.Polynomial.Gammas() + case PhaseNone: + return nil, nil + default: + return nil, nil + } + + if err := m.replayCommitmentsAndEvals(ctx, queries, pure, ownIndex, keyperSetIndex, retryCounter); err != nil { + return nil, err + } + + if blockPhase == PhaseDealing || blockPhase == PhaseAccusing { + return pure, nil + } + + // Fast-forward into Accusing so HandleAccusationMsg accepts the stored + // rows. The output is discarded — the on-chain accusations are the + // authoritative copy. + _ = pure.StartPhase2Accusing() + if err := m.replayAccusations(ctx, queries, pure, keyperSetIndex, retryCounter); err != nil { + return nil, err + } + + if blockPhase == PhaseApologizing { + return pure, nil + } + + // PhaseFinalizing: fast-forward into Apologizing and replay apologies. + // StartPhase3Apologizing reads pure.Polynomial — which is alive because + // it was loaded from dkg_initial_states. + _ = pure.StartPhase3Apologizing() + if err := m.replayApologies(ctx, queries, pure, keyperSetIndex, retryCounter); err != nil { + return nil, err + } + return pure, nil +} + +// replayCommitmentsAndEvals feeds stored PolyCommitment and PolyEval rows +// back into `pure`. Both handlers require Phase ≤ Dealing; the caller is +// responsible for the puredkg being at that phase or below. +// +// Duplicate-row errors (e.g. our own PolyCommitment row indexed by the +// chain syncer after the on-chain submitDealing event, which collides +// with `pure.Commitments[ownIndex]` already derived from the polynomial) +// are logged at debug level and ignored. +func (m *Manager) replayCommitmentsAndEvals( + ctx context.Context, + queries *corekeyperdb.Queries, + pure *puredkg.PureDKG, + ownIndex uint64, + keyperSetIndex, retryCounter int64, +) error { + eonForMsg := uint64(keyperSetIndex) //nolint:gosec // G115: keyper set index is bounded by the on-chain contract + + commitments, err := queries.GetDKGPolyCommitments(ctx, corekeyperdb.GetDKGPolyCommitmentsParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + }) + if err != nil { + return errors.Wrap(err, "load stored poly commitments") + } + for _, c := range commitments { + gammas := &shcrypto.Gammas{} + if err := gammas.Unmarshal(c.Commitment); err != nil { + return errors.Wrapf(err, "decode commitment from sender %d", c.KeyperIndex) + } + err := pure.HandlePolyCommitmentMsg(puredkg.PolyCommitmentMsg{ + Eon: eonForMsg, + Sender: uint64(c.KeyperIndex), //nolint:gosec // G115: keyper index is bounded by the keyper set size + Gammas: gammas, + }) + if err != nil { + log.Debug().Err(err). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Int64("sender", c.KeyperIndex). + Msg("ignoring stored commitment on replay") + } + } + + polyEvals, err := queries.GetDKGPolyEvals(ctx, corekeyperdb.GetDKGPolyEvalsParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + }) + if err != nil { + return errors.Wrap(err, "load stored poly evals") + } + for _, ev := range polyEvals { + if uint64(ev.ReceiverIndex) != ownIndex { //nolint:gosec // G115: keyper index is bounded by the keyper set size + continue + } + eval, err := m.decryptPolyEval(ev.EncryptedEval) + if err != nil { + log.Debug().Err(err). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Int64("sender", ev.SenderIndex). + Msg("ignoring undecryptable poly eval on replay") + continue + } + err = pure.HandlePolyEvalMsg(puredkg.PolyEvalMsg{ + Eon: eonForMsg, + Sender: uint64(ev.SenderIndex), //nolint:gosec // G115: keyper index is bounded by the keyper set size + Receiver: ownIndex, + Eval: eval, + }) + if err != nil { + log.Debug().Err(err). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Int64("sender", ev.SenderIndex). + Msg("ignoring poly eval on replay") + } + } + return nil +} + +func (m *Manager) replayAccusations( + ctx context.Context, + queries *corekeyperdb.Queries, + pure *puredkg.PureDKG, + keyperSetIndex, retryCounter int64, +) error { + eonForMsg := uint64(keyperSetIndex) //nolint:gosec // G115: keyper set index is bounded by the on-chain contract + accusations, err := queries.GetDKGAccusations(ctx, corekeyperdb.GetDKGAccusationsParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + }) + if err != nil { + return errors.Wrap(err, "load stored accusations") + } + for _, a := range accusations { + err := pure.HandleAccusationMsg(puredkg.AccusationMsg{ + Eon: eonForMsg, + Accuser: uint64(a.AccuserIndex), //nolint:gosec // G115: keyper index is bounded by the keyper set size + Accused: uint64(a.AccusedIndex), //nolint:gosec // G115: keyper index is bounded by the keyper set size + }) + if err != nil { + log.Debug().Err(err). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Int64("accuser", a.AccuserIndex). + Int64("accused", a.AccusedIndex). + Msg("ignoring accusation on replay") + } + } + return nil +} + +func (m *Manager) replayApologies( + ctx context.Context, + queries *corekeyperdb.Queries, + pure *puredkg.PureDKG, + keyperSetIndex, retryCounter int64, +) error { + eonForMsg := uint64(keyperSetIndex) //nolint:gosec // G115: keyper set index is bounded by the on-chain contract + apologies, err := queries.GetDKGApologies(ctx, corekeyperdb.GetDKGApologiesParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + }) + if err != nil { + return errors.Wrap(err, "load stored apologies") + } + for _, ap := range apologies { + eval := new(big.Int).SetBytes(ap.PolyEval) + err := pure.HandleApologyMsg(puredkg.ApologyMsg{ + Eon: eonForMsg, + Accuser: uint64(ap.AccuserIndex), //nolint:gosec // G115: keyper index is bounded by the keyper set size + Accused: uint64(ap.ApologizerIndex), //nolint:gosec // G115: keyper index is bounded by the keyper set size + Eval: eval, + }) + if err != nil { + log.Debug().Err(err). + Int64("keyper-set-index", keyperSetIndex). + Int64("retry-counter", retryCounter). + Int64("apologizer", ap.ApologizerIndex). + Int64("accuser", ap.AccuserIndex). + Msg("ignoring apology on replay") + } + } + return nil +} + +func (m *Manager) decryptPolyEval(encrypted []byte) (*big.Int, error) { + plaintext, err := m.cfg.ECIESPrivateKey.Decrypt(encrypted, []byte(""), []byte("")) + if err != nil { + return nil, errors.Wrap(err, "decrypt poly eval") + } + return new(big.Int).SetBytes(plaintext), nil +} + +func (m *Manager) encryptPolyEvalFor( + ctx context.Context, + queries *corekeyperdb.Queries, + receiverAddr common.Address, + eval *big.Int, +) ([]byte, error) { + row, err := queries.GetECIESKey(ctx, shdb.EncodeAddress(receiverAddr)) + if err != nil { + return nil, errors.Wrapf(err, "look up ECIES key for %s", receiverAddr.Hex()) + } + pubKey, err := shdb.DecodeEciesPublicKey(row.EciesPublicKey) + if err != nil { + return nil, errors.Wrap(err, "decode ECIES public key") + } + ciphertext, err := ecies.Encrypt(rand.Reader, pubKey, eval.Bytes(), []byte(""), []byte("")) + if err != nil { + return nil, errors.Wrap(err, "encrypt poly eval") + } + return ciphertext, nil +} + +// eciesPrivateKeyFromECDSA constructs an `*ecies.PrivateKey` from a standard +// `*ecdsa.PrivateKey`. Callers without ready access to the ecies form can use +// this helper when building Config. +func eciesPrivateKeyFromECDSA(priv *ecdsa.PrivateKey) *ecies.PrivateKey { + return ecies.ImportECDSA(priv) +} diff --git a/rolling-shutter/dkg/puredkg_test.go b/rolling-shutter/dkg/puredkg_test.go new file mode 100644 index 000000000..d40790f8c --- /dev/null +++ b/rolling-shutter/dkg/puredkg_test.go @@ -0,0 +1,240 @@ +package dkg + +import ( + "bytes" + "crypto/rand" + "math/big" + "testing" + + "gotest.tools/v3/assert" + + "github.com/shutter-network/shutter/shlib/puredkg" +) + +func TestReceiverIndicesForSender(t *testing.T) { + cases := []struct { + n uint64 + sender uint64 + want []uint64 + }{ + {n: 1, sender: 0, want: []uint64{}}, + {n: 3, sender: 0, want: []uint64{1, 2}}, + {n: 3, sender: 1, want: []uint64{0, 2}}, + {n: 4, sender: 3, want: []uint64{0, 1, 2}}, + } + for _, tc := range cases { + got := ReceiverIndicesForSender(tc.n, tc.sender) + assert.Equal(t, len(got), len(tc.want)) + for i := range tc.want { + assert.Equal(t, got[i], tc.want[i]) + } + } +} + +// TestPureDKGReplayMatchesLive verifies that applying the standard message +// set to a fresh PureDKG (the "replay" path) produces the same observable +// state as applying the same set to the same DKG that drove the messages +// (the "live" path). This is the property the rebuild-from-DB logic depends +// on for restart recovery. +// +//nolint:gocyclo // narrative test: live path, replay path, then side-by-side comparison. Splitting would fragment the scenario. +func TestPureDKGReplayMatchesLive(t *testing.T) { + const ( + eon uint64 = 17 + numKeypers uint64 = 3 + threshold uint64 = 2 + ) + + live := make([]*puredkg.PureDKG, numKeypers) + commitments := make([]puredkg.PolyCommitmentMsg, 0, numKeypers) + allEvals := make([][]puredkg.PolyEvalMsg, numKeypers) + for i := uint64(0); i < numKeypers; i++ { + p := puredkg.NewPureDKG(eon, numKeypers, threshold, i) + live[i] = &p + commit, evals, err := p.StartPhase1Dealing() + assert.NilError(t, err) + commitments = append(commitments, commit) + allEvals[i] = evals + } + + for i := uint64(0); i < numKeypers; i++ { + for j := uint64(0); j < numKeypers; j++ { + if i == j { + continue + } + assert.NilError(t, live[i].HandlePolyCommitmentMsg(commitments[j])) + } + for _, evalsFromJ := range allEvals { + for _, ev := range evalsFromJ { + if ev.Receiver != i { + continue + } + assert.NilError(t, live[i].HandlePolyEvalMsg(ev)) + } + } + } + + replay := puredkg.NewPureDKG(eon, numKeypers, threshold, 0) + for _, c := range commitments { + assert.NilError(t, replay.HandlePolyCommitmentMsg(c)) + } + for _, evalsFromJ := range allEvals { + for _, ev := range evalsFromJ { + if ev.Receiver != 0 { + continue + } + assert.NilError(t, replay.HandlePolyEvalMsg(ev)) + } + } + + for i := uint64(0); i < numKeypers; i++ { + if i == replay.Keyper { + continue + } + assert.Assert(t, + liveCommitmentEqualsReplay(live[0].Commitments[i], replay.Commitments[i]), + "commitment from %d mismatched between live and replay", i) + } + for i := uint64(0); i < numKeypers; i++ { + if i == replay.Keyper { + continue + } + liveEval := live[0].Evals[i] + replayEval := replay.Evals[i] + assert.Assert(t, + (liveEval == nil) == (replayEval == nil), + "eval from %d nil-state mismatched between live and replay", i, + ) + if liveEval != nil { + assert.Equal(t, 0, liveEval.Cmp(replayEval), + "eval from %d differs between live and replay", i) + } + } +} + +// TestPureDKGReplayLateAccusations replays only an accusation message at +// phase Off and asserts the puredkg accepts it. This is the second leg of +// the rebuild logic: an accusation stored from a chain event must be +// applicable to a freshly-built puredkg. +func TestPureDKGReplayLateAccusations(t *testing.T) { + p := puredkg.NewPureDKG(1, 3, 2, 0) + err := p.HandleAccusationMsg(puredkg.AccusationMsg{ + Eon: 1, Accuser: 1, Accused: 2, + }) + assert.NilError(t, err) +} + +// TestPureDKGReplayLateApologies likewise checks the apology path. +func TestPureDKGReplayLateApologies(t *testing.T) { + p := puredkg.NewPureDKG(1, 3, 2, 0) + evalRaw, _ := rand.Int(rand.Reader, big.NewInt(1<<60)) + err := p.HandleApologyMsg(puredkg.ApologyMsg{ + Eon: 1, Accuser: 0, Accused: 2, Eval: evalRaw, + }) + assert.NilError(t, err) +} + +// TestComputeResultAfterReplayWithSelfEval asserts the recovery path used +// by `startFinalizing`: rebuild puredkg from stored messages (commitments +// and per-receiver evals, including a self-eval row), set `pure.Phase` to +// Finalized directly, and call `ComputeResult`. The resulting eon public +// key must equal what a "live" non-restarted DKG would have produced. +func TestComputeResultAfterReplayWithSelfEval(t *testing.T) { + const ( + eon uint64 = 17 + numKeypers uint64 = 3 + threshold uint64 = 2 + ) + + // Drive a full live DKG: each keyper deals, cross-delivers messages, + // and finalizes in-process. + live := make([]*puredkg.PureDKG, numKeypers) + commitments := make([]puredkg.PolyCommitmentMsg, numKeypers) + evalsBySender := make([][]puredkg.PolyEvalMsg, numKeypers) + selfEvals := make([]*big.Int, numKeypers) + for i := uint64(0); i < numKeypers; i++ { + p := puredkg.NewPureDKG(eon, numKeypers, threshold, i) + live[i] = &p + commit, evals, err := p.StartPhase1Dealing() + assert.NilError(t, err) + commitments[i] = commit + evalsBySender[i] = evals + // puredkg consumes the self-eval inside StartPhase1Dealing; capture + // what it set so the recovery path can replay it. + selfEvals[i] = p.Evals[i] + } + for i := uint64(0); i < numKeypers; i++ { + for j := uint64(0); j < numKeypers; j++ { + if i == j { + continue + } + assert.NilError(t, live[i].HandlePolyCommitmentMsg(commitments[j])) + } + for _, evals := range evalsBySender { + for _, ev := range evals { + if ev.Receiver != i { + continue + } + assert.NilError(t, live[i].HandlePolyEvalMsg(ev)) + } + } + assert.NilError(t, live[i].HandlePolyCommitmentMsg(commitments[i])) + } + for i := uint64(0); i < numKeypers; i++ { + advanceLiveToFinalized(live[i]) + } + liveResult, err := live[0].ComputeResult() + assert.NilError(t, err) + + // Now simulate the recovery path for keyper 0: start fresh, replay all + // commitments, replay every received eval including the self-eval row, + // then jump straight to Finalized and compute the result. + replay := puredkg.NewPureDKG(eon, numKeypers, threshold, 0) + for _, c := range commitments { + assert.NilError(t, replay.HandlePolyCommitmentMsg(c)) + } + for _, evals := range evalsBySender { + for _, ev := range evals { + if ev.Receiver != 0 { + continue + } + assert.NilError(t, replay.HandlePolyEvalMsg(ev)) + } + } + // The self-eval (sender=0, receiver=0) is what `maybeDeal` now also + // persists as a dedicated row. Replay it here. + assert.NilError(t, replay.HandlePolyEvalMsg(puredkg.PolyEvalMsg{ + Eon: eon, Sender: 0, Receiver: 0, Eval: selfEvals[0], + })) + replay.Phase = puredkg.Finalized + recoveredResult, err := replay.ComputeResult() + assert.NilError(t, err) + + // The eon public key — which is what the success vote carries — must + // match. + livePK, err := liveResult.PublicKey.GobEncode() + assert.NilError(t, err) + recoveredPK, err := recoveredResult.PublicKey.GobEncode() + assert.NilError(t, err) + assert.Equal(t, string(livePK), string(recoveredPK)) +} + +func advanceLiveToFinalized(p *puredkg.PureDKG) { + p.StartPhase2Accusing() + p.StartPhase3Apologizing() + p.Finalize() +} + +func liveCommitmentEqualsReplay(a, b interface{}) bool { + type marshaller interface { + MarshalText() ([]byte, error) + } + am, aok := a.(marshaller) + bm, bok := b.(marshaller) + if !aok || !bok { + return a == b + } + at, _ := am.MarshalText() + bt, _ := bm.MarshalText() + return bytes.Equal(at, bt) +} diff --git a/rolling-shutter/docs/rolling-shutter.md b/rolling-shutter/docs/rolling-shutter.md index 1f1a4d858..bab33bcb8 100644 --- a/rolling-shutter/docs/rolling-shutter.md +++ b/rolling-shutter/docs/rolling-shutter.md @@ -13,12 +13,9 @@ A collection of commands to run and interact with Rolling Shutter nodes ### SEE ALSO -* [rolling-shutter bootstrap](rolling-shutter_bootstrap.md) - Bootstrap Shuttermint by submitting the initial batch config -* [rolling-shutter chain](rolling-shutter_chain.md) - Run a node for Shutter's Tendermint chain * [rolling-shutter crypto](rolling-shutter_crypto.md) - CLI tool to access crypto functions * [rolling-shutter gnosis-access-node](rolling-shutter_gnosis-access-node.md) - Run an access node for the keyper network of Shutterized Gnosis Chain * [rolling-shutter gnosiskeyper](rolling-shutter_gnosiskeyper.md) - Run a Shutter keyper for Gnosis Chain -* [rolling-shutter op-bootstrap](rolling-shutter_op-bootstrap.md) - Bootstrap validator utility functions for a shuttermint chain * [rolling-shutter op-keyper](rolling-shutter_op-keyper.md) - Run a Shutter optimism keyper node * [rolling-shutter p2pnode](rolling-shutter_p2pnode.md) - Run a Shutter p2p bootstrap node * [rolling-shutter primevkeyper](rolling-shutter_primevkeyper.md) - Run a Shutter keyper for PrimeV POC diff --git a/rolling-shutter/docs/rolling-shutter_bootstrap.md b/rolling-shutter/docs/rolling-shutter_bootstrap.md deleted file mode 100644 index 84d4f6c74..000000000 --- a/rolling-shutter/docs/rolling-shutter_bootstrap.md +++ /dev/null @@ -1,39 +0,0 @@ -## rolling-shutter bootstrap - -Bootstrap Shuttermint by submitting the initial batch config - -### Synopsis - -This command sends a batch config to the Shuttermint chain in a message signed -with the given private key. This will instruct a newly created chain to update -its validator set according to the keyper set defined in the batch config. The -private key must correspond to the initial validator address as defined in the -chain's genesis config. - -``` -rolling-shutter bootstrap [flags] -``` - -### Options - -``` - --deployment-dir string Deployment directory (default "./deployments/localhost") - --ethereum-url string Ethereum URL (default "http://localhost:8545") - -h, --help help for bootstrap - -i, --index int keyper config index to bootstrap with (use latest if negative) (default 1) - -s, --shuttermint-url string Shuttermint RPC URL (default "http://localhost:26657") - -k, --signing-key string private key of the keyper to send the message with -``` - -### Options inherited from parent commands - -``` - --logformat string set log format, possible values: min, short, long, max (default "long") - --loglevel string set log level, possible values: warn, info, debug (default "info") - --no-color do not write colored logs -``` - -### SEE ALSO - -* [rolling-shutter](rolling-shutter.md) - A collection of commands to run and interact with Rolling Shutter nodes - diff --git a/rolling-shutter/docs/rolling-shutter_chain.md b/rolling-shutter/docs/rolling-shutter_chain.md deleted file mode 100644 index e89c40a0a..000000000 --- a/rolling-shutter/docs/rolling-shutter_chain.md +++ /dev/null @@ -1,32 +0,0 @@ -## rolling-shutter chain - -Run a node for Shutter's Tendermint chain - -### Synopsis - -This command runs a node that will connect to Shutter's Tendermint chain. - -``` -rolling-shutter chain [flags] -``` - -### Options - -``` - --config string config file (required) - -h, --help help for chain -``` - -### Options inherited from parent commands - -``` - --logformat string set log format, possible values: min, short, long, max (default "long") - --loglevel string set log level, possible values: warn, info, debug (default "info") - --no-color do not write colored logs -``` - -### SEE ALSO - -* [rolling-shutter](rolling-shutter.md) - A collection of commands to run and interact with Rolling Shutter nodes -* [rolling-shutter chain init](rolling-shutter_chain_init.md) - Create a config file for a Shuttermint node - diff --git a/rolling-shutter/docs/rolling-shutter_chain_init.md b/rolling-shutter/docs/rolling-shutter_chain_init.md deleted file mode 100644 index 1654d5eb6..000000000 --- a/rolling-shutter/docs/rolling-shutter_chain_init.md +++ /dev/null @@ -1,36 +0,0 @@ -## rolling-shutter chain init - -Create a config file for a Shuttermint node - -``` -rolling-shutter chain init [flags] -``` - -### Options - -``` - --blocktime float block time in seconds (default 1) - --dev turn on devmode (disables validator set changes) - --forks.check-in-update.disabled whether the check-in update fork is disabled - --forks.check-in-update.height int block height at which to activate the check-in update fork - --genesis-keyper strings genesis keyper address - -h, --help help for init - --index int keyper index - --initial-eon uint initial eon - --listen-address string tendermint RPC listen address (default "tcp://127.0.0.1:26657") - --role string tendermint node role (validator, isolated-validator, sentry, seed) (default "validator") - --root string root directory -``` - -### Options inherited from parent commands - -``` - --logformat string set log format, possible values: min, short, long, max (default "long") - --loglevel string set log level, possible values: warn, info, debug (default "info") - --no-color do not write colored logs -``` - -### SEE ALSO - -* [rolling-shutter chain](rolling-shutter_chain.md) - Run a node for Shutter's Tendermint chain - diff --git a/rolling-shutter/docs/rolling-shutter_gnosiskeyper.md b/rolling-shutter/docs/rolling-shutter_gnosiskeyper.md index 382f4c561..25f5c4471 100644 --- a/rolling-shutter/docs/rolling-shutter_gnosiskeyper.md +++ b/rolling-shutter/docs/rolling-shutter_gnosiskeyper.md @@ -4,8 +4,7 @@ Run a Shutter keyper for Gnosis Chain ### Synopsis -This command runs a keyper node. It will connect to both a Gnosis and a -Shuttermint node which have to be started separately in advance. +This command runs a keyper node. It will connect to a Gnosis execution node which has to be started separately in advance. ``` rolling-shutter gnosiskeyper [flags] diff --git a/rolling-shutter/docs/rolling-shutter_op-bootstrap.md b/rolling-shutter/docs/rolling-shutter_op-bootstrap.md deleted file mode 100644 index d6f2b918b..000000000 --- a/rolling-shutter/docs/rolling-shutter_op-bootstrap.md +++ /dev/null @@ -1,29 +0,0 @@ -## rolling-shutter op-bootstrap - -Bootstrap validator utility functions for a shuttermint chain - -``` -rolling-shutter op-bootstrap [flags] -``` - -### Options - -``` - --config string config file - -h, --help help for op-bootstrap -``` - -### Options inherited from parent commands - -``` - --logformat string set log format, possible values: min, short, long, max (default "long") - --loglevel string set log level, possible values: warn, info, debug (default "info") - --no-color do not write colored logs -``` - -### SEE ALSO - -* [rolling-shutter](rolling-shutter.md) - A collection of commands to run and interact with Rolling Shutter nodes -* [rolling-shutter op-bootstrap fetch-keyperset](rolling-shutter_op-bootstrap_fetch-keyperset.md) - fetch-keyperset -* [rolling-shutter op-bootstrap generate-config](rolling-shutter_op-bootstrap_generate-config.md) - Generate a 'op-bootstrap' configuration file - diff --git a/rolling-shutter/docs/rolling-shutter_op-bootstrap_fetch-keyperset.md b/rolling-shutter/docs/rolling-shutter_op-bootstrap_fetch-keyperset.md deleted file mode 100644 index c2c839899..000000000 --- a/rolling-shutter/docs/rolling-shutter_op-bootstrap_fetch-keyperset.md +++ /dev/null @@ -1,27 +0,0 @@ -## rolling-shutter op-bootstrap fetch-keyperset - -fetch-keyperset - -``` -rolling-shutter op-bootstrap fetch-keyperset [flags] -``` - -### Options - -``` - -h, --help help for fetch-keyperset -``` - -### Options inherited from parent commands - -``` - --config string config file - --logformat string set log format, possible values: min, short, long, max (default "long") - --loglevel string set log level, possible values: warn, info, debug (default "info") - --no-color do not write colored logs -``` - -### SEE ALSO - -* [rolling-shutter op-bootstrap](rolling-shutter_op-bootstrap.md) - Bootstrap validator utility functions for a shuttermint chain - diff --git a/rolling-shutter/docs/rolling-shutter_op-bootstrap_generate-config.md b/rolling-shutter/docs/rolling-shutter_op-bootstrap_generate-config.md deleted file mode 100644 index 074cd74cf..000000000 --- a/rolling-shutter/docs/rolling-shutter_op-bootstrap_generate-config.md +++ /dev/null @@ -1,29 +0,0 @@ -## rolling-shutter op-bootstrap generate-config - -Generate a 'op-bootstrap' configuration file - -``` -rolling-shutter op-bootstrap generate-config [flags] -``` - -### Options - -``` - -f, --force overwrite existing file - -h, --help help for generate-config - --output string output file -``` - -### Options inherited from parent commands - -``` - --config string config file - --logformat string set log format, possible values: min, short, long, max (default "long") - --loglevel string set log level, possible values: warn, info, debug (default "info") - --no-color do not write colored logs -``` - -### SEE ALSO - -* [rolling-shutter op-bootstrap](rolling-shutter_op-bootstrap.md) - Bootstrap validator utility functions for a shuttermint chain - diff --git a/rolling-shutter/docs/rolling-shutter_op-keyper.md b/rolling-shutter/docs/rolling-shutter_op-keyper.md index 30ae10467..fb7138363 100644 --- a/rolling-shutter/docs/rolling-shutter_op-keyper.md +++ b/rolling-shutter/docs/rolling-shutter_op-keyper.md @@ -4,8 +4,7 @@ Run a Shutter optimism keyper node ### Synopsis -This command runs a keyper node. It will connect to both an Optimism and a -Shuttermint node which have to be started separately in advance. +This command runs a keyper node. It will connect to an Optimism execution node which has to be started separately in advance. ``` rolling-shutter op-keyper [flags] diff --git a/rolling-shutter/docs/rolling-shutter_primevkeyper.md b/rolling-shutter/docs/rolling-shutter_primevkeyper.md index ab30b4ded..2e8e735e2 100644 --- a/rolling-shutter/docs/rolling-shutter_primevkeyper.md +++ b/rolling-shutter/docs/rolling-shutter_primevkeyper.md @@ -4,8 +4,7 @@ Run a Shutter keyper for PrimeV POC ### Synopsis -This command runs a keyper node. It will connect to both a PrimeV and a -Shuttermint node which have to be started separately in advance. +This command runs a keyper node. It will connect to a PrimeV execution node which has to be started separately in advance. ``` rolling-shutter primevkeyper [flags] diff --git a/rolling-shutter/docs/rolling-shutter_shutterservicekeyper.md b/rolling-shutter/docs/rolling-shutter_shutterservicekeyper.md index c1ab9f709..c3418c5f4 100644 --- a/rolling-shutter/docs/rolling-shutter_shutterservicekeyper.md +++ b/rolling-shutter/docs/rolling-shutter_shutterservicekeyper.md @@ -4,8 +4,7 @@ Run a Shutter keyper for Shutter Service ### Synopsis -This command runs a keyper node. It will connect to both a Shutter service and a -Shuttermint node which have to be started separately in advance. +This command runs a keyper node. It will connect to a Shutter service execution node which has to be started separately in advance. ``` rolling-shutter shutterservicekeyper [flags] diff --git a/rolling-shutter/docs/rolling-shutter_snapshotkeyper.md b/rolling-shutter/docs/rolling-shutter_snapshotkeyper.md index 9ab0abc24..a2f9b5faa 100644 --- a/rolling-shutter/docs/rolling-shutter_snapshotkeyper.md +++ b/rolling-shutter/docs/rolling-shutter_snapshotkeyper.md @@ -4,8 +4,7 @@ Run a Shutter snapshotkeyper node ### Synopsis -This command runs a keyper node. It will connect to both an Ethereum and a -Shuttermint node which have to be started separately in advance. +This command runs a keyper node. It will connect to an Ethereum execution node which has to be started separately in advance. ``` rolling-shutter snapshotkeyper [flags] diff --git a/rolling-shutter/eonkeypublisher/eonkeypublisher.go b/rolling-shutter/eonkeypublisher/eonkeypublisher.go deleted file mode 100644 index e6b88d4b7..000000000 --- a/rolling-shutter/eonkeypublisher/eonkeypublisher.go +++ /dev/null @@ -1,243 +0,0 @@ -package eonkeypublisher - -import ( - "context" - "crypto/ecdsa" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/jackc/pgx/v4/pgxpool" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/shutter-network/shop-contracts/bindings" - - obskeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" - corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/retry" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" -) - -const ( - eonKeyChannelSize = 32 - retryInterval = time.Second * 12 -) - -// EonKeyPublisher is a service that publishes eon keys via a eon key publisher contract. -type EonKeyPublisher struct { - dbpool *pgxpool.Pool - client *ethclient.Client - keyperSetManager *bindings.KeyperSetManager - privateKey *ecdsa.PrivateKey - - keys chan keyper.EonPublicKey -} - -func NewEonKeyPublisher( - dbpool *pgxpool.Pool, - client *ethclient.Client, - keyperSetManagerAddress common.Address, - privateKey *ecdsa.PrivateKey, -) (*EonKeyPublisher, error) { - keyperSetManager, err := bindings.NewKeyperSetManager(keyperSetManagerAddress, client) - if err != nil { - return nil, errors.Wrapf(err, "failed to instantiate keyper set manager contract at address %s", keyperSetManagerAddress.Hex()) - } - return &EonKeyPublisher{ - dbpool: dbpool, - client: client, - keyperSetManager: keyperSetManager, - privateKey: privateKey, - - keys: make(chan keyper.EonPublicKey, eonKeyChannelSize), - }, nil -} - -func (p *EonKeyPublisher) Start(ctx context.Context, runner service.Runner) error { //nolint: unparam - log.Info().Msg("starting eon key publisher") - runner.Go(func() error { - p.publishOldKeys(ctx) - for { - select { - case key := <-p.keys: - p.publishIfResponsible(ctx, key) - case <-ctx.Done(): - return ctx.Err() - } - } - }) - return nil -} - -// Publish schedules a eon key to be published. -func (p *EonKeyPublisher) Publish(key keyper.EonPublicKey) { - p.keys <- key -} - -// publishIfResponsible publishes a eon key if the keyper is part of the corresponding keyper -// set, unless the key is already confirmed or the keyper has already voted on it. -func (p *EonKeyPublisher) publishIfResponsible(ctx context.Context, key keyper.EonPublicKey) { - db := obskeyperdb.New(p.dbpool) - keyperSet, err := db.GetKeyperSetByKeyperConfigIndex(ctx, int64(key.KeyperConfigIndex)) - if err != nil { - log.Error(). - Err(err). - Uint64("keyper-set-index", key.KeyperConfigIndex). - Hex("key", key.PublicKey). - Msg("failed to check if eon key should be published") - return - } - keyperAddress := ethcrypto.PubkeyToAddress(p.privateKey.PublicKey) - keyperIndex, err := keyperSet.GetIndex(keyperAddress) - if err != nil { - log.Info(). - Uint64("keyper-set-index", key.KeyperConfigIndex). - Str("keyper-address", keyperAddress.Hex()). - Hex("key", key.PublicKey). - Msg("not publishing eon key as keyper is not part of corresponding keyper set") - return - } - p.publish(ctx, key.PublicKey, key.KeyperConfigIndex, keyperIndex) -} - -// publishOldKeys publishes all eon keys that are already in the database, unless they're already -// confirmed or the keyper has already voted on them. -func (p *EonKeyPublisher) publishOldKeys(ctx context.Context) { - db := corekeyperdb.New(p.dbpool) - dkgResultsDB, err := db.GetAllDKGResults(ctx) - if err != nil { - err := errors.Wrap(err, "failed to query DKG results from db") - log.Error().Err(err).Msg("failed to publish old eon keys") - return - } - for _, dkgResultDB := range dkgResultsDB { - if !dkgResultDB.Success { - continue - } - dkgResult, err := shdb.DecodePureDKGResult(dkgResultDB.PureResult) - if err != nil { - log.Error(). - Err(err). - Int64("eon", dkgResultDB.Eon). - Msg("failed to decode DKG result to publish old eon key") - continue - } - eon, err := db.GetEon(ctx, dkgResultDB.Eon) - if err != nil { - log.Error(). - Err(err). - Int64("eon", dkgResultDB.Eon). - Msg("failed to fetch eon to publish old eon public key") - continue - } - p.publish(ctx, dkgResult.PublicKey.Marshal(), uint64(eon.KeyperConfigIndex), dkgResult.Keyper) - } -} - -// publish publishes an eon key, unless it's already confirmed or the keyper has already voted on -// it. On errors, publishing will be retried a few times and eventually aborted. -func (p *EonKeyPublisher) publish(ctx context.Context, key []byte, keyperSetIndex uint64, keyperIndex uint64) { - _, err := retry.FunctionCall[struct{}](ctx, func(ctx context.Context) (struct{}, error) { - return struct{}{}, p.tryPublish(ctx, key, keyperSetIndex, keyperIndex) - }, retry.Interval(retryInterval)) - if err != nil { - log.Error(). - Err(err). - Uint64("keyper-set-index", keyperSetIndex). - Hex("key", key). - Msg("failed to publish eon key") - } -} - -func (p *EonKeyPublisher) tryPublish(ctx context.Context, key []byte, keyperSetIndex uint64, keyperIndex uint64) error { - contract, err := p.getEonKeyPublisherContract(keyperSetIndex) - if err != nil { - return err - } - keyperAddress := ethcrypto.PubkeyToAddress(p.privateKey.PublicKey) - hasAlreadyVoted, err := contract.HasKeyperVoted(&bind.CallOpts{}, keyperAddress) - if err != nil { - return errors.Wrap(err, "failed to query eon key publisher contract if keyper has already voted") - } - if hasAlreadyVoted { - log.Info(). - Uint64("keyper-set-index", keyperSetIndex). - Str("keyper-address", keyperAddress.Hex()). - Hex("key", key). - Msg("not publishing eon key as keyper has already voted") - return nil - } - isAlreadyConfirmed, err := contract.EonKeyConfirmed(&bind.CallOpts{}, key) - if err != nil { - return errors.Wrap(err, "failed to query eon key publisher contract if eon key is confirmed") - } - if isAlreadyConfirmed { - log.Info(). - Uint64("keyper-set-index", keyperSetIndex). - Hex("key", key). - Msg("not publishing eon key as it is already confirmed") - return nil - } - - chainID, err := p.client.ChainID(ctx) - if err != nil { - return errors.Wrap(err, "failed to get chain ID") - } - opts, err := bind.NewKeyedTransactorWithChainID(p.privateKey, chainID) - if err != nil { - return errors.Wrap(err, "failed to construct tx opts") - } - tx, err := contract.PublishEonKey(opts, key, keyperIndex) - if err != nil { - return errors.Wrap(err, "failed to send publish eon key tx") - } - log.Info(). - Uint64("keyper-set-index", keyperSetIndex). - Hex("key", key). - Hex("tx-hash", tx.Hash().Bytes()). - Msg("eon key publish tx sent") - receipt, err := bind.WaitMined(ctx, p.client, tx) - if err != nil { - log.Error().Err(err).Msg("error waiting for eon key publish tx to be mined") - return err - } - if receipt.Status != types.ReceiptStatusSuccessful { - log.Error(). - Hex("tx-hash", tx.Hash().Bytes()). - Interface("receipt", receipt). - Msg("eon key publish tx failed") - return errors.New("eon key publish tx failed") - } - log.Info(). - Uint64("keyper-set-index", keyperSetIndex). - Hex("key", key). - Hex("tx-hash", tx.Hash().Bytes()). - Msg("successfully published eon key") - return nil -} - -func (p *EonKeyPublisher) getEonKeyPublisherContract(keyperSetIndex uint64) (*bindings.EonKeyPublish, error) { - opts := &bind.CallOpts{} - keyperSetAddress, err := p.keyperSetManager.GetKeyperSetAddress(opts, keyperSetIndex) - if err != nil { - return nil, errors.Wrapf(err, "failed to get keyper set address from manager for index %d", keyperSetIndex) - } - keyperSet, err := bindings.NewKeyperSet(keyperSetAddress, p.client) - if err != nil { - return nil, errors.Wrapf(err, "failed to instantiate keyper set contract at address %s", keyperSetAddress.Hex()) - } - eonKeyPublisherAddress, err := keyperSet.GetPublisher(opts) - if err != nil { - return nil, errors.Wrapf(err, "failed to get eon key publisher contract from keyper set at address %s", keyperSetAddress.Hex()) - } - eonKeyPublisher, err := bindings.NewEonKeyPublish(eonKeyPublisherAddress, p.client) - if err != nil { - return nil, errors.Wrapf(err, "failed to instantiate eon key publisher contract at address %s", eonKeyPublisherAddress.Hex()) - } - return eonKeyPublisher, nil -} diff --git a/rolling-shutter/go.mod b/rolling-shutter/go.mod index 792930fc9..320e7c4f8 100644 --- a/rolling-shutter/go.mod +++ b/rolling-shutter/go.mod @@ -17,11 +17,10 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-multierror v1.1.1 - github.com/icza/gog v0.0.0-20240529172513-3355cf65d018 github.com/ipfs/go-log/v2 v2.6.0 github.com/jackc/pgconn v1.14.1 + github.com/jackc/pgtype v1.14.0 github.com/jackc/pgx/v4 v4.18.1 - github.com/kr/pretty v0.3.1 github.com/libp2p/go-libp2p v0.41.1 github.com/libp2p/go-libp2p-kad-dht v0.33.0 github.com/libp2p/go-libp2p-pubsub v0.13.1 @@ -33,7 +32,7 @@ require ( github.com/primev/mev-commit/contracts-abi v0.0.0-20250922193515-6d402958637d github.com/prometheus/client_golang v1.22.0 github.com/rs/zerolog v1.28.0 - github.com/shutter-network/contracts/v2 v2.0.0-beta.2.0.20250908105003-7e53b1579b04 + github.com/shutter-network/contracts/v2 v2.0.0-beta.2.0.20260715224904-80d0dd0f354d github.com/shutter-network/gnosh-contracts v0.4.0 github.com/shutter-network/shop-contracts v0.0.0-20240407151512-08ef5d8355b6 github.com/shutter-network/shutter/shlib v0.1.19 @@ -43,8 +42,6 @@ require ( github.com/spf13/viper v1.13.0 github.com/stretchr/testify v1.10.0 github.com/supranational/blst v0.3.14 - github.com/tendermint/go-amino v0.16.0 - github.com/tendermint/tendermint v0.37.0-rc2 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 go.opentelemetry.io/otel/sdk v1.31.0 @@ -66,9 +63,6 @@ require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect - github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect - github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/errors v1.11.3 // indirect github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a // indirect @@ -80,20 +74,13 @@ require ( github.com/consensys/gnark-crypto v0.16.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cosmos/gogoproto v1.4.1 // indirect - github.com/cosmos/gorocksdb v1.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect - github.com/creachadair/taskgroup v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/dgraph-io/badger/v2 v2.2007.4 // indirect - github.com/dgraph-io/ristretto v0.0.3 // indirect - github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/elastic/gosigar v0.14.3 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect @@ -103,9 +90,6 @@ require ( github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect github.com/getsentry/sentry-go v0.28.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-kit/kit v0.12.0 // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -114,18 +98,15 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/flock v0.8.1 // indirect + github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect - github.com/google/btree v1.0.1 // indirect github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8 // indirect github.com/google/gopacket v1.1.19 // indirect - github.com/google/orderedcode v0.0.1 // indirect github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/gtank/merlin v0.1.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-bexpr v0.1.11 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect @@ -145,15 +126,14 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/puddle v1.3.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/jmhodges/levigo v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/koron/go-ssdp v0.0.5 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect @@ -176,8 +156,6 @@ require ( github.com/miekg/dns v1.1.66 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect - github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect - github.com/minio/highwayhash v1.0.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect @@ -197,7 +175,6 @@ require ( github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/petermattis/goid v0.0.0-20230808133559-b036b712a89b // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v3 v3.0.4 // indirect @@ -227,19 +204,16 @@ require ( github.com/quic-go/quic-go v0.50.1 // indirect github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect - github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rs/cors v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect - github.com/tendermint/tm-db v0.6.7 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect github.com/urfave/cli/v2 v2.27.5 // indirect @@ -247,7 +221,6 @@ require ( github.com/wlynxg/anet v0.0.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect - go.etcd.io/bbolt v1.3.7 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.uber.org/dig v1.18.0 // indirect @@ -263,8 +236,6 @@ require ( golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.33.0 // indirect gonum.org/v1/gonum v0.16.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect - google.golang.org/grpc v1.67.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/rolling-shutter/go.sum b/rolling-shutter/go.sum index 296d47b56..76d9f9529 100644 --- a/rolling-shutter/go.sum +++ b/rolling-shutter/go.sum @@ -45,34 +45,21 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE= github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= -github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/adlio/schema v1.3.3 h1:oBJn8I02PyTB466pZO1UZEn1TV5XLlifBSyMrmHl/1I= -github.com/adlio/schema v1.3.3/go.mod h1:1EsRssiv9/Ce2CMzq5DoL7RiMshhuigQxrR4DMV9fHg= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -83,21 +70,10 @@ github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3M github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/btcsuite/btcd v0.22.0-beta h1:LTDpDKUM5EeOFBPM8IXpinEcmZ6FWfNZbE3lfrfdnWo= -github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= -github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ= -github.com/btcsuite/btcd/btcutil v1.1.2/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -132,11 +108,6 @@ github.com/consensys/gnark-crypto v0.16.0/go.mod h1:Ke3j06ndtPTVvo++PhGNgvm+lgpL github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= -github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -144,13 +115,6 @@ github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU= -github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= -github.com/cosmos/gogoproto v1.4.1 h1:WoyH+0/jbCTzpKNvyav5FL1ZTWsp1im1MxEpJEzKUB8= -github.com/cosmos/gogoproto v1.4.1/go.mod h1:Ac9lzL4vFpBMcptJROQ6dQ4M3pOEK5Z/l0Q9p+LoCr4= -github.com/cosmos/gorocksdb v1.2.0 h1:d0l3jJG8M4hBouIZq0mDUHZ+zjOx044J3nGRskwTb4Y= -github.com/cosmos/gorocksdb v1.2.0/go.mod h1:aaKvKItm514hKfNJpUJXnnOWeBnk2GL4+Qw9NHizILw= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -162,8 +126,6 @@ github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOV github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= -github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM= -github.com/creachadair/taskgroup v0.3.2/go.mod h1:wieWwecHVzsidg2CsUnFinW1faVN4+kq+TDlRJQ0Wbk= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= @@ -183,22 +145,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.9.1 h1:yHmEnA7jSTUMQgV+uN02WpZtwHnz2CBW3mZRIxr1vtI= github.com/deepmap/oapi-codegen v1.9.1/go.mod h1:PLqNAhdedP8ttRpBBkzLKU3bp+Fpy+tTgeAMlztR2cw= -github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= -github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= -github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI= -github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= @@ -214,19 +164,11 @@ github.com/ethereum/go-ethereum v1.15.11 h1:JK73WKeu0WC0O1eyX+mdQAVHUV+UR1a9VB/d github.com/ethereum/go-ethereum v1.15.11/go.mod h1:mf8YiHIb0GR4x4TipcvBUPxJLw1mFdmxzoDi11sDRoI= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= -github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= -github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= -github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= -github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= -github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= -github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/ferranbt/fastssz v0.1.3 h1:ZI+z3JH05h4kgmFXdHuR1aWYsgrg7o+Fw7/NCzM16Mo= github.com/ferranbt/fastssz v0.1.3/go.mod h1:0Y9TEd/9XuFlh7mskMPfXiI2Dkw4Ddg9EyXt1W7MRvE= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -256,14 +198,8 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= -github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -321,7 +257,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -337,17 +272,12 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -362,7 +292,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8 h1:Ep/joEub9YwcjRY6ND3+Y/w0ncE540RtGatVhtZL0/Q= github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -371,8 +300,6 @@ github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= -github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -408,10 +335,6 @@ github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY4 github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= -github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= -github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= -github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -438,9 +361,6 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/icza/gog v0.0.0-20240529172513-3355cf65d018 h1:5SPFJCbm/boJpqPpzA/THc44Qi4wPQI0JVKZGCfdB+8= -github.com/icza/gog v0.0.0-20240529172513-3355cf65d018/go.mod h1:DfGPW5hjxzL8lvxIU3S1/jYrehLWYdue9RDw0FFA/fI= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= @@ -522,8 +442,6 @@ github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+ github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= -github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -537,7 +455,6 @@ github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPci github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= @@ -614,7 +531,6 @@ github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8S github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po= github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -651,17 +567,10 @@ github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUM github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= -github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= -github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 h1:QRUSJEgZn2Snx0EmT/QLXibWjSUDjKWvXIT19NBVp94= -github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= -github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= -github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -726,32 +635,20 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= -github.com/opencontainers/runc v1.1.3/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= -github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= -github.com/petermattis/goid v0.0.0-20230808133559-b036b712a89b h1:vab8deKC4QoIfm9fJM59iuNz1ELGsuLoYYpiF+pHiG8= -github.com/petermattis/goid v0.0.0-20230808133559-b036b712a89b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= @@ -832,8 +729,6 @@ github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6 github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -855,8 +750,6 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= -github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= @@ -887,8 +780,8 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go. github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/shutter-network/contracts/v2 v2.0.0-beta.2.0.20250908105003-7e53b1579b04 h1:DfSfDw8IqXHqz6JXkZLIcvPS4CdoeXNaDhk+HEX61rM= -github.com/shutter-network/contracts/v2 v2.0.0-beta.2.0.20250908105003-7e53b1579b04/go.mod h1:V8KhVM75wyWVSzZJ6GeC9dWCjRrinIQVb7mYNP+knbg= +github.com/shutter-network/contracts/v2 v2.0.0-beta.2.0.20260715224904-80d0dd0f354d h1:glanWlhjmTKNtb0RU9VkMn+PL7Q6C2Wukp4PouiVWyE= +github.com/shutter-network/contracts/v2 v2.0.0-beta.2.0.20260715224904-80d0dd0f354d/go.mod h1:V8KhVM75wyWVSzZJ6GeC9dWCjRrinIQVb7mYNP+knbg= github.com/shutter-network/gnosh-contracts v0.4.0 h1:2GJcHK9w4lJZMsccklmxDhNnrkRLJDRwsL1acBnAeak= github.com/shutter-network/gnosh-contracts v0.4.0/go.mod h1:QB0d64ybbVFKMrLjrc1tldri87KNjTmKQjhk9jaso2E= github.com/shutter-network/shop-contracts v0.0.0-20240407151512-08ef5d8355b6 h1:m6Ti1/IH+GBTtGqyAX3xbh+ruUKvC+m+/uzYDUa+JDQ= @@ -898,33 +791,24 @@ github.com/shutter-network/shutter/shlib v0.1.19/go.mod h1:miY10OJ0FKjLZTwvmG0AJ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= @@ -933,8 +817,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -956,19 +838,12 @@ github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3 github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= -github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= -github.com/tendermint/tendermint v0.37.0-rc2 h1:2n1em+jfbhSv6QnBj8F6KHCpbIzZCB8KgcjidJUQNlY= -github.com/tendermint/tendermint v0.37.0-rc2/go.mod h1:uYQO9DRNPeZROa9X3hJOZpYcVREDC2/HST+EiU5g2+A= -github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu8= -github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/ulope/jrpc2 v0.0.0-20230706135348-a95cf3d96bd2 h1:rk0z/6CEJbstiHqv8+4ZIMv4Sm2zBZ5v5C56P8JXd+I= @@ -993,7 +868,6 @@ github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1004,8 +878,6 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -1055,7 +927,6 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= @@ -1203,8 +1074,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1216,7 +1085,6 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1448,8 +1316,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -1469,8 +1335,6 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1488,7 +1352,6 @@ google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9x google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/rolling-shutter/keyper/database/definition.go b/rolling-shutter/keyper/database/definition.go index 1594f20ef..6f493b029 100644 --- a/rolling-shutter/keyper/database/definition.go +++ b/rolling-shutter/keyper/database/definition.go @@ -3,10 +3,8 @@ package database import ( "context" "embed" - "time" "github.com/jackc/pgx/v4" - "github.com/pkg/errors" "github.com/rs/zerolog/log" chainobsdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db" @@ -46,19 +44,7 @@ func (d *KeyperDB) Create(ctx context.Context, tx pgx.Tx) error { } func (d *KeyperDB) Init(ctx context.Context, tx pgx.Tx) error { - err := d.Definition.Init(ctx, tx) - if err != nil { - return err - } - err = New(tx).TMSetSyncMeta(ctx, TMSetSyncMetaParams{ - CurrentBlock: 0, - LastCommittedHeight: -1, - SyncTimestamp: time.Now(), - }) - if err != nil { - return errors.Wrap(err, "failed to set current block") - } - return nil + return d.Definition.Init(ctx, tx) } func (d *KeyperDB) Validate(ctx context.Context, tx pgx.Tx) error { diff --git a/rolling-shutter/keyper/database/extend.go b/rolling-shutter/keyper/database/extend.go index 2822ea7a9..518df2943 100644 --- a/rolling-shutter/keyper/database/extend.go +++ b/rolling-shutter/keyper/database/extend.go @@ -4,28 +4,34 @@ import ( "context" "github.com/ethereum/go-ethereum/common" + "github.com/jackc/pgx/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" - "google.golang.org/protobuf/proto" + obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/identitypreimage" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2pmsg" "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" ) -func GetKeyperIndex(addr common.Address, keypers []string) (uint64, bool) { - hexaddr := shdb.EncodeAddress(addr) - for i, a := range keypers { - if a == hexaddr { - return uint64(i), true +// GetKeyperIndex returns the index of the keyper with the given address in the +// keyper set with the given keyperConfigIndex (which equals the eon for the +// keyper set). The keyper set is read from the chainobserver tables. +func (q *Queries) GetKeyperIndex(ctx context.Context, keyperConfigIndex int64, addr common.Address) (int64, bool, error) { + ks, err := obskeyper.New(q.db).GetKeyperSetByKeyperConfigIndex(ctx, keyperConfigIndex) + if errors.Is(err, pgx.ErrNoRows) { + return -1, false, nil + } + if err != nil { + return -1, false, errors.Wrapf(err, "failed to get keyper set %d from db", keyperConfigIndex) + } + encodedAddress := shdb.EncodeAddress(addr) + for i, address := range ks.Keypers { + if address == encodedAddress { + return int64(i), true, nil } } - return uint64(0), false -} - -func (bc *TendermintBatchConfig) KeyperIndex(addr common.Address) (uint64, bool) { - return GetKeyperIndex(addr, bc.Keypers) + return -1, false, nil } func (q *Queries) InsertDecryptionKeysMsg(ctx context.Context, msg *p2pmsg.DecryptionKeys) error { @@ -65,41 +71,3 @@ func (q *Queries) InsertDecryptionKeySharesMsg(ctx context.Context, msg *p2pmsg. } return nil } - -func (q *Queries) ScheduleShutterMessage( - ctx context.Context, - description string, - msg *shmsg.Message, -) error { - data, err := proto.Marshal(msg) - if err != nil { - return err - } - msgid, err := q.ScheduleSerializedShutterMessage(ctx, ScheduleSerializedShutterMessageParams{ - Description: description, - Msg: data, - }) - if err != nil { - return err - } - log.Info().Int32("id", msgid).Str("description", description). - Msg("scheduled shuttermint message") - return nil -} - -// GetKeyperIndex returns the index of the keyper with the given address in the keyper set with -// the given index. If the keyper is not found, the second return value is false. -func (q *Queries) GetKeyperIndex(ctx context.Context, keyperConfigIndex int64, addr common.Address) (int64, bool, error) { - batchConfig, err := q.GetBatchConfig(ctx, int32(keyperConfigIndex)) - if err != nil { - return -1, false, errors.Wrapf(err, "failed to get config %d from db", keyperConfigIndex) - } - - encodedAddress := shdb.EncodeAddress(addr) - for i, address := range batchConfig.Keypers { - if address == encodedAddress { - return int64(i), true, nil - } - } - return -1, false, nil -} diff --git a/rolling-shutter/keyper/database/keyper.sqlc.gen.go b/rolling-shutter/keyper/database/keyper.sqlc.gen.go index 2f441c00e..156fee1d8 100644 --- a/rolling-shutter/keyper/database/keyper.sqlc.gen.go +++ b/rolling-shutter/keyper/database/keyper.sqlc.gen.go @@ -8,60 +8,11 @@ package database import ( "context" "database/sql" - "time" "github.com/jackc/pgconn" + "github.com/jackc/pgtype" ) -const countBatchConfigs = `-- name: CountBatchConfigs :one -SELECT count(*) FROM tendermint_batch_config -` - -func (q *Queries) CountBatchConfigs(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, countBatchConfigs) - var count int64 - err := row.Scan(&count) - return count, err -} - -const countBatchConfigsInBlockRange = `-- name: CountBatchConfigsInBlockRange :one -SELECT COUNT(*) -FROM tendermint_batch_config -WHERE $1 <= activation_block_number AND activation_block_number < $2 -` - -type CountBatchConfigsInBlockRangeParams struct { - StartBlock int64 - EndBlock int64 -} - -func (q *Queries) CountBatchConfigsInBlockRange(ctx context.Context, arg CountBatchConfigsInBlockRangeParams) (int64, error) { - row := q.db.QueryRow(ctx, countBatchConfigsInBlockRange, arg.StartBlock, arg.EndBlock) - var count int64 - err := row.Scan(&count) - return count, err -} - -const countBatchConfigsInBlockRangeWithKeyper = `-- name: CountBatchConfigsInBlockRangeWithKeyper :one -SELECT COUNT(*) -FROM tendermint_batch_config -WHERE ($1::TEXT[]) && keypers AND $2 <= activation_block_number AND activation_block_number < $3 -` - -type CountBatchConfigsInBlockRangeWithKeyperParams struct { - KeyperAddress []string - StartBlock int64 - EndBlock int64 -} - -// Due to https://github.com/sqlc-dev/sqlc/issues/3083 we need to use this awkward construction to pass and query for the keyper address parameter as a single element slice -func (q *Queries) CountBatchConfigsInBlockRangeWithKeyper(ctx context.Context, arg CountBatchConfigsInBlockRangeWithKeyperParams) (int64, error) { - row := q.db.QueryRow(ctx, countBatchConfigsInBlockRangeWithKeyper, arg.KeyperAddress, arg.StartBlock, arg.EndBlock) - var count int64 - err := row.Scan(&count) - return count, err -} - const countDecryptionKeyShares = `-- name: CountDecryptionKeyShares :one SELECT count(*) FROM decryption_key_share WHERE eon = $1 AND epoch_id = $2 @@ -79,56 +30,37 @@ func (q *Queries) CountDecryptionKeyShares(ctx context.Context, arg CountDecrypt return count, err } -const deletePolyEval = `-- name: DeletePolyEval :exec - -DELETE FROM poly_evals ev WHERE ev.eon=$1 AND ev.receiver_address=$2 -` - -type DeletePolyEvalParams struct { - Eon int64 - ReceiverAddress string -} - -// PolyEvalsWithEncryptionKeys could probably already delete the entries from the poly_evals table. -// I wasn't able to make this work, because of bugs in sqlc -func (q *Queries) DeletePolyEval(ctx context.Context, arg DeletePolyEvalParams) error { - _, err := q.db.Exec(ctx, deletePolyEval, arg.Eon, arg.ReceiverAddress) - return err -} - -const deletePolyEvalByEon = `-- name: DeletePolyEvalByEon :execresult -DELETE FROM poly_evals ev WHERE ev.eon=$1 -` - -func (q *Queries) DeletePolyEvalByEon(ctx context.Context, eon int64) (pgconn.CommandTag, error) { - return q.db.Exec(ctx, deletePolyEvalByEon, eon) -} - -const deletePureDKG = `-- name: DeletePureDKG :exec -DELETE FROM puredkg WHERE eon=$1 +const existsDKGResultSuccess = `-- name: ExistsDKGResultSuccess :one +SELECT EXISTS ( + SELECT 1 FROM dkg_result WHERE eon = $1 AND success = TRUE +) ` -func (q *Queries) DeletePureDKG(ctx context.Context, eon int64) error { - _, err := q.db.Exec(ctx, deletePureDKG, eon) - return err +func (q *Queries) ExistsDKGResultSuccess(ctx context.Context, eon int64) (bool, error) { + row := q.db.QueryRow(ctx, existsDKGResultSuccess, eon) + var exists bool + err := row.Scan(&exists) + return exists, err } -const deleteShutterMessage = `-- name: DeleteShutterMessage :exec -DELETE FROM tendermint_outgoing_messages WHERE id=$1 +const existsDKGSentAction = `-- name: ExistsDKGSentAction :one +SELECT EXISTS ( + SELECT 1 FROM dkg_sent_actions + WHERE keyper_set_index = $1 AND retry_counter = $2 AND action = $3 +) ` -func (q *Queries) DeleteShutterMessage(ctx context.Context, id int32) error { - _, err := q.db.Exec(ctx, deleteShutterMessage, id) - return err +type ExistsDKGSentActionParams struct { + KeyperSetIndex int64 + RetryCounter int64 + Action string } -const deleteShutterMessageByDesc = `-- name: DeleteShutterMessageByDesc :exec -DELETE FROM tendermint_outgoing_messages WHERE description=$1 -` - -func (q *Queries) DeleteShutterMessageByDesc(ctx context.Context, description string) error { - _, err := q.db.Exec(ctx, deleteShutterMessageByDesc, description) - return err +func (q *Queries) ExistsDKGSentAction(ctx context.Context, arg ExistsDKGSentActionParams) (bool, error) { + row := q.db.QueryRow(ctx, existsDKGSentAction, arg.KeyperSetIndex, arg.RetryCounter, arg.Action) + var exists bool + err := row.Scan(&exists) + return exists, err } const existsDecryptionKey = `-- name: ExistsDecryptionKey :one @@ -172,6 +104,19 @@ func (q *Queries) ExistsDecryptionKeyShare(ctx context.Context, arg ExistsDecryp return exists, err } +const existsECIESKey = `-- name: ExistsECIESKey :one +SELECT EXISTS ( + SELECT 1 FROM ecies_keys WHERE keyper_address = $1 +) +` + +func (q *Queries) ExistsECIESKey(ctx context.Context, keyperAddress string) (bool, error) { + row := q.db.QueryRow(ctx, existsECIESKey, keyperAddress) + var exists bool + err := row.Scan(&exists) + return exists, err +} + const getAllDKGResults = `-- name: GetAllDKGResults :many SELECT eon, success, error, pure_result FROM dkg_result ORDER BY eon ASC @@ -203,7 +148,7 @@ func (q *Queries) GetAllDKGResults(ctx context.Context) ([]DkgResult, error) { } const getAllEons = `-- name: GetAllEons :many -SELECT eon, height, activation_block_number, keyper_config_index FROM eons ORDER BY eon +SELECT eon, activation_block_number, keyper_config_index, dkg_contract, phase_length, lead_length, max_retries FROM eons ORDER BY eon ` func (q *Queries) GetAllEons(ctx context.Context) ([]Eon, error) { @@ -217,9 +162,12 @@ func (q *Queries) GetAllEons(ctx context.Context) ([]Eon, error) { var i Eon if err := rows.Scan( &i.Eon, - &i.Height, &i.ActivationBlockNumber, &i.KeyperConfigIndex, + &i.DkgContract, + &i.PhaseLength, + &i.LeadLength, + &i.MaxRetries, ); err != nil { return nil, err } @@ -231,39 +179,31 @@ func (q *Queries) GetAllEons(ctx context.Context) ([]Eon, error) { return items, nil } -const getAndDeleteEonPublicKeys = `-- name: GetAndDeleteEonPublicKeys :many -WITH t1 AS (DELETE FROM outgoing_eon_keys RETURNING eon_public_key, eon) -SELECT t1.eon_public_key, t1.eon, eons.activation_block_number, tbc.keypers, tbc.keyper_config_index -FROM t1 -INNER JOIN eons - ON t1.eon = eons.eon -INNER JOIN tendermint_batch_config tbc - ON eons.keyper_config_index = tbc.keyper_config_index +const getDKGAccusations = `-- name: GetDKGAccusations :many +SELECT keyper_set_index, retry_counter, accuser_index, accused_index FROM dkg_accusations +WHERE keyper_set_index = $1 AND retry_counter = $2 +ORDER BY accuser_index, accused_index ` -type GetAndDeleteEonPublicKeysRow struct { - EonPublicKey []byte - Eon int64 - ActivationBlockNumber int64 - Keypers []string - KeyperConfigIndex int32 +type GetDKGAccusationsParams struct { + KeyperSetIndex int64 + RetryCounter int64 } -func (q *Queries) GetAndDeleteEonPublicKeys(ctx context.Context) ([]GetAndDeleteEonPublicKeysRow, error) { - rows, err := q.db.Query(ctx, getAndDeleteEonPublicKeys) +func (q *Queries) GetDKGAccusations(ctx context.Context, arg GetDKGAccusationsParams) ([]DkgAccusation, error) { + rows, err := q.db.Query(ctx, getDKGAccusations, arg.KeyperSetIndex, arg.RetryCounter) if err != nil { return nil, err } defer rows.Close() - var items []GetAndDeleteEonPublicKeysRow + var items []DkgAccusation for rows.Next() { - var i GetAndDeleteEonPublicKeysRow + var i DkgAccusation if err := rows.Scan( - &i.EonPublicKey, - &i.Eon, - &i.ActivationBlockNumber, - &i.Keypers, - &i.KeyperConfigIndex, + &i.KeyperSetIndex, + &i.RetryCounter, + &i.AccuserIndex, + &i.AccusedIndex, ); err != nil { return nil, err } @@ -275,48 +215,122 @@ func (q *Queries) GetAndDeleteEonPublicKeys(ctx context.Context) ([]GetAndDelete return items, nil } -const getBatchConfig = `-- name: GetBatchConfig :one -SELECT keyper_config_index, height, keypers, threshold, started, activation_block_number -FROM tendermint_batch_config -WHERE keyper_config_index = $1 +const getDKGApologies = `-- name: GetDKGApologies :many +SELECT keyper_set_index, retry_counter, apologizer_index, accuser_index, poly_eval FROM dkg_apologies +WHERE keyper_set_index = $1 AND retry_counter = $2 +ORDER BY apologizer_index, accuser_index ` -func (q *Queries) GetBatchConfig(ctx context.Context, keyperConfigIndex int32) (TendermintBatchConfig, error) { - row := q.db.QueryRow(ctx, getBatchConfig, keyperConfigIndex) - var i TendermintBatchConfig - err := row.Scan( - &i.KeyperConfigIndex, - &i.Height, - &i.Keypers, - &i.Threshold, - &i.Started, - &i.ActivationBlockNumber, - ) +type GetDKGApologiesParams struct { + KeyperSetIndex int64 + RetryCounter int64 +} + +func (q *Queries) GetDKGApologies(ctx context.Context, arg GetDKGApologiesParams) ([]DkgApology, error) { + rows, err := q.db.Query(ctx, getDKGApologies, arg.KeyperSetIndex, arg.RetryCounter) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DkgApology + for rows.Next() { + var i DkgApology + if err := rows.Scan( + &i.KeyperSetIndex, + &i.RetryCounter, + &i.ApologizerIndex, + &i.AccuserIndex, + &i.PolyEval, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getDKGInitialState = `-- name: GetDKGInitialState :one +SELECT keyper_set_index, retry_counter, puredkg_bytes FROM dkg_initial_states +WHERE keyper_set_index = $1 AND retry_counter = $2 +` + +type GetDKGInitialStateParams struct { + KeyperSetIndex int64 + RetryCounter int64 +} + +func (q *Queries) GetDKGInitialState(ctx context.Context, arg GetDKGInitialStateParams) (DkgInitialState, error) { + row := q.db.QueryRow(ctx, getDKGInitialState, arg.KeyperSetIndex, arg.RetryCounter) + var i DkgInitialState + err := row.Scan(&i.KeyperSetIndex, &i.RetryCounter, &i.PuredkgBytes) return i, err } -const getBatchConfigs = `-- name: GetBatchConfigs :many -SELECT keyper_config_index, height, keypers, threshold, started, activation_block_number -FROM tendermint_batch_config -ORDER BY keyper_config_index +const getDKGPolyCommitments = `-- name: GetDKGPolyCommitments :many +SELECT keyper_set_index, retry_counter, keyper_index, commitment FROM dkg_poly_commitments +WHERE keyper_set_index = $1 AND retry_counter = $2 +ORDER BY keyper_index ` -func (q *Queries) GetBatchConfigs(ctx context.Context) ([]TendermintBatchConfig, error) { - rows, err := q.db.Query(ctx, getBatchConfigs) +type GetDKGPolyCommitmentsParams struct { + KeyperSetIndex int64 + RetryCounter int64 +} + +func (q *Queries) GetDKGPolyCommitments(ctx context.Context, arg GetDKGPolyCommitmentsParams) ([]DkgPolyCommitment, error) { + rows, err := q.db.Query(ctx, getDKGPolyCommitments, arg.KeyperSetIndex, arg.RetryCounter) if err != nil { return nil, err } defer rows.Close() - var items []TendermintBatchConfig + var items []DkgPolyCommitment for rows.Next() { - var i TendermintBatchConfig + var i DkgPolyCommitment if err := rows.Scan( - &i.KeyperConfigIndex, - &i.Height, - &i.Keypers, - &i.Threshold, - &i.Started, - &i.ActivationBlockNumber, + &i.KeyperSetIndex, + &i.RetryCounter, + &i.KeyperIndex, + &i.Commitment, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getDKGPolyEvals = `-- name: GetDKGPolyEvals :many +SELECT keyper_set_index, retry_counter, sender_index, receiver_index, encrypted_eval FROM dkg_poly_evals +WHERE keyper_set_index = $1 AND retry_counter = $2 +ORDER BY sender_index, receiver_index +` + +type GetDKGPolyEvalsParams struct { + KeyperSetIndex int64 + RetryCounter int64 +} + +func (q *Queries) GetDKGPolyEvals(ctx context.Context, arg GetDKGPolyEvalsParams) ([]DkgPolyEval, error) { + rows, err := q.db.Query(ctx, getDKGPolyEvals, arg.KeyperSetIndex, arg.RetryCounter) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DkgPolyEval + for rows.Next() { + var i DkgPolyEval + if err := rows.Scan( + &i.KeyperSetIndex, + &i.RetryCounter, + &i.SenderIndex, + &i.ReceiverIndex, + &i.EncryptedEval, ); err != nil { return nil, err } @@ -348,7 +362,7 @@ func (q *Queries) GetDKGResult(ctx context.Context, eon int64) (DkgResult, error const getDKGResultForBlockNumber = `-- name: GetDKGResultForBlockNumber :one SELECT eon, success, error, pure_result FROM dkg_result WHERE eon = (SELECT eon FROM eons WHERE activation_block_number <= $1 -ORDER BY activation_block_number DESC, height DESC +ORDER BY activation_block_number DESC LIMIT 1) ` @@ -421,34 +435,19 @@ func (q *Queries) GetDecryptionKeyShare(ctx context.Context, arg GetDecryptionKe return i, err } -const getEncryptionKeys = `-- name: GetEncryptionKeys :many -SELECT DISTINCT ON (address) address, encryption_public_key, height -FROM tendermint_encryption_key -ORDER BY address, height DESC +const getECIESKey = `-- name: GetECIESKey :one +SELECT keyper_address, ecies_public_key FROM ecies_keys WHERE keyper_address = $1 ` -func (q *Queries) GetEncryptionKeys(ctx context.Context) ([]TendermintEncryptionKey, error) { - rows, err := q.db.Query(ctx, getEncryptionKeys) - if err != nil { - return nil, err - } - defer rows.Close() - var items []TendermintEncryptionKey - for rows.Next() { - var i TendermintEncryptionKey - if err := rows.Scan(&i.Address, &i.EncryptionPublicKey, &i.Height); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil +func (q *Queries) GetECIESKey(ctx context.Context, keyperAddress string) (EciesKey, error) { + row := q.db.QueryRow(ctx, getECIESKey, keyperAddress) + var i EciesKey + err := row.Scan(&i.KeyperAddress, &i.EciesPublicKey) + return i, err } const getEon = `-- name: GetEon :one -SELECT eon, height, activation_block_number, keyper_config_index FROM eons WHERE eon=$1 +SELECT eon, activation_block_number, keyper_config_index, dkg_contract, phase_length, lead_length, max_retries FROM eons WHERE eon=$1 ` func (q *Queries) GetEon(ctx context.Context, eon int64) (Eon, error) { @@ -456,17 +455,20 @@ func (q *Queries) GetEon(ctx context.Context, eon int64) (Eon, error) { var i Eon err := row.Scan( &i.Eon, - &i.Height, &i.ActivationBlockNumber, &i.KeyperConfigIndex, + &i.DkgContract, + &i.PhaseLength, + &i.LeadLength, + &i.MaxRetries, ) return i, err } const getEonForBlockNumber = `-- name: GetEonForBlockNumber :one -SELECT eon, height, activation_block_number, keyper_config_index FROM eons +SELECT eon, activation_block_number, keyper_config_index, dkg_contract, phase_length, lead_length, max_retries FROM eons WHERE activation_block_number <= $1 -ORDER BY activation_block_number DESC, height DESC +ORDER BY activation_block_number DESC LIMIT 1 ` @@ -475,85 +477,12 @@ func (q *Queries) GetEonForBlockNumber(ctx context.Context, blockNumber int64) ( var i Eon err := row.Scan( &i.Eon, - &i.Height, &i.ActivationBlockNumber, &i.KeyperConfigIndex, - ) - return i, err -} - -const getKeyperStateForEon = `-- name: GetKeyperStateForEon :one -SELECT ($1::TEXT[] && tbc.keypers)::BOOL AS is_keyper -FROM tendermint_batch_config AS tbc -LEFT JOIN eons ON eons.keyper_config_index = tbc.keyper_config_index -WHERE eons.eon = $2 -` - -type GetKeyperStateForEonParams struct { - KeyperAddress []string - Eon int64 -} - -func (q *Queries) GetKeyperStateForEon(ctx context.Context, arg GetKeyperStateForEonParams) (bool, error) { - row := q.db.QueryRow(ctx, getKeyperStateForEon, arg.KeyperAddress, arg.Eon) - var is_keyper bool - err := row.Scan(&is_keyper) - return is_keyper, err -} - -const getLastBatchConfigProcessed = `-- name: GetLastBatchConfigProcessed :one -SELECT keyper_config_index FROM last_batch_config_sent LIMIT 1 -` - -func (q *Queries) GetLastBatchConfigProcessed(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, getLastBatchConfigProcessed) - var keyper_config_index int64 - err := row.Scan(&keyper_config_index) - return keyper_config_index, err -} - -const getLastBlockSeen = `-- name: GetLastBlockSeen :one -SELECT block_number FROM last_block_seen LIMIT 1 -` - -func (q *Queries) GetLastBlockSeen(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, getLastBlockSeen) - var block_number int64 - err := row.Scan(&block_number) - return block_number, err -} - -const getLastCommittedHeight = `-- name: GetLastCommittedHeight :one -SELECT last_committed_height -FROM tendermint_sync_meta -ORDER BY current_block DESC, last_committed_height DESC -LIMIT 1 -` - -func (q *Queries) GetLastCommittedHeight(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, getLastCommittedHeight) - var last_committed_height int64 - err := row.Scan(&last_committed_height) - return last_committed_height, err -} - -const getLatestBatchConfig = `-- name: GetLatestBatchConfig :one -SELECT keyper_config_index, height, keypers, threshold, started, activation_block_number -FROM tendermint_batch_config -ORDER BY keyper_config_index DESC -LIMIT 1 -` - -func (q *Queries) GetLatestBatchConfig(ctx context.Context) (TendermintBatchConfig, error) { - row := q.db.QueryRow(ctx, getLatestBatchConfig) - var i TendermintBatchConfig - err := row.Scan( - &i.KeyperConfigIndex, - &i.Height, - &i.Keypers, - &i.Threshold, - &i.Started, - &i.ActivationBlockNumber, + &i.DkgContract, + &i.PhaseLength, + &i.LeadLength, + &i.MaxRetries, ) return i, err } @@ -572,7 +501,7 @@ func (q *Queries) GetLatestEonForKeyperConfig(ctx context.Context, keyperConfigI } const getLatestStartedEonByKeyperConfigIndex = `-- name: GetLatestStartedEonByKeyperConfigIndex :one -SELECT eon, height, activation_block_number, keyper_config_index +SELECT eon, activation_block_number, keyper_config_index, dkg_contract, phase_length, lead_length, max_retries FROM eons WHERE keyper_config_index = $1 ORDER BY eon DESC @@ -584,48 +513,224 @@ func (q *Queries) GetLatestStartedEonByKeyperConfigIndex(ctx context.Context, ke var i Eon err := row.Scan( &i.Eon, - &i.Height, &i.ActivationBlockNumber, &i.KeyperConfigIndex, + &i.DkgContract, + &i.PhaseLength, + &i.LeadLength, + &i.MaxRetries, ) return i, err } -const getNextShutterMessage = `-- name: GetNextShutterMessage :one -SELECT id, description, msg from tendermint_outgoing_messages +const getPendingTxs = `-- name: GetPendingTxs :many +SELECT id, to_address, data, value, status, tx_hash, nonce, error, created_at, updated_at, label FROM tx_outbox +WHERE status = 'pending' ORDER BY id -LIMIT 1 ` -func (q *Queries) GetNextShutterMessage(ctx context.Context) (TendermintOutgoingMessage, error) { - row := q.db.QueryRow(ctx, getNextShutterMessage) - var i TendermintOutgoingMessage - err := row.Scan(&i.ID, &i.Description, &i.Msg) +func (q *Queries) GetPendingTxs(ctx context.Context) ([]TxOutbox, error) { + rows, err := q.db.Query(ctx, getPendingTxs) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TxOutbox + for rows.Next() { + var i TxOutbox + if err := rows.Scan( + &i.ID, + &i.ToAddress, + &i.Data, + &i.Value, + &i.Status, + &i.TxHash, + &i.Nonce, + &i.Error, + &i.CreatedAt, + &i.UpdatedAt, + &i.Label, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getSubmittedTxs = `-- name: GetSubmittedTxs :many +SELECT id, to_address, data, value, status, tx_hash, nonce, error, created_at, updated_at, label FROM tx_outbox +WHERE status = 'submitted' +ORDER BY id +` + +func (q *Queries) GetSubmittedTxs(ctx context.Context) ([]TxOutbox, error) { + rows, err := q.db.Query(ctx, getSubmittedTxs) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TxOutbox + for rows.Next() { + var i TxOutbox + if err := rows.Scan( + &i.ID, + &i.ToAddress, + &i.Data, + &i.Value, + &i.Status, + &i.TxHash, + &i.Nonce, + &i.Error, + &i.CreatedAt, + &i.UpdatedAt, + &i.Label, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTxOutboxByID = `-- name: GetTxOutboxByID :one +SELECT id, to_address, data, value, status, tx_hash, nonce, error, created_at, updated_at, label FROM tx_outbox WHERE id = $1 +` + +func (q *Queries) GetTxOutboxByID(ctx context.Context, id int64) (TxOutbox, error) { + row := q.db.QueryRow(ctx, getTxOutboxByID, id) + var i TxOutbox + err := row.Scan( + &i.ID, + &i.ToAddress, + &i.Data, + &i.Value, + &i.Status, + &i.TxHash, + &i.Nonce, + &i.Error, + &i.CreatedAt, + &i.UpdatedAt, + &i.Label, + ) return i, err } -const insertBatchConfig = `-- name: InsertBatchConfig :exec -INSERT INTO tendermint_batch_config (keyper_config_index, height, keypers, threshold, started, activation_block_number) -VALUES ($1, $2, $3, $4, $5, $6) +const insertDKGAccusation = `-- name: InsertDKGAccusation :exec +INSERT INTO dkg_accusations (keyper_set_index, retry_counter, accuser_index, accused_index) +VALUES ($1, $2, $3, $4) +ON CONFLICT DO NOTHING ` -type InsertBatchConfigParams struct { - KeyperConfigIndex int32 - Height int64 - Keypers []string - Threshold int32 - Started bool - ActivationBlockNumber int64 +type InsertDKGAccusationParams struct { + KeyperSetIndex int64 + RetryCounter int64 + AccuserIndex int64 + AccusedIndex int64 } -func (q *Queries) InsertBatchConfig(ctx context.Context, arg InsertBatchConfigParams) error { - _, err := q.db.Exec(ctx, insertBatchConfig, - arg.KeyperConfigIndex, - arg.Height, - arg.Keypers, - arg.Threshold, - arg.Started, - arg.ActivationBlockNumber, +func (q *Queries) InsertDKGAccusation(ctx context.Context, arg InsertDKGAccusationParams) error { + _, err := q.db.Exec(ctx, insertDKGAccusation, + arg.KeyperSetIndex, + arg.RetryCounter, + arg.AccuserIndex, + arg.AccusedIndex, + ) + return err +} + +const insertDKGApology = `-- name: InsertDKGApology :exec +INSERT INTO dkg_apologies (keyper_set_index, retry_counter, apologizer_index, accuser_index, poly_eval) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT DO NOTHING +` + +type InsertDKGApologyParams struct { + KeyperSetIndex int64 + RetryCounter int64 + ApologizerIndex int64 + AccuserIndex int64 + PolyEval []byte +} + +func (q *Queries) InsertDKGApology(ctx context.Context, arg InsertDKGApologyParams) error { + _, err := q.db.Exec(ctx, insertDKGApology, + arg.KeyperSetIndex, + arg.RetryCounter, + arg.ApologizerIndex, + arg.AccuserIndex, + arg.PolyEval, + ) + return err +} + +const insertDKGInitialState = `-- name: InsertDKGInitialState :exec +INSERT INTO dkg_initial_states (keyper_set_index, retry_counter, puredkg_bytes) +VALUES ($1, $2, $3) +ON CONFLICT DO NOTHING +` + +type InsertDKGInitialStateParams struct { + KeyperSetIndex int64 + RetryCounter int64 + PuredkgBytes []byte +} + +func (q *Queries) InsertDKGInitialState(ctx context.Context, arg InsertDKGInitialStateParams) error { + _, err := q.db.Exec(ctx, insertDKGInitialState, arg.KeyperSetIndex, arg.RetryCounter, arg.PuredkgBytes) + return err +} + +const insertDKGPolyCommitment = `-- name: InsertDKGPolyCommitment :exec +INSERT INTO dkg_poly_commitments (keyper_set_index, retry_counter, keyper_index, commitment) +VALUES ($1, $2, $3, $4) +ON CONFLICT DO NOTHING +` + +type InsertDKGPolyCommitmentParams struct { + KeyperSetIndex int64 + RetryCounter int64 + KeyperIndex int64 + Commitment []byte +} + +func (q *Queries) InsertDKGPolyCommitment(ctx context.Context, arg InsertDKGPolyCommitmentParams) error { + _, err := q.db.Exec(ctx, insertDKGPolyCommitment, + arg.KeyperSetIndex, + arg.RetryCounter, + arg.KeyperIndex, + arg.Commitment, + ) + return err +} + +const insertDKGPolyEval = `-- name: InsertDKGPolyEval :exec +INSERT INTO dkg_poly_evals (keyper_set_index, retry_counter, sender_index, receiver_index, encrypted_eval) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT DO NOTHING +` + +type InsertDKGPolyEvalParams struct { + KeyperSetIndex int64 + RetryCounter int64 + SenderIndex int64 + ReceiverIndex int64 + EncryptedEval []byte +} + +func (q *Queries) InsertDKGPolyEval(ctx context.Context, arg InsertDKGPolyEvalParams) error { + _, err := q.db.Exec(ctx, insertDKGPolyEval, + arg.KeyperSetIndex, + arg.RetryCounter, + arg.SenderIndex, + arg.ReceiverIndex, + arg.EncryptedEval, ) return err } @@ -652,6 +757,28 @@ func (q *Queries) InsertDKGResult(ctx context.Context, arg InsertDKGResultParams return err } +const insertDKGSentAction = `-- name: InsertDKGSentAction :exec +INSERT INTO dkg_sent_actions (keyper_set_index, retry_counter, action, tx_outbox_id) +VALUES ($1, $2, $3, $4) +` + +type InsertDKGSentActionParams struct { + KeyperSetIndex int64 + RetryCounter int64 + Action string + TxOutboxID sql.NullInt64 +} + +func (q *Queries) InsertDKGSentAction(ctx context.Context, arg InsertDKGSentActionParams) error { + _, err := q.db.Exec(ctx, insertDKGSentAction, + arg.KeyperSetIndex, + arg.RetryCounter, + arg.Action, + arg.TxOutboxID, + ) + return err +} + const insertDecryptionKey = `-- name: InsertDecryptionKey :execresult INSERT INTO decryption_key (eon, epoch_id, decryption_key) VALUES ($1, $2, $3) @@ -691,159 +818,101 @@ func (q *Queries) InsertDecryptionKeyShare(ctx context.Context, arg InsertDecryp return err } -const insertEncryptionKey = `-- name: InsertEncryptionKey :exec -INSERT INTO tendermint_encryption_key (address, encryption_public_key, height) -VALUES ($1, $2, $3) -ON CONFLICT (address, height) DO UPDATE -SET encryption_public_key = EXCLUDED.encryption_public_key -` - -type InsertEncryptionKeyParams struct { - Address string - EncryptionPublicKey []byte - Height int64 -} - -func (q *Queries) InsertEncryptionKey(ctx context.Context, arg InsertEncryptionKeyParams) error { - _, err := q.db.Exec(ctx, insertEncryptionKey, arg.Address, arg.EncryptionPublicKey, arg.Height) - return err -} - const insertEon = `-- name: InsertEon :exec -INSERT INTO eons (eon, height, activation_block_number, keyper_config_index) -VALUES ($1, $2, $3, $4) +INSERT INTO eons (eon, activation_block_number, keyper_config_index, dkg_contract, phase_length, lead_length, max_retries) +VALUES ($1, $2, $3, $4, $5, $6, $7) ` type InsertEonParams struct { Eon int64 - Height int64 ActivationBlockNumber int64 KeyperConfigIndex int64 + DkgContract sql.NullString + PhaseLength sql.NullInt64 + LeadLength sql.NullInt64 + MaxRetries int64 } func (q *Queries) InsertEon(ctx context.Context, arg InsertEonParams) error { _, err := q.db.Exec(ctx, insertEon, arg.Eon, - arg.Height, arg.ActivationBlockNumber, arg.KeyperConfigIndex, + arg.DkgContract, + arg.PhaseLength, + arg.LeadLength, + arg.MaxRetries, ) return err } -const insertEonPublicKey = `-- name: InsertEonPublicKey :exec -INSERT INTO outgoing_eon_keys (eon_public_key, eon) -VALUES ($1, $2) +const insertPendingTx = `-- name: InsertPendingTx :one +INSERT INTO tx_outbox (to_address, data, value, label) +VALUES ($1, $2, $3, $4) +RETURNING id ` -type InsertEonPublicKeyParams struct { - EonPublicKey []byte - Eon int64 +type InsertPendingTxParams struct { + ToAddress string + Data []byte + Value pgtype.Numeric + Label string } -func (q *Queries) InsertEonPublicKey(ctx context.Context, arg InsertEonPublicKeyParams) error { - _, err := q.db.Exec(ctx, insertEonPublicKey, arg.EonPublicKey, arg.Eon) - return err +func (q *Queries) InsertPendingTx(ctx context.Context, arg InsertPendingTxParams) (int64, error) { + row := q.db.QueryRow(ctx, insertPendingTx, + arg.ToAddress, + arg.Data, + arg.Value, + arg.Label, + ) + var id int64 + err := row.Scan(&id) + return id, err } -const insertPolyEval = `-- name: InsertPolyEval :exec -INSERT INTO poly_evals (eon, receiver_address, eval) -VALUES ($1, $2, $3) +const markTxConfirmed = `-- name: MarkTxConfirmed :exec +UPDATE tx_outbox +SET status = 'confirmed', updated_at = NOW() +WHERE id = $1 ` -type InsertPolyEvalParams struct { - Eon int64 - ReceiverAddress string - Eval []byte -} - -func (q *Queries) InsertPolyEval(ctx context.Context, arg InsertPolyEvalParams) error { - _, err := q.db.Exec(ctx, insertPolyEval, arg.Eon, arg.ReceiverAddress, arg.Eval) +func (q *Queries) MarkTxConfirmed(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, markTxConfirmed, id) return err } -const insertPureDKG = `-- name: InsertPureDKG :exec -INSERT INTO puredkg (eon, puredkg) VALUES ($1, $2) -ON CONFLICT (eon) DO UPDATE SET puredkg=EXCLUDED.puredkg +const markTxFailed = `-- name: MarkTxFailed :exec +UPDATE tx_outbox +SET status = 'failed', error = $2, updated_at = NOW() +WHERE id = $1 ` -type InsertPureDKGParams struct { - Eon int64 - Puredkg []byte +type MarkTxFailedParams struct { + ID int64 + Error sql.NullString } -func (q *Queries) InsertPureDKG(ctx context.Context, arg InsertPureDKGParams) error { - _, err := q.db.Exec(ctx, insertPureDKG, arg.Eon, arg.Puredkg) +func (q *Queries) MarkTxFailed(ctx context.Context, arg MarkTxFailedParams) error { + _, err := q.db.Exec(ctx, markTxFailed, arg.ID, arg.Error) return err } -const polyEvalsWithEncryptionKeys = `-- name: PolyEvalsWithEncryptionKeys :many -WITH latest_keys AS ( - SELECT DISTINCT ON (address) address, encryption_public_key, height - FROM tendermint_encryption_key - ORDER BY address, height DESC -) -SELECT ev.eon, ev.receiver_address, ev.eval, - k.encryption_public_key, - eon.height -FROM poly_evals ev -INNER JOIN latest_keys k - ON ev.receiver_address = k.address -INNER JOIN eons eon - ON ev.eon = eon.eon -ORDER BY ev.eon -` - -type PolyEvalsWithEncryptionKeysRow struct { - Eon int64 - ReceiverAddress string - Eval []byte - EncryptionPublicKey []byte - Height int64 -} - -func (q *Queries) PolyEvalsWithEncryptionKeys(ctx context.Context) ([]PolyEvalsWithEncryptionKeysRow, error) { - rows, err := q.db.Query(ctx, polyEvalsWithEncryptionKeys) - if err != nil { - return nil, err - } - defer rows.Close() - var items []PolyEvalsWithEncryptionKeysRow - for rows.Next() { - var i PolyEvalsWithEncryptionKeysRow - if err := rows.Scan( - &i.Eon, - &i.ReceiverAddress, - &i.Eval, - &i.EncryptionPublicKey, - &i.Height, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const scheduleSerializedShutterMessage = `-- name: ScheduleSerializedShutterMessage :one -INSERT INTO tendermint_outgoing_messages (description, msg) -VALUES ($1, $2) -RETURNING id +const markTxSubmitted = `-- name: MarkTxSubmitted :exec +UPDATE tx_outbox +SET status = 'submitted', tx_hash = $2, nonce = $3, updated_at = NOW() +WHERE id = $1 ` -type ScheduleSerializedShutterMessageParams struct { - Description string - Msg []byte +type MarkTxSubmittedParams struct { + ID int64 + TxHash sql.NullString + Nonce sql.NullInt64 } -func (q *Queries) ScheduleSerializedShutterMessage(ctx context.Context, arg ScheduleSerializedShutterMessageParams) (int32, error) { - row := q.db.QueryRow(ctx, scheduleSerializedShutterMessage, arg.Description, arg.Msg) - var id int32 - err := row.Scan(&id) - return id, err +func (q *Queries) MarkTxSubmitted(ctx context.Context, arg MarkTxSubmittedParams) error { + _, err := q.db.Exec(ctx, markTxSubmitted, arg.ID, arg.TxHash, arg.Nonce) + return err } const selectDecryptionKeyShares = `-- name: SelectDecryptionKeyShares :many @@ -881,88 +950,19 @@ func (q *Queries) SelectDecryptionKeyShares(ctx context.Context, arg SelectDecry return items, nil } -const selectPureDKG = `-- name: SelectPureDKG :many -SELECT eon, puredkg FROM puredkg -` - -func (q *Queries) SelectPureDKG(ctx context.Context) ([]Puredkg, error) { - rows, err := q.db.Query(ctx, selectPureDKG) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Puredkg - for rows.Next() { - var i Puredkg - if err := rows.Scan(&i.Eon, &i.Puredkg); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const setBatchConfigStarted = `-- name: SetBatchConfigStarted :exec -UPDATE tendermint_batch_config SET started = TRUE -WHERE keyper_config_index = $1 -` - -func (q *Queries) SetBatchConfigStarted(ctx context.Context, keyperConfigIndex int32) error { - _, err := q.db.Exec(ctx, setBatchConfigStarted, keyperConfigIndex) - return err -} - -const setLastBatchConfigProcessed = `-- name: SetLastBatchConfigProcessed :exec -INSERT INTO last_batch_config_sent (keyper_config_index) VALUES ($1) -ON CONFLICT (enforce_one_row) DO UPDATE -SET keyper_config_index = $1 -` - -func (q *Queries) SetLastBatchConfigProcessed(ctx context.Context, keyperConfigIndex int64) error { - _, err := q.db.Exec(ctx, setLastBatchConfigProcessed, keyperConfigIndex) - return err -} - -const setLastBlockSeen = `-- name: SetLastBlockSeen :exec -INSERT INTO last_block_seen (block_number) VALUES ($1) -ON CONFLICT (enforce_one_row) DO UPDATE -SET block_number = $1 -` - -func (q *Queries) SetLastBlockSeen(ctx context.Context, blockNumber int64) error { - _, err := q.db.Exec(ctx, setLastBlockSeen, blockNumber) - return err -} - -const tMGetSyncMeta = `-- name: TMGetSyncMeta :one -SELECT current_block, last_committed_height, sync_timestamp -FROM tendermint_sync_meta -ORDER BY current_block DESC, last_committed_height DESC -LIMIT 1 -` - -func (q *Queries) TMGetSyncMeta(ctx context.Context) (TendermintSyncMetum, error) { - row := q.db.QueryRow(ctx, tMGetSyncMeta) - var i TendermintSyncMetum - err := row.Scan(&i.CurrentBlock, &i.LastCommittedHeight, &i.SyncTimestamp) - return i, err -} - -const tMSetSyncMeta = `-- name: TMSetSyncMeta :exec -INSERT INTO tendermint_sync_meta (current_block, last_committed_height, sync_timestamp) -VALUES ($1, $2, $3) +const upsertECIESKey = `-- name: UpsertECIESKey :exec +INSERT INTO ecies_keys (keyper_address, ecies_public_key) +VALUES ($1, $2) +ON CONFLICT (keyper_address) DO UPDATE +SET ecies_public_key = EXCLUDED.ecies_public_key ` -type TMSetSyncMetaParams struct { - CurrentBlock int64 - LastCommittedHeight int64 - SyncTimestamp time.Time +type UpsertECIESKeyParams struct { + KeyperAddress string + EciesPublicKey []byte } -func (q *Queries) TMSetSyncMeta(ctx context.Context, arg TMSetSyncMetaParams) error { - _, err := q.db.Exec(ctx, tMSetSyncMeta, arg.CurrentBlock, arg.LastCommittedHeight, arg.SyncTimestamp) +func (q *Queries) UpsertECIESKey(ctx context.Context, arg UpsertECIESKeyParams) error { + _, err := q.db.Exec(ctx, upsertECIESKey, arg.KeyperAddress, arg.EciesPublicKey) return err } diff --git a/rolling-shutter/keyper/database/models.sqlc.gen.go b/rolling-shutter/keyper/database/models.sqlc.gen.go index 1a95f6154..77aa0e3b3 100644 --- a/rolling-shutter/keyper/database/models.sqlc.gen.go +++ b/rolling-shutter/keyper/database/models.sqlc.gen.go @@ -7,6 +7,8 @@ package database import ( "database/sql" "time" + + "github.com/jackc/pgtype" ) type DecryptionKey struct { @@ -26,69 +28,81 @@ type DecryptionTrigger struct { EpochID []byte } -type DkgResult struct { - Eon int64 - Success bool - Error sql.NullString - PureResult []byte -} - -type Eon struct { - Eon int64 - Height int64 - ActivationBlockNumber int64 - KeyperConfigIndex int64 +type DkgAccusation struct { + KeyperSetIndex int64 + RetryCounter int64 + AccuserIndex int64 + AccusedIndex int64 } -type LastBatchConfigSent struct { - EnforceOneRow bool - KeyperConfigIndex int64 +type DkgApology struct { + KeyperSetIndex int64 + RetryCounter int64 + ApologizerIndex int64 + AccuserIndex int64 + PolyEval []byte } -type LastBlockSeen struct { - EnforceOneRow bool - BlockNumber int64 +type DkgInitialState struct { + KeyperSetIndex int64 + RetryCounter int64 + PuredkgBytes []byte } -type OutgoingEonKey struct { - EonPublicKey []byte - Eon int64 +type DkgPolyCommitment struct { + KeyperSetIndex int64 + RetryCounter int64 + KeyperIndex int64 + Commitment []byte } -type PolyEval struct { - Eon int64 - ReceiverAddress string - Eval []byte +type DkgPolyEval struct { + KeyperSetIndex int64 + RetryCounter int64 + SenderIndex int64 + ReceiverIndex int64 + EncryptedEval []byte } -type Puredkg struct { - Eon int64 - Puredkg []byte +type DkgResult struct { + Eon int64 + Success bool + Error sql.NullString + PureResult []byte } -type TendermintBatchConfig struct { - KeyperConfigIndex int32 - Height int64 - Keypers []string - Threshold int32 - Started bool - ActivationBlockNumber int64 +type DkgSentAction struct { + KeyperSetIndex int64 + RetryCounter int64 + Action string + TxOutboxID sql.NullInt64 } -type TendermintEncryptionKey struct { - Address string - EncryptionPublicKey []byte - Height int64 +type EciesKey struct { + KeyperAddress string + EciesPublicKey []byte } -type TendermintOutgoingMessage struct { - ID int32 - Description string - Msg []byte +type Eon struct { + Eon int64 + ActivationBlockNumber int64 + KeyperConfigIndex int64 + DkgContract sql.NullString + PhaseLength sql.NullInt64 + LeadLength sql.NullInt64 + MaxRetries int64 } -type TendermintSyncMetum struct { - CurrentBlock int64 - LastCommittedHeight int64 - SyncTimestamp time.Time +type TxOutbox struct { + ID int64 + ToAddress string + Data []byte + Value pgtype.Numeric + Status string + TxHash sql.NullString + Nonce sql.NullInt64 + Error sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime + Label string } diff --git a/rolling-shutter/keyper/database/sql/migrations/V2_updatable_encryption_keys.sql b/rolling-shutter/keyper/database/sql/migrations/V002_updatable_encryption_keys.sql similarity index 100% rename from rolling-shutter/keyper/database/sql/migrations/V2_updatable_encryption_keys.sql rename to rolling-shutter/keyper/database/sql/migrations/V002_updatable_encryption_keys.sql diff --git a/rolling-shutter/keyper/database/sql/migrations/V003_ecies_keys.sql b/rolling-shutter/keyper/database/sql/migrations/V003_ecies_keys.sql new file mode 100644 index 000000000..33b0618c9 --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V003_ecies_keys.sql @@ -0,0 +1,7 @@ +-- schema-version: keyper-17 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +CREATE TABLE ecies_keys ( + keyper_address text NOT NULL PRIMARY KEY, + ecies_public_key bytea NOT NULL +); diff --git a/rolling-shutter/keyper/database/sql/migrations/V004_dkg_messages.sql b/rolling-shutter/keyper/database/sql/migrations/V004_dkg_messages.sql new file mode 100644 index 000000000..28a7aa2da --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V004_dkg_messages.sql @@ -0,0 +1,38 @@ +-- schema-version: keyper-18 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +CREATE TABLE dkg_poly_commitment ( + keyper_config_index bigint NOT NULL, + retry_counter bigint NOT NULL, + keyper_index bigint NOT NULL, + commitment bytea NOT NULL, + PRIMARY KEY (keyper_config_index, retry_counter, keyper_index) +); + +CREATE TABLE dkg_poly_eval ( + keyper_config_index bigint NOT NULL, + retry_counter bigint NOT NULL, + sender_index bigint NOT NULL, + receiver_index bigint NOT NULL, + encrypted_eval bytea NOT NULL, + PRIMARY KEY (keyper_config_index, retry_counter, sender_index, receiver_index) +); + +CREATE TABLE dkg_accusation ( + keyper_config_index bigint NOT NULL, + retry_counter bigint NOT NULL, + accuser_index bigint NOT NULL, + accused_index bigint NOT NULL, + PRIMARY KEY (keyper_config_index, retry_counter, accuser_index, accused_index) +); + +CREATE TABLE dkg_apology ( + keyper_config_index bigint NOT NULL, + retry_counter bigint NOT NULL, + apologizer_index bigint NOT NULL, + accuser_index bigint NOT NULL, + poly_eval bytea NOT NULL, + PRIMARY KEY (keyper_config_index, retry_counter, apologizer_index, accuser_index) +); + +ALTER TABLE eons DROP COLUMN height; diff --git a/rolling-shutter/keyper/database/sql/migrations/V005_drop_shuttermint_tables.sql b/rolling-shutter/keyper/database/sql/migrations/V005_drop_shuttermint_tables.sql new file mode 100644 index 000000000..d626d9f75 --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V005_drop_shuttermint_tables.sql @@ -0,0 +1,12 @@ +-- schema-version: keyper-19 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +DROP TABLE IF EXISTS tendermint_batch_config; +DROP TABLE IF EXISTS tendermint_encryption_key; +DROP TABLE IF EXISTS tendermint_outgoing_messages; +DROP TABLE IF EXISTS tendermint_sync_meta; +DROP TABLE IF EXISTS poly_evals; +DROP TABLE IF EXISTS puredkg; +DROP TABLE IF EXISTS outgoing_eon_keys; +DROP TABLE IF EXISTS last_batch_config_sent; +DROP TABLE IF EXISTS last_block_seen; diff --git a/rolling-shutter/keyper/database/sql/migrations/V006_eons_per_keyperset_phase_params.sql b/rolling-shutter/keyper/database/sql/migrations/V006_eons_per_keyperset_phase_params.sql new file mode 100644 index 000000000..e755a0e3c --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V006_eons_per_keyperset_phase_params.sql @@ -0,0 +1,7 @@ +-- schema-version: keyper-20 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +ALTER TABLE eons + ADD COLUMN dkg_contract TEXT, + ADD COLUMN phase_length BIGINT, + ADD COLUMN lead_length BIGINT; diff --git a/rolling-shutter/keyper/database/sql/migrations/V007_tx_outbox.sql b/rolling-shutter/keyper/database/sql/migrations/V007_tx_outbox.sql new file mode 100644 index 000000000..ab9162ff8 --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V007_tx_outbox.sql @@ -0,0 +1,17 @@ +-- schema-version: keyper-21 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +CREATE TABLE tx_outbox ( + id BIGSERIAL PRIMARY KEY, + to_address TEXT NOT NULL, + data BYTEA NOT NULL, + value NUMERIC NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'pending', + tx_hash TEXT, + nonce BIGINT, + error TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ +); + +CREATE INDEX tx_outbox_status_idx ON tx_outbox (status, id); diff --git a/rolling-shutter/keyper/database/sql/migrations/V008_tx_outbox_label.sql b/rolling-shutter/keyper/database/sql/migrations/V008_tx_outbox_label.sql new file mode 100644 index 000000000..7d54ab4ff --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V008_tx_outbox_label.sql @@ -0,0 +1,5 @@ +-- schema-version: keyper-22 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +ALTER TABLE tx_outbox + ADD COLUMN label TEXT NOT NULL DEFAULT ''; diff --git a/rolling-shutter/keyper/database/sql/migrations/V009_dkg_initial_states_and_renames.sql b/rolling-shutter/keyper/database/sql/migrations/V009_dkg_initial_states_and_renames.sql new file mode 100644 index 000000000..fce2be706 --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V009_dkg_initial_states_and_renames.sql @@ -0,0 +1,14 @@ +-- schema-version: keyper-23 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +ALTER TABLE dkg_poly_commitment RENAME TO dkg_poly_commitments; +ALTER TABLE dkg_poly_eval RENAME TO dkg_poly_evals; +ALTER TABLE dkg_accusation RENAME TO dkg_accusations; +ALTER TABLE dkg_apology RENAME TO dkg_apologies; + +CREATE TABLE dkg_initial_states ( + keyper_config_index bigint NOT NULL, + retry_counter bigint NOT NULL, + puredkg_bytes bytea NOT NULL, + PRIMARY KEY (keyper_config_index, retry_counter) +); diff --git a/rolling-shutter/keyper/database/sql/migrations/V010_dkg_sent_actions.sql b/rolling-shutter/keyper/database/sql/migrations/V010_dkg_sent_actions.sql new file mode 100644 index 000000000..bc8d0cbf1 --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V010_dkg_sent_actions.sql @@ -0,0 +1,10 @@ +-- schema-version: keyper-24 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +CREATE TABLE dkg_sent_actions ( + keyper_config_index bigint NOT NULL, + retry_counter bigint NOT NULL, + action text NOT NULL, + outbox_id bigint NOT NULL REFERENCES tx_outbox(id), + PRIMARY KEY (keyper_config_index, retry_counter, action) +); diff --git a/rolling-shutter/keyper/database/sql/migrations/V011_dkg_sent_actions_tx_outbox_id_nullable.sql b/rolling-shutter/keyper/database/sql/migrations/V011_dkg_sent_actions_tx_outbox_id_nullable.sql new file mode 100644 index 000000000..2d92188ac --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V011_dkg_sent_actions_tx_outbox_id_nullable.sql @@ -0,0 +1,5 @@ +-- schema-version: keyper-25 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +ALTER TABLE dkg_sent_actions RENAME COLUMN outbox_id TO tx_outbox_id; +ALTER TABLE dkg_sent_actions ALTER COLUMN tx_outbox_id DROP NOT NULL; diff --git a/rolling-shutter/keyper/database/sql/migrations/V012_rename_keyper_config_index_to_keyper_set_index.sql b/rolling-shutter/keyper/database/sql/migrations/V012_rename_keyper_config_index_to_keyper_set_index.sql new file mode 100644 index 000000000..4c04cced8 --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V012_rename_keyper_config_index_to_keyper_set_index.sql @@ -0,0 +1,9 @@ +-- schema-version: keyper-26 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +ALTER TABLE dkg_poly_commitments RENAME COLUMN keyper_config_index TO keyper_set_index; +ALTER TABLE dkg_poly_evals RENAME COLUMN keyper_config_index TO keyper_set_index; +ALTER TABLE dkg_accusations RENAME COLUMN keyper_config_index TO keyper_set_index; +ALTER TABLE dkg_apologies RENAME COLUMN keyper_config_index TO keyper_set_index; +ALTER TABLE dkg_initial_states RENAME COLUMN keyper_config_index TO keyper_set_index; +ALTER TABLE dkg_sent_actions RENAME COLUMN keyper_config_index TO keyper_set_index; diff --git a/rolling-shutter/keyper/database/sql/migrations/V013_eons_max_retries.sql b/rolling-shutter/keyper/database/sql/migrations/V013_eons_max_retries.sql new file mode 100644 index 000000000..a791e5833 --- /dev/null +++ b/rolling-shutter/keyper/database/sql/migrations/V013_eons_max_retries.sql @@ -0,0 +1,5 @@ +-- schema-version: keyper-27 -- +-- migrations need to start from V2... as file name, as the V1 was initial schema + +ALTER TABLE eons + ADD COLUMN max_retries BIGINT NOT NULL; diff --git a/rolling-shutter/keyper/database/sql/queries/keyper.sql b/rolling-shutter/keyper/database/sql/queries/keyper.sql index c6119d84e..4b90ca633 100644 --- a/rolling-shutter/keyper/database/sql/queries/keyper.sql +++ b/rolling-shutter/keyper/database/sql/queries/keyper.sql @@ -38,100 +38,9 @@ SELECT EXISTS ( SELECT count(*) FROM decryption_key_share WHERE eon = $1 AND epoch_id = $2; --- name: InsertBatchConfig :exec -INSERT INTO tendermint_batch_config (keyper_config_index, height, keypers, threshold, started, activation_block_number) -VALUES ($1, $2, $3, $4, $5, $6); - --- name: CountBatchConfigs :one -SELECT count(*) FROM tendermint_batch_config; - --- name: GetLatestBatchConfig :one -SELECT * -FROM tendermint_batch_config -ORDER BY keyper_config_index DESC -LIMIT 1; - --- name: CountBatchConfigsInBlockRange :one -SELECT COUNT(*) -FROM tendermint_batch_config -WHERE @start_block <= activation_block_number AND activation_block_number < @end_block; - --- name: CountBatchConfigsInBlockRangeWithKeyper :one --- Due to https://github.com/sqlc-dev/sqlc/issues/3083 we need to use this awkward construction to pass and query for the keyper address parameter as a single element slice -SELECT COUNT(*) -FROM tendermint_batch_config -WHERE (@keyper_address::TEXT[]) && keypers AND @start_block <= activation_block_number AND activation_block_number < @end_block; - --- name: GetBatchConfigs :many -SELECT * -FROM tendermint_batch_config -ORDER BY keyper_config_index; - --- name: GetBatchConfig :one -SELECT * -FROM tendermint_batch_config -WHERE keyper_config_index = $1; - --- name: SetBatchConfigStarted :exec -UPDATE tendermint_batch_config SET started = TRUE -WHERE keyper_config_index = $1; - --- name: TMSetSyncMeta :exec -INSERT INTO tendermint_sync_meta (current_block, last_committed_height, sync_timestamp) -VALUES ($1, $2, $3); - --- name: TMGetSyncMeta :one -SELECT * -FROM tendermint_sync_meta -ORDER BY current_block DESC, last_committed_height DESC -LIMIT 1; - --- name: GetLastCommittedHeight :one -SELECT last_committed_height -FROM tendermint_sync_meta -ORDER BY current_block DESC, last_committed_height DESC -LIMIT 1; - --- name: InsertPureDKG :exec -INSERT INTO puredkg (eon, puredkg) VALUES ($1, $2) -ON CONFLICT (eon) DO UPDATE SET puredkg=EXCLUDED.puredkg; - --- name: SelectPureDKG :many -SELECT * FROM puredkg; - --- name: DeletePureDKG :exec -DELETE FROM puredkg WHERE eon=$1; - --- name: InsertEncryptionKey :exec -INSERT INTO tendermint_encryption_key (address, encryption_public_key, height) -VALUES ($1, $2, $3) -ON CONFLICT (address, height) DO UPDATE -SET encryption_public_key = EXCLUDED.encryption_public_key; - --- name: GetEncryptionKeys :many -SELECT DISTINCT ON (address) * -FROM tendermint_encryption_key -ORDER BY address, height DESC; - --- name: ScheduleSerializedShutterMessage :one -INSERT INTO tendermint_outgoing_messages (description, msg) -VALUES ($1, $2) -RETURNING id; - --- name: GetNextShutterMessage :one -SELECT * from tendermint_outgoing_messages -ORDER BY id -LIMIT 1; - --- name: DeleteShutterMessage :exec -DELETE FROM tendermint_outgoing_messages WHERE id=$1; - --- name: DeleteShutterMessageByDesc :exec -DELETE FROM tendermint_outgoing_messages WHERE description=$1; - -- name: InsertEon :exec -INSERT INTO eons (eon, height, activation_block_number, keyper_config_index) -VALUES ($1, $2, $3, $4); +INSERT INTO eons (eon, activation_block_number, keyper_config_index, dkg_contract, phase_length, lead_length, max_retries) +VALUES ($1, $2, $3, $4, $5, $6, $7); -- name: GetEon :one SELECT * FROM eons WHERE eon=$1; @@ -139,41 +48,12 @@ SELECT * FROM eons WHERE eon=$1; -- name: GetEonForBlockNumber :one SELECT * FROM eons WHERE activation_block_number <= sqlc.arg(block_number) -ORDER BY activation_block_number DESC, height DESC +ORDER BY activation_block_number DESC LIMIT 1; -- name: GetAllEons :many SELECT * FROM eons ORDER BY eon; --- name: InsertPolyEval :exec -INSERT INTO poly_evals (eon, receiver_address, eval) -VALUES ($1, $2, $3); - --- name: PolyEvalsWithEncryptionKeys :many -WITH latest_keys AS ( - SELECT DISTINCT ON (address) address, encryption_public_key, height - FROM tendermint_encryption_key - ORDER BY address, height DESC -) -SELECT ev.eon, ev.receiver_address, ev.eval, - k.encryption_public_key, - eon.height -FROM poly_evals ev -INNER JOIN latest_keys k - ON ev.receiver_address = k.address -INNER JOIN eons eon - ON ev.eon = eon.eon -ORDER BY ev.eon; - --- PolyEvalsWithEncryptionKeys could probably already delete the entries from the poly_evals table. --- I wasn't able to make this work, because of bugs in sqlc - --- name: DeletePolyEval :exec -DELETE FROM poly_evals ev WHERE ev.eon=$1 AND ev.receiver_address=$2; - --- name: DeletePolyEvalByEon :execresult -DELETE FROM poly_evals ev WHERE ev.eon=$1; - -- name: InsertDKGResult :exec INSERT INTO dkg_result (eon,success,error,pure_result) VALUES ($1,$2,$3,$4); @@ -182,10 +62,15 @@ VALUES ($1,$2,$3,$4); SELECT * FROM dkg_result WHERE eon = $1; +-- name: ExistsDKGResultSuccess :one +SELECT EXISTS ( + SELECT 1 FROM dkg_result WHERE eon = $1 AND success = TRUE +); + -- name: GetDKGResultForBlockNumber :one SELECT * FROM dkg_result WHERE eon = (SELECT eon FROM eons WHERE activation_block_number <= sqlc.arg(block_number) -ORDER BY activation_block_number DESC, height DESC +ORDER BY activation_block_number DESC LIMIT 1); -- name: GetDKGResultForKeyperConfigIndex :one @@ -196,42 +81,6 @@ WHERE eon = (SELECT max(eon) FROM eons WHERE keyper_config_index = $1); SELECT * FROM dkg_result ORDER BY eon ASC; --- name: InsertEonPublicKey :exec -INSERT INTO outgoing_eon_keys (eon_public_key, eon) -VALUES ($1, $2); - --- name: GetAndDeleteEonPublicKeys :many -WITH t1 AS (DELETE FROM outgoing_eon_keys RETURNING *) -SELECT t1.*, eons.activation_block_number, tbc.keypers, tbc.keyper_config_index -FROM t1 -INNER JOIN eons - ON t1.eon = eons.eon -INNER JOIN tendermint_batch_config tbc - ON eons.keyper_config_index = tbc.keyper_config_index; - --- name: SetLastBatchConfigProcessed :exec -INSERT INTO last_batch_config_sent (keyper_config_index) VALUES ($1) -ON CONFLICT (enforce_one_row) DO UPDATE -SET keyper_config_index = $1; - --- name: GetLastBatchConfigProcessed :one -SELECT keyper_config_index FROM last_batch_config_sent LIMIT 1; - - --- name: SetLastBlockSeen :exec -INSERT INTO last_block_seen (block_number) VALUES ($1) -ON CONFLICT (enforce_one_row) DO UPDATE -SET block_number = $1; - --- name: GetLastBlockSeen :one -SELECT block_number FROM last_block_seen LIMIT 1; - --- name: GetKeyperStateForEon :one -SELECT (@keyper_address::TEXT[] && tbc.keypers)::BOOL AS is_keyper -FROM tendermint_batch_config AS tbc -LEFT JOIN eons ON eons.keyper_config_index = tbc.keyper_config_index -WHERE eons.eon = @eon; - -- name: GetLatestEonForKeyperConfig :one SELECT max(eons.eon)::INT FROM eons @@ -243,3 +92,109 @@ FROM eons WHERE keyper_config_index = $1 ORDER BY eon DESC LIMIT 1; + +-- name: UpsertECIESKey :exec +INSERT INTO ecies_keys (keyper_address, ecies_public_key) +VALUES ($1, $2) +ON CONFLICT (keyper_address) DO UPDATE +SET ecies_public_key = EXCLUDED.ecies_public_key; + +-- name: GetECIESKey :one +SELECT * FROM ecies_keys WHERE keyper_address = $1; + +-- name: ExistsECIESKey :one +SELECT EXISTS ( + SELECT 1 FROM ecies_keys WHERE keyper_address = $1 +); + +-- name: InsertDKGPolyCommitment :exec +INSERT INTO dkg_poly_commitments (keyper_set_index, retry_counter, keyper_index, commitment) +VALUES ($1, $2, $3, $4) +ON CONFLICT DO NOTHING; + +-- name: GetDKGPolyCommitments :many +SELECT * FROM dkg_poly_commitments +WHERE keyper_set_index = $1 AND retry_counter = $2 +ORDER BY keyper_index; + +-- name: InsertDKGPolyEval :exec +INSERT INTO dkg_poly_evals (keyper_set_index, retry_counter, sender_index, receiver_index, encrypted_eval) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT DO NOTHING; + +-- name: GetDKGPolyEvals :many +SELECT * FROM dkg_poly_evals +WHERE keyper_set_index = $1 AND retry_counter = $2 +ORDER BY sender_index, receiver_index; + +-- name: InsertDKGAccusation :exec +INSERT INTO dkg_accusations (keyper_set_index, retry_counter, accuser_index, accused_index) +VALUES ($1, $2, $3, $4) +ON CONFLICT DO NOTHING; + +-- name: GetDKGAccusations :many +SELECT * FROM dkg_accusations +WHERE keyper_set_index = $1 AND retry_counter = $2 +ORDER BY accuser_index, accused_index; + +-- name: InsertDKGApology :exec +INSERT INTO dkg_apologies (keyper_set_index, retry_counter, apologizer_index, accuser_index, poly_eval) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT DO NOTHING; + +-- name: GetDKGApologies :many +SELECT * FROM dkg_apologies +WHERE keyper_set_index = $1 AND retry_counter = $2 +ORDER BY apologizer_index, accuser_index; + +-- name: InsertDKGInitialState :exec +INSERT INTO dkg_initial_states (keyper_set_index, retry_counter, puredkg_bytes) +VALUES ($1, $2, $3) +ON CONFLICT DO NOTHING; + +-- name: GetDKGInitialState :one +SELECT * FROM dkg_initial_states +WHERE keyper_set_index = $1 AND retry_counter = $2; + +-- name: InsertDKGSentAction :exec +INSERT INTO dkg_sent_actions (keyper_set_index, retry_counter, action, tx_outbox_id) +VALUES ($1, $2, $3, $4); + +-- name: ExistsDKGSentAction :one +SELECT EXISTS ( + SELECT 1 FROM dkg_sent_actions + WHERE keyper_set_index = $1 AND retry_counter = $2 AND action = $3 +); + +-- name: InsertPendingTx :one +INSERT INTO tx_outbox (to_address, data, value, label) +VALUES ($1, $2, $3, $4) +RETURNING id; + +-- name: GetPendingTxs :many +SELECT * FROM tx_outbox +WHERE status = 'pending' +ORDER BY id; + +-- name: GetSubmittedTxs :many +SELECT * FROM tx_outbox +WHERE status = 'submitted' +ORDER BY id; + +-- name: GetTxOutboxByID :one +SELECT * FROM tx_outbox WHERE id = $1; + +-- name: MarkTxSubmitted :exec +UPDATE tx_outbox +SET status = 'submitted', tx_hash = $2, nonce = $3, updated_at = NOW() +WHERE id = $1; + +-- name: MarkTxConfirmed :exec +UPDATE tx_outbox +SET status = 'confirmed', updated_at = NOW() +WHERE id = $1; + +-- name: MarkTxFailed :exec +UPDATE tx_outbox +SET status = 'failed', error = $2, updated_at = NOW() +WHERE id = $1; diff --git a/rolling-shutter/keyper/database/sql/schemas/keyper.sql b/rolling-shutter/keyper/database/sql/schemas/keyper.sql index f50627fe2..a9c26b046 100644 --- a/rolling-shutter/keyper/database/sql/schemas/keyper.sql +++ b/rolling-shutter/keyper/database/sql/schemas/keyper.sql @@ -19,28 +19,20 @@ CREATE TABLE decryption_key ( PRIMARY KEY (eon, epoch_id) ); ------ tendermint events +----- legacy tendermint tables. Dropped in migration V5. --- store the last batch config message we sent to shuttermint. We store this in order to prevent us --- from sending the message multiple times. CREATE TABLE last_batch_config_sent( enforce_one_row BOOL PRIMARY KEY DEFAULT TRUE, keyper_config_index bigint NOT NULL ); INSERT INTO last_batch_config_sent (keyper_config_index) VALUES (0); --- store the last block number seen we sent to shuttermint. CREATE TABLE last_block_seen( enforce_one_row BOOL PRIMARY KEY DEFAULT TRUE, block_number bigint NOT NULL ); INSERT INTO last_block_seen (block_number) VALUES (-1); --- tendermint_sync_meta contains meta information about the synchronization process with the --- tendermint app. At the moment we just insert new entries into the table and sort by --- current_block to get the latest entry. When handling new events from shuttermint, we do that in --- batches inside a PostgreSQL transaction. last_committed_height is the last block that we know is --- available, current_block is the last block in the batch we're currently handling. CREATE TABLE tendermint_sync_meta ( current_block bigint NOT NULL, last_committed_height bigint NOT NULL, @@ -48,9 +40,6 @@ CREATE TABLE tendermint_sync_meta ( PRIMARY KEY (current_block, last_committed_height) ); --- puredkg contains a gob serialized puredkg instance. We already have the DKG process --- implemented in go, without any database access. When new events come in, we feed those to the --- go object and store it afterwards in the puredkg table. CREATE TABLE puredkg ( eon bigint PRIMARY KEY, puredkg BYTEA NOT NULL diff --git a/rolling-shutter/keyper/dkgphase/phase.go b/rolling-shutter/keyper/dkgphase/phase.go deleted file mode 100644 index a925ff0dd..000000000 --- a/rolling-shutter/keyper/dkgphase/phase.go +++ /dev/null @@ -1,64 +0,0 @@ -// Package dkgphase contains the PhaseLength struct, which is used to determine the DKG phase given -// a block number. -package dkgphase - -import ( - "fmt" - "strconv" - - "github.com/shutter-network/shutter/shlib/puredkg" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable" -) - -// PhaseLength is used to store the accumulated lengths of the DKG phases. -type PhaseLength struct { - off int64 - dealing int64 - accusing int64 - apologizing int64 -} - -// NewConstantPhaseLength creates a new phase length definition where each phase has the same -// length. -func NewConstantPhaseLength(l int64) *PhaseLength { - return &PhaseLength{ - off: 0 * l, - dealing: 1 * l, - accusing: 2 * l, - apologizing: 3 * l, - } -} - -func (plen *PhaseLength) UnmarshalText(b []byte) error { - phaseLenInt, err := strconv.ParseInt(string(b), 10, 64) - if err != nil { - return err - } - *plen = *NewConstantPhaseLength(phaseLenInt) - return nil -} - -func (plen PhaseLength) String() string { - return encodeable.String(plen) -} - -func (plen PhaseLength) MarshalText() (text []byte, err error) { - return []byte(fmt.Sprint(plen.dealing)), nil -} - -func (plen *PhaseLength) GetPhaseAtHeight(height int64, eonStartHeight int64) puredkg.Phase { - if height < eonStartHeight+plen.off { - return puredkg.Off - } - if height < eonStartHeight+plen.dealing { - return puredkg.Dealing - } - if height < eonStartHeight+plen.accusing { - return puredkg.Accusing - } - if height < eonStartHeight+plen.apologizing { - return puredkg.Apologizing - } - return puredkg.Finalized -} diff --git a/rolling-shutter/keyper/eonpkhandler.go b/rolling-shutter/keyper/eonpkhandler.go deleted file mode 100644 index 9601161c1..000000000 --- a/rolling-shutter/keyper/eonpkhandler.go +++ /dev/null @@ -1,134 +0,0 @@ -package keyper - -import ( - "context" - "time" - - "github.com/jackc/pgx/v4/pgxpool" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" - "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" - "github.com/shutter-network/rolling-shutter/rolling-shutter/p2pmsg" -) - -var eonPubkeyTickerTime = 2 * time.Second - -type EonPublicKey struct { - PublicKey []byte - ActivationBlock uint64 - KeyperConfigIndex uint64 - Eon uint64 -} - -func newEonPubKeyHandler(keyper *KeyperCore) *eonPubKeyHandler { - return &eonPubKeyHandler{ - dbpool: keyper.dbpool, - config: keyper.config, - messaging: keyper.messaging, - eonPubkeyHandler: keyper.opts.eonPubkeyHandler, - broadcastEonPubKey: keyper.opts.broadcastEonPubKey, - stopOnErrors: false, - } -} - -type eonPubKeyHandler struct { - dbpool *pgxpool.Pool - config *kprconfig.Config - messaging p2p.Messaging - - eonPubkeyHandler EonPublicKeyHandlerFunc - broadcastEonPubKey bool - - stopOnErrors bool -} - -func (pkh *eonPubKeyHandler) Start(ctx context.Context, runner service.Runner) error { - runner.Go(func() error { - return pkh.loop(ctx) - }) - return nil -} - -func (pkh *eonPubKeyHandler) loop(ctx context.Context) error { - t := time.NewTicker(eonPubkeyTickerTime) - for { - err := pkh.queryAndHandleNewEonPubKeys(ctx) - if err != nil { - if pkh.stopOnErrors { - return err - } - log.Error().Err(err).Msg("error during handling of new eon public keys") - } - select { - case <-ctx.Done(): - t.Stop() - return ctx.Err() - case <-t.C: - } - } -} - -func (pkh *eonPubKeyHandler) broadcastEonPublicKey(ctx context.Context, eonPubKey EonPublicKey) error { - msg, err := p2pmsg.NewSignedEonPublicKey( - pkh.config.InstanceID, - eonPubKey.PublicKey, - eonPubKey.ActivationBlock, - eonPubKey.KeyperConfigIndex, - eonPubKey.Eon, - pkh.config.Ethereum.PrivateKey.Key, - ) - if err != nil { - return errors.Wrap(err, "error while signing EonPublicKey") - } - - err = pkh.messaging.SendMessage(ctx, msg) - if err != nil { - return errors.Wrap(err, "error while broadcasting EonPublicKey") - } - return nil -} - -func (pkh *eonPubKeyHandler) queryAndHandleNewEonPubKeys(ctx context.Context) error { - eonPublicKeys, err := database.New(pkh.dbpool).GetAndDeleteEonPublicKeys(ctx) - if err != nil { - return err - } - for _, eonPublicKey := range eonPublicKeys { - _, exists := database.GetKeyperIndex(pkh.config.GetAddress(), eonPublicKey.Keypers) - if !exists { - return errors.Errorf("own keyper index not found for Eon=%d", eonPublicKey.Eon) - } - activationBlock, err := medley.Int64ToUint64Safe(eonPublicKey.ActivationBlockNumber) - if err != nil { - return errors.Wrap(err, "failed safe int cast") - } - keyperIndex, err := medley.Int32ToUint64Safe(eonPublicKey.KeyperConfigIndex) - if err != nil { - return errors.Wrap(err, "failed safe int cast") - } - eon, err := medley.Int64ToUint64Safe(eonPublicKey.Eon) - if err != nil { - return errors.Wrap(err, "failed safe int cast") - } - eonPubKey := EonPublicKey{ - PublicKey: eonPublicKey.EonPublicKey, - ActivationBlock: activationBlock, - KeyperConfigIndex: keyperIndex, - Eon: eon, - } - if pkh.broadcastEonPubKey { - err := pkh.broadcastEonPublicKey(ctx, eonPubKey) - return errors.Wrap(err, "failed to broadcast eon public key") - } - if pkh.eonPubkeyHandler != nil { - err := pkh.eonPubkeyHandler(ctx, eonPubKey) - return errors.Wrap(err, "failed to handle eon public key") - } - } - return nil -} diff --git a/rolling-shutter/keyper/eonpublickey.go b/rolling-shutter/keyper/eonpublickey.go new file mode 100644 index 000000000..28401884a --- /dev/null +++ b/rolling-shutter/keyper/eonpublickey.go @@ -0,0 +1,10 @@ +package keyper + +// EonPublicKey describes a successfully generated eon public key that other +// components in the keyper stack want to consume. +type EonPublicKey struct { + PublicKey []byte + ActivationBlock uint64 + KeyperConfigIndex uint64 + Eon uint64 +} diff --git a/rolling-shutter/keyper/fx/messagesender.go b/rolling-shutter/keyper/fx/messagesender.go deleted file mode 100644 index f55585fe5..000000000 --- a/rolling-shutter/keyper/fx/messagesender.go +++ /dev/null @@ -1,156 +0,0 @@ -package fx - -import ( - "context" - "crypto/ecdsa" - "crypto/rand" - "encoding/base64" - "encoding/binary" - "fmt" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/tendermint/tendermint/rpc/client" - tmtypes "github.com/tendermint/tendermint/types" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/shutterevents/shtxresp" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -// IRetriable is an error that knows if it makes sense to retry an action. -type IRetriable interface { - error - IsRetriable() bool -} - -// RemoteError is raised for shuttermint messages that return with a result code != 0, i.e. where -// the shuttermint app generated an error. -type RemoteError struct { - msg string -} - -var _ IRetriable = &RemoteError{} - -func (remoteError *RemoteError) Error() string { - return fmt.Sprintf("remote error: %s", remoteError.msg) -} - -func (remoteError *RemoteError) IsRetriable() bool { - return false -} - -// MessageSender defines the interface of sending messages to shuttermint. -type MessageSender interface { - SendMessage(context.Context, *shmsg.Message) error -} - -// RPCMessageSender signs messages and sends them via RPC to shuttermint. -type RPCMessageSender struct { - rpcclient client.Client - chainID string - signingKey *ecdsa.PrivateKey -} - -var _ MessageSender = &RPCMessageSender{} - -// MockMessageSender sends all messages to a channel so that they can be checked for testing. -type MockMessageSender struct { - Msgs chan *shmsg.Message -} - -var _ MessageSender = &MockMessageSender{} - -var mockMessageSenderBufferSize = 0x10000 - -// NewRPCMessageSender creates a new RPCMessageSender. -func NewRPCMessageSender(cl client.Client, signingKey *ecdsa.PrivateKey) RPCMessageSender { - ms := RPCMessageSender{ - rpcclient: cl, - chainID: "", - signingKey: signingKey, - } - return ms -} - -// SendMessage signs the given shmsg.Message and sends the message to shuttermint. -func (ms *RPCMessageSender) SendMessage(ctx context.Context, msg *shmsg.Message) error { - if err := ms.maybeFetchChainID(ctx); err != nil { - return err - } - - msgWithNonce := ms.addNonceAndChainID(msg) - signedMessage, err := shmsg.SignMessage(msgWithNonce, ms.signingKey) - if err != nil { - return err - } - tx := tmtypes.Tx(base64.RawURLEncoding.EncodeToString(signedMessage)) - res, err := ms.rpcclient.BroadcastTxCommit(ctx, tx) - if err != nil { - return err - } - - if res.CheckTx.Code != 0 { - return &RemoteError{ - msg: fmt.Sprintf("checktx: %s", res.CheckTx.Log), - } - } - - switch res.DeliverTx.Code { - case shtxresp.Ok: - return nil - case shtxresp.Seen: - log.Warn().Str("tx", res.DeliverTx.Log).Msg("delivertx: message already seen, ignoring") - return nil - case shtxresp.Error: - return &RemoteError{ - msg: fmt.Sprintf("delivertx: %s", res.DeliverTx.Log), - } - } - return nil -} - -func (ms *RPCMessageSender) addNonceAndChainID(msg *shmsg.Message) *shmsg.MessageWithNonce { - return &shmsg.MessageWithNonce{ - ChainId: []byte(ms.chainID), - RandomNonce: randomNonce(), - Msg: msg, - } -} - -func (ms *RPCMessageSender) maybeFetchChainID(ctx context.Context) error { - if ms.chainID != "" { - return nil - } - - info, err := ms.rpcclient.BlockchainInfo(ctx, 0, 0) - if err != nil { - return err - } - if len(info.BlockMetas) == 0 { - return errors.Errorf("failed to fetch block meta to check chain id") - } - - ms.chainID = info.BlockMetas[0].Header.ChainID - return nil -} - -func randomNonce() uint64 { - var bytes [8]byte - if _, err := rand.Read(bytes[:]); err != nil { - panic("Failed to read random bytes for nonce.") - } - return binary.LittleEndian.Uint64(bytes[:]) -} - -// NewMockMessageSender creates a new MockMessageSender. We use a buffered channel with a rather -// large size in order to simplify writing our tests. -func NewMockMessageSender() MockMessageSender { - return MockMessageSender{ - Msgs: make(chan *shmsg.Message, mockMessageSenderBufferSize), - } -} - -func (ms *MockMessageSender) SendMessage(_ context.Context, msg *shmsg.Message) error { - ms.Msgs <- msg - return nil -} diff --git a/rolling-shutter/keyper/fx/send.go b/rolling-shutter/keyper/fx/send.go deleted file mode 100644 index 6cef51e89..000000000 --- a/rolling-shutter/keyper/fx/send.go +++ /dev/null @@ -1,54 +0,0 @@ -package fx - -import ( - "context" - - "github.com/jackc/pgx/v4" - "github.com/rs/zerolog/log" - "google.golang.org/protobuf/proto" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -// SendShutterMessages fetches shuttermint messages from the database and sends them to shuttermint -// via the given MesssageSender. -func SendShutterMessages( - ctx context.Context, queries *database.Queries, messageSender MessageSender, -) error { - for { - outgoing, err := queries.GetNextShutterMessage(ctx) - if err == pgx.ErrNoRows { - return nil - } else if err != nil { - return err - } - - msg := &shmsg.Message{} - err = proto.Unmarshal(outgoing.Msg, msg) - if err != nil { - return err - } - err = messageSender.SendMessage(ctx, msg) - if err != nil { - if !isRetrieable(msg) { - log.Err(err).Str("msg", msg.String()).Msg("sending non-retrieable msg failed") - return err - } - log.Warn().Err(err).Str("msg", msg.String()).Msg("msg not accepted, will be retried") - return nil - } - log.Info().Int32("id", outgoing.ID). - Str("description", outgoing.Description). - Msg("send shuttermint message") - err = queries.DeleteShutterMessage(ctx, outgoing.ID) - if err != nil { - return err - } - } -} - -// FIXME: isRetrieable is a no-op so far. -func isRetrieable(_ *shmsg.Message) bool { - return true -} diff --git a/rolling-shutter/keyper/keyper.go b/rolling-shutter/keyper/keyper.go index 08f391aa0..b264fed12 100644 --- a/rolling-shutter/keyper/keyper.go +++ b/rolling-shutter/keyper/keyper.go @@ -2,37 +2,24 @@ package keyper import ( "context" - "fmt" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/tendermint/tendermint/rpc/client" - tmhttp "github.com/tendermint/tendermint/rpc/client/http" - obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/contract/deployment" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/epochkghandler" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/fx" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/keypermetrics" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprapi" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/smobserver" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/broker" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/channel" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/db" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/metricsserver" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/retry" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" ) type KeyperCore struct { @@ -40,14 +27,11 @@ type KeyperCore struct { opts *options config *kprconfig.Config - dbpool *pgxpool.Pool - shuttermintClient client.Client - messaging p2p.Messaging - messageSender fx.RPCMessageSender - blockSyncClient *ethclient.Client + dbpool *pgxpool.Pool + messaging p2p.Messaging + blockSyncClient *ethclient.Client - shuttermintState *smobserver.ShuttermintState - metricsServer *metricsserver.MetricsServer + metricsServer *metricsserver.MetricsServer } func New( @@ -99,13 +83,10 @@ func LinkConfigToDB(ctx context.Context, config *kprconfig.Config, dbpool *pgxpo } func (kpr *KeyperCore) initOptions(ctx context.Context, runner service.Runner) error { - err := validateOptions(kpr.opts) - if err != nil { - return err - } if kpr.opts.dbpool == nil { // connect, but don't validate any database version. // If that is desired, it should be done in the keyper-implementation + var err error kpr.dbpool, err = db.Connect(ctx, runner, kpr.config.DatabaseURL, database.Definition.Name()) if err != nil { return err @@ -141,11 +122,6 @@ func (kpr *KeyperCore) Start(ctx context.Context, runner service.Runner) error { if err != nil { return err } - shuttermintClient, err := tmhttp.New(config.Shuttermint.ShuttermintURL, "/websocket") - if err != nil { - return err - } - messageSender := fx.NewRPCMessageSender(shuttermintClient, config.Ethereum.PrivateKey.Key) if kpr.config.Metrics.Enabled { keypermetrics.InitMetrics(kpr.dbpool, *kpr.config) @@ -154,10 +130,6 @@ func (kpr *KeyperCore) Start(ctx context.Context, runner service.Runner) error { kpr.metricsServer = metricsserver.New(kpr.config.Metrics) } - kpr.shuttermintClient = shuttermintClient - kpr.messageSender = messageSender - kpr.shuttermintState = smobserver.NewShuttermintState(config) - kpr.messaging.AddMessageHandler( epochkghandler.NewDecryptionKeyHandler(kpr.config, kpr.dbpool), epochkghandler.NewDecryptionKeyShareHandler(kpr.config, kpr.dbpool), @@ -171,8 +143,6 @@ func (kpr *KeyperCore) Start(ctx context.Context, runner service.Runner) error { func (kpr *KeyperCore) getServices() []service.Service { services := []service.Service{ kpr.messaging, - service.Function{Func: kpr.operateShuttermint}, - newEonPubKeyHandler(kpr), } keyTrigger := kpr.trigger if kpr.config.HTTPEnabled { @@ -199,234 +169,3 @@ func (kpr *KeyperCore) getServices() []service.Service { } return services } - -func (kpr *KeyperCore) handleOnChainChanges( - ctx context.Context, - tx pgx.Tx, - syncBlockNumber uint64, -) error { - log.Debug().Uint64("sync-block-number", syncBlockNumber).Msg("handle on chain changes") - err := kpr.handleOnChainKeyperSetChanges(ctx, tx, syncBlockNumber) - if err != nil { - return err - } - err = kpr.sendNewBlockSeen(ctx, tx, syncBlockNumber) - if err != nil { - return err - } - return nil -} - -// sendNewBlockSeen sends shmsg.NewBlockSeen messages to the shuttermint chain. This function sends -// NewBlockSeen messages to the shuttermint chain, so that the chain can start new batch configs if -// enough keypers have seen a block past the start block of some BatchConfig. We only send messages -// when the current block we see, could lead to a batch config being started. -func (kpr *KeyperCore) sendNewBlockSeen(ctx context.Context, tx pgx.Tx, l1BlockNumber uint64) error { - q := database.New(tx) - lastBlock, err := q.GetLastBlockSeen(ctx) - if err != nil { - return err - } - - count, err := q.CountBatchConfigsInBlockRangeWithKeyper(ctx, - database.CountBatchConfigsInBlockRangeWithKeyperParams{ - KeyperAddress: []string{kpr.config.GetAddress().String()}, - StartBlock: lastBlock, - EndBlock: int64(l1BlockNumber), - }) - if err != nil { - return err - } - if count == 0 { - return nil - } - - blockSeenMsg := shmsg.NewBlockSeen(l1BlockNumber) - err = q.ScheduleShutterMessage( - ctx, - fmt.Sprintf("block seen (block=%d)", l1BlockNumber), - blockSeenMsg, - ) - if err != nil { - return err - } - err = q.SetLastBlockSeen(ctx, int64(l1BlockNumber)) - if err != nil { - return err - } - log.Info().Uint64("block-number", l1BlockNumber).Msg("block seen") - return nil -} - -// validateBatchConfig mirrors shuttermint's BatchConfig validations (EnsureValid, checkConfig, -// BatchConfigFromMessage, and allowedToVoteOnConfigChanges) to avoid sending batch configs that -// would be rejected. -func validateBatchConfig( - latestBatchConfig database.TendermintBatchConfig, - keyperSet obskeyper.KeyperSet, - keypers []common.Address, -) error { - // mirrors EnsureValid (shutterevents/batchconfig.go) - if len(keypers) == 0 { - return errors.New("no keypers in batch config") - } - if keyperSet.Threshold <= 0 { - return errors.New("threshold must not be zero") - } - if int(keyperSet.Threshold) > len(keypers) { - return errors.Errorf("threshold %d exceeds number of keypers %d", keyperSet.Threshold, len(keypers)) - } - - // mirrors EnsureUniqueAddresses check in BatchConfigFromMessage (shutterevents/batchconfig.go) - if err := medley.EnsureUniqueAddresses(keypers); err != nil { - return errors.Wrap(err, "duplicate keyper addresses in batch config") - } - - // mirrors checkConfig (app/app.go) - if keyperSet.ActivationBlockNumber < latestBatchConfig.ActivationBlockNumber { - return errors.Errorf( - "activation block number of next config (%d) lower than current one (%d)", - keyperSet.ActivationBlockNumber, - latestBatchConfig.ActivationBlockNumber, - ) - } - if keyperSet.KeyperConfigIndex <= int64(latestBatchConfig.KeyperConfigIndex) { - return errors.Errorf( - "config index of next config (%d) not greater than current one (%d)", - keyperSet.KeyperConfigIndex, - latestBatchConfig.KeyperConfigIndex, - ) - } - - return nil -} - -// handleOnChainKeyperSetChanges looks for changes in the keyper_set table. -func (kpr *KeyperCore) handleOnChainKeyperSetChanges( - ctx context.Context, - tx pgx.Tx, - blockNumber uint64, -) error { - q := database.New(tx) - latestBatchConfig, err := q.GetLatestBatchConfig(ctx) - if err == pgx.ErrNoRows { - log.Print("no batch config found in tendermint") - return nil - } else if err != nil { - return err - } - cq := obskeyper.New(tx) - - lastProcessedConfig, err := q.GetLastBatchConfigProcessed(ctx) - if err != nil { - return err - } - - // Advance from whichever progress marker is further ahead: the latest config known to - // Tendermint or the last config we already handled locally. - nextKeyperConfigIndex := int64(latestBatchConfig.KeyperConfigIndex) - if lastProcessedConfig > nextKeyperConfigIndex { - nextKeyperConfigIndex = lastProcessedConfig - } - - keyperSet, err := cq.GetKeyperSetByKeyperConfigIndex(ctx, nextKeyperConfigIndex+1) - if err == pgx.ErrNoRows { - return nil - } - if err != nil { - return err - } - - keypers, err := shdb.DecodeAddresses(keyperSet.Keypers) - if err != nil { - return err - } - - if err := validateBatchConfig(latestBatchConfig, keyperSet, keypers); err != nil { - log.Warn().Err(err). - Int64("keyper-config-index", keyperSet.KeyperConfigIndex). - Msg("batch config validation failed, not sending to shuttermint") - // Mark as sent so we don't retry the same invalid config every cycle. - if err := q.SetLastBatchConfigProcessed(ctx, keyperSet.KeyperConfigIndex); err != nil { - return err - } - return nil - } - - activationBlockNumber, err := medley.Int64ToUint64Safe(keyperSet.ActivationBlockNumber) - if err != nil { - return err - } - // We *MUST* check if the blockNumber is smaller than the activationBlockNumber since both are - // uint64 and therefore subtraction can never result in negative numbers. - // This means that if we missed the activationBlockNumber we will never submit the config. - if blockNumber < activationBlockNumber && activationBlockNumber-blockNumber > kpr.config.Shuttermint.DKGStartBlockDelta { - log.Info().Interface("keyper-set", keyperSet). - Uint64("l1-block-number", blockNumber). - Uint64("dkg-start-delta", kpr.config.Shuttermint.DKGStartBlockDelta). - Msg("not yet submitting config") - return nil - } - - err = q.SetLastBatchConfigProcessed(ctx, keyperSet.KeyperConfigIndex) - if err != nil { - log.Warn().Err(err). - Interface("keyper-set", keyperSet). - Int64("keyper-config-index", keyperSet.KeyperConfigIndex). - Msg("error when setting last batch config sent. Returning nil.") - return nil - } - - log.Info().Interface("keyper-set", keyperSet). - Uint64("sync-block-number", blockNumber). - Uint64("dkg-start-delta", kpr.config.Shuttermint.DKGStartBlockDelta). - Msg("have a new config to be scheduled") - batchConfigMsg := shmsg.NewBatchConfig( - uint64(keyperSet.ActivationBlockNumber), - keypers, - uint64(keyperSet.Threshold), - uint64(keyperSet.KeyperConfigIndex), - ) - err = q.ScheduleShutterMessage( - ctx, - fmt.Sprintf("new batch config (activation-block-number=%d, config-index=%d)", - keyperSet.ActivationBlockNumber, keyperSet.KeyperConfigIndex), - batchConfigMsg, - ) - if err != nil { - return err - } - return nil -} - -// TODO: we need a better block syncing mechanism! -// Also this is doing too much work sequentially in one routine. -func (kpr *KeyperCore) operateShuttermint(ctx context.Context, _ service.Runner) error { - for { - syncBlockNumber, err := retry.FunctionCall(ctx, kpr.blockSyncClient.BlockNumber) - if err != nil { - return err - } - keypermetrics.MetricsKeyperCurrentBlockL1.Set(float64(syncBlockNumber)) - - err = smobserver.SyncAppWithDB(ctx, kpr.shuttermintClient, kpr.dbpool, kpr.shuttermintState) - if err != nil { - return err - } - err = kpr.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { - return kpr.handleOnChainChanges(ctx, tx, syncBlockNumber) - }) - if err != nil { - return err - } - err = fx.SendShutterMessages(ctx, database.New(kpr.dbpool), &kpr.messageSender) - if err != nil { - return err - } - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(2 * time.Second): - } - } -} diff --git a/rolling-shutter/keyper/keyper_test.go b/rolling-shutter/keyper/keyper_test.go deleted file mode 100644 index 5669f8eba..000000000 --- a/rolling-shutter/keyper/keyper_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package keyper - -import ( - "context" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/jackc/pgx/v4" - "gotest.tools/assert" - - obskeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" - keyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/testsetup" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" -) - -func TestHandleOnChainKeyperSetChangesSkipsPastInvalidConfig(t *testing.T) { - ctx := context.Background() - - dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, keyperdb.Definition) - t.Cleanup(dbclose) - - currentKeypers := []common.Address{ - common.HexToAddress("0x0000000000000000000000000000000000000001"), - common.HexToAddress("0x0000000000000000000000000000000000000002"), - } - duplicateAddr := common.HexToAddress("0x0000000000000000000000000000000000000003") - nextKeypers := []common.Address{ - common.HexToAddress("0x0000000000000000000000000000000000000004"), - common.HexToAddress("0x0000000000000000000000000000000000000005"), - } - - err := dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { - q := keyperdb.New(tx) - cq := obskeyperdb.New(tx) - - if err := q.InsertBatchConfig(ctx, keyperdb.InsertBatchConfigParams{ - KeyperConfigIndex: 1, - Height: 1, - Keypers: shdb.EncodeAddresses(currentKeypers), - Threshold: 2, - Started: true, - ActivationBlockNumber: 10, - }); err != nil { - return err - } - if err := cq.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ - KeyperConfigIndex: 2, - ActivationBlockNumber: 11, - Keypers: shdb.EncodeAddresses([]common.Address{duplicateAddr, duplicateAddr}), - Threshold: 2, - }); err != nil { - return err - } - return cq.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ - KeyperConfigIndex: 3, - ActivationBlockNumber: 12, - Keypers: shdb.EncodeAddresses(nextKeypers), - Threshold: 2, - }) - }) - assert.NilError(t, err) - - kpr := &KeyperCore{ - config: &kprconfig.Config{ - Shuttermint: &kprconfig.ShuttermintConfig{ - DKGStartBlockDelta: 0, - }, - }, - } - - err = dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { - return kpr.handleOnChainKeyperSetChanges(ctx, tx, 100) - }) - assert.NilError(t, err) - - queries := keyperdb.New(dbpool) - lastSent, err := queries.GetLastBatchConfigProcessed(ctx) - assert.NilError(t, err) - assert.Equal(t, lastSent, int64(2)) - - err = dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { - return kpr.handleOnChainKeyperSetChanges(ctx, tx, 100) - }) - assert.NilError(t, err) - - lastSent, err = queries.GetLastBatchConfigProcessed(ctx) - assert.NilError(t, err) - assert.Equal(t, lastSent, int64(3)) - - msg, err := queries.GetNextShutterMessage(ctx) - assert.NilError(t, err) - assert.Check(t, strings.Contains(msg.Description, "config-index=3")) -} diff --git a/rolling-shutter/keyper/keypermetrics/metrics.go b/rolling-shutter/keyper/keypermetrics/metrics.go index 927d541e0..1117ff9e4 100644 --- a/rolling-shutter/keyper/keypermetrics/metrics.go +++ b/rolling-shutter/keyper/keypermetrics/metrics.go @@ -3,7 +3,6 @@ package keypermetrics import ( "context" "strconv" - "strings" "github.com/jackc/pgx/v4/pgxpool" "github.com/prometheus/client_golang/prometheus" @@ -30,15 +29,6 @@ var MetricsKeyperCurrentBlockL1 = prometheus.NewGauge( }, ) -var MetricsKeyperCurrentBlockShuttermint = prometheus.NewGauge( - prometheus.GaugeOpts{ - Namespace: "shutter", - Subsystem: "keyper", - Name: "current_block_shuttermint", - Help: "Current shuttermint block number", - }, -) - var MetricsKeyperCurrentEon = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "shutter", @@ -58,16 +48,6 @@ var MetricsKeyperEonStartBlock = prometheus.NewGaugeVec( []string{"eon"}, ) -var MetricsKeyperIsKeyper = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Namespace: "shutter", - Subsystem: "keyper", - Name: "is_keyper", - Help: "Is this node a Keyper in the respective batch config", - }, - []string{"batch_config_index"}, -) - var MetricsKeyperCurrentPhase = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "shutter", @@ -98,24 +78,6 @@ var MetricsKeyperDKGMessagesReceived = prometheus.NewCounterVec( []string{"eon", "message_type"}, ) -var MetricsKeyperCurrentBatchConfigIndex = prometheus.NewGauge( - prometheus.GaugeOpts{ - Namespace: "shutter", - Subsystem: "keyper", - Name: "current_batch_config_index", - Help: "Current batch config index", - }, -) - -var MetricsKeyperBatchConfigInfo = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Namespace: "shutter", - Subsystem: "keyper", - Name: "batch_config_info", - Help: "Information about the batch configuration in use", - }, - []string{"batch_config_index", "keyper_addresses"}) - var MetricsKeyperDKGStatus = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "shutter", @@ -145,13 +107,9 @@ var MetricsExecutionClientVersion = prometheus.NewGaugeVec( func InitMetrics(dbpool *pgxpool.Pool, config kprconfig.Config) { prometheus.MustRegister(MetricsKeyperCurrentBlockL1) - prometheus.MustRegister(MetricsKeyperCurrentBlockShuttermint) prometheus.MustRegister(MetricsKeyperCurrentEon) prometheus.MustRegister(MetricsKeyperEonStartBlock) - prometheus.MustRegister(MetricsKeyperIsKeyper) prometheus.MustRegister(MetricsKeyperCurrentPhase) - prometheus.MustRegister(MetricsKeyperCurrentBatchConfigIndex) - prometheus.MustRegister(MetricsKeyperBatchConfigInfo) prometheus.MustRegister(MetricsKeyperDKGStatus) prometheus.MustRegister(MetricsKeyperEthAddress) prometheus.MustRegister(MetricsExecutionClientVersion) @@ -181,8 +139,6 @@ func InitMetrics(dbpool *pgxpool.Pool, config kprconfig.Config) { MetricsKeyperCurrentEon.Set(float64(currentEon.Eon)) - MetricsKeyperCurrentBatchConfigIndex.Set(float64(currentEon.KeyperConfigIndex)) - for _, eon := range eons { eonStr := strconv.FormatInt(eon.Eon, 10) MetricsKeyperEonStartBlock.WithLabelValues(eonStr).Set(float64(eon.ActivationBlockNumber)) @@ -216,36 +172,5 @@ func InitMetrics(dbpool *pgxpool.Pool, config kprconfig.Config) { } } - // Populate MetricsKeyperBatchConfigInfo && MetricsKeyperIsKeyper - batchConfigs, err := queries.GetBatchConfigs(ctx) - if err != nil { - log.Error().Err(err).Msg("keypermetrics | Failed to fetch batch configs") - } else { - currentAddress := config.GetAddress().Hex() - - for _, batchConfig := range batchConfigs { - batchConfigIndexStr := strconv.Itoa(int(batchConfig.KeyperConfigIndex)) - - // Join keyper addresses for the label - keyperAddresses := strings.Join(batchConfig.Keypers, ",") - MetricsKeyperBatchConfigInfo.WithLabelValues(batchConfigIndexStr, keyperAddresses).Set(1) - - // Check if current node is a keyper in this batch config - isKeyper := false - for _, keyperAddr := range batchConfig.Keypers { - if strings.EqualFold(keyperAddr, currentAddress) { - isKeyper = true - break - } - } - - var isKeyperValue float64 - if isKeyper { - isKeyperValue = 1 - } - MetricsKeyperIsKeyper.WithLabelValues(batchConfigIndexStr).Set(isKeyperValue) - } - } - log.Info().Msg("keypermetrics | Metrics population completed") } diff --git a/rolling-shutter/keyper/kprconfig/config.go b/rolling-shutter/keyper/kprconfig/config.go index 5181e32c7..d24402aa3 100644 --- a/rolling-shutter/keyper/kprconfig/config.go +++ b/rolling-shutter/keyper/kprconfig/config.go @@ -1,23 +1,13 @@ package kprconfig import ( - "crypto/ed25519" - "crypto/rand" - "io" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/pkg/errors" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/dkgphase" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/configuration" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/keys" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/metricsserver" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" ) -var _ configuration.Config = &ShuttermintConfig{} - type Config struct { InstanceID uint64 DatabaseURL string @@ -26,10 +16,9 @@ type Config struct { HTTPReadOnly bool HTTPListenAddress string - P2P *p2p.Config - Shuttermint *ShuttermintConfig - Ethereum *configuration.EthnodeConfig - Metrics *metricsserver.MetricsConfig + P2P *p2p.Config + Ethereum *configuration.EthnodeConfig + Metrics *metricsserver.MetricsConfig MaxNumKeysPerMessage uint64 } @@ -38,20 +27,6 @@ func (c *Config) GetAddress() common.Address { return c.Ethereum.PrivateKey.EthereumAddress() } -func (c *Config) GetDKGPhaseLength() *dkgphase.PhaseLength { - return dkgphase.NewConstantPhaseLength(c.Shuttermint.DKGPhaseLength) -} - -func (c *Config) GetValidatorPublicKey() ed25519.PublicKey { - return c.Shuttermint.ValidatorPublicKey.Key -} - -func (c *Config) GetEncryptionKey() *ecies.PrivateKey { - // OPTIM this could be cached, but it is only used for - // eon DKG (rarely) and does not do any computation - return ecies.ImportECDSA(c.Shuttermint.EncryptionKey.Key) -} - func (c *Config) GetInstanceID() uint64 { return c.InstanceID } @@ -67,63 +42,3 @@ func (c *Config) GetEnableWriteOperations() bool { func (c *Config) GetMaxNumKeysPerMessage() uint64 { return c.MaxNumKeysPerMessage } - -func NewShuttermintConfig() *ShuttermintConfig { - c := &ShuttermintConfig{} - c.Init() - return c -} - -type ShuttermintConfig struct { - ShuttermintURL string - ValidatorPublicKey *keys.Ed25519Public `shconfig:",required"` - EncryptionKey *keys.ECDSAPrivate `shconfig:",required"` - DKGPhaseLength int64 // in shuttermint blocks - DKGStartBlockDelta uint64 -} - -func (c *ShuttermintConfig) Init() { - c.ValidatorPublicKey = &keys.Ed25519Public{} - c.EncryptionKey = &keys.ECDSAPrivate{} -} - -func (c *ShuttermintConfig) Name() string { - return "shuttermint" -} - -func (c *ShuttermintConfig) Validate() error { - if c.DKGPhaseLength < 0 { - return errors.New("DKGPhaseLength can't be negative") - } - return nil -} - -func (c *ShuttermintConfig) SetDefaultValues() error { - c.ShuttermintURL = "http://localhost:26657" - c.DKGPhaseLength = 30 - c.DKGStartBlockDelta = 200 - return nil -} - -func (c *ShuttermintConfig) SetExampleValues() error { - err := c.SetDefaultValues() - if err != nil { - return err - } - - valPriv, err := keys.GenerateEd25519Key(rand.Reader) - if err != nil { - return err - } - - c.EncryptionKey, err = keys.GenerateECDSAKey(rand.Reader) - if err != nil { - return err - } - c.ValidatorPublicKey = valPriv.Public().(*keys.Ed25519Public) - return nil -} - -func (c ShuttermintConfig) TOMLWriteHeader(_ io.Writer) (int, error) { - return 0, nil -} diff --git a/rolling-shutter/keyper/options.go b/rolling-shutter/keyper/options.go index 3c1b05d36..64f81c461 100644 --- a/rolling-shutter/keyper/options.go +++ b/rolling-shutter/keyper/options.go @@ -1,69 +1,26 @@ package keyper import ( - "context" - "errors" - "reflect" - "github.com/ethereum/go-ethereum/ethclient" "github.com/jackc/pgx/v4/pgxpool" - "github.com/shutter-network/rolling-shutter/rolling-shutter/contract" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" ) type Option func(*options) error type options struct { - dbpool *pgxpool.Pool - broadcastEonPubKey bool - messaging p2p.Messaging - blockSyncClient *ethclient.Client - messageHandler []p2p.MessageHandler - eonPubkeyHandler EonPublicKeyHandlerFunc + dbpool *pgxpool.Pool + messaging p2p.Messaging + blockSyncClient *ethclient.Client + messageHandler []p2p.MessageHandler } func newDefaultOptions() *options { return &options{ - dbpool: nil, - broadcastEonPubKey: true, - blockSyncClient: nil, - messageHandler: []p2p.MessageHandler{}, - eonPubkeyHandler: nil, - } -} - -var keyperNewConfigType = reflect.TypeOf(contract.KeypersConfigsListNewConfig{}) - -func validateOptions(o *options) error { - if !o.broadcastEonPubKey && o.eonPubkeyHandler == nil { - return errors.New("no eon public key broadcast nor handler function provided. " + - "newly negotiated eon public-keys would not be forwarded") - } - return nil -} - -// NoBroadcastEonPublicKey deactivates the broadcasting of -// the keyper's newly negotiated DKG public-keys via the P2P network. -// If this option is given, an EonPublicKeyHandlerFunc MUST be -// provided via the WithEonPublicKeyHandler option. -func NoBroadcastEonPublicKey() Option { - return func(o *options) error { - o.broadcastEonPubKey = false - return nil - } -} - -type EonPublicKeyHandlerFunc func(context.Context, EonPublicKey) error - -// WithEonPublicKeyHandler registers a handler function that will -// be called whenever the keyper newly negotiated a DKG public key. -// If the NoBroadcastEonPublicKey() option is given, an -// EonPublicKeyHandlerFunc MUST be provided. -func WithEonPublicKeyHandler(handler EonPublicKeyHandlerFunc) Option { - return func(o *options) error { - o.eonPubkeyHandler = handler - return nil + dbpool: nil, + blockSyncClient: nil, + messageHandler: []p2p.MessageHandler{}, } } diff --git a/rolling-shutter/keyper/shutterevents/batchconfig.go b/rolling-shutter/keyper/shutterevents/batchconfig.go deleted file mode 100644 index 0ac2dd594..000000000 --- a/rolling-shutter/keyper/shutterevents/batchconfig.go +++ /dev/null @@ -1,66 +0,0 @@ -package shutterevents - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -// KeyperIndex returns the index of the keyper identified by the given address. -func (bc *BatchConfig) KeyperIndex(address common.Address) (uint64, bool) { - for i, k := range bc.Keypers { - if k == address { - return uint64(i), true - } - } - return 0, false -} - -// IsKeyper checks if the given address is a keyper. -func (bc *BatchConfig) IsKeyper(candidate common.Address) bool { - _, ok := bc.KeyperIndex(candidate) - return ok -} - -// EnsureValid checks if the BatchConfig is valid and returns an error if it's not valid. -func (bc *BatchConfig) EnsureValid() error { - if len(bc.Keypers) == 0 { - return errors.Errorf("no keypers in batch config") - } - if bc.Threshold == 0 { - return errors.Errorf("threshold must not be zero") - } - if int(bc.Threshold) > len(bc.Keypers) { - return errors.Errorf("threshold too high") - } - // XXX maybe we should check for duplicate addresses - return nil -} - -// BatchConfigFromMessage extracts the batch config received in a message. Started and -// ValidatorsUpdated which are not present on the message are set to false. -func BatchConfigFromMessage(m *shmsg.BatchConfig) (BatchConfig, error) { - var keypers []common.Address - for _, b := range m.Keypers { - if len(b) != common.AddressLength { - return BatchConfig{}, errors.Errorf("keyper address has invalid length") - } - keypers = append(keypers, common.BytesToAddress(b)) - } - - if err := medley.EnsureUniqueAddresses(keypers); err != nil { - return BatchConfig{}, err - } - - bc := BatchConfig{ - ActivationBlockNumber: m.ActivationBlockNumber, - Keypers: keypers, - Threshold: m.Threshold, - KeyperConfigIndex: m.KeyperConfigIndex, - Started: false, - ValidatorsUpdated: false, - } - return bc, nil -} diff --git a/rolling-shutter/keyper/shutterevents/events.go b/rolling-shutter/keyper/shutterevents/events.go deleted file mode 100644 index e12b90b10..000000000 --- a/rolling-shutter/keyper/shutterevents/events.go +++ /dev/null @@ -1,554 +0,0 @@ -// Package shutterevents contains types to represent deserialized shuttermint/tendermint events -package shutterevents - -import ( - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/pkg/errors" - abcitypes "github.com/tendermint/tendermint/abci/types" - - "github.com/shutter-network/shutter/shlib/shcrypto" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/shutterevents/evtype" -) - -/* All of the event types defined here have a "Height" field, that is *not* being serialized when - calling MakeABCIEvent. We need this field in the keyper. It's set via passing the height - argument to MakeEvent. -*/ - -// Accusation represents a broadcasted accusation message against one or more keypers. -type Accusation struct { - Height int64 - Eon uint64 - Sender common.Address - Accused []common.Address -} - -func (acc *Accusation) String() string { - return fmt.Sprintf( - "Accusation{Height=%d, Eon=%d, Sender=%s, Accused=%s}", - acc.Height, acc.Eon, acc.Sender.String(), encodeAddresses(acc.Accused)) -} - -func (acc Accusation) MakeABCIEvent() abcitypes.Event { - return abcitypes.Event{ - Type: evtype.Accusation, - Attributes: []abcitypes.EventAttribute{ - newAddressPair("Sender", acc.Sender), - newUintPair("Eon", acc.Eon), - newAddressesPair("Accused", acc.Accused), - }, - } -} - -func expectAttributes(ev abcitypes.Event, names ...string) error { - if len(ev.Attributes) < len(names) { - return errors.Errorf("expected at least %d attributes", len(names)) - } - - for i, n := range names { - if ev.Attributes[i].Key != n { - return errors.Errorf( - "bad attribute, parsing event %s: expected %s, got %s at position %d", - ev.Type, - n, - ev.Attributes[i].Key, - i, - ) - } - } - return nil -} - -func makeAccusation(ev abcitypes.Event, height int64) (*Accusation, error) { - err := expectAttributes(ev, "Sender", "Eon", "Accused") - if err != nil { - return nil, err - } - - sender, err := decodeAddress(ev.Attributes[0].Value) - if err != nil { - return nil, err - } - - eon, err := decodeUint64(ev.Attributes[1].Value) - if err != nil { - return nil, err - } - - accused, err := decodeAddresses(ev.Attributes[2].Value) - if err != nil { - return nil, err - } - - return &Accusation{ - Height: height, - Sender: sender, - Eon: eon, - Accused: accused, - }, nil -} - -// Apology represents an apology broadcasted in response to a prior accusation. -type Apology struct { - Height int64 - Eon uint64 - Sender common.Address - Accusers []common.Address - PolyEval []*big.Int -} - -func (msg *Apology) String() string { - return fmt.Sprintf( - "Apology{Height=%d, Eon=%d, Sender=%s, Accusers=%s}", - msg.Height, msg.Eon, msg.Sender, encodeAddresses(msg.Accusers)) -} - -func (msg Apology) MakeABCIEvent() abcitypes.Event { - var polyEvalBytes [][]byte - for _, e := range msg.PolyEval { - polyEvalBytes = append(polyEvalBytes, e.Bytes()) - } - return abcitypes.Event{ - Type: evtype.Apology, - Attributes: []abcitypes.EventAttribute{ - newAddressPair("Sender", msg.Sender), - newUintPair("Eon", msg.Eon), - newAddressesPair("Accusers", msg.Accusers), - newByteSequencePair("PolyEvals", polyEvalBytes), - }, - } -} - -func makeApology(ev abcitypes.Event, height int64) (*Apology, error) { - err := expectAttributes(ev, "Sender", "Eon", "Accusers", "PolyEvals") - if err != nil { - return nil, err - } - - sender, err := decodeAddress(ev.Attributes[0].Value) - if err != nil { - return nil, err - } - - eon, err := decodeUint64(ev.Attributes[1].Value) - if err != nil { - return nil, err - } - - accusers, err := decodeAddresses(ev.Attributes[2].Value) - if err != nil { - return nil, err - } - var polyEval []*big.Int - polyEvalBytes, err := decodeByteSequence(ev.Attributes[3].Value) - if err != nil { - return nil, err - } - for _, b := range polyEvalBytes { - e := new(big.Int) - e.SetBytes(b) - polyEval = append(polyEval, e) - } - return &Apology{ - Height: height, - Sender: sender, - Eon: eon, - Accusers: accusers, - PolyEval: polyEval, - }, nil -} - -// BatchConfig is the configuration we use for a consecutive sequence of batches/epochs. This -// should be synchronized with the list of BatchConfig structures stored in the -// KeypersConfigsList deployed on the main chain. The keypers use the corresponding -// shmsg.BatchConfig message to vote on new configs. This struct is sent as an event, when a new -// batch config has enough votes. -type BatchConfig struct { - Height int64 - Keypers []common.Address - ActivationBlockNumber uint64 - Threshold uint64 - KeyperConfigIndex uint64 - Started bool - ValidatorsUpdated bool -} - -func (bc *BatchConfig) String() string { - return fmt.Sprintf( - "BatchConfig{Height=%d, Keypers=%s, ActivationBlockNumber=%d, Threshold=%d, ConfigIndex=%d, Started=%t, ValidatorsUpdated=%t}", - bc.Height, - encodeAddresses(bc.Keypers), - bc.ActivationBlockNumber, - bc.Threshold, - bc.KeyperConfigIndex, - bc.Started, - bc.ValidatorsUpdated, - ) -} - -func (bc BatchConfig) MakeABCIEvent() abcitypes.Event { - return abcitypes.Event{ - Type: evtype.BatchConfig, - Attributes: []abcitypes.EventAttribute{ - { - Key: "ActivationBlockNumber", - Value: fmt.Sprintf("%d", bc.ActivationBlockNumber), - Index: true, - }, - { - Key: "Threshold", - Value: fmt.Sprintf("%d", bc.Threshold), - }, - { - Key: "Keypers", - Value: encodeAddresses(bc.Keypers), - }, - { - Key: "ConfigIndex", - Value: fmt.Sprintf("%d", bc.KeyperConfigIndex), - }, - }, - } -} - -// makeBatchConfig creates a BatchConfigEvent from the given tendermint event of type -// "shutter.batch-config". -func makeBatchConfig(ev abcitypes.Event, height int64) (*BatchConfig, error) { - err := expectAttributes(ev, "ActivationBlockNumber", "Threshold", "Keypers", "ConfigIndex") - if err != nil { - return nil, err - } - - activationBlockNumber, err := decodeUint64(ev.Attributes[0].Value) - if err != nil { - return nil, err - } - - threshold, err := decodeUint64(ev.Attributes[1].Value) - if err != nil { - return nil, err - } - keypers, err := decodeAddresses(ev.Attributes[2].Value) - if err != nil { - return nil, err - } - - keyperConfigIndex, err := decodeUint64(ev.Attributes[3].Value) - if err != nil { - return nil, err - } - return &BatchConfig{ - Height: height, - ActivationBlockNumber: activationBlockNumber, - Threshold: threshold, - Keypers: keypers, - KeyperConfigIndex: keyperConfigIndex, - }, nil -} - -type BatchConfigStarted struct { - Height int64 - KeyperConfigIndex uint64 -} - -func (bcs BatchConfigStarted) MakeABCIEvent() abcitypes.Event { - return abcitypes.Event{ - Type: evtype.BatchConfigStarted, - Attributes: []abcitypes.EventAttribute{ - { - Key: "ConfigIndex", - Value: fmt.Sprintf("%d", bcs.KeyperConfigIndex), - }, - }, - } -} - -func (bcs *BatchConfigStarted) String() string { - return fmt.Sprintf( - "BatchConfigStarted{Height=%d, ConfigIndex=%d}", - bcs.Height, - bcs.KeyperConfigIndex, - ) -} - -// makeBatchConfigStarted creates a BatchConfigEvent from the given tendermint event of type -// "shutter.batch-config-started". -func makeBatchConfigStarted(ev abcitypes.Event, height int64) (*BatchConfigStarted, error) { - err := expectAttributes(ev, "ConfigIndex") - if err != nil { - return nil, err - } - keyperConfigIndex, err := decodeUint64(ev.Attributes[0].Value) - if err != nil { - return nil, err - } - return &BatchConfigStarted{ - Height: height, - KeyperConfigIndex: keyperConfigIndex, - }, nil -} - -// CheckIn is emitted by shuttermint when a keyper sends their check in message. -type CheckIn struct { - Height int64 - Sender common.Address - EncryptionPublicKey *ecies.PublicKey -} - -func (msg CheckIn) MakeABCIEvent() abcitypes.Event { - return abcitypes.Event{ - Type: evtype.CheckIn, - Attributes: []abcitypes.EventAttribute{ - newAddressPair("Sender", msg.Sender), - { - Key: "EncryptionPublicKey", - Value: encodeECIESPublicKey(msg.EncryptionPublicKey), - }, - }, - } -} - -func (msg *CheckIn) String() string { - return fmt.Sprintf( - "CheckIn{Height=%d, Sender=%s}", - msg.Height, - msg.Sender.String(), - ) -} - -// makeCheckIn creates a CheckInEvent from the given tendermint event of type "shutter.check-in". -func makeCheckIn(ev abcitypes.Event, height int64) (*CheckIn, error) { - err := expectAttributes(ev, "Sender", "EncryptionPublicKey") - if err != nil { - return nil, err - } - sender, err := decodeAddress(ev.Attributes[0].Value) - if err != nil { - return nil, err - } - - publicKey, err := decodeECIESPublicKey(ev.Attributes[1].Value) - if err != nil { - return nil, err - } - - return &CheckIn{ - Sender: sender, - EncryptionPublicKey: publicKey, - Height: height, - }, nil -} - -// EonStarted is generated by shuttermint when a new eon is started. The activation block number -// identifies the main chain block number from which on this eon shall be used. -type EonStarted struct { - Height int64 - Eon uint64 - ActivationBlockNumber uint64 - KeyperConfigIndex uint64 -} - -func (msg EonStarted) MakeABCIEvent() abcitypes.Event { - return abcitypes.Event{ - Type: evtype.EonStarted, - Attributes: []abcitypes.EventAttribute{ - newUintPair("Eon", msg.Eon), - newUintPair("ActivationBlockNumber", msg.ActivationBlockNumber), - newUintPair("KeyperConfigIndex", msg.KeyperConfigIndex), - }, - } -} - -func (msg *EonStarted) String() string { - return fmt.Sprintf( - "EonStarted{Height=%d, Eon=%d, ActivationBlockNumber=%d, ConfigIndex=%d}", - msg.Height, - msg.Eon, - msg.ActivationBlockNumber, - msg.KeyperConfigIndex, - ) -} - -// PolyCommitment represents a broadcasted polynomial commitment message. -type PolyCommitment struct { - Height int64 - Eon uint64 - Sender common.Address - Gammas *shcrypto.Gammas -} - -func (msg *PolyCommitment) String() string { - return fmt.Sprintf( - "PolyCommitment{Height=%d, Eon=%d, Sender=%s}", - msg.Height, - msg.Eon, - msg.Sender.String(), - ) -} - -func (msg PolyCommitment) MakeABCIEvent() abcitypes.Event { - return abcitypes.Event{ - Type: evtype.PolyCommitment, - Attributes: []abcitypes.EventAttribute{ - newAddressPair("Sender", msg.Sender), - newUintPair("Eon", msg.Eon), - newGammas("Gammas", msg.Gammas), - }, - } -} - -func makePolyCommitment(ev abcitypes.Event, height int64) (*PolyCommitment, error) { - err := expectAttributes(ev, "Sender", "Eon", "Gammas") - if err != nil { - return nil, err - } - sender, err := decodeAddress(ev.Attributes[0].Value) - if err != nil { - return nil, err - } - - eon, err := decodeUint64(ev.Attributes[1].Value) - if err != nil { - return nil, err - } - - gammas, err := decodeGammas(ev.Attributes[2].Value) - if err != nil { - return nil, err - } - - return &PolyCommitment{ - Height: height, - Sender: sender, - Eon: eon, - Gammas: &gammas, - }, nil -} - -// PolyEval represents an encrypted polynomial evaluation message from one keyper to another. -type PolyEval struct { - Height int64 - Sender common.Address - Eon uint64 - Receivers []common.Address - EncryptedEvals [][]byte -} - -func (msg *PolyEval) String() string { - return fmt.Sprintf( - "PolyEval{Height=%d, Sender=%s, Eon=%d, Receivers=%s}", - msg.Height, - msg.Sender, - msg.Eon, - encodeAddresses(msg.Receivers)) -} - -func (msg PolyEval) MakeABCIEvent() abcitypes.Event { - return abcitypes.Event{ - Type: evtype.PolyEval, - Attributes: []abcitypes.EventAttribute{ - newAddressPair("Sender", msg.Sender), - newUintPair("Eon", msg.Eon), - newAddressesPair("Receivers", msg.Receivers), - newByteSequencePair("EncryptedEvals", msg.EncryptedEvals), - }, - } -} - -func makePolyEval(ev abcitypes.Event, height int64) (*PolyEval, error) { - err := expectAttributes(ev, "Sender", "Eon", "Receivers", "EncryptedEvals") - if err != nil { - return nil, err - } - - sender, err := decodeAddress(ev.Attributes[0].Value) - if err != nil { - return nil, err - } - - eon, err := decodeUint64(ev.Attributes[1].Value) - if err != nil { - return nil, err - } - - receivers, err := decodeAddresses(ev.Attributes[2].GetValue()) - if err != nil { - return nil, err - } - - encryptedEvals, err := decodeByteSequence(ev.Attributes[3].Value) - if err != nil { - return nil, err - } - - return &PolyEval{ - Height: height, - Eon: eon, - Sender: sender, - Receivers: receivers, - EncryptedEvals: encryptedEvals, - }, nil -} - -// IEvent is an interface for the event types declared above. -type IEvent interface { - MakeABCIEvent() abcitypes.Event - String() string -} - -// makeEonStarted creates a EonStartedEvent from the given tendermint event of type -// "shutter.eon-started". -func makeEonStarted(ev abcitypes.Event, height int64) (*EonStarted, error) { - err := expectAttributes(ev, "Eon", "ActivationBlockNumber", "KeyperConfigIndex") - if err != nil { - return nil, err - } - - eon, err := decodeUint64(ev.Attributes[0].Value) - if err != nil { - return nil, err - } - activationBlockNumber, err := decodeUint64(ev.Attributes[1].Value) - if err != nil { - return nil, err - } - keyperConfigIndex, err := decodeUint64(ev.Attributes[2].Value) - if err != nil { - return nil, err - } - return &EonStarted{ - Height: height, - Eon: eon, - ActivationBlockNumber: activationBlockNumber, - KeyperConfigIndex: keyperConfigIndex, - }, nil -} - -// MakeEvent creates an Event from the given tendermint event. -func MakeEvent(ev abcitypes.Event, height int64) (IEvent, error) { - switch ev.Type { - case evtype.CheckIn: - return makeCheckIn(ev, height) - case evtype.BatchConfig: - return makeBatchConfig(ev, height) - case evtype.BatchConfigStarted: - return makeBatchConfigStarted(ev, height) - case evtype.EonStarted: - return makeEonStarted(ev, height) - case evtype.PolyCommitment: - return makePolyCommitment(ev, height) - case evtype.PolyEval: - return makePolyEval(ev, height) - case evtype.Accusation: - return makeAccusation(ev, height) - case evtype.Apology: - return makeApology(ev, height) - default: - return nil, errors.Errorf("cannot make event from type %s", ev.Type) - } -} diff --git a/rolling-shutter/keyper/shutterevents/events_test.go b/rolling-shutter/keyper/shutterevents/events_test.go deleted file mode 100644 index 7f617f0d1..000000000 --- a/rolling-shutter/keyper/shutterevents/events_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package shutterevents_test - -import ( - "crypto/rand" - "fmt" - "math/big" - "reflect" - "testing" - - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - gocmp "github.com/google/go-cmp/cmp" - "gotest.tools/v3/assert" - - "github.com/shutter-network/shutter/shlib/shcrypto" - "github.com/shutter-network/shutter/shlib/shtest" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/shutterevents" -) - -var ( - polynomial *shcrypto.Polynomial - gammas shcrypto.Gammas - eon = uint64(64738) - sender = common.BytesToAddress([]byte("foo")) - addresses = []common.Address{ - common.BigToAddress(big.NewInt(1)), - common.BigToAddress(big.NewInt(2)), - common.BigToAddress(big.NewInt(3)), - } -) - -func init() { - var err error - polynomial, err = shcrypto.RandomPolynomial(rand.Reader, 3) - if err != nil { - panic(err) - } - - gammas = *polynomial.Gammas() -} - -var eciesPublicKeyComparer = gocmp.Comparer(func(x, y *ecies.PublicKey) bool { - return reflect.DeepEqual(x, y) -}) - -// roundtrip checks that the given IEvent round-trips, i.e. it can be serialized as an ABCI Event -// and deserialized back again to an equal value. -func roundtrip(t *testing.T, ev shutterevents.IEvent) { - t.Helper() - ev2, err := shutterevents.MakeEvent(ev.MakeABCIEvent(), 0) - assert.NilError(t, err) - assert.DeepEqual(t, ev, ev2, shtest.BigIntComparer, eciesPublicKeyComparer) -} - -func TestAccusation(t *testing.T) { - ev := &shutterevents.Accusation{ - Eon: eon, - Sender: sender, - Accused: addresses, - } - roundtrip(t, ev) -} - -func TestEmptyAccusation(t *testing.T) { - ev := &shutterevents.Accusation{ - Eon: eon, - Sender: sender, - } - roundtrip(t, ev) -} - -func TestApology(t *testing.T) { - accusers := addresses - var polyEval []*big.Int - for i := 0; i < len(accusers); i++ { - eval := big.NewInt(int64(100 + i)) - polyEval = append(polyEval, eval) - } - ev := &shutterevents.Apology{ - Eon: eon, - Sender: sender, - Accusers: accusers, - PolyEval: polyEval, - } - roundtrip(t, ev) -} - -func TestEmptyApology(t *testing.T) { - ev := &shutterevents.Apology{ - Eon: eon, - Sender: sender, - } - roundtrip(t, ev) -} - -func TestBatchConfig(t *testing.T) { - ev := &shutterevents.BatchConfig{ - ActivationBlockNumber: 111, - Threshold: 2, - Keypers: addresses, - KeyperConfigIndex: uint64(0xffffffffffffffff), - } - roundtrip(t, ev) - // XXX should we implement this or drop the field? - // ev.Started = true - // roundtrip(t, ev) -} - -func TestBatchConfigStarted(t *testing.T) { - ev := &shutterevents.BatchConfigStarted{ - KeyperConfigIndex: uint64(0xffffffffffffffff), - } - roundtrip(t, ev) -} - -func TestCheckIn(t *testing.T) { - privateKeyECDSA, err := ethcrypto.GenerateKey() - assert.NilError(t, err) - publicKey := ecies.ImportECDSAPublic(&privateKeyECDSA.PublicKey) - ev := &shutterevents.CheckIn{Sender: sender, EncryptionPublicKey: publicKey} - roundtrip(t, ev) -} - -func TestEonStarted(t *testing.T) { - ev := &shutterevents.EonStarted{Eon: eon, ActivationBlockNumber: 9999, KeyperConfigIndex: 567} - roundtrip(t, ev) -} - -func TestPolyCommitment(t *testing.T) { - ev := &shutterevents.PolyCommitment{ - Eon: eon, - Sender: sender, - Gammas: &gammas, - } - roundtrip(t, ev) -} - -func TestPolyEval(t *testing.T) { - var receivers []common.Address - var encryptedEvals [][]byte - for i := 1; i < 10; i++ { - receivers = append(receivers, common.BigToAddress(new(big.Int).SetUint64(uint64(i)))) - encryptedEvals = append(encryptedEvals, []byte(fmt.Sprintf("encrypted: %d", i))) - } - ev := &shutterevents.PolyEval{ - Eon: eon, - Sender: sender, - Receivers: receivers, - EncryptedEvals: encryptedEvals, - } - roundtrip(t, ev) -} diff --git a/rolling-shutter/keyper/shutterevents/evtype/evtype.go b/rolling-shutter/keyper/shutterevents/evtype/evtype.go deleted file mode 100644 index 62bf3e218..000000000 --- a/rolling-shutter/keyper/shutterevents/evtype/evtype.go +++ /dev/null @@ -1,13 +0,0 @@ -// evtype declares the the different event types sent by shuttermint -package evtype - -var ( - Accusation = "shutter.accusation-registered" - Apology = "shutter.apology-registered" - BatchConfig = "shutter.batch-config" - BatchConfigStarted = "shutter.batch-config-started" - CheckIn = "shutter.check-in" - EonStarted = "shutter.eon-started" - PolyCommitment = "shutter.poly-commitment-registered" - PolyEval = "shutter.poly-eval-registered" -) diff --git a/rolling-shutter/keyper/shutterevents/helpers.go b/rolling-shutter/keyper/shutterevents/helpers.go deleted file mode 100644 index 3b08cfa7a..000000000 --- a/rolling-shutter/keyper/shutterevents/helpers.go +++ /dev/null @@ -1,49 +0,0 @@ -package shutterevents - -import ( - "github.com/ethereum/go-ethereum/common" - abcitypes "github.com/tendermint/tendermint/abci/types" - - "github.com/shutter-network/shutter/shlib/shcrypto" -) - -// -// Encoding/decoding helpers -// - -func newAddressPair(key string, value common.Address) abcitypes.EventAttribute { //nolint:unparam - return abcitypes.EventAttribute{ - Key: key, - Value: encodeAddress(value), - Index: true, - } -} - -func newAddressesPair(key string, value []common.Address) abcitypes.EventAttribute { - return abcitypes.EventAttribute{ - Key: key, - Value: encodeAddresses(value), - } -} - -func newByteSequencePair(key string, value [][]byte) abcitypes.EventAttribute { - return abcitypes.EventAttribute{ - Key: key, - Value: encodeByteSequence(value), - } -} - -func newUintPair(key string, value uint64) abcitypes.EventAttribute { - return abcitypes.EventAttribute{ - Key: key, - Value: encodeUint64(value), - Index: true, - } -} - -func newGammas(key string, gammas *shcrypto.Gammas) abcitypes.EventAttribute { - return abcitypes.EventAttribute{ - Key: key, - Value: encodeGammas(gammas), - } -} diff --git a/rolling-shutter/keyper/shutterevents/marshal.go b/rolling-shutter/keyper/shutterevents/marshal.go deleted file mode 100644 index 9981eb9c9..000000000 --- a/rolling-shutter/keyper/shutterevents/marshal.go +++ /dev/null @@ -1,136 +0,0 @@ -package shutterevents - -import ( - "crypto/ecdsa" - "encoding/base64" - "encoding/hex" - "strconv" - "strings" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/pkg/errors" - - "github.com/shutter-network/shutter/shlib/shcrypto" -) - -func encodeUint64(val uint64) string { - return strconv.FormatUint(val, 10) -} - -func decodeUint64(val string) (uint64, error) { - v, err := strconv.ParseUint(val, 10, 64) - if err != nil { - return 0, errors.Wrap(err, "failed to parse event") - } - return v, nil -} - -// encodeAddresses encodes the given slice of Addresses as comma-separated list of addresses. -func encodeAddresses(addr []common.Address) string { - var hexstrings []string - for _, a := range addr { - hexstrings = append(hexstrings, a.Hex()) - } - return strings.Join(hexstrings, ",") -} - -// decodeAddresses reverses the encodeAddressesForEvent operation, i.e. it parses a list -// of addresses from a comma-separated string. -func decodeAddresses(s string) ([]common.Address, error) { - var res []common.Address - if s == "" { - return res, nil - } - for _, a := range strings.Split(s, ",") { - if !common.IsHexAddress(a) { - return nil, errors.Errorf("malformed address: %q", s) - } - - res = append(res, common.HexToAddress(a)) - } - return res, nil -} - -// encodeByteSequence encodes a slice o byte strings as a comma separated string. -func encodeByteSequence(v [][]byte) string { - var hexstrings []string - for _, a := range v { - hexstrings = append(hexstrings, hexutil.Encode(a)) - } - return strings.Join(hexstrings, ",") -} - -// decodeByteSequence parses a list of hex encoded, comma-separated byte slices. -func decodeByteSequence(s string) ([][]byte, error) { - var res [][]byte - if s == "" { - return res, nil - } - for _, v := range strings.Split(s, ",") { - bs, err := hexutil.Decode(v) - if err != nil { - return [][]byte{}, err - } - res = append(res, bs) - } - return res, nil -} - -// encodePubkey encodes the PublicKey as a string suitable for putting it into a tendermint -// event, i.e. an utf-8 compatible string. -func encodePubkey(pubkey *ecdsa.PublicKey) string { - return base64.RawURLEncoding.EncodeToString(ethcrypto.FromECDSAPub(pubkey)) -} - -// decodePubkey decodes a public key from a tendermint event. -func decodePubkey(val string) (*ecdsa.PublicKey, error) { - data, err := base64.RawURLEncoding.DecodeString(val) - if err != nil { - return nil, err - } - return ethcrypto.UnmarshalPubkey(data) -} - -func encodeGammas(gammas *shcrypto.Gammas) string { - gammasBytes := gammas.Marshal() - return hex.EncodeToString(gammasBytes) -} - -func decodeGammas(eventValue string) (shcrypto.Gammas, error) { - gammasBytes, err := hex.DecodeString(eventValue) - if err != nil { - return shcrypto.Gammas{}, errors.Wrapf(err, "failed to decode gammas") - } - gammas := shcrypto.Gammas{} - if err := gammas.Unmarshal(gammasBytes); err != nil { - return shcrypto.Gammas{}, errors.Wrap(err, "failed to unmarshal gammas") - } - return gammas, nil -} - -func encodeAddress(a common.Address) string { - return a.Hex() -} - -func decodeAddress(s string) (common.Address, error) { - a := common.HexToAddress(s) - if a.Hex() != s { - return common.Address{}, errors.Errorf("invalid address %s", s) - } - return a, nil -} - -func encodeECIESPublicKey(key *ecies.PublicKey) string { - return encodePubkey(key.ExportECDSA()) -} - -func decodeECIESPublicKey(val string) (*ecies.PublicKey, error) { - publicKeyECDSA, err := decodePubkey(val) - if err != nil { - return nil, err - } - return ecies.ImportECDSAPublic(publicKeyECDSA), nil -} diff --git a/rolling-shutter/keyper/shutterevents/shtxresp/code.go b/rolling-shutter/keyper/shutterevents/shtxresp/code.go deleted file mode 100644 index a7f6f27e8..000000000 --- a/rolling-shutter/keyper/shutterevents/shtxresp/code.go +++ /dev/null @@ -1,8 +0,0 @@ -// Used for the return codes between shuttermint and tx senders (e.g. Keyper, etc.) -package shtxresp - -const ( - Ok uint32 = iota - Error - Seen -) diff --git a/rolling-shutter/keyper/smobserver/smdriver.go b/rolling-shutter/keyper/smobserver/smdriver.go deleted file mode 100644 index 532dd2b4b..000000000 --- a/rolling-shutter/keyper/smobserver/smdriver.go +++ /dev/null @@ -1,298 +0,0 @@ -package smobserver - -import ( - "context" - "fmt" - "time" - - "github.com/jackc/pgx/v4" - "github.com/jackc/pgx/v4/pgxpool" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - abcitypes "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/rpc/client" - coretypes "github.com/tendermint/tendermint/rpc/core/types" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/keypermetrics" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/shutterevents" -) - -type ShuttermintDriver struct { - shmcl client.Client - dbpool *pgxpool.Pool - shuttermintState *ShuttermintState -} - -var errEmptyBlockchain = errors.New("empty shuttermint blockchain") - -func getLastCommittedHeight(ctx context.Context, shmcl client.Client) (int64, error) { - latestBlock, err := shmcl.Block(ctx, nil) - if err != nil { - return 0, errors.Wrap(err, "failed to get last committed height of shuttermint chain") - } - if latestBlock.Block == nil || latestBlock.Block.LastCommit == nil { - return 0, errEmptyBlockchain - } - return latestBlock.Block.LastCommit.Height, nil -} - -// SyncAppWithDB queries the shutter tendermint app for events and inserts them into our database. -func SyncAppWithDB( - ctx context.Context, - shmcl client.Client, - dbpool *pgxpool.Pool, - shuttermintState *ShuttermintState, -) error { - a := ShuttermintDriver{ - shmcl: shmcl, - dbpool: dbpool, - shuttermintState: shuttermintState, - } - return a.sync(ctx) -} - -func (smdrv *ShuttermintDriver) sync(ctx context.Context) error { - q := database.New(smdrv.dbpool) - oldMeta, err := q.TMGetSyncMeta(ctx) - if err != nil { - return err - } - currentBlock := oldMeta.CurrentBlock - - lastCommittedHeight, err := getLastCommittedHeight(ctx, smdrv.shmcl) - if errors.Is(err, errEmptyBlockchain) { - log.Info().Msg("Shuttermint blockchain still empty") - return nil - } - if err != nil { - return err - } - - if lastCommittedHeight == currentBlock { - return nil - } - - keypermetrics.MetricsKeyperCurrentBlockShuttermint.Set(float64(lastCommittedHeight)) - - err = smdrv.fetchEvents2(ctx, currentBlock, lastCommittedHeight) - return err -} - -func makeEvents(height int64, events []abcitypes.Event) []shutterevents.IEvent { - var res []shutterevents.IEvent - for _, ev := range events { - x, err := shutterevents.MakeEvent(ev, height) - if err != nil { - log.Error().Err(err).Str("event", ev.String()).Msg("malformed event") - } else { - res = append(res, x) - } - } - return res -} - -func (smdrv *ShuttermintDriver) fetchEvents2(ctx context.Context, heightFrom, lastCommittedHeight int64) error { - for currentBlock := heightFrom + 1; currentBlock < lastCommittedHeight; currentBlock++ { - results, err := smdrv.shmcl.BlockResults(ctx, ¤tBlock) - if err != nil { - return err - } - err = smdrv.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { - return smdrv.handleBlock(ctx, database.New(tx), results, lastCommittedHeight) - }) - if err != nil { - smdrv.shuttermintState.Invalidate() - return err - } - } - return nil -} - -func (smdrv *ShuttermintDriver) fetchEvents(ctx context.Context, heightFrom, lastCommittedHeight int64) error { - const perQuery = 500 - currentBlock := heightFrom - logProgress := currentBlock+perQuery < lastCommittedHeight - - for currentBlock := heightFrom; currentBlock < lastCommittedHeight; currentBlock += perQuery { - height := currentBlock + perQuery - if height > lastCommittedHeight { - height = lastCommittedHeight - } - query := fmt.Sprintf("tx.height >= %d and tx.height <= %d", currentBlock+1, height) - if logProgress { - log.Info().Str("query", query).Int64("target-height", lastCommittedHeight).Msg("fetch events") - } - // tendermint silently caps the perPage value at 100, make sure to stay below, otherwise - // our exit condition is wrong and the log.Fatal will trigger a panic below; see - // https://github.com/shutter-network/shutter/issues/50 - perPage := 100 - page := 1 - total := 0 - txs := []*coretypes.ResultTx{} - for { - res, err := smdrv.shmcl.TxSearch(ctx, query, false, &page, &perPage, "") - if err != nil { - return errors.Wrap(err, "failed to fetch shuttermint txs") - } - - total += len(res.Txs) - txs = append(txs, res.Txs...) - if page*perPage >= res.TotalCount { - if total != res.TotalCount { - log.Fatal(). - Int("num-fetched-transactions", total). - Int("num-expected-transactions", res.TotalCount). - Int64("from-height", currentBlock+1). - Int64("to-height", lastCommittedHeight). - Msg("internal error. fetched fewer shuttermint transactions than expected") - } - break - } - page++ - } - err := smdrv.handleTransactions(ctx, txs, currentBlock, height, lastCommittedHeight) - if err != nil { - return err - } - } - return nil -} - -func (smdrv *ShuttermintDriver) handleBlock( - ctx context.Context, - queries *database.Queries, - block *coretypes.ResultBlockResults, - lastCommittedHeight int64, -) error { - err := smdrv.shuttermintState.Load(ctx, queries) - if err != nil { - return err - } - oldMeta, err := queries.TMGetSyncMeta(ctx) - if err != nil { - return err - } - if block.Height != oldMeta.CurrentBlock+1 { - return errors.Errorf( - "blocks should be handled in order, expected %d, got %d", - oldMeta.CurrentBlock+1, - block.Height, - ) - } - - err = queries.TMSetSyncMeta(ctx, database.TMSetSyncMetaParams{ - CurrentBlock: block.Height, - LastCommittedHeight: lastCommittedHeight, - SyncTimestamp: time.Now(), - }) - if err != nil { - return err - } - - err = smdrv.shuttermintState.shiftPhases(ctx, queries, block.Height) - if err != nil { - return err - } - - handleEvents := func(abciEvents []abcitypes.Event) error { - events := makeEvents(block.Height, abciEvents) - for _, ev := range events { - err = smdrv.shuttermintState.HandleEvent(ctx, queries, ev) - if err != nil { - return err - } - } - return nil - } - - err = handleEvents(block.BeginBlockEvents) - if err != nil { - return err - } - for _, txres := range block.TxsResults { - err = handleEvents(txres.Events) - if err != nil { - return err - } - } - err = handleEvents(block.EndBlockEvents) - if err != nil { - return err - } - - err = smdrv.shuttermintState.BeforeSaveHook(ctx, queries) - if err != nil { - return err - } - return smdrv.shuttermintState.Save(ctx, queries) -} - -func (smdrv *ShuttermintDriver) innerHandleTransactions( - ctx context.Context, queries *database.Queries, - txs []*coretypes.ResultTx, - oldCurrentBlock, newCurrentBlock, lastCommittedHeight int64, -) error { - oldMeta, err := queries.TMGetSyncMeta(ctx) - if err != nil { - return err - } - if oldMeta.CurrentBlock != oldCurrentBlock { - return errors.Errorf( - "wrong current block stored in database: stored=%d expected=%d", - oldMeta.CurrentBlock, oldCurrentBlock) - } - err = smdrv.shuttermintState.Load(ctx, queries) - if err != nil { - return err - } - err = queries.TMSetSyncMeta(ctx, database.TMSetSyncMetaParams{ - CurrentBlock: newCurrentBlock, - LastCommittedHeight: lastCommittedHeight, - SyncTimestamp: time.Now(), - }) - if err != nil { - return err - } - for _, tx := range txs { - err = smdrv.shuttermintState.shiftPhases(ctx, queries, tx.Height) - if err != nil { - return err - } - - events := makeEvents(tx.Height, tx.TxResult.GetEvents()) - for _, ev := range events { - err = smdrv.shuttermintState.HandleEvent(ctx, queries, ev) - if err != nil { - return err - } - } - } - - // XXX We should move the following call into the BeforeSaveHook - err = smdrv.shuttermintState.shiftPhases(ctx, queries, newCurrentBlock) - if err != nil { - return err - } - err = smdrv.shuttermintState.BeforeSaveHook(ctx, queries) - if err != nil { - return err - } - return smdrv.shuttermintState.Save(ctx, queries) -} - -func (smdrv *ShuttermintDriver) handleTransactions( - ctx context.Context, - txs []*coretypes.ResultTx, - oldCurrentBlock, newCurrentBlock, lastCommittedHeight int64, -) error { - err := smdrv.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { - return smdrv.innerHandleTransactions( - ctx, database.New(tx), txs, oldCurrentBlock, newCurrentBlock, lastCommittedHeight, - ) - }) - if err != nil { - smdrv.shuttermintState.Invalidate() - } - return err -} diff --git a/rolling-shutter/keyper/smobserver/smstate.go b/rolling-shutter/keyper/smobserver/smstate.go deleted file mode 100644 index 62a4542ac..000000000 --- a/rolling-shutter/keyper/smobserver/smstate.go +++ /dev/null @@ -1,828 +0,0 @@ -package smobserver - -import ( - "context" - "crypto/ed25519" - "crypto/rand" - "database/sql" - "encoding/hex" - "fmt" - "math" - "math/big" - "reflect" - "strconv" - "strings" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/icza/gog" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - - "github.com/shutter-network/shutter/shlib/puredkg" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/dkgphase" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/keypermetrics" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/shutterevents" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -type Config interface { - GetAddress() common.Address - GetDKGPhaseLength() *dkgphase.PhaseLength - GetValidatorPublicKey() ed25519.PublicKey - GetEncryptionKey() *ecies.PrivateKey -} - -type ActiveDKG struct { - pure *puredkg.PureDKG - startHeight int64 - dirty bool - keypers []common.Address -} - -func (dkg *ActiveDKG) markDirty() { - dkg.dirty = true -} - -type PhaseLength interface { - GetPhaseAtHeight(height int64, eonStartHeight int64) puredkg.Phase -} - -// ShuttermintState contains our view of the remote shutter state. Strictly speaking everything is -// stored in the database, and what we have here is kind of a cache. -type ShuttermintState struct { - config Config - synchronized bool // are we synchronized - isKeyper bool - dkg map[uint64]*ActiveDKG - phaseLength PhaseLength -} - -func NewShuttermintState(config Config) *ShuttermintState { - return &ShuttermintState{ - config: config, - dkg: make(map[uint64]*ActiveDKG), - phaseLength: config.GetDKGPhaseLength(), - } -} - -// Invalidate invalidates the current state. This is being called, when an error happens. -func (st *ShuttermintState) Invalidate() { - *st = *NewShuttermintState(st.config) -} - -func (st *ShuttermintState) Load(ctx context.Context, queries *database.Queries) error { - if st.synchronized { - return nil - } - numBatchConfigs, err := queries.CountBatchConfigs(ctx) - if err != nil { - return err - } - st.isKeyper = numBatchConfigs > 0 // XXX need to look - err = st.loadDKG(ctx, queries) - if err != nil { - return err - } - - st.synchronized = true - return nil -} - -func (st *ShuttermintState) loadDKG(ctx context.Context, queries *database.Queries) error { - dkgs, err := queries.SelectPureDKG(ctx) - if err != nil { - return err - } - for _, dkg := range dkgs { - pure, err := shdb.DecodePureDKG(dkg.Puredkg) - if err != nil { - return err - } - - keyperEon, err := queries.GetEon(ctx, dkg.Eon) - if err != nil { - return err - } - - batchConfig, err := queries.GetBatchConfig(ctx, int32(keyperEon.KeyperConfigIndex)) - if err != nil { - return err - } - - keypers := []common.Address{} - for _, k := range batchConfig.Keypers { - a, err := shdb.DecodeAddress(k) - if err != nil { - return err - } - keypers = append(keypers, a) - } - - st.dkg[uint64(dkg.Eon)] = &ActiveDKG{ - pure: pure, - startHeight: keyperEon.Height, - dirty: false, - keypers: keypers, - } - } - return nil -} - -func (st *ShuttermintState) BeforeSaveHook(ctx context.Context, queries *database.Queries) error { - return st.sendPolyEvals(ctx, queries) -} - -func (st *ShuttermintState) Save(ctx context.Context, queries *database.Queries) error { - for eon, a := range st.dkg { - if !a.dirty { - continue - } - pureBytes, err := shdb.EncodePureDKG(a.pure) - if err != nil { - return err - } - - err = queries.InsertPureDKG(ctx, database.InsertPureDKGParams{ - Eon: int64(eon), - Puredkg: pureBytes, - }) - if err != nil { - return err - } - a.dirty = false - } - return nil -} - -func (st *ShuttermintState) sendPolyEvals(ctx context.Context, queries *database.Queries) error { - evals, err := queries.PolyEvalsWithEncryptionKeys(ctx) - if err != nil { - return err - } - - var receivers []common.Address - var encryptedEvals [][]byte - currentEon := int64(-1) - - send := func() error { - if len(encryptedEvals) == 0 { - return nil - } - defer func() { - receivers = nil - encryptedEvals = nil - }() - eonLabel := strconv.FormatInt(currentEon, 10) - err := queries.ScheduleShutterMessage( - ctx, - fmt.Sprintf("poly eval (eon=%d)", currentEon), - shmsg.NewPolyEval(uint64(currentEon), receivers, encryptedEvals), - ) - if err != nil { - return err - } - for range receivers { - keypermetrics.MetricsKeyperDKGMessagesSent.WithLabelValues( - eonLabel, - keypermetrics.DKGMessageTypePolyEval, - ).Inc() - } - return nil - } - - for _, eval := range evals { - if eval.Eon != currentEon { - err = send() - if err != nil { - return err - } - currentEon = eval.Eon - } - - log.Info(). - Int64("eon", eval.Eon). - Str("receiverAddress", eval.ReceiverAddress). - Msg("sending poly eval") - receiver, err := shdb.DecodeAddress(eval.ReceiverAddress) - if err != nil { - return err - } - pubkey, err := shdb.DecodeEciesPublicKey(eval.EncryptionPublicKey) - if err != nil { - return fmt.Errorf("failed to decode encryption public key for %s from db: %w", eval.ReceiverAddress, err) - } - encrypted, err := ecies.Encrypt(rand.Reader, pubkey, eval.Eval, nil, nil) - if err != nil { - return err - } - receivers = append(receivers, receiver) - encryptedEvals = append(encryptedEvals, encrypted) - - err = queries.DeletePolyEval(ctx, database.DeletePolyEvalParams{ - Eon: eval.Eon, - ReceiverAddress: eval.ReceiverAddress, - }) - if err != nil { - return err - } - } - return send() -} - -func (st *ShuttermintState) handleBatchConfig( - ctx context.Context, queries *database.Queries, e *shutterevents.BatchConfig, -) error { - if !e.IsKeyper(st.config.GetAddress()) { - keypermetrics.MetricsKeyperIsKeyper.WithLabelValues(strconv.FormatUint(e.KeyperConfigIndex, 10)).Set(0) - } - if e.IsKeyper(st.config.GetAddress()) { - // Send a check-in message to announce our encryption and validator key. If we have already checked in - // for a previous batch config and neither of the keys has changed, this is redundant but harmless. - st.isKeyper = true - pubKey := st.config.GetValidatorPublicKey() - err := queries.ScheduleShutterMessage( - ctx, - fmt.Sprintf("check-in (validator-pub-key=%s)", hex.EncodeToString(pubKey)), - shmsg.NewCheckIn( - pubKey, - &st.config.GetEncryptionKey().PublicKey, - ), - ) - if err != nil { - return err - } - keypermetrics.MetricsKeyperIsKeyper.WithLabelValues(strconv.FormatUint(e.KeyperConfigIndex, 10)).Set(1) - } - keypers := []string{} - for _, k := range e.Keypers { - keypers = append(keypers, shdb.EncodeAddress(k)) - } - keypermetrics.MetricsKeyperBatchConfigInfo.WithLabelValues(strconv.FormatUint(e.KeyperConfigIndex, 10), strings.Join(keypers, ",")).Set(1) - if err := queries.InsertBatchConfig( - ctx, - database.InsertBatchConfigParams{ - KeyperConfigIndex: int32(e.KeyperConfigIndex), - Height: e.Height, - Threshold: int32(e.Threshold), - Keypers: keypers, - Started: e.Started, - ActivationBlockNumber: int64(e.ActivationBlockNumber), - }, - ); err != nil { - return err - } - - return queries.DeleteShutterMessageByDesc(ctx, fmt.Sprintf("new batch config (activation-block-number=%d, config-index=%d)", - e.ActivationBlockNumber, e.KeyperConfigIndex)) -} - -func (st *ShuttermintState) handleBatchConfigStarted( - ctx context.Context, - queries *database.Queries, - e *shutterevents.BatchConfigStarted, -) error { - eon, err := queries.GetLatestEonForKeyperConfig(ctx, int64(e.KeyperConfigIndex)) - if err != nil { - log.Warn().Uint64("keyperConfig", e.KeyperConfigIndex).Err(err).Msg("Couldn't get latest eon for keyper config index") - } else { - keypermetrics.MetricsKeyperCurrentEon.Set(float64(eon)) - } - keypermetrics.MetricsKeyperCurrentBatchConfigIndex.Set(float64(e.KeyperConfigIndex)) - return queries.SetBatchConfigStarted(ctx, int32(e.KeyperConfigIndex)) -} - -func (st *ShuttermintState) handleEonStarted( - ctx context.Context, queries *database.Queries, e *shutterevents.EonStarted, -) error { - if e.ActivationBlockNumber > math.MaxInt64 { - return errors.Errorf("activation block number %d of eon start would overflow int64", e.ActivationBlockNumber) - } - err := queries.InsertEon(ctx, database.InsertEonParams{ - Eon: int64(e.Eon), - Height: e.Height, - ActivationBlockNumber: int64(e.ActivationBlockNumber), - KeyperConfigIndex: int64(e.KeyperConfigIndex), - }) - if err != nil { - return err - } - - if !st.isKeyper { - return nil - } - - batchConfig, err := queries.GetBatchConfig(ctx, int32(e.KeyperConfigIndex)) - if err != nil { - return err - } - - keypers := []common.Address{} - for _, k := range batchConfig.Keypers { - a, err := shdb.DecodeAddress(k) - if err != nil { - return err - } - keypers = append(keypers, a) - } - - keyperIndex, err := medley.FindAddressIndex(keypers, st.config.GetAddress()) - if err != nil { - return nil - } - - keypermetrics.MetricsKeyperEonStartBlock.WithLabelValues(strconv.FormatUint(e.Eon, 10)).Set(float64(e.ActivationBlockNumber)) - - lastCommittedHeight, err := queries.GetLastCommittedHeight(ctx) - if err != nil { - return err - } - - phase := st.phaseLength.GetPhaseAtHeight(lastCommittedHeight+1, e.Height) - if phase > puredkg.Dealing { - log.Info().Uint64("eon", e.Eon).Msg("missed the dealing phase") - } - if phase == puredkg.Off { - panic("phase is off") - } - - pure := puredkg.NewPureDKG( - e.Eon, - uint64(len(batchConfig.Keypers)), - uint64(batchConfig.Threshold), - uint64(keyperIndex), - ) - dkg := &ActiveDKG{ - pure: &pure, - dirty: true, - startHeight: e.Height, - keypers: keypers, - } - st.dkg[e.Eon] = dkg - return st.shiftPhase(ctx, queries, e.Height, e.Eon, dkg) -} - -func (st *ShuttermintState) startPhase1Dealing( - ctx context.Context, queries *database.Queries, eon uint64, dkg *ActiveDKG, -) error { - pure := dkg.pure - commitment, polyEvals, err := pure.StartPhase1Dealing() - if err != nil { - log.Fatal().Err(err).Msg("aborting due to unexpected error") - } - dkg.markDirty() - eonLabel := strconv.FormatUint(eon, 10) - err = queries.ScheduleShutterMessage( - ctx, - fmt.Sprintf("poly commitment (eon=%d)", eon), - shmsg.NewPolyCommitment(eon, commitment.Gammas), - ) - if err != nil { - return err - } - keypermetrics.MetricsKeyperDKGMessagesSent.WithLabelValues( - eonLabel, - keypermetrics.DKGMessageTypePolyCommitment, - ).Inc() - - for _, eval := range polyEvals { - err = queries.InsertPolyEval(ctx, database.InsertPolyEvalParams{ - Eon: int64(eon), - ReceiverAddress: shdb.EncodeAddress(dkg.keypers[eval.Receiver]), - Eval: shdb.EncodeBigint(eval.Eval), - }) - if err != nil { - return err - } - } - return nil -} - -func (st *ShuttermintState) startPhase2Accusing( - ctx context.Context, queries *database.Queries, eon uint64, dkg *ActiveDKG, -) error { - accusations := dkg.pure.StartPhase2Accusing() - dkg.markDirty() - eonLabel := strconv.FormatUint(eon, 10) - if len(accusations) > 0 { - var accused []common.Address - for _, a := range accusations { - keypermetrics.MetricsKeyperDKGMessagesSent.WithLabelValues( - eonLabel, - keypermetrics.DKGMessageTypeAccusation, - ).Inc() - accused = append(accused, dkg.keypers[a.Accused]) - } - err := queries.ScheduleShutterMessage( - ctx, - fmt.Sprintf("accusations (eon=%d, count=%d)", eon, len(accusations)), - shmsg.NewAccusation(eon, accused), - ) - if err != nil { - return err - } - } else { - log.Info().Uint64("eon", eon).Msg("no one to accuse") - } - - return nil -} - -func (st *ShuttermintState) startPhase3Apologizing( - ctx context.Context, queries *database.Queries, eon uint64, dkg *ActiveDKG, -) error { - apologies := dkg.pure.StartPhase3Apologizing() - dkg.markDirty() - eonLabel := strconv.FormatUint(eon, 10) - if len(apologies) > 0 { - var accusers []common.Address - var polyEvals []*big.Int - - for _, a := range apologies { - keypermetrics.MetricsKeyperDKGMessagesSent.WithLabelValues( - eonLabel, - keypermetrics.DKGMessageTypeApology, - ).Inc() - accusers = append(accusers, dkg.keypers[a.Accuser]) - polyEvals = append(polyEvals, a.Eval) - } - - err := queries.ScheduleShutterMessage( - ctx, - fmt.Sprintf("apologies (eon=%d, count=%d)", eon, len(apologies)), - shmsg.NewApology(eon, accusers, polyEvals), - ) - if err != nil { - return err - } - } else { - log.Info().Uint64("eon", eon).Msg("no apologies needed") - } - - return nil -} - -func (st *ShuttermintState) finalizeDKG( - ctx context.Context, queries *database.Queries, eon uint64, dkg *ActiveDKG, -) error { - dkg.pure.Finalize() - // There's no need to call dkg.markDirty() here, since we now remove the object from memory - // and the database: - delete(st.dkg, eon) - err := queries.DeletePureDKG(ctx, int64(eon)) - if err != nil { - return err - } - - // There's really no need to keep them around now. We could even get rid of them earlier. - tag, err := queries.DeletePolyEvalByEon(ctx, int64(eon)) - if err != nil { - return err - } - log.Info().Int64("count", tag.RowsAffected()).Msg("deleted poly evals") - - var dkgerror sql.NullString - var pureResult []byte - - dkgresult, err := dkg.pure.ComputeResult() - - dkgresultmsg := shmsg.NewDKGResult(eon, err == nil) - - if err != nil { - keypermetrics.MetricsKeyperDKGStatus.WithLabelValues(strconv.FormatUint(eon, 10)).Set(0) - log.Error().Err(err).Uint64("eon", eon).Bool("success", false). - Msg("DKG process failed") - dkgerror = sql.NullString{String: err.Error(), Valid: true} - _, err := queries.GetEon(ctx, int64(eon)) - if err != nil { - return err - } - } else { - keypermetrics.MetricsKeyperDKGStatus.WithLabelValues(strconv.FormatUint(eon, 10)).Set(1) - log.Info().Uint64("eon", eon).Bool("success", true).Msg("DKG process succeeded") - pureResult, err = shdb.EncodePureDKGResult(&dkgresult) - if err != nil { - return err - } - publicKeyBytes, _ := dkgresult.PublicKey.GobEncode() - err := queries.InsertEonPublicKey(ctx, database.InsertEonPublicKeyParams{EonPublicKey: publicKeyBytes, Eon: int64(dkgresult.Eon)}) - if err != nil { - return err - } - } - - err = queries.ScheduleShutterMessage( - ctx, - fmt.Sprintf("reporting DKG result (eon=%d)", dkgresult.Eon), - dkgresultmsg, - ) - if err != nil { - return err - } - - return queries.InsertDKGResult(ctx, database.InsertDKGResultParams{ - Eon: int64(eon), - Success: pureResult != nil, - Error: dkgerror, - PureResult: pureResult, - }) -} - -func (st *ShuttermintState) shiftPhase( - ctx context.Context, queries *database.Queries, height int64, eon uint64, dkg *ActiveDKG, -) error { - phase := st.phaseLength.GetPhaseAtHeight(height, dkg.startHeight) - for currentPhase := dkg.pure.Phase; currentPhase < phase; currentPhase = dkg.pure.Phase { - log.Info(). - Uint64("eon", eon). - Int64("height", height). - Str("phaseAtHeight", phase.String()). - Str("current-phase", currentPhase.String()). - Str("next-phase", (currentPhase + 1).String()). - Msg("phase transition") - - var err error - switch currentPhase { - case puredkg.Off: - err = st.startPhase1Dealing(ctx, queries, eon, dkg) - case puredkg.Dealing: - err = st.startPhase2Accusing(ctx, queries, eon, dkg) - case puredkg.Accusing: - err = st.startPhase3Apologizing(ctx, queries, eon, dkg) - case puredkg.Apologizing: - err = st.finalizeDKG(ctx, queries, eon, dkg) - case puredkg.Finalized: - panic("internal error, currentPhase is Finalized") - } - if err != nil { - return err - } - if dkg.pure.Phase == currentPhase { - panic("phase did not change") - } - for i := 0; i <= int(puredkg.Finalized); i++ { // <- WTF Go - keypermetrics.MetricsKeyperCurrentPhase. - WithLabelValues( - strconv.FormatUint(eon, 10), - fmt.Sprintf("%d-%s", i, puredkg.Phase(i).String())). - Set(gog.If[float64](int(dkg.pure.Phase) == i, 1, 0)) - } - } - return nil -} - -func (st *ShuttermintState) shiftPhases( - ctx context.Context, queries *database.Queries, height int64, -) error { - for eon, dkg := range st.dkg { - err := st.shiftPhase(ctx, queries, height, eon, dkg) - if err != nil { - return err - } - } - return nil -} - -func (st *ShuttermintState) handleCheckIn( - ctx context.Context, queries *database.Queries, e *shutterevents.CheckIn, -) error { - // Store the key in the database, along with the height from which on it is - // valid. If there are multiple keys for the same address and height, only - // the last inserted one is kept. - err := queries.InsertEncryptionKey(ctx, database.InsertEncryptionKeyParams{ - Address: shdb.EncodeAddress(e.Sender), - EncryptionPublicKey: shdb.EncodeEciesPublicKey(e.EncryptionPublicKey), - Height: e.Height, - }) - return err -} - -func (st *ShuttermintState) handlePolyCommitment( - _ context.Context, _ *database.Queries, e *shutterevents.PolyCommitment, -) error { //nolint:unparam - dkg, ok := st.dkg[e.Eon] - if !ok { - log.Info().Str("event", e.String()). - Msg("event for non existent eon received") - return nil - } - senderIndex, err := medley.FindAddressIndex(dkg.keypers, e.Sender) - if err != nil { - log.Info().Str("event", e.String()). - Msg("Received PolyCommitment from non keyper address") - return nil - } - - err = dkg.pure.HandlePolyCommitmentMsg(puredkg.PolyCommitmentMsg{ - Eon: e.Eon, - Sender: uint64(senderIndex), - Gammas: e.Gammas, - }) - if err != nil { - log.Info().Str("event", e.String()).Err(err). - Msg("failed to handle PolyCommitment") - return nil - } - dkg.markDirty() - keypermetrics.MetricsKeyperDKGMessagesReceived.WithLabelValues( - strconv.FormatUint(e.Eon, 10), - keypermetrics.DKGMessageTypePolyCommitment, - ).Inc() - return nil -} - -func (st *ShuttermintState) decryptPolyEval(encrypted []byte) ([]byte, error) { - return st.config.GetEncryptionKey().Decrypt(encrypted, []byte(""), []byte("")) -} - -func (st *ShuttermintState) handlePolyEval( - _ context.Context, _ *database.Queries, e *shutterevents.PolyEval, -) error { - myAddress := st.config.GetAddress() - if e.Sender == myAddress { - return nil - } - - dkg, ok := st.dkg[e.Eon] - if !ok { - log.Info().Str("event", e.String()). - Msg("event for non existent eon received") - return nil - } - eonLabel := strconv.FormatUint(e.Eon, 10) - sender, err := medley.FindAddressIndex(dkg.keypers, e.Sender) - if err != nil { - return nil - } - keyperIndex, err := medley.FindAddressIndex(dkg.keypers, myAddress) - if err != nil { - return err - } - - myIndex, err := medley.FindAddressIndex(e.Receivers, myAddress) - if err != nil { - return nil - } - encrypted := e.EncryptedEvals[myIndex] - - evalBytes, err := st.decryptPolyEval(encrypted) - if err != nil { - log.Info().Str("event", e.String()).Err(err). - Msg("could not decrypt poly eval") - return nil - } - - err = dkg.pure.HandlePolyEvalMsg( - puredkg.PolyEvalMsg{ - Eon: e.Eon, - Sender: uint64(sender), - Receiver: uint64(keyperIndex), - Eval: new(big.Int).SetBytes(evalBytes), - }, - ) - if err != nil { - log.Info().Err(err).Msg("failed to handle PolyEvalMsg") - return nil - } - log.Info().Str("event", e.String()).Int("keyper", sender). - Msg("got poly eval message") - keypermetrics.MetricsKeyperDKGMessagesReceived.WithLabelValues( - eonLabel, - keypermetrics.DKGMessageTypePolyEval, - ).Inc() - dkg.markDirty() - return nil -} - -func (st *ShuttermintState) handleAccusation( - _ context.Context, _ *database.Queries, e *shutterevents.Accusation, -) error { //nolint:unparam - dkg, ok := st.dkg[e.Eon] - if !ok { - log.Info().Str("event", e.String()). - Msg("event for non existent eon received") - return nil - } - - if dkg.pure.Phase != puredkg.Accusing { - log.Warn().Str("event", e.String()).Str("phase", dkg.pure.Phase.String()). - Msg("received accusation in wrong phase") - return nil - } - eonLabel := strconv.FormatUint(e.Eon, 10) - sender, err := medley.FindAddressIndex(dkg.keypers, e.Sender) - if err != nil { - log.Info().Str("event", e.String()). - Msg("cannot handle accusation. bad sender") - return nil - } - - for _, accused := range e.Accused { - accusedIndex, err := medley.FindAddressIndex(dkg.keypers, accused) - if err != nil { - log.Info().Str("event", e.String()).Str("address", accused.Hex()). - Msg("accused address is not a keyper") - continue - } - err = dkg.pure.HandleAccusationMsg( - puredkg.AccusationMsg{ - Eon: e.Eon, - Accuser: uint64(sender), - Accused: uint64(accusedIndex), - }, - ) - if err != nil { - log.Info().Str("event", e.String()).Err(err). - Msg("cannot handle accusation") - continue - } - keypermetrics.MetricsKeyperDKGMessagesReceived.WithLabelValues( - eonLabel, - keypermetrics.DKGMessageTypeAccusation, - ).Inc() - } - dkg.markDirty() - return nil -} - -func (st *ShuttermintState) handleApology( - _ context.Context, _ *database.Queries, e *shutterevents.Apology, -) error { //nolint:unparam - dkg, ok := st.dkg[e.Eon] - if !ok { - log.Info().Str("event", e.String()). - Msg("event for non existent eon received") - return nil - } - if dkg.pure.Phase != puredkg.Apologizing { - log.Warn().Str("phase", dkg.pure.Phase.String()). - Msg("Warning: received apology in wrong phase") - return nil - } - eonLabel := strconv.FormatUint(e.Eon, 10) - sender, err := medley.FindAddressIndex(dkg.keypers, e.Sender) - if err != nil { - log.Info().Str("event", e.String()).Msg("failed to handle apology. bad sender") - return nil - } - - for j, accuser := range e.Accusers { - accuserIndex, err := medley.FindAddressIndex(dkg.keypers, accuser) - if err != nil { - log.Info().Str("event", e.String()).Str("address", accuser.Hex()). - Msg("accuser address is not a keyper") - continue - } - err = dkg.pure.HandleApologyMsg( - puredkg.ApologyMsg{ - Eon: e.Eon, - Accuser: uint64(accuserIndex), - Accused: uint64(sender), - Eval: e.PolyEval[j], - }) - if err != nil { - log.Info().Str("event", e.String()).Err(err).Msg("failed to handle apology") - continue - } - keypermetrics.MetricsKeyperDKGMessagesReceived.WithLabelValues( - eonLabel, - keypermetrics.DKGMessageTypeApology, - ).Inc() - } - dkg.markDirty() - return nil -} - -func (st *ShuttermintState) HandleEvent( - ctx context.Context, queries *database.Queries, event shutterevents.IEvent, -) error { - var err error - log.Info().Str("event", event.String()).Msg("handle shuttermint event") - switch e := event.(type) { - case *shutterevents.CheckIn: - err = st.handleCheckIn(ctx, queries, e) - case *shutterevents.BatchConfig: - err = st.handleBatchConfig(ctx, queries, e) - case *shutterevents.BatchConfigStarted: - err = st.handleBatchConfigStarted(ctx, queries, e) - case *shutterevents.EonStarted: - err = st.handleEonStarted(ctx, queries, e) - case *shutterevents.PolyCommitment: - err = st.handlePolyCommitment(ctx, queries, e) - case *shutterevents.PolyEval: - err = st.handlePolyEval(ctx, queries, e) - case *shutterevents.Accusation: - err = st.handleAccusation(ctx, queries, e) - case *shutterevents.Apology: - err = st.handleApology(ctx, queries, e) - default: - log.Warn().Str("type", reflect.TypeOf(event).String()).Interface("event", event). - Msg("HandleEvent not yet implemented for event type") - } - - return err -} diff --git a/rolling-shutter/keyperimpl/gnosis/config.go b/rolling-shutter/keyperimpl/gnosis/config.go index b0f3c1536..b24bd4556 100644 --- a/rolling-shutter/keyperimpl/gnosis/config.go +++ b/rolling-shutter/keyperimpl/gnosis/config.go @@ -1,14 +1,15 @@ package gnosis import ( + "crypto/rand" "io" "math" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/configuration" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/keys" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/metricsserver" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" ) @@ -33,8 +34,8 @@ func NewConfig() *Config { func (c *Config) Init() { c.P2P = p2p.NewConfig() c.Gnosis = NewGnosisConfig() - c.Shuttermint = kprconfig.NewShuttermintConfig() c.Metrics = metricsserver.NewConfig() + c.ECIESPrivateKey = &keys.ECDSAPrivate{} } type Config struct { @@ -46,10 +47,14 @@ type Config struct { HTTPReadOnly bool HTTPListenAddress string - Gnosis *GnosisConfig - P2P *p2p.Config - Shuttermint *kprconfig.ShuttermintConfig - Metrics *metricsserver.MetricsConfig + Gnosis *GnosisConfig + P2P *p2p.Config + Metrics *metricsserver.MetricsConfig + + // ECIESPrivateKey is the keyper's ECIES private key used to decrypt + // PolyEval blobs sent by other keypers during DKG. The corresponding + // public key is registered on-chain via the ECIES Key Registry. + ECIESPrivateKey *keys.ECDSAPrivate `shconfig:",required"` MaxNumKeysPerMessage uint64 } @@ -88,6 +93,12 @@ func (c *Config) SetExampleValues() error { c.DatabaseURL = "postgres://pguser:pgpassword@localhost:5432/shutter" c.BeaconAPIURL = "http://localhost:5052" + eciesKey, err := keys.GenerateECDSAKey(rand.Reader) + if err != nil { + return err + } + c.ECIESPrivateKey = eciesKey + return nil } @@ -176,6 +187,7 @@ type GnosisContractsConfig struct { KeyBroadcastContract common.Address `shconfig:",required"` Sequencer common.Address `shconfig:",required"` ValidatorRegistry common.Address `shconfig:",required"` + ECIESKeyRegistry common.Address `shconfig:",required"` } func NewGnosisContractsConfig() *GnosisContractsConfig { @@ -184,6 +196,7 @@ func NewGnosisContractsConfig() *GnosisContractsConfig { KeyBroadcastContract: common.Address{}, Sequencer: common.Address{}, ValidatorRegistry: common.Address{}, + ECIESKeyRegistry: common.Address{}, } } diff --git a/rolling-shutter/keyperimpl/gnosis/database/sql/migrations/V2_validatorRegistrations.sql b/rolling-shutter/keyperimpl/gnosis/database/sql/migrations/V002_validatorRegistrations.sql similarity index 100% rename from rolling-shutter/keyperimpl/gnosis/database/sql/migrations/V2_validatorRegistrations.sql rename to rolling-shutter/keyperimpl/gnosis/database/sql/migrations/V002_validatorRegistrations.sql diff --git a/rolling-shutter/keyperimpl/gnosis/keyper.go b/rolling-shutter/keyperimpl/gnosis/keyper.go index 70d8fbd8f..65996c39c 100644 --- a/rolling-shutter/keyperimpl/gnosis/keyper.go +++ b/rolling-shutter/keyperimpl/gnosis/keyper.go @@ -13,7 +13,7 @@ import ( sequencerBindings "github.com/shutter-network/gnosh-contracts/gnoshcontracts/sequencer" validatorRegistryBindings "github.com/shutter-network/gnosh-contracts/gnoshcontracts/validatorregistry" - "github.com/shutter-network/rolling-shutter/rolling-shutter/eonkeypublisher" + "github.com/shutter-network/rolling-shutter/rolling-shutter/dkg" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/epochkghandler" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" @@ -26,6 +26,7 @@ import ( "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/slotticker" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" + "github.com/shutter-network/rolling-shutter/rolling-shutter/txsender" ) var ErrParseKeyperSet = errors.New("cannot parse KeyperSet") @@ -47,15 +48,19 @@ type Keyper struct { chainSyncClient *chainsync.Client sequencerSyncer *SequencerSyncer validatorSyncer *ValidatorSyncer - eonKeyPublisher *eonkeypublisher.EonKeyPublisher latestTriggeredSlot *uint64 syncMonitor *SyncMonitor + txSender *txsender.TxSender + + // dkgMgr drives the per-block DKG participation loop. It owns no chain + // subscriptions and lives entirely off the database (see ADR 0004). + dkgMgr *dkg.Manager // input events - newBlocks chan *syncevent.LatestBlock - newKeyperSets chan *syncevent.KeyperSet - newEonPublicKeys chan keyper.EonPublicKey - slotTicker *slotticker.SlotTicker + newBlocks chan *syncevent.LatestBlock + newKeyperSets chan *syncevent.KeyperSet + newDKGEvents chan syncevent.DKGEvent + slotTicker *slotticker.SlotTicker // outputs decryptionTriggerChannel chan *broker.Event[*epochkghandler.DecryptionTrigger] @@ -73,7 +78,7 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { kpr.newBlocks = make(chan *syncevent.LatestBlock) kpr.newKeyperSets = make(chan *syncevent.KeyperSet) - kpr.newEonPublicKeys = make(chan keyper.EonPublicKey) + kpr.newDKGEvents = make(chan syncevent.DKGEvent) kpr.decryptionTriggerChannel = make(chan *broker.Event[*epochkghandler.DecryptionTrigger]) kpr.latestTriggeredSlot = nil @@ -114,8 +119,11 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { chainsync.WithClientURL(kpr.config.Gnosis.Node.EthereumURL), chainsync.WithKeyperSetManager(kpr.config.Gnosis.Contracts.KeyperSetManager), chainsync.WithKeyBroadcastContract(kpr.config.Gnosis.Contracts.KeyBroadcastContract), + chainsync.WithECIESKeyRegistry(kpr.config.Gnosis.Contracts.ECIESKeyRegistry), chainsync.WithSyncNewBlock(kpr.channelNewBlock), chainsync.WithSyncNewKeyperSet(kpr.channelNewKeyperSet), + chainsync.WithSyncECIESKey(kpr.processNewECIESKey), + chainsync.WithSyncDKGEvent(kpr.channelNewDKGEvent), chainsync.WithPrivateKey(kpr.config.Gnosis.Node.PrivateKey.Key), chainsync.WithLogger(gethLog.NewLogger(slog.Default().Handler())), ) @@ -123,20 +131,6 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { return err } - eonKeyPublisherClient, err := ethclient.DialContext(ctx, kpr.config.Gnosis.Node.EthereumURL) - if err != nil { - return errors.Wrapf(err, "failed to dial ethereum node at %s", kpr.config.Gnosis.Node.EthereumURL) - } - kpr.eonKeyPublisher, err = eonkeypublisher.NewEonKeyPublisher( - kpr.dbpool, - eonKeyPublisherClient, - kpr.config.Gnosis.Contracts.KeyperSetManager, - kpr.config.Gnosis.Node.PrivateKey.Key, - ) - if err != nil { - return errors.Wrap(err, "failed to initialize eon key publisher") - } - err = kpr.initSequencerSyncer(ctx) if err != nil { return err @@ -146,6 +140,13 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { return err } + kpr.dkgMgr = dkg.New(dkg.NewConfigFromECDSA( + kpr.dbpool, + kpr.config.GetAddress(), + kpr.config.ECIESPrivateKey.Key, + kpr.config.Gnosis.Contracts.ECIESKeyRegistry, + )) + if kpr.config.Metrics.Enabled { InitMetrics(kpr.beaconAPIClient) } @@ -164,9 +165,14 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { DBPool: kpr.dbpool, CheckInterval: time.Duration(kpr.config.Gnosis.SyncMonitorCheckInterval) * time.Second, } + kpr.txSender = txsender.New(txsender.Config{ + DBPool: kpr.dbpool, + Client: kpr.chainSyncClient, + PrivateKey: kpr.config.Gnosis.Node.PrivateKey.Key, + }) runner.Go(func() error { return kpr.processInputs(ctx) }) - return runner.StartService(kpr.core, kpr.chainSyncClient, kpr.slotTicker, kpr.eonKeyPublisher) + return runner.StartService(kpr.core, kpr.chainSyncClient, kpr.slotTicker, kpr.txSender) } func NewKeyper(kpr *Keyper, messagingMiddleware *MessagingMiddleware) (*keyper.KeyperCore, error) { @@ -179,14 +185,11 @@ func NewKeyper(kpr *Keyper, messagingMiddleware *MessagingMiddleware) (*keyper.K HTTPListenAddress: kpr.config.HTTPListenAddress, P2P: kpr.config.P2P, Ethereum: kpr.config.Gnosis.Node, - Shuttermint: kpr.config.Shuttermint, Metrics: kpr.config.Metrics, MaxNumKeysPerMessage: kpr.config.MaxNumKeysPerMessage, }, kpr.decryptionTriggerChannel, keyper.WithDBPool(kpr.dbpool), - keyper.NoBroadcastEonPublicKey(), - keyper.WithEonPublicKeyHandler(kpr.channelNewEonPublicKey), keyper.WithMessaging(messagingMiddleware), ) return core, err @@ -278,8 +281,8 @@ func (kpr *Keyper) processInputs(ctx context.Context) error { err = kpr.processNewBlock(ctx, ev) case ev := <-kpr.newKeyperSets: err = kpr.processNewKeyperSet(ctx, ev) - case ev := <-kpr.newEonPublicKeys: - err = kpr.processNewEonPublicKey(ctx, ev) + case ev := <-kpr.newDKGEvents: + err = kpr.processNewDKGEvent(ctx, ev) case slot := <-kpr.slotTicker.C: err = kpr.processNewSlot(ctx, slot) case <-ctx.Done(): @@ -313,9 +316,9 @@ func (kpr *Keyper) channelNewKeyperSet(ctx context.Context, ev *syncevent.Keyper } } -func (kpr *Keyper) channelNewEonPublicKey(ctx context.Context, key keyper.EonPublicKey) error { +func (kpr *Keyper) channelNewDKGEvent(ctx context.Context, ev syncevent.DKGEvent) error { select { - case kpr.newEonPublicKeys <- key: + case kpr.newDKGEvents <- ev: return nil case <-ctx.Done(): return ctx.Err() diff --git a/rolling-shutter/keyperimpl/gnosis/newblock.go b/rolling-shutter/keyperimpl/gnosis/newblock.go index 8a1af2400..f2549ab00 100644 --- a/rolling-shutter/keyperimpl/gnosis/newblock.go +++ b/rolling-shutter/keyperimpl/gnosis/newblock.go @@ -17,6 +17,9 @@ func (kpr *Keyper) processNewBlock(ctx context.Context, ev *syncevent.LatestBloc if err != nil { return err } + if err := kpr.dkgMgr.HandleBlock(ctx, ev.Header.Number.Uint64()); err != nil { + return err + } slot := medley.BlockTimestampToSlot( ev.Header.Time, kpr.config.Gnosis.GenesisSlotTimestamp, diff --git a/rolling-shutter/keyperimpl/gnosis/newdkgevent.go b/rolling-shutter/keyperimpl/gnosis/newdkgevent.go new file mode 100644 index 000000000..ca579689a --- /dev/null +++ b/rolling-shutter/keyperimpl/gnosis/newdkgevent.go @@ -0,0 +1,208 @@ +package gnosis + +import ( + "context" + + "github.com/jackc/pgx/v4" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + "github.com/shutter-network/rolling-shutter/rolling-shutter/dkg" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" + syncevent "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" +) + +// processNewDKGEvent stores a DKG Contract event in the appropriate per-type +// table and writes a `dkg_result` row when the event is a success notification. +// Events for a `keyper_set_index` whose DKG already succeeded locally are +// silently ignored so re-emitted or retried-and-superseded messages do not +// pollute the message tables. +func (kpr *Keyper) processNewDKGEvent(ctx context.Context, ev syncevent.DKGEvent) error { + keyperSetIndex, retryCounter := dkgEventKeys(ev) + keyperSetIndexInt, err := medley.Uint64ToInt64Safe(keyperSetIndex) + if err != nil { + return errors.Wrap(err, "convert keyper set index") + } + retryCounterInt, err := medley.Uint64ToInt64Safe(retryCounter) + if err != nil { + return errors.Wrap(err, "convert retry counter") + } + + return kpr.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + queries := corekeyperdb.New(tx) + obsQueries := obskeyper.New(tx) + + // Skip events for DKG instances we have already recorded as successful. + // Once a keyper set has produced a usable eon key, late-arriving + // dealings/accusations from retries we no longer participate in are not + // interesting and would just bloat the message tables. + exists, err := queries.ExistsDKGResultSuccess(ctx, keyperSetIndexInt) + if err != nil { + return errors.Wrap(err, "check existing dkg_result success") + } + if _, isSuccess := ev.(*syncevent.SuccessEvent); exists && !isSuccess { + log.Debug(). + Uint64("keyper-set-index", keyperSetIndex). + Uint64("retry-counter", retryCounter). + Msg("ignoring DKG event for already-succeeded keyper set") + return nil + } + + switch e := ev.(type) { + case *syncevent.DealingEvent: + return storeDealing(ctx, queries, obsQueries, e, keyperSetIndexInt, retryCounterInt) + case *syncevent.AccusationEvent: + return storeAccusation(ctx, queries, e, keyperSetIndexInt, retryCounterInt) + case *syncevent.ApologyEvent: + return storeApology(ctx, queries, e, keyperSetIndexInt, retryCounterInt) + case *syncevent.SuccessVoteEvent: + // Success votes are not stored in their own table; the aggregate + // outcome arrives as a separate DKGSucceeded event. + log.Debug(). + Uint64("keyper-set-index", e.KeyperSetIndex). + Uint64("retry-counter", e.RetryCounter). + Uint64("voter", e.KeyperIndex). + Msg("observed DKG success vote") + return nil + case *syncevent.SuccessEvent: + return kpr.dkgMgr.HandleDKGSuccess(ctx, tx, keyperSetIndexInt, retryCounterInt) + default: + return errors.Errorf("unknown DKG event type %T", ev) + } + }) +} + +// dkgEventKeys extracts the (keyperSetIndex, retryCounter) pair common to every +// DKG event variant. SuccessEvent has no retry counter and reports zero. +func dkgEventKeys(ev syncevent.DKGEvent) (keyperSetIndex, retryCounter uint64) { + switch e := ev.(type) { + case *syncevent.DealingEvent: + return e.KeyperSetIndex, e.RetryCounter + case *syncevent.AccusationEvent: + return e.KeyperSetIndex, e.RetryCounter + case *syncevent.ApologyEvent: + return e.KeyperSetIndex, e.RetryCounter + case *syncevent.SuccessVoteEvent: + return e.KeyperSetIndex, e.RetryCounter + case *syncevent.SuccessEvent: + return e.KeyperSetIndex, e.RetryCounter + } + return 0, 0 +} + +func storeDealing( + ctx context.Context, + queries *corekeyperdb.Queries, + obsQueries *obskeyper.Queries, + ev *syncevent.DealingEvent, + keyperSetIndex, retryCounter int64, +) error { + senderIndex, err := medley.Uint64ToInt64Safe(ev.KeyperIndex) + if err != nil { + return errors.Wrap(err, "convert sender index") + } + + if err := queries.InsertDKGPolyCommitment(ctx, corekeyperdb.InsertDKGPolyCommitmentParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + KeyperIndex: senderIndex, + Commitment: ev.Commitment, + }); err != nil { + return errors.Wrap(err, "insert dkg_poly_commitment") + } + + evals := ev.PolyEvals + + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, keyperSetIndex) + if err != nil { + return errors.Wrapf(err, "load keyper set %d for polyEval split", keyperSetIndex) + } + n := uint64(len(keyperSet.Keypers)) + receivers := dkg.ReceiverIndicesForSender(n, ev.KeyperIndex) + if uint64(len(evals)) != uint64(len(receivers)) { + return errors.Errorf( + "polyEval count %d does not match expected receiver count %d for keyper set %d", + len(evals), len(receivers), keyperSetIndex, + ) + } + + for i, encryptedEval := range evals { + receiverIndex, err := medley.Uint64ToInt64Safe(receivers[i]) + if err != nil { + return errors.Wrap(err, "convert receiver index") + } + if err := queries.InsertDKGPolyEval(ctx, corekeyperdb.InsertDKGPolyEvalParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + SenderIndex: senderIndex, + ReceiverIndex: receiverIndex, + EncryptedEval: encryptedEval, + }); err != nil { + return errors.Wrap(err, "insert dkg_poly_eval") + } + } + return nil +} + +func storeAccusation( + ctx context.Context, + queries *corekeyperdb.Queries, + ev *syncevent.AccusationEvent, + keyperSetIndex, retryCounter int64, +) error { + accuserIndex, err := medley.Uint64ToInt64Safe(ev.KeyperIndex) + if err != nil { + return errors.Wrap(err, "convert accuser index") + } + for _, accused := range ev.AccusedIndices { + accusedIndex, err := medley.Uint64ToInt64Safe(accused) + if err != nil { + return errors.Wrap(err, "convert accused index") + } + if err := queries.InsertDKGAccusation(ctx, corekeyperdb.InsertDKGAccusationParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + AccuserIndex: accuserIndex, + AccusedIndex: accusedIndex, + }); err != nil { + return errors.Wrap(err, "insert dkg_accusation") + } + } + return nil +} + +func storeApology( + ctx context.Context, + queries *corekeyperdb.Queries, + ev *syncevent.ApologyEvent, + keyperSetIndex, retryCounter int64, +) error { + if len(ev.AccuserIndices) != len(ev.PolyEvalData) { + return errors.Errorf( + "apology accuser count %d does not match polyEval data count %d", + len(ev.AccuserIndices), len(ev.PolyEvalData), + ) + } + apologizerIndex, err := medley.Uint64ToInt64Safe(ev.KeyperIndex) + if err != nil { + return errors.Wrap(err, "convert apologizer index") + } + for i, accuser := range ev.AccuserIndices { + accuserIndex, err := medley.Uint64ToInt64Safe(accuser) + if err != nil { + return errors.Wrap(err, "convert accuser index") + } + if err := queries.InsertDKGApology(ctx, corekeyperdb.InsertDKGApologyParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + ApologizerIndex: apologizerIndex, + AccuserIndex: accuserIndex, + PolyEval: ev.PolyEvalData[i], + }); err != nil { + return errors.Wrap(err, "insert dkg_apology") + } + } + return nil +} diff --git a/rolling-shutter/keyperimpl/gnosis/newecieskey.go b/rolling-shutter/keyperimpl/gnosis/newecieskey.go new file mode 100644 index 000000000..8f57f679c --- /dev/null +++ b/rolling-shutter/keyperimpl/gnosis/newecieskey.go @@ -0,0 +1,25 @@ +package gnosis + +import ( + "context" + + "github.com/rs/zerolog/log" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + syncevent "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" +) + +// processNewECIESKey upserts the keyper's ECIES public key into the local +// `ecies_keys` cache. It is invoked for both the synthetic events emitted by +// the initial poll and live `KeyRegistered` events from the registry contract. +func (kpr *Keyper) processNewECIESKey(ctx context.Context, ev *syncevent.ECIESKey) error { + log.Debug(). + Str("keyper", ev.Keyper.Hex()). + Int("key-len", len(ev.EciesPublicKey)). + Msg("storing ECIES public key") + return corekeyperdb.New(kpr.dbpool).UpsertECIESKey(ctx, corekeyperdb.UpsertECIESKeyParams{ + KeyperAddress: shdb.EncodeAddress(ev.Keyper), + EciesPublicKey: ev.EciesPublicKey, + }) +} diff --git a/rolling-shutter/keyperimpl/gnosis/neweonpublickey.go b/rolling-shutter/keyperimpl/gnosis/neweonpublickey.go deleted file mode 100644 index cdba7f693..000000000 --- a/rolling-shutter/keyperimpl/gnosis/neweonpublickey.go +++ /dev/null @@ -1,12 +0,0 @@ -package gnosis - -import ( - "context" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" -) - -func (kpr *Keyper) processNewEonPublicKey(_ context.Context, key keyper.EonPublicKey) error { //nolint: unparam - kpr.eonKeyPublisher.Publish(key) - return nil -} diff --git a/rolling-shutter/keyperimpl/gnosis/newkeyperset.go b/rolling-shutter/keyperimpl/gnosis/newkeyperset.go index 801c379de..c49e57112 100644 --- a/rolling-shutter/keyperimpl/gnosis/newkeyperset.go +++ b/rolling-shutter/keyperimpl/gnosis/newkeyperset.go @@ -2,21 +2,28 @@ package gnosis import ( "context" + "database/sql" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" "github.com/jackc/pgx/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + keypersetBindings "github.com/shutter-network/contracts/v2/bindings/keyperset" obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" syncevent "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" ) func (kpr *Keyper) processNewKeyperSet(ctx context.Context, ev *syncevent.KeyperSet) error { + ownAddress := kpr.config.GetAddress() isMember := false for _, m := range ev.Members { - if m.Cmp(kpr.config.GetAddress()) == 0 { + if m.Cmp(ownAddress) == 0 { isMember = true break } @@ -29,13 +36,32 @@ func (kpr *Keyper) processNewKeyperSet(ctx context.Context, ev *syncevent.Keyper Bool("is-member", isMember). Msg("new keyper set added") - return kpr.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { - obskeyperdb := obskeyper.New(tx) + keyperSetIndex, err := medley.Uint64ToInt64Safe(ev.Eon) + if err != nil { + return errors.Wrap(err, ErrParseKeyperSet.Error()) + } - keyperConfigIndex, err := medley.Uint64ToInt64Safe(ev.Eon) + // Fetch DKG phase params before opening the DB transaction: the RPC calls + // can be slow and we do not want to hold row locks across them. On event + // replay this is a wasted round-trip because the existing-row check inside + // the transaction will short-circuit, but replays are rare. + var ( + dkgContract sql.NullString + phaseLength sql.NullInt64 + leadLength sql.NullInt64 + maxRetries int64 + ) + if isMember { + dkgContract, phaseLength, leadLength, maxRetries, err = kpr.fetchDKGParamsForKeyperSet(ctx, ev.Contract) if err != nil { - return errors.Wrap(err, ErrParseKeyperSet.Error()) + return errors.Wrap(err, "fetch DKG params for new keyper set") } + } + + if err := kpr.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + obskeyperdb := obskeyper.New(tx) + coredb := corekeyperdb.New(tx) + activationBlockNumber, err := medley.Uint64ToInt64Safe(ev.ActivationBlock) if err != nil { return errors.Wrap(err, ErrParseKeyperSet.Error()) @@ -45,11 +71,111 @@ func (kpr *Keyper) processNewKeyperSet(ctx context.Context, ev *syncevent.Keyper return errors.Wrap(err, ErrParseKeyperSet.Error()) } - return obskeyperdb.InsertKeyperSet(ctx, obskeyper.InsertKeyperSetParams{ - KeyperConfigIndex: keyperConfigIndex, + if err := obskeyperdb.InsertKeyperSet(ctx, obskeyper.InsertKeyperSetParams{ + KeyperConfigIndex: keyperSetIndex, ActivationBlockNumber: activationBlockNumber, Keypers: shdb.EncodeAddresses(ev.Members), Threshold: int32(threshold), - }) - }) + }); err != nil { + return err + } + + if isMember { + // Eagerly insert an eons row so the DKG participation loop has + // somewhere to anchor when the activation block approaches. + // Existing rows are tolerated because the chainsync initial poll + // can re-deliver KeyperSetAdded events that were already processed. + if _, err := coredb.GetEon(ctx, keyperSetIndex); err == nil { + return nil + } else if !errors.Is(err, pgx.ErrNoRows) { + return errors.Wrap(err, "check existing eon row") + } + if err := coredb.InsertEon(ctx, corekeyperdb.InsertEonParams{ + Eon: keyperSetIndex, + ActivationBlockNumber: activationBlockNumber, + KeyperConfigIndex: keyperSetIndex, + DkgContract: dkgContract, + PhaseLength: phaseLength, + LeadLength: leadLength, + MaxRetries: maxRetries, + }); err != nil { + return errors.Wrap(err, "insert eon row for new keyper set") + } + } + return nil + }); err != nil { + return err + } + + // Must run after the transaction commits: MaybeRegisterECIESKey reads + // the keyper set row inserted above. + // + // This can still land too late. If DKG starts immediately (the + // KeyperSetAdded tx was mined inside the lead-time window), peers + // dispatch dealings before our ECIES registration is on chain and + // encrypt without our key. Returning keypers already have a key from + // a previous set, so this only bites new members. Mitigation: give + // keyper sets enough lead time. + return kpr.dkgMgr.MaybeRegisterECIESKey(ctx, keyperSetIndex) +} + +// fetchDKGParamsForKeyperSet asks the keyper set contract for its DKG contract +// address and then reads the immutable phase parameters from that DKG contract. +// Any failure (zero address, RPC error, missing methods on an old contract) is +// returned as an error. The DKG manager treats NULL phase params in the eons +// row as a fatal configuration error and has no chain-client fallback, so we +// must not insert the eon row without these values; propagating the error lets +// the outer event loop log and retry on the next chainsync re-delivery. +func (kpr *Keyper) fetchDKGParamsForKeyperSet( + ctx context.Context, + keyperSetAddr common.Address, +) (sql.NullString, sql.NullInt64, sql.NullInt64, int64, error) { + var ( + nullStr sql.NullString + nullInt sql.NullInt64 + ) + if (keyperSetAddr == common.Address{}) { + return nullStr, nullInt, nullInt, 0, errors.New("keyper set event missing contract address") + } + ks, err := keypersetBindings.NewKeyperset(keyperSetAddr, kpr.chainSyncClient.Client) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "bind keyper set contract %s", keyperSetAddr.Hex()) + } + callOpts := &bind.CallOpts{Context: ctx} + dkgAddr, err := ks.GetDKGContract(callOpts) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "call getDKGContract on keyper set %s", keyperSetAddr.Hex()) + } + if (dkgAddr == common.Address{}) { + return nullStr, nullInt, nullInt, 0, errors.Errorf("keyper set %s has no DKG contract configured", keyperSetAddr.Hex()) + } + dkg, err := dkgcontract.NewDkgcontract(dkgAddr, kpr.chainSyncClient.Client) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "bind DKG contract %s", dkgAddr.Hex()) + } + phaseLength, err := dkg.PHASELENGTH(callOpts) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "read PHASE_LENGTH from %s", dkgAddr.Hex()) + } + leadLength, err := dkg.DKGLEADLENGTH(callOpts) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "read DKG_LEAD_LENGTH from %s", dkgAddr.Hex()) + } + maxRetries, err := dkg.MAXRETRIES(callOpts) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "read MAX_RETRIES from %s", dkgAddr.Hex()) + } + log.Info(). + Str("keyper-set", keyperSetAddr.Hex()). + Str("dkg-contract", dkgAddr.Hex()). + Uint64("phase-length", phaseLength). + Uint64("lead-length", leadLength). + Uint64("max-retries", maxRetries). + Msg("resolved per-keyper-set DKG contract params") + //nolint:gosec // G115: phase length, lead length, and max retries come from the on-chain contract and fit well within int64 + return sql.NullString{String: dkgAddr.Hex(), Valid: true}, + sql.NullInt64{Int64: int64(phaseLength), Valid: true}, + sql.NullInt64{Int64: int64(leadLength), Valid: true}, + int64(maxRetries), + nil } diff --git a/rolling-shutter/keyperimpl/optimism/bootstrap/bootstrap.go b/rolling-shutter/keyperimpl/optimism/bootstrap/bootstrap.go deleted file mode 100644 index 439f69cc4..000000000 --- a/rolling-shutter/keyperimpl/optimism/bootstrap/bootstrap.go +++ /dev/null @@ -1,56 +0,0 @@ -package bootstrap - -import ( - "context" - "encoding/json" - "os" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/tendermint/tendermint/rpc/client/http" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/fx" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -func BootstrapValidators(config *Config) error { - file, err := os.ReadFile(config.KeyperSetFilePath) - if err != nil { - log.Fatal().Err(err).Msg("failed to read keyper-set file") - } - ks := &event.KeyperSet{} - - err = json.Unmarshal(file, ks) - if err != nil { - log.Fatal().Err(err).Msg("failed to read parse keyper-set") - } - - ctx := context.Background() - - shmcl, err := http.New(config.ShuttermintURL, "/websocket") - if err != nil { - log.Fatal().Err(err).Msg("failed to connect to Shuttermint node") - } - - ms := fx.NewRPCMessageSender(shmcl, config.SigningKey.Key) - batchConfigMsg := shmsg.NewBatchConfig( - ks.ActivationBlock, - ks.Members, - ks.Threshold, - ks.Eon, - ) - - err = ms.SendMessage(ctx, batchConfigMsg) - if err != nil { - return errors.Errorf("Failed to send batch config message: %v", err) - } - - blockSeenMsg := shmsg.NewBlockSeen(ks.ActivationBlock) - err = ms.SendMessage(ctx, blockSeenMsg) - if err != nil { - return errors.Errorf("Failed to send start message: %v", err) - } - - return nil -} diff --git a/rolling-shutter/keyperimpl/optimism/bootstrap/config.go b/rolling-shutter/keyperimpl/optimism/bootstrap/config.go deleted file mode 100644 index 5e1e55862..000000000 --- a/rolling-shutter/keyperimpl/optimism/bootstrap/config.go +++ /dev/null @@ -1,73 +0,0 @@ -package bootstrap - -import ( - "crypto/rand" - "io" - - "github.com/ethereum/go-ethereum/common" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/configuration" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/keys" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/number" -) - -var _ configuration.Config = &Config{} - -func NewConfig() *Config { - c := &Config{} - c.Init() - return c -} - -func (c *Config) Init() { - c.SigningKey = &keys.ECDSAPrivate{} - c.ByActivationBlockNumber = number.NewBlockNumber(nil) -} - -type Config struct { - InstanceID uint64 `shconfig:",required"` - - JSONRPCURL string ` comment:"The op-geth JSON RPC endpoint"` - ByActivationBlockNumber *number.BlockNumber - KeyperSetManager common.Address - ByIndex *uint64 - KeyperSetFilePath string - - ShuttermintURL string - SigningKey *keys.ECDSAPrivate `shconfig:",required"` -} - -func (c *Config) Validate() error { - return nil -} - -func (c *Config) Name() string { - return "op-bootstrap" -} - -func (c *Config) SetDefaultValues() error { - c.JSONRPCURL = "http://localhost:8545" - c.ShuttermintURL = "http://localhost:26657" - c.KeyperSetFilePath = "keyperset.json" - c.ByActivationBlockNumber = nil - i := uint64(1) - c.ByIndex = &i - return nil -} - -func (c *Config) SetExampleValues() error { - err := c.SetDefaultValues() - if err != nil { - return err - } - c.SigningKey, err = keys.GenerateECDSAKey(rand.Reader) - if err != nil { - return err - } - c.InstanceID = 42 - return nil -} - -func (c Config) TOMLWriteHeader(_ io.Writer) (int, error) { - return 0, nil -} diff --git a/rolling-shutter/keyperimpl/optimism/bootstrap/keyperset.go b/rolling-shutter/keyperimpl/optimism/bootstrap/keyperset.go deleted file mode 100644 index cbf30cee3..000000000 --- a/rolling-shutter/keyperimpl/optimism/bootstrap/keyperset.go +++ /dev/null @@ -1,41 +0,0 @@ -package bootstrap - -import ( - "context" - "encoding/json" - "errors" - "os" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" - "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/number" -) - -func GetKeyperSet(ctx context.Context, config *Config) error { - sl2, err := chainsync.NewClient( - ctx, - chainsync.WithClientURL(config.JSONRPCURL), - chainsync.WithKeyperSetManager(config.KeyperSetManager), - ) - if err != nil { - return err - } - var keyperSet *event.KeyperSet - switch { - case config.ByIndex == nil && config.ByActivationBlockNumber == nil: - keyperSet, err = sl2.GetKeyperSetForBlock(ctx, number.LatestBlock) - case config.ByIndex == nil && config.ByActivationBlockNumber != nil: - keyperSet, err = sl2.GetKeyperSetForBlock(ctx, config.ByActivationBlockNumber) - case config.ByIndex != nil && config.ByActivationBlockNumber == nil: - keyperSet, err = sl2.GetKeyperSetByIndex(ctx, *config.ByIndex) - case config.ByIndex != nil && config.ByActivationBlockNumber != nil: - return errors.New("can only retrieve keyper set by either index or activation-blocknumber") - default: - return nil - } - if err != nil { - return err - } - file, _ := json.MarshalIndent(keyperSet, "", " ") - return os.WriteFile(config.KeyperSetFilePath, file, 0o644) -} diff --git a/rolling-shutter/keyperimpl/optimism/config/config.go b/rolling-shutter/keyperimpl/optimism/config/config.go index 374f03194..295cde3d9 100644 --- a/rolling-shutter/keyperimpl/optimism/config/config.go +++ b/rolling-shutter/keyperimpl/optimism/config/config.go @@ -5,7 +5,6 @@ import ( "github.com/ethereum/go-ethereum/common" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/configuration" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/metricsserver" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" @@ -22,7 +21,6 @@ func NewConfig() *Config { func (c *Config) Init() { c.P2P = p2p.NewConfig() c.Optimism = NewEthnodeConfig() - c.Shuttermint = kprconfig.NewShuttermintConfig() c.Metrics = metricsserver.NewConfig() } @@ -33,10 +31,9 @@ type Config struct { HTTPEnabled bool HTTPListenAddress string - P2P *p2p.Config - Optimism *OptimismConfig - Shuttermint *kprconfig.ShuttermintConfig - Metrics *metricsserver.MetricsConfig + P2P *p2p.Config + Optimism *OptimismConfig + Metrics *metricsserver.MetricsConfig MaxNumKeysPerMessage uint64 } diff --git a/rolling-shutter/keyperimpl/optimism/keyper.go b/rolling-shutter/keyperimpl/optimism/keyper.go index 07ec6191e..9a1903c5e 100644 --- a/rolling-shutter/keyperimpl/optimism/keyper.go +++ b/rolling-shutter/keyperimpl/optimism/keyper.go @@ -3,7 +3,6 @@ package optimism import ( "context" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" "github.com/pkg/errors" @@ -66,14 +65,11 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { HTTPListenAddress: kpr.config.HTTPListenAddress, P2P: kpr.config.P2P, Ethereum: ethConfig, - Shuttermint: kpr.config.Shuttermint, Metrics: kpr.config.Metrics, MaxNumKeysPerMessage: kpr.config.MaxNumKeysPerMessage, }, trigger, keyper.WithDBPool(dbpool), - keyper.NoBroadcastEonPublicKey(), - keyper.WithEonPublicKeyHandler(kpr.newEonPublicKey), ) if err != nil { return errors.Wrap(err, "can't instantiate keyper core") @@ -140,35 +136,3 @@ func (kpr *Keyper) newKeyperSet(ctx context.Context, ev *syncevent.KeyperSet) er }) }) } - -func (kpr *Keyper) newEonPublicKey(ctx context.Context, pubKey keyper.EonPublicKey) error { - log.Info(). - Uint64("eon", pubKey.Eon). - Uint64("activation-block", pubKey.ActivationBlock). - Msg("new eon pk") - // Currently all keypers call this and race to call this function first. - // For now this is fine, but a keyper should only send a transaction if - // the key is not set yet. - // Best would be a coordinatated leader election who will broadcast the key. - tx, err := kpr.l2Client.BroadcastEonKey(ctx, pubKey.Eon, pubKey.PublicKey) - if err != nil { - log.Error().Err(err).Msg("error broadcasting eon public key") - return errors.Wrap(err, "error broadcasting eon public-key") - } - log.Info(). - Str("hash", tx.Hash().Hex()). - Msg("sent eon pubkey transaction") - - receipt, err := bind.WaitMined(ctx, kpr.l2Client, tx) - if err != nil { - log.Error().Err(err).Msg("error waiting for pubkey tx mined") - return err - } - // NOCHECKIN: log the JSON receipt or only specific fields - log.Info(). - Interface("receipt", receipt). - Msg("eon pubkey transaction mined") - // TODO: - // wait / confirm of tx, otherwise resend - return nil -} diff --git a/rolling-shutter/keyperimpl/primev/config.go b/rolling-shutter/keyperimpl/primev/config.go index ce322d788..0f2b49f6f 100644 --- a/rolling-shutter/keyperimpl/primev/config.go +++ b/rolling-shutter/keyperimpl/primev/config.go @@ -5,7 +5,6 @@ import ( "github.com/ethereum/go-ethereum/common" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/configuration" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/metricsserver" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" @@ -19,10 +18,9 @@ type Config struct { Primev *PrimevConfig - Chain *ChainConfig - P2P *p2p.Config - Shuttermint *kprconfig.ShuttermintConfig - Metrics *metricsserver.MetricsConfig + Chain *ChainConfig + P2P *p2p.Config + Metrics *metricsserver.MetricsConfig MaxNumKeysPerMessage uint64 } @@ -36,7 +34,6 @@ func NewConfig() *Config { func (c *Config) Init() { c.P2P = p2p.NewConfig() c.Primev = NewPrimevConfig() - c.Shuttermint = kprconfig.NewShuttermintConfig() c.Chain = NewChainConfig() c.Metrics = metricsserver.NewConfig() } diff --git a/rolling-shutter/keyperimpl/primev/keyper.go b/rolling-shutter/keyperimpl/primev/keyper.go index 609a35672..6f65afd88 100644 --- a/rolling-shutter/keyperimpl/primev/keyper.go +++ b/rolling-shutter/keyperimpl/primev/keyper.go @@ -11,7 +11,6 @@ import ( providerregistry "github.com/primev/mev-commit/contracts-abi/clients/ProviderRegistry" "github.com/rs/zerolog/log" - "github.com/shutter-network/rolling-shutter/rolling-shutter/eonkeypublisher" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/epochkghandler" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" @@ -33,9 +32,7 @@ type Keyper struct { chainSyncClient *chainsync.Client providerRegistrySyncer *ProviderRegistrySyncer - eonKeyPublisher *eonkeypublisher.EonKeyPublisher newKeyperSets chan *syncevent.KeyperSet - newEonPublicKeys chan keyper.EonPublicKey newBlocks chan *syncevent.LatestBlock // outputs @@ -52,7 +49,6 @@ func (k *Keyper) Start(ctx context.Context, runner service.Runner) error { var err error k.newKeyperSets = make(chan *syncevent.KeyperSet) - k.newEonPublicKeys = make(chan keyper.EonPublicKey) k.newBlocks = make(chan *syncevent.LatestBlock) k.decryptionTriggerChannel = make(chan *broker.Event[*epochkghandler.DecryptionTrigger]) @@ -91,27 +87,13 @@ func (k *Keyper) Start(ctx context.Context, runner service.Runner) error { return err } - eonKeyPublisherClient, err := ethclient.DialContext(ctx, k.config.Chain.Node.EthereumURL) - if err != nil { - return errors.Wrapf(err, "failed to dial ethereum node at %s", k.config.Chain.Node.EthereumURL) - } - k.eonKeyPublisher, err = eonkeypublisher.NewEonKeyPublisher( - k.dbpool, - eonKeyPublisherClient, - k.config.Chain.Contracts.KeyperSetManager, - k.config.Chain.Node.PrivateKey.Key, - ) - if err != nil { - return errors.Wrap(err, "failed to initialize eon key publisher") - } - err = k.initRegistrySyncer(ctx) if err != nil { return err } runner.Go(func() error { return k.processInputs(ctx) }) - return runner.StartService(k.core, k.chainSyncClient, k.eonKeyPublisher) + return runner.StartService(k.core, k.chainSyncClient) } func NewKeyper(kpr *Keyper, messagingMiddleware p2p.Messaging) (*keyper.KeyperCore, error) { @@ -124,14 +106,11 @@ func NewKeyper(kpr *Keyper, messagingMiddleware p2p.Messaging) (*keyper.KeyperCo HTTPListenAddress: kpr.config.HTTPListenAddress, P2P: kpr.config.P2P, Ethereum: kpr.config.Chain.Node, - Shuttermint: kpr.config.Shuttermint, Metrics: kpr.config.Metrics, MaxNumKeysPerMessage: kpr.config.MaxNumKeysPerMessage, }, kpr.decryptionTriggerChannel, keyper.WithDBPool(kpr.dbpool), - keyper.NoBroadcastEonPublicKey(), - keyper.WithEonPublicKeyHandler(kpr.channelNewEonPublicKey), keyper.WithMessaging(messagingMiddleware), ) } @@ -177,26 +156,15 @@ func (k *Keyper) processInputs(ctx context.Context) error { err = k.processNewBlock(ctx, ev) case ev := <-k.newKeyperSets: err = k.processNewKeyperSet(ctx, ev) - case ev := <-k.newEonPublicKeys: - err = k.processNewEonPublicKey(ctx, ev) case <-ctx.Done(): return ctx.Err() } if err != nil { - // TODO: Check if it's safe to drop those events. If not, we should store the - // ones that remain on the channel in the db and process them when we restart. - // TODO: also, should we stop the keyper or just log the error and continue? - // return err log.Error().Err(err).Msg("error processing event") } } } -func (k *Keyper) channelNewEonPublicKey(_ context.Context, key keyper.EonPublicKey) error { - k.newEonPublicKeys <- key - return nil -} - func (k *Keyper) channelNewKeyperSet(_ context.Context, ev *syncevent.KeyperSet) error { k.newKeyperSets <- ev return nil diff --git a/rolling-shutter/keyperimpl/primev/neweonpublickey.go b/rolling-shutter/keyperimpl/primev/neweonpublickey.go deleted file mode 100644 index 4dc145e9c..000000000 --- a/rolling-shutter/keyperimpl/primev/neweonpublickey.go +++ /dev/null @@ -1,12 +0,0 @@ -package primev - -import ( - "context" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" -) - -func (k *Keyper) processNewEonPublicKey(_ context.Context, key keyper.EonPublicKey) error { //nolint: unparam - k.eonKeyPublisher.Publish(key) - return nil -} diff --git a/rolling-shutter/keyperimpl/shutterservice/config.go b/rolling-shutter/keyperimpl/shutterservice/config.go index 3c6f4236d..64b150377 100644 --- a/rolling-shutter/keyperimpl/shutterservice/config.go +++ b/rolling-shutter/keyperimpl/shutterservice/config.go @@ -1,12 +1,13 @@ package shutterservice import ( + "crypto/rand" "io" "github.com/ethereum/go-ethereum/common" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/configuration" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/keys" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/metricsserver" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" ) @@ -25,9 +26,9 @@ func NewConfig() *Config { func (c *Config) Init() { c.P2P = p2p.NewConfig() - c.Shuttermint = kprconfig.NewShuttermintConfig() c.Metrics = metricsserver.NewConfig() c.Chain = NewChainConfig() + c.ECIESPrivateKey = &keys.ECDSAPrivate{} } type Config struct { @@ -38,10 +39,14 @@ type Config struct { HTTPReadOnly bool HTTPListenAddress string - Chain *ChainConfig - P2P *p2p.Config - Shuttermint *kprconfig.ShuttermintConfig - Metrics *metricsserver.MetricsConfig + Chain *ChainConfig + P2P *p2p.Config + Metrics *metricsserver.MetricsConfig + + // ECIESPrivateKey is the keyper's ECIES private key used to decrypt + // PolyEval blobs sent by other keypers during DKG. The corresponding + // public key is registered on-chain via the ECIES Key Registry. + ECIESPrivateKey *keys.ECDSAPrivate `shconfig:",required"` MaxNumKeysPerMessage uint64 } @@ -70,6 +75,11 @@ func (c *Config) SetExampleValues() error { } c.InstanceID = 42 c.DatabaseURL = "postgres://pguser:pgpassword@localhost:5432/shutter" + eciesKey, err := keys.GenerateECDSAKey(rand.Reader) + if err != nil { + return err + } + c.ECIESPrivateKey = eciesKey return nil } @@ -139,6 +149,7 @@ type ContractsConfig struct { ShutterRegistry common.Address `shconfig:",required"` ShutterEventTriggerRegistry common.Address KeyBroadcastContract common.Address `shconfig:",required"` + ECIESKeyRegistry common.Address `shconfig:",required"` } func NewContractsConfig() *ContractsConfig { @@ -146,6 +157,7 @@ func NewContractsConfig() *ContractsConfig { KeyperSetManager: common.Address{}, ShutterRegistry: common.Address{}, KeyBroadcastContract: common.Address{}, + ECIESKeyRegistry: common.Address{}, } } diff --git a/rolling-shutter/keyperimpl/shutterservice/database/sql/migrations/V2_event_based_triggers.sql b/rolling-shutter/keyperimpl/shutterservice/database/sql/migrations/V002_event_based_triggers.sql similarity index 100% rename from rolling-shutter/keyperimpl/shutterservice/database/sql/migrations/V2_event_based_triggers.sql rename to rolling-shutter/keyperimpl/shutterservice/database/sql/migrations/V002_event_based_triggers.sql diff --git a/rolling-shutter/keyperimpl/shutterservice/database/sql/migrations/V3_event_trigger_identity_key.sql b/rolling-shutter/keyperimpl/shutterservice/database/sql/migrations/V003_event_trigger_identity_key.sql similarity index 100% rename from rolling-shutter/keyperimpl/shutterservice/database/sql/migrations/V3_event_trigger_identity_key.sql rename to rolling-shutter/keyperimpl/shutterservice/database/sql/migrations/V003_event_trigger_identity_key.sql diff --git a/rolling-shutter/keyperimpl/shutterservice/keyper.go b/rolling-shutter/keyperimpl/shutterservice/keyper.go index 4d5960c41..ca507cf72 100644 --- a/rolling-shutter/keyperimpl/shutterservice/keyper.go +++ b/rolling-shutter/keyperimpl/shutterservice/keyper.go @@ -14,7 +14,7 @@ import ( triggerRegistryV1Bindings "github.com/shutter-network/contracts/v2/bindings/shuttereventtriggerregistryv1" registryBindings "github.com/shutter-network/contracts/v2/bindings/shutterregistry" - "github.com/shutter-network/rolling-shutter/rolling-shutter/eonkeypublisher" + "github.com/shutter-network/rolling-shutter/rolling-shutter/dkg" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/epochkghandler" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" @@ -25,6 +25,7 @@ import ( "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/db" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" + "github.com/shutter-network/rolling-shutter/rolling-shutter/txsender" ) var ErrParseKeyperSet = errors.New("cannot parse KeyperSet") @@ -36,15 +37,19 @@ type Keyper struct { chainSyncClient *chainsync.Client registrySyncer *RegistrySyncer - eonKeyPublisher *eonkeypublisher.EonKeyPublisher latestTriggeredTime *uint64 syncMonitor *SyncMonitor multiEventSyncer *MultiEventSyncer + txSender *txsender.TxSender + + // dkgMgr drives the per-block DKG participation loop. It owns no chain + // subscriptions and lives entirely off the database (see ADR 0004). + dkgMgr *dkg.Manager // input events - newBlocks chan *syncevent.LatestBlock - newKeyperSets chan *syncevent.KeyperSet - newEonPublicKeys chan keyper.EonPublicKey + newBlocks chan *syncevent.LatestBlock + newKeyperSets chan *syncevent.KeyperSet + newDKGEvents chan syncevent.DKGEvent // outputs decryptionTriggerChannel chan *broker.Event[*epochkghandler.DecryptionTrigger] @@ -61,7 +66,7 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { kpr.newBlocks = make(chan *syncevent.LatestBlock) kpr.newKeyperSets = make(chan *syncevent.KeyperSet) - kpr.newEonPublicKeys = make(chan keyper.EonPublicKey) + kpr.newDKGEvents = make(chan syncevent.DKGEvent) kpr.decryptionTriggerChannel = make(chan *broker.Event[*epochkghandler.DecryptionTrigger]) kpr.latestTriggeredTime = nil @@ -88,8 +93,11 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { chainsync.WithClientURL(kpr.config.Chain.Node.EthereumURL), chainsync.WithKeyperSetManager(kpr.config.Chain.Contracts.KeyperSetManager), chainsync.WithKeyBroadcastContract(kpr.config.Chain.Contracts.KeyBroadcastContract), + chainsync.WithECIESKeyRegistry(kpr.config.Chain.Contracts.ECIESKeyRegistry), chainsync.WithSyncNewBlock(kpr.channelNewBlock), chainsync.WithSyncNewKeyperSet(kpr.channelNewKeyperSet), + chainsync.WithSyncECIESKey(kpr.processNewECIESKey), + chainsync.WithSyncDKGEvent(kpr.channelNewDKGEvent), chainsync.WithPrivateKey(kpr.config.Chain.Node.PrivateKey.Key), chainsync.WithLogger(gethLog.NewLogger(slog.Default().Handler())), ) @@ -97,19 +105,12 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { return err } - eonKeyPublisherClient, err := ethclient.DialContext(ctx, kpr.config.Chain.Node.EthereumURL) - if err != nil { - return errors.Wrapf(err, "failed to dial ethereum node at %s", kpr.config.Chain.Node.EthereumURL) - } - kpr.eonKeyPublisher, err = eonkeypublisher.NewEonKeyPublisher( + kpr.dkgMgr = dkg.New(dkg.NewConfigFromECDSA( kpr.dbpool, - eonKeyPublisherClient, - kpr.config.Chain.Contracts.KeyperSetManager, - kpr.config.Chain.Node.PrivateKey.Key, - ) - if err != nil { - return errors.Wrap(err, "failed to initialize eon key publisher") - } + kpr.config.GetAddress(), + kpr.config.ECIESPrivateKey.Key, + kpr.config.Chain.Contracts.ECIESKeyRegistry, + )) err = kpr.initRegistrySyncer(ctx) if err != nil { @@ -127,8 +128,13 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { DBPool: kpr.dbpool, CheckInterval: time.Duration(kpr.config.Chain.SyncMonitorCheckInterval) * time.Second, } + kpr.txSender = txsender.New(txsender.Config{ + DBPool: kpr.dbpool, + Client: kpr.chainSyncClient, + PrivateKey: kpr.config.Chain.Node.PrivateKey.Key, + }) runner.Go(func() error { return kpr.processInputs(ctx) }) - return runner.StartService(kpr.core, kpr.chainSyncClient, kpr.eonKeyPublisher, kpr.syncMonitor) + return runner.StartService(kpr.core, kpr.chainSyncClient, kpr.syncMonitor, kpr.txSender) } func NewKeyper(kpr *Keyper, messagingMiddleware *MessagingMiddleware) (*keyper.KeyperCore, error) { @@ -141,14 +147,11 @@ func NewKeyper(kpr *Keyper, messagingMiddleware *MessagingMiddleware) (*keyper.K HTTPListenAddress: kpr.config.HTTPListenAddress, P2P: kpr.config.P2P, Ethereum: kpr.config.Chain.Node, - Shuttermint: kpr.config.Shuttermint, Metrics: kpr.config.Metrics, MaxNumKeysPerMessage: kpr.config.MaxNumKeysPerMessage, }, kpr.decryptionTriggerChannel, keyper.WithDBPool(kpr.dbpool), - keyper.NoBroadcastEonPublicKey(), - keyper.WithEonPublicKeyHandler(kpr.channelNewEonPublicKey), keyper.WithMessaging(messagingMiddleware), ) } @@ -259,8 +262,8 @@ func (kpr *Keyper) processInputs(ctx context.Context) error { err = kpr.processNewBlock(ctx, ev) case ev := <-kpr.newKeyperSets: err = kpr.processNewKeyperSet(ctx, ev) - case ev := <-kpr.newEonPublicKeys: - err = kpr.processNewEonPublicKey(ctx, ev) + case ev := <-kpr.newDKGEvents: + err = kpr.processNewDKGEvent(ctx, ev) case <-ctx.Done(): return ctx.Err() } @@ -274,27 +277,27 @@ func (kpr *Keyper) processInputs(ctx context.Context) error { } } -func (kpr *Keyper) channelNewEonPublicKey(ctx context.Context, key keyper.EonPublicKey) error { +func (kpr *Keyper) channelNewBlock(ctx context.Context, ev *syncevent.LatestBlock) error { select { - case kpr.newEonPublicKeys <- key: + case kpr.newBlocks <- ev: return nil case <-ctx.Done(): return ctx.Err() } } -func (kpr *Keyper) channelNewBlock(ctx context.Context, ev *syncevent.LatestBlock) error { +func (kpr *Keyper) channelNewKeyperSet(ctx context.Context, ev *syncevent.KeyperSet) error { select { - case kpr.newBlocks <- ev: + case kpr.newKeyperSets <- ev: return nil case <-ctx.Done(): return ctx.Err() } } -func (kpr *Keyper) channelNewKeyperSet(ctx context.Context, ev *syncevent.KeyperSet) error { +func (kpr *Keyper) channelNewDKGEvent(ctx context.Context, ev syncevent.DKGEvent) error { select { - case kpr.newKeyperSets <- ev: + case kpr.newDKGEvents <- ev: return nil case <-ctx.Done(): return ctx.Err() diff --git a/rolling-shutter/keyperimpl/shutterservice/newblock.go b/rolling-shutter/keyperimpl/shutterservice/newblock.go index 8b8dfa460..9b5ad0bee 100644 --- a/rolling-shutter/keyperimpl/shutterservice/newblock.go +++ b/rolling-shutter/keyperimpl/shutterservice/newblock.go @@ -31,6 +31,9 @@ func (kpr *Keyper) processNewBlock(ctx context.Context, ev *syncevent.LatestBloc return err } } + if err := kpr.dkgMgr.HandleBlock(ctx, ev.Header.Number.Uint64()); err != nil { + return err + } return kpr.maybeTriggerDecryption(ctx, ev) } diff --git a/rolling-shutter/keyperimpl/shutterservice/newblock_test.go b/rolling-shutter/keyperimpl/shutterservice/newblock_test.go index 14fbd040d..7d740f3ad 100644 --- a/rolling-shutter/keyperimpl/shutterservice/newblock_test.go +++ b/rolling-shutter/keyperimpl/shutterservice/newblock_test.go @@ -15,6 +15,7 @@ import ( pubsub "github.com/libp2p/go-libp2p-pubsub" "gotest.tools/assert" + obskeyperdatabase "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" corekeyperdatabase "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/epochkghandler" servicedatabase "github.com/shutter-network/rolling-shutter/rolling-shutter/keyperimpl/shutterservice/database" @@ -79,7 +80,7 @@ func TestProcessBlockSuccess(t *testing.T) { identity := identityPrefix identity = append(identity, sender.Bytes()...) - insertBatchConfig(ctx, t, coreKeyperDB, []string{kpr.config.GetAddress().Hex()}, int64(activationBlockNumber)) + insertBatchConfig(ctx, t, dbpool, []string{kpr.config.GetAddress().Hex()}, int64(activationBlockNumber)) insertEon(ctx, t, coreKeyperDB, eonInt64, int64(activationBlockNumber)) insertDKGResult(ctx, t, coreKeyperDB, eonInt64, true) @@ -164,7 +165,7 @@ func TestShouldTriggerDecryption(t *testing.T) { t.Fatalf("Eon is too large: %d", eon) } - insertBatchConfig(ctx, t, coreKeyperDB, []string{kpr.config.GetAddress().Hex()}, int64(activationBlockNumber)) + insertBatchConfig(ctx, t, dbpool, []string{kpr.config.GetAddress().Hex()}, int64(activationBlockNumber)) insertEon(ctx, t, coreKeyperDB, int64(eon), int64(activationBlockNumber)) insertDKGResult(ctx, t, coreKeyperDB, int64(eon), true) @@ -283,7 +284,7 @@ func TestShouldTriggerDecryptionDifferentEon(t *testing.T) { decryptionTriggerChannel: decryptionTriggerChannel, } - insertBatchConfig(ctx, t, coreKeyperDB, []string{kpr.config.GetAddress().Hex()}, int64(activationBlockNumber)) + insertBatchConfig(ctx, t, dbpool, []string{kpr.config.GetAddress().Hex()}, int64(activationBlockNumber)) insertEon(ctx, t, coreKeyperDB, earlierEon, int64(activationBlockNumber)) insertDKGResult(ctx, t, coreKeyperDB, earlierEon, true) insertEon(ctx, t, coreKeyperDB, laterEon, int64(laterActivationBlockNumber)) @@ -361,7 +362,7 @@ func TestShouldNotTriggerDecryptionBeforeActivation(t *testing.T) { t.Fatalf("Eon is too large: %d", eon) } - insertBatchConfig(ctx, t, coreKeyperDB, []string{kpr.config.GetAddress().Hex()}, int64(activationBlockNumber)) + insertBatchConfig(ctx, t, dbpool, []string{kpr.config.GetAddress().Hex()}, int64(activationBlockNumber)) insertEon(ctx, t, coreKeyperDB, int64(eon), int64(activationBlockNumber)) insertDKGResult(ctx, t, coreKeyperDB, int64(eon), true) @@ -423,7 +424,7 @@ func TestShouldNotTriggerDecryptionWithoutSuccessfulDKG(t *testing.T) { t.Fatalf("blockTimestamp is negative: %d", blockTimestamp) } - insertBatchConfig(ctx, t, coreKeyperDB, []string{kpr.config.GetAddress().Hex()}, activationBlockNumber) + insertBatchConfig(ctx, t, dbpool, []string{kpr.config.GetAddress().Hex()}, activationBlockNumber) insertEon(ctx, t, coreKeyperDB, eonInt64, activationBlockNumber) insertDKGResult(ctx, t, coreKeyperDB, eonInt64, false) @@ -567,14 +568,14 @@ func TestFiredTriggersProducesOrderedShares(t *testing.T) { func insertBatchConfig( ctx context.Context, t *testing.T, - coreKeyperDB *corekeyperdatabase.Queries, + dbpool *pgxpool.Pool, keypers []string, activationBlockNumber int64, ) { t.Helper() - - err := coreKeyperDB.InsertBatchConfig(ctx, corekeyperdatabase.InsertBatchConfigParams{ - KeyperConfigIndex: testKeyperConfigIndex32, + obsKeyperDB := obskeyperdatabase.New(dbpool) + err := obsKeyperDB.InsertKeyperSet(ctx, obskeyperdatabase.InsertKeyperSetParams{ + KeyperConfigIndex: testKeyperConfigIndex, Keypers: keypers, Threshold: 1, ActivationBlockNumber: activationBlockNumber, @@ -593,9 +594,9 @@ func insertEon( err := coreKeyperDB.InsertEon(ctx, corekeyperdatabase.InsertEonParams{ Eon: eon, - Height: 0, ActivationBlockNumber: activationBlockNumber, KeyperConfigIndex: testKeyperConfigIndex, + MaxRetries: 10, }) assert.NilError(t, err) } diff --git a/rolling-shutter/keyperimpl/shutterservice/newdkgevent.go b/rolling-shutter/keyperimpl/shutterservice/newdkgevent.go new file mode 100644 index 000000000..d6ea1dfbc --- /dev/null +++ b/rolling-shutter/keyperimpl/shutterservice/newdkgevent.go @@ -0,0 +1,202 @@ +package shutterservice + +import ( + "context" + + "github.com/jackc/pgx/v4" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + "github.com/shutter-network/rolling-shutter/rolling-shutter/dkg" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" + syncevent "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" +) + +// processNewDKGEvent stores a DKG Contract event in the appropriate per-type +// table and writes a `dkg_result` row when the event is a success notification. +// Events for a `keyper_set_index` whose DKG already succeeded locally are +// silently ignored so re-emitted or retried-and-superseded messages do not +// pollute the message tables. +func (kpr *Keyper) processNewDKGEvent(ctx context.Context, ev syncevent.DKGEvent) error { + keyperSetIndex, retryCounter := dkgEventKeys(ev) + keyperSetIndexInt, err := medley.Uint64ToInt64Safe(keyperSetIndex) + if err != nil { + return errors.Wrap(err, "convert keyper set index") + } + retryCounterInt, err := medley.Uint64ToInt64Safe(retryCounter) + if err != nil { + return errors.Wrap(err, "convert retry counter") + } + + return kpr.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + queries := corekeyperdb.New(tx) + obsQueries := obskeyper.New(tx) + + exists, err := queries.ExistsDKGResultSuccess(ctx, keyperSetIndexInt) + if err != nil { + return errors.Wrap(err, "check existing dkg_result success") + } + if _, isSuccess := ev.(*syncevent.SuccessEvent); exists && !isSuccess { + log.Debug(). + Uint64("keyper-set-index", keyperSetIndex). + Uint64("retry-counter", retryCounter). + Msg("ignoring DKG event for already-succeeded keyper set") + return nil + } + + switch e := ev.(type) { + case *syncevent.DealingEvent: + return storeDealing(ctx, queries, obsQueries, e, keyperSetIndexInt, retryCounterInt) + case *syncevent.AccusationEvent: + return storeAccusation(ctx, queries, e, keyperSetIndexInt, retryCounterInt) + case *syncevent.ApologyEvent: + return storeApology(ctx, queries, e, keyperSetIndexInt, retryCounterInt) + case *syncevent.SuccessVoteEvent: + log.Debug(). + Uint64("keyper-set-index", e.KeyperSetIndex). + Uint64("retry-counter", e.RetryCounter). + Uint64("voter", e.KeyperIndex). + Msg("observed DKG success vote") + return nil + case *syncevent.SuccessEvent: + return kpr.dkgMgr.HandleDKGSuccess(ctx, tx, keyperSetIndexInt, retryCounterInt) + default: + return errors.Errorf("unknown DKG event type %T", ev) + } + }) +} + +// dkgEventKeys extracts the (keyperSetIndex, retryCounter) pair common to every +// DKG event variant. SuccessEvent has no retry counter and reports zero. +func dkgEventKeys(ev syncevent.DKGEvent) (keyperSetIndex, retryCounter uint64) { + switch e := ev.(type) { + case *syncevent.DealingEvent: + return e.KeyperSetIndex, e.RetryCounter + case *syncevent.AccusationEvent: + return e.KeyperSetIndex, e.RetryCounter + case *syncevent.ApologyEvent: + return e.KeyperSetIndex, e.RetryCounter + case *syncevent.SuccessVoteEvent: + return e.KeyperSetIndex, e.RetryCounter + case *syncevent.SuccessEvent: + return e.KeyperSetIndex, e.RetryCounter + } + return 0, 0 +} + +func storeDealing( + ctx context.Context, + queries *corekeyperdb.Queries, + obsQueries *obskeyper.Queries, + ev *syncevent.DealingEvent, + keyperSetIndex, retryCounter int64, +) error { + senderIndex, err := medley.Uint64ToInt64Safe(ev.KeyperIndex) + if err != nil { + return errors.Wrap(err, "convert sender index") + } + + if err := queries.InsertDKGPolyCommitment(ctx, corekeyperdb.InsertDKGPolyCommitmentParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + KeyperIndex: senderIndex, + Commitment: ev.Commitment, + }); err != nil { + return errors.Wrap(err, "insert dkg_poly_commitment") + } + + evals := ev.PolyEvals + + keyperSet, err := obsQueries.GetKeyperSetByKeyperConfigIndex(ctx, keyperSetIndex) + if err != nil { + return errors.Wrapf(err, "load keyper set %d for polyEval split", keyperSetIndex) + } + n := uint64(len(keyperSet.Keypers)) + receivers := dkg.ReceiverIndicesForSender(n, ev.KeyperIndex) + if uint64(len(evals)) != uint64(len(receivers)) { + return errors.Errorf( + "polyEval count %d does not match expected receiver count %d for keyper set %d", + len(evals), len(receivers), keyperSetIndex, + ) + } + + for i, encryptedEval := range evals { + receiverIndex, err := medley.Uint64ToInt64Safe(receivers[i]) + if err != nil { + return errors.Wrap(err, "convert receiver index") + } + if err := queries.InsertDKGPolyEval(ctx, corekeyperdb.InsertDKGPolyEvalParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + SenderIndex: senderIndex, + ReceiverIndex: receiverIndex, + EncryptedEval: encryptedEval, + }); err != nil { + return errors.Wrap(err, "insert dkg_poly_eval") + } + } + return nil +} + +func storeAccusation( + ctx context.Context, + queries *corekeyperdb.Queries, + ev *syncevent.AccusationEvent, + keyperSetIndex, retryCounter int64, +) error { + accuserIndex, err := medley.Uint64ToInt64Safe(ev.KeyperIndex) + if err != nil { + return errors.Wrap(err, "convert accuser index") + } + for _, accused := range ev.AccusedIndices { + accusedIndex, err := medley.Uint64ToInt64Safe(accused) + if err != nil { + return errors.Wrap(err, "convert accused index") + } + if err := queries.InsertDKGAccusation(ctx, corekeyperdb.InsertDKGAccusationParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + AccuserIndex: accuserIndex, + AccusedIndex: accusedIndex, + }); err != nil { + return errors.Wrap(err, "insert dkg_accusation") + } + } + return nil +} + +func storeApology( + ctx context.Context, + queries *corekeyperdb.Queries, + ev *syncevent.ApologyEvent, + keyperSetIndex, retryCounter int64, +) error { + if len(ev.AccuserIndices) != len(ev.PolyEvalData) { + return errors.Errorf( + "apology accuser count %d does not match polyEval data count %d", + len(ev.AccuserIndices), len(ev.PolyEvalData), + ) + } + apologizerIndex, err := medley.Uint64ToInt64Safe(ev.KeyperIndex) + if err != nil { + return errors.Wrap(err, "convert apologizer index") + } + for i, accuser := range ev.AccuserIndices { + accuserIndex, err := medley.Uint64ToInt64Safe(accuser) + if err != nil { + return errors.Wrap(err, "convert accuser index") + } + if err := queries.InsertDKGApology(ctx, corekeyperdb.InsertDKGApologyParams{ + KeyperSetIndex: keyperSetIndex, + RetryCounter: retryCounter, + ApologizerIndex: apologizerIndex, + AccuserIndex: accuserIndex, + PolyEval: ev.PolyEvalData[i], + }); err != nil { + return errors.Wrap(err, "insert dkg_apology") + } + } + return nil +} diff --git a/rolling-shutter/keyperimpl/shutterservice/newecieskey.go b/rolling-shutter/keyperimpl/shutterservice/newecieskey.go new file mode 100644 index 000000000..ee5b803bf --- /dev/null +++ b/rolling-shutter/keyperimpl/shutterservice/newecieskey.go @@ -0,0 +1,25 @@ +package shutterservice + +import ( + "context" + + "github.com/rs/zerolog/log" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + syncevent "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" + "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" +) + +// processNewECIESKey upserts the keyper's ECIES public key into the local +// `ecies_keys` cache. It is invoked for both the synthetic events emitted by +// the initial poll and live `KeyRegistered` events from the registry contract. +func (kpr *Keyper) processNewECIESKey(ctx context.Context, ev *syncevent.ECIESKey) error { + log.Debug(). + Str("keyper", ev.Keyper.Hex()). + Int("key-len", len(ev.EciesPublicKey)). + Msg("storing ECIES public key") + return corekeyperdb.New(kpr.dbpool).UpsertECIESKey(ctx, corekeyperdb.UpsertECIESKeyParams{ + KeyperAddress: shdb.EncodeAddress(ev.Keyper), + EciesPublicKey: ev.EciesPublicKey, + }) +} diff --git a/rolling-shutter/keyperimpl/shutterservice/neweonpublickey.go b/rolling-shutter/keyperimpl/shutterservice/neweonpublickey.go deleted file mode 100644 index 87f6c9b22..000000000 --- a/rolling-shutter/keyperimpl/shutterservice/neweonpublickey.go +++ /dev/null @@ -1,12 +0,0 @@ -package shutterservice - -import ( - "context" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper" -) - -func (kpr *Keyper) processNewEonPublicKey(_ context.Context, key keyper.EonPublicKey) error { //nolint: unparam - kpr.eonKeyPublisher.Publish(key) - return nil -} diff --git a/rolling-shutter/keyperimpl/shutterservice/newkeyperset.go b/rolling-shutter/keyperimpl/shutterservice/newkeyperset.go index 1268767a4..00811d6ba 100644 --- a/rolling-shutter/keyperimpl/shutterservice/newkeyperset.go +++ b/rolling-shutter/keyperimpl/shutterservice/newkeyperset.go @@ -2,21 +2,28 @@ package shutterservice import ( "context" + "database/sql" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" "github.com/jackc/pgx/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + keypersetBindings "github.com/shutter-network/contracts/v2/bindings/keyperset" obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" syncevent "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" ) func (kpr *Keyper) processNewKeyperSet(ctx context.Context, ev *syncevent.KeyperSet) error { + ownAddress := kpr.config.GetAddress() isMember := false for _, m := range ev.Members { - if m.Cmp(kpr.config.GetAddress()) == 0 { + if m.Cmp(ownAddress) == 0 { isMember = true break } @@ -30,13 +37,32 @@ func (kpr *Keyper) processNewKeyperSet(ctx context.Context, ev *syncevent.Keyper Bool("is-member", isMember). Msg("new keyper set added") - return kpr.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { - obskeyperdb := obskeyper.New(tx) + keyperSetIndex, err := medley.Uint64ToInt64Safe(ev.Eon) + if err != nil { + return errors.Wrap(err, ErrParseKeyperSet.Error()) + } - keyperConfigIndex, err := medley.Uint64ToInt64Safe(ev.Eon) + // Fetch DKG phase params before opening the DB transaction: the RPC calls + // can be slow and we do not want to hold row locks across them. On event + // replay this is a wasted round-trip because the existing-row check inside + // the transaction will short-circuit, but replays are rare. + var ( + dkgContract sql.NullString + phaseLength sql.NullInt64 + leadLength sql.NullInt64 + maxRetries int64 + ) + if isMember { + dkgContract, phaseLength, leadLength, maxRetries, err = kpr.fetchDKGParamsForKeyperSet(ctx, ev.Contract) if err != nil { - return errors.Wrap(err, ErrParseKeyperSet.Error()) + return errors.Wrap(err, "fetch DKG params for new keyper set") } + } + + if err := kpr.dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + obskeyperdb := obskeyper.New(tx) + coredb := corekeyperdb.New(tx) + activationBlockNumber, err := medley.Uint64ToInt64Safe(ev.ActivationBlock) if err != nil { return errors.Wrap(err, ErrParseKeyperSet.Error()) @@ -46,11 +72,111 @@ func (kpr *Keyper) processNewKeyperSet(ctx context.Context, ev *syncevent.Keyper return errors.Wrap(err, ErrParseKeyperSet.Error()) } - return obskeyperdb.InsertKeyperSet(ctx, obskeyper.InsertKeyperSetParams{ - KeyperConfigIndex: keyperConfigIndex, + if err := obskeyperdb.InsertKeyperSet(ctx, obskeyper.InsertKeyperSetParams{ + KeyperConfigIndex: keyperSetIndex, ActivationBlockNumber: activationBlockNumber, Keypers: shdb.EncodeAddresses(ev.Members), Threshold: int32(threshold), - }) - }) + }); err != nil { + return err + } + + if isMember { + // Eagerly insert an eons row so the DKG participation loop has + // somewhere to anchor when the activation block approaches. + // Existing rows are tolerated because the chainsync initial poll + // can re-deliver KeyperSetAdded events that were already processed. + if _, err := coredb.GetEon(ctx, keyperSetIndex); err == nil { + return nil + } else if !errors.Is(err, pgx.ErrNoRows) { + return errors.Wrap(err, "check existing eon row") + } + if err := coredb.InsertEon(ctx, corekeyperdb.InsertEonParams{ + Eon: keyperSetIndex, + ActivationBlockNumber: activationBlockNumber, + KeyperConfigIndex: keyperSetIndex, + DkgContract: dkgContract, + PhaseLength: phaseLength, + LeadLength: leadLength, + MaxRetries: maxRetries, + }); err != nil { + return errors.Wrap(err, "insert eon row for new keyper set") + } + } + return nil + }); err != nil { + return err + } + + // Must run after the transaction commits: MaybeRegisterECIESKey reads + // the keyper set row inserted above. + // + // This can still land too late. If DKG starts immediately (the + // KeyperSetAdded tx was mined inside the lead-time window), peers + // dispatch dealings before our ECIES registration is on chain and + // encrypt without our key. Returning keypers already have a key from + // a previous set, so this only bites new members. Mitigation: give + // keyper sets enough lead time. + return kpr.dkgMgr.MaybeRegisterECIESKey(ctx, keyperSetIndex) +} + +// fetchDKGParamsForKeyperSet asks the keyper set contract for its DKG contract +// address and then reads the immutable phase parameters from that DKG contract. +// Any failure (zero address, RPC error, missing methods on an old contract) is +// returned as an error. The DKG manager treats NULL phase params in the eons +// row as a fatal configuration error and has no chain-client fallback, so we +// must not insert the eon row without these values; propagating the error lets +// the outer event loop log and retry on the next chainsync re-delivery. +func (kpr *Keyper) fetchDKGParamsForKeyperSet( + ctx context.Context, + keyperSetAddr common.Address, +) (sql.NullString, sql.NullInt64, sql.NullInt64, int64, error) { + var ( + nullStr sql.NullString + nullInt sql.NullInt64 + ) + if (keyperSetAddr == common.Address{}) { + return nullStr, nullInt, nullInt, 0, errors.New("keyper set event missing contract address") + } + ks, err := keypersetBindings.NewKeyperset(keyperSetAddr, kpr.chainSyncClient.Client) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "bind keyper set contract %s", keyperSetAddr.Hex()) + } + callOpts := &bind.CallOpts{Context: ctx} + dkgAddr, err := ks.GetDKGContract(callOpts) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "call getDKGContract on keyper set %s", keyperSetAddr.Hex()) + } + if (dkgAddr == common.Address{}) { + return nullStr, nullInt, nullInt, 0, errors.Errorf("keyper set %s has no DKG contract configured", keyperSetAddr.Hex()) + } + dkg, err := dkgcontract.NewDkgcontract(dkgAddr, kpr.chainSyncClient.Client) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "bind DKG contract %s", dkgAddr.Hex()) + } + phaseLength, err := dkg.PHASELENGTH(callOpts) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "read PHASE_LENGTH from %s", dkgAddr.Hex()) + } + leadLength, err := dkg.DKGLEADLENGTH(callOpts) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "read DKG_LEAD_LENGTH from %s", dkgAddr.Hex()) + } + maxRetries, err := dkg.MAXRETRIES(callOpts) + if err != nil { + return nullStr, nullInt, nullInt, 0, errors.Wrapf(err, "read MAX_RETRIES from %s", dkgAddr.Hex()) + } + log.Info(). + Str("keyper-set", keyperSetAddr.Hex()). + Str("dkg-contract", dkgAddr.Hex()). + Uint64("phase-length", phaseLength). + Uint64("lead-length", leadLength). + Uint64("max-retries", maxRetries). + Msg("resolved per-keyper-set DKG contract params") + //nolint:gosec // G115: phase length, lead length, and max retries come from the on-chain contract and fit well within int64 + return sql.NullString{String: dkgAddr.Hex(), Valid: true}, + sql.NullInt64{Int64: int64(phaseLength), Valid: true}, + sql.NullInt64{Int64: int64(leadLength), Valid: true}, + int64(maxRetries), + nil } diff --git a/rolling-shutter/keyperimpl/snapshot/config.go b/rolling-shutter/keyperimpl/snapshot/config.go index ac395c3d3..9da022ade 100644 --- a/rolling-shutter/keyperimpl/snapshot/config.go +++ b/rolling-shutter/keyperimpl/snapshot/config.go @@ -5,7 +5,6 @@ import ( "github.com/ethereum/go-ethereum/common" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/kprconfig" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/configuration" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/metricsserver" "github.com/shutter-network/rolling-shutter/rolling-shutter/p2p" @@ -22,7 +21,6 @@ func NewConfig() *Config { func (c *Config) Init() { c.P2P = p2p.NewConfig() c.Ethereum = configuration.NewEthnodeConfig() - c.Shuttermint = kprconfig.NewShuttermintConfig() c.Metrics = metricsserver.NewConfig() } @@ -33,10 +31,9 @@ type Config struct { HTTPEnabled bool HTTPListenAddress string - P2P *p2p.Config - Ethereum *configuration.EthnodeConfig - Shuttermint *kprconfig.ShuttermintConfig - Metrics *metricsserver.MetricsConfig + P2P *p2p.Config + Ethereum *configuration.EthnodeConfig + Metrics *metricsserver.MetricsConfig MaxNumKeysPerMessage uint64 } diff --git a/rolling-shutter/keyperimpl/snapshot/keyper.go b/rolling-shutter/keyperimpl/snapshot/keyper.go index 1791b94a7..9f60fbbdf 100644 --- a/rolling-shutter/keyperimpl/snapshot/keyper.go +++ b/rolling-shutter/keyperimpl/snapshot/keyper.go @@ -69,7 +69,6 @@ func (kpr *Keyper) Start(ctx context.Context, runner service.Runner) error { HTTPListenAddress: kpr.config.HTTPListenAddress, P2P: kpr.config.P2P, Ethereum: kpr.config.Ethereum, - Shuttermint: kpr.config.Shuttermint, Metrics: kpr.config.Metrics, MaxNumKeysPerMessage: kpr.config.MaxNumKeysPerMessage, }, diff --git a/rolling-shutter/medley/chainsync/client.go b/rolling-shutter/medley/chainsync/client.go index 2f2ea577b..83cde7ecc 100644 --- a/rolling-shutter/medley/chainsync/client.go +++ b/rolling-shutter/medley/chainsync/client.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" "github.com/pkg/errors" + "github.com/shutter-network/contracts/v2/bindings/ecieskeyregistry" "github.com/shutter-network/shop-contracts/bindings" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/client" @@ -34,11 +35,14 @@ type Client struct { KeyperSetManager *bindings.KeyperSetManager KeyBroadcast *bindings.KeyBroadcastContract - - sssync *syncer.ShutterStateSyncer - kssync *syncer.KeyperSetSyncer - uhsync *syncer.UnsafeHeadSyncer - epksync *syncer.EonPubKeySyncer + ECIESKeyRegistry *ecieskeyregistry.Ecieskeyregistry + + sssync *syncer.ShutterStateSyncer + kssync *syncer.KeyperSetSyncer + uhsync *syncer.UnsafeHeadSyncer + epksync *syncer.EonPubKeySyncer + eciessync *syncer.ECIESKeySyncer + dkgsync *syncer.DKGSyncer services []service.Service } diff --git a/rolling-shutter/medley/chainsync/event/events.go b/rolling-shutter/medley/chainsync/event/events.go index 9f7c3b014..2c4ed2f8e 100644 --- a/rolling-shutter/medley/chainsync/event/events.go +++ b/rolling-shutter/medley/chainsync/event/events.go @@ -13,6 +13,10 @@ type ( Members []common.Address Threshold uint64 Eon uint64 + // Contract is the deployed `KeyperSet` contract address for this eon. + // Needed for callers that read per-keyper-set state (e.g. + // `getDKGContract`) that is not part of `KeyperSetManager`. + Contract common.Address AtBlockNumber *number.BlockNumber `json:",omitempty"` } @@ -32,4 +36,68 @@ type ( BlockHash common.Hash Header *types.Header } + ECIESKey struct { + Keyper common.Address + EciesPublicKey []byte + + AtBlockNumber *number.BlockNumber `json:",omitempty"` + } ) + +// DKGEvent is any event observed from the DKG Contract (live or synthesized +// at startup). Consumers type-switch on the concrete event types below. +type DKGEvent interface { + isDKGEvent() +} + +type DealingEvent struct { + KeyperSetIndex uint64 + RetryCounter uint64 + KeyperIndex uint64 + Commitment []byte + PolyEvals [][]byte + + AtBlockNumber *number.BlockNumber `json:",omitempty"` +} + +type AccusationEvent struct { + KeyperSetIndex uint64 + RetryCounter uint64 + KeyperIndex uint64 + AccusedIndices []uint64 + + AtBlockNumber *number.BlockNumber `json:",omitempty"` +} + +type ApologyEvent struct { + KeyperSetIndex uint64 + RetryCounter uint64 + KeyperIndex uint64 + AccuserIndices []uint64 + PolyEvalData [][]byte + + AtBlockNumber *number.BlockNumber `json:",omitempty"` +} + +type SuccessVoteEvent struct { + KeyperSetIndex uint64 + RetryCounter uint64 + KeyperIndex uint64 + EonPublicKey []byte + + AtBlockNumber *number.BlockNumber `json:",omitempty"` +} + +type SuccessEvent struct { + KeyperSetIndex uint64 + RetryCounter uint64 + EonPublicKey []byte + + AtBlockNumber *number.BlockNumber `json:",omitempty"` +} + +func (*DealingEvent) isDKGEvent() {} +func (*AccusationEvent) isDKGEvent() {} +func (*ApologyEvent) isDKGEvent() {} +func (*SuccessVoteEvent) isDKGEvent() {} +func (*SuccessEvent) isDKGEvent() {} diff --git a/rolling-shutter/medley/chainsync/event/handler.go b/rolling-shutter/medley/chainsync/event/handler.go index 60f5758da..245d5ddca 100644 --- a/rolling-shutter/medley/chainsync/event/handler.go +++ b/rolling-shutter/medley/chainsync/event/handler.go @@ -7,4 +7,6 @@ type ( EonPublicKeyHandler func(context.Context, *EonPublicKey) error BlockHandler func(context.Context, *LatestBlock) error ShutterStateHandler func(context.Context, *ShutterState) error + ECIESKeyHandler func(context.Context, *ECIESKey) error + DKGEventHandler func(context.Context, DKGEvent) error ) diff --git a/rolling-shutter/medley/chainsync/options.go b/rolling-shutter/medley/chainsync/options.go index 580876243..589fcca98 100644 --- a/rolling-shutter/medley/chainsync/options.go +++ b/rolling-shutter/medley/chainsync/options.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/pkg/errors" + "github.com/shutter-network/contracts/v2/bindings/ecieskeyregistry" "github.com/shutter-network/shop-contracts/bindings" "github.com/shutter-network/shop-contracts/predeploy" @@ -23,6 +24,7 @@ type Option func(*options) error type options struct { keyperSetManagerAddress *common.Address keyBroadcastContractAddress *common.Address + eciesKeyRegistryAddress *common.Address clientURL string client syncclient.Client logger log.Logger @@ -34,6 +36,8 @@ type options struct { handlerKeyperSet event.KeyperSetHandler handlerEonPublicKey event.EonPublicKeyHandler handlerBlock event.BlockHandler + handlerECIESKey event.ECIESKeyHandler + handlerDKGEvent event.DKGEventHandler } func (o *options) verify() error { @@ -132,6 +136,36 @@ func (o *options) apply(ctx context.Context, c *Client) error { if o.handlerBlock != nil { c.services = append(c.services, c.uhsync) } + + if o.handlerECIESKey != nil { + if o.eciesKeyRegistryAddress == nil { + return errors.New("ECIES key registry address must be set when an ECIES key handler is registered") + } + c.ECIESKeyRegistry, err = ecieskeyregistry.NewEcieskeyregistry(*o.eciesKeyRegistryAddress, client) + if err != nil { + return err + } + c.eciessync = &syncer.ECIESKeySyncer{ + Client: client, + Contract: c.ECIESKeyRegistry, + Log: c.log, + StartBlock: o.syncStart, + Handler: o.handlerECIESKey, + } + c.services = append(c.services, c.eciessync) + } + + if o.handlerDKGEvent != nil { + c.dkgsync = &syncer.DKGSyncer{ + Client: client, + KeyperSetManager: c.KeyperSetManager, + Log: c.log, + StartBlock: o.syncStart, + Handler: o.handlerDKGEvent, + } + c.services = append(c.services, c.dkgsync) + } + c.privKey = o.privKey return nil } @@ -234,3 +268,24 @@ func WithSyncNewShutterState(handler event.ShutterStateHandler) Option { return nil } } + +func WithECIESKeyRegistry(address common.Address) Option { + return func(o *options) error { + o.eciesKeyRegistryAddress = &address + return nil + } +} + +func WithSyncECIESKey(handler event.ECIESKeyHandler) Option { + return func(o *options) error { + o.handlerECIESKey = handler + return nil + } +} + +func WithSyncDKGEvent(handler event.DKGEventHandler) Option { + return func(o *options) error { + o.handlerDKGEvent = handler + return nil + } +} diff --git a/rolling-shutter/medley/chainsync/syncer/dkg.go b/rolling-shutter/medley/chainsync/syncer/dkg.go new file mode 100644 index 000000000..7b5d09853 --- /dev/null +++ b/rolling-shutter/medley/chainsync/syncer/dkg.go @@ -0,0 +1,579 @@ +package syncer + +import ( + "context" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethevent "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/pkg/errors" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + keypersetBindings "github.com/shutter-network/contracts/v2/bindings/keyperset" + "github.com/shutter-network/shop-contracts/bindings" + + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/client" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/number" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" +) + +// keyperSetIndexer is the subset of *bindings.KeyperSetManager used by the +// DKG contract discovery scan. Pulling it out as an interface lets tests +// substitute a fake manager without spinning up a simulated chain. +type keyperSetIndexer interface { + GetNumKeyperSets(opts *bind.CallOpts) (uint64, error) + GetKeyperSetAddress(opts *bind.CallOpts, index uint64) (common.Address, error) +} + +// dkgContractResolver resolves a KeyperSet contract address to the DKG contract +// address it points at (via KeyperSet.getDKGContract()). +type dkgContractResolver func(ctx context.Context, opts *bind.CallOpts, keyperSetAddr common.Address) (common.Address, error) + +// dkgContractBackend is the subset of *dkgcontract.Dkgcontract used by the +// per-contract subscription goroutine: the Succeeded(ksi) / SucceededAtRetry(ksi) +// reads for initial success synthesis and the five bulletin-board event +// subscriptions. Pulling it out as an interface lets tests substitute a fake +// backend that emits canned events without touching a simulated chain. +type dkgContractBackend interface { + Succeeded(opts *bind.CallOpts, keyperSetIndex uint64) (bool, error) + SucceededAtRetry(opts *bind.CallOpts, keyperSetIndex uint64) (uint64, error) + WatchDealingSubmitted( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractDealingSubmitted, + keyperSetIndex []uint64, + retryCounter []uint64, + keyperIndex []uint64, + ) (gethevent.Subscription, error) + WatchAccusationSubmitted( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractAccusationSubmitted, + keyperSetIndex []uint64, + retryCounter []uint64, + keyperIndex []uint64, + ) (gethevent.Subscription, error) + WatchApologySubmitted( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractApologySubmitted, + keyperSetIndex []uint64, + retryCounter []uint64, + keyperIndex []uint64, + ) (gethevent.Subscription, error) + WatchSuccessVoteSubmitted( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractSuccessVoteSubmitted, + keyperSetIndex []uint64, + retryCounter []uint64, + keyperIndex []uint64, + ) (gethevent.Subscription, error) + WatchDKGSucceeded( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractDKGSucceeded, + keyperSetIndex []uint64, + retryCounter []uint64, + ) (gethevent.Subscription, error) +} + +// dkgContractBinder constructs a dkgContractBackend for a given DKG contract +// address. The production implementation binds a *dkgcontract.Dkgcontract; tests +// substitute an in-memory fake. +type dkgContractBinder func(addr common.Address) (dkgContractBackend, error) + +// DKGSyncer discovers DKG contract addresses autonomously by scanning +// every keyper set in KeyperSetManager and subscribing to KeyperSetAdded. +// For each unique non-zero DKG contract address it starts one DKGContractSyncer +// that watches the five bulletin-board event types (DealingSubmitted, +// AccusationSubmitted, ApologySubmitted, SuccessVoteSubmitted, DKGSucceeded) and +// forwards them to the shared Handler. +// +// Discovery and subscription are deduplicated by DKG contract address, so +// multiple keyper sets sharing one DKG contract result in exactly one +// DKGContractSyncer and no double event delivery. +type DKGSyncer struct { + Client client.Client + KeyperSetManager *bindings.KeyperSetManager + Log log.Logger + StartBlock *number.BlockNumber + Handler event.DKGEventHandler + + // resolveDKGContract is the function used to read the DKG contract address + // from a KeyperSet contract. It is overridable so tests can avoid binding + // to a real contract; production callers leave it nil and Start() fills in + // the default binding-backed implementation. + resolveDKGContract dkgContractResolver + + // bindDKGContract constructs a dkgContractBackend for a DKG contract + // address. Overridable for tests; Start() fills in the default + // binding-backed implementation when nil. + bindDKGContract dkgContractBinder + + // getKeyperSetAddress reads the KeyperSet contract address for a given + // Keyper Set Index. It is the subset of KeyperSetManager the gap-backfill + // path needs; overridable so tests can inject canned addresses. Start() + // fills in s.KeyperSetManager.GetKeyperSetAddress when nil. + getKeyperSetAddress func(opts *bind.CallOpts, index uint64) (common.Address, error) + + // runner is captured from Start() so that handleKeyperSetAdded can spawn + // new per-contract subscription goroutines as new DKG contract addresses + // are discovered at runtime. + runner service.Runner + + keyperSetAddedCh chan *bindings.KeyperSetManagerKeyperSetAdded + + // eventCh is the fan-in channel through which every DKG event -- initial + // successes synthesized at startup and live bulletin-board events from all + // DKGContractSyncer goroutines -- is funneled to a single consumer + // goroutine. The consumer calls the real Handler sequentially, so handler + // state needs no internal locking. It is created in Start() with the shared + // channelSize buffer. + eventCh chan event.DKGEvent + + trackedMu sync.Mutex + trackedDKGContracts map[common.Address]struct{} + // numKnownKeyperSets is the number of Keyper Set Indices the node has + // observed in KeyperSetManager: an exclusive upper bound used to iterate + // the Succeeded(ksi) queries when a new DKG contract subscription is + // started. It is monotonically non-decreasing because keyper set indices + // in KeyperSetManager are append-only. + numKnownKeyperSets uint64 +} + +func (s *DKGSyncer) Start(ctx context.Context, runner service.Runner) error { + if s.Handler == nil { + return errors.New("no handler registered") + } + + if s.StartBlock.IsLatest() { + latest, err := s.Client.BlockNumber(ctx) + if err != nil { + return err + } + s.StartBlock.SetUint64(latest) + } + + if s.resolveDKGContract == nil { + s.resolveDKGContract = defaultDKGContractResolver(s.Client) + } + if s.bindDKGContract == nil { + s.bindDKGContract = defaultDKGContractBinder(s.Client) + } + if s.getKeyperSetAddress == nil { + s.getKeyperSetAddress = s.KeyperSetManager.GetKeyperSetAddress + } + s.runner = runner + s.startEventConsumer(ctx, runner) + + startBlock := *s.StartBlock.ToUInt64Ptr() + callOpts := &bind.CallOpts{ + Context: ctx, + BlockNumber: s.StartBlock.Int, + } + if err := s.scanInitialDKGContracts(ctx, callOpts, s.KeyperSetManager); err != nil { + return errors.Wrap(err, "initial DKG contract scan") + } + + for _, addr := range s.trackedDKGContractList() { + if err := s.startContractSyncer(ctx, runner, addr, startBlock); err != nil { + return errors.Wrapf(err, "start syncer for DKG contract %s", addr.Hex()) + } + } + + watchOpts := &bind.WatchOpts{ + Start: &startBlock, + Context: ctx, + } + s.keyperSetAddedCh = make(chan *bindings.KeyperSetManagerKeyperSetAdded, channelSize) + keyperAddedSub, err := s.KeyperSetManager.WatchKeyperSetAdded(watchOpts, s.keyperSetAddedCh) + if err != nil { + return errors.Wrap(err, "watch KeyperSetAdded") + } + + runner.Go(func() error { + err := s.watchKeyperSetAdded(ctx, keyperAddedSub.Err()) + if err != nil { + s.Log.Error("error watching KeyperSetAdded events", "error", err.Error()) + } + keyperAddedSub.Unsubscribe() + return err + }) + return nil +} + +// startEventConsumer initializes the fan-in channel and starts the single +// consumer goroutine that drains it, calling the real Handler sequentially. +// Funneling every DKG event through one consumer guarantees the Handler is +// never invoked concurrently, so DKGEventHandler implementations need no +// internal locking. Handler errors are logged and draining continues, matching +// the deliver() behavior DKGContractSyncer used before this fan-in existed. +// The consumer exits on context cancellation; producers send via enqueueEvent, +// whose send is context-aware, so shutdown never deadlocks. +func (s *DKGSyncer) startEventConsumer(ctx context.Context, runner service.Runner) { + s.eventCh = make(chan event.DKGEvent, channelSize) + runner.Go(func() error { + for { + select { + case ev := <-s.eventCh: + if err := s.Handler(ctx, ev); err != nil { + s.Log.Error("handler for DKG event errored", "error", err.Error()) + } + case <-ctx.Done(): + return ctx.Err() + } + } + }) +} + +// enqueueEvent hands a DKG event to the fan-in channel for serialized delivery +// by the consumer goroutine. It is the Handler passed to every DKGContractSyncer +// and the delivery path for initial-success events. The send is context-aware so +// that a producer never blocks forever when the node is shutting down and the +// consumer has already exited. +func (s *DKGSyncer) enqueueEvent(ctx context.Context, ev event.DKGEvent) error { + select { + case s.eventCh <- ev: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// scanInitialDKGContracts iterates every keyper set known to the manager and +// records the DKG contract address each one points at via tryTrack. Failures to +// read an individual keyper set's DKG contract (RPC error) are surfaced; zero +// addresses are logged and skipped inside tryTrack, mirroring the runtime +// behavior for KeyperSetAdded. As a side effect, numKnownKeyperSets advances to +// the number of keyper sets visited. The tryTrack return value is ignored here; +// Start() iterates trackedDKGContractList() afterwards to spawn syncers. +func (s *DKGSyncer) scanInitialDKGContracts( + ctx context.Context, + opts *bind.CallOpts, + indexer keyperSetIndexer, +) error { + if err := guardCallOpts(opts, false); err != nil { + return err + } + numKS, err := indexer.GetNumKeyperSets(opts) + if err != nil { + return errors.Wrap(err, "get num keyper sets") + } + for i := uint64(0); i < numKS; i++ { + ksAddr, err := indexer.GetKeyperSetAddress(opts, i) + if err != nil { + return errors.Wrapf(err, "get keyper set address %d", i) + } + dkgAddr, err := s.resolveDKGContract(ctx, opts, ksAddr) + if err != nil { + return errors.Wrapf(err, "resolve DKG contract for keyper set %d (%s)", i, ksAddr.Hex()) + } + s.tryTrack(dkgAddr, i) + } + return nil +} + +// tryTrack is the single write path for both trackedDKGContracts and +// numKnownKeyperSets, performing all mutation under one trackedMu acquisition so +// that no observer ever sees the count advanced without the contract recorded or +// vice versa. It always advances numKnownKeyperSets to max(current, ksi+1). +// +// It returns true only when addr is a new, non-zero DKG contract address (i.e. a +// DKGContractSyncer should be started for it). A zero address logs a warning and +// returns false; an already-tracked address returns false. In both of those +// cases the count still advances, so shared-contract or unconfigured keyper sets +// never leave a gap in numKnownKeyperSets. +func (s *DKGSyncer) tryTrack(addr common.Address, ksi uint64) bool { + s.trackedMu.Lock() + defer s.trackedMu.Unlock() + + if ksi+1 > s.numKnownKeyperSets { + s.numKnownKeyperSets = ksi + 1 + } + + if (addr == common.Address{}) { + s.Log.Warn( + "keyper set has no DKG contract configured; skipping", + "keyper-set-index", ksi, + ) + return false + } + + if s.trackedDKGContracts == nil { + s.trackedDKGContracts = map[common.Address]struct{}{} + } + if _, exists := s.trackedDKGContracts[addr]; exists { + return false + } + s.trackedDKGContracts[addr] = struct{}{} + return true +} + +// trackedDKGContractList returns a snapshot of the currently tracked DKG +// contract addresses. Intended for tests and for Start() to iterate the set +// when spawning per-contract subscription goroutines. +func (s *DKGSyncer) trackedDKGContractList() []common.Address { + s.trackedMu.Lock() + defer s.trackedMu.Unlock() + out := make([]common.Address, 0, len(s.trackedDKGContracts)) + for addr := range s.trackedDKGContracts { + out = append(out, addr) + } + return out +} + +func (s *DKGSyncer) getNumKnownKeyperSets() uint64 { + s.trackedMu.Lock() + defer s.trackedMu.Unlock() + return s.numKnownKeyperSets +} + +// startContractSyncer delivers the initial success state for the given DKG +// contract and then starts a DKGContractSyncer to watch it for live events. +// It first queries Succeeded(ksi) for every known keyper set index at +// startBlock and enqueues synthetic SuccessEvents for already-completed +// instances onto the fan-in channel. It then constructs a DKGContractSyncer +// bound to the same backend -- whose Handler is the same fan-in send -- and +// starts it via runner.StartService, which sets up the five bulletin-board +// subscriptions from startBlock and forwards live events onto the fan-in +// channel too. Because initial successes are enqueued before the live watcher +// starts and a single consumer drains the channel in FIFO order, catch-up +// state always reaches the real Handler before live events for the contract. +// +// startBlock is the height from which live events are watched. At startup +// it is the resolved StartBlock; at runtime it is the block of the triggering +// KeyperSetAdded event so events fired between keyper set registration and +// subscription startup are not missed. +func (s *DKGSyncer) startContractSyncer( + ctx context.Context, + runner service.Runner, + addr common.Address, + startBlock uint64, +) error { + backend, err := s.bindDKGContract(addr) + if err != nil { + return errors.Wrapf(err, "bind DKG contract %s", addr.Hex()) + } + + initial, err := s.initialSuccessesForContract(ctx, backend, startBlock) + if err != nil { + return errors.Wrapf(err, "initial successes for DKG contract %s", addr.Hex()) + } + // Enqueue initial successes before starting the live watcher so that, with a + // single FIFO consumer, catch-up state for this contract is always handled + // before any live event from the same contract. + for _, ev := range initial { + if err := s.enqueueEvent(ctx, ev); err != nil { + return errors.Wrapf(err, "enqueue initial success for DKG contract %s", addr.Hex()) + } + } + + contractSyncer := &DKGContractSyncer{ + Addr: addr, + Log: s.Log, + Handler: s.enqueueEvent, + StartBlock: number.NewBlockNumber(&startBlock), + backend: backend, + } + if err := runner.StartService(contractSyncer); err != nil { + return errors.Wrapf(err, "start DKG contract syncer %s", addr.Hex()) + } + return nil +} + +// initialSuccessesForContract queries DKGContract.succeeded(ksi) on the given +// contract for each known keyper set index. Already-succeeded instances are +// returned as synthetic SuccessEvents so the local cache (e.g. dkg_result) +// can be populated before any live events for the same contract arrive. +func (s *DKGSyncer) initialSuccessesForContract( + ctx context.Context, + backend dkgContractBackend, + startBlock uint64, +) ([]event.DKGEvent, error) { + opts := &bind.CallOpts{ + Context: ctx, + BlockNumber: new(big.Int).SetUint64(startBlock), + } + if err := guardCallOpts(opts, false); err != nil { + return nil, err + } + numKS := s.getNumKnownKeyperSets() + events := make([]event.DKGEvent, 0, numKS) + for i := uint64(0); i < numKS; i++ { + succeeded, err := backend.Succeeded(opts, i) + if err != nil { + return nil, errors.Wrapf(err, "query succeeded for keyper set %d", i) + } + if !succeeded { + continue + } + retry, err := backend.SucceededAtRetry(opts, i) + if err != nil { + return nil, errors.Wrapf(err, "query succeededAtRetry for keyper set %d", i) + } + events = append(events, &event.SuccessEvent{ + KeyperSetIndex: i, + RetryCounter: retry, + AtBlockNumber: number.BigToBlockNumber(opts.BlockNumber), + }) + } + return events, nil +} + +// watchKeyperSetAdded drains the KeyperSetAdded subscription channel and +// dispatches each event to handleKeyperSetAdded for DKG contract discovery +// and subscription spawning. RPC failures and zero addresses are tolerated; +// a single bad keyper set must not bring down DKG event syncing for the rest. +func (s *DKGSyncer) watchKeyperSetAdded(ctx context.Context, subErr <-chan error) error { + for { + select { + case ev, ok := <-s.keyperSetAddedCh: + if !ok { + return nil + } + s.handleKeyperSetAdded(ctx, ev) + case err := <-subErr: + if err != nil { + return errors.Wrap(err, "KeyperSetAdded subscription") + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// handleKeyperSetAdded processes a KeyperSetAdded event for DKG contract +// discovery, detecting and repairing gaps in the event stream first. +// +// The incoming Keyper Set Index (ev.Eon) is compared against numKnownKeyperSets: +// +// - smaller (already seen): the event is a duplicate or arrived out of order; +// a warning is logged and the event is ignored. +// - larger (gap): one or more KeyperSetAdded events were missed. A warning is +// logged and every missed index in [numKnownKeyperSets, ev.Eon) is +// backfilled -- read, resolved, tracked, and (if newly added) synced from +// the triggering event's block -- before the current event is processed. +// - equal (expected): the event is processed directly. +// +// Backfill runs synchronously in the watchKeyperSetAdded goroutine; gaps are +// expected to be rare, so blocking the event loop for its duration is accepted. +// +// For the triggering event itself, the DKG contract address is resolved, +// recorded via tryTrack, and -- if newly added and a runner is available -- a +// per-contract subscription goroutine is spawned from the block of the event. +// RPC failures and zero addresses are logged but do not abort the event loop. +// On a resolve error the state (including numKnownKeyperSets) is left untouched, +// trading partial-update semantics for a single write path through tryTrack. +func (s *DKGSyncer) handleKeyperSetAdded(ctx context.Context, ev *bindings.KeyperSetManagerKeyperSetAdded) { + opts := logToCallOpts(ctx, &ev.Raw) + + switch known := s.getNumKnownKeyperSets(); { + case ev.Eon < known: + s.Log.Warn( + "received KeyperSetAdded for an already-seen keyper set index; ignoring", + "expected-keyper-set-index", known, + "received-keyper-set-index", ev.Eon, + ) + return + case ev.Eon > known: + s.Log.Warn( + "gap in KeyperSetAdded event stream; backfilling missed keyper set indices", + "expected-keyper-set-index", known, + "received-keyper-set-index", ev.Eon, + ) + for i := known; i < ev.Eon; i++ { + s.backfillKeyperSet(ctx, opts, i, ev.Raw.BlockNumber) + } + } + + dkgAddr, err := s.resolveDKGContract(ctx, opts, ev.KeyperSetContract) + if err != nil { + s.Log.Error( + "could not resolve DKG contract for new keyper set", + "error", err.Error(), + "keyper-set", ev.KeyperSetContract.Hex(), + "eon", ev.Eon, + ) + return + } + if !s.tryTrack(dkgAddr, ev.Eon) { + return + } + if s.runner == nil || s.bindDKGContract == nil { + return + } + if err := s.startContractSyncer(ctx, s.runner, dkgAddr, ev.Raw.BlockNumber); err != nil { + s.Log.Error( + "could not start DKG contract subscription for new keyper set", + "error", err.Error(), + "dkg-contract", dkgAddr.Hex(), + "keyper-set", ev.KeyperSetContract.Hex(), + "eon", ev.Eon, + ) + } +} + +// backfillKeyperSet recovers a single Keyper Set Index that was missing from the +// KeyperSetAdded event stream: it reads the KeyperSet contract address, resolves +// its DKG contract, records it via tryTrack, and -- if the address is newly +// added and a runner is available -- starts a DKGContractSyncer from startBlock +// (the block of the KeyperSetAdded event that revealed the gap). All RPC calls +// use opts derived from that triggering event. RPC failures and zero addresses +// are logged and tolerated so that one unreadable keyper set does not abort +// backfill of the rest or processing of the triggering event. +func (s *DKGSyncer) backfillKeyperSet(ctx context.Context, opts *bind.CallOpts, ksi, startBlock uint64) { + ksAddr, err := s.getKeyperSetAddress(opts, ksi) + if err != nil { + s.Log.Error( + "could not read keyper set address while backfilling gap", + "error", err.Error(), + "keyper-set-index", ksi, + ) + return + } + dkgAddr, err := s.resolveDKGContract(ctx, opts, ksAddr) + if err != nil { + s.Log.Error( + "could not resolve DKG contract while backfilling gap", + "error", err.Error(), + "keyper-set", ksAddr.Hex(), + "keyper-set-index", ksi, + ) + return + } + if !s.tryTrack(dkgAddr, ksi) { + return + } + if s.runner == nil || s.bindDKGContract == nil { + return + } + if err := s.startContractSyncer(ctx, s.runner, dkgAddr, startBlock); err != nil { + s.Log.Error( + "could not start DKG contract subscription while backfilling gap", + "error", err.Error(), + "dkg-contract", dkgAddr.Hex(), + "keyper-set-index", ksi, + ) + } +} + +// defaultDKGContractResolver binds the KeyperSet contract at keyperSetAddr and +// calls its getDKGContract() view. This is the production resolver; tests +// override DKGSyncer.resolveDKGContract with an in-memory stub. +func defaultDKGContractResolver(backend bind.ContractBackend) dkgContractResolver { + return func(_ context.Context, opts *bind.CallOpts, keyperSetAddr common.Address) (common.Address, error) { + ks, err := keypersetBindings.NewKeyperset(keyperSetAddr, backend) + if err != nil { + return common.Address{}, errors.Wrap(err, "bind KeyperSet contract") + } + return ks.GetDKGContract(opts) + } +} + +// defaultDKGContractBinder binds a *dkgcontract.Dkgcontract at the given address. +// This is the production binder; tests override DKGSyncer.bindDKGContract +// with an in-memory fake that can emit canned events. +func defaultDKGContractBinder(backend bind.ContractBackend) dkgContractBinder { + return func(addr common.Address) (dkgContractBackend, error) { + return dkgcontract.NewDkgcontract(addr, backend) + } +} diff --git a/rolling-shutter/medley/chainsync/syncer/dkg_contract.go b/rolling-shutter/medley/chainsync/syncer/dkg_contract.go new file mode 100644 index 000000000..1b84d2e5d --- /dev/null +++ b/rolling-shutter/medley/chainsync/syncer/dkg_contract.go @@ -0,0 +1,260 @@ +package syncer + +import ( + "context" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethevent "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/pkg/errors" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/number" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" +) + +// dkgEventWatcher is the subset of *dkgcontract.Dkgcontract that DKGContractSyncer +// uses: the five bulletin-board event subscriptions. Pulling it out as an +// interface lets tests substitute a fake backend that emits canned events +// without touching a simulated chain. +type dkgEventWatcher interface { + WatchDealingSubmitted( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractDealingSubmitted, + keyperSetIndex []uint64, + retryCounter []uint64, + keyperIndex []uint64, + ) (gethevent.Subscription, error) + WatchAccusationSubmitted( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractAccusationSubmitted, + keyperSetIndex []uint64, + retryCounter []uint64, + keyperIndex []uint64, + ) (gethevent.Subscription, error) + WatchApologySubmitted( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractApologySubmitted, + keyperSetIndex []uint64, + retryCounter []uint64, + keyperIndex []uint64, + ) (gethevent.Subscription, error) + WatchSuccessVoteSubmitted( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractSuccessVoteSubmitted, + keyperSetIndex []uint64, + retryCounter []uint64, + keyperIndex []uint64, + ) (gethevent.Subscription, error) + WatchDKGSucceeded( + opts *bind.WatchOpts, + sink chan<- *dkgcontract.DkgcontractDKGSucceeded, + keyperSetIndex []uint64, + retryCounter []uint64, + ) (gethevent.Subscription, error) +} + +// DKGContractSyncer watches exactly one DKG contract for the five bulletin-board +// event types (DealingSubmitted, AccusationSubmitted, ApologySubmitted, +// SuccessVoteSubmitted, DKGSucceeded) and forwards them to the shared Handler. +// It is a leaf service.Service with no knowledge of the keyper set universe; +// it is constructed and started by DKGSyncer once per unique DKG contract +// address. +type DKGContractSyncer struct { + Contract *dkgcontract.Dkgcontract + Addr common.Address + Log log.Logger + Handler event.DKGEventHandler + StartBlock *number.BlockNumber + + // backend overrides Contract for test injection. When nil, Start binds the + // five Watch* subscriptions to the pre-bound Contract. + backend dkgEventWatcher +} + +func (s *DKGContractSyncer) Start(ctx context.Context, runner service.Runner) error { + if s.Handler == nil { + return errors.New("no handler registered") + } + + backend := s.backend + if backend == nil { + backend = s.Contract + } + + startBlock := *s.StartBlock.ToUInt64Ptr() + watchOpts := &bind.WatchOpts{ + Start: &startBlock, + Context: ctx, + } + + dealingCh := make(chan *dkgcontract.DkgcontractDealingSubmitted, channelSize) + accusationCh := make(chan *dkgcontract.DkgcontractAccusationSubmitted, channelSize) + apologyCh := make(chan *dkgcontract.DkgcontractApologySubmitted, channelSize) + successVoteCh := make(chan *dkgcontract.DkgcontractSuccessVoteSubmitted, channelSize) + successCh := make(chan *dkgcontract.DkgcontractDKGSucceeded, channelSize) + + dealingSub, err := backend.WatchDealingSubmitted(watchOpts, dealingCh, nil, nil, nil) + if err != nil { + return errors.Wrap(err, "watch DealingSubmitted") + } + accusationSub, err := backend.WatchAccusationSubmitted(watchOpts, accusationCh, nil, nil, nil) + if err != nil { + dealingSub.Unsubscribe() + return errors.Wrap(err, "watch AccusationSubmitted") + } + apologySub, err := backend.WatchApologySubmitted(watchOpts, apologyCh, nil, nil, nil) + if err != nil { + dealingSub.Unsubscribe() + accusationSub.Unsubscribe() + return errors.Wrap(err, "watch ApologySubmitted") + } + successVoteSub, err := backend.WatchSuccessVoteSubmitted(watchOpts, successVoteCh, nil, nil, nil) + if err != nil { + dealingSub.Unsubscribe() + accusationSub.Unsubscribe() + apologySub.Unsubscribe() + return errors.Wrap(err, "watch SuccessVoteSubmitted") + } + successSub, err := backend.WatchDKGSucceeded(watchOpts, successCh, nil, nil) + if err != nil { + dealingSub.Unsubscribe() + accusationSub.Unsubscribe() + apologySub.Unsubscribe() + successVoteSub.Unsubscribe() + return errors.Wrap(err, "watch DKGSucceeded") + } + + runner.Go(func() error { + err := s.watchContractEvents( + ctx, + dealingCh, accusationCh, apologyCh, successVoteCh, successCh, + dealingSub.Err(), accusationSub.Err(), apologySub.Err(), successVoteSub.Err(), successSub.Err(), + ) + if err != nil { + s.Log.Error("error watching DKG events", "error", err.Error(), "dkg-contract", s.Addr.Hex()) + } + dealingSub.Unsubscribe() + accusationSub.Unsubscribe() + apologySub.Unsubscribe() + successVoteSub.Unsubscribe() + successSub.Unsubscribe() + return err + }) + return nil +} + +// watchContractEvents is the per-contract subscription loop. It drains the five +// event channels, forwards events to the shared Handler via deliver(), and exits +// on context cancellation or subscription error. +// +//nolint:gocyclo // fan-in select over five event channels and five error channels; splitting the branches would obscure the dispatch. +func (s *DKGContractSyncer) watchContractEvents( + ctx context.Context, + dealingCh <-chan *dkgcontract.DkgcontractDealingSubmitted, + accusationCh <-chan *dkgcontract.DkgcontractAccusationSubmitted, + apologyCh <-chan *dkgcontract.DkgcontractApologySubmitted, + successVoteCh <-chan *dkgcontract.DkgcontractSuccessVoteSubmitted, + successCh <-chan *dkgcontract.DkgcontractDKGSucceeded, + dealingErr, accusationErr, apologyErr, successVoteErr, successErr <-chan error, +) error { + for { + select { + case ev, ok := <-dealingCh: + if !ok { + return nil + } + bn := ev.Raw.BlockNumber + s.deliver(ctx, &event.DealingEvent{ + KeyperSetIndex: ev.KeyperSetIndex, + RetryCounter: ev.RetryCounter, + KeyperIndex: ev.KeyperIndex, + Commitment: ev.Commitment, + PolyEvals: ev.PolyEvals, + AtBlockNumber: number.NewBlockNumber(&bn), + }) + case ev, ok := <-accusationCh: + if !ok { + return nil + } + bn := ev.Raw.BlockNumber + s.deliver(ctx, &event.AccusationEvent{ + KeyperSetIndex: ev.KeyperSetIndex, + RetryCounter: ev.RetryCounter, + KeyperIndex: ev.KeyperIndex, + AccusedIndices: ev.AccusedIndices, + AtBlockNumber: number.NewBlockNumber(&bn), + }) + case ev, ok := <-apologyCh: + if !ok { + return nil + } + bn := ev.Raw.BlockNumber + s.deliver(ctx, &event.ApologyEvent{ + KeyperSetIndex: ev.KeyperSetIndex, + RetryCounter: ev.RetryCounter, + KeyperIndex: ev.KeyperIndex, + AccuserIndices: ev.AccuserIndices, + PolyEvalData: ev.PolyEvalData, + AtBlockNumber: number.NewBlockNumber(&bn), + }) + case ev, ok := <-successVoteCh: + if !ok { + return nil + } + bn := ev.Raw.BlockNumber + s.deliver(ctx, &event.SuccessVoteEvent{ + KeyperSetIndex: ev.KeyperSetIndex, + RetryCounter: ev.RetryCounter, + KeyperIndex: ev.KeyperIndex, + EonPublicKey: ev.EonPublicKey, + AtBlockNumber: number.NewBlockNumber(&bn), + }) + case ev, ok := <-successCh: + if !ok { + return nil + } + bn := ev.Raw.BlockNumber + s.deliver(ctx, &event.SuccessEvent{ + KeyperSetIndex: ev.KeyperSetIndex, + RetryCounter: ev.RetryCounter, + EonPublicKey: ev.EonPublicKey, + AtBlockNumber: number.NewBlockNumber(&bn), + }) + case err := <-dealingErr: + if err != nil { + return errors.Wrapf(err, "DealingSubmitted subscription (%s)", s.Addr.Hex()) + } + case err := <-accusationErr: + if err != nil { + return errors.Wrapf(err, "AccusationSubmitted subscription (%s)", s.Addr.Hex()) + } + case err := <-apologyErr: + if err != nil { + return errors.Wrapf(err, "ApologySubmitted subscription (%s)", s.Addr.Hex()) + } + case err := <-successVoteErr: + if err != nil { + return errors.Wrapf(err, "SuccessVoteSubmitted subscription (%s)", s.Addr.Hex()) + } + case err := <-successErr: + if err != nil { + return errors.Wrapf(err, "DKGSucceeded subscription (%s)", s.Addr.Hex()) + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func (s *DKGContractSyncer) deliver(ctx context.Context, ev event.DKGEvent) { + if err := s.Handler(ctx, ev); err != nil { + s.Log.Error( + "handler for DKG event errored", + "error", + err.Error(), + ) + } +} diff --git a/rolling-shutter/medley/chainsync/syncer/dkg_contract_test.go b/rolling-shutter/medley/chainsync/syncer/dkg_contract_test.go new file mode 100644 index 000000000..1f7b4421b --- /dev/null +++ b/rolling-shutter/medley/chainsync/syncer/dkg_contract_test.go @@ -0,0 +1,213 @@ +package syncer + +import ( + "context" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + "gotest.tools/assert" + + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/number" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/logger" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" +) + +// DKGContractSyncer must be an independently startable service.Service, and +// fakeDKGBackend must satisfy the leaf's backend interface. +var ( + _ service.Service = (*DKGContractSyncer)(nil) + _ dkgEventWatcher = (*fakeDKGBackend)(nil) +) + +func blockNumber(u uint64) *number.BlockNumber { + return number.NewBlockNumber(&u) +} + +// newContractSyncer builds a DKGContractSyncer wired to a fake backend, with a +// recording handler so tests can pull delivered events off a channel. +func newContractSyncer(addr common.Address, backend dkgEventWatcher, startBlock uint64) (*DKGContractSyncer, *recordingHandler) { + handler := newRecordingHandler() + s := &DKGContractSyncer{ + Addr: addr, + Log: &logger.NoopLogger{}, + Handler: handler.Handle, + StartBlock: blockNumber(startBlock), + backend: backend, + } + return s, handler +} + +func TestDKGContractSyncerStartRequiresHandler(t *testing.T) { + s := &DKGContractSyncer{ + Log: &logger.NoopLogger{}, + StartBlock: blockNumber(1), + backend: newFakeDKGBackend(common.HexToAddress("0xaa")), + } + runner := newFakeRunner(context.Background()) + t.Cleanup(runner.Wait) + err := s.Start(context.Background(), runner) + assert.Assert(t, err != nil, "Start without a handler must error") +} + +func TestDKGContractSyncerDeliversDealingEvent(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + s, handler := newContractSyncer(addr, backend, 42) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + assert.NilError(t, s.Start(ctx, runner)) + + backend.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 7, KeyperIndex: 3}) + + ev := handler.expectNextEvent(t) + de, ok := ev.(*event.DealingEvent) + assert.Assert(t, ok, "expected *DealingEvent, got %T", ev) + assert.Equal(t, uint64(7), de.KeyperSetIndex) + assert.Equal(t, uint64(3), de.KeyperIndex) +} + +func TestDKGContractSyncerDeliversAccusationEvent(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + s, handler := newContractSyncer(addr, backend, 42) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + assert.NilError(t, s.Start(ctx, runner)) + + backend.emitAccusation(t, &dkgcontract.DkgcontractAccusationSubmitted{KeyperSetIndex: 1, KeyperIndex: 2}) + + ev := handler.expectNextEvent(t) + ae, ok := ev.(*event.AccusationEvent) + assert.Assert(t, ok, "expected *AccusationEvent, got %T", ev) + assert.Equal(t, uint64(1), ae.KeyperSetIndex) + assert.Equal(t, uint64(2), ae.KeyperIndex) +} + +func TestDKGContractSyncerDeliversApologyEvent(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + s, handler := newContractSyncer(addr, backend, 42) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + assert.NilError(t, s.Start(ctx, runner)) + + backend.emitApology(t, &dkgcontract.DkgcontractApologySubmitted{KeyperSetIndex: 4, KeyperIndex: 5}) + + ev := handler.expectNextEvent(t) + ae, ok := ev.(*event.ApologyEvent) + assert.Assert(t, ok, "expected *ApologyEvent, got %T", ev) + assert.Equal(t, uint64(4), ae.KeyperSetIndex) + assert.Equal(t, uint64(5), ae.KeyperIndex) +} + +func TestDKGContractSyncerDeliversSuccessVoteEvent(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + s, handler := newContractSyncer(addr, backend, 42) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + assert.NilError(t, s.Start(ctx, runner)) + + backend.emitSuccessVote(t, &dkgcontract.DkgcontractSuccessVoteSubmitted{KeyperSetIndex: 8, KeyperIndex: 9}) + + ev := handler.expectNextEvent(t) + sve, ok := ev.(*event.SuccessVoteEvent) + assert.Assert(t, ok, "expected *SuccessVoteEvent, got %T", ev) + assert.Equal(t, uint64(8), sve.KeyperSetIndex) + assert.Equal(t, uint64(9), sve.KeyperIndex) +} + +func TestDKGContractSyncerDeliversSuccessEvent(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + s, handler := newContractSyncer(addr, backend, 42) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + assert.NilError(t, s.Start(ctx, runner)) + + backend.emitSuccess(t, &dkgcontract.DkgcontractDKGSucceeded{KeyperSetIndex: 6, RetryCounter: 2}) + + ev := handler.expectNextEvent(t) + se, ok := ev.(*event.SuccessEvent) + assert.Assert(t, ok, "expected *SuccessEvent, got %T", ev) + assert.Equal(t, uint64(6), se.KeyperSetIndex) + assert.Equal(t, uint64(2), se.RetryCounter) +} + +func TestDKGContractSyncerWatchStartIsRequestedBlock(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + s, _ := newContractSyncer(addr, backend, 4242) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + assert.NilError(t, s.Start(ctx, runner)) + + assert.Equal(t, uint64(4242), backend.dealingStart, "DealingSubmitted watch must start from requested block") + assert.Equal(t, uint64(4242), backend.accusationStart) + assert.Equal(t, uint64(4242), backend.apologyStart) + assert.Equal(t, uint64(4242), backend.successVoteStart) + assert.Equal(t, uint64(4242), backend.successStart) +} + +func TestDKGContractSyncerSubscriptionSetupErrorPropagates(t *testing.T) { + for _, eventType := range []string{"dealing", "accusation", "apology", "successVote", "success"} { + t.Run(eventType, func(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + backend.watchErrs = map[string]error{eventType: errors.New("simulated subscription failure")} + s, _ := newContractSyncer(addr, backend, 1) + + runner := newFakeRunner(context.Background()) + t.Cleanup(runner.Wait) + + err := s.Start(context.Background(), runner) + assert.Assert(t, err != nil, "subscription setup error on %s channel must propagate from Start", eventType) + }) + } +} + +func TestDKGContractSyncerContextCancellationExitsCleanly(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + s, _ := newContractSyncer(addr, backend, 1) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + + assert.NilError(t, s.Start(ctx, runner)) + + cancel() + + done := make(chan struct{}) + go func() { + runner.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("watch goroutine did not exit after context cancellation") + } +} diff --git a/rolling-shutter/medley/chainsync/syncer/dkg_test.go b/rolling-shutter/medley/chainsync/syncer/dkg_test.go new file mode 100644 index 000000000..70fc2ff37 --- /dev/null +++ b/rolling-shutter/medley/chainsync/syncer/dkg_test.go @@ -0,0 +1,1137 @@ +package syncer + +import ( + "context" + "math/big" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethevent "github.com/ethereum/go-ethereum/event" + gethlog "github.com/ethereum/go-ethereum/log" + "github.com/pkg/errors" + "github.com/shutter-network/contracts/v2/bindings/dkgcontract" + "github.com/shutter-network/shop-contracts/bindings" + "gotest.tools/assert" + + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/logger" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" +) + +// recordingLogger captures every Warn call for assertion in tests. All other +// log levels are inherited from NoopLogger. +type recordingLogger struct { + *logger.NoopLogger + mu sync.Mutex + warns []logEntry +} + +type logEntry struct { + msg string + ctx []interface{} +} + +func newRecordingLogger() *recordingLogger { + return &recordingLogger{NoopLogger: &logger.NoopLogger{}} +} + +func (r *recordingLogger) Warn(msg string, ctx ...interface{}) { + r.mu.Lock() + defer r.mu.Unlock() + r.warns = append(r.warns, logEntry{msg: msg, ctx: ctx}) +} + +func (r *recordingLogger) warnCount() int { + r.mu.Lock() + defer r.mu.Unlock() + return len(r.warns) +} + +// fakeKeyperSetIndexer implements keyperSetIndexer for unit tests. +type fakeKeyperSetIndexer struct { + addresses []common.Address +} + +func (f *fakeKeyperSetIndexer) GetNumKeyperSets(*bind.CallOpts) (uint64, error) { + return uint64(len(f.addresses)), nil +} + +func (f *fakeKeyperSetIndexer) GetKeyperSetAddress(_ *bind.CallOpts, index uint64) (common.Address, error) { + if index >= uint64(len(f.addresses)) { + return common.Address{}, errors.New("index out of range") + } + return f.addresses[index], nil +} + +// stubResolver returns a resolver that maps each KeyperSet address to a +// preconfigured DKG contract address. +func stubResolver(mapping map[common.Address]common.Address) dkgContractResolver { + return func(_ context.Context, _ *bind.CallOpts, ksAddr common.Address) (common.Address, error) { + addr, ok := mapping[ksAddr] + if !ok { + return common.Address{}, errors.Errorf("unknown keyper set %s", ksAddr.Hex()) + } + return addr, nil + } +} + +func newSyncer(t *testing.T, resolver dkgContractResolver) (*DKGSyncer, *recordingLogger) { + t.Helper() + rec := newRecordingLogger() + s := &DKGSyncer{ + Log: rec, + resolveDKGContract: resolver, + } + return s, rec +} + +func TestTryTrackAddsNewAddress(t *testing.T) { + s, rec := newSyncer(t, nil) + addr := common.HexToAddress("0x0000000000000000000000000000000000000001") + + added := s.tryTrack(addr, 0) + assert.Equal(t, true, added, "first insertion should be reported as newly added") + assert.Equal(t, 0, rec.warnCount(), "non-zero address must not produce a warning") + assert.DeepEqual(t, []common.Address{addr}, s.trackedDKGContractList()) + assert.Equal(t, uint64(1), s.getNumKnownKeyperSets(), "count must advance to ksi+1") +} + +func TestTryTrackDeduplicatesButStillAdvancesCount(t *testing.T) { + s, _ := newSyncer(t, nil) + addr := common.HexToAddress("0x0000000000000000000000000000000000000001") + + addedFirst := s.tryTrack(addr, 0) + addedSecond := s.tryTrack(addr, 1) + + assert.Equal(t, true, addedFirst) + assert.Equal(t, false, addedSecond, "second insertion of the same address must be reported as not newly added") + assert.Equal(t, 1, len(s.trackedDKGContractList()), "tracked set must contain a single entry after dedup") + assert.Equal(t, uint64(2), s.getNumKnownKeyperSets(), + "count must advance past both keyper set indices even though the address is shared") +} + +func TestTryTrackZeroAddressWarnsSkipsAndAdvancesCount(t *testing.T) { + s, rec := newSyncer(t, nil) + + added := s.tryTrack(common.Address{}, 7) + + assert.Equal(t, false, added, "zero address must not be added") + assert.Equal(t, 0, len(s.trackedDKGContractList())) + assert.Equal(t, 1, rec.warnCount(), "zero address must produce exactly one warning") + assert.Equal(t, uint64(8), s.getNumKnownKeyperSets(), + "count must advance even when the DKG contract address is zero") +} + +func TestScanInitialDKGContractsPopulatesSet(t *testing.T) { + ks0 := common.HexToAddress("0xaa") + ks1 := common.HexToAddress("0xbb") + dkg0 := common.HexToAddress("0xcc") + dkg1 := common.HexToAddress("0xdd") + + indexer := &fakeKeyperSetIndexer{addresses: []common.Address{ks0, ks1}} + resolver := stubResolver(map[common.Address]common.Address{ks0: dkg0, ks1: dkg1}) + s, _ := newSyncer(t, resolver) + + opts := &bind.CallOpts{Context: context.Background(), BlockNumber: big.NewInt(1)} + err := s.scanInitialDKGContracts(context.Background(), opts, indexer) + assert.NilError(t, err) + + got := s.trackedDKGContractList() + assert.Equal(t, 2, len(got)) + want := map[common.Address]struct{}{dkg0: {}, dkg1: {}} + for _, addr := range got { + _, ok := want[addr] + assert.Equal(t, true, ok, "unexpected DKG contract %s in tracked set", addr.Hex()) + } +} + +func TestScanInitialDKGContractsDeduplicatesSharedContract(t *testing.T) { + ks0 := common.HexToAddress("0xaa") + ks1 := common.HexToAddress("0xbb") + ks2 := common.HexToAddress("0xcc") + shared := common.HexToAddress("0xdd") + + indexer := &fakeKeyperSetIndexer{addresses: []common.Address{ks0, ks1, ks2}} + resolver := stubResolver(map[common.Address]common.Address{ks0: shared, ks1: shared, ks2: shared}) + s, _ := newSyncer(t, resolver) + + opts := &bind.CallOpts{Context: context.Background(), BlockNumber: big.NewInt(1)} + err := s.scanInitialDKGContracts(context.Background(), opts, indexer) + assert.NilError(t, err) + + assert.DeepEqual(t, []common.Address{shared}, s.trackedDKGContractList()) +} + +func TestScanInitialDKGContractsZeroAddressSkippedWithWarning(t *testing.T) { + ks0 := common.HexToAddress("0xaa") + ks1 := common.HexToAddress("0xbb") + dkg1 := common.HexToAddress("0xdd") + + indexer := &fakeKeyperSetIndexer{addresses: []common.Address{ks0, ks1}} + resolver := stubResolver(map[common.Address]common.Address{ks0: {}, ks1: dkg1}) + s, rec := newSyncer(t, resolver) + + opts := &bind.CallOpts{Context: context.Background(), BlockNumber: big.NewInt(1)} + err := s.scanInitialDKGContracts(context.Background(), opts, indexer) + assert.NilError(t, err) + + assert.DeepEqual(t, []common.Address{dkg1}, s.trackedDKGContractList()) + assert.Equal(t, 1, rec.warnCount(), "exactly one warning should be emitted for the zero-address keyper set") +} + +func TestScanInitialDKGContractsEmptyKeyperSetManager(t *testing.T) { + indexer := &fakeKeyperSetIndexer{} + resolver := stubResolver(nil) + s, rec := newSyncer(t, resolver) + + opts := &bind.CallOpts{Context: context.Background(), BlockNumber: big.NewInt(1)} + err := s.scanInitialDKGContracts(context.Background(), opts, indexer) + assert.NilError(t, err) + + assert.Equal(t, 0, len(s.trackedDKGContractList())) + assert.Equal(t, 0, rec.warnCount()) +} + +func TestHandleKeyperSetAddedRecordsNewContract(t *testing.T) { + ksAddr := common.HexToAddress("0xaa") + dkgAddr := common.HexToAddress("0xbb") + resolver := stubResolver(map[common.Address]common.Address{ksAddr: dkgAddr}) + s, _ := newSyncer(t, resolver) + + // Eon 0 is the expected next index given numKnownKeyperSets starts at 0, so + // this exercises the normal (no-gap) recording path. + ev := &bindings.KeyperSetManagerKeyperSetAdded{ + KeyperSetContract: ksAddr, + Eon: 0, + } + ev.Raw.BlockNumber = 42 + + s.handleKeyperSetAdded(context.Background(), ev) + + assert.DeepEqual(t, []common.Address{dkgAddr}, s.trackedDKGContractList()) +} + +func TestHandleKeyperSetAddedDeduplicatesAgainstExisting(t *testing.T) { + ks0 := common.HexToAddress("0xaa") + ks1 := common.HexToAddress("0xbb") + shared := common.HexToAddress("0xcc") + resolver := stubResolver(map[common.Address]common.Address{ks0: shared, ks1: shared}) + s, _ := newSyncer(t, resolver) + + indexer := &fakeKeyperSetIndexer{addresses: []common.Address{ks0}} + opts := &bind.CallOpts{Context: context.Background(), BlockNumber: big.NewInt(1)} + assert.NilError(t, s.scanInitialDKGContracts(context.Background(), opts, indexer)) + assert.DeepEqual(t, []common.Address{shared}, s.trackedDKGContractList()) + + ev := &bindings.KeyperSetManagerKeyperSetAdded{ + KeyperSetContract: ks1, + Eon: 1, + } + ev.Raw.BlockNumber = 99 + s.handleKeyperSetAdded(context.Background(), ev) + + assert.DeepEqual(t, []common.Address{shared}, s.trackedDKGContractList()) +} + +func TestHandleKeyperSetAddedZeroAddressLogsWarning(t *testing.T) { + ksAddr := common.HexToAddress("0xaa") + resolver := stubResolver(map[common.Address]common.Address{ksAddr: {}}) + s, rec := newSyncer(t, resolver) + + // Eon 0 keeps this on the no-gap path; the single warning asserted below is + // the zero-address warning, not a gap warning. + ev := &bindings.KeyperSetManagerKeyperSetAdded{ + KeyperSetContract: ksAddr, + Eon: 0, + } + s.handleKeyperSetAdded(context.Background(), ev) + + assert.Equal(t, 0, len(s.trackedDKGContractList())) + assert.Equal(t, 1, rec.warnCount()) +} + +func TestHandleKeyperSetAddedResolverErrorIsToleratedWithoutTracking(t *testing.T) { + resolver := func(context.Context, *bind.CallOpts, common.Address) (common.Address, error) { + return common.Address{}, errors.New("simulated RPC failure") + } + s, _ := newSyncer(t, resolver) + + // Eon 0 keeps this on the no-gap path so the test isolates resolver-error + // tolerance for the triggering event. + ev := &bindings.KeyperSetManagerKeyperSetAdded{ + KeyperSetContract: common.HexToAddress("0xaa"), + Eon: 0, + } + s.handleKeyperSetAdded(context.Background(), ev) + + assert.Equal(t, 0, len(s.trackedDKGContractList()), + "a resolver error must not result in any DKG contract being tracked") +} + +func TestHandleKeyperSetAddedBackfillsGap(t *testing.T) { + ks0 := common.HexToAddress("0xa0") + ks1 := common.HexToAddress("0xa1") + ks2 := common.HexToAddress("0xa2") + dkg0 := common.HexToAddress("0xb0") + dkg1 := common.HexToAddress("0xb1") + dkg2 := common.HexToAddress("0xb2") + + keyperSets := map[uint64]common.Address{0: ks0, 1: ks1, 2: ks2} + resolver := stubResolver(map[common.Address]common.Address{ks0: dkg0, ks1: dkg1, ks2: dkg2}) + backend0 := newFakeDKGBackend(dkg0) + backend1 := newFakeDKGBackend(dkg1) + backend2 := newFakeDKGBackend(dkg2) + backends := map[common.Address]*fakeDKGBackend{dkg0: backend0, dkg1: backend1, dkg2: backend2} + + rec := newRecordingLogger() + handler := newRecordingHandler() + s := &DKGSyncer{ + Log: rec, + Handler: handler.Handle, + resolveDKGContract: resolver, + bindDKGContract: func(addr common.Address) (dkgContractBackend, error) { + b, ok := backends[addr] + if !ok { + return nil, errors.Errorf("unknown DKG contract %s", addr.Hex()) + } + return b, nil + }, + getKeyperSetAddress: func(_ *bind.CallOpts, index uint64) (common.Address, error) { + addr, ok := keyperSets[index] + if !ok { + return common.Address{}, errors.Errorf("unknown keyper set index %d", index) + } + return addr, nil + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + s.runner = runner + s.startEventConsumer(ctx, runner) + + // numKnownKeyperSets starts at 0; a KeyperSetAdded event for index 2 implies + // indices 0 and 1 were missed and must be backfilled before processing 2. + ev := &bindings.KeyperSetManagerKeyperSetAdded{KeyperSetContract: ks2, Eon: 2} + ev.Raw.BlockNumber = 500 + s.handleKeyperSetAdded(ctx, ev) + + // All three DKG contracts are tracked: the two backfilled plus the + // triggering event's own contract. + tracked := s.trackedDKGContractList() + assert.Equal(t, 3, len(tracked)) + want := map[common.Address]struct{}{dkg0: {}, dkg1: {}, dkg2: {}} + for _, addr := range tracked { + _, ok := want[addr] + assert.Assert(t, ok, "unexpected tracked contract %s", addr.Hex()) + } + assert.Equal(t, uint64(3), s.getNumKnownKeyperSets(), + "count must advance past the triggering keyper set index") + + // Backfilled syncers and the triggering syncer all start from the event block. + assert.Equal(t, uint64(500), backend0.dealingStart, "backfilled syncer 0 must start at the triggering event block") + assert.Equal(t, uint64(500), backend1.dealingStart, "backfilled syncer 1 must start at the triggering event block") + assert.Equal(t, uint64(500), backend2.dealingStart, "triggering event syncer must start at its block") + + // Exactly one warning identifies the gap. + assert.Equal(t, 1, rec.warnCount(), "exactly one gap warning expected") + + // Each spawned syncer is live: an event from each backend reaches the handler. + backend0.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 0}) + backend1.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 1}) + backend2.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 2}) + got := map[uint64]bool{} + for i := 0; i < 3; i++ { + de, ok := handler.expectNextEvent(t).(*event.DealingEvent) + assert.Assert(t, ok, "expected *DealingEvent") + got[de.KeyperSetIndex] = true + } + assert.Assert(t, got[0] && got[1] && got[2], "events from all three syncers must be delivered") +} + +func TestHandleKeyperSetAddedAlreadySeenIndexLogsWarningAndSkips(t *testing.T) { + ksAddr := common.HexToAddress("0xaa") + dkgAddr := common.HexToAddress("0xbb") + backend := newFakeDKGBackend(dkgAddr) + resolver := stubResolver(map[common.Address]common.Address{ksAddr: dkgAddr}) + + rec := newRecordingLogger() + handler := newRecordingHandler() + s := &DKGSyncer{ + Log: rec, + Handler: handler.Handle, + resolveDKGContract: resolver, + bindDKGContract: func(common.Address) (dkgContractBackend, error) { + return backend, nil + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + s.runner = runner + s.startEventConsumer(ctx, runner) + + // Simulate three keyper sets already seen (non-zero addresses, no warnings). + s.tryTrack(common.HexToAddress("0xc0"), 0) + s.tryTrack(common.HexToAddress("0xc1"), 1) + s.tryTrack(common.HexToAddress("0xc2"), 2) + assert.Equal(t, uint64(3), s.getNumKnownKeyperSets()) + assert.Equal(t, 0, rec.warnCount()) + + // An event for an already-seen index (1 < 3) is ignored with a warning. + ev := &bindings.KeyperSetManagerKeyperSetAdded{KeyperSetContract: ksAddr, Eon: 1} + ev.Raw.BlockNumber = 700 + s.handleKeyperSetAdded(ctx, ev) + + assert.Equal(t, 1, rec.warnCount(), "an already-seen index must log exactly one warning") + assert.Equal(t, uint64(3), s.getNumKnownKeyperSets(), "count must be unchanged") + assert.Equal(t, uint64(0), backend.dealingStart, "already-seen index must not start a syncer") + for _, addr := range s.trackedDKGContractList() { + assert.Assert(t, addr != dkgAddr, "ignored event must not track its DKG contract") + } + + // No event is delivered for the ignored index. + select { + case ev := <-handler.ch: + t.Fatalf("already-seen index must not deliver any event, got %T", ev) + case <-time.After(100 * time.Millisecond): + } +} + +// gethLoggerInterface compile-time assertion: recordingLogger must satisfy the +// log.Logger interface so it can be wired into DKGSyncer.Log in real +// scenarios as well as in these tests. +var _ gethlog.Logger = (*recordingLogger)(nil) + +// fakeSubscription is a no-op gethevent.Subscription whose Err channel never +// fires and Unsubscribe is idempotent. Tests that need subscription errors +// to surface drive them via a separate channel. +type fakeSubscription struct { + errCh chan error + once sync.Once +} + +func newFakeSubscription() *fakeSubscription { + return &fakeSubscription{errCh: make(chan error, 1)} +} + +func (f *fakeSubscription) Err() <-chan error { return f.errCh } +func (f *fakeSubscription) Unsubscribe() { f.once.Do(func() { close(f.errCh) }) } + +// fakeDKGBackend is an in-memory dkgContractBackend that records the watch +// start block per event type and lets tests fire events by calling the +// emit* helpers. Subscription channels are captured on the first Watch call; +// tests should set up the syncer before emitting events. +type fakeDKGBackend struct { + addr common.Address + // succeededRetry may be nil; tests that do not care about the retry value + // leave it unset and every success reports retry 0. + succeeded map[uint64]bool + succeededRetry map[uint64]uint64 + + // watchErrs maps an event-type key ("dealing", "accusation", "apology", + // "successVote", "success") to an error the corresponding Watch* call should + // return, letting tests exercise subscription-setup failure. A missing key + // (the nil zero value) yields a working subscription. + watchErrs map[string]error + + mu sync.Mutex + dealingSink chan<- *dkgcontract.DkgcontractDealingSubmitted + accusationSink chan<- *dkgcontract.DkgcontractAccusationSubmitted + apologySink chan<- *dkgcontract.DkgcontractApologySubmitted + successVoteSink chan<- *dkgcontract.DkgcontractSuccessVoteSubmitted + successSink chan<- *dkgcontract.DkgcontractDKGSucceeded + + dealingStart uint64 + accusationStart uint64 + apologyStart uint64 + successVoteStart uint64 + successStart uint64 +} + +func newFakeDKGBackend(addr common.Address) *fakeDKGBackend { + return &fakeDKGBackend{addr: addr, succeeded: map[uint64]bool{}} +} + +func (b *fakeDKGBackend) Succeeded(_ *bind.CallOpts, ksi uint64) (bool, error) { + return b.succeeded[ksi], nil +} + +// SucceededAtRetry errors for keyper sets whose DKG did not succeed, matching +// the require(v != 0, "not succeeded") revert on chain. +func (b *fakeDKGBackend) SucceededAtRetry(_ *bind.CallOpts, ksi uint64) (uint64, error) { + if !b.succeeded[ksi] { + return 0, errors.New("not succeeded") + } + if b.succeededRetry == nil { + return 0, nil + } + return b.succeededRetry[ksi], nil +} + +func (b *fakeDKGBackend) WatchDealingSubmitted( + opts *bind.WatchOpts, sink chan<- *dkgcontract.DkgcontractDealingSubmitted, _, _, _ []uint64, +) (gethevent.Subscription, error) { + b.mu.Lock() + defer b.mu.Unlock() + if err := b.watchErrs["dealing"]; err != nil { + return nil, err + } + b.dealingSink = sink + if opts != nil && opts.Start != nil { + b.dealingStart = *opts.Start + } + return newFakeSubscription(), nil +} + +func (b *fakeDKGBackend) WatchAccusationSubmitted( + opts *bind.WatchOpts, sink chan<- *dkgcontract.DkgcontractAccusationSubmitted, _, _, _ []uint64, +) (gethevent.Subscription, error) { + b.mu.Lock() + defer b.mu.Unlock() + if err := b.watchErrs["accusation"]; err != nil { + return nil, err + } + b.accusationSink = sink + if opts != nil && opts.Start != nil { + b.accusationStart = *opts.Start + } + return newFakeSubscription(), nil +} + +func (b *fakeDKGBackend) WatchApologySubmitted( + opts *bind.WatchOpts, sink chan<- *dkgcontract.DkgcontractApologySubmitted, _, _, _ []uint64, +) (gethevent.Subscription, error) { + b.mu.Lock() + defer b.mu.Unlock() + if err := b.watchErrs["apology"]; err != nil { + return nil, err + } + b.apologySink = sink + if opts != nil && opts.Start != nil { + b.apologyStart = *opts.Start + } + return newFakeSubscription(), nil +} + +func (b *fakeDKGBackend) WatchSuccessVoteSubmitted( + opts *bind.WatchOpts, sink chan<- *dkgcontract.DkgcontractSuccessVoteSubmitted, _, _, _ []uint64, +) (gethevent.Subscription, error) { + b.mu.Lock() + defer b.mu.Unlock() + if err := b.watchErrs["successVote"]; err != nil { + return nil, err + } + b.successVoteSink = sink + if opts != nil && opts.Start != nil { + b.successVoteStart = *opts.Start + } + return newFakeSubscription(), nil +} + +func (b *fakeDKGBackend) WatchDKGSucceeded( + opts *bind.WatchOpts, sink chan<- *dkgcontract.DkgcontractDKGSucceeded, _, _ []uint64, +) (gethevent.Subscription, error) { + b.mu.Lock() + defer b.mu.Unlock() + if err := b.watchErrs["success"]; err != nil { + return nil, err + } + b.successSink = sink + if opts != nil && opts.Start != nil { + b.successStart = *opts.Start + } + return newFakeSubscription(), nil +} + +func (b *fakeDKGBackend) emitDealing(t *testing.T, ev *dkgcontract.DkgcontractDealingSubmitted) { + t.Helper() + b.mu.Lock() + sink := b.dealingSink + b.mu.Unlock() + assert.Assert(t, sink != nil, "dealing subscription not set up yet") + select { + case sink <- ev: + case <-time.After(time.Second): + t.Fatal("dealing channel blocked") + } +} + +func (b *fakeDKGBackend) emitSuccess(t *testing.T, ev *dkgcontract.DkgcontractDKGSucceeded) { + t.Helper() + b.mu.Lock() + sink := b.successSink + b.mu.Unlock() + assert.Assert(t, sink != nil, "success subscription not set up yet") + select { + case sink <- ev: + case <-time.After(time.Second): + t.Fatal("success channel blocked") + } +} + +func (b *fakeDKGBackend) emitAccusation(t *testing.T, ev *dkgcontract.DkgcontractAccusationSubmitted) { + t.Helper() + b.mu.Lock() + sink := b.accusationSink + b.mu.Unlock() + assert.Assert(t, sink != nil, "accusation subscription not set up yet") + select { + case sink <- ev: + case <-time.After(time.Second): + t.Fatal("accusation channel blocked") + } +} + +func (b *fakeDKGBackend) emitApology(t *testing.T, ev *dkgcontract.DkgcontractApologySubmitted) { + t.Helper() + b.mu.Lock() + sink := b.apologySink + b.mu.Unlock() + assert.Assert(t, sink != nil, "apology subscription not set up yet") + select { + case sink <- ev: + case <-time.After(time.Second): + t.Fatal("apology channel blocked") + } +} + +func (b *fakeDKGBackend) emitSuccessVote(t *testing.T, ev *dkgcontract.DkgcontractSuccessVoteSubmitted) { + t.Helper() + b.mu.Lock() + sink := b.successVoteSink + b.mu.Unlock() + assert.Assert(t, sink != nil, "success-vote subscription not set up yet") + select { + case sink <- ev: + case <-time.After(time.Second): + t.Fatal("success-vote channel blocked") + } +} + +// fakeRunner implements service.Runner for tests. Goroutines spawned via Go +// are tracked so the test can wait for them to drain after canceling its +// context. StartService starts each service synchronously under the runner's +// context, mirroring the real runner so that a DKGContractSyncer started by +// DKGSyncer comes up and observes context cancellation. +type fakeRunner struct { + ctx context.Context + wg sync.WaitGroup +} + +func newFakeRunner(ctx context.Context) *fakeRunner { return &fakeRunner{ctx: ctx} } + +func (r *fakeRunner) Go(f func() error) { + r.wg.Add(1) + go func() { + defer r.wg.Done() + _ = f() + }() +} + +func (r *fakeRunner) StartService(services ...service.Service) error { + for _, s := range services { + if err := s.Start(r.ctx, r); err != nil { + return err + } + } + return nil +} + +func (r *fakeRunner) Defer(_ func()) {} + +func (r *fakeRunner) Wait() { r.wg.Wait() } + +// recordingHandler delivers every DKG event the syncer hands it onto a +// buffered channel so tests can pull them off in order. +type recordingHandler struct { + ch chan event.DKGEvent +} + +func newRecordingHandler() *recordingHandler { + return &recordingHandler{ch: make(chan event.DKGEvent, 32)} +} + +func (h *recordingHandler) Handle(_ context.Context, ev event.DKGEvent) error { + h.ch <- ev + return nil +} + +// expectNextEvent waits up to the timeout for one more event and returns it. +func (h *recordingHandler) expectNextEvent(t *testing.T) event.DKGEvent { + t.Helper() + select { + case ev := <-h.ch: + return ev + case <-time.After(2 * time.Second): + t.Fatal("expected DKG event, got nothing") + return nil + } +} + +func newSubscriptionSyncer(t *testing.T, backends map[common.Address]*fakeDKGBackend) (*DKGSyncer, *recordingHandler) { + t.Helper() + handler := newRecordingHandler() + binder := func(addr common.Address) (dkgContractBackend, error) { + b, ok := backends[addr] + if !ok { + return nil, errors.Errorf("unknown DKG contract %s", addr.Hex()) + } + return b, nil + } + s := &DKGSyncer{ + Log: &logger.NoopLogger{}, + Handler: handler.Handle, + bindDKGContract: binder, + } + return s, handler +} + +func TestStartContractSubscriptionDeliversLiveDealingEvent(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + s, handler := newSubscriptionSyncer(t, map[common.Address]*fakeDKGBackend{addr: backend}) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + s.startEventConsumer(ctx, runner) + assert.NilError(t, s.startContractSyncer(ctx, runner, addr, 42)) + + backend.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{ + KeyperSetIndex: 7, + KeyperIndex: 3, + }) + + ev := handler.expectNextEvent(t) + de, ok := ev.(*event.DealingEvent) + assert.Assert(t, ok, "expected *DealingEvent, got %T", ev) + assert.Equal(t, uint64(7), de.KeyperSetIndex) + assert.Equal(t, uint64(3), de.KeyperIndex) +} + +func TestStartContractSubscriptionWatchStartIsRequestedBlock(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + s, _ := newSubscriptionSyncer(t, map[common.Address]*fakeDKGBackend{addr: backend}) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + s.startEventConsumer(ctx, runner) + assert.NilError(t, s.startContractSyncer(ctx, runner, addr, 4242)) + + assert.Equal(t, uint64(4242), backend.dealingStart, "DealingSubmitted watch must start from requested block") + assert.Equal(t, uint64(4242), backend.accusationStart) + assert.Equal(t, uint64(4242), backend.apologyStart) + assert.Equal(t, uint64(4242), backend.successVoteStart) + assert.Equal(t, uint64(4242), backend.successStart) +} + +func TestStartContractSubscriptionInitialSuccessesDeliveredBeforeLiveEvents(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + backend.succeeded[0] = true + backend.succeeded[2] = true + + s, handler := newSubscriptionSyncer(t, map[common.Address]*fakeDKGBackend{addr: backend}) + // tryTrack(addr, 2) advances numKnownKeyperSets to 3 so initialSuccessesForContract + // queries keyper set indices 0..2. Tracking addr itself is harmless here because + // the test calls startContractSyncer directly rather than via the tracked list. + s.tryTrack(addr, 2) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + s.startEventConsumer(ctx, runner) + assert.NilError(t, s.startContractSyncer(ctx, runner, addr, 100)) + + // Initial successes are enqueued before the live watcher starts, so with a + // single FIFO consumer reading the handler channel first must yield the + // SuccessEvents. + ev1 := handler.expectNextEvent(t) + _, ok := ev1.(*event.SuccessEvent) + assert.Assert(t, ok, "first delivered event must be initial *SuccessEvent, got %T", ev1) + + ev2 := handler.expectNextEvent(t) + _, ok = ev2.(*event.SuccessEvent) + assert.Assert(t, ok, "second delivered event must be initial *SuccessEvent, got %T", ev2) + + // Only after both initial successes is the live event emitted; it must + // arrive after them on the same channel. + backend.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 0, KeyperIndex: 1}) + ev3 := handler.expectNextEvent(t) + _, ok = ev3.(*event.DealingEvent) + assert.Assert(t, ok, "live event after initial successes must be *DealingEvent, got %T", ev3) +} + +// TestInitialSuccessesCarryRetryCounterFromSucceededAtRetry asserts that +// synthetic SuccessEvents emitted for already-completed DKGs at startup carry +// the RetryCounter the DKG actually succeeded at. +func TestInitialSuccessesCarryRetryCounterFromSucceededAtRetry(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + backend.succeeded[0] = true + backend.succeeded[2] = true + backend.succeededRetry = map[uint64]uint64{ + 0: 0, + 2: 3, + } + + s, handler := newSubscriptionSyncer(t, map[common.Address]*fakeDKGBackend{addr: backend}) + s.tryTrack(addr, 2) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + s.startEventConsumer(ctx, runner) + assert.NilError(t, s.startContractSyncer(ctx, runner, addr, 100)) + + got := map[uint64]uint64{} + for i := 0; i < 2; i++ { + ev := handler.expectNextEvent(t) + se, ok := ev.(*event.SuccessEvent) + assert.Assert(t, ok, "delivered event %d must be *SuccessEvent, got %T", i, ev) + got[se.KeyperSetIndex] = se.RetryCounter + } + assert.Equal(t, uint64(0), got[0], "ksi 0 succeeded at retry 0") + assert.Equal(t, uint64(3), got[2], "ksi 2 succeeded at retry 3") +} + +func TestStartViaTrackedListDeliversEventsFromTwoDistinctContracts(t *testing.T) { + addr0 := common.HexToAddress("0xa0") + addr1 := common.HexToAddress("0xa1") + backend0 := newFakeDKGBackend(addr0) + backend1 := newFakeDKGBackend(addr1) + + s, handler := newSubscriptionSyncer(t, map[common.Address]*fakeDKGBackend{ + addr0: backend0, + addr1: backend1, + }) + // Both addresses are pre-tracked, simulating the outcome of the initial + // scan -- this is the same state Start() leaves the syncer in before it + // spawns per-contract subscriptions. + s.tryTrack(addr0, 0) + s.tryTrack(addr1, 1) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + s.startEventConsumer(ctx, runner) + for _, addr := range s.trackedDKGContractList() { + assert.NilError(t, s.startContractSyncer(ctx, runner, addr, 100)) + } + + backend0.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 0, KeyperIndex: 1}) + backend1.emitSuccess(t, &dkgcontract.DkgcontractDKGSucceeded{KeyperSetIndex: 1, RetryCounter: 4}) + + got := map[uint64]bool{} + for i := 0; i < 2; i++ { + ev := handler.expectNextEvent(t) + switch e := ev.(type) { + case *event.DealingEvent: + got[e.KeyperSetIndex] = true + case *event.SuccessEvent: + got[e.KeyperSetIndex] = true + default: + t.Fatalf("unexpected event type %T", ev) + } + } + assert.Assert(t, got[0], "DealingEvent from contract 0 was not delivered") + assert.Assert(t, got[1], "SuccessEvent from contract 1 was not delivered") +} + +func TestSharedContractIsSubscribedExactlyOnce(t *testing.T) { + shared := common.HexToAddress("0xa0") + backend := newFakeDKGBackend(shared) + s, handler := newSubscriptionSyncer(t, map[common.Address]*fakeDKGBackend{shared: backend}) + s.tryTrack(shared, 0) + s.tryTrack(shared, 1) + s.tryTrack(shared, 2) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + s.startEventConsumer(ctx, runner) + // trackedDKGContractList must contain exactly one entry after dedup. + tracked := s.trackedDKGContractList() + assert.Equal(t, 1, len(tracked), "shared DKG contract must appear in tracked list exactly once") + for _, addr := range tracked { + assert.NilError(t, s.startContractSyncer(ctx, runner, addr, 100)) + } + + backend.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 0, KeyperIndex: 5}) + + // First delivery is captured. + ev := handler.expectNextEvent(t) + _, ok := ev.(*event.DealingEvent) + assert.Assert(t, ok) + + // No second delivery should follow (one goroutine, one event). + select { + case extra := <-handler.ch: + t.Fatalf("unexpected duplicate event delivery: %T", extra) + case <-time.After(100 * time.Millisecond): + } +} + +func TestHandleKeyperSetAddedSubscribesFromEventBlockNumber(t *testing.T) { + ksAddr := common.HexToAddress("0xaa") + dkgAddr := common.HexToAddress("0xbb") + backend := newFakeDKGBackend(dkgAddr) + resolver := stubResolver(map[common.Address]common.Address{ksAddr: dkgAddr}) + + handler := newRecordingHandler() + s := &DKGSyncer{ + Log: &logger.NoopLogger{}, + Handler: handler.Handle, + resolveDKGContract: resolver, + bindDKGContract: func(addr common.Address) (dkgContractBackend, error) { + if addr != dkgAddr { + return nil, errors.Errorf("unknown DKG contract %s", addr.Hex()) + } + return backend, nil + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + s.runner = runner + s.startEventConsumer(ctx, runner) + + // Eon 0 is the expected next index (numKnownKeyperSets starts at 0), so this + // exercises the normal no-gap path; only the subscription start block matters + // here. + ev := &bindings.KeyperSetManagerKeyperSetAdded{ + KeyperSetContract: ksAddr, + Eon: 0, + } + ev.Raw.BlockNumber = 1234 + + s.handleKeyperSetAdded(ctx, ev) + + assert.DeepEqual(t, []common.Address{dkgAddr}, s.trackedDKGContractList()) + assert.Equal(t, uint64(1234), backend.dealingStart, + "runtime subscription must start from the triggering KeyperSetAdded block number") + + // And the spawned goroutine should be live: delivering an event from the + // fake backend must reach the handler. + backend.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 2, KeyperIndex: 0}) + got := handler.expectNextEvent(t) + _, ok := got.(*event.DealingEvent) + assert.Assert(t, ok, "expected *DealingEvent from runtime-spawned subscription, got %T", got) +} + +func TestHandleKeyperSetAddedSkipsSubscriptionForExistingAddress(t *testing.T) { + ksAddr := common.HexToAddress("0xaa") + ksAddr2 := common.HexToAddress("0xab") + dkgAddr := common.HexToAddress("0xbb") + backend := newFakeDKGBackend(dkgAddr) + resolver := stubResolver(map[common.Address]common.Address{ksAddr: dkgAddr, ksAddr2: dkgAddr}) + + handler := newRecordingHandler() + s := &DKGSyncer{ + Log: &logger.NoopLogger{}, + Handler: handler.Handle, + resolveDKGContract: resolver, + bindDKGContract: func(_ common.Address) (dkgContractBackend, error) { + return backend, nil + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + s.runner = runner + s.startEventConsumer(ctx, runner) + + first := &bindings.KeyperSetManagerKeyperSetAdded{KeyperSetContract: ksAddr, Eon: 0} + first.Raw.BlockNumber = 100 + s.handleKeyperSetAdded(ctx, first) + firstStart := backend.dealingStart + assert.Equal(t, uint64(100), firstStart) + + // A second keyper set added at a later block but pointing at the same + // DKG contract must NOT re-subscribe and must not overwrite the + // existing subscription's start block in the fake backend. + second := &bindings.KeyperSetManagerKeyperSetAdded{KeyperSetContract: ksAddr2, Eon: 1} + second.Raw.BlockNumber = 200 + s.handleKeyperSetAdded(ctx, second) + + // Tracked set still has one entry; backend.dealingStart unchanged. + assert.DeepEqual(t, []common.Address{dkgAddr}, s.trackedDKGContractList()) + assert.Equal(t, firstStart, backend.dealingStart, + "second KeyperSetAdded for an already-tracked DKG contract must not re-subscribe") + // The count still advances past the second keyper set index even though no + // new syncer was started for the shared DKG contract. + assert.Equal(t, uint64(2), s.getNumKnownKeyperSets(), + "numKnownKeyperSets must advance past the second keyper set index") + + // And there should be only one live subscription goroutine -- emitting + // once delivers once. + backend.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 0, KeyperIndex: 0}) + _ = handler.expectNextEvent(t) + select { + case extra := <-handler.ch: + t.Fatalf("unexpected duplicate event delivery: %T", extra) + case <-time.After(100 * time.Millisecond): + } +} + +// serialisationProbe is a DKGEventHandler that detects whether the syncer ever +// invokes it concurrently. On entry it increments an active-call counter and +// records the maximum ever observed; the brief sleep widens the window during +// which an overlapping call would be visible. With the fan-in channel funneling +// every event through a single consumer, maxActive must stay at 1. +type serialisationProbe struct { + mu sync.Mutex + active int + maxActive int + delivered int + want int + done chan struct{} +} + +func newSerialisationProbe(want int) *serialisationProbe { + return &serialisationProbe{want: want, done: make(chan struct{})} +} + +func (p *serialisationProbe) Handle(_ context.Context, _ event.DKGEvent) error { + p.mu.Lock() + p.active++ + if p.active > p.maxActive { + p.maxActive = p.active + } + p.mu.Unlock() + + // Widen the window during which a concurrent invocation would be observed. + time.Sleep(2 * time.Millisecond) + + p.mu.Lock() + p.active-- + p.delivered++ + if p.delivered == p.want { + close(p.done) + } + p.mu.Unlock() + return nil +} + +// TestEventChannelSerialisesConcurrentHandlerCalls verifies that events produced +// concurrently by two independent DKGContractSyncer backends reach the real +// Handler one at a time: the fan-in channel and its single consumer goroutine +// must serialize all delivery so handler state needs no internal locking. +func TestEventChannelSerialisesConcurrentHandlerCalls(t *testing.T) { + addr0 := common.HexToAddress("0xa0") + addr1 := common.HexToAddress("0xa1") + backend0 := newFakeDKGBackend(addr0) + backend1 := newFakeDKGBackend(addr1) + + const perBackend = 5 + probe := newSerialisationProbe(2 * perBackend) + s := &DKGSyncer{ + Log: &logger.NoopLogger{}, + Handler: probe.Handle, + bindDKGContract: func(addr common.Address) (dkgContractBackend, error) { + switch addr { + case addr0: + return backend0, nil + case addr1: + return backend1, nil + } + return nil, errors.Errorf("unknown DKG contract %s", addr.Hex()) + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + s.startEventConsumer(ctx, runner) + assert.NilError(t, s.startContractSyncer(ctx, runner, addr0, 1)) + assert.NilError(t, s.startContractSyncer(ctx, runner, addr1, 1)) + + // Pre-fill both backends' buffered subscription channels so their two + // DKGContractSyncer goroutines drain and forward events concurrently. Without + // the fan-in channel they would call the handler at the same time. + for i := 0; i < perBackend; i++ { + //nolint:gosec // G115: loop counter is bounded by perBackend + backend0.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 0, KeyperIndex: uint64(i)}) + //nolint:gosec // G115: loop counter is bounded by perBackend + backend1.emitSuccess(t, &dkgcontract.DkgcontractDKGSucceeded{KeyperSetIndex: 1, RetryCounter: uint64(i)}) + } + + select { + case <-probe.done: + case <-time.After(5 * time.Second): + t.Fatal("handler did not receive all events") + } + + probe.mu.Lock() + defer probe.mu.Unlock() + assert.Equal(t, 2*perBackend, probe.delivered, "every emitted event must reach the handler") + assert.Equal(t, 1, probe.maxActive, "handler must never be invoked concurrently") +} + +// TestEventChannelInitialSuccessesPrecedeLiveEvents verifies the ordering +// guarantee the fan-in channel provides: a live event emitted while the initial +// successes for the same contract are still queued is still handled after them. +// The live event is emitted before any event is read from the handler, so only +// the FIFO consumer -- not synchronous delivery -- can be ordering them. +func TestEventChannelInitialSuccessesPrecedeLiveEvents(t *testing.T) { + addr := common.HexToAddress("0xaa") + backend := newFakeDKGBackend(addr) + backend.succeeded[0] = true + backend.succeeded[1] = true + + handler := newRecordingHandler() + s := &DKGSyncer{ + Log: &logger.NoopLogger{}, + Handler: handler.Handle, + bindDKGContract: func(a common.Address) (dkgContractBackend, error) { + if a != addr { + return nil, errors.Errorf("unknown DKG contract %s", a.Hex()) + } + return backend, nil + }, + } + // Two keyper set indices have succeeded, so initialSuccessesForContract + // synthesizes two SuccessEvents to enqueue ahead of the live watcher. + s.tryTrack(addr, 1) + + ctx, cancel := context.WithCancel(context.Background()) + runner := newFakeRunner(ctx) + t.Cleanup(func() { cancel(); runner.Wait() }) + + s.startEventConsumer(ctx, runner) + assert.NilError(t, s.startContractSyncer(ctx, runner, addr, 100)) + + // Emit a live event before reading anything: the initial successes were + // enqueued during startContractSyncer (before the live watcher existed), so + // FIFO ordering on the single channel must still hand them over first. + backend.emitDealing(t, &dkgcontract.DkgcontractDealingSubmitted{KeyperSetIndex: 0, KeyperIndex: 9}) + + first := handler.expectNextEvent(t) + _, ok := first.(*event.SuccessEvent) + assert.Assert(t, ok, "first delivered event must be an initial *SuccessEvent, got %T", first) + + second := handler.expectNextEvent(t) + _, ok = second.(*event.SuccessEvent) + assert.Assert(t, ok, "second delivered event must be an initial *SuccessEvent, got %T", second) + + third := handler.expectNextEvent(t) + _, ok = third.(*event.DealingEvent) + assert.Assert(t, ok, "the live event must be delivered after the initial successes, got %T", third) +} diff --git a/rolling-shutter/medley/chainsync/syncer/ecies.go b/rolling-shutter/medley/chainsync/syncer/ecies.go new file mode 100644 index 000000000..b8732842c --- /dev/null +++ b/rolling-shutter/medley/chainsync/syncer/ecies.go @@ -0,0 +1,148 @@ +package syncer + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/log" + "github.com/pkg/errors" + "github.com/shutter-network/contracts/v2/bindings/ecieskeyregistry" + + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/client" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/number" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" +) + +type ECIESKeySyncer struct { + Client client.Client + Contract *ecieskeyregistry.Ecieskeyregistry + Log log.Logger + StartBlock *number.BlockNumber + Handler event.ECIESKeyHandler + + keyRegisteredCh chan *ecieskeyregistry.EcieskeyregistryKeyRegistered +} + +func (s *ECIESKeySyncer) Start(ctx context.Context, runner service.Runner) error { + if s.Handler == nil { + return errors.New("no handler registered") + } + + // the latest block still has to be fixed. + // otherwise we could skip some block events + // between the initial poll and the subscription. + if s.StartBlock.IsLatest() { + latest, err := s.Client.BlockNumber(ctx) + if err != nil { + return err + } + s.StartBlock.SetUint64(latest) + } + + watchOpts := &bind.WatchOpts{ + Start: s.StartBlock.ToUInt64Ptr(), + Context: ctx, + } + initial, err := s.getInitialKeys(ctx) + if err != nil { + return err + } + for _, k := range initial { + err = s.Handler(ctx, k) + if err != nil { + s.Log.Error( + "handler for `ECIESKey` errored for initial sync", + "error", + err.Error(), + ) + } + } + + s.keyRegisteredCh = make(chan *ecieskeyregistry.EcieskeyregistryKeyRegistered, channelSize) + subs, err := s.Contract.WatchKeyRegistered(watchOpts, s.keyRegisteredCh, nil) + if err != nil { + return err + } + runner.Go(func() error { + err := s.watchNewKeys(ctx, subs.Err()) + if err != nil { + s.Log.Error("error watching new ECIES keys", err.Error()) + } + subs.Unsubscribe() + return err + }) + return nil +} + +// getInitialKeys iterates the registry's own keyper list and emits a synthetic +// ECIESKey event for every keyper whose key is already registered, so the +// local cache is populated before any live events arrive. +func (s *ECIESKeySyncer) getInitialKeys(ctx context.Context) ([]*event.ECIESKey, error) { + opts := &bind.CallOpts{ + Context: ctx, + BlockNumber: s.StartBlock.Int, + } + if err := guardCallOpts(opts, false); err != nil { + return nil, err + } + + count, err := s.Contract.GetKeyperCount(opts) + if err != nil { + return nil, errors.Wrap(err, "get keyper count") + } + + keys := []*event.ECIESKey{} + total := count.Uint64() + for i := uint64(0); i < total; i++ { + addr, err := s.Contract.GetKeyperAt(opts, new(big.Int).SetUint64(i)) + if err != nil { + return nil, errors.Wrapf(err, "get keyper at index %d", i) + } + key, err := s.Contract.GetKey(opts, addr) + if err != nil { + return nil, errors.Wrapf(err, "get ECIES key for %s", addr.Hex()) + } + if len(key) == 0 { + continue + } + keys = append(keys, &event.ECIESKey{ + Keyper: addr, + EciesPublicKey: key, + AtBlockNumber: number.BigToBlockNumber(opts.BlockNumber), + }) + } + return keys, nil +} + +func (s *ECIESKeySyncer) watchNewKeys(ctx context.Context, subsErr <-chan error) error { + for { + select { + case ev, ok := <-s.keyRegisteredCh: + if !ok { + return nil + } + bn := ev.Raw.BlockNumber + err := s.Handler(ctx, &event.ECIESKey{ + Keyper: ev.Keyper, + EciesPublicKey: ev.EciesPublicKey, + AtBlockNumber: number.NewBlockNumber(&bn), + }) + if err != nil { + s.Log.Error( + "handler for `ECIESKey` errored", + "error", + err.Error(), + ) + } + case err := <-subsErr: + if err != nil { + s.Log.Error("subscription error for watchNewKeys", err.Error()) + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} diff --git a/rolling-shutter/medley/chainsync/syncer/keyperset.go b/rolling-shutter/medley/chainsync/syncer/keyperset.go index 9daada16e..614ebf670 100644 --- a/rolling-shutter/medley/chainsync/syncer/keyperset.go +++ b/rolling-shutter/medley/chainsync/syncer/keyperset.go @@ -204,6 +204,7 @@ func (s *KeyperSetSyncer) newEvent( Members: members, Threshold: threshold, Eon: eon, + Contract: keyperSetContract, AtBlockNumber: number.BigToBlockNumber(opts.BlockNumber), }, nil } diff --git a/rolling-shutter/medley/testsetup/eon.go b/rolling-shutter/medley/testsetup/eon.go index 951b4d32d..450e6baf5 100644 --- a/rolling-shutter/medley/testsetup/eon.go +++ b/rolling-shutter/medley/testsetup/eon.go @@ -16,6 +16,7 @@ import ( "github.com/shutter-network/shutter/shlib/shcrypto" chainobsdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/collator" + obskeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/db" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/testkeygen" @@ -80,18 +81,19 @@ func InitializeEon( dkgResultEncoded, err := shdb.EncodePureDKGResult(&dkgResult) assert.NilError(tb, err) - err = keyperDB.InsertBatchConfig(ctx, database.InsertBatchConfigParams{ - KeyperConfigIndex: 1, - Height: 0, - Keypers: keypers, - Threshold: int32(eonKeys.Threshold), + obskeyperDB := obskeyperdb.New(dbpool) + err = obskeyperDB.InsertKeyperSet(ctx, obskeyperdb.InsertKeyperSetParams{ + KeyperConfigIndex: 1, + ActivationBlockNumber: 0, + Keypers: keypers, + Threshold: int32(eonKeys.Threshold), //nolint:gosec // G115: threshold is a small positive integer }) assert.NilError(tb, err) err = keyperDB.InsertEon(ctx, database.InsertEonParams{ Eon: int64(config.GetEon()), - Height: 0, ActivationBlockNumber: 0, KeyperConfigIndex: 1, + MaxRetries: 10, }) assert.NilError(tb, err) err = keyperDB.InsertDKGResult(ctx, database.InsertDKGResultParams{ diff --git a/rolling-shutter/migration-guide-dev.md b/rolling-shutter/migration-guide-dev.md index f51e30ab4..e6b55710d 100644 --- a/rolling-shutter/migration-guide-dev.md +++ b/rolling-shutter/migration-guide-dev.md @@ -33,11 +33,13 @@ Where `{keyper_name}` represents the specific keyper implementation (e.g., V{version_number}_{migration_description}.sql ``` +The version number must be zero-padded to 3 digits. + #### Examples: -- `V2_validatorRegistrations.sql` -- `V3_addUserTable.sql` -- `V4_updateIndexes.sql` +- `V002_validatorRegistrations.sql` +- `V003_addUserTable.sql` +- `V004_updateIndexes.sql` #### Version Number Rules: diff --git a/rolling-shutter/sandbox/testclient/testclient.go b/rolling-shutter/sandbox/testclient/testclient.go deleted file mode 100644 index de10b5a76..000000000 --- a/rolling-shutter/sandbox/testclient/testclient.go +++ /dev/null @@ -1,135 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/base64" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/kr/pretty" - "github.com/rs/zerolog/log" - abcitypes "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/rpc/client" - "github.com/tendermint/tendermint/rpc/client/http" - "github.com/tendermint/tendermint/types" - - "github.com/shutter-network/rolling-shutter/rolling-shutter/cmd/shversion" - "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/shutterevents" - "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" -) - -func makeMessage() *shmsg.MessageWithNonce { - msg := &shmsg.Message{ - Payload: &shmsg.Message_CheckIn{ - CheckIn: &shmsg.CheckIn{ - ValidatorPublicKey: bytes.Repeat([]byte("x"), 32), - EncryptionPublicKey: bytes.Repeat([]byte("y"), 33), - }, - }, - } - return &shmsg.MessageWithNonce{ - RandomNonce: uint64(0), - Msg: msg, - } -} - -func printEvents(events []abcitypes.Event, height int64) { - for _, ev := range events { - x, err := shutterevents.MakeEvent(ev, height) - if err != nil { - fmt.Println(err) - } else { - pretty.Println(ev.Type, "=>", x) - } - } -} - -func txsearch(cl client.Client) { - query := "shutter.batch-config.ActivationBlockNumber>=0" - page := 1 - perPage := 50 - res, err := cl.TxSearch(context.Background(), query, false, &page, &perPage, "") - if err != nil { - panic(err) - } - fmt.Println("transaction count", res.TotalCount) - for _, tx := range res.Txs { - fmt.Printf("=== tx height=%d\n", tx.Height) - printEvents(tx.TxResult.GetEvents(), tx.Height) - } -} - -func status(cl client.StatusClient) { - st, err := cl.Status(context.Background()) - if err != nil { - panic(err) - } - pretty.Println("Status:", st) -} - -func subscribe(cl client.Client) { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - query := "tx.height > 3" - - txs, err := cl.Subscribe(ctx, "test-client", query) - if err != nil { - panic(err) - } - - go func() { - for e := range txs { - d := e.Data.(types.EventDataTx) - events := d.TxResult.Result.Events - printEvents(events, d.Height) - } - }() - time.Sleep(time.Hour) - - privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19") - if err != nil { - panic(err) - } - msg := makeMessage() - signedMessage, err := shmsg.SignMessage(msg, privateKey) - if err != nil { - panic(err) - } - - tx := types.Tx(base64.RawURLEncoding.EncodeToString(signedMessage)) - res, err := cl.BroadcastTxCommit(context.Background(), tx) - - fmt.Println("Msg:", base64.RawURLEncoding.EncodeToString(signedMessage)) - - if err != nil { - panic(err) - } - pretty.Println("Res", res) -} - -func main() { - log.Info().Str("version", shversion.Version()).Msg("starting testclient") - - var cl client.Client - cl, err := http.New("http://localhost:26657", "/websocket") - if err != nil { - panic(err) - } - err = cl.Start() - if err != nil { - panic(err) - } - - defer func() { - err = cl.Stop() - if err != nil { - panic(err) - } - }() - - status(cl) - txsearch(cl) - subscribe(cl) -} diff --git a/rolling-shutter/shmsg/doc.go b/rolling-shutter/shmsg/doc.go deleted file mode 100644 index fea4c1514..000000000 --- a/rolling-shutter/shmsg/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package shmsg contains the protocol buffer defined messages and auto-generated code as well as -// some utility code to work with those 'shutter messages'. -package shmsg - -//go:generate protoc shmsg.proto --go_out=./ diff --git a/rolling-shutter/shmsg/encode.go b/rolling-shutter/shmsg/encode.go deleted file mode 100644 index caca1bb06..000000000 --- a/rolling-shutter/shmsg/encode.go +++ /dev/null @@ -1,70 +0,0 @@ -package shmsg - -import ( - "crypto/ecdsa" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/pkg/errors" - "golang.org/x/crypto/sha3" - "google.golang.org/protobuf/proto" -) - -// Instead of relying on protocol buffers we simply send a signature, followed by the marshaled message - -// Add a prefix to avoid accidentally signing data with special meaning in different context, in -// particular Ethereum transactions (c.f. EIP191 https://eips.ethereum.org/EIPS/eip-191). -var shmsgHashPrefix = []byte{0x19, 's', 'h', 'm', 's', 'g'} - -// SignMessage signs the given Message with the given private key. -func SignMessage(msg proto.Message, privkey *ecdsa.PrivateKey) ([]byte, error) { - marshaled, err := proto.Marshal(msg) - if err != nil { - return nil, err - } - hash := sha3.New256() - hash.Write(shmsgHashPrefix) - hash.Write(marshaled) - h := hash.Sum(nil) - signature, err := crypto.Sign(h, privkey) - if err != nil { - return nil, err - } - - return append(signature, marshaled...), nil -} - -// GetSigner returns the signer address of a signed message. -func GetSigner(signedMessage []byte) (common.Address, error) { - var signer common.Address - if len(signedMessage) < crypto.SignatureLength { - return signer, errors.New("message too short") - } - hash := sha3.New256() - hash.Write(shmsgHashPrefix) - hash.Write(signedMessage[crypto.SignatureLength:]) - h := hash.Sum(nil) - pubkey, err := crypto.SigToPub(h, signedMessage[:crypto.SignatureLength]) - if err != nil { - return signer, err - } - signer = crypto.PubkeyToAddress(*pubkey) - return signer, nil -} - -// GetMessage returns the unmarshalled Message of a signed message. -func GetMessage(signedMessage []byte) (*MessageWithNonce, error) { - msg := MessageWithNonce{} - if err := proto.Unmarshal(signedMessage[crypto.SignatureLength:], &msg); err != nil { - return nil, err - } - return &msg, nil -} - -func (m *Message) GobEncode() ([]byte, error) { - return proto.Marshal(m) -} - -func (m *Message) GobDecode(data []byte) error { - return proto.Unmarshal(data, m) -} diff --git a/rolling-shutter/shmsg/encode_test.go b/rolling-shutter/shmsg/encode_test.go deleted file mode 100644 index 244671d0f..000000000 --- a/rolling-shutter/shmsg/encode_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package shmsg - -import ( - "bytes" - "testing" - - "github.com/ethereum/go-ethereum/crypto" - "gotest.tools/v3/assert" -) - -func makeMessage() *MessageWithNonce { - msg := &Message{ - Payload: &Message_CheckIn{ - CheckIn: &CheckIn{ - ValidatorPublicKey: bytes.Repeat([]byte("x"), 32), - EncryptionPublicKey: bytes.Repeat([]byte("y"), 33), - }, - }, - } - msgWithNonce := &MessageWithNonce{ - Msg: msg, - RandomNonce: 123, - } - return msgWithNonce -} - -func TestSignMessage(t *testing.T) { - privateKey, err := crypto.GenerateKey() - address := crypto.PubkeyToAddress(privateKey.PublicKey) - - if err != nil { - t.Fatalf("fatal: %s", err) - } - signedMessage, err := SignMessage(makeMessage(), privateKey) - assert.NilError(t, err) - t.Logf("signed message size %d", len(signedMessage)) - signer, err := GetSigner(signedMessage) - if err != nil { - t.Fatalf("could not get signer: %s", err) - } - if signer != address { - t.Fatalf("wrong signer %s, expected %s", signer, address) - } - msg, err := GetMessage(signedMessage) - if err != nil { - t.Fatalf("could not get message: %s", err) - } - if msg.Msg.GetCheckIn() == nil { - t.Fatal("got no check in") - } -} diff --git a/rolling-shutter/shmsg/messages.go b/rolling-shutter/shmsg/messages.go deleted file mode 100644 index 956d91a32..000000000 --- a/rolling-shutter/shmsg/messages.go +++ /dev/null @@ -1,149 +0,0 @@ -package shmsg - -import ( - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - - shcrypto "github.com/shutter-network/shutter/shlib/shcrypto" -) - -// NewBatchConfig creates a new BatchConfig message. -func NewBatchConfig( - activationBlockNumber uint64, - keypers []common.Address, - threshold uint64, - keyperConfigIndex uint64, -) *Message { - var keypersBytes [][]byte - for _, k := range keypers { - keypersBytes = append(keypersBytes, k.Bytes()) - } - - return &Message{ - Payload: &Message_BatchConfig{ - BatchConfig: &BatchConfig{ - ActivationBlockNumber: activationBlockNumber, - Keypers: keypersBytes, - Threshold: threshold, - KeyperConfigIndex: keyperConfigIndex, - }, - }, - } -} - -// NewApology creates a new apology message used in the DKG process. This message reveals the -// polyEvals, that where sent encrypted via the PolyEval messages to each accuser. -func NewApology(eon uint64, accusers []common.Address, polyEvals []*big.Int) *Message { - if len(accusers) != len(polyEvals) { - panic("bad call to NewApology") - } - - var accusersBytes [][]byte - for _, a := range accusers { - accusersBytes = append(accusersBytes, a.Bytes()) - } - - var polyEvalsBytes [][]byte - for _, e := range polyEvals { - polyEvalsBytes = append(polyEvalsBytes, e.Bytes()) - } - - return &Message{ - Payload: &Message_Apology{ - Apology: &Apology{ - Eon: eon, - Accusers: accusersBytes, - PolyEvals: polyEvalsBytes, - }, - }, - } -} - -func NewAccusation(eon uint64, accused []common.Address) *Message { - accusedBytes := [][]byte{} - for _, a := range accused { - accusedBytes = append(accusedBytes, a.Bytes()) - } - return &Message{ - Payload: &Message_Accusation{ - Accusation: &Accusation{ - Eon: eon, - Accused: accusedBytes, - }, - }, - } -} - -// NewPolyCommitment creates a new poly commitment message containing gamma values. -func NewPolyCommitment(eon uint64, gammas *shcrypto.Gammas) *Message { - gammaBytes := [][]byte{} - for _, gamma := range *gammas { - gammaBytes = append(gammaBytes, gamma.Compress()) - } - - return &Message{ - Payload: &Message_PolyCommitment{ - PolyCommitment: &PolyCommitment{ - Eon: eon, - Gammas: gammaBytes, - }, - }, - } -} - -// NewPolyEval creates a new poly eval message. -func NewPolyEval(eon uint64, receivers []common.Address, encryptedEvals [][]byte) *Message { - rs := [][]byte{} - for _, receiver := range receivers { - rs = append(rs, receiver.Bytes()) - } - - return &Message{ - Payload: &Message_PolyEval{ - PolyEval: &PolyEval{ - Eon: eon, - Receivers: rs, - EncryptedEvals: encryptedEvals, - }, - }, - } -} - -func NewDKGResult(eon uint64, success bool) *Message { - return &Message{ - Payload: &Message_DkgResult{ - DkgResult: &DKGResult{ - Eon: eon, - Success: success, - }, - }, - } -} - -// NewBlockSeen creates a new BlockSeen message. The keypers send this when they see a new block on -// the main chain that possibly leads to starting a batch config. -func NewBlockSeen(blockNumber uint64) *Message { - return &Message{ - Payload: &Message_BlockSeen{ - BlockSeen: &BlockSeen{ - BlockNumber: blockNumber, - }, - }, - } -} - -// NewCheckIn creates a new CheckIn message. -func NewCheckIn(validatorPublicKey []byte, encryptionKey *ecies.PublicKey) *Message { - encryptionKeyECDSA := encryptionKey.ExportECDSA() - return &Message{ - Payload: &Message_CheckIn{ - CheckIn: &CheckIn{ - ValidatorPublicKey: validatorPublicKey, - EncryptionPublicKey: crypto.CompressPubkey(encryptionKeyECDSA), - }, - }, - } -} diff --git a/rolling-shutter/shmsg/messages_test.go b/rolling-shutter/shmsg/messages_test.go deleted file mode 100644 index ad90b1d8f..000000000 --- a/rolling-shutter/shmsg/messages_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package shmsg - -import ( - "crypto/rand" - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "gotest.tools/v3/assert" - - shcrypto "github.com/shutter-network/shutter/shlib/shcrypto" -) - -func TestNewPolyCommitmentMsg(t *testing.T) { - eon := uint64(10) - threshold := uint64(5) - poly, err := shcrypto.RandomPolynomial(rand.Reader, threshold) - assert.NilError(t, err) - gammas := poly.Gammas() - - msgContainer := NewPolyCommitment(eon, gammas) - msg := msgContainer.GetPolyCommitment() - assert.Assert(t, msg != nil) - - assert.Equal(t, eon, msg.Eon) - assert.Equal(t, int(threshold)+1, len(msg.Gammas)) - for i := 0; i < int(threshold)+1; i++ { - gammaBytes := msg.Gammas[i] - assert.DeepEqual(t, gammaBytes, (*gammas)[i].Compress()) - } -} - -func TestNewPolyEvalMsg(t *testing.T) { - eon := uint64(10) - receiver := common.BigToAddress(big.NewInt(0xaabbcc)) - encryptedEval := []byte("secret") - - msgContainer := NewPolyEval(eon, []common.Address{receiver}, [][]byte{encryptedEval}) - msg := msgContainer.GetPolyEval() - assert.Assert(t, msg != nil) - - assert.Equal(t, eon, msg.Eon) - assert.DeepEqual(t, receiver.Bytes(), msg.Receivers[0]) -} diff --git a/rolling-shutter/shmsg/shmsg.pb.go b/rolling-shutter/shmsg/shmsg.pb.go deleted file mode 100644 index 8ffa518eb..000000000 --- a/rolling-shutter/shmsg/shmsg.pb.go +++ /dev/null @@ -1,869 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.5 -// protoc v5.29.3 -// source: shmsg.proto - -package shmsg - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type BatchConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - ActivationBlockNumber uint64 `protobuf:"varint,1,opt,name=activation_block_number,json=activationBlockNumber,proto3" json:"activation_block_number,omitempty"` - Keypers [][]byte `protobuf:"bytes,2,rep,name=keypers,proto3" json:"keypers,omitempty"` - Threshold uint64 `protobuf:"varint,3,opt,name=threshold,proto3" json:"threshold,omitempty"` - KeyperConfigIndex uint64 `protobuf:"varint,5,opt,name=keyper_config_index,json=keyperConfigIndex,proto3" json:"keyper_config_index,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BatchConfig) Reset() { - *x = BatchConfig{} - mi := &file_shmsg_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BatchConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BatchConfig) ProtoMessage() {} - -func (x *BatchConfig) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use BatchConfig.ProtoReflect.Descriptor instead. -func (*BatchConfig) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{0} -} - -func (x *BatchConfig) GetActivationBlockNumber() uint64 { - if x != nil { - return x.ActivationBlockNumber - } - return 0 -} - -func (x *BatchConfig) GetKeypers() [][]byte { - if x != nil { - return x.Keypers - } - return nil -} - -func (x *BatchConfig) GetThreshold() uint64 { - if x != nil { - return x.Threshold - } - return 0 -} - -func (x *BatchConfig) GetKeyperConfigIndex() uint64 { - if x != nil { - return x.KeyperConfigIndex - } - return 0 -} - -type BlockSeen struct { - state protoimpl.MessageState `protogen:"open.v1"` - BlockNumber uint64 `protobuf:"varint,1,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BlockSeen) Reset() { - *x = BlockSeen{} - mi := &file_shmsg_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BlockSeen) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BlockSeen) ProtoMessage() {} - -func (x *BlockSeen) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use BlockSeen.ProtoReflect.Descriptor instead. -func (*BlockSeen) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{1} -} - -func (x *BlockSeen) GetBlockNumber() uint64 { - if x != nil { - return x.BlockNumber - } - return 0 -} - -type CheckIn struct { - state protoimpl.MessageState `protogen:"open.v1"` - ValidatorPublicKey []byte `protobuf:"bytes,1,opt,name=validator_public_key,json=validatorPublicKey,proto3" json:"validator_public_key,omitempty"` // 32 byte ed25519 public key - EncryptionPublicKey []byte `protobuf:"bytes,2,opt,name=encryption_public_key,json=encryptionPublicKey,proto3" json:"encryption_public_key,omitempty"` // compressed ecies public key - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CheckIn) Reset() { - *x = CheckIn{} - mi := &file_shmsg_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CheckIn) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CheckIn) ProtoMessage() {} - -func (x *CheckIn) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CheckIn.ProtoReflect.Descriptor instead. -func (*CheckIn) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{2} -} - -func (x *CheckIn) GetValidatorPublicKey() []byte { - if x != nil { - return x.ValidatorPublicKey - } - return nil -} - -func (x *CheckIn) GetEncryptionPublicKey() []byte { - if x != nil { - return x.EncryptionPublicKey - } - return nil -} - -type PolyEval struct { - state protoimpl.MessageState `protogen:"open.v1"` - Eon uint64 `protobuf:"varint,1,opt,name=eon,proto3" json:"eon,omitempty"` - Receivers [][]byte `protobuf:"bytes,2,rep,name=receivers,proto3" json:"receivers,omitempty"` - EncryptedEvals [][]byte `protobuf:"bytes,3,rep,name=encrypted_evals,json=encryptedEvals,proto3" json:"encrypted_evals,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PolyEval) Reset() { - *x = PolyEval{} - mi := &file_shmsg_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PolyEval) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PolyEval) ProtoMessage() {} - -func (x *PolyEval) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PolyEval.ProtoReflect.Descriptor instead. -func (*PolyEval) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{3} -} - -func (x *PolyEval) GetEon() uint64 { - if x != nil { - return x.Eon - } - return 0 -} - -func (x *PolyEval) GetReceivers() [][]byte { - if x != nil { - return x.Receivers - } - return nil -} - -func (x *PolyEval) GetEncryptedEvals() [][]byte { - if x != nil { - return x.EncryptedEvals - } - return nil -} - -type PolyCommitment struct { - state protoimpl.MessageState `protogen:"open.v1"` - Eon uint64 `protobuf:"varint,1,opt,name=eon,proto3" json:"eon,omitempty"` - Gammas [][]byte `protobuf:"bytes,2,rep,name=gammas,proto3" json:"gammas,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PolyCommitment) Reset() { - *x = PolyCommitment{} - mi := &file_shmsg_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PolyCommitment) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PolyCommitment) ProtoMessage() {} - -func (x *PolyCommitment) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PolyCommitment.ProtoReflect.Descriptor instead. -func (*PolyCommitment) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{4} -} - -func (x *PolyCommitment) GetEon() uint64 { - if x != nil { - return x.Eon - } - return 0 -} - -func (x *PolyCommitment) GetGammas() [][]byte { - if x != nil { - return x.Gammas - } - return nil -} - -type Accusation struct { - state protoimpl.MessageState `protogen:"open.v1"` - Eon uint64 `protobuf:"varint,1,opt,name=eon,proto3" json:"eon,omitempty"` - Accused [][]byte `protobuf:"bytes,2,rep,name=accused,proto3" json:"accused,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Accusation) Reset() { - *x = Accusation{} - mi := &file_shmsg_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Accusation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Accusation) ProtoMessage() {} - -func (x *Accusation) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Accusation.ProtoReflect.Descriptor instead. -func (*Accusation) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{5} -} - -func (x *Accusation) GetEon() uint64 { - if x != nil { - return x.Eon - } - return 0 -} - -func (x *Accusation) GetAccused() [][]byte { - if x != nil { - return x.Accused - } - return nil -} - -type Apology struct { - state protoimpl.MessageState `protogen:"open.v1"` - Eon uint64 `protobuf:"varint,1,opt,name=eon,proto3" json:"eon,omitempty"` - Accusers [][]byte `protobuf:"bytes,2,rep,name=accusers,proto3" json:"accusers,omitempty"` - PolyEvals [][]byte `protobuf:"bytes,3,rep,name=poly_evals,json=polyEvals,proto3" json:"poly_evals,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Apology) Reset() { - *x = Apology{} - mi := &file_shmsg_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Apology) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Apology) ProtoMessage() {} - -func (x *Apology) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Apology.ProtoReflect.Descriptor instead. -func (*Apology) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{6} -} - -func (x *Apology) GetEon() uint64 { - if x != nil { - return x.Eon - } - return 0 -} - -func (x *Apology) GetAccusers() [][]byte { - if x != nil { - return x.Accusers - } - return nil -} - -func (x *Apology) GetPolyEvals() [][]byte { - if x != nil { - return x.PolyEvals - } - return nil -} - -// DKGResult is sent by the keyper if the DKG process for an eon has -// finished. The field 'success' is used to signal whether the DKG process was -// successful. If the DKG process fails for a majority of keypers, the -// shuttermint app will restart the DKG process. This replaces the EonStartVote -// the keypers sent previously when the DKG process failed. -type DKGResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - Eon uint64 `protobuf:"varint,2,opt,name=eon,proto3" json:"eon,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DKGResult) Reset() { - *x = DKGResult{} - mi := &file_shmsg_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DKGResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DKGResult) ProtoMessage() {} - -func (x *DKGResult) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[7] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DKGResult.ProtoReflect.Descriptor instead. -func (*DKGResult) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{7} -} - -func (x *DKGResult) GetSuccess() bool { - if x != nil { - return x.Success - } - return false -} - -func (x *DKGResult) GetEon() uint64 { - if x != nil { - return x.Eon - } - return 0 -} - -type Message struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *Message_BatchConfig - // *Message_BlockSeen - // *Message_CheckIn - // *Message_PolyEval - // *Message_PolyCommitment - // *Message_Accusation - // *Message_Apology - // *Message_DkgResult - Payload isMessage_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Message) Reset() { - *x = Message{} - mi := &file_shmsg_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Message) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Message) ProtoMessage() {} - -func (x *Message) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[8] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Message.ProtoReflect.Descriptor instead. -func (*Message) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{8} -} - -func (x *Message) GetPayload() isMessage_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *Message) GetBatchConfig() *BatchConfig { - if x != nil { - if x, ok := x.Payload.(*Message_BatchConfig); ok { - return x.BatchConfig - } - } - return nil -} - -func (x *Message) GetBlockSeen() *BlockSeen { - if x != nil { - if x, ok := x.Payload.(*Message_BlockSeen); ok { - return x.BlockSeen - } - } - return nil -} - -func (x *Message) GetCheckIn() *CheckIn { - if x != nil { - if x, ok := x.Payload.(*Message_CheckIn); ok { - return x.CheckIn - } - } - return nil -} - -func (x *Message) GetPolyEval() *PolyEval { - if x != nil { - if x, ok := x.Payload.(*Message_PolyEval); ok { - return x.PolyEval - } - } - return nil -} - -func (x *Message) GetPolyCommitment() *PolyCommitment { - if x != nil { - if x, ok := x.Payload.(*Message_PolyCommitment); ok { - return x.PolyCommitment - } - } - return nil -} - -func (x *Message) GetAccusation() *Accusation { - if x != nil { - if x, ok := x.Payload.(*Message_Accusation); ok { - return x.Accusation - } - } - return nil -} - -func (x *Message) GetApology() *Apology { - if x != nil { - if x, ok := x.Payload.(*Message_Apology); ok { - return x.Apology - } - } - return nil -} - -func (x *Message) GetDkgResult() *DKGResult { - if x != nil { - if x, ok := x.Payload.(*Message_DkgResult); ok { - return x.DkgResult - } - } - return nil -} - -type isMessage_Payload interface { - isMessage_Payload() -} - -type Message_BatchConfig struct { - BatchConfig *BatchConfig `protobuf:"bytes,4,opt,name=batch_config,json=batchConfig,proto3,oneof"` -} - -type Message_BlockSeen struct { - // BatchConfigStarted batch_config_started = 6; - BlockSeen *BlockSeen `protobuf:"bytes,14,opt,name=block_seen,json=blockSeen,proto3,oneof"` -} - -type Message_CheckIn struct { - CheckIn *CheckIn `protobuf:"bytes,7,opt,name=check_in,json=checkIn,proto3,oneof"` -} - -type Message_PolyEval struct { - // DKG messages - PolyEval *PolyEval `protobuf:"bytes,9,opt,name=poly_eval,json=polyEval,proto3,oneof"` -} - -type Message_PolyCommitment struct { - PolyCommitment *PolyCommitment `protobuf:"bytes,10,opt,name=poly_commitment,json=polyCommitment,proto3,oneof"` -} - -type Message_Accusation struct { - Accusation *Accusation `protobuf:"bytes,11,opt,name=accusation,proto3,oneof"` -} - -type Message_Apology struct { - Apology *Apology `protobuf:"bytes,12,opt,name=apology,proto3,oneof"` -} - -type Message_DkgResult struct { - DkgResult *DKGResult `protobuf:"bytes,15,opt,name=dkg_result,json=dkgResult,proto3,oneof"` // EonStartVote eon_start_vote = 13; -} - -func (*Message_BatchConfig) isMessage_Payload() {} - -func (*Message_BlockSeen) isMessage_Payload() {} - -func (*Message_CheckIn) isMessage_Payload() {} - -func (*Message_PolyEval) isMessage_Payload() {} - -func (*Message_PolyCommitment) isMessage_Payload() {} - -func (*Message_Accusation) isMessage_Payload() {} - -func (*Message_Apology) isMessage_Payload() {} - -func (*Message_DkgResult) isMessage_Payload() {} - -type MessageWithNonce struct { - state protoimpl.MessageState `protogen:"open.v1"` - Msg *Message `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` - ChainId []byte `protobuf:"bytes,2,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` - RandomNonce uint64 `protobuf:"varint,3,opt,name=random_nonce,json=randomNonce,proto3" json:"random_nonce,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *MessageWithNonce) Reset() { - *x = MessageWithNonce{} - mi := &file_shmsg_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *MessageWithNonce) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MessageWithNonce) ProtoMessage() {} - -func (x *MessageWithNonce) ProtoReflect() protoreflect.Message { - mi := &file_shmsg_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MessageWithNonce.ProtoReflect.Descriptor instead. -func (*MessageWithNonce) Descriptor() ([]byte, []int) { - return file_shmsg_proto_rawDescGZIP(), []int{9} -} - -func (x *MessageWithNonce) GetMsg() *Message { - if x != nil { - return x.Msg - } - return nil -} - -func (x *MessageWithNonce) GetChainId() []byte { - if x != nil { - return x.ChainId - } - return nil -} - -func (x *MessageWithNonce) GetRandomNonce() uint64 { - if x != nil { - return x.RandomNonce - } - return 0 -} - -var File_shmsg_proto protoreflect.FileDescriptor - -var file_shmsg_proto_rawDesc = string([]byte{ - 0x0a, 0x0b, 0x73, 0x68, 0x6d, 0x73, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x73, - 0x68, 0x6d, 0x73, 0x67, 0x22, 0xad, 0x01, 0x0a, 0x0b, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x15, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, - 0x6b, 0x65, 0x79, 0x70, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x07, 0x6b, - 0x65, 0x79, 0x70, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, - 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, - 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x2e, 0x0a, 0x13, 0x6b, 0x65, 0x79, 0x70, 0x65, 0x72, 0x5f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x11, 0x6b, 0x65, 0x79, 0x70, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x22, 0x2e, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x65, 0x65, - 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, - 0x6d, 0x62, 0x65, 0x72, 0x22, 0x6f, 0x0a, 0x07, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x6e, 0x12, - 0x30, 0x0a, 0x14, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x70, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, - 0x79, 0x12, 0x32, 0x0a, 0x15, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x13, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x63, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x79, 0x45, 0x76, 0x61, - 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, - 0x65, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x09, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, - 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x65, - 0x76, 0x61, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0e, 0x65, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x45, 0x76, 0x61, 0x6c, 0x73, 0x22, 0x3a, 0x0a, 0x0e, 0x50, 0x6f, - 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x65, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x65, 0x6f, 0x6e, 0x12, 0x16, - 0x0a, 0x06, 0x67, 0x61, 0x6d, 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x06, - 0x67, 0x61, 0x6d, 0x6d, 0x61, 0x73, 0x22, 0x38, 0x0a, 0x0a, 0x41, 0x63, 0x63, 0x75, 0x73, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x03, 0x65, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x75, 0x73, 0x65, - 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x63, 0x63, 0x75, 0x73, 0x65, 0x64, - 0x22, 0x56, 0x0a, 0x07, 0x41, 0x70, 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x65, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x65, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, - 0x08, 0x61, 0x63, 0x63, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, - 0x08, 0x61, 0x63, 0x63, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x6f, 0x6c, - 0x79, 0x5f, 0x65, 0x76, 0x61, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x09, 0x70, - 0x6f, 0x6c, 0x79, 0x45, 0x76, 0x61, 0x6c, 0x73, 0x22, 0x37, 0x0a, 0x09, 0x44, 0x4b, 0x47, 0x52, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, - 0x10, 0x0a, 0x03, 0x65, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x65, 0x6f, - 0x6e, 0x22, 0xb3, 0x03, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x37, 0x0a, - 0x0c, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x73, 0x68, 0x6d, 0x73, 0x67, 0x2e, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x0b, 0x62, 0x61, 0x74, 0x63, 0x68, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, - 0x73, 0x65, 0x65, 0x6e, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x73, 0x68, 0x6d, - 0x73, 0x67, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x65, 0x65, 0x6e, 0x48, 0x00, 0x52, 0x09, - 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x08, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x73, 0x68, - 0x6d, 0x73, 0x67, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x6e, 0x48, 0x00, 0x52, 0x07, 0x63, - 0x68, 0x65, 0x63, 0x6b, 0x49, 0x6e, 0x12, 0x2e, 0x0a, 0x09, 0x70, 0x6f, 0x6c, 0x79, 0x5f, 0x65, - 0x76, 0x61, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x73, 0x68, 0x6d, 0x73, - 0x67, 0x2e, 0x50, 0x6f, 0x6c, 0x79, 0x45, 0x76, 0x61, 0x6c, 0x48, 0x00, 0x52, 0x08, 0x70, 0x6f, - 0x6c, 0x79, 0x45, 0x76, 0x61, 0x6c, 0x12, 0x40, 0x0a, 0x0f, 0x70, 0x6f, 0x6c, 0x79, 0x5f, 0x63, - 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x73, 0x68, 0x6d, 0x73, 0x67, 0x2e, 0x50, 0x6f, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x6f, 0x6c, 0x79, 0x43, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x33, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x75, - 0x73, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x73, - 0x68, 0x6d, 0x73, 0x67, 0x2e, 0x41, 0x63, 0x63, 0x75, 0x73, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, - 0x00, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x75, 0x73, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x0a, - 0x07, 0x61, 0x70, 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x73, 0x68, 0x6d, 0x73, 0x67, 0x2e, 0x41, 0x70, 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x48, 0x00, - 0x52, 0x07, 0x61, 0x70, 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x12, 0x31, 0x0a, 0x0a, 0x64, 0x6b, 0x67, - 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, - 0x73, 0x68, 0x6d, 0x73, 0x67, 0x2e, 0x44, 0x4b, 0x47, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, - 0x00, 0x52, 0x09, 0x64, 0x6b, 0x67, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x42, 0x09, 0x0a, 0x07, - 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x72, 0x0a, 0x10, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x57, 0x69, 0x74, 0x68, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x03, 0x6d, - 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x73, 0x68, 0x6d, 0x73, 0x67, - 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x19, 0x0a, - 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x61, 0x6e, 0x64, - 0x6f, 0x6d, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, - 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x42, 0x0a, 0x5a, 0x08, 0x2e, - 0x2f, 0x3b, 0x73, 0x68, 0x6d, 0x73, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -}) - -var ( - file_shmsg_proto_rawDescOnce sync.Once - file_shmsg_proto_rawDescData []byte -) - -func file_shmsg_proto_rawDescGZIP() []byte { - file_shmsg_proto_rawDescOnce.Do(func() { - file_shmsg_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_shmsg_proto_rawDesc), len(file_shmsg_proto_rawDesc))) - }) - return file_shmsg_proto_rawDescData -} - -var file_shmsg_proto_msgTypes = make([]protoimpl.MessageInfo, 10) -var file_shmsg_proto_goTypes = []any{ - (*BatchConfig)(nil), // 0: shmsg.BatchConfig - (*BlockSeen)(nil), // 1: shmsg.BlockSeen - (*CheckIn)(nil), // 2: shmsg.CheckIn - (*PolyEval)(nil), // 3: shmsg.PolyEval - (*PolyCommitment)(nil), // 4: shmsg.PolyCommitment - (*Accusation)(nil), // 5: shmsg.Accusation - (*Apology)(nil), // 6: shmsg.Apology - (*DKGResult)(nil), // 7: shmsg.DKGResult - (*Message)(nil), // 8: shmsg.Message - (*MessageWithNonce)(nil), // 9: shmsg.MessageWithNonce -} -var file_shmsg_proto_depIdxs = []int32{ - 0, // 0: shmsg.Message.batch_config:type_name -> shmsg.BatchConfig - 1, // 1: shmsg.Message.block_seen:type_name -> shmsg.BlockSeen - 2, // 2: shmsg.Message.check_in:type_name -> shmsg.CheckIn - 3, // 3: shmsg.Message.poly_eval:type_name -> shmsg.PolyEval - 4, // 4: shmsg.Message.poly_commitment:type_name -> shmsg.PolyCommitment - 5, // 5: shmsg.Message.accusation:type_name -> shmsg.Accusation - 6, // 6: shmsg.Message.apology:type_name -> shmsg.Apology - 7, // 7: shmsg.Message.dkg_result:type_name -> shmsg.DKGResult - 8, // 8: shmsg.MessageWithNonce.msg:type_name -> shmsg.Message - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name -} - -func init() { file_shmsg_proto_init() } -func file_shmsg_proto_init() { - if File_shmsg_proto != nil { - return - } - file_shmsg_proto_msgTypes[8].OneofWrappers = []any{ - (*Message_BatchConfig)(nil), - (*Message_BlockSeen)(nil), - (*Message_CheckIn)(nil), - (*Message_PolyEval)(nil), - (*Message_PolyCommitment)(nil), - (*Message_Accusation)(nil), - (*Message_Apology)(nil), - (*Message_DkgResult)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_shmsg_proto_rawDesc), len(file_shmsg_proto_rawDesc)), - NumEnums: 0, - NumMessages: 10, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_shmsg_proto_goTypes, - DependencyIndexes: file_shmsg_proto_depIdxs, - MessageInfos: file_shmsg_proto_msgTypes, - }.Build() - File_shmsg_proto = out.File - file_shmsg_proto_goTypes = nil - file_shmsg_proto_depIdxs = nil -} diff --git a/rolling-shutter/shmsg/shmsg.proto b/rolling-shutter/shmsg/shmsg.proto deleted file mode 100644 index e90ce5ee8..000000000 --- a/rolling-shutter/shmsg/shmsg.proto +++ /dev/null @@ -1,77 +0,0 @@ -syntax = "proto3"; -package shmsg; - -option go_package = "./;shmsg"; - -message BatchConfig { - uint64 activation_block_number = 1; - repeated bytes keypers = 2; - uint64 threshold = 3; - uint64 keyper_config_index = 5; -} - -message BlockSeen { - uint64 block_number = 1; -} - -message CheckIn { - bytes validator_public_key = 1;// 32 byte ed25519 public key - bytes encryption_public_key = 2;// compressed ecies public key -} - - -message PolyEval { - uint64 eon = 1; - repeated bytes receivers = 2; - repeated bytes encrypted_evals = 3; -} - -message PolyCommitment { - uint64 eon = 1; - repeated bytes gammas = 2; -} - -message Accusation { - uint64 eon = 1; - repeated bytes accused = 2; -} - -message Apology { - uint64 eon = 1; - repeated bytes accusers = 2; - repeated bytes poly_evals = 3; -} - -// DKGResult is sent by the keyper if the DKG process for an eon has -// finished. The field 'success' is used to signal whether the DKG process was -// successful. If the DKG process fails for a majority of keypers, the -// shuttermint app will restart the DKG process. This replaces the EonStartVote -// the keypers sent previously when the DKG process failed. -message DKGResult { - bool success = 1; - uint64 eon = 2; -} - -message Message { - oneof payload { - BatchConfig batch_config = 4; - // BatchConfigStarted batch_config_started = 6; - BlockSeen block_seen = 14; - CheckIn check_in = 7; - - // DKG messages - PolyEval poly_eval = 9; - PolyCommitment poly_commitment = 10; - Accusation accusation = 11; - Apology apology = 12; - - DKGResult dkg_result = 15; - // EonStartVote eon_start_vote = 13; - } -} - -message MessageWithNonce { - Message msg = 1; - bytes chain_id = 2; - uint64 random_nonce = 3; -} diff --git a/rolling-shutter/txsender/txsender.go b/rolling-shutter/txsender/txsender.go new file mode 100644 index 000000000..e598faa06 --- /dev/null +++ b/rolling-shutter/txsender/txsender.go @@ -0,0 +1,391 @@ +// Package txsender implements a database-backed transaction outbox sender. +// +// The DKG participation loop (and any other producer) writes a pending row to +// the `tx_outbox` table with ABI-packed calldata, in the same database +// transaction as its business state. TxSender polls those rows, signs and +// submits them to the chain, then watches for receipts and updates the row +// status accordingly. +// +// Status lifecycle: pending -> submitted -> confirmed | failed. +package txsender + +import ( + "context" + "crypto/ecdsa" + "database/sql" + "io" + "math/big" + "net" + "syscall" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/jackc/pgtype" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" +) + +// Client is the subset of go-ethereum's ethclient.Client functionality that +// TxSender depends on. Kept narrow so tests can swap in a fake. +type Client interface { + ChainID(ctx context.Context) (*big.Int, error) + PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) + SuggestGasTipCap(ctx context.Context) (*big.Int, error) + HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) + EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) + SendTransaction(ctx context.Context, tx *types.Transaction) error + TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) +} + +// Config carries the dependencies needed to run TxSender. +type Config struct { + DBPool *pgxpool.Pool + Client Client + PrivateKey *ecdsa.PrivateKey + PollInterval time.Duration +} + +// TxSender is a service that drains the `tx_outbox` table and reports +// receipts back to it. It runs a single poll loop that, on each tick, first +// submits all pending rows in id order, then checks all submitted rows for +// receipts. +type TxSender struct { + cfg Config + address common.Address + chainID *big.Int +} + +// New constructs a TxSender. It does not perform any network calls; chain +// state (chain ID, nonce) is fetched lazily on first send. +func New(cfg Config) *TxSender { + if cfg.PollInterval == 0 { + cfg.PollInterval = 1 * time.Second + } + addr := crypto.PubkeyToAddress(cfg.PrivateKey.PublicKey) + return &TxSender{cfg: cfg, address: addr} +} + +// Start implements service.Service. A single poll loop runs both phases +// sequentially per tick: submit pending rows, then check submitted rows. +func (s *TxSender) Start(ctx context.Context, runner service.Runner) error { + chainID, err := s.cfg.Client.ChainID(ctx) + if err != nil { + return errors.Wrap(err, "tx_outbox sender: read chain id") + } + s.chainID = chainID + log.Info(). + Str("from", s.address.Hex()). + Str("chain-id", chainID.String()). + Dur("poll-interval", s.cfg.PollInterval). + Msg("starting tx outbox sender") + + runner.Go(func() error { return s.pollLoop(ctx) }) + return nil +} + +func (s *TxSender) pollLoop(ctx context.Context) error { + ticker := time.NewTicker(s.cfg.PollInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + s.poll(ctx) + } + } +} + +// poll runs one iteration of the sender: it first submits all pending rows, +// then checks all submitted rows for receipts. The phases are sequential so +// that a row enqueued and submitted within the same tick can also be checked +// for a receipt on the next tick without ordering surprises. +func (s *TxSender) poll(ctx context.Context) { + if err := s.processPending(ctx); err != nil { + log.Error().Err(err).Msg("tx outbox: submit phase failed") + } + if err := s.processSubmitted(ctx); err != nil { + log.Error().Err(err).Msg("tx outbox: confirm phase failed") + } +} + +// processPending picks up every pending row in id order and submits it. Rows +// are processed sequentially because nonce ordering matters; if one row's +// submission fails the caller is left to inspect the row's `error` column. +func (s *TxSender) processPending(ctx context.Context) error { + queries := corekeyperdb.New(s.cfg.DBPool) + rows, err := queries.GetPendingTxs(ctx) + if err != nil { + return errors.Wrap(err, "list pending tx_outbox rows") + } + for _, row := range rows { + if err := ctx.Err(); err != nil { + return err + } + s.submitRow(ctx, row) + } + return nil +} + +// submitRow signs and submits the transaction for a single outbox row. All +// errors are recorded against the row itself; the loop never aborts because +// of one bad row. +func (s *TxSender) submitRow(ctx context.Context, row corekeyperdb.TxOutbox) { + queries := corekeyperdb.New(s.cfg.DBPool) + + value, err := numericToBigInt(row.Value) + if err != nil { + s.markFailed(ctx, row.ID, row.Label, errors.Wrap(err, "decode tx_outbox value")) + return + } + to := common.HexToAddress(row.ToAddress) + + nonce, err := s.cfg.Client.PendingNonceAt(ctx, s.address) + if err != nil { + // Transient network error — leave the row pending so the next tick + // retries. + log.Warn().Err(err).Int64("id", row.ID).Str("label", row.Label).Msg("tx outbox: read nonce") + return + } + gasEstimate, err := s.cfg.Client.EstimateGas(ctx, ethereum.CallMsg{ + From: s.address, + To: &to, + Value: value, + Data: row.Data, + }) + if err != nil { + s.markFailedUnlessTransient(ctx, row.ID, row.Label, errors.Wrap(err, "estimate gas")) + return + } + // Multiply gas estimate to provide headroom for state changing between now + // and execution. If customization is needed in the future, the multiplier + // could become a optional column in TxOutbox + gasLimit := gasEstimate * 2 + tipCap, err := s.cfg.Client.SuggestGasTipCap(ctx) + if err != nil { + log.Warn().Err(err).Int64("id", row.ID).Str("label", row.Label).Msg("tx outbox: suggest gas tip cap") + return + } + latestHeader, err := s.cfg.Client.HeaderByNumber(ctx, nil) + if err != nil { + log.Warn().Err(err).Int64("id", row.ID).Str("label", row.Label).Msg("tx outbox: read latest header") + return + } + if latestHeader.BaseFee == nil { + s.markFailed(ctx, row.ID, row.Label, errors.New("latest header has no base fee (non-EIP-1559 chain)")) + return + } + // GasFeeCap = 2 * baseFee + tipCap. The 2x multiplier provides headroom + // for base-fee growth across a few blocks of inclusion delay. + feeCap := new(big.Int).Mul(latestHeader.BaseFee, big.NewInt(2)) + feeCap.Add(feeCap, tipCap) + + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: s.chainID, + Nonce: nonce, + GasTipCap: tipCap, + GasFeeCap: feeCap, + Gas: gasLimit, + To: &to, + Value: value, + Data: row.Data, + }) + signed, err := types.SignTx(tx, types.LatestSignerForChainID(s.chainID), s.cfg.PrivateKey) + if err != nil { + s.markFailed(ctx, row.ID, row.Label, errors.Wrap(err, "sign transaction")) + return + } + + // Mark the row submitted BEFORE broadcasting. If the process crashes + // between this update and the SendTransaction call, the row stays + // `submitted` with its tx hash stored; the confirm loop then polls for the + // receipt. This is preferable to the inverse ordering (send first, mark + // after), which would leave a crashed row as `pending` and cause a + // duplicate broadcast on restart with a fresh nonce. + if err := queries.MarkTxSubmitted(ctx, corekeyperdb.MarkTxSubmittedParams{ + ID: row.ID, + TxHash: sql.NullString{String: signed.Hash().Hex(), Valid: true}, + Nonce: sql.NullInt64{Int64: int64(nonce), Valid: true}, //nolint:gosec // G115: Ethereum nonces fit well within int64 + }); err != nil { + log.Error().Err(err). + Int64("id", row.ID). + Str("label", row.Label). + Str("tx-hash", signed.Hash().Hex()). + Msg("tx outbox: mark submitted failed; skipping send") + return + } + + if err := s.cfg.Client.SendTransaction(ctx, signed); err != nil { + s.markFailedUnlessTransient(ctx, row.ID, row.Label, errors.Wrap(err, "send transaction")) + return + } + + log.Info(). + Int64("id", row.ID). + Str("label", row.Label). + Str("to", row.ToAddress). + Str("tx-hash", signed.Hash().Hex()). + Uint64("nonce", nonce). + Msg("tx outbox: submitted") +} + +// processSubmitted polls every submitted row for a receipt. Rows without a +// receipt yet are left alone for the next tick. +func (s *TxSender) processSubmitted(ctx context.Context) error { + queries := corekeyperdb.New(s.cfg.DBPool) + rows, err := queries.GetSubmittedTxs(ctx) + if err != nil { + return errors.Wrap(err, "list submitted tx_outbox rows") + } + for _, row := range rows { + if err := ctx.Err(); err != nil { + return err + } + s.checkReceipt(ctx, row) + } + return nil +} + +func (s *TxSender) checkReceipt(ctx context.Context, row corekeyperdb.TxOutbox) { + if !row.TxHash.Valid { + // Submitted without a tx hash is a bug — flag it and stop polling. + s.markFailed(ctx, row.ID, row.Label, errors.New("submitted row has no tx_hash")) + return + } + queries := corekeyperdb.New(s.cfg.DBPool) + hash := common.HexToHash(row.TxHash.String) + receipt, err := s.cfg.Client.TransactionReceipt(ctx, hash) + if err != nil { + if errors.Is(err, ethereum.NotFound) { + return + } + log.Warn().Err(err).Int64("id", row.ID).Str("label", row.Label).Str("tx-hash", row.TxHash.String). + Msg("tx outbox: fetch receipt") + return + } + if receipt.Status == types.ReceiptStatusSuccessful { + if err := queries.MarkTxConfirmed(ctx, row.ID); err != nil { + log.Error().Err(err).Int64("id", row.ID).Str("label", row.Label).Msg("tx outbox: mark confirmed") + return + } + log.Info().Int64("id", row.ID).Str("label", row.Label).Str("tx-hash", row.TxHash.String). + Msg("tx outbox: confirmed") + return + } + + s.markFailed(ctx, row.ID, row.Label, errors.Errorf("tx receipt status %d", receipt.Status)) +} + +// isTransient reports whether err is a known transient chain-client failure +// that warrants leaving the outbox row for the next tick rather than terminally +// failing it. It is intentionally a positive allowlist: unknown errors fall +// through and are treated as terminal by callers. Other transient failures +// (e.g. HTTP 5xx from the RPC endpoint) may exist and are not covered here; +// extend the list if a class of transient error is observed terminally failing +// rows in production. +func isTransient(err error) bool { + if err == nil { + return false + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return true + } + var errno syscall.Errno + if errors.As(err, &errno) { + return true + } + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + return true + } + var netErr net.Error + if errors.As(err, &netErr) { + return true + } + return false +} + +// markFailedUnlessTransient records the row as failed unless cause is a known +// transient error, in which case it is logged and the row is left in its +// current state for the next tick. Use this at chain-call sites where the +// default disposition is to mark failed; sites that already retry on all +// errors keep their own warn-and-return pattern. +func (s *TxSender) markFailedUnlessTransient(ctx context.Context, id int64, label string, cause error) { + if isTransient(cause) { + log.Warn().Err(cause).Int64("id", id).Str("label", label). + Msg("tx outbox: transient chain-client error, will retry") + return + } + s.markFailed(ctx, id, label, cause) +} + +func (s *TxSender) markFailed(ctx context.Context, id int64, label string, cause error) { + log.Error().Err(cause).Int64("id", id).Str("label", label).Msg("tx outbox: marking failed") + queries := corekeyperdb.New(s.cfg.DBPool) + if err := queries.MarkTxFailed(ctx, corekeyperdb.MarkTxFailedParams{ + ID: id, + Error: sql.NullString{String: cause.Error(), Valid: true}, + }); err != nil { + log.Error().Err(err).Int64("id", id).Str("label", label).Msg("tx outbox: mark failed update") + } +} + +// EnqueueTx inserts a pending row into `tx_outbox`. Producers use this from +// within their own database transactions so the intent to send a transaction +// is committed atomically with the state that motivated it. +// +// The label is an opaque, caller-supplied string. TxSender stores it as-is +// and includes it in every log line for the row so operators can identify +// what a transaction is for without decoding calldata. +func EnqueueTx( + ctx context.Context, + tx pgx.Tx, + to common.Address, + data []byte, + value *big.Int, + label string, +) (int64, error) { + num, err := bigIntToNumeric(value) + if err != nil { + return 0, errors.Wrap(err, "encode value") + } + return corekeyperdb.New(tx).InsertPendingTx(ctx, corekeyperdb.InsertPendingTxParams{ + ToAddress: to.Hex(), + Data: data, + Value: num, + Label: label, + }) +} + +func bigIntToNumeric(v *big.Int) (pgtype.Numeric, error) { + if v == nil { + v = new(big.Int) + } + var num pgtype.Numeric + if err := num.Set(v.String()); err != nil { + return pgtype.Numeric{}, err + } + return num, nil +} + +func numericToBigInt(num pgtype.Numeric) (*big.Int, error) { + if num.Status != pgtype.Present { + return new(big.Int), nil + } + var s string + if err := num.AssignTo(&s); err != nil { + return nil, err + } + bi, ok := new(big.Int).SetString(s, 10) + if !ok { + return nil, errors.Errorf("not a base-10 integer: %q", s) + } + return bi, nil +} diff --git a/rolling-shutter/txsender/txsender_test.go b/rolling-shutter/txsender/txsender_test.go new file mode 100644 index 000000000..a690d1791 --- /dev/null +++ b/rolling-shutter/txsender/txsender_test.go @@ -0,0 +1,390 @@ +package txsender + +import ( + "context" + "database/sql" + stderrors "errors" + "io" + "math/big" + "net" + "sync" + "sync/atomic" + "syscall" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/jackc/pgtype" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/pkg/errors" + "gotest.tools/assert" + + corekeyperdb "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" + "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/testsetup" +) + +func TestBigIntNumericRoundtrip(t *testing.T) { + cases := []*big.Int{ + big.NewInt(0), + big.NewInt(1), + big.NewInt(1_000_000_000_000_000_000), // 1 ETH in wei + new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1e9)), + } + for _, want := range cases { + num, err := bigIntToNumeric(want) + assert.NilError(t, err) + got, err := numericToBigInt(num) + assert.NilError(t, err) + assert.Equal(t, want.Cmp(got), 0, "roundtrip mismatch: want %s got %s", want, got) + } +} + +func TestNilBigIntEncodesAsZero(t *testing.T) { + num, err := bigIntToNumeric(nil) + assert.NilError(t, err) + got, err := numericToBigInt(num) + assert.NilError(t, err) + assert.Equal(t, got.Sign(), 0) +} + +func TestNullNumericDecodesAsZero(t *testing.T) { + got, err := numericToBigInt(pgtype.Numeric{Status: pgtype.Null}) + assert.NilError(t, err) + assert.Equal(t, got.Sign(), 0) +} + +// fakeClient records the calls made into it so tests can assert on which +// chain-side operations the sender performed. +type fakeClient struct { + mu sync.Mutex + chainID *big.Int + nonce uint64 + gasLimit uint64 + tipCap *big.Int + baseFee *big.Int + receiptErr error + sendErr error + sentTxs []*types.Transaction + receiptCallsByHash map[common.Hash]int + sendTransactionCallCount int32 +} + +func newFakeClient() *fakeClient { + return &fakeClient{ + chainID: big.NewInt(1337), + gasLimit: 21000, + tipCap: big.NewInt(2_000_000_000), + baseFee: big.NewInt(5_000_000_000), + receiptErr: ethereum.NotFound, + receiptCallsByHash: map[common.Hash]int{}, + } +} + +func (c *fakeClient) ChainID(_ context.Context) (*big.Int, error) { + return c.chainID, nil +} + +func (c *fakeClient) PendingNonceAt(_ context.Context, _ common.Address) (uint64, error) { + c.mu.Lock() + defer c.mu.Unlock() + n := c.nonce + c.nonce++ + return n, nil +} + +func (c *fakeClient) SuggestGasTipCap(_ context.Context) (*big.Int, error) { + return new(big.Int).Set(c.tipCap), nil +} + +func (c *fakeClient) HeaderByNumber(_ context.Context, _ *big.Int) (*types.Header, error) { + return &types.Header{BaseFee: new(big.Int).Set(c.baseFee)}, nil +} + +func (c *fakeClient) EstimateGas(_ context.Context, _ ethereum.CallMsg) (uint64, error) { + return c.gasLimit, nil +} + +func (c *fakeClient) SendTransaction(_ context.Context, tx *types.Transaction) error { + atomic.AddInt32(&c.sendTransactionCallCount, 1) + c.mu.Lock() + defer c.mu.Unlock() + if c.sendErr != nil { + return c.sendErr + } + c.sentTxs = append(c.sentTxs, tx) + return nil +} + +func (c *fakeClient) TransactionReceipt(_ context.Context, h common.Hash) (*types.Receipt, error) { + c.mu.Lock() + c.receiptCallsByHash[h]++ + c.mu.Unlock() + return nil, c.receiptErr +} + +func newTestSender(t *testing.T, dbpool *pgxpool.Pool) (*TxSender, *fakeClient) { + t.Helper() + key, err := crypto.GenerateKey() + assert.NilError(t, err) + fc := newFakeClient() + s := New(Config{ + DBPool: dbpool, + Client: fc, + PrivateKey: key, + }) + s.chainID = fc.chainID + return s, fc +} + +// TestPollProcessesBothPhasesInOneIteration verifies the merged poll loop: +// injecting one pending row and one submitted row results in both being +// processed within a single poll iteration. The pending row should be sent +// via the fake client, and the submitted row should have its receipt queried. +func TestPollProcessesBothPhasesInOneIteration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + s, fc := newTestSender(t, dbpool) + + queries := corekeyperdb.New(dbpool) + zero, err := bigIntToNumeric(big.NewInt(0)) + assert.NilError(t, err) + + // One pending row, to be picked up by the submit phase. + pendingID, err := queries.InsertPendingTx(ctx, corekeyperdb.InsertPendingTxParams{ + ToAddress: common.HexToAddress("0x000000000000000000000000000000000000dead").Hex(), + Data: []byte{0x01, 0x02, 0x03}, + Value: zero, + }) + assert.NilError(t, err) + + // One row pre-marked submitted, to be picked up by the confirm phase. + submittedID, err := queries.InsertPendingTx(ctx, corekeyperdb.InsertPendingTxParams{ + ToAddress: common.HexToAddress("0x00000000000000000000000000000000000000ff").Hex(), + Data: []byte{0xaa, 0xbb}, + Value: zero, + }) + assert.NilError(t, err) + preexistingHash := common.HexToHash("0xabc0000000000000000000000000000000000000000000000000000000000001") + err = queries.MarkTxSubmitted(ctx, corekeyperdb.MarkTxSubmittedParams{ + ID: submittedID, + TxHash: sql.NullString{String: preexistingHash.Hex(), Valid: true}, + Nonce: sql.NullInt64{Int64: 42, Valid: true}, + }) + assert.NilError(t, err) + + s.poll(ctx) + + assert.Equal(t, int32(1), atomic.LoadInt32(&fc.sendTransactionCallCount), + "submit phase: expected exactly one SendTransaction call (the pending row)") + pendingAfter, err := queries.GetTxOutboxByID(ctx, pendingID) + assert.NilError(t, err) + assert.Equal(t, "submitted", pendingAfter.Status, + "submit phase: pending row should be marked submitted") + + assert.Equal(t, 1, fc.receiptCallsByHash[preexistingHash], + "confirm phase: expected one TransactionReceipt call for the pre-existing submitted row") +} + +// TestSubmitRowMarksFailedWhenSendFails verifies the mark-before-send ordering: +// when SendTransaction returns an error after the row has already been marked +// submitted, the row must end up in `failed` status (the submitted -> failed +// transition is intentional and recorded via MarkTxFailed). +func TestSubmitRowMarksFailedWhenSendFails(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + s, fc := newTestSender(t, dbpool) + fc.sendErr = stubError("rpc connection refused") + + queries := corekeyperdb.New(dbpool) + zero, err := bigIntToNumeric(big.NewInt(0)) + assert.NilError(t, err) + id, err := queries.InsertPendingTx(ctx, corekeyperdb.InsertPendingTxParams{ + ToAddress: common.HexToAddress("0x000000000000000000000000000000000000dead").Hex(), + Data: []byte{0x01, 0x02, 0x03}, + Value: zero, + }) + assert.NilError(t, err) + + s.poll(ctx) + + assert.Equal(t, int32(1), atomic.LoadInt32(&fc.sendTransactionCallCount), + "SendTransaction should be invoked exactly once before failure is recorded") + row, err := queries.GetTxOutboxByID(ctx, id) + assert.NilError(t, err) + assert.Equal(t, "failed", row.Status, + "row should be marked failed after SendTransaction error") + assert.Assert(t, row.Error.Valid, "failure error column should be populated") + assert.Assert(t, row.TxHash.Valid, + "tx_hash should be persisted (mark-before-send wrote it prior to send)") +} + +// TestSubmitRowSkipsSendWhenMarkSubmittedFails verifies that if the DB update +// to mark the row submitted fails, SendTransaction is never called. We force +// the failure by dropping the tx_outbox table mid-flight, then driving +// submitRow directly with a hand-constructed row so the absent table is only +// surfaced at the MarkTxSubmitted step (not earlier in GetPendingTxs). +func TestSubmitRowSkipsSendWhenMarkSubmittedFails(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + s, fc := newTestSender(t, dbpool) + + zero, err := bigIntToNumeric(big.NewInt(0)) + assert.NilError(t, err) + row := corekeyperdb.TxOutbox{ + ID: 1, + ToAddress: common.HexToAddress("0x000000000000000000000000000000000000dead").Hex(), + Data: []byte{0x01, 0x02, 0x03}, + Value: zero, + Status: "pending", + } + + // CASCADE because dkg_sent_actions has a FK on tx_outbox; the goal is to + // force MarkTxSubmitted to fail, and dropping the table accomplishes that + // regardless of what depends on it. + _, err = dbpool.Exec(ctx, "DROP TABLE tx_outbox CASCADE") + assert.NilError(t, err) + + s.submitRow(ctx, row) + + assert.Equal(t, int32(0), atomic.LoadInt32(&fc.sendTransactionCallCount), + "SendTransaction must not be called when MarkTxSubmitted fails") +} + +// TestEnqueueTxPersistsLabel verifies that a label passed to EnqueueTx is +// stored on the row and readable back via the standard query path. +func TestEnqueueTxPersistsLabel(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + const wantLabel = "submitDealing ksi=7 retry=2" + + var id int64 + err := dbpool.BeginFunc(ctx, func(tx pgx.Tx) error { + var inner error + id, inner = EnqueueTx( + ctx, + tx, + common.HexToAddress("0x000000000000000000000000000000000000dead"), + []byte{0x01, 0x02, 0x03}, + big.NewInt(0), + wantLabel, + ) + return inner + }) + assert.NilError(t, err) + + row, err := corekeyperdb.New(dbpool).GetTxOutboxByID(ctx, id) + assert.NilError(t, err) + assert.Equal(t, wantLabel, row.Label) +} + +// TestSubmitRowBuildsDynamicFeeTx verifies that submitRow constructs an +// EIP-1559 DynamicFeeTx with GasTipCap from SuggestGasTipCap and +// GasFeeCap = 2 * baseFee + tipCap from the latest header's BaseFee. +func TestSubmitRowBuildsDynamicFeeTx(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() + + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, corekeyperdb.Definition) + t.Cleanup(dbclose) + + s, fc := newTestSender(t, dbpool) + fc.tipCap = big.NewInt(1_500_000_000) + fc.baseFee = big.NewInt(7_000_000_000) + + queries := corekeyperdb.New(dbpool) + zero, err := bigIntToNumeric(big.NewInt(0)) + assert.NilError(t, err) + _, err = queries.InsertPendingTx(ctx, corekeyperdb.InsertPendingTxParams{ + ToAddress: common.HexToAddress("0x000000000000000000000000000000000000dead").Hex(), + Data: []byte{0x01, 0x02, 0x03}, + Value: zero, + }) + assert.NilError(t, err) + + s.poll(ctx) + + assert.Equal(t, 1, len(fc.sentTxs), "expected exactly one submitted tx") + tx := fc.sentTxs[0] + assert.Equal(t, uint8(types.DynamicFeeTxType), tx.Type(), + "submitted tx should be EIP-1559 DynamicFeeTx") + assert.Equal(t, fc.tipCap.String(), tx.GasTipCap().String(), + "GasTipCap should match SuggestGasTipCap") + // 2*baseFee + tipCap = 2*7e9 + 1.5e9 = 15.5e9 + wantFeeCap := new(big.Int).Mul(fc.baseFee, big.NewInt(2)) + wantFeeCap.Add(wantFeeCap, fc.tipCap) + assert.Equal(t, wantFeeCap.String(), tx.GasFeeCap().String(), + "GasFeeCap should equal 2*baseFee + tipCap") +} + +type stubError string + +func (e stubError) Error() string { return string(e) } + +// stubRPCError mimics the shape of a JSON-RPC application error returned by +// go-ethereum's rpc package (see rpc.Error). isTransient must NOT treat these +// as transient: they represent deliberate rejections from the node. +type stubRPCError struct { + msg string + code int +} + +func (e *stubRPCError) Error() string { return e.msg } +func (e *stubRPCError) ErrorCode() int { return e.code } + +func TestIsTransient(t *testing.T) { + cases := []struct { + name string + err error + want bool + }{ + {"nil", nil, false}, + {"context canceled", context.Canceled, true}, + {"context deadline exceeded", context.DeadlineExceeded, true}, + {"wrapped context canceled", errors.Wrap(context.Canceled, "outer"), true}, + {"syscall ECONNREFUSED", syscall.ECONNREFUSED, true}, + {"syscall ECONNRESET", syscall.ECONNRESET, true}, + {"wrapped syscall errno", errors.Wrap(syscall.ETIMEDOUT, "dial"), true}, + {"io.EOF", io.EOF, true}, + {"io.ErrUnexpectedEOF", io.ErrUnexpectedEOF, true}, + {"net.OpError wrapping syscall", &net.OpError{Op: "dial", Net: "tcp", Err: syscall.ECONNREFUSED}, true}, + {"net.DNSError timeout", &net.DNSError{Err: "i/o timeout", Name: "example", IsTimeout: true}, true}, + {"plain error", stderrors.New("boom"), false}, + {"wrapped plain error", errors.Wrap(stderrors.New("boom"), "outer"), false}, + {"rpc application error", &stubRPCError{msg: "already known", code: -32000}, false}, + {"wrapped rpc application error", errors.Wrap(&stubRPCError{msg: "nonce too low", code: -32000}, "send transaction"), false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, isTransient(tc.err)) + }) + } +}