Ballot Encoding Specification
This document specifies the ballot encoding used to transform human-readable ballot selections into the compact numeric representation that is later serialized, encrypted, audited, decoded, and tallied.
The goal of this specification is to be implementation-neutral. Any compatible encoder or decoder should be able to interoperate as long as it follows the rules below.
1. Design goals
The codec is designed to satisfy the following requirements:
- Determinism — the same ballot configuration and the same voter selections must always produce the same encoded values.
- Compactness — the encoded ballot must fit in a fixed-size encrypted payload.
- Reversibility — decoding must reconstruct the original ballot semantics, including explicit invalid and explicit blank markers.
- Auditability — the representation must be stable enough for independent verifiers and external implementations.
- Policy preservation — the codec must preserve the distinctions required
by ballot validation and tallying, especially the difference between:
- a normal vote,
- an explicit invalid vote,
- an implicit blank vote,
- an explicit blank vote.
2. Terms
2.1 Contest
A contest is a single voting unit within a ballot. Each contest has its own:
- candidate list,
- counting algorithm,
- minimum and maximum selections,
- validation policies,
- optional write-in support.
2.2 Candidate classes
Each candidate belongs to exactly one of the following logical classes for encoding purposes:
- Normal candidate
- Explicit invalid marker
- Explicit blank marker
- Write-in candidate
2.3 Ballot states relevant to encoding
For each contest, the codec must distinguish the following states:
- Normal vote: one or more normal candidates are selected
- Explicit invalid: the contest is deliberately marked invalid
- Implicit blank: no selectable option is chosen
- Explicit blank: the dedicated blank option is chosen
3. Global rules
3.1 Stable ordering
Whenever a sequence of candidates must be encoded positionally, candidates are ordered by candidate identifier in ascending order unless a more specific rule is defined for a different codec mode.
3.2 Uniqueness constraints
A valid contest configuration may contain:
- at most one explicit invalid marker
- at most one explicit blank marker
Configurations that violate either rule are invalid and must be rejected before the voter is allowed to proceed. An implementation must not silently choose one marker and ignore the others.
3.3 Acclaimed contests
A contest with is_acclaimed: true is part of the displayed ballot
configuration but is not part of the encoded ballot. Encoders and decoders must
exclude acclaimed contests before ordering contests, constructing mixed-radix
bases, validating selections, or comparing decoded contests.
Consequently:
- an acclaimed contest contributes no choice, radix position, ciphertext, or decoded contest;
- adding or removing an acclaimed contest does not change the encoded value of the remaining votable contests;
- a selection-state entry for an acclaimed contest is ignored at the encoding boundary; and
- a missing or false
is_acclaimedvalue means the contest is votable, which preserves ballot styles created before this field existed.
Applications may merge acclaimed contests from the embedded ballot configuration into review or verification screens, but these entries are presentation-only and must never be added to decoded ballot data.
The ballot configuration is still embedded in the auditable ballot and covered by its hash. Changing an acclaimed contest after publication can therefore change or invalidate Ballot IDs even though the contest has no plaintext slot.
3.4 Mixed-radix representation
The codec produces two aligned vectors:
bases: the radix of each positionchoices: the encoded value stored at that position
The encoded integer is obtained by interpreting choices under the positional
radices defined by bases.
4. Single-contest codec
The single-contest codec is the reference format for one contest encoded into a fixed ballot payload.
4.1 Layout overview
The encoded vector is composed in this order:
- explicit invalid flag
- explicit blank flag, if the contest defines an explicit blank marker
- ordinary candidate section
- write-in text section, if write-ins are enabled
The fixed-size part of the vector therefore contains:
1
+ (1 if the contest defines an explicit blank marker else 0)
+ number_of_ordinary_candidates
positions, followed by the variable-length write-in section.
4.2 Explicit invalid flag
The first position is always reserved for the explicit invalid flag.
- base:
2 - values:
0= contest is not explicitly invalid1= contest is explicitly invalid
This flag is orthogonal to all candidate slots.
4.3 Explicit blank flag
If the contest defines an explicit blank marker, the second position is reserved for the explicit blank flag.
- base:
2 - values:
0= explicit blank not selected1= explicit blank selected
The explicit blank marker is not encoded as an ordinary candidate choice. It has its own dedicated slot so that an explicit blank vote remains distinct from an implicit blank vote.
4.4 Ordinary candidate section
The ordinary candidate section contains all candidates that are not:
- explicit invalid markers, and
- explicit blank markers.
These positions encode the actual voted options.
4.4.1 Plurality-style contests
For plurality-style contests, each ordinary candidate receives one slot.
- base:
2 - values:
0= not selected1= selected
4.4.2 Ranked or preferential contests
For preferential contests, each ordinary candidate receives one slot whose base is:
max_votes + 1
Values are interpreted as:
0= candidate not ranked1= first usable rank2= second usable rank...n= rankn
The zero value is therefore reserved for absence of selection, and positive values encode rank plus an offset of one.
4.4.3 Cumulative contests
For cumulative contests, each ordinary candidate slot uses:
cumulative_number_of_checkboxes + 1
The zero value means no selection, and positive values represent the configured selection strength according to the contest’s cumulative policy.
4.5 Write-in section
If write-ins are enabled, a text section is appended after the candidate section.
Each write-in string is encoded as:
- zero or more character values using the configured character map
- a terminating zero value
Therefore each write-in occupies a variable-length subsequence ending in a sentinel zero.
An empty write-in is encoded as only the terminating zero.
5. Meaning of blank votes
5.1 Implicit blank
A contest is an implicit blank vote if:
- the explicit invalid flag is
0 - the explicit blank flag is either absent or
0 - all ordinary candidate slots are unset
- all write-in values are empty
5.2 Explicit blank
A contest is an explicit blank vote if:
- the explicit blank flag is
1 - no ordinary candidate is selected
The explicit blank state must survive round-trip encode/decode unchanged.
5.3 Tallying interpretation
For tallying purposes:
- implicit blank and explicit blank both contribute to the overall blank total
- explicit blank must remain distinguishable from implicit blank in decoded and reported data
- explicit blank must not be counted as a normal candidate vote
- a contest that selects explicit blank together with an ordinary candidate is an implicit invalid vote, not a blank vote
6. Meaning of explicit invalid votes
An explicit invalid vote is represented only by the explicit invalid flag.
It is not encoded as a candidate selection and must remain distinct from:
- overvotes,
- undervotes,
- implicit blank votes,
- explicit blank votes.
7. Decoding rules for the single-contest codec
A decoder must process the encoded vector in the following order:
- read the explicit invalid flag
- if present, read the explicit blank flag
- decode the ordinary candidate section
- decode the write-in section
- reconstruct the semantic contest state
7.1 Reconstruction requirements
After decoding:
- an explicit invalid flag must reconstruct the explicit invalid state
- an explicit blank flag must reconstruct the dedicated blank candidate as selected
- ordinary candidate slots must reconstruct normal candidate selections
- write-in text must reconstruct the original text sequence
8. Validation rules
Validation happens at two distinct levels and both are required.
8.1 Configuration validation
Before any ballot is cast, the contest definition must be checked for structural consistency, including:
- no more than one explicit invalid marker
- no more than one explicit blank marker
- valid numeric constraints for minimum and maximum votes
- any other constraints required by the contest type
If configuration validation fails, the voting flow must stop before casting.
8.2 Ballot-state validation
After decoding, the resulting selections are evaluated against the contest policies, including:
- minimum selections
- maximum selections
- invalid vote policy
- blank vote policy
- undervote and overvote policies
- duplicated rank policy
- preference-gap policy
An explicit blank vote must still be treated as a blank vote when applying blank vote policy. It must not be treated as a normal candidate selection.
For the minimum-selections rule, a set explicit invalid flag or a set explicit blank flag counts as one selection: a contest that is deliberately marked invalid or blank must not additionally be reported as under the minimum.
9. Error conditions
Compatible implementations must detect and report at least the following error classes:
9.1 Configuration errors
- more than one explicit invalid marker
- more than one explicit blank marker
- invalid voting bounds
- a generated multi-contest ballot style whose maximum encoded size exceeds the platform's fixed-size payload (Section 11.2)
9.2 Structural decoding errors
- too few encoded positions
- values outside the allowed base range
- incomplete write-in terminators
- trailing unexpected data
- invalid character-map decoding
9.3 Semantic ballot errors
- overvote
- undervote
- disallowed explicit invalid vote
- duplicated preference positions
- gaps in ranked preferences
10. Canonical examples
Assume a plurality contest with:
- one explicit invalid marker
- one explicit blank marker
- two normal candidates
- no write-ins
The base vector is:
[2, 2, 2, 2]
The positions mean:
- explicit invalid flag
- explicit blank flag
- first ordinary candidate
- second ordinary candidate
10.1 Implicit blank
choices = [0, 0, 0, 0]
10.2 Explicit blank
choices = [0, 1, 0, 0]
10.3 Normal vote for the first ordinary candidate
choices = [0, 0, 1, 0]
10.4 Explicit invalid
choices = [1, 0, 0, 0]
11. Multi-contest codec
The platform also defines a compact multi-contest codec for bundling several plurality contests into one encoded payload.
11.1 Scope
This codec is intended for:
- plurality-at-large contests only
- no write-ins
- an optional ballot-level decline-to-vote flag
- one explicit invalid flag per contest
- one explicit blank flag per contest when the contest defines an explicit blank marker
11.2 Encoding mode
Each ballot style persists an encoding mode that determines how many ordinary
candidate slots (vote_slot_count) each contest reserves:
legacy:vote_slot_countequalscontest.max_votes. A selection with more ordinary candidates thanmax_votescannot be encoded.expanded-capacity:vote_slot_countequals the contest's number of ordinary candidates, so a selection can include every ordinary candidate.
The mode is resolved once per election, when its ballot styles are generated:
expanded-capacity applies to every contest in the election if any contest
in that election has an over-vote policy of allowed, allowed-with-msg, or
allowed-with-msg-and-alert — the policies that let a voter select more than
max_votes in the first place. Otherwise the election uses legacy.
The mode must be resolved once and persisted on the ballot style, not re-derived from a contest's current over-vote policy at encode or decode time. Encoding and decoding of the same ballot style can happen far apart (a ballot may be decoded for tallying long after it was cast), so re-deriving the mode from live configuration risks disagreement between the encode-time and decode-time layouts if that configuration changes in between.
Maximum-selections and overvote validation (Section 8.2) always uses
contest.max_votes, independently of the encoding mode: expanded-capacity
only widens what the codec can represent; it does not change what counts as
a valid vote. A selection wider than max_votes is still reported as an
overvote error — expanded-capacity only keeps that overvote from also being
an encoding failure.
Because a ballot style's total encoded size must still fit the platform's fixed-size encrypted payload, election publication must validate the maximum encoded size of every generated ballot style and reject publication if any contest's expanded slot count would exceed that limit.
11.3 Layout
The multi-contest representation is:
- one ballot-level decline-to-vote flag, when decline-to-vote is enabled
- a contiguous block of fixed-size contest segments, sorted by contest identifier
Each contest segment is encoded as:
- explicit invalid flag
- explicit blank flag, if the contest defines an explicit blank marker
vote_slot_countsparse ordinary-candidate positions (Section 11.2)
The number of positions is therefore:
(1 if decline-to-vote is enabled else 0)
+ number_of_contests
+ number_of_contests_with_explicit_blank_markers
+ sum(contest.vote_slot_count)
11.4 Slot meaning
The explicit invalid and explicit blank flags use base 2.
Each sparse ordinary-candidate position uses:
number_of_ordinary_candidates + 1
Values are interpreted as:
0= unused slot1..n= selected candidate position plus one
Unlike the single-contest codec, the multi-contest codec is sparse: it
stores up to vote_slot_count selected candidate references rather than one
boolean slot per ordinary candidate.
Explicit invalid and explicit blank marker candidates are not part of the sparse ordinary-candidate positions. Selecting one of those marker candidates sets the corresponding bit instead, and decoding that bit reconstructs the marker candidate as selected.
11.5 Decoding
To decode a multi-contest ballot:
- read the ballot-level decline-to-vote flag, when present
- for each contest in contest identifier order, read the explicit invalid flag
- read the explicit blank flag when the contest defines an explicit blank marker
- decode each non-zero sparse slot back to its ordinary candidate reference
- reconstruct explicit invalid and explicit blank marker candidates from their bits
- apply the same semantic validation rules described earlier
A decoder must use the same encoding mode (Section 11.2) that was used to encode the ballot style, read from the persisted ballot style rather than re-derived, or it will misread the sparse ordinary-candidate positions.
11.6 Illustrative capacity limits
Contest segments compose multiplicatively, not additively (Section 11.4), so
the number of contests needed to exceed the platform's MAX_SIZE_BYTES limit
(29 bytes, Section 11.2) falls quickly as each contest's candidate count
grows. The table below assumes single-choice (max_votes = 1)
plurality-at-large contests with no decline-to-vote flag and no explicit
blank markers — the smallest possible per-contest footprint of one explicit
invalid bit plus one ordinary-candidate slot.
| Candidates per contest | Contests needed to exceed 29 bytes |
|---|---|
| 2 | 90 |
| 5 | 65 |
| 10 | 53 |
| 20 | 44 |
| 50 | 35 |
| 100 | 31 |
12. Interoperability requirements
An implementation is compatible with this specification only if it:
- preserves the distinction between explicit blank and implicit blank
- preserves explicit invalid markers
- uses the same stable candidate ordering
- applies the same positional offsets and zero-value conventions
- rejects invalid configurations with duplicated explicit markers
- decodes write-ins using the same delimiter semantics
- applies policy validation after decoding
13. Backward-compatibility expectation
Consumers that only understand the aggregate blank count may continue to use the consolidated blank total. Consumers that need full semantic fidelity must retain the explicit-versus-implicit blank distinction exposed by decoding and tally results.