src/ballotBox.ts
import { ProxyVote } from './types'
const BN = require('bn.js')
import * as R from 'ramda'
import * as assert from 'assert'
import * as web3Utils from 'web3-utils'
import * as svCrypto from './crypto'
/**
* This object tracks the flags used for SV ballot boxes. They determine the submission
* methods and whether ballots are tracked as binding, official, or testing.
*
* For more info see docs.secure.vote
*/
export const flags = {
// flags on submission methods
USE_ETH: 2 ** 0,
USE_SIGNED: 2 ** 1,
USE_NO_ENC: 2 ** 2,
USE_ENC: 2 ** 3,
// other ballot settings
IS_BINDING: 2 ** 13,
IS_OFFICIAL: 2 ** 14,
USE_TESTING: 2 ** 15
}
/**
* Creates a packed copy of start and end times with submissionBits
*
* @param {number} start
* Start time in seconds since epoch
* @param {number} end
* End time in seconds since epoch
* @param {number} submissionBits
* Submission bits - can be created using mkSubmissionBits
* @returns {BN}
* Returns a `bn.js` BigNum of the packed values.
* Format: [submissionBits(16)][startTime(64)][endTime(64)]
*/
export const mkPacked = (start, end, submissionBits) => {
const max64Bit = new BN('ffffffffffffffff', 16)
const s = new BN(start)
assert.equal(
s.lte(max64Bit) && s.gtn(0),
true,
'start time must be >0 and <2^64'
)
const e = new BN(end)
assert.equal(
e.lte(max64Bit) && e.gtn(0),
true,
'end time must be >0 and <2^64'
)
const sb = new BN(submissionBits)
assert.equal(
sb.ltn(2 ** 16) && sb.gtn(0),
true,
'submission bits must be >0 and <2^16'
) // note: submission bits of 0 is invalid
return sb
.shln(64)
.add(s)
.shln(64)
.add(e)
}
/**
* This combines flags into a finished submissionBits. It also does some validation.
* @param {number[]} toCombine
* Array of all submission flags to combine. See SV.ballotBox.flags for flag options.
* All flags must be a power of 2 (which indicates they occupy a single bit in the number when combining).
* @returns {number}
* A 16 bit integer of combined flags.
*/
export const mkSubmissionBits = (...toCombine) => {
const toRet = R.reduce((acc, i) => acc | i, 0, toCombine)
assert.equal(
R.all(i => typeof i == 'number', toCombine),
true,
`Bad input to mkSubmissionBits. Input is required to be an array of numbers. Instead got: ${toCombine}`
)
assert.equal(
R.all(i => i === (i | 0), toCombine),
true,
`Bad input to mkSubmissionBits. Input was not an array of integers. Instead got: ${toCombine}`
)
assert.equal(
toRet,
R.sum(toCombine),
`Bad input provided to mkSubmissionBits. Logical OR and sum sanity check failed. Input was: ${toCombine}`
)
assert.equal(
toRet < 2 ** 16,
true,
`Submission bits must fit into a 16 bit integer (i.e. less than 2^16). Result was: ${toRet}`
)
return toRet
}
/**
* Take the arguments and produce web3 data fitting the `submitProxyVote` method.
* @param {string} ballotId
* a BN.js or Hex ballotId
* @param {number} sequence
* the sequence number to use (0 < sequence < 2^32)
* @param {string} voteData
* the vote data to use, should be 32 bytes hex encoded
* @param {string} extra
* any extra data included with the vote (such as curve25519 pubkeys)
* @param {string} privateKey
* the privkey used to sign
* @param {object?} opts
* options:
* - skipSequenceSizeCheck: boolean (will not throw if sequence is >= 2^32)
* @returns {object}
* { proxyReq (bytes32[5]), extra (bytes) } in the required format for `submitProxyVote`
*/
export const mkSignedBallotForProxy = (
ballotId,
sequence,
voteData,
extra,
privateKey,
opts: any = {}
): ProxyVote => {
if (opts.skipSequenceSizeCheck !== true)
assert.equal(
0 < sequence && sequence < 2 ** 32,
true,
'sequence number out of bounds'
)
assert.equal(
web3Utils.isHexStrict(ballotId) || web3Utils.isBN(ballotId),
true,
'ballotId incorrect format (either not a BN or not hex)'
)
assert.equal(
web3Utils.isHexStrict(voteData),
true,
'vote data is not hex (strict)'
)
assert.equal(
web3Utils.isHexStrict(extra),
true,
'extra param is not hex (strict)'
)
const _ballotId = web3Utils.isBN(ballotId)
? web3Utils.padLeft(web3Utils.toHex(ballotId), 64)
: ballotId
assert.equal(_ballotId.length, 66, 'ballotId incorrect length')
assert.equal(voteData.length, 66, 'voteData incorrect length')
const sequenceHex = web3Utils.padLeft(web3Utils.toHex(sequence), 8)
const messageHash = web3Utils.soliditySha3(
{ t: 'bytes31', v: web3Utils.padLeft(sequenceHex, '62') },
{ t: 'bytes32', v: _ballotId },
{ t: 'bytes32', v: voteData },
{ t: 'bytes', v: extra }
)
const { v, r, s } = svCrypto.ethSignHash(messageHash, privateKey)
const vBytes = web3Utils.hexToBytes(v)
const midBytes = web3Utils.hexToBytes(web3Utils.padRight('0x', 54))
const sequenceBytes = web3Utils.hexToBytes(sequenceHex)
const packed2Bytes = R.concat(vBytes, R.concat(midBytes, sequenceBytes))
const packed2 = web3Utils.bytesToHex(packed2Bytes)
return {
proxyReq: [r, s, packed2, _ballotId, voteData],
extra
}
}
/**
* Verify a signed vote to be submitted via proxy as generated by `mkSignedBallotForProxy`
*
* @param {ProxyVote} proxyVote The ProxyVote object
* @param {*} [opts={}] Not used currently; for future options
* @returns {{verified: bool, address: EthAddress}}
*/
export const verifySignedBallotForProxy = (
proxyVote: ProxyVote,
opts: any = {}
) => {
const {
proxyReq: [r, s, packed2, ballotId, voteData],
extra
} = proxyVote
const p2Bytes = web3Utils.hexToBytes(packed2)
const v = web3Utils.bytesToHex(p2Bytes.slice(0, 1))
const seqNum = web3Utils.bytesToHex(p2Bytes.slice(27, 32))
const messageHash = web3Utils.soliditySha3(
{ t: 'bytes31', v: web3Utils.bytesToHex(p2Bytes.slice(1)) },
{ t: 'bytes32', v: ballotId },
{ t: 'bytes32', v: voteData },
{ t: 'bytes', v: extra }
)
return svCrypto.ethVerifySig(messageHash, [v, r, s])
}
/**
* Prepares voteData for a Range3 ballot from an array of votes
*
* @param {array} votesArray
* Takes an array of numbers which represent the votes to be transformed
* Format: [1, 2, -1]
*
* @returns {string}
* Returns an eth hex string of the vote data
*/
export const genRange3VoteData = (votesArray: number[]) => {
assert.equal(
R.all(v => (v | 0) === v, votesArray),
true,
'All array elements must be defined and integers.'
)
assert.equal(
R.all(v => -3 <= v && v <= 3, votesArray),
true,
'Votes must be in range -3 to 3.'
)
assert.equal(
votesArray.length <= 85,
true,
'Too many votes; maximum capacity of 32 bytes is 85 individual items.'
)
// Generate list of binary encoded votes. Read bottom to top.
const binaryVotes = R.compose(
// pad to 3 bits
R.map((vBin: string) => R.join('', R.repeat('0', 3 - vBin.length)) + vBin),
// convert votes to binary
R.map((v: number) => v.toString(2)),
// offset votes to be in range 0,6
R.map((v: number) => v + 3)
)(votesArray)
// check we have converted votes to bitstring representation of length 3
assert.equal(
R.all(bVote => bVote.length == 3, binaryVotes),
true,
'Assertion failed: all binary-encoded votes should be 3 bits long'
)
// create the binary voteData
const rawBinVotes = R.join('', binaryVotes)
// and pad it with 0s to length 256 (32 bytes total)
const binVoteData =
rawBinVotes + R.join('', R.repeat('0', 32 * 8 - rawBinVotes.length))
assert.equal(
binVoteData.length,
256,
'Assertion failed: generated voteData bit string does not have length 256'
)
// Convert to bytes
const voteBytes = R.map(
bStr => parseInt(bStr, 2),
R.splitEvery(8, binVoteData)
)
// check bytes are in range
assert.equal(
R.all(vByte => 0 <= vByte && vByte <= 255, voteBytes),
true,
'Assertion failed: voteBytes (byte array) had a byte out of bounds (<0 or >255)'
)
// generate final hex
const voteData = web3Utils.bytesToHex(voteBytes)
assert.equal(
voteData.length,
66,
'Assertion failed: final hex was not 66 characters long (32 bytes)'
)
return voteData
}