> For the complete documentation index, see [llms.txt](https://valargroup.gitbook.io/shielded-vote-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://valargroup.gitbook.io/shielded-vote-docs/chain/ea-key-setup-ceremony.md).

# EA Key Setup Ceremony

The ceremony establishes the election authority keypair (`ea_sk`, `ea_pk`) used for El Gamal vote encryption. A fresh ceremony runs for **every voting round** — each round gets its own `ea_pk`, and the current validator set at the moment of round publication determines who participates.

## Parameters

| Parameter       | Value                              | Description                                                                               |
| --------------- | ---------------------------------- | ----------------------------------------------------------------------------------------- |
| `G`             | Pallas generator                   | Generator of the prime-order subgroup                                                     |
| `deal_timeout`  | TBD (e.g. 30 blocks)               | How long a selected dealer has to submit the DealerTx before the next proposer takes over |
| `ack_threshold` | `ceil(n/2)` of eligible validators | Minimum ACKs required to confirm the ceremony (matches TSS reconstruction threshold)      |
| `phase_timeout` | TBD (e.g. 30 minutes)              | Total time allowed for the ACK phase before fallback logic kicks in                       |

## Overview

```mermaid
flowchart TD
    A[Round published] --> B[Snapshot eligible validators]
    B --> C[Select dealer\nnext block proposer]
    C --> D{Dealer acts\nin time?}
    D -->|Yes| E[DealerTx lands on-chain]
    D -->|No, timeout| F[Next proposer\nbecomes dealer]
    F --> D
    E --> G[Validators auto-ACK\nvia PrepareProposal]
    G --> H{All ACK'd?}
    H -->|Yes| I[CONFIRMED\nfast path]
    H -->|No| L{Timeout?}
    L -->|No| G
    L -->|"Yes, ≥ ceil(n/2)"| I2[CONFIRMED\nstrip non-ackers]
    L -->|"Yes, < ceil(n/2)"| J[Reset, new dealer selected]
    J --> D
    I --> K[Round becomes ACTIVE]
    I2 --> K
```

Each round follows this flow independently. A new round means a new ceremony, a new `ea_sk`, and a new `ea_pk`.

## Ceremony Flow

### Step 1: Round Publication Triggers Ceremony

When the vote manager publishes a voting round via `MsgCreateVotingSession`, the round enters a **PENDING** state and a fresh ceremony begins. The round cannot accept votes until the ceremony confirms.

### Step 2: Validator Eligibility Snapshot

Validators register their Pallas public key **once**, at join time (via `MsgCreateValidatorWithPallasKey` or `MsgRegisterPallasKey`). The PK persists in chain state — there is no per-round re-registration.

When a round is published, the chain snapshots which validators currently have a registered Pallas PK. **If you haven't registered your Pallas PK by this point, you are excluded from this round's ceremony.** You can still participate in consensus, but you won't receive this round's `ea_sk` and cannot participate in its tally decryption.

Participation in a specific round is signaled by the **ACK**, not by registration. The ACK happens automatically via `PrepareProposal` when it's your turn to propose — so "participating" just means your node was online and proposing blocks during the ceremony window.

### Step 3: Dealer Selection

The chain selects the **next block proposer** (after the round enters PENDING) as the dealer. The dealer is responsible for:

1. Generating `ea_sk` (random Pallas scalar)
2. Computing `ea_pk = ea_sk * G`
3. Encrypting `ea_sk` to every eligible validator's Pallas PK using ECIES
4. Submitting `MsgDealExecutiveAuthorityKey` containing `ea_pk` and all encrypted payloads

**Dealer timeout**: If the selected dealer doesn't submit the DealerTx within `deal_timeout`, the next proposer in rotation becomes the dealer. This repeats until someone deals.

Dealing is **fully automated** via `PrepareProposal` — when a proposer builds a block and sees a PENDING round without a DealerTx, they generate `ea_sk`, encrypt it, and inject the DealerTx into the block. No human intervention, same pattern as auto-ACK.

### Step 4: ECIES Key Distribution

The dealer encrypts `ea_sk` individually for each eligible validator using ECIES on Pallas:

For each validator `i` with public key `pk_i`:

1. Sample ephemeral scalar `e_i`, compute `E_i = e_i * G`
2. Compute shared secret `S_i = e_i * pk_i` (ECDH)
3. Derive symmetric key `k_i = SHA256(E_i || S_i.x)`
4. Encrypt `ct_i = ChaCha20-Poly1305(k_i, nonce=0, plaintext=ea_sk)`

The DealerTx contains `ea_pk` and for each validator: `(validator_address, E_i, ct_i)`.

Only the intended validator can decrypt their payload — they compute the same shared secret using their private Pallas key.

### Step 5: Validator Acknowledgment

Each validator, upon seeing the DealerTx in state:

1. Finds their `(E_i, ct_i)` payload
2. Computes shared secret: `S_i = sk_i * E_i`
3. Derives key: `k_i = SHA256(E_i || S_i.x)`
4. Decrypts: `ea_sk = ChaCha20-Poly1305.decrypt(k_i, nonce=0, ct_i)`
5. Verifies: `ea_sk * G == ea_pk`
6. Submits `MsgAckExecutiveAuthorityKey` with signature over `H("ack" || ea_pk || validator_address)`
7. Stores `ea_sk` to disk for this round's tally decryption

This is **fully automated** via `PrepareProposal` — when a validator's turn to propose comes, they decrypt and inject the ACK into their block. No manual action required.

### Step 6: Confirmation

There are two confirmation paths:

**Fast path** — all eligible validators ACK. The ceremony confirms immediately with no stripping. This is the happy path when all validators are online. **Timeout path** — the ACK phase timeout expires. If **≥ ceil(n/2) of eligible validators** have ACK'd, the ceremony confirms. Non-ACKing validators are stripped from the round's ceremony and have their consecutive miss counter incremented. After **3 consecutive misses**, a validator is **jailed**. A successful ACK in any round resets the counter. If fewer than `ceil(n/2)` have ACK'd, the ceremony resets and a new dealer is selected. This quorum matches the TSS reconstruction threshold so that enough validators always survive stripping to reconstruct the EA key at tally time.

At confirmation (either path):

* The round's ceremony state transitions to **CONFIRMED**
* The PENDING round transitions to **ACTIVE** (voting can begin)
* `ea_pk` is bound to this round

## Why Per-Round Ceremony

Running a fresh ceremony for each round provides several benefits:

* **Scoped key compromise**: If a validator is compromised, the attacker can only decrypt votes for rounds where that validator received `ea_sk`. Past and future rounds with different validator sets use different keys.
* **Natural validator rotation**: Validators can join or leave between rounds without needing a key-sharing mechanism. New validators simply participate in the next round's ceremony.
* **No re-initialization complexity**: No need for `MsgReInitializeElectionAuthority` or key rotation protocols. Each round starts fresh.
* **Clean eligibility**: "Are you registered?" is checked once per round at publication time. No ambiguity about when registration matters.

The cost is running the ceremony for every round, but with auto-injection via `PrepareProposal` and a small validator set, this completes within a few blocks.

## Decryption (Tally Time)

When a voting round's window closes and transitions to TALLYING, any validator who ACK'd this round's ceremony can decrypt:

1. Load this round's `ea_sk` from disk
2. For each `(proposal_id, decision)`, grab the aggregate ciphertext `(C1_agg, C2_agg)`
3. Decrypt: `total_value * G = C2_agg - ea_sk * C1_agg`
4. Recover `total_value` via baby-step-giant-step (feasible — bounded by ZEC supply)
5. Submit `MsgSubmitTally` with the result and a Chaum-Pedersen DLEQ proof

This is automated via `PrepareProposal` — the block proposer auto-decrypts and injects the tally.

Validators store one `ea_sk` file per round: `~/.zallyd/ea_sk.<round_id_hex>`. Keys are never deleted — they must be retained for retallying past rounds (auditability). Disk cost is negligible (32 bytes per key). Multiple rounds can run in parallel; each round references its own key file.

## Security Properties

* **Tally liveness**: At least `t = ceil(n/2)` ACK'd validators must submit partial decryptions for the tally to complete. The ack quorum ensures enough validators survive stripping.
* **Ceremony liveness**: The fast path completes when all validators ACK. If some are offline, the timeout path confirms with `ceil(n/2)` acks (stripping the rest). Dealer fallback ensures progress even if the selected dealer is offline.
* **Verifiable decryption**: The DLEQ proof lets anyone verify the tally is correct without knowing `ea_sk`.
* **Trust model (TSS)**: Each ACK'd validator holds a Shamir share of `ea_sk`, not the full key. Fewer than `t = ceil(n/2)` compromised validators cannot recover `ea_sk`. Only the dealer knows the full key during the deal block (it is zeroed immediately after). The impact of a single compromised non-dealer validator is nil for vote decryption.
* **Trusted dealer**: The dealer generates `ea_sk` directly and knows it during the deal block. Deletion cannot be proven. Future DKG (see below) eliminates this assumption.
* **Key isolation**: Different rounds use different keys. A departing validator's knowledge of old shares does not help them decrypt future rounds.

## Threshold Secret Sharing (TSS) — Current

The dealer splits `ea_sk` into Shamir shares with a `t`-of-`n` threshold (`t = ceil(n/2)`). Validators hold shares rather than the full key. At tally time, `t` validators each submit a partial decryption (`D_i = share_i * C1`) and results are combined via Lagrange interpolation.

Benefits:

* `ceil(n/2) - 1` compromised non-dealer validators cannot recover `ea_sk`
* Stronger privacy guarantees — no single non-dealer validator can decrypt individual shares

Costs:

* Requires `t` validators to be online and cooperating at tally time (reduces tally liveness)
* More complex tally flow (partial decryptions + aggregation)

See `sdk/docs/tss-ceremony.md` for the full TSS specification including deal/ack mechanics, on-disk key files, and the partial decryption tally pipeline.

## Future Extensions

### Full DKG

Replace the single dealer with a protocol where each validator contributes randomness. `ea_sk` never exists in one place. This eliminates the trusted dealer assumption entirely but adds significant protocol complexity.

***

> \[!info] Current Implementation (as of Mar 2026)
>
> The current implementation uses TSS (Threshold Secret Sharing) with `t = ceil(n/2)`:
>
> * **Ceremony is per-round.** Each `MsgCreateVotingSession` creates a PENDING round with ceremony fields snapshotted from the current validator set. No global singleton ceremony state.
> * **Auto-deal and auto-ack via PrepareProposal.** The block proposer automatically deals (Shamir-splits `ea_sk` into `(t, n)` shares) when a PENDING round is in REGISTERING state, and auto-acks (verifies `share_i * G == VK_i`) when in DEALT state.
> * **Fast path requires all validators to ACK.** The `ceil(n/2)` quorum (integer arithmetic: `acks * 2 >= n`) only applies on the timeout path, which strips non-ackers.
> * **Miss tracking and jailing.** Validators snapshotted into a ceremony who fail to ack have a consecutive miss counter incremented. After 3 consecutive misses, the validator is jailed. Successful ack resets the counter.
> * **ACK phase timeout is 30 minutes** (`DefaultDealTimeout = 1800 seconds`).
> * **Ceremony log.** Each state transition appends a timestamped entry to `ceremony_log` on the VoteRound, visible in round queries and the admin UI.
> * **`make ceremony`** (from `sdk/`) runs a Rust E2E test that registers a Pallas key, creates a round, and polls until it becomes ACTIVE.
>
> Key code locations:
>
> * Ceremony message handlers: `sdk/x/vote/keeper/msg_server_ceremony.go`
> * Ceremony helpers (log, quorum check, strip): `sdk/x/vote/keeper/keeper_ceremony.go`
> * Auto-deal/ack injection: `sdk/app/prepare_proposal_ceremony.go`
> * EndBlocker timeout + jailing: `sdk/x/vote/module.go`
> * ECIES encryption: `sdk/crypto/ecies/`
> * E2E ceremony test: `e2e-tests/tests/ceremony_bootstrap.rs`
