FusionRound.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. /* Import core modules. */
  2. const _ = require('lodash')
  3. const bch = require('bitcore-lib-cash')
  4. const debug = require('debug')('cashshuffle:round')
  5. const EventEmitter = require('events').EventEmitter
  6. /* Import local modules. */
  7. const cryptoUtils = require('./cryptoUtils.js')
  8. const coinUtils = require('./coinUtils.js')
  9. /* Import CommChannel (Class). */
  10. const CommChannel = require('./CommChannel.js')
  11. /* Initialize magic number. */
  12. // const magic = Buffer.from('42bcc32669467873', 'hex')
  13. /**
  14. * Delay (Execution)
  15. */
  16. const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))
  17. /**
  18. * Shuffle Round (Class)
  19. */
  20. class FusionRound extends EventEmitter {
  21. constructor (clientOptions) {
  22. super()
  23. /* Initialize client options. */
  24. for (let oneOption in clientOptions) {
  25. this[oneOption] = clientOptions[oneOption]
  26. }
  27. /* Initialize done flag. */
  28. this.done = false
  29. /* Initialize phase. */
  30. this.phase = ''
  31. /* Initialize util. */
  32. // TODO: Rename to `utils`.
  33. this.util = {
  34. /* Tools for encryption and message sign / verify. */
  35. crypto: cryptoUtils,
  36. /* Tools that make REST calls for blockchain data. */
  37. coin: coinUtils
  38. }
  39. /* Initialize ephemeral keypair. */
  40. // NOTE: A public and private keypair, destroyed at the end of a shuffle
  41. // round. Its only purpose is to sign and verify protocol
  42. // messages to ensure the participants aren't being
  43. // cheated / attacked by the server or each other.
  44. this.ephemeralKeypair = this.util.crypto.generateKeypair()
  45. /* Initialize encryption keypair. */
  46. // NOTE: A public and private keypair, destroyed at the end of a shuffle
  47. // round. It's used to encrypt and decrypt message fields during
  48. // the shuffle round so they are kept private from the server and
  49. // the other players in the round.
  50. this.encryptionKeypair = this.util.crypto.generateKeypair()
  51. /* Initialize hooks. */
  52. this.hooks = this.hooks || {}
  53. /* Validate shuffled hook. */
  54. if (!_.isFunction(this.hooks.shuffled)) {
  55. debug(`A valid shuffle address generation hook was not provided!`)
  56. throw new Error('BAD_SHUFFLE_FN')
  57. }
  58. /* Initialize shuffled. */
  59. this.shuffled = this.hooks.shuffled()
  60. /* Validate change hook. */
  61. // NOTE: Make sure either a change generation function or change
  62. // keypair object was provided. Use the keypair, if we got both.
  63. if (!_.isFunction(this.hooks.change)) {
  64. debug(`A valid change generation hook was not provided!`)
  65. throw new Error('BAD_CHANGE_FN')
  66. }
  67. /* Initialize change. */
  68. this.change = this.hooks.change()
  69. /* Initialize (shuffle) players. */
  70. // NOTE: This is where we keep our representation of all the shufflers
  71. // in the round (including us).
  72. this.players = []
  73. /* Initialize output addresses. */
  74. // NOTE: Once we reach the "shuffle" phase, this array will house the
  75. // addresses that each player's shuffled coins will be sent to.
  76. this.outputAddresses = []
  77. /* Initialize shuffle transaction. */
  78. // NOTE: Used to store the partially signed transaction after it
  79. // is generated but before its broadcasted to the network.
  80. this.shuffleTx = {
  81. isBuilding: false,
  82. // NOTE: We will add each signature and input data to this
  83. // collection as it's received during the verification
  84. // and submission phase.
  85. signatures: []
  86. }
  87. /* Initialize round completion flag. */
  88. this.roundComplete = false
  89. /* Initialize success flag. */
  90. this.success = false
  91. /* Initialize round error. */
  92. // NOTE: This object will be extended with error data in the event
  93. // that the round ends unexpectedly for any reason. This
  94. // includes a protocol error on behalf of any player in the
  95. // round (ourselves included) as well as if an exception is
  96. // thrown in this library.
  97. this.roundError = {
  98. // shortCode: 'BAD_SIG',
  99. // errorObject: [ Error instance containing a stacktrace ],
  100. // isProtocolError: true,
  101. // isException: false,
  102. // accusedPlayer: [ Object containing player data ]
  103. }
  104. /* Initialize communications channel. */
  105. this.comms = new CommChannel({
  106. serverUri: this.serverUri
  107. }, this)
  108. /* Handle server message. */
  109. this.comms.on('serverMessage', async (someServerMessage) => {
  110. try {
  111. await this.actOnMessage(someServerMessage)
  112. } catch (nope) {
  113. debug('Failed to act right in response to server message:', nope)
  114. this.writeDebugFile()
  115. }
  116. })
  117. /* Handle protocol violation. */
  118. this.comms.on('protocolViolation', this.assignBlame.bind(this))
  119. /* Handle connection error. */
  120. this.comms.on('connectionError', this.handleCommsError.bind(this))
  121. /* Handle disconnection. */
  122. this.comms.on('disconnected', (commsDisconnectMessage) => {
  123. debug('Our connection to the CashShuffle server is REKT!')
  124. /* Validate round completion. */
  125. if (this.roundComplete) {
  126. debug('The shuffle Round has completed')
  127. } else {
  128. /* Set success flag. */
  129. this.success = false
  130. /* Set round copmletion flag. */
  131. this.roundComplete = true
  132. /* Update round error. */
  133. _.extend(this.roundError, {
  134. shortCode: 'COMMS_DISCONNECT',
  135. errorObject: new Error(commsDisconnectMessage),
  136. isProtocolError: false,
  137. isException: false
  138. })
  139. /* End shuffle round. */
  140. this.endShuffleRound()
  141. }
  142. })
  143. /* Handle connection. */
  144. // this.comms.on('connected', (socket) => {
  145. // debug('socket', socket)
  146. this.comms.on('connected', () => {
  147. /* Set round phase. */
  148. this.phase = 'registration'
  149. try {
  150. // console.log(
  151. // '\nSENDING REGISTRATION MESSAGE',
  152. // this.protocolVersion,
  153. // this.poolAmount,
  154. // this.ephemeralKeypair.publicKey
  155. // )
  156. this.comms
  157. .sendMessage(
  158. 'registration',
  159. this.protocolVersion,
  160. this.poolAmount,
  161. this.ephemeralKeypair.publicKey
  162. )
  163. } catch (nope) {
  164. debug('Couldnt send registration message:', nope.message)
  165. }
  166. })
  167. this.ready()
  168. .catch((nope) => {
  169. debug('ERROR:', nope)
  170. })
  171. .then(() => {
  172. //
  173. })
  174. return this
  175. }
  176. /**
  177. * Handle Communications Error
  178. */
  179. handleCommsError (someError) {
  180. debug('Something has gone wrong with our communication channel:', someError.message)
  181. /* Update round error. */
  182. this.roundError = {
  183. shortCode: 'COMS_ERR',
  184. errorObject: someError,
  185. isProtocolError: false,
  186. isException: true
  187. }
  188. /* End shuffle round. */
  189. this.endShuffleRound()
  190. }
  191. /**
  192. * Ready
  193. */
  194. async ready () {
  195. this.emit('debug', { message: 'beginning-round' })
  196. /* Setup server connection. */
  197. try {
  198. await this.comms.connect()
  199. } catch (nope) {
  200. debug('Failure!', nope)
  201. throw nope
  202. }
  203. }
  204. /**
  205. * Act On Message
  206. *
  207. * Process incoming websocket events which contain the prototype buffer
  208. * encoded server messages.
  209. */
  210. async actOnMessage (jsonMessage) {
  211. // console.log('\nACTING ON MESSAGE', jsonMessage) // eslint-disable-line no-console
  212. debug('Acting on message:', jsonMessage.pruned.message)
  213. /* Set message type. */
  214. const messageType =
  215. jsonMessage.pruned.message && jsonMessage.pruned.messageType
  216. /* Validate message type. */
  217. if (!messageType) {
  218. throw new Error('BAD_MESSAGE_PARSING')
  219. }
  220. /* Set message. */
  221. const message = jsonMessage.pruned.message
  222. /* Initialize new phase name. */
  223. let newPhaseName
  224. // debug('Attempting to act on', messageType, 'message\n\n');
  225. /* Handle message type. */
  226. switch (messageType) {
  227. /**
  228. * The server has informed us of the number of players currently in the
  229. * pool. This fires every time a player joins or leaves.
  230. *
  231. * NOTE: We always get one along without server greeting.
  232. */
  233. case 'playerCount':
  234. /* Set number of players. */
  235. this.numberOfPlayers = Number(message['number'])
  236. break
  237. /**
  238. * The server has accepted our pool registration message and replied
  239. * with our player number and a session id to identify us within this
  240. * pool and round.
  241. */
  242. case 'serverGreeting':
  243. /* Set our player number. */
  244. this.myPlayerNumber = Number(message['number'])
  245. /* Set our session id. */
  246. this.session = message['session']
  247. break
  248. /**
  249. * This is a message sent to all players to inform them that it's now
  250. * time to share their change address as well as their second ephemeral
  251. * public key (later used to decrypt the encrypted output addresses).
  252. */
  253. case 'announcementPhase':
  254. /* Set new phase name. */
  255. newPhaseName = _.isString(
  256. message['phase']) ? message['phase'].toLowerCase() : undefined
  257. /* Validate new phase name. */
  258. if (newPhaseName && newPhaseName === 'announcement') {
  259. /* Set phase. */
  260. this.phase = 'announcement'
  261. /* Set number of players. */
  262. this.numberOfPlayers = Number(message['number'])
  263. try {
  264. this.broadcastTransactionInput()
  265. } catch (nope) {
  266. debug('Error broadcasting broadcastTransactionInput:', nope)
  267. }
  268. } else {
  269. debug('Problem with server phase message')
  270. if (_.get(jsonMessage, 'packets[0].packet.fromKey.key')) {
  271. this.assignBlame({
  272. reason: 'INVALIDFORMAT',
  273. accused: _.get(jsonMessage, 'packets[0].packet.fromKey.key')
  274. })
  275. }
  276. }
  277. break
  278. case 'incomingVerificationKeys':
  279. try {
  280. await this.addPlayerToRound(message)
  281. } catch (nope) {
  282. debug('Error broadcasting broadcastTransactionInput:', nope)
  283. }
  284. // If we've received the message from all players (including us)
  285. // containing their `verificationKey` and the coin they wish to
  286. // shuffle, send the next protocol message if we are player one.
  287. if (this.myPlayerNumber === _.get(_.minBy(this.players, 'playerNumber'), 'playerNumber')) {
  288. try {
  289. await this.announceChangeAddress()
  290. } catch (nope) {
  291. debug('Error broadcasting changeAddress:', nope)
  292. this.endShuffleRound()
  293. }
  294. }
  295. break
  296. case 'incomingChangeAddress':
  297. /* Validate change address announcement. */
  298. // NOTE: If we are player one, we will have already sent
  299. // this message.
  300. if (!this.comms.outbox.sent['changeAddressAnnounce']) {
  301. await this.announceChangeAddress()
  302. }
  303. debug('Incoming change address',
  304. 'Encryption pubkey', message['message']['key']['key'],
  305. 'Legacy address', message['message']['address']['address'])
  306. /* Update this player with their change address. */
  307. _.extend(this.players[_.findIndex(this.players, { session: message['session'] })], {
  308. encryptionPubKey: message['message']['key']['key'],
  309. change: {
  310. legacyAddress: message['message']['address']['address']
  311. }
  312. })
  313. /**
  314. * If we are player 1, go ahead and send the first encrypted
  315. * unicast message containing the Bitcoin address that will
  316. * house our shuffled output. This function will return without
  317. * doing anything unless all players.
  318. */
  319. if (_.get(_.minBy(this.players, 'playerNumber'), 'playerNumber') === this.myPlayerNumber) {
  320. this.phase = 'shuffle'
  321. try {
  322. await this.forwardEncryptedShuffleTxOutputs(undefined, undefined)
  323. } catch (nope) {
  324. debug('Error broadcasting changeAddress:', nope)
  325. this.endShuffleRound()
  326. }
  327. }
  328. break
  329. case 'incomingEncryptedOutputs': {
  330. newPhaseName = _.isString(message['phase']) ? message['phase'].toLowerCase() : undefined
  331. // Grab the sender of this message by using the verificationKey used
  332. // to sign this protobuff message. The signature has already been
  333. // verified successfully but we're not sure yet if the sender is lying
  334. // about their player number. This check will be performed in the the
  335. // `forwardEncryptedShuffleTxOutputs` function.
  336. const sentBy = _.find(this.players, {
  337. verificationKey: _.get(jsonMessage, 'packets[0].packet.fromKey.key')
  338. })
  339. if (this.phase === 'announcement' && newPhaseName === 'shuffle') {
  340. this.phase = 'shuffle'
  341. this.forwardEncryptedShuffleTxOutputs(jsonMessage.packets, sentBy)
  342. }
  343. break
  344. }
  345. case 'finalTransactionOutputs':
  346. debug('got final transaction outputs!')
  347. newPhaseName = _.isString(
  348. message['phase']) ? message['phase'].toLowerCase() : undefined
  349. /* Set new phase name. */
  350. this.phase = newPhaseName
  351. this.checkFinalOutputsAndDoEquivCheck(jsonMessage.packets)
  352. break
  353. case 'incomingEquivCheck':
  354. try {
  355. await this.processEquivCheckMessage(message)
  356. } catch (nope) {
  357. debug('Error processing incoming equivCheck:', nope)
  358. }
  359. break
  360. case 'blame':
  361. this.handleBlameMessage(message)
  362. break
  363. case 'incomingInputAndSig':
  364. try {
  365. await this.verifyAndSubmit(message)
  366. } catch (nope) {
  367. debug('Error processing incoming output and signature:', nope)
  368. }
  369. break
  370. // case '':
  371. // break;
  372. default:
  373. break
  374. }
  375. // debug('Finished acting on', messageType, 'message\n\n');
  376. }
  377. /**
  378. * Process Websockets Error
  379. */
  380. processWsError (someError) {
  381. debug('Oh goodness, something is amiss!', someError)
  382. }
  383. /**
  384. * Write Debug File
  385. */
  386. writeDebugFile () {
  387. this.comms.writeDebugFile(true)
  388. }
  389. /***************************************************************************
  390. BEGIN COINFUSION PROTOCOL METHODS
  391. ----------------------------------
  392. **************************************************************************/
  393. // TODO:
  394. }
  395. module.exports = ShuffleFusion