Shielded Vote Chain

A Cosmos SDK application chain for private on-chain voting using Zcash-derived cryptography (Halo2 ZKP circuits, RedPallas signatures, ElGamal encryption, Poseidon Merkle trees).
Specification: Shielded Voting Protocol ZIP
Quick Start
Prerequisites
- mise — task runner + toolchain installer (recommended)
- Alternatively, install Go 1.24+ and Rust stable directly
Run a local chain with mise
# 1. Install the toolchain listed in mise.toml (Go, Rust, Node, buf, jq).
mise install
# 2. Build + install svoted and create-val-tx into $GOBIN (on PATH).
mise run install
# 3. Create .env with the vote-manager private key(s) (one-time).
cp .env.example .env
# Edit .env and set VM_PRIVKEYS (comma-separated 64-char hex keys; generate
# each with: openssl rand -hex 32). For a single-vote-manager chain, provide
# exactly one key — any-of-N means any vote manager in the list can authorize
# vote-manager-gated operations.
# 4. Wipe + init a single-validator devnet, then start the daemon.
mise run chain:init
mise run chain:start
Other useful tasks: mise run chain:init-multi + mise run chain:start-multi
for a 3-validator local chain, mise run test:go for the Go test suite,
mise tasks to list everything.
Contributing from a clone (local binaries, join script with SVOTE_LOCAL_BINARIES, task reference): see CONTRIBUTING.md.
Without mise (direct make)
# Build and install
make circuits # Rust ZKP circuit static library
make build-ffi # svoted with ZKP + signature verification
# (or) make build # no FFI — stubs Halo2/RedPallas verification; dev only
# Init + start (same .env step as above applies)
make init
make start
Consensus timing defaults
svoted overrides CometBFT defaults at startup to reduce block time (~1.2s
with 30 validators vs ~4-6s with defaults). See
docs/blocktimes.md for the full parameter table, Osmosis
comparison, and benchmark results confirming safety.
Test
# Go unit tests (no Rust dependency)
make test
# Halo2 ZKP verification tests (requires: make circuits)
make test-halo2
# All FFI-backed tests (Halo2 + RedPallas)
make test-all-ffi
# Rust circuit unit tests
make circuits-test
Project Structure
app/ Cosmos app, ABCI handlers, PrepareProposal/ProcessProposal
api/ REST API handlers, codec, tx wrapper
circuits/ Rust crate: Halo2 ZKP + RedPallas FFI (staticlib for CGo)
cmd/svoted/ Chain binary and CLI commands
crypto/ Cryptographic primitives (ECIES, ElGamal, Shamir, ZKP, etc.)
deploy/ Caddyfile for HTTPS reverse proxy
docs/ Deployment and protocol documentation
internal/ Helper server (share submission, SQLite, random delays)
proto/ Protobuf definitions (buf-managed)
scripts/ Chain init scripts, validator tooling
x/vote/ Vote module: ceremony, voting rounds, tally, keeper
License
This project is licensed under the MIT License.
Protocol Documentation
Technical Assumptions
- The chain launches with a single genesis validator. Additional validators join post-genesis via
MsgCreateValidatorWithPallasKey, which atomically creates the validator and registers their Pallas key for the ceremony. Raw MsgCreateValidator is blocked in the ante handler for live transactions. Validator set changes beyond that are handled via major upgrades or a PoA module (future).
- Client interaction avoids Cosmos SDK protobuf encoding:
- Tx submission: Client sends a plain JSON POST; server handler parses JSON and encodes as needed.
- Query: gRPC gateway supports JSON out-of-the-box.
- No native
x/gov module. The vote module implements custom private voting instead of reusing standard Cosmos governance.
Architecture
Module: x/vote
The vote module has two major subsystems: the EA Key Ceremony (automatic per voting round) and Voting Rounds (transition from PENDING through ceremony to ACTIVE).
EA Key Ceremony (Per-Round)
The EA key ceremony runs automatically per voting round. Each MsgCreateVotingSession creates a round in PENDING status and snapshots all eligible validators (bonded + registered Pallas key) into the round's ceremony fields. The ceremony proceeds automatically via PrepareProposal — no manual intervention is needed after initial Pallas key registration.
Per-Round Ceremony State Machine
Ceremony state is stored on the VoteRound itself (fields ceremony_status, ceremony_validators, etc.). There is no global singleton ceremony state.
PENDING (REGISTERING) ──> PENDING (DEALT) ──> ACTIVE (CONFIRMED)
│ (all acked)
timeout │
(< 1/2) │ timeout (≥ 1/2)
│ │
v v
REGISTERING ACTIVE (CONFIRMED)
(reset for + strip non-ackers
re-deal)
| From |
To |
Trigger |
Condition |
| REGISTERING |
DEALT |
Auto-deal via PrepareProposal |
Block proposer is a ceremony validator |
| DEALT |
CONFIRMED + ACTIVE |
MsgAckExecutiveAuthorityKey |
All validators acked (fast path) |
| DEALT |
CONFIRMED + ACTIVE |
EndBlocker timeout |
>= 1/2 acked at timeout; non-ackers stripped |
| DEALT |
REGISTERING |
EndBlocker timeout |
< 1/2 acked; reset for re-deal by next proposer |
Key behaviors:
- Fast path vs timeout — the fast path confirms when ALL validators ack (no stripping needed). The timeout path confirms with >= 1/2 acks (integer arithmetic:
acks * 2 >= validators) and strips non-ackers.
- Auto-deal — the block proposer automatically deals when it detects a PENDING round in REGISTERING state. No manual
ceremony.sh deal step.
- Auto-ack — each block proposer auto-acks via PrepareProposal when it detects a DEALT round.
- Non-acker stripping — validators who fail to ack within the timeout are stripped from the round's ceremony (removed from
ceremony_validators and ceremony_payloads). No miss counters or ceremony-based jailing — liveness enforcement is handled by x/slashing block-miss detection.
- Ceremony log — each state transition appends a timestamped entry to
ceremony_log on the round, visible in queries and the admin UI.
Pallas Key Registration and Rotation
Validators register their Pallas key via MsgRegisterPallasKey or MsgCreateValidatorWithPallasKey. Keys are stored in a global registry (prefix 0x0C) and persist across rounds.
A registered key can be replaced via MsgRotatePallasKey. Rotation is rejected while the validator is participating in any PENDING round ceremony (the snapshotted key would become stale and ECIES payloads would be undecryptable). The old key's reverse-lookup index is cleaned up so it can be reused.
Auto-Deal and Auto-Ack via PrepareProposal
PrepareProposal composes two ceremony injectors:
- Auto-deal — if a PENDING round is in REGISTERING state and the proposer is a ceremony validator, generate
ea_sk, Shamir-split it into (t, n) shares, ECIES-encrypt share_i to each validator, publish VK_i = share_i * G and threshold = ceil(n/2), and inject MsgDealExecutiveAuthorityKey.
- Auto-ack — if a PENDING round is in DEALT state and the proposer hasn't acked, decrypt the payload to recover their share, verify
share_i * G == VK_i (threshold mode) or ea_sk * G == ea_pk (legacy), inject MsgAckExecutiveAuthorityKey, and write the share/key to disk.
Timeout (EndBlocker)
REGISTERING and DEALT phases have timeouts (default: 10 minutes). On DEALT timeout:
- >= 1/2 acked: Confirm ceremony, strip non-ackers, activate round.
- < 1/2 acked: Reset to REGISTERING for re-deal by the next proposer.
ECIES Encryption Scheme
Each validator's ea_sk share is encrypted using ECIES over the Pallas curve with SpendAuthG as the generator:
E = e * SpendAuthG (ephemeral public key)
S = e * pk_i (ECDH shared secret)
k = SHA256(E_compressed || S.x) (symmetric key)
ct = ChaCha20-Poly1305(k, nonce=0, ea_sk) (authenticated encryption)
Vote-Manager Set (any-of-N)
The vote-manager set is a list of on-chain account addresses that gate who can create voting sessions and authorize unrestricted sends. Any member of the set can act alone (any-of-N semantics — strict membership, no threshold, no multi-sig aggregation). The vote-manager set must be non-empty at genesis (no bootstrap path).
MsgUpdateVoteManagers -- Atomically replaces the vote-manager set.
- Callable by any current vote manager
- Validation: the new set must be non-empty, each address must be valid bech32, and no duplicates
- Does not move balances — each vote manager holds their own funds (the bank-module per-account balance). Removed vote managers keep whatever
usvote they had, but their sends become rejected because they are no longer vote managers and are not bonded validators (see "Drain before removal" below)
- Stored as a singleton
VoteManagerSet { repeated string addresses } in the KV store (key 0x0A)
QueryVoteManagers returns the current vote-manager set.
Drain before removal. When a vote manager is removed via MsgUpdateVoteManagers, their remaining usvote balance stays in their account but becomes one-way frozen: they cannot MsgAuthorizedSend (no longer a vote manager, not a validator), but can still receive from active vote managers. To avoid stranded balance, an active vote manager should MsgAuthorizedSend to drain a departing vote manager's balance before calling MsgUpdateVoteManagers to remove them.
Voting Rounds
After the ceremony reaches CONFIRMED and at least one vote manager is set, voting sessions can be created.
ACTIVE ──> TALLYING ──> FINALIZED
^
│ (gated: requires CONFIRMED ceremony + non-empty vote-manager set)
MsgCreateVotingSession reads ea_pk from the confirmed ceremony state (not from the message). The round stores its own copy of ea_pk for future key rotation support. Any vote manager can create voting sessions. An optional description field provides human-readable context for the round.
MsgSubmitPartialDecryption is auto-injected via PrepareProposal when a round is in TALLYING state and threshold mode is active. Each validator submits D_i = share_i * C1 per accumulator. Cannot be submitted through the mempool.
MsgSubmitTally is auto-injected via PrepareProposal once t partial decryptions exist on-chain. The proposer Lagrange-combines them to recover ea_sk * C1, runs BSGS, and submits plaintext totals. Cannot be submitted through the mempool.
PrepareProposal / ProcessProposal Pipeline
PrepareProposal composes four injectors that run sequentially on each proposed block:
- Ceremony deal injection — if a PENDING round is in REGISTERING and the proposer is a ceremony validator, auto-deal via
MsgDealExecutiveAuthorityKey
- Ceremony ack injection — if a PENDING round is in DEALT and the proposer hasn't acked, auto-ack via
MsgAckExecutiveAuthorityKey
- Partial decryption injection (threshold mode) — if a TALLYING round has
threshold > 0 and the proposer hasn't yet submitted, compute D_i = share_i * C1 per accumulator and inject MsgSubmitPartialDecryption
- Tally injection — when
t partials are on-chain (threshold mode) or ea_sk is on disk (legacy), Lagrange-combine and BSGS-solve, then inject MsgSubmitTally
ProcessProposal validates all injected txs on non-proposer validators before accepting a block. MsgAckExecutiveAuthorityKey, MsgSubmitPartialDecryption, and MsgSubmitTally are all blocked from the mempool (CheckTx rejects them).
Rationale
The standard Cosmos SDK Tx envelope requires a signer address, fee fields, and a conventional signature (secp256k1 or ed25519). Vote-round messages (MsgDelegateVote, MsgCastVote, MsgRevealShare) cannot use this envelope because they are authenticated via ZKP + RedPallas spend-auth signatures — there is no conventional Cosmos account involved. Similarly, MsgDealExecutiveAuthorityKey, MsgAckExecutiveAuthorityKey, and MsgSubmitPartialDecryption are auto-injected by the block proposer via PrepareProposal and are never client-signed at all.
The custom wire format is the minimal encoding that satisfies both cases: a single-byte type tag lets the TxDecoder unambiguously identify the message type without parsing a full TxBody, and the tag byte acts as the sole discriminator between the custom path and the standard Cosmos SDK path. Messages that do have a conventional signer (ceremony setup messages, MsgCreateVotingSession) still use the standard Tx envelope and flow through normal signature verification.
Each custom transaction is a 1-byte tag followed by a protobuf-encoded message body:
[tag (1 byte)] [proto-encoded message body]
| Tag |
Message |
Category |
Auth mechanism |
0x01 |
MsgCreateVotingSession |
Voting round |
Standard Cosmos Tx (signed) |
0x02 |
MsgDelegateVote |
Voting round |
ZKP #1 + RedPallas |
0x03 |
MsgCastVote |
Voting round |
ZKP #2 + RedPallas |
0x04 |
MsgRevealShare |
Voting round |
ZKP #3 |
0x05 |
MsgSubmitTally |
Voting round (injected) |
Proposer identity check |
0x06 |
MsgRegisterPallasKey |
Ceremony |
Standard Cosmos Tx (signed) |
0x07 |
MsgDealExecutiveAuthorityKey |
Ceremony (injected) |
Proposer identity check |
0x08 |
MsgAckExecutiveAuthorityKey |
Ceremony (injected) |
Proposer identity check |
0x09 |
MsgCreateValidatorWithPallasKey |
Ceremony |
Standard Cosmos Tx (signed) |
0x0C |
MsgUpdateVoteManagers |
Management |
Standard Cosmos Tx (signed) |
0x0D |
MsgSubmitPartialDecryption |
Tallying (injected) |
Proposer identity check |
Any transaction whose first byte does not match a known tag is decoded as a standard Cosmos SDK Tx. Tag 0x0A is deliberately skipped because it collides with the standard Cosmos Tx protobuf encoding (field 1, wire type 2) — this collision is what makes the two decoders unambiguously distinguishable by a single byte peek. Note that raw MsgCreateValidator is blocked by the ante handler for live transactions -- post-genesis validators must use MsgCreateValidatorWithPallasKey (tag 0x09) instead.
Message Authentication Invariants
Every message has a specific set of auth checks enforced across the ABCI pipeline. The table below lists the complete check set for each message. "PP" = PrepareProposal, "ProcessProp" = ProcessProposal.
Standard Cosmos SDK messages
These flow through the standard ante chain: signature verification (SigVerificationDecorator), then CeremonyValidatorDecorator for validator-gated types.
| Message |
Who can submit |
Ante checks |
MsgServer checks |
MsgRegisterPallasKey |
Any bonded validator |
secp256k1 sig + CeremonyValidatorDecorator (bonded validator gate) |
Valid Pallas point; no duplicate registration |
MsgRotatePallasKey |
Any bonded validator |
secp256k1 sig + CeremonyValidatorDecorator (bonded validator gate) |
Valid Pallas point; existing key required; no in-flight ceremony; new PK globally unique |
MsgCreateValidatorWithPallasKey |
Anyone (becomes a validator) |
secp256k1 sig; exempt from CeremonyValidatorDecorator |
Delegates to x/staking CreateValidator; registers Pallas key; rejects duplicates |
MsgUpdateVoteManagers |
Any current vote manager |
secp256k1 sig; exempt from CeremonyValidatorDecorator (has own auth) |
ValidateVoteManagerOnly: creator must be in the vote-manager set; atomic replace of the whole set; no balance movement |
MsgCreateVotingSession |
Any current vote manager |
secp256k1 sig (standard Cosmos Tx) |
ValidateVoteManagerOnly: creator must be in the vote-manager set |
MsgAuthorizedSend |
Any vote manager or bonded validator |
secp256k1 sig (standard Cosmos Tx) |
Any vote manager can send to anyone; validators can send to any vote manager or another bonded validator; all other senders rejected |
MsgCreateValidator |
Blocked post-genesis |
Ante handler rejects at BlockHeight > 0 |
N/A — never reaches MsgServer |
MsgSend / MsgMultiSend |
Blocked |
Not in message whitelist |
N/A — never reaches MsgServer |
These use the custom wire format and bypass the Cosmos Tx envelope. Auth is handled by the ValidateVoteTx pipeline in x/vote/ante.
| Message |
Who can submit |
Ante checks |
MsgServer checks |
MsgDelegateVote |
Any Zcash note holder (anonymous) |
ValidateBasic (field sizes); round ACTIVE; gov nullifier uniqueness; RedPallas sig over sighash; ZKP #1 (delegation proof: note ownership, VAN encoding, nc_root, nf_imt_root) |
Record gov nullifiers; append van_cmx to commitment tree |
MsgCastVote |
Any delegation holder (anonymous) |
ValidateBasic; round ACTIVE; VAN nullifier uniqueness; RedPallas sig over canonical sighash; ZKP #2 (vote commitment: VAN ownership, ea_pk binding, commitment tree anchor) |
Record VAN nullifier; append vote_authority_note_new + vote_commitment to tree |
MsgRevealShare |
Any vote holder (anonymous) |
ValidateBasic; round ACTIVE or TALLYING; share nullifier uniqueness; ZKP #3 (vote share: share ownership, commitment tree anchor) |
Record share nullifier; HomomorphicAdd enc_share into tally accumulator |
These are auto-injected by PrepareProposal and cannot be submitted through the mempool (CheckTx/ReCheckTx reject them). Auth is enforced at three layers: ante handler (per-tag dispatch), ProcessProposal (non-proposer validators verify before accepting a block), and MsgServer (FinalizeBlock execution).
| Message |
Ante check |
ProcessProposal check |
MsgServer check |
MsgDealExecutiveAuthorityKey |
ValidateProposerIsCreator (mempool block + creator == proposer) |
Round PENDING + REGISTERING; payload count matches; creator is ceremony validator; creator == proposer |
ValidateProposerIsCreator; round PENDING + REGISTERING; creator in ceremony validators; ea_pk valid; payloads 1:1 with validators; threshold + VK validation |
MsgAckExecutiveAuthorityKey |
ValidateProposerIsCreator (mempool block + creator == proposer) |
Round PENDING + DEALT; creator is ceremony validator; no duplicate ack; creator == proposer |
ValidateProposerIsCreator; round PENDING + DEALT; creator in ceremony validators; no duplicate ack |
MsgSubmitPartialDecryption |
ValidateProposerIsCreator (mempool block + creator == proposer) |
Round TALLYING + threshold > 0; creator is ceremony validator; ValidatorIndex == ShamirIndex; no duplicate; creator == proposer |
ValidateProposerIsCreator; round TALLYING + threshold > 0; creator in ceremony validators; ValidatorIndex == ShamirIndex; no duplicate; entries are valid Pallas points |
MsgSubmitTally |
ValidateVoteTx → ValidateProposerIsCreator (mempool block + creator == proposer) |
Round TALLYING; creator == proposer |
ValidateProposerIsCreator; round TALLYING; verify each entry against on-chain accumulators (DLEQ proof in legacy mode, Lagrange re-derivation in threshold mode); transition to FINALIZED |
Key design invariant
ValidateProposerIsCreator is the unified proposer identity check shared by all four injected message types. It enforces two properties:
- Mempool exclusion:
IsCheckTx() || IsReCheckTx() returns an error, preventing external submission.
- Proposer binding: During FinalizeBlock,
msg.Creator must equal the operator address of the validator whose consensus key matches BlockHeader.ProposerAddress. This prevents a malicious proposer from injecting messages on behalf of other validators.
Message Whitelist and Ante Handler Design
Standard Cosmos transactions pass through a two-layer gate before reaching the decorator chain. Both layers are in app/ante.go and app/ante_whitelist.go.
Layer 1: Pre-filter in NewDualAnteHandler (before any decorator runs)
- Single-message only. Multi-message transactions are rejected unconditionally. This eliminates the noop-signer attack class where a zero-signer message (e.g.
MsgRevealShare) piggybacks on a legitimately-signed carrier message.
- Vote/ceremony messages blocked (defense-in-depth).
isVoteModuleMsg rejects MsgDelegateVote, MsgCastVote, MsgRevealShare, MsgDealExecutiveAuthorityKey, MsgAckExecutiveAuthorityKey, MsgSubmitPartialDecryption, and MsgSubmitTally. These must enter via the custom VoteTxWrapper path where ZKP/RedPallas verification runs. The whitelist (Layer 2) would also catch these, but this explicit type check fires earlier and produces a more actionable error.
MsgCreateValidator blocked post-genesis. At BlockHeight > 0, raw MsgCreateValidator is rejected — validators must use MsgCreateValidatorWithPallasKey to atomically register a Pallas key. The message is allowed at height 0 for genesis gentx bootstrapping.
Layer 2: MessageWhitelistDecorator (inside the standard ante chain)
A positive-security allowlist: only messages whose proto type URL appears in DefaultAllowedMessages() are accepted. Any new message type must be explicitly added here before it can be processed. The current allowed set is:
| Module |
Allowed messages |
| Vote |
MsgCreateVotingSession, MsgRegisterPallasKey, MsgRotatePallasKey, MsgCreateValidatorWithPallasKey, MsgUpdateVoteManagers, MsgAuthorizedSend |
| Staking |
MsgCreateValidator (genesis only — blocked post-genesis by Layer 1), MsgEditValidator |
| Slashing |
MsgUnjail |
Explicitly excluded (and the reasons):
| Excluded messages |
Reason |
MsgSend, MsgMultiSend |
Replaced by MsgAuthorizedSend with role-based restrictions. Unrestricted transfers would let anyone accumulate stake and create a validator, bypassing the controlled validator set. |
MsgDelegate, MsgUndelegate, MsgBeginRedelegate |
Prevents validators from reorganizing stake without a vote manager. Initial self-delegation is handled atomically by MsgCreateValidatorWithPallasKey. |
MsgWithdrawDelegatorReward, MsgWithdrawValidatorCommission |
Prevents extracting staking rewards as liquid tokens that could be transferred outside of vote-manager control. |
MsgFundCommunityPool, MsgSetWithdrawAddress, MsgUpdateParams |
No governance module; these have no legitimate use on this chain. |
| All vote/ceremony ZKP messages |
Must use the custom VoteTxWrapper wire format with ZKP/RedPallas authentication. Blocked by both Layer 1 and Layer 2. |
MsgAuthorizedSend authorization rules
MsgAuthorizedSend is the only coin-transfer path on this chain. Authorization is enforced in the MsgServer handler (x/vote/keeper/msg_server_send.go):
- Any vote manager can send to anyone (distributes stake to new validators).
- Bonded validators can send to any vote manager or other bonded validators (operational redistribution within the trusted set).
- All other senders are rejected. Note that a former vote manager who has been removed from the set is neither a vote manager nor a validator, so their remaining balance becomes one-way frozen — drain before removal.
Design assumptions
- The vote-manager set has full control over stake distribution. Validators receive tokens via
MsgAuthorizedSend and bond them during MsgCreateValidatorWithPallasKey. Each vote manager holds their own pre-funded balance; the genesis stake pool is split evenly across the set to preserve total supply.
- The whitelist is a compile-time constant. Adding a new message type requires a code change, rebuild, and coordinated chain upgrade.
- Custom wire format messages (
VoteTxWrapper) are never subject to the whitelist — they are routed to the ZKP/RedPallas validation pipeline before the standard ante chain runs.
- If a custom wire format message is somehow removed from
isVoteModuleMsg (Layer 1), the whitelist (Layer 2) still blocks it since it is not in DefaultAllowedMessages().
REST API
The chain exposes a JSON REST API alongside CometBFT RPC. Clients POST JSON bodies for transaction submission and GET for queries — no protobuf encoding required on the client side.
Vote-round messages use the custom wire format and are submitted as JSON POST requests:
| Method |
Path |
Description |
| POST |
/shielded-vote/v1/delegate-vote |
Submit a delegation proof (ZKP #1) |
| POST |
/shielded-vote/v1/cast-vote |
Cast an encrypted vote (ZKP #2) |
| POST |
/shielded-vote/v1/reveal-share |
Reveal an encrypted share (ZKP #3) |
These endpoints accept JSON, encode the message with the custom wire format, and broadcast via CometBFT's broadcast_tx_sync. MsgSubmitTally, MsgDealExecutiveAuthorityKey, MsgAckExecutiveAuthorityKey, and MsgSubmitPartialDecryption have no REST endpoints — they are proposer-only and auto-injected via PrepareProposal.
Ceremony and management messages (MsgRegisterPallasKey, MsgRotatePallasKey, MsgCreateValidatorWithPallasKey, MsgUpdateVoteManagers, MsgCreateVotingSession) are standard Cosmos SDK transactions routed through the MsgServiceRouter. They can be submitted via the Cosmos SDK CLI or gRPC gateway.
Query Endpoints
| Method |
Path |
Description |
| GET |
/shielded-vote/v1/ceremony |
Current ceremony state and status |
| GET |
/shielded-vote/v1/rounds |
List all stored vote rounds |
| GET |
/shielded-vote/v1/rounds/active |
Currently active voting round |
| GET |
/shielded-vote/v1/round/{round_id} |
Voting round by hex round ID |
| GET |
/shielded-vote/v1/vote-summary/{round_id} |
Denormalized round summary with proposals |
| GET |
/shielded-vote/v1/tally/{round_id}/{proposal_id} |
Tally for a specific proposal |
| GET |
/shielded-vote/v1/tally-results/{round_id} |
All tally results for a round |
| GET |
/shielded-vote/v1/commitment-tree/{round_id}/{height} |
Vote commitment tree at block height |
| GET |
/shielded-vote/v1/commitment-tree/{round_id}/latest |
Latest vote commitment tree |
| GET |
/shielded-vote/v1/commitment-tree/{round_id}/leaves |
Tree leaves (?from_height=X&to_height=Y) |
| GET |
/shielded-vote/v1/pallas-keys |
All registered Pallas keys |
| GET |
/shielded-vote/v1/vote-managers |
Current vote-manager set (any-of-N) |
| GET |
/shielded-vote/v1/genesis |
Chain genesis JSON |
| GET |
/shielded-vote/v1/snapshot-data/{height} |
Nullifier snapshot data at block height |
| GET |
/shielded-vote/v1/tx/{hash} |
Transaction status by hash |
Helper Sentry Observability
The helper server uses Sentry when [helper].sentry_dsn is set in app.toml
or SENTRY_DSN is present in the environment. Sentry events include a stage
tag that identifies the helper code path that emitted the error, such as
enqueue, process_share, leaf_read, helper_new, or tree_status.
When a voting round closes, the helper summarizes queued shares before purging
expired witness data. If any shares for that round are still pending or failed,
it emits a Sentry error with stage=round_closed_unsubmitted_shares and tags
for round_id, total_shares, pending_shares, failed_shares,
submitted_shares, and unsubmitted_shares. Configure Sentry issue alerts on
that stage tag to page when share data was accepted by the helper but not
submitted on-chain before the round closed.
On-Chain State (KV Store Keys)
| Key |
Type |
Description |
0x09 |
CeremonyState (singleton) |
EA key ceremony lifecycle |
0x0A |
VoteManagerSet (singleton) |
Vote-manager addresses (any-of-N) |
0x01 |
VoteRound (per round) |
Voting session state |
0x02-0x08 |
Various |
Nullifiers, tallies, commitment tree, etc. |
CeremonyState Fields
enum CeremonyStatus {
CEREMONY_STATUS_UNSPECIFIED = 0;
CEREMONY_STATUS_REGISTERING = 1; // Accepting validator pk_i registrations (no timeout)
CEREMONY_STATUS_DEALT = 2; // DealerTx landed, awaiting acks
CEREMONY_STATUS_CONFIRMED = 3; // All acked (fast path) or >=1/2 acked at timeout, ea_pk ready
}
message CeremonyState {
CeremonyStatus status = 1;
bytes ea_pk = 2; // Set when DealerTx lands
repeated ValidatorPallasKey validators = 3; // All registered pk_i
repeated DealerPayload payloads = 4; // ECIES envelopes from DealerTx
repeated AckEntry acks = 5; // Per-validator ack status
string dealer = 6; // Validator address of the dealer
uint64 phase_start = 7; // Unix seconds when current phase started
uint64 phase_timeout = 8; // Timeout in seconds for current phase
}