Skip to content

feat: support optional memo on votes (soft fork)#1419

Draft
lucasmenendez wants to merge 4 commits into
mainfrom
feat/vote-memo
Draft

feat: support optional memo on votes (soft fork)#1419
lucasmenendez wants to merge 4 commits into
mainfrom
feat/vote-memo

Conversation

@lucasmenendez

@lucasmenendez lucasmenendez commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Consumer side of the VoteEnvelope.memo feature. Depends on vocdoni/dvote-protobuf#75.

What

Adds support for the optional free-text memo field on votes (max 256 bytes), enabling an open "Other" answer. Because the memo is persisted in consensus state and hashed into the vote, it is gated behind a height-activated soft fork so every validator enables it at the same block.

Commits

  1. chore [temporary] activate the local-path replace go.vocdoni.io/proto => ../dvote-protobuf so this PR builds against the unpublished proto. Requires a sibling dvote-protobuf checkout on the Clean workspace before compose test #75 branch with make golang run locally (bindings are CI-generated, not committed).
  2. feat(vochain) consensus core — fork gate (genesis.VoteMemoActive), 256-byte validation + memo copy in VoteTxCheck, persistence + hashing in state.
  3. test(vochain) fork gate, hash determinism, StateDBVote persistence + empty-memo determinism.
  4. feat(api) expose memo in GET /votes/{voteId} and let clients set it via apiclient.VoteData.

Safety / determinism

  • Backward compatible — existing votes never carried a memo, so their signatures (over the whole marshalled Tx) still verify.
  • Safe rollout window — before the fork height the memo is ignored (not stored, not hashed), so an upgraded node produces the same state as a pre-fork node. Vote.Hash() only mixes in the memo when non-empty.
  • optional field handling — the proto field is optional (per Clean workspace before compose test #75 review), so Memo is a *string. An empty memo is stored as nil (absent), never a present empty string, so the marshalled StateDBVote bytes — and thus the state hash — stay identical to a pre-fork vote.
  • The memo is signed (it rides inside the Tx), set on the envelope before marshalling in apiclient.

⚠️ Before merge

  1. Merge/publish dvote-protobuf#75 first. Then, as the final commit here, bump require go.vocdoni.io/proto to the published tag (v1.16.0) and re-comment the local replace (commit 1).
  2. Schedule activation heights in vochain/genesis/forks.go for dev/stage/lts (currently forkNever; only the local vocdoni/TEST/1 chain is active from genesis). Coordinate so every validator runs this binary before the chosen height.

Note: CI will be red until step 1 — the local replace points at a path that only exists in a developer checkout. This is expected for the draft.

Notes

  • GET /votes/{voteId} reads the memo directly from authoritative state (the indexer doesn't store it) — consistent with how it already reaches into state for decryption keys. The memo is intentionally not added to the paginated list endpoint.
  • No transaction-cost change (votes are free).
  • End-to-end coverage on a live devnet is a follow-up; enable it by setting the e2e chain's memo fork height to 0.

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 28508953749

Coverage at 62.835% (no base build to compare)

Details

  • Coverage remained the same as the base build.
  • Patch coverage: 7 uncovered changes across 1 file (20 of 27 lines covered, 74.07%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
vochain/transaction/vote_tx.go 8 1 12.5%
Total (5 files) 27 20 74.07%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 27332
Covered Lines: 17174
Line Coverage: 62.83%
Coverage Strength: 37495.1 hits per line

💛 - Coveralls

Activate the repo's local-path replace so this PR builds against the unpublished
VoteEnvelope.memo field while vocdoni/dvote-protobuf#75 is in review. The final
commit of this PR replaces this with a bump to the published tag
(go.vocdoni.io/proto v1.16.0) and re-comments the directive. DO NOT MERGE while
this replace is present.
Honor the new optional VoteEnvelope.memo free-text field (max 256 bytes):

- genesis: add VoteMemoActive(chainID, height), a per-chain soft-fork gate.
  Heights are placeholders (forkNever) except the local TEST chain; they must
  be scheduled for dev/stage/lts before release, coordinated with the validator
  upgrade so every validator runs this binary before the activation height.
- transaction: in VoteTxCheck, once the fork is active for the chain, validate
  the 256-byte cap and copy the memo onto the state Vote. Before activation the
  memo is ignored (not stored, not hashed) so upgraded nodes produce the same
  state as pre-fork nodes, keeping the rollout window safe.
- state: persist the memo in StateDBVote and include it in Vote.Hash() only when
  non-empty. Because the proto field is `optional`, an empty memo is stored as
  nil (absent), not a present empty string, so the marshaled StateDBVote bytes
  and the state hash stay identical to a pre-fork vote.
- VoteMemoActive: unknown chain inactive, TEST active from genesis, below/at/above
  a scheduled height, and forkNever never active.
- Vote.Hash: empty memo matches the pre-fork baseline, a non-empty memo changes
  the hash deterministically, and DeepCopy preserves the memo.
- AddVote/Vote: a memo survives the StateDBVote round-trip, and an empty memo is
  stored as an absent field (nil), preserving state-hash determinism.
- api: add `memo` to the Vote response and populate it in GET /votes/{voteId}
  from the authoritative state (the memo is not indexed).
- apiclient: add Memo to VoteData and attach it to the VoteEnvelope before the
  tx is marshaled and signed, so it is covered by the vote signature.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants