1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477 |
- /* Import core modules. */
- const _ = require('lodash')
- const bch = require('bitcore-lib-cash')
- const debug = require('debug')('shuffle:round')
- const EventEmitter = require('events').EventEmitter
- const Nito = require('nitojs')
- /* Import local modules. */
- const cryptoUtils = require('./cryptoUtils.js')
- const coinUtils = require('./coinUtils.js')
- /* Import CommChannel (Class). */
- const CommChannel = require('./CommChannel.js')
- /**
- * Shuffle
- *
- * The de-facto unbiased shuffle algorithm is the Fisher-Yates
- * (aka Knuth) Shuffle. (see: https://github.com/coolaj86/knuth-shuffle)
- */
- const _shuffle = function (array) {
- var currentIndex = array.length, temporaryValue, randomIndex
- // While there remain elements to shuffle...
- while (0 !== currentIndex) {
- // Pick a remaining element...
- randomIndex = Math.floor(Math.random() * currentIndex)
- currentIndex -= 1
- // And swap it with the current element.
- temporaryValue = array[currentIndex]
- array[currentIndex] = array[randomIndex]
- array[randomIndex] = temporaryValue
- }
- return array
- }
- /* Initialize magic number. */
- // const magic = Buffer.from('42bcc32669467873', 'hex')
- /**
- * Delay (Execution)
- */
- const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))
- /**
- * Shuffle Round (Class)
- */
- class ShuffleRound extends EventEmitter {
- constructor (clientOptions) {
- super()
- /* Initialize client options. */
- for (let oneOption in clientOptions) {
- this[oneOption] = clientOptions[oneOption]
- }
- /* Initialize done flag. */
- this.done = false
- /* Initialize phase. */
- this.phase = ''
- /* Initialize util. */
- // TODO: Rename to `utils`.
- this.util = {
- /* Tools for encryption and message sign / verify. */
- crypto: cryptoUtils,
- /* Tools that make REST calls for blockchain data. */
- coin: coinUtils
- }
- /* Initialize ephemeral keypair. */
- // NOTE: A public and private keypair, destroyed at the end of a shuffle
- // round. Its only purpose is to sign and verify protocol
- // messages to ensure the participants aren't being
- // cheated / attacked by the server or each other.
- this.ephemeralKeypair = this.util.crypto.generateKeypair()
- /* Initialize encryption keypair. */
- // NOTE: A public and private keypair, destroyed at the end of a shuffle
- // round. It's used to encrypt and decrypt message fields during
- // the shuffle round so they are kept private from the server and
- // the other players in the round.
- this.encryptionKeypair = this.util.crypto.generateKeypair()
- /* Initialize hooks. */
- this.hooks = this.hooks || {}
- /* Validate shuffled hook. */
- if (!_.isFunction(this.hooks.shuffled)) {
- console.error(`A valid shuffle address generation hook was not provided!`) // eslint-disable-line no-console
- throw new Error('BAD_SHUFFLE_FN')
- }
- /* Initialize shuffled. */
- this.shuffled = this.hooks.shuffled()
- /* Validate change hook. */
- // NOTE: Make sure either a change generation function or change
- // keypair object was provided. Use the keypair, if we got both.
- if (!_.isFunction(this.hooks.change)) {
- console.error(`A valid change generation hook was not provided!`) // eslint-disable-line no-console
- throw new Error('BAD_CHANGE_FN')
- }
- /* Initialize change. */
- this.change = this.hooks.change()
- /* Initialize (shuffle) players. */
- // NOTE: This is where we keep our representation of all the shufflers
- // in the round (including us).
- this.players = []
- /* Initialize output addresses. */
- // NOTE: Once we reach the "shuffle" phase, this array will house the
- // addresses that each player's shuffled coins will be sent to.
- this.outputAddresses = []
- /* Initialize shuffle transaction. */
- // NOTE: Used to store the partially signed transaction after it
- // is generated but before its broadcasted to the network.
- this.shuffleTx = {
- isBuilding: false,
- // NOTE: We will add each signature and input data to this
- // collection as it's received during the verification
- // and submission phase.
- signatures: []
- }
- /* Initialize round completion flag. */
- this.roundComplete = false
- /* Initialize success flag. */
- this.success = false
- /* Initialize round error. */
- // NOTE: This object will be extended with error data in the event
- // that the round ends unexpectedly for any reason. This
- // includes a protocol error on behalf of any player in the
- // round (ourselves included) as well as if an exception is
- // thrown in this library.
- this.roundError = {
- // shortCode: 'BAD_SIG',
- // errorObject: [ Error instance containing a stacktrace ],
- // isProtocolError: true,
- // isException: false,
- // accusedPlayer: [ Object containing player data ]
- }
- /* Initialize communications channel. */
- this.comms = new CommChannel({
- serverUri: this.serverUri
- }, this)
- /* Handle server message. */
- this.comms.on('serverMessage', async (someServerMessage) => {
- try {
- await this.actOnMessage(someServerMessage)
- } catch (nope) {
- console.error('Failed to act right in response to server message:', nope) // eslint-disable-line no-console
- this.writeDebugFile()
- }
- })
- /* Handle protocol violation. */
- this.comms.on('protocolViolation', this.assignBlame.bind(this))
- /* Handle connection error. */
- this.comms.on('connectionError', this.handleCommsError.bind(this))
- /* Handle disconnection. */
- this.comms.on('disconnected', (commsDisconnectMessage) => {
- /* Validate round completion. */
- if (this.roundComplete) {
- debug('The shuffle Round has completed!')
- } else {
- /* eslint-disable-next-line no-console */
- console.error('Our connection to the CashShuffle server is REKT!')
- /* Set success flag. */
- this.success = false
- /* Set round copmletion flag. */
- this.roundComplete = true
- /* Update round error. */
- Object.assign(this.roundError, {
- shortCode: 'COMMS_DISCONNECT',
- errorObject: new Error(commsDisconnectMessage),
- isProtocolError: false,
- isException: false
- })
- /* End shuffle round. */
- this.endShuffleRound()
- }
- })
- /* Handle connection. */
- // this.comms.on('connected', (socket) => {
- // debug('socket', socket)
- this.comms.on('connected', () => {
- /* Set round phase. */
- this.phase = 'registration'
- this.emit('phase', 'registration')
- try {
- // console.log(
- // '\nSENDING REGISTRATION MESSAGE',
- // this.protocolVersion,
- // this.poolAmount,
- // this.ephemeralKeypair.publicKey
- // )
- this.comms
- .sendMessage(
- 'registration',
- this.protocolVersion,
- this.poolAmount,
- this.ephemeralKeypair.publicKey
- )
- } catch (nope) {
- console.error('Couldnt send registration message:', nope.message) // eslint-disable-line no-console
- }
- })
- this.ready()
- .catch((nope) => {
- console.error('ERROR:', nope) // eslint-disable-line no-console
- })
- .then(() => {
- //
- })
- return this
- }
- /**
- * Handle Communications Error
- */
- handleCommsError (someError) {
- /* eslint-disable-next-line no-console */
- console.error('Something has gone wrong with our communication channel:', someError.message)
- /* Update round error. */
- this.roundError = {
- shortCode: 'COMS_ERR',
- errorObject: someError,
- isProtocolError: false,
- isException: true
- }
- /* End shuffle round. */
- this.endShuffleRound()
- }
- /**
- * Ready
- */
- async ready () {
- this.emit('debug', { message: 'beginning-round' })
- /* Setup server connection. */
- try {
- await this.comms.connect()
- } catch (nope) {
- console.error('Failure!', nope) // eslint-disable-line no-console
- throw nope
- }
- }
- /**
- * Act On Message
- *
- * Process incoming websocket events which contain the prototype buffer
- * encoded server messages.
- */
- async actOnMessage (jsonMessage) {
- if (typeof window !== 'undefined') {
- /* eslint-disable-next-line no-console */
- console.log('Act on message (jsonMessage):', jsonMessage.pruned.message)
- } else {
- debug('Act on message (jsonMessage):', jsonMessage.pruned.message)
- }
- this.emit('notice', jsonMessage.pruned.message)
- /* Set message type. */
- const messageType =
- jsonMessage.pruned.message && jsonMessage.pruned.messageType
- /* Validate message type. */
- if (!messageType) {
- throw new Error('BAD_MESSAGE_PARSING')
- }
- /* Set message. */
- const message = jsonMessage.pruned.message
- /* Initialize new phase name. */
- let newPhaseName
- // debug('Attempting to act on', messageType, 'message\n\n');
- /* Handle message type. */
- switch (messageType) {
- /**
- * The server has informed us of the number of players currently in the
- * pool. This fires every time a player joins or leaves.
- *
- * NOTE: We always get one along without server greeting.
- */
- case 'playerCount':
- /* Set number of players. */
- this.numberOfPlayers = Number(message['number'])
- break
- /**
- * The server has accepted our pool registration message and replied
- * with our player number and a session id to identify us within this
- * pool and round.
- */
- case 'serverGreeting':
- /* Set our player number. */
- this.myPlayerNumber = Number(message['number'])
- /* Set our session id. */
- this.session = message['session']
- break
- /**
- * This is a message sent to all players to inform them that it's now
- * time to share their change address as well as their second ephemeral
- * public key (later used to decrypt the encrypted output addresses).
- */
- case 'announcementPhase':
- /* Set new phase name. */
- newPhaseName = _.isString(
- message['phase']) ? message['phase'].toLowerCase() : undefined
- /* Validate new phase name. */
- if (newPhaseName && newPhaseName === 'announcement') {
- /* Set phase. */
- this.phase = 'announcement'
- this.emit('phase', 'announcement')
- /* Set number of players. */
- this.numberOfPlayers = Number(message['number'])
- try {
- this.broadcastTransactionInput()
- } catch (nope) {
- console.error('Error broadcasting broadcastTransactionInput:', nope) // eslint-disable-line no-console
- }
- } else {
- console.error('Problem with server phase message') // eslint-disable-line no-console
- if (_.get(jsonMessage, 'packets[0].packet.fromKey.key')) {
- this.assignBlame({
- reason: 'INVALIDFORMAT',
- accused: _.get(jsonMessage, 'packets[0].packet.fromKey.key')
- })
- }
- }
- break
- case 'incomingVerificationKeys':
- try {
- await this.addPlayerToRound(message)
- } catch (nope) {
- console.error('Error broadcasting broadcastTransactionInput:', nope) // eslint-disable-line no-console
- }
- // If we've received the message from all players (including us)
- // containing their `verificationKey` and the coin they wish to
- // shuffle, send the next protocol message if we are player one.
- if (this.myPlayerNumber === _.get(_.minBy(this.players, 'playerNumber'), 'playerNumber')) {
- try {
- await this.announceChangeAddress()
- } catch (nope) {
- console.error('Error broadcasting changeAddress:', nope) // eslint-disable-line no-console
- this.endShuffleRound()
- }
- }
- break
- case 'incomingChangeAddress':
- /* Validate change address announcement. */
- // NOTE: If we are player one, we will have already sent
- // this message.
- if (!this.comms.outbox.sent['changeAddressAnnounce']) {
- await this.announceChangeAddress()
- }
- debug('Act on message (incomingChangeAddress):',
- 'Encryption pubkey', message['message']['key']['key'],
- 'Legacy address', message['message']['address']['address']
- )
- console.log('Act on message (incomingChangeAddress):',
- 'Encryption pubkey', message['message']['key']['key'],
- 'Legacy address', message['message']['address']['address']
- )
- /* Update this player with their change address. */
- Object.assign(
- this.players[_.findIndex(this.players, { session: message['session'] })], {
- encryptionPubKey: message['message']['key']['key'],
- change: {
- legacyAddress: message['message']['address']['address']
- }
- }
- )
- /**
- * If we are player 1, go ahead and send the first encrypted
- * unicast message containing the Bitcoin address that will
- * house our shuffled output. This function will return without
- * doing anything unless all players.
- */
- if (_.get(_.minBy(this.players, 'playerNumber'), 'playerNumber') === this.myPlayerNumber) {
- this.phase = 'shuffle'
- this.emit('phase', 'shuffle')
- try {
- await this.forwardEncryptedShuffleTxOutputs(undefined, undefined)
- } catch (nope) {
- console.error('Error broadcasting changeAddress:', nope) // eslint-disable-line no-console
- this.endShuffleRound()
- }
- }
- break
- case 'incomingEncryptedOutputs': {
- newPhaseName = _.isString(message['phase']) ? message['phase'].toLowerCase() : undefined
- // Grab the sender of this message by using the verificationKey used
- // to sign this protobuff message. The signature has already been
- // verified successfully but we're not sure yet if the sender is lying
- // about their player number. This check will be performed in the the
- // `forwardEncryptedShuffleTxOutputs` function.
- const sentBy = _.find(this.players, {
- verificationKey: _.get(jsonMessage, 'packets[0].packet.fromKey.key')
- })
- if (this.phase === 'announcement' && newPhaseName === 'shuffle') {
- this.phase = 'shuffle'
- this.emit('phase', 'shuffle')
- this.forwardEncryptedShuffleTxOutputs(jsonMessage.packets, sentBy)
- }
- break
- }
- case 'finalTransactionOutputs':
- newPhaseName = _.isString(
- message['phase']) ? message['phase'].toLowerCase() : undefined
- /* Set new phase name. */
- this.phase = newPhaseName
- this.emit('phase', newPhaseName)
- this.checkFinalOutputsAndDoEquivCheck(jsonMessage.packets)
- break
- case 'incomingEquivCheck':
- try {
- await this.processEquivCheckMessage(message)
- } catch (nope) {
- console.error('Error processing incoming equivCheck:', nope) // eslint-disable-line no-console
- }
- break
- case 'blame':
- this.handleBlameMessage(message)
- break
- case 'incomingInputAndSig':
- try {
- await this.verifyAndSubmit(message)
- } catch (nope) {
- console.error('Error processing incoming output and signature:', nope) // eslint-disable-line no-console
- }
- break
- // case '':
- // break;
- default:
- break
- }
- // debug('Finished acting on', messageType, 'message\n\n');
- }
- /**
- * Process Websockets Error
- */
- processWsError (someError) {
- console.error('Oh goodness, something is amiss!', someError) // eslint-disable-line no-console
- }
- /**
- * Write Debug File
- */
- writeDebugFile () {
- this.comms.writeDebugFile(true)
- }
- /***************************************************************************
- BEGIN COINSHUFFLE PROTOCOL METHODS
- ----------------------------------
- **************************************************************************/
- /**
- * Broadcast Transaction Input
- *
- * This function reveals the coin our client wishes to shuffle as well as
- * our verificationKey. Although we revealed our verificationKey in our
- * server registration message, that message isn't relayed to our peers.
- * This is the first message where our peers see the vk.
- */
- broadcastTransactionInput () {
- if (this.comms.outbox.sent['broadcastTransactionInput']) {
- return
- }
- // debug('Revealing our verificationKey and coin to our peers!');
- /* Initialize inputs. */
- const inputsObject = {}
- /* Set inputs. */
- inputsObject[this.coin.publicKey.toString('hex')] =
- [this.coin.txid + ':' + this.coin.vout]
- try {
- this.comms
- .sendMessage(
- 'broadcastTransactionInput',
- inputsObject,
- this.session,
- this.myPlayerNumber,
- this.ephemeralKeypair.publicKey
- )
- } catch (nope) {
- console.error('Couldnt send broadcastTransactionInput message:', nope.message) // eslint-disable-line no-console
- return this.endShuffleRound()
- }
- }
- /**
- * Add Player to Round
- *
- * This function is called in response to us receiving a new message from
- * either ourselves or another player that announces which coin they will
- * be shuffling. We should receive one of these messages for each player
- * in the round (including ourselves). The messages are unicast
- * (no toKey field).
- *
- * In this function we do ALL of the following:
- * 1. Check that the coin exists on the blockchain.
- * 2. Check that the coin value is appropriate for the current round.
- * 3. Add the player to our internal state data.
- *
- * NOTE: It's also here where we record each player's verificationKey,
- * that the `CommChannel` class uses to verify the signature on all
- * future messages.
- */
- async addPlayerToRound (message) {
- /* Initialize player coin. */
- const playerCoin = {
- publicKey: _.keys(message['message']['inputs'])[0]
- }
- /* Initialize UTXO info. */
- const utxoInfo = _.values(message['message']['inputs'])[0]['coins'][0].split(':')
- /* Set player transaction id. */
- playerCoin.txid = utxoInfo[0]
- /* Set player coin output (index). */
- playerCoin.vout = Number(utxoInfo[1])
- /* Set player to add. */
- const playerToAdd = {
- session: message['session'],
- playerNumber: Number(message['number']),
- isMe: message['session'] === this.session,
- verificationKey: message['fromKey']['key'],
- coin: playerCoin
- }
- /* Validate player. */
- if (playerToAdd.isMe) {
- Object.assign(playerToAdd.coin, this.coin)
- }
- /* Add player. */
- this.players.push(playerToAdd)
- // debug('Added player', playerToAdd);
- /* Initialize coin details. */
- // NOTE: We've already added the player to our pool but we
- // still need to verify the data they sent us.
- const coinDetails = await this.util.coin
- .getCoinDetails(playerCoin.txid, playerCoin.vout)
- .catch(err => {
- /* eslint-disable-next-line no-console */
- console.error('Cannot get coin details', err)
- /* Assign blame. */
- this.assignBlame({
- reason: 'INSUFFICIENTFUNDS',
- accused: playerToAdd.verificationKey
- })
- })
- /* Validate coin details. */
- // NOTE: Check that the coin is there and big enough
- // before adding the player.
- if (!coinDetails.satoshis || this.shuffleFee + this.poolAmount > coinDetails.satoshis) {
- debug('Insufficient funds for player (coinDetails):', coinDetails)
- /* Assign blame. */
- this.assignBlame({
- reason: 'INSUFFICIENTFUNDS',
- accused: playerToAdd.verificationKey
- })
- return
- }
- /* Grab player. */
- const grabPlayer = _.find(this.players, { session: playerToAdd.session })
- /* Validate player. */
- // NOTE: If it's our message, add our coin object to
- // the player and only update the fiscal properties.
- if (playerToAdd.isMe) {
- Object.assign(grabPlayer.coin, {
- amount: coinDetails.amount,
- satoshis: coinDetails.satoshis,
- // confirmations: coinDetails.confirmations,
- spent: coinDetails.spent
- })
- } else {
- Object.assign(grabPlayer.coin, coinDetails)
- }
- // debug(`Player ${grabPlayer.playerNumber} updated`);
- }
- /**
- * Announce Change Address
- *
- * Here we announce our change address, as well as the public key that
- * other players should use to encrypt messages meant for our eyes only.
- * Primarily, they will use it when encrypting the transaction output
- * addresses so we can decrypt them, add our own, and re-encrypt all of
- * them for the next player to do the same.
- *
- * NOTE: Encrypting these output addresses keeps the server from being
- * able to keep a record of which coin belongs to which player.
- *
- * NOTE: This function fires many times but we should only announce our
- * change address once. If we've already done this, just return.
- */
- announceChangeAddress () {
- if (this.comms.outbox.sent['changeAddressAnnounce'] ||
- this.players.length < this.numberOfPlayers
- ) {
- return
- }
- /* Send message. */
- this.comms.sendMessage(
- 'changeAddressAnnounce',
- this.session,
- this.myPlayerNumber,
- this.change.legacyAddress,
- this.encryptionKeypair.publicKeyHex,
- this.phase,
- this.ephemeralKeypair.publicKey,
- this.ephemeralKeypair.privateKey
- )
- }
- /**
- * Forward Encrypted Shuffle Transaction Outputs
- *
- * Implements processing of messages during the shuffling phase.
- *
- * It performs the following:
- * 1. Accepts the encrypted output addresses sent to us in the first
- * message of the "shuffle" phase. There should be as few as zero
- * and at most all-but-one address(es) in legacy format. They are
- * encrypted to our public `encryptionKey` that we shared along
- * with our change address in the previous step.
- *
- * 2. The remaining behavior in this function varies depending on if
- * we are are the last player. In both cases though, we will first
- * do all of the following:
- * (a) Make sure this message came from the previous player.
- * (b) Strip off the top layer of encryption from all strings.
- * (c) Encrypt and add our own output address that will soon
- * contain our shuffled coin.
- * (d) Shuffle the entire set of addresses well. If we are the
- * last player, we may choose not to.
- * (e) Check that each string is unique. If not, assign blame
- * and end the round.
- *
- * 3. If we are the last player, as determined by the highest `number`
- * returned by the server in response to our our registration
- * message, then we will signal the end of the shuffle stage by
- * broadcasting the complete set of shuffled addresses in decrypted
- * form to every player as a regular multicast message.
- *
- * 4. If we are not the last player, we encrypt and add our own
- * addresses. We must add one layer of encryption to our address
- * for every subsequent player in the round. For example, if we are
- * player 3 in a 10 player round, we must add 7 layers of
- * encryption, starting with player #10 then working our way back
- * to player #4. Then we send all of them to player #4 as a
- * signed multi-packet unicast message. Unicast messages are those
- * that include toKey field which the server relays only to them.
- *
- * NOTE: If we are player 1, this function has been called without any
- * parameters so there will be nothing for us to decrypt and our
- * message will be the first message of the shuffle phase. Before we
- * send it though, we need to make sure everyone has sent us their
- * decryption keys. If they haven't, just return without doing
- * anything. This function will be called again with each new key
- * received.
- */
- forwardEncryptedShuffleTxOutputs (arrayOfPacketObjects, sender) {
- /* Initialize ourselves. */
- const me = _.find(this.players, { isMe: true })
- /* Initialize ordered players. */
- const orderedPlayers = _.orderBy(
- this.players, ['playerNumber'], ['asc'])
- /* Set first player. */
- const firstPlayer = _.minBy(orderedPlayers, 'playerNumber')
- /* Set last player. */
- const lastPlayer = _.maxBy(orderedPlayers, 'playerNumber')
- /* Set next player. */
- const nextPlayer = orderedPlayers[_.findIndex(orderedPlayers, { isMe: true }) + 1]
- /* Set previous player. */
- const previousPlayer = orderedPlayers[_.findIndex(orderedPlayers, { isMe: true }) - 1]
- /* Validate encryption keys. */
- // NOTE: Check that we have received a decryption key from all players,
- // and that they are all unique.
- //
- // NOTE: Uniqueness isn't a protocol requirement, but it probably
- // should be.
- if (
- _.uniq(
- _.compact(
- orderedPlayers.map(obj => obj['encryptionPubKey'])
- )
- ).length !== this.players.length) {
- debug('Waiting for the remaining encryption keys:', orderedPlayers)
- return
- }
- /* Initialize string for next player. */
- const stringsForNextPlayer = []
- /* Validate player data. */
- if (me.playerNumber !== firstPlayer.playerNumber) {
- // Make sure the player who sent us this message is who it should be
- if (sender.playerNumber !== previousPlayer.playerNumber) {
- debug(`Player ${sender.playerNumber} is not player ${previousPlayer.playerNumber} despite saying so`)
- this.assignBlame({
- reason: 'LIAR',
- accused: sender.verificationKey
- })
- return
- }
- /* Retrieve decrypted string. */
- const decryptedStrings = _.reduce(arrayOfPacketObjects, (results, onePacket) => {
- try {
- /* Set decryption results. */
- const decryptionResults = this.util.crypto
- .decrypt(
- _.get(onePacket, 'packet.message.str'),
- this.encryptionKeypair.privateKeyHex
- )
- debug('Forward encrypted shuffle tx outputs:',
- 'onePacket', onePacket,
- 'privateKeyHex', this.encryptionKeypair.privateKeyHex,
- decryptionResults
- )
- /* Add decryption to results. */
- results.strings.push(decryptionResults.toString('utf-8'))
- } catch (nope) {
- console.error('Cannot decrypt') // eslint-disable-line no-console
- results.errors.push({
- packet: onePacket,
- error: nope
- })
- }
- return results
- }, {
- strings: [],
- errors: []
- })
- debug('Forward encrypted shuffle tx outputs (decryptedStrings):', decryptedStrings)
- /* Validate decrypted string. */
- // NOTE: Blame our sender if the ciphertext cannot be decrypted.
- // It may or may not be their fault, but someone has to be
- // the fall guy.
- if (decryptedStrings.errors.length) {
- this.assignBlame({
- reason: 'INVALIDFORMAT',
- accused: sender.verificationKey
- })
- }
- _.each(
- decryptedStrings.strings, (oneThing) => {
- stringsForNextPlayer.push(oneThing)
- }
- )
- }
- /* Set our encrypted output address. */
- // NOTE: Add our output address after first encrypting it with the
- // public keys of all subsequent players in the round except.
- const ourEncryptedOutputAddress = _.reduceRight(
- orderedPlayers, (
- encryptedAddressInfo, onePlayer
- ) => {
- if (nextPlayer && onePlayer.playerNumber >= nextPlayer.playerNumber) {
- try {
- encryptedAddressInfo.string = this.util.crypto
- .encrypt(
- encryptedAddressInfo.string,
- onePlayer.encryptionPubKey
- )
- debug(
- 'Forward encrypted shuffle tx outputs (encryptedAddressInfo):',
- encryptedAddressInfo
- )
- } catch (nope) {
- /* eslint-disable-next-line no-console */
- console.error(`Cannot encrypt address for encryptionPubKey ${onePlayer.encryptionPubKey} because ${nope.message}`)
- encryptedAddressInfo.errors.push({
- player: onePlayer,
- error: nope
- })
- }
- }
- return encryptedAddressInfo
- }, {
- errors: [],
- string: this.shuffled.legacyAddress
- })
- /* Validate our encrypted output address. */
- if (ourEncryptedOutputAddress.errors.length) {
- this.assignBlame({
- reason: 'INVALIDFORMAT',
- accused: sender.verificationKey
- })
- }
- /* Add our encrypted output address to string for next player. */
- stringsForNextPlayer.push(ourEncryptedOutputAddress.string)
- /* Do a uniqueness check on the output addresses / ciphertexts. */
- if (_.compact(_.uniq(stringsForNextPlayer)).length !== stringsForNextPlayer.length) {
- this.assignBlame({
- reason: 'MISSINGOUTPUT',
- accused: sender.verificationKey
- })
- }
- /**
- * Shuffle Array
- */
- const shuffleArray = function (someArray, num) {
- return (
- num > 0 ?
- shuffleArray(_shuffle(someArray), num - 1) :
- _shuffle(someArray)
- )
- }
- /* Validate if we are the last player. */
- if (me.playerNumber === lastPlayer.playerNumber) {
- debug(`Broadcasting final shuffled output addresses ${stringsForNextPlayer}!`)
- /* Send message. */
- this.comms.sendMessage(
- 'broadcastFinalOutputAddresses',
- this.session,
- me.playerNumber,
- shuffleArray(stringsForNextPlayer, 100),
- 'broadcast',
- this.ephemeralKeypair.publicKey,
- this.ephemeralKeypair.privateKey
- )
- } else {
- debug('Sending encrypted outputs:',
- stringsForNextPlayer,
- 'to player',
- nextPlayer.playerNumber,
- '(', nextPlayer.verificationKey, ')'
- )
- /* Send message. */
- this.comms.sendMessage(
- 'forwardEncryptedOutputs',
- this.session,
- me.playerNumber,
- shuffleArray(stringsForNextPlayer, 100),
- this.phase,
- nextPlayer.verificationKey,
- this.ephemeralKeypair.publicKey,
- this.ephemeralKeypair.privateKey
- )
- }
- }
- /**
- * Check Final Outputs and Do Equivocation Check
- *
- * This function performs processing of the "broadcast" phase message sent
- * by the final player in the round. This message announces the final
- * set of shuffled output addresses.
- *
- * This function does all of the following:
- *
- * 1. Ensure our address is in list of output addresses.
- * If not, blame and exit.
- * 2. Broadcast our own "equivocation check" message.
- * 3. Compute hash of outputs string and broadcast it.
- *
- * TODO: Check that the message was actually sent by the last player
- * in the round.
- */
- checkFinalOutputsAndDoEquivCheck (signedPackets) {
- const me = _.find(this.players, { isMe: true })
- const finalOutputAddresses = signedPackets.map(obj => obj['packet']['message']['str'])
- debug(
- 'Check final outputs and do equiv check (finalOutputAddresses):',
- finalOutputAddresses
- )
- /* Make sure our address was included. If not, blame! */
- if (finalOutputAddresses.indexOf(this.shuffled.legacyAddress) < 0) {
- debug(`Our address isn't in the final outputs!`)
- this.assignBlame({
- reason: 'MISSINGOUTPUT',
- // accused: _.get(messageObject, _.get(signedPackets[0], 'packet.fromKey.key'))
- accused: _.get(signedPackets, _.get(signedPackets[0], 'packet.fromKey.key'))
- })
- }
- // Attach the entire array of ordered output addresses to our
- // players. Although we don't know which address belongs to which
- // player ( they've been shuffled by everyone ), the order becomes
- // important later because it effects the transaction output order
- // which has implications for it's signature.
- for (let n = this.players.length; n >= 0; n--) {
- debug('this.players[ n ]:', n, this.players[ n ])
- if (typeof this.players[ n ] !== 'undefined') {
- Object.assign(this.players[ n ], { finalOutputAddresses })
- }
- }
- /* Set equivocation hash (plaintext). */
- this.equivHashPlaintext = '[\'' +
- finalOutputAddresses.join('\', \'') +
- '\'][\'' +
- _.orderBy(this.players, 'playerNumber').map(obj => obj['encryptionPubKey']
- ).join('\', \'') +
- '\']'
- /* Calculate equivocation hash. */
- this.equivHash = bch.crypto.Hash
- .sha256sha256(Buffer.from(this.equivHashPlaintext, 'utf-8'))
- .toString('base64')
- /* Advance to the next phase. */
- this.phase = 'EQUIVOCATION_CHECK'
- this.emit('phase', 'EQUIVOCATION_CHECK')
- /* Now broadcast the results of our "equivocation check". */
- this.comms.sendMessage(
- 'broadcastEquivCheck',
- this.session,
- me.playerNumber,
- this.equivHash,
- this.phase,
- this.ephemeralKeypair.publicKey,
- this.ephemeralKeypair.privateKey
- )
- }
- /**
- * Process Equivocation Check Message
- *
- * This function implements processing of messages on Equivocation Check
- * phase(phase # 4).
- *
- * It does the following:
- * 1. Verify if hashes from all players are the same. If it's not,
- * goes to the blame phase.
- *
- * 2. If hashes are the same it sets the next phase as verification
- * and submission phase.
- */
- async processEquivCheckMessage (prunedMessage) {
- const me = _.find(this.players, { isMe: true })
- /* Set first player. */
- const firstPlayer = _.minBy(this.players, 'playerNumber')
- /* Set last player. */
- // const lastPlayer = _.maxBy(this.players, 'playerNumber')
- /* Add the hash provided by the player to that player's state data. */
- const sender = Object.assign(
- this.players[_.findIndex(this.players, { session: prunedMessage['session'] })], {
- equivCheck: _.get(prunedMessage, 'message.hash.hash')
- }
- )
- debug(
- 'Got a processEquivCheck message from', sender.verificationKey,
- 'with hash', sender.equivCheck
- )
- const allHashes = _.compact(this.players.map(obj => obj['equivCheck']))
- if (allHashes.length === this.players.length) {
- // Are all the hashes the same and do they equal ours?
- if (_.uniq(allHashes).length === 1 && _.uniq(allHashes)[0] === this.equivHash) {
- debug('Everyone passes the EQUIVOCATION_CHECK!')
- this.phase = 'VERIFICATION_AND_SUBMISSION'
- this.emit('phase', 'VERIFICATION_AND_SUBMISSION')
- if (me.playerNumber === firstPlayer.playerNumber) {
- try {
- await this.verifyAndSubmit()
- } catch (nope) {
- console.error('Error processing incoming output and signature:', nope) // eslint-disable-line no-console
- }
- }
- } else {
- debug('Someone failed the equivCheck!')
- // for (let onePlayer of round.players) {
- for (let onePlayer of this.players) {
- if (onePlayer.equivCheck !== me.equivCheck) {
- this.assignBlame({
- reason: 'EQUIVOCATIONFAILURE',
- accused: sender.verificationKey,
- hash: onePlayer.equivCheck
- }, true)
- }
- }
- }
- } else {
- // debug('Waiting for more equivCheck messages');
- }
- }
- /**
- * Verify and Submit
- *
- * This function handles messages for the final phase of the protocol
- * (phase # 5).
- *
- * It does the following:
- * 1. Creates an unsigned transaction that adheres to the
- * CashShuffle spec (input order and amounts, etc).
- *
- * 2. Partially sign the transaction. Sign our input then broadcast
- * its signature to the other players.
- *
- * 3. Check if we've received the input signature all the other players.
- *
- * 4. Verify the input signature's of all players. If there is a wrong
- * signature, go to the blame phase.
- *
- * 5. If everything is good, use the signatures to finish signing
- * the transaction.
- *
- * 6. Broadcast the transaction to the network.
- *
- * 7. Set the done flag and cleanup the round.
- */
- async verifyAndSubmit (prunedMessage) {
- debug('Verify and submit (prunedMessage):', prunedMessage)
- /* Initialize ordered players. */
- const orderedPlayers = _.orderBy(this.players, ['playerNumber'], ['asc'])
- /* Initialize first player. */
- const firstPlayer = _.minBy(orderedPlayers, 'playerNumber')
- /* Initialize last player. */
- // const lastPlayer = _.maxBy(orderedPlayers, 'playerNumber')
- /* Initialize ourselves. */
- const me = _.find(this.players, { isMe: true })
- // If we got a signature message before we've finished building
- // the partially signed transaction, wait up to 15 seconds or until
- // the transaction is done building before letting processing
- // this message. Otherwise, chaos reigns.
- if (this.shuffleTx.isBuilding) {
- /* Set wait until time. */
- const waitUntilThisTime = new Date().getTime() + (1000 * 15)
- while (this.shuffleTx.isBuilding) {
- /* Set time now. */
- const timeNow = new Date().getTime()
- if (timeNow > waitUntilThisTime || !this.shuffleTx.isBuilding) {
- this.shuffleTx.isBuilding = false
- } else {
- await delay(500)
- }
- }
- }
- // If we haven't built the shuffle transaction and
- // broadcast our signature, do so now.
- if (!this.comms.outbox.sent['broadcastSignatureAndUtxo']) {
- // Set the isBuilding flag so incoming messages don't trigger
- // multiple transaction build attempts and multiple signature
- // broadcasts. It sometimes takes a few seconds to build the
- // partially signed transactions because we also hit a REST
- // endpoint to validate user's have sufficient funds.
- this.shuffleTx.isBuilding = true
- let shuffleTransaction
- try {
- shuffleTransaction = await this.util.coin.buildShuffleTransaction({
- players: this.players,
- feeSatoshis: this.shuffleFee
- })
- } catch (nope) {
- console.error('Problem building shuffle transaction:', nope) // eslint-disable-line no-console
- this.writeDebugFile()
- }
- Object.assign(this.shuffleTx, {
- serialized: shuffleTransaction.serialized,
- tx: shuffleTransaction.tx,
- inputs: shuffleTransaction.inputs,
- outputs: shuffleTransaction.outputs
- })
- // Broadcast our transaction signature. If the other players
- // are able to apply it to their copy of the transaction then
- // the shuffleRound is complete.
- this.comms.sendMessage(
- 'broadcastSignatureAndUtxo',
- this.session,
- me.playerNumber,
- me.coin.txid + ':' + me.coin.vout,
- shuffleTransaction.signatureBase64,
- this.phase,
- this.ephemeralKeypair.publicKey,
- this.ephemeralKeypair.privateKey
- )
- // Turn off the isBuilding sign so any queued up signature
- // checks may now occur.
- this.shuffleTx.isBuilding = false
- }
- // The CashShuffle protocol dictates that the first player in the
- // round is responsible for first broadcasting their transaction
- // signature. So if we ARE the first player, we call this function
- // without any parameters after we've received and verified the hashes.
- // We will exit now after sending the protocol message and this function
- // will be immediately called again (this time with parameters) as the
- // server sends us our own signature message.
- if (firstPlayer.playerNumber === me.playerNumber && !prunedMessage) {
- return
- }
- /* Set unspent transaction output. */
- const utxo = _.get(prunedMessage, 'message.signatures[0].utxo')
- /* Build new signature data. */
- const newSigData = {
- prevTxId: utxo.split(':')[0],
- vout: Number(utxo.split(':')[1]),
- signature: Buffer.from(
- _.get(
- prunedMessage,
- 'message.signatures[0].signature.signature'
- ), 'base64').toString('utf-8')
- }
- debug('Verify and submit (newSigData):', newSigData)
- // Assert(len(sig) >= 8 and len(sig) <= 72)
- // Assert(sig[0] == 0x30)
- // Assert(sig[1] == len(sig)-2) # Check length
- // Assert(sig[2] == 0x02)
- /* Add new signature data to shuffle signatures. */
- this.shuffleTx.signatures.push(newSigData)
- /* Retrieve signer. */
- const signer = _.find(this.players, (onePlayer) => {
- return onePlayer.coin.txid === newSigData.prevTxId && Number(onePlayer.coin.vout) === newSigData.vout
- })
- debug('Verify and submit (signer):', signer)
- /* Validate signer. */
- if (!signer) {
- this.assignBlame({
- reason: 'INVALIDSIGNATURE',
- accused: _.get(prunedMessage, 'fromKey.key')
- })
- return
- }
- debug(`Got a shuffle transaction signature for coin ${utxo}`)
- // Verify that the signature we've been given is valid for the shuffle
- // transaction input they've stated. If so, we will add that signature
- // to our transaction. If not, we will abort and blame the sender.
- // Note, the function returns the data necessary to add the signature.
- // That data takes the form below.
- //
- // {
- // success: true,
- // inputIndex: signatureObject.inputIndex,
- // signature: signatureObject
- // };
- let sigVerifyResults
- try {
- sigVerifyResults = this.util.coin
- .verifyTransactionSignature(
- this.shuffleTx.tx,
- newSigData,
- _.get(signer, 'coin.publicKey')
- )
- } catch (nope) {
- console.error('Error when trying to validate signature', nope) // eslint-disable-line no-console
- this.assignBlame({
- reason: 'INVALIDSIGNATURE',
- accused: _.get(prunedMessage, 'fromKey.key')
- })
- return
- }
- /* Validate signature (for UTXO). */
- if (sigVerifyResults && sigVerifyResults.success) {
- debug(`Shuffle transaction signature for ${utxo} checks out!`)
- // If it was us that sent the message, we don't need to apply
- // the signature. Our signature was applied during the creation
- // of the shuffle transaction. We only need to apply the other
- // player's signatures.
- if (!signer.isMe) {
- // debug(`Applying signature to input${sigVerifyResults.inputIndex}!`);
- try {
- this.shuffleTx.tx.inputs[sigVerifyResults.inputIndex]
- .addSignature(this.shuffleTx.tx, sigVerifyResults.signature)
- } catch (nope) {
- /* eslint-disable-next-line no-console */
- console.error('We failed to apply a signature to our transaction. Looks like our fault', nope)
- // TODO: throw and cleanup
- }
- }
- } else {
- debug(`Bad signature for coin ${utxo}`)
- this.assignBlame({
- reason: 'INVALIDSIGNATURE',
- accused: _.get(prunedMessage, 'fromKey.key')
- })
- }
- /* Initialize fully signed flag. */
- let txIsFullySigned
- try {
- txIsFullySigned = this.shuffleTx.tx.isFullySigned()
- } catch (nope) {
- console.error('Malformed shuffle transaction', nope) // eslint-disable-line no-console
- this.endShuffleRound()
- }
- if (txIsFullySigned && this.shuffleTx.signatures.length === this.numberOfPlayers) {
- debug(`Broadcasting CashShuffle tx ${this.shuffleTx.tx.hash} to the network!`)
- let submissionResults
- // debug('Broadcasting raw tx:',
- // this.shuffleTx.tx.toBuffer('hex').toString('hex'))
- debug('Broadcasting raw tx:',
- this.shuffleTx.tx.toBuffer('hex').toString('hex'))
- try {
- /* Send raw transaction. */
- submissionResults = await Nito.Transaction
- .sendRawTransaction(this.shuffleTx.tx.toBuffer('hex').toString('hex'))
- this.emit('complete', {
- txid: this.shuffleTx.tx.toBuffer('hex').toString('hex'),
- submissionResults,
- })
- } catch (nope) {
- console.error('Error broadcasting transaction to the network:', nope) // eslint-disable-line no-console
- this.endShuffleRound()
- return
- }
- if (submissionResults) {
- Object.assign(this.shuffleTx, {
- results: submissionResults
- })
- }
- const allOutputAddressesUsed = this.shuffleTx.tx.outputs.map(oneOutput => {
- return oneOutput.script.toAddress().toString()
- })
- // Add a property so the user's wallet logic can
- // quickly tell if this change address can be reused.
- Object.assign(this.change, {
- usedInShuffle: allOutputAddressesUsed
- .indexOf(this.change.legacyAddress) > -1
- })
- this.success = true
- this.endShuffleRound()
- } else {
- debug('Waiting on more signatures...')
- }
- }
- /**
- * End Shuffle Round
- */
- endShuffleRound(writeDebugFileAnyway) {
- debug(`Shuffle has ended with success [ ${ this.success } ]`)
- this.roundComplete = true
- if (!this.success || writeDebugFileAnyway) {
- debug('Writing debug file..')
- this.writeDebugFile()
- }
- // Close this round's connection to the server
- this.comms._wsClient.close()
- const msg = `Coin ${this.coin.txid}:${this.coin.vout} has been successfully shuffled!`
- this.emit('notice', msg)
- this.emit('shuffle')
- }
- /**
- * Stop
- */
- stop() {
- /* Set complete flag. */
- this.roundComplete = true
- // Close this round's connection to the server
- this.comms._wsClient.close()
- debug(`Shuffle has stopped.`)
- }
- /**
- * Handle Blame
- *
- * When we receive a message from another player accusing someone
- * of violating the protocol.
- */
- handleBlameMessage (messageObject) {
- /* Retrieve key of accused. */
- const keyOfAccused = _.get(messageObject, 'message.blame.accused.key')
- /* Set accused. */
- const accused = _.find(this.players, { verificationKey: keyOfAccused })
- /* Validate accused. */
- if (accused.isMe) {
- debug(`I'M THE ONE BEING BLAMED. HOW RUDE!!`)
- console.log(`I'M THE ONE BEING BLAMED. HOW RUDE!!`)
- } else {
- debug('Player', accused.verificationKey, 'is to blame!')
- console.log('Player', accused.verificationKey, 'is to blame!')
- }
- /* Write debug file. */
- this.writeDebugFile()
- }
- /**
- * Assign Blame
- *
- * When we conclude that a player has violated the protocol and we need to
- * send out a blame message.
- *
- * Possible Ban Reasons:
- * INSUFFICIENTFUNDS = 0
- * DOUBLESPEND = 1
- * EQUIVOCATIONFAILURE = 2
- * SHUFFLEFAILURE = 3
- * SHUFFLEANDEQUIVOCATIONFAILURE = 4
- * INVALIDSIGNATURE = 5
- * MISSINGOUTPUT = 6
- * LIAR = 7
- * INVALIDFORMAT = 8
- */
- /*
- {
- reason: < enum string citing reason for blame accusation >,
- accused: < verification key in hex format of player who 's being accused >,
- invalid: < an array of protobuff packets that provide evidence of fault > ,
- hash: < hash provided by accused which differs from our own > ,
- keypair: {
- key: < private key > ,
- public: < public key >
- }
- }
- */
- assignBlame (details, keepAlive) {
- debug(`Issuing a formal blame message against ${details.accused} for ${details.reason}`)
- /* Send message. */
- this.comms.sendMessage(
- 'blameMessage',
- details,
- this.session,
- this.myPlayerNumber,
- this.ephemeralKeypair.publicKey,
- this.ephemeralKeypair.privateKey
- )
- if (!keepAlive) {
- this.endShuffleRound()
- }
- }
- }
- module.exports = ShuffleRound
|