Skip to content

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.

logo

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.

Other implementations

Contact

Join the Telegram channel