BUIP 224: Move VotePeer to Nexa

BUIP 224: Move VotePeer to Nexa
Proposer: Dolaned
Submitted: 2026-02-08

Abstract

VotePeer is an on-chain voting system that BU previously developed on top of BCH that is used to vote on BUIPs proposed by members. BU no longer works on the BCH chain and the current VotePeer solution is both Android-only and no longer downloadable via the google play store requiring a growing number of BU members to have to perform their votes manually via the forum. This proposal establishes a development plan to move VotePeer on to Nexa and replace the Android App with a web-based dApp that utilises Nexa’s on-chain capabilities to make voting on BU governance easier.

Motivation

There has been a lot of back and forth discussion on varying ways to move VotePeer to Nexa, it is clear this should be a priority as the organisation and team scales so the motivation for this BUIP is to bring alignment to the team, and make a commitment to proceed using the outlined technical specification in this proposal.

Overview

VotePeer is a covenant-based on-chain voting system that uses Nexa group tokens, script templates, and covenants to provide transparent, verifiable governance for BUIP proposals. It replaces the current VotePeer Android app with a web-based dApp that enforces ballot restriction at the protocol level: each voter receives exactly one fungible group token whose spending is covenant-restricted to a set of predefined vote-option addresses. Vote tallying reduces to a simple address-balance query, producing an immutable, auditable record on-chain.

An election in VotePeer consists of four on-chain transactions followed by a balance query:

TX 0  Vote Creation        → Parent group token genesis (32-byte groupId)
TX 1  Token Minting        → Mint N voter tokens under the parent group
TX 2  Token Distribution   → Send 1 token per voter to covenant-locked P2ST addresses
TX 3+ Vote Casting         → Each voter spends their token to a vote-option address
      Tally                → Query token balance at each vote-option address
TX 4  Cleanup (optional)   → Voter can send vote token to a did not vote address

Address-Based Tallying

Each vote option (e.g., Accept, Reject, Abstain) is assigned a unique address derived from a covenant script. When a voter casts their ballot, their single group token is transferred to the chosen option’s address. The vote count for any option equals the confirmed token balance at that address:

acceptVotes  = rostrumProvider.getBalance(acceptAddress).confirmed
rejectVotes  = rostrumProvider.getBalance(rejectAddress).confirmed
abstainVotes = rostrumProvider.getBalance(abstainAddress).confirmed

This design requires no transaction parsing or indexer logic beyond standard balance queries.

First time setup

Before beginning votes the president needs to setup the parent group that will be used for all votes on BUIPS, and sets ticker name and token data. All vote tokens will be subgroup tokens under this parent group, so that voters know that the vote tokens are real vote tokens for BU.

Group Token Lifecycle

Step 1 (TX 0) — Election creation. The vote manager creates a subgroup token. The transaction embeds the election name (BUIP name), ticker (BUIP + buip number), and an optional document_url pointing to IPFS metadata for the BUIP.

Step 2 (TX 1) — Mint voter tokens. The creator mints exactly N tokens (one per eligible voter) for the BUIP vote subgroup.

Step 3 (TX 2) — Distribute with covenant. Each voter receives exactly 1 token at a Pay-to-Script-Template (P2ST) address.

Step 4 (TX 3+) — Cast votes. Each voter constructs and broadcasts a transaction spending their token to their chosen vote-option address. the outputs are restricted to the vote option addresses by the template script.

Step 5 (TX 4) — Cleanup. After the voting period ends, the voters or vote creator may spend uncast vote tokens to the “did not vote” address, that way the vote organiser can clean up and has the ability to check who did not vote by checking a single address

Election Metadata via IPFS

Election metadata is stored as a JSON document on IPFS, optionally referenced in the group token’s document_url field:

Below is an example of what the ipfs metadata could be.

{
  "title": "BUIP XXX: Proposal Title",
  "description": "Detailed description of the proposal",
  "options": ["Accept", "Reject", "Abstain"],
  "endHeight": 800000,
  "voteOptionAddresses": {
    "Accept": "nexa:...",
    "Reject": "nexa:...",
    "Abstain": "nexa:..."
  },
  "voterAddresses": ["nexa:voter1", "nexa:voter2"],
  "voterPubkeys": {
    "nexa:voter1": "02abcd...",
    "nexa:voter2": "02ef01..."
  }
}

This enables any participant to reconstruct the election state from a single IPFS hash and the parent group ID, without relying on centralized infrastructure.

Wallet Comms Protocol (WCP) Integration

VotePeer never holds or has access to private keys. All transaction signing is delegated to an external wallet via the wallet-comms-sdk Wallet Comms Protocol:

  1. The dApp constructs unsigned transactions.
  2. The user pairs with their mobile wallet (QR code).
  3. The wallet signs and broadcasts the transaction.
  4. The dApp receives the transaction ID as confirmation.

Wallet Compatibility

VotePeer delegates all transaction signing to external wallets via the Wallet Comms Protocol (WCP) — a WebSocket-based communication layer that allows any wallet to pair with the dApp via QR code. The dApp constructs unsigned transactions and the paired wallet handles signing and broadcasting. VotePeer never holds or has access to private keys.

WCP is wallet-agnostic by design. The wallet-side integration is lightweight, requiring only standard WebSocket connectivity and the ability to parse a pairing URI and respond to signing requests. This can be implemented in any language or platform, making it straightforward for existing Nexa wallets — including those written in Kotlin — to add WCP support.

Work to add wcp support to existing Nexa wallets that do not yet support it is part of this BUIP, ensuring the broadest possible voter participation from day one.

Implementation

The following components are implemented in TypeScript and Vue 3:

Service Layer

Service Responsibility
VotingElectionWallet Election creation, token minting, covenant construction, P2ST distribution
VoterClientWallet Vote casting — UTXO lookup, unlocking script assembly, broadcast
VoteTally Address-balance tallying, turnout calculation, quorum detection, live polling
RostrumQuery Blockchain queries — balance, UTXO, transaction, height, broadcast
IpfsUploader Metadata and vote data upload to IPFS, retrieval, and parsing

UI Components

Component Role
CreateElectionForm Election parameter input, voter registration (address + pubkey CSV), group token creation
MintTokensModal Token minting transaction construction and signing
DistributeTokensModal Covenant-locked distribution to all registered voters
CastVotePanel Vote choice selection, IPFS import, pubkey extraction, vote broadcast
ResultsCard Live tally display with progress bars, turnout percentage, winner detection

Dependencies

Package Version Purpose
libnexa-ts ^1.0.0 Script, Opcode, Hash, Address primitives
nexa-wallet-sdk ^0.8.0 Rostrum provider, transaction builders
wallet-comms-sdk ^0.7.2 Wallet Comms Protocol mobile wallet pairing
vue ^3.4.0 UI framework

The entire codebase uses TypeScript with Tailwind CSS/Nuxt for styling.

Deployment Plan

Phase 1: Pseudonymous Voting (This BUIP)

Phase 1 replaces the existing VotePeer Android app with the web-based dApp described in this proposal. The current implementation provides pseudonymous voting — each voter’s public key hash is embedded in their constraint script, making votes linkable to a known public key but not to a real-world identity.

Limitations: A determined observer who knows the voter-to-pubkey mapping can determine how each member voted after ballots are cast. This is acceptable for BU governance where membership is public, but insufficient for scenarios requiring true ballot secrecy.

Phase 2: Ring Signature Anonymity (Potential future development)

Phase 2 could use ring signatures to enable anonymous voting similar to how the android app does it currently.

Potential Blockchain Voting as a Service Product

In the future, this voting solution can easily be turned into a white label service that BU would be able to provide/sell to other organisations as a Blockchain Voting as a Service product. This future development is out of the scope of this specific BUIP.

1 Like

Thanks for putting this together, it’s great to see VotePeer moving forward on Nexa. A few comments:

Maybe I’m missing something, but why are subgroups needed? If you create a group per BUIP, you can mint tokens for each voter and send them directly. Keep it simple — subgroups would just introduce complexity without much benefit. And without subgroups, the “cleanup” stage becomes redundant because after voting, the group is no longer relevant.

Is there a reason to include voter addresses and pubkeys in the metadata? The metadata should describe the BUIP itself, not the voters. Voter registration data could be managed separately.

If this is only “potential,” I suggest stating that Phase 2 is open for discussion and that this BUIP does not commit to or force that phase.

This BUIP is disingenuous because it is actually proposing to completely discard the existing VotePeer work and do what is a complete reimplementation in a different language, using a fundamentally different mechanism.

It is unstated why we cannot port the existing libraries, especially the valuable (and platform agnostic) ring-signature library. This library already exists, yet it is only proposed as Phase 2 in this BUIP.

The reason this is unstated in this proposal is because we can easily do this port. Except that the developers are refusing to work in the Kotlin programming language, which was a requirement that was clearly communicated to them at the time the services contract was offered.

This is another re-invention of something already invented, without the smallest analysis of and justification based on the existing code. Such an analysis would be step 1 for any honest appraisal.

This proposal also suggests using a wallet to application communication protocol that is redundant to the existing TDPP standard that has existed in Nexa for approximately 4 years.

The creation of this new protocol was a recent unauthorized misuse of Bitcoin Unlimited funds, and is causing incompatibilities within our own ecosystem. It is very bad for Nexa if basic functionality does not interoperate.

Ensuring compatibility with this protocol in our other wallets will push work onto those who are innovating, because those who are copying cannot adhere to (or suggest reasonable improvements on) an existing specification.

As such, I recommend that members vote NO to this proposal.

The subgroup design is intentional, it supports whitelabel deployment. The parent group represents the organization (e.g., Bitcoin Unlimited), and each election is a subgroup under that parent. This means any entity deploying VotePeer gets a single parent group that acts as their organizational identity, with all their elections grouped beneath it. If we use one group per BUIP with no parent, we lose that organizational hierarchy and VotePeer becomes a single purpose BU tool rather than something any organization can deploy.

I’d prefer to keep voter registration data in the IPFS metadata alongside the election parameters. That way anyone can verify the full election state, including who was eligible to vote, using just the parent group ID and the IPFS hash. If voter data lives somewhere else off chain, you’re trusting whoever manages that process, which defeats the point of doing this on chain in the first place.

We have the group token system and contracts on nexa, it would be good to show that these can be used in applications that BU itself use internally, Current system currently relys on google cloud platform and Firebase within the android app, both that and the current votepeerjs have been good resources for this proposal.

The ring signature library uses wasm so this will 100% be utilised

I have contributed to both nifty and wally in the past both being kotlin projects. i have also assisted new team members with coming upto speed with both projects, i was hired via BUIP and no where in my services agreement does it say that i am only allowed to work in Kotlin. My proposal is based on what i think is better for the end user and generally a responsive website should be suffice for this kind of application.

i have been digging through all the code bases for around 12 months and have let solex know that this is a project i have been keen on for quiet some time i have also spoken to the original Votepeer creators about certain parts of the current implementation.

The proposal states that effort and support will be provided to make sure that all current nexa wallets support this new implementation of votepeer.

This is not true, the protocol was made in a relatively short period off BU paid time because it follows similar standards to other crypto DAPPS, i cant speak to it fully as i did not implement it.

i would prefer that members read through the proposal and come to their own conclusion