Decisions
ADR-0022: Visible-vote convergent group polls
- Status: Accepted
- Date: 2026-07-16
Context
A poll is replicated conversation state, not a mutable server object. Komms has no authoritative service that can serialize votes, decide who belonged to a poll, or close it while devices are partitioned. Group history can arrive late, duplicated, or reordered, and membership may change between creation, voting, and closure. A design based on timestamps, arrival order, or a mutable tally would diverge. Calling ordinary authenticated votes anonymous would also be a false privacy claim.
Decision
Komms uses content-v1 kind 0x0006 for three immutable sender-key group
events: creation, vote, and manual closure. All integers are little-endian.
Every payload starts with version(1) || operation(1) || policy/reserved(2).
Creation (operation = 1) carries:
version=1 || operation=1 || close_policy=1 || reserved=0
group_generation(8) || question_len(2) || option_count(1) || voter_count(1)
question(question_len)
repeated option_id(16) || option_text_len(2) || option_text
repeated sorted voter_peer_id(32)The common content id is the stable poll id. Option ids are random and unique within the poll; presentation order is the encoded order. The supported local API snapshots the exact sorted current roster and its generation. The payload declares a creator and electorate because a receiver cannot reconstruct an old roster after arbitrary offline membership changes. The claimed creator must appear in the electorate. Under the current sender-key construction, another member can forge that creator claim.
Vote (operation = 2) carries:
version=1 || operation=2 || reserved(2)=0
poll_author(32) || poll_id(16) || option_id(16) || revision(8)The enclosing shared sender-key ciphertext protects the vote from outsiders.
ADR-0029's recipient wrapper binds the verified voter account/device separately
for every recipient before this poll state is applied. Structurally, a vote is
valid only for a listed electorate member and a stable option id. For each
voter, the current head is the maximum (revision, vote content id). Revisions are positive;
supported local authors increment their own maximum and are capped at 64 vote
events per poll. Duplicate and reordered records therefore converge without
clocks. Votes and voter identities are visible to every member that
holds the poll. Polls are single-choice and explicitly not anonymous.
Closure (operation = 3) carries:
version=1 || operation=3 || reserved(2)=0
poll_author(32) || poll_id(16) || head_count(1) || reserved(3)=0
repeated sorted voter(32) || vote_event_id(16) || option_id(16) || revision(8)The sender field must claim the poll creator to close. Under the current shared sender key, a malicious member can forge that claim; signed owner-moderated closures are a distinct attributable control path. Closure is an irreversible snapshot of the visible vote heads the apparent creator accepted at that moment. This makes the final tally converge even when another replica never received an underlying vote before a member was removed or a partition ended. If multiple structurally valid creator closures exist, the smallest closure content id wins. Closure is not proof that the creator authored it, observed every vote, or counted fairly.
This Accepted ADR defines deterministic replicated poll state. ADR-0029 now supplies recipient-verifiable voter and creator origins around the unchanged shared sender-key ciphertext. That authentication is recipient-deniable and does not make the poll anonymous, fair, complete, or publicly verifiable.
Question text is exact non-empty UTF-8 up to 1,024 bytes. A poll has 2–12 non-empty exact UTF-8 choices of at most 256 bytes each and 1–64 sorted unique voters. Reserved bytes, unknown operations or policies, duplicate option ids, unsorted/duplicate voters, zero revisions, noncanonical lengths, invalid UTF-8, and trailing bytes fail closed. A future poll payload version is retained as unsupported rather than treated as malformed. Decoding is allocation-free and total over arbitrary input.
Membership, compatibility, and storage
The electorate never changes. A later addition does not gain a vote or receive historical poll backfill under the existing group-history rule. Removal does not rewrite a previously accepted or final vote. A removed device retains the history it already received, just as it retains ordinary group messages.
Typed create/vote/close requires every current co-member to advertise
(content v1, kind 0x0006) in authenticated session capabilities. Missing or
old clients block the action before persistence or send. Anonymous first
flights and generic pairwise/group raw-send APIs cannot carry canonical poll
content. Unexpected future or unknown content remains unsupported rather than
being guessed as text.
Poll events remain individually sealed ordinary group-history rows. The node
derives cards, visible vote heads, and tallies on read; it persists no mutable
tally. Current KKR10 carries those rows unchanged, so restart and restore recompute the
same result without a backup version or database migration.
Alternatives considered
- Mutable poll/tally record. Rejected because arrival order would decide state after partitions and restore.
- Wall-clock last vote wins. Rejected because clocks are neither trusted nor consistently ordered.
- Current roster defines eligibility. Rejected because removal would rewrite history and additions could vote without receiving creation.
- Closure marker without vote heads. Rejected because replicas missing a pre-close vote could permanently disagree on the final tally.
- Anonymous label on membership-authenticated votes. Rejected as privacy theater. A separate cryptographic anonymous-voting protocol would be required.
- Creator-only private votes. Rejected for v1 because sender-key fan-out delivers the same group event to all current members and the product needs an honest, inspectable rule.
Consequences
The apparent poll creator determines when to close and declares the final
observed snapshot; Komms guarantees deterministic convergence, not
malicious-member origin, election fairness, or secret ballots. The fixed
electorate and visible identities must be shown before creation and voting.
Parser fuzzing, changed/duplicate/reordered vote tests, membership and old-client
gates, closure conflicts, current KKR10 restore, legacy archive omission of
groups, C2 owned-device convergence, strict
RPC/CLI, UniFFI, and all shell contracts are release requirements. ADR-0029
wrong-recipient/device/context, malicious-member, replay/reorder, rotation,
restore, and announcement-race tests are additionally required for origin
claims.