src/ballotBox.js
const R = require('ramda')
const BN = require('bn.js')
const assert = require('assert')
const web3Utils = require('web3-utils')
const svCrypto = require('./crypto')
const Account = require('eth-lib/lib/account')
/**
* 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 s = new BN(start)
const e = new BN(end)
const sb = new BN(submissionBits)
return sb.shln(64).add(s).shln(64).add(e);
}
/**
* This combines flags into a finished submissionBits. It also does some validation.
* @param {*} 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) => {
if (Array.isArray(toCombine[0]) && typeof toCombine[0][0] == "number") {
console.warn("Warning: mkSubmissionBits does not take an Array<number> anymore.")
toCombine = toCombine[0];
}
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 = {}) => {
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
}
}
/**
* 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 => {
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 => R.join('', R.repeat('0', 3 - vBin.length)) + vBin),
// convert votes to binary
R.map(v => v.toString(2)),
// offset votes to be in range 0,6
R.map(v => 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
};