Home Reference Source

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'
import axios from 'axios'
import * as Light from './light'

import BBFarmAbi from './smart_contracts/BBFarm.abi.json'

/**
 * 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
}

/**
 * Prepares a transaction for sending with the users web3 browser
 *
 * @param {object} txInfo
 *  Object literal containing the information required to generate the web3 transaction.
 * @param {object} svNetwork
 *  config object containing svNetwork
 *
 * @returns {object}
 *  Returns an object with all fields required to cast the transaction
 */
export const prepareWeb3BBVoteTx = async ({ txInfo }, { svNetwork }) => {
    const { bbFarm, ballotId, userAddress, voteData } = txInfo
    const { web3 } = svNetwork

    assert.equal(web3Utils.isAddress(bbFarm), true, 'BBFarm address supplied is not a valid ethereum address.')
    assert.equal(web3Utils.isAddress(userAddress), true, 'User address supplied is not a valid ethereum address.')
    assert.equal(voteData.length, 66, 'Assertion failed: final hex was not 66 characters long (32 bytes)')

    const BBFarmContract = new web3.eth.Contract(BBFarmAbi, bbFarm)
    const submitVote = BBFarmContract.methods.submitVote(ballotId, voteData, '0x')
    const gasEstimate = await submitVote.estimateGas()
    const abiValue = await submitVote.encodeABI()
    const gasPrice = await Light.getCurrentGasPrice()

    const web3Tx = {
        to: bbFarm,
        data: abiValue,
        gas: web3.utils.toHex((gasEstimate * 1.05) | 0), // 5% added just in case
        gasPrice: gasPrice.average * 1000000000,
        from: userAddress
    }
    return web3Tx
}

export const castProxyVote = async (request, svConfig) => {
    assert.equal(web3Utils.isBN(request.ballotId), true, 'Ballot id is not a BN')
    assert.equal(request.proxyReq.length == 5, true, 'Proxy vote req does not contain the correct number of parameters')
    assert.equal(
        request.hasOwnProperty('extra') && request.hasOwnProperty('democHash'),
        true,
        'Request does not contain extra and democ hash data'
    )

    return new Promise((resolve, reject) => {
        const svApiUrl = svConfig.svApiUrl
        const proxyVotePath = '/sv/light/submitProxyVote'
        const requestUrl = `${svApiUrl}${proxyVotePath}`
        axios
            .post(requestUrl, request)
            .then(response => {
                const { data } = response
                resolve(data)
            })
            .catch(error => {
                console.log('error :', error.response)
                reject(error)
            })
    })
}