POST /ballots
Broadcasts a transaction to the canonical node. Used for all transaction types
(TxTypePollCreate, TxTypeBallotCast, TxTypePollClose, TxTypeConfirmReceipt,
TxTypeRegisterValidator).
The request body is a JSON-encoded tx.Tx envelope. The canonical state machine performs
all authentication and validation; this endpoint does not authenticate the sender.
Request body
{
"type": 2,
"signature": "<base64-encoded-bytes>",
"data": { ... }
}
Response (success)
Response (failure)
{
"code": 3,
"log": "tx validation failed: duplicate vote: identity abc... already voted in poll poll-1"
}
Casting a ballot (TxType = 2)
The ballot path supports both the preferred non-ZK tier and the optional ZK tier.
Preferred non-ZK tier
{
"type": 2,
"signature": "<voter-device-sig>",
"data": {
"poll_id": "referendum-2026-1",
"choices": ["yes"],
"ballot_nonce": "<random-32-bytes-hex>",
"voter_pub_key": "<ed25519-pub-key-hex>",
"voter_sig": "<base64-ed25519-sig>",
"didit_device_sig": "<ed25519-sig-base64>",
"timestamp": 1700040000
}
}
ZK tier
{
"type": 2,
"signature": "<voter-device-sig>",
"data": {
"poll_id": "referendum-2026-1",
"zk_nullifier": "<MiMC(person_secret, poll_id)>",
"zk_nullifier_proof": "<groth16-proof-base64>",
"zk_commitment": "<MiMC(person_secret)>",
"didit_commitment_sig": "<ed25519-sig-base64>",
"choices": ["yes"],
"ballot_nonce": "<random-32-bytes-hex>",
"timestamp": 1700040000
}
}
Validation sequence in the canonical runtime
tx.Tx.Validate() — stateless: field presence, JSON well-formedness
state.validateBallotCast:
- In the preferred non-ZK tier: verify
voter_sig and provider signature over sha256(voter_pub_key ‖ poll_id)
- In the ZK tier: verify provider signature over
sha256(zk_commitment ‖ poll_id) and verify the Groth16 proof
- Check the active dedup key not already present in
voterRegistry for this poll
state.executeBallotCast:
- Append
VoteRecord to List 1 (keyed by ballot_id = H(ballot_nonce))
- Append
VoterRecord to List 2 (keyed by the active identity hash or nullifier)
- Assert count-match invariant
Steps 1–2 occur in CheckTx (mempool gate) and again in FinalizeBlock.
Which fields are required depends on the active identity tier. If the runtime is
started without a ZK verifier, the non-ZK device-key path is authoritative. If a
ZK verifier is loaded, the ZK fields become required.