votepeerjs - VotePeer Javascript Library
Javascript library with support, or partial support for VotePeer contracts. This library was developed for use with the VotePeer web interface, VotePeer API backend and for research.

The following vote contracts support is added to this library, on Bitcoin Cash (BCH) and Nexa.
| Vote contract | Tally votes | Validate votes | Casting vote (BCH) | Casting vote (Nexa) |
|---|---|---|---|---|
| two-option-vote | ✅ | ✅ | ✅ | ✅ |
| multi-option-vote | ✅ | ✅ | ❌ | ✅ |
| ring-signature-vote | ✅ | ✅ | ❌ | ❌ |
The non-vote contract input payload contract is also implemented in this library. See VotePeer documentation to learn about the contracts.
Features
Transparent Voting
A Bitcoin Cash smart contract and protocol for transparent on-chain voting.
"Two option vote" is a minimalistic and transparent voting protocol on top of Bitcoin Cash and similar blockchains. Participating, verifying and tallying does not require a full node, but is fully SPV client compatible. Votes are non-transferable.
This repository hosts the protocol/smart contract specification and a reference implementation in javascript.
Example
See this and more examples in the examples directory.
import {
TwoOptionVote, TwoOptionVoteContract, tallyTwoOptionVotes, Blockchain,
DEFAULT_CAST_TX_FEE, derivePublicKey, getPublicKeyHash,
DEFAULT_DUST_THRESHOLD, createSalt, generatePrivateKey
} from '@bitcoinunlimited/votepeerjs';
const alice = generatePrivateKey();
const bob = generatePrivateKey();
// Initialize an election
const votersPKH = [
await getPublicKeyHash(await derivePublicKey(alice)),
await getPublicKeyHash(await derivePublicKey(bob)),
];
const election: TwoOptionVote = {
network: 'mainnet',
salt: createSalt(),
description: 'Pizza for lunch?',
optionA: 'Yes',
optionB: 'No',
endHeight: 1_000_000,
votersPKH,
};
// Setup contracts
const aliceContract = await TwoOptionVoteContract.make(election, alice);
const bobContract = await TwoOptionVoteContract.make(election, bob);
const electrum = new Blockchain();
await electrum.connect();
for (const contract of [aliceContract, bobContract]) {
const contractAddress = await contract.getContractAddress();
await contract.waitForBalance(electrum, DEFAULT_CAST_TX_FEE + DEFAULT_DUST_THRESHOLD, (balance) => {
console.log(`Too low balance (${balance} sats) in contract ${contractAddress}`);
});
}
// Alice casts vote for option A ("Yes") and bob for B ("No")
const aliceTxID = await aliceContract.castVote(electrum, await aliceContract.optionAHash());
const bobTxID = await bobContract.castVote(electrum, await bobContract.optionBHash());
console.log(`Alice voted in tx ${aliceTxID}`);
console.log(`Bob voted in tx ${bobTxID}`);
const tally = await tallyTwoOptionVotes(electrum, election, true);
console.log(`Results: ${JSON.stringify(tally, null, 4)}`);
Nexa
Pass chain: 'nexa' to use Nexa instead of Bitcoin Cash. BCH stays the default.
const nexa = new Blockchain({ chain: 'nexa' }); // rostrum servers at electrum.nexa.org
await nexa.connect();
// Two-option vote
const election: TwoOptionVote = { ...electionAsAbove, chain: 'nexa' };
// Multi-option and ring signature votes take an options argument
const mov = new MultiOptionVote(salt, 'Lunch?', beginHeight, endHeight,
['Pizza', 'Sushi'], participantsPKH, { chain: 'nexa' });
await mov.castVote(nexa, voterPrivateKey, await hash160Salted(salt, Buffer.from('Sushi')));
const result = await fetchMultiOptionVotes(nexa, mov);
On Nexa, the vote contracts are script templates rather than P2SH scripts.
Every contract of one kind shares a single template (pinned in
lib/nexa/contracts.ts, byte-identical to
libvotepeer). The per-voter
parameters, such as the election ID and the voter's public key hash, are
visible constraint arguments, so they can be read off an unspent contract
output. Contract addresses are pay-to-script-template (nexa:np…) addresses.
| Contract | Template hash | Visible arguments |
|---|---|---|
| two-option-vote | c0b66a66… |
proposal ID, option B, option A, voter PKH |
| multi-option-vote | 807ce674… |
election ID, voter PKH |
| payload contract (ring-signature-vote) | 80bd0a68… |
payload hash, owner public key |
The ring-signature vote's notification address is a payload contract output with an unspendable owner key, so the election ID is visible on chain.
Notes:
- Voters fund and receive change on their P2PKT (
nexa:nq…) address. - Nexa transactions are identified by their txid, not the idem. The ballot lives in the satisfier, which the idem does not cover, so multi-option-vote tallies on Nexa only count confirmed ballots.
Developers
Tests vectors
See files with *.test.ts suffix.
Build project
npm run build
npm run test
Run the above example: node build/examples/readme-example.js
Building Fujisaki Ring signature dependency
For building the fujisaki ring signature dependency, you'll need to install wasm-pack and Rust.
When these dependencies are installed, run:
npm run build-ringsig
To use your new build, rm -rf node_modules/fujisaki-ringsig-wasm && npm install.