1
0

CommChannel.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. /* Import core modules. */
  2. const _ = require('lodash')
  3. const debug = require('debug')('shuffle:comm')
  4. const EventEmitter = require('events').EventEmitter
  5. const moment = require('moment')
  6. const WebSocket = require('ws')
  7. /* Import local modules. */
  8. const serverMessages = require('./serverMessages.js')
  9. /**
  10. * Communications Channel (Class)
  11. */
  12. class CommChannel extends EventEmitter {
  13. constructor (connectionOptions, shuffleRoundInstance) {
  14. super()
  15. /* Set instance properties. */
  16. for (let oneOption in connectionOptions) {
  17. this[oneOption] = connectionOptions[oneOption]
  18. }
  19. /* Set server URI. */
  20. this.serverUri = connectionOptions.serverUri
  21. /* Validate server URI. */
  22. if (!this.serverUri) {
  23. /* Set connection error. */
  24. const connectionError = new Error('BAD_SERVER_URI')
  25. /* Emit connection error. */
  26. this.emit('connectionError', connectionError)
  27. }
  28. /* Initialize Websocket client. */
  29. this._wsClient = undefined
  30. /* Initialize server messages. */
  31. this.msg = serverMessages
  32. /* Initialize shuffle round. */
  33. this.round = shuffleRoundInstance
  34. /* Initialize outbox. */
  35. // NOTE: Our internal records for sent and received server messages.
  36. // Used for debugging bad rounds.
  37. this.outbox = {
  38. sent: {}
  39. }
  40. /* Initialize inbox. */
  41. this.inbox = {}
  42. return this
  43. }
  44. /**
  45. * Connect
  46. *
  47. * Establish websockets connection with shuffle server.
  48. */
  49. async connect () {
  50. console.info('\nServer Connection', this.serverUri) // eslint-disable-line no-console
  51. // TODO: This and all communication functionality will be moved to
  52. // a separate class. The `Round` should only touch messages
  53. // after they have been parsed, validated, and classified.
  54. if (typeof window !== 'undefined') {
  55. /* eslint-disable-next-line no-undef */
  56. this._wsClient = new window.WebSocket(this.serverUri)
  57. } else {
  58. this._wsClient = new WebSocket(this.serverUri, {
  59. origin: 'http://localhost'
  60. })
  61. }
  62. /* Handle incoming message from CashShuffle server. */
  63. // this._wsClient.on('message', (someMessageBuffer) => {
  64. this._wsClient.addEventListener('message', async (someMessageBuffer) => {
  65. /* Set message. */
  66. const message = await this.msg.decodeAndClassify(someMessageBuffer)
  67. // debug('Handling websocket (message):', message)
  68. /* Initialize message sub-class. */
  69. let messageSubClass
  70. /* Validate message. */
  71. if (message.pruned.messageType === '_unicast') {
  72. /* Validate round phase. */
  73. if (this.round.phase.toLowerCase() === 'announcement') {
  74. messageSubClass = 'incomingEncryptedOutputs'
  75. } else {
  76. messageSubClass = 'UNKNOWN'
  77. }
  78. }
  79. /* Change the message type for unicast messages. */
  80. Object.assign(message.pruned, {
  81. messageType: messageSubClass || message.pruned.messageType
  82. })
  83. /* Add the message to our inbox, in case we need it later. */
  84. const inboxEntry = {
  85. messageType: message.pruned.messageType,
  86. time: new Date().getTime(),
  87. protobuffMessage: {
  88. unpacked: message.full,
  89. components: message.components
  90. }
  91. }
  92. this.inbox[message.pruned.messageType] =
  93. this.inbox[message.pruned.messageType] ? _.sortBy(
  94. this.inbox[message.pruned.messageType]
  95. .concat(
  96. [inboxEntry]), ['time'], ['desc']) : [inboxEntry]
  97. /* Initialize packet verification results. */
  98. const packetVerifyResults = {
  99. success: [],
  100. fail: []
  101. }
  102. if (message.packets[0].packet.fromKey) {
  103. /* Retrieve sender. */
  104. const sender = _.find(this.round.players, {
  105. verificationKey: message.packets[0].packet.fromKey.key
  106. })
  107. debug('Websocket message (this.round.players / sender):',
  108. this.round.players, sender)
  109. // debug('Checking signature for',
  110. // message.pruned.messageType.toUpperCase(),
  111. // 'message from',
  112. // (sender ? sender.session + ' ( ' + sender.verificationKey + ' ) ' : 'player with sessionid ' + message.pruned.message.session)
  113. // )
  114. }
  115. /* Loop through ALL message packets. */
  116. for (let onePacket of message.packets) {
  117. /* Validate signature. */
  118. if (onePacket.signature) {
  119. if (!this.msg.checkPacketSignature(onePacket)) {
  120. packetVerifyResults.fail.push(onePacket)
  121. } else {
  122. packetVerifyResults.success.push(onePacket)
  123. }
  124. } else {
  125. // The signature doesn't need to be verified.
  126. packetVerifyResults.success.push(onePacket)
  127. }
  128. }
  129. /* Validate packet results. */
  130. if (!packetVerifyResults.fail.length) {
  131. this.emit('serverMessage', message)
  132. } else {
  133. console.error('Signature check failed!') // eslint-disable-line no-console
  134. // This event will be piped right into the
  135. // `assignBlame` method on the `ShuffleClient`
  136. // class
  137. this.emit('protocolViolation', {
  138. reason: 'INVALIDSIGNATURE',
  139. // accused: _.get(oneSignedPacket, 'packet.fromKey.key'),
  140. accused: _.get(packetVerifyResults, 'packet.fromKey.key'),
  141. invalid: packetVerifyResults.fail
  142. })
  143. }
  144. })
  145. /* Handle a NEW Websockets connection with the CashShuffle server. */
  146. // this._wsClient.on('open', () => {
  147. this._wsClient.addEventListener('open', () => {
  148. // this._wsClient.onopen = () => {
  149. this._wsConnected = true
  150. // debug('We are now connected to the cashshuffle server:', this.serverUri)
  151. this.emit('connected', this._wsClient)
  152. })
  153. /* Handle closing the Websockets connection. */
  154. // this._wsClient.on('close', (details) => {
  155. this._wsClient.addEventListener('close', (details) => {
  156. if (!this.round.roundComplete) {
  157. // FIXME: Should we attempt to automatically reconnect
  158. // and restart the process??
  159. debug('Socket connection closed:', details)
  160. this.emit('disconnected', details)
  161. }
  162. })
  163. /* Handle Websockets errors. */
  164. // this._wsClient.on('error', (someError) => {
  165. this._wsClient.addEventListener('error', (someError) => {
  166. console.error('Socket error!', someError) // eslint-disable-line no-console
  167. this.emit('connectionError', someError)
  168. })
  169. }
  170. /**
  171. * Send Message
  172. */
  173. sendMessage () {
  174. /* Set message type. */
  175. const messageType = arguments[0]
  176. /* Set message parameters. */
  177. // const messageParams = [].slice.call(arguments, 1, ) // FIXME: Why this trailing space??
  178. const messageParams = [].slice.call(arguments, 1) // FIXME: Why this trailing space??
  179. // debug('Now sending message:', arguments,
  180. // 'type:', messageType,
  181. // 'params:', messageParams)
  182. /* Initialize packet message. */
  183. let packedMessage = null
  184. /* Validate message type. */
  185. if (messageType && typeof this.msg[messageType] === 'function') {
  186. try {
  187. packedMessage = this.msg[messageType].apply(this, messageParams)
  188. } catch (nope) {
  189. console.error( // eslint-disable-line no-console
  190. 'Couldnt create', messageType,
  191. 'message using params', messageParams,
  192. '\n', nope)
  193. // TODO: Throw exception?
  194. }
  195. } else {
  196. // TODO: Should we throw an exception now?
  197. }
  198. /* Add the message to our outbox, in case we need it later. */
  199. const outboxEntry = {
  200. messageType: messageType,
  201. time: new Date().getTime(),
  202. protobuffMessage: {
  203. // packed: packedMessage.packed.toString('base64'),
  204. unpacked: packedMessage.unpacked.toJSON(),
  205. components: packedMessage.components
  206. }
  207. }
  208. /* Validate outbox. */
  209. if (!this.outbox[messageType]) {
  210. /* Initialize object. */
  211. const obj = {}
  212. /* Initialize object's message type. */
  213. obj[messageType] = []
  214. /* Update outbox. */
  215. Object.assign(this.outbox, obj)
  216. }
  217. /* Set outbox sent message flag. */
  218. this.outbox.sent[messageType] = true
  219. /* Add entry to outbox. */
  220. this.outbox[messageType].push(outboxEntry)
  221. /* Send packed message. */
  222. this._wsClient.send(packedMessage.packed)
  223. }
  224. /**
  225. * Write Debug File
  226. */
  227. writeDebugFile () {
  228. /* Loop through inbox. */
  229. for (let oneKey in this.inbox) {
  230. if (_.isArray(this.inbox[oneKey])) {
  231. this.inbox[oneKey] = _.sortBy(this.inbox[oneKey], ['time'], ['desc'])
  232. }
  233. }
  234. /* Loop through outbox. */
  235. for (let oneKey in this.outbox) {
  236. if (_.isArray(this.outbox[oneKey])) {
  237. this.outbox[oneKey] = _.sortBy(this.outbox[oneKey], ['time'], ['desc'])
  238. }
  239. }
  240. /* Build data package (for disk). */
  241. const writeThisToDisk = {
  242. phase: this.round.phase,
  243. coin: this.round.coin,
  244. ephemeralKeypair: this.round.ephemeralKeypair,
  245. encryptionKeypair: this.round.encryptionKeypair,
  246. shuffled: this.round.shuffled,
  247. change: this.round.change,
  248. players: this.round.players,
  249. equivHashPlaintext: this.round.equivHashPlaintext,
  250. equivHash: this.round.equivHash,
  251. shuffleTx: {
  252. signatures: this.round.shuffleTx.signatures,
  253. hex: this.round.shuffleTx.hex,
  254. serialized: this.round.shuffleTx.tx ? this.round.shuffleTx.tx.toObject() : {},
  255. results: this.round.shuffleTx.results
  256. },
  257. inbox: this.inbox,
  258. outbox: this.outbox
  259. }
  260. /* Set data. */
  261. const data = JSON.stringify(writeThisToDisk, null, 2)
  262. if (typeof window !== 'undefined') {
  263. console.error(data) // eslint-disable-line no-console
  264. } else {
  265. /* Write data to disk. */
  266. require('fs').writeFileSync(
  267. `_failedShuffle-${moment().unix()}.js`,
  268. 'module.exports = ' + data
  269. )
  270. }
  271. /* Quit application. */
  272. process.exit(0)
  273. }
  274. }
  275. /* Export module. */
  276. module.exports = CommChannel