/* Import core modules. */ const _ = require('lodash') const bch = require('bitcore-lib-cash') const debug = require('debug')('shuffle:server-messages') const path = require('path') const protobuf = require('protobufjs') /* Import local modules. */ const BetterMessage = require('./BetterMessage.js') /* Initialize magic (bytes). */ const magic = Buffer.from('42bcc32669467873', 'hex') /* Initialize protobuf classes. */ const pbEnums = ['Phase', 'Reason'] /* Initialize protobuf types. */ const pbTypes = [ 'Signed', 'Packet', 'Coins', 'Signatures', 'Message', 'Address', 'Registration', 'VerificationKey', 'EncryptionKey', 'DecryptionKey', 'Hash', 'Signature', 'Transaction', 'Blame', 'Invalid', 'Inputs', 'Packets' ] /* Initialize protobuf. */ // NOTE: Problem loading from local file, so copied to web root. See issue: // https://github.com/protobufjs/protobuf.js/issues/1023#issuecomment-629165262 // TODO: Translate `.proto` to `.json` and use JSON load. let PB = {} let protoFile = null if (typeof window !== 'undefined') { protoFile = 'shuffle.proto' } else { protoFile = path.join(__dirname, 'shuffle.proto') } protobuf.load(protoFile, (err, root) => { if (err) return console.error(err) // eslint-disable-line no-console /* Set root. */ PB.root = root /* Loop through ALL protobuf types. */ for (let oneTypeName of pbTypes) { PB[oneTypeName] = PB.root.lookupType(oneTypeName) } /* Loop through ALL protobuf classes. */ for (let oneClassName of pbEnums) { PB[oneClassName] = PB.root.lookupEnum(oneClassName) } }) /** * Message to Buffers */ function messageToBuffers (someBase64Message) { /* Initialize message buffer. */ const messageBuffer = Buffer.from(someBase64Message, 'base64') /* Validate message buffer. */ if (messageBuffer.length < 12) { throw new Error('bad_length') } else { /* Set message magic (bytes). */ const messageMagic = messageBuffer.slice(0, 8) /* Validate message magic (bytes). */ if (messageMagic.toString('hex') !== magic.toString('hex')) { throw new Error('message_magic') } /* Initialize message length. */ let messageLength = messageBuffer.slice(8, 12) /* Set message length. */ messageLength = messageLength.readUInt32BE(0) /* Set message payload. */ // const messagePayload = messageBuffer.slice(12, ) // FIXME: Why do we have a trailing space?? const messagePayload = messageBuffer.slice(12) // FIXME: Why do we have a trailing space?? /* Validate message payload. */ if (messagePayload.length !== messageLength) { console.error( // eslint-disable-line no-console 'Incorrect payload size', messagePayload.length, '!==', messageLength) throw new Error('message_payload') } else { /* Return message buffers. */ return { magic: messageBuffer.slice(0, 8).toString('base64'), length: messageBuffer.slice(8, 12).toString('base64'), payload: messageBuffer.slice(12).toString('base64'), buffer: messageBuffer.toString('base64') } } } } const handleMessageBuffer = (messageBuffer) => { /* Validate message buffer. */ if (messageBuffer.length < 12) { throw new Error('bad_length') } /* Set message magic (bytes). */ const messageMagic = messageBuffer.slice(0, 8) /* Validate message magic (bytes). */ if (messageMagic.toString('hex') !== magic.toString('hex')) { throw new Error('message_magic') } /* Initialize message length. */ let messageLength = messageBuffer.slice(8, 12) /* Set message length. */ messageLength = messageLength.readUInt32BE(0) /* Set message payload. */ const messagePayload = messageBuffer.slice(12) /* Build server message. */ const serverMessage = { packets: [], full: undefined, pruned: undefined, components: messageToBuffers(messageBuffer) } /* Validate message payload. */ if (messagePayload.length !== messageLength) { debug( 'Incorrect payload size:', messagePayload.length, '!==', messageLength ) throw new Error('message_payload') } else { /* Set decoded packets. */ const decodedPackets = PB.Packets.decode(messagePayload) /* Loop through ALL decoded packets. */ for (let onePacket of decodedPackets.packet) { serverMessage.packets.push(onePacket) } /* Set (full) server message. */ serverMessage.full = decodedPackets.toJSON() /* Set (pruned) server message. */ serverMessage.pruned = { message: _.get(serverMessage.full, 'packet[0].packet'), signature: _.get(serverMessage.full, 'packet[0].signature.signature') } } /* Validate (pruned) server message. */ if (!serverMessage.pruned.message) { throw new Error('message_parsing') } /* Initialize message types. */ // TODO: Pick more intuitive and more consistent message names. let messageTypes = [ { name: 'playerCount', required: ['number'] }, { name: 'serverGreeting', required: ['number', 'session'] }, { name: 'announcementPhase', required: ['number', 'phase'] }, { name: 'incomingVerificationKeys', required: ['session', 'fromKey.key', 'message.inputs'] }, { name: 'incomingChangeAddress', required: ['session', 'number', 'fromKey.key', 'message.address.address', 'message.key.key', 'phase'] }, /** * This message name will be changed before the `serverMessage` * event is emitted by the `CommChannel` class. * * We set the final message name there because that's where we * have access to round state data and the purpose of the message * (which should inform the name) changes based on the state of the * round. * * Yep, this is yet another hack to deal with the fact that there * is no support for a unique `messageName` field on the protocol * messages. */ { name: '_unicast', required: ['number', 'session', 'fromKey.key', 'toKey.key', 'message.str'] }, { name: 'incomingEquivCheck', required: ['number', 'session', 'phase', 'fromKey.key', 'message.hash.hash'] }, { name: 'incomingInputAndSig', required: ['number', 'session', 'phase', 'fromKey.key', 'message.signatures'] }, { name: 'finalTransactionOutputs', required: ['session', 'number', 'phase', 'fromKey.key', 'message.str'] }, { name: 'blame', required: ['number', 'session', 'fromKey.key', 'message.blame', 'phase'] } ] // Order the message types so that the most // specific descriptions are seen first by // the function that attempts to find a match. messageTypes = _.orderBy( messageTypes, function (ot) { return ot.required.length }, ['desc'] ) /* Set matching message type. */ const matchingMessageType = _.reduce(messageTypes, function (winner, oneObject) { /* Set required parameter values. */ const requiredParamValues = _.at(serverMessage.pruned.message, oneObject.required) /* Validate required parameter values. */ // NOTE: If none of the required parameters are missing, // consider this object a match. const isMatch = oneObject.required.length === _.compact(requiredParamValues).length /* Validate match. */ // NOTE: If our match has more matching params than // our previous match, use this one instead if (isMatch && winner.required.length < requiredParamValues.length) { return oneObject } else { return winner } }, { required: [] }) /* Update server message. */ Object.assign(serverMessage.pruned, { messageType: matchingMessageType.name || 'UNKNOWN' }) /* Return server message. */ return serverMessage } /** * Decode and Classify * * TODO: Clean this up so it better handles multi-packet messages. */ function decodeAndClassify (messageBuffer) { return new Promise(function (resolve, reject) { /* Validate and parse message data. */ /* eslint-disable-next-line no-undef */ if (messageBuffer && messageBuffer.data && messageBuffer.data instanceof Blob) { /* Initialize file reader. */ const reader = new FileReader() // eslint-disable-line no-undef /* Handle onload. */ reader.onload = () => { /* Handle message buffer. */ resolve(handleMessageBuffer(Buffer.from(reader.result))) } /* Handle onerror. */ reader.onerror = reject /* Read data as buffer. */ reader.readAsArrayBuffer(messageBuffer.data) } else { /* Handle message buffer. */ resolve(handleMessageBuffer(messageBuffer)) } }) } /** * Registration */ function registration (protocolVersion, amount, key) { /* Validate key. */ if (_.isObject(key) && typeof key.toString) { key = key.toString() } /* Set message. */ const message = PB.Signed.create({ packet: PB.Packet.create({ fromKey: PB.VerificationKey.create({ key }), registration: PB.Registration.create({ amount: amount, version: protocolVersion, // type: 'DEFAULT' // WHY AREN'T WE USING THIS?? type: 'DEFAULT' }) }) }) /* Return packed message. */ return packMessage(message) } /** * 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. */ function broadcastTransactionInput ( inputsObject, session, playerNumber, verificationPublicKey ) { /* Validate verification key. */ if (_.isObject(verificationPublicKey) && typeof verificationPublicKey.toString) { verificationPublicKey = verificationPublicKey.toString() } /* Initialize message. */ let message /* Set message. */ message = PB.Signed.create({ packet: PB.Packet.create({ fromKey: PB.VerificationKey.create({ key: verificationPublicKey }), message: PB.Message.create({ inputs: {} }), session: session, number: playerNumber }) }) /* Loop through ALL input objects. */ for (let key in inputsObject) { message.packet.message.inputs[key] = PB.Coins.create({ coins: inputsObject[key] }) } /* Return packed message. */ return packMessage(message) } /* { "packet": [ { "packet": { "session": "aGFIYURRd0JBWFN5MkJJNnBhdzZ6OQ==", "number": 2, "from_key": { "key": "03b9bf1605aa851945bd72e575d42f7ca874d9d7099f686c70893f927512010853" }, "to_key": { "key": "03aa863d01fd4c44043b73fccd820101f8bdc3bdf59a2472f1f1ecf6822ce4ad7b" }, "phase": 2, "message": { "str": "QklFMQNiC79dSfQjlIRKY/nHYE9KblxLkT6na8kelVoL8OIHW9/QqooDxTgtNm5Xhfh3R6kMWslw+uF6sYdhYZ53ce2sJBaaRWMLO8twqjfJGBPt/97XAAIVA57KNfzJOzdx6a8e/oUZ99xKPp6MRDBPGmME" } }, "signature": { "signature": "H/zYhGuMsptl9hL76Wn9ylNUmHKAzO+ZQbEHAkTPIp1aP0DiAjtvsyFmS1ZK03nTS5d5/4Vb5GoKnty7UijANds=" } }, { "packet": { "session": "aGFIYURRd0JBWFN5MkJJNnBhdzZ6OQ==", "number": 2, "from_key": { "key": "03b9bf1605aa851945bd72e575d42f7ca874d9d7099f686c70893f927512010853" }, "to_key": { "key": "03aa863d01fd4c44043b73fccd820101f8bdc3bdf59a2472f1f1ecf6822ce4ad7b" }, "phase": 2, "message": { "str": "QklFMQJtTrG5IQiUX1C0ZR67t5cQbN4v72uSzrCOuy1QEtOI41wQ2CGGK7lgtxyS9g8tzd9YHe+4DyMaSyrCIx/Ft/U27P6dU5xR6lVhf3ekV3mIW8/vH2lpb2AWY3Djl0egotBFrIylX+2W0nC9MVaU98xZ" } }, "signature": { "signature": "H8zKYj3OsPF/EUstjST/pzI8AqwcsK8OySDIxm9WABUYHWODcoBAzKsnEh1I7gLfABpAqnYkM0WK0SiyBeVUuq8=" } } ] } */ // this.comms.sendMessage('forwardEncryptedOutputs', [ // this.session, me.playerNumber, encryptedOutputAddresses.success, // this.phase.toUpperCase(), nextPlayer.verificationKey, this.ephemeralKeypair.publicKey, // this.ephemeralKeypair.privateKey // ]) /** * Forward Encrypted Outputs */ function forwardEncryptedOutputs ( session, fromPlayerNumber, arrayOfOutputs, phase, toVerificationKey, myVerificationPubKey, myVerificationPrivKey ) { /* Validate verification public key. */ if (_.isObject(myVerificationPubKey) && typeof myVerificationPubKey.toString) { myVerificationPubKey = myVerificationPubKey.toString() } /* Validate (to) verification key. */ if (_.isObject(toVerificationKey) && typeof toVerificationKey.toString) { toVerificationKey = toVerificationKey.toString() } // TODO: Make these server messages consistent with // respect to param validation and type checking. /* Initialize signed messages. */ const signedMessages = [] /* Loop through ALL outputs. */ for (let oneEncryptedAddress of arrayOfOutputs) { /* Set message. */ const message = PB.Signed.create({ packet: PB.Packet.create({ session: session, number: fromPlayerNumber, fromKey: PB.VerificationKey.create({ key: myVerificationPubKey }), toKey: PB.VerificationKey.create({ key: toVerificationKey }), phase: PB.Phase.values[phase.toUpperCase()], message: PB.Message.create({ str: oneEncryptedAddress }) }) }) /* Set message. */ const msg = Buffer.from(PB.Packet.encode(message.packet).finish()) .toString('base64') /* Set signature. */ const signature = new BetterMessage(msg, 'base64') .sign(bch.PrivateKey(myVerificationPrivKey)) /* Set message signature. */ message.signature = PB.Signature.create({ signature }) /* Add message to signed messages. */ signedMessages.push(message) } /* Return packed signed messages. */ return packMessage(signedMessages) } /* { "packet": [ { "packet": { "session": "OGlTZkdsSTZUQVgwNkU4Yk4wMkowTw==", "number": 1, "from_key": { "key": "0202135e4f7217957db961f26e3856a239e89023f6cd6088d6303775c3a61572bf" }, "phase": 6, "message": { "signatures": [ { "utxo": "3a019c3a44d5269edf8a6ca2588ead452b03f8fcdda2b622906c98e4d1d5778f:0", "signature": { "signature": "MzA0NDAyMjAwYjZiYzIyMDMzZTQwYzA5ZjJhNTdiZjZhOTc1YTEwMTk0OGU5ODAzMGY2OWRjMWZhYzlmZWY5ZTk0YTcxZTMzMDIyMDY4MDFlYzMzYWZiOTU5ZDZlZGZiMDQyMDM2NzcwYjA4MjI1NzczM2ExMjBhMDdmMTgzM2RlZTdhZTUzNDhlNzM0MQ==" } } ] } }, "signature": { "signature": "H0fwrr75/6GcPzPB6etyWyZD4mLlDGacVIs/j+VTldLCdE8yAfactL8jdXrJRwE7RqYhCFJ6vIHFpTu6Xa+SW2Y=" } } ] } */ /** * Broadcast Signature and UTXO */ function broadcastSignatureAndUtxo ( session, fromPlayerNumber, coinUtxoData, signatureString, phase, myVerificationPubKey, myVerificationPrivKey ) { /* Validate verification public key. */ if (_.isObject(myVerificationPubKey) && typeof myVerificationPubKey.toString) { myVerificationPubKey = myVerificationPubKey.toString() } /* Set message. */ const message = PB.Signed.create({ packet: PB.Packet.create({ session: session, number: fromPlayerNumber, fromKey: PB.VerificationKey.create({ key: myVerificationPubKey }), phase: PB.Phase.values[phase.toUpperCase()], message: PB.Message.create({ signatures: [] }) }) }) /* Add packet message signatures. */ message.packet.message.signatures.push(PB.Signature.create({ utxo: coinUtxoData, signature: PB.Signature.create({ signature: signatureString }) })) /* Set message. */ const msg = Buffer.from(PB.Packet.encode(message.packet).finish()) .toString('base64') /* Set signature. */ const signature = new BetterMessage(msg, 'base64') .sign(bch.PrivateKey(myVerificationPrivKey)) /* Set message signature. */ message.signature = PB.Signature.create({ signature }) /* Return packed message. */ return packMessage(message) } /* { "packet": [ { "packet": { "session": "c25hMmNwNm8xcXJIejlNMDhJdGFNZA==", "number": 2, "from_key": { "key": "03343954c832a7b870eb8758c1c280b954bfed8b8fb65a33d52f848aabdbf31dce" }, "phase": 4, "message": { "hash": { "hash": "1WDdy4zstoNgSnuSjagCxL5P8aqDbBerN92WSs1c2hY=" } } }, "signature": { "signature": "ICz+h2V5JBhHTronVb2FB4rCLHrIDi3gmsCn/+VphuojdofZBx5LCjefnnoGhwyYVQ40pSPi1u+JPXduPurrOBo=" } } ] } */ /** * Broadcast Equivocation Check */ function broadcastEquivCheck ( session, fromPlayerNumber, equivCheckHash, phase, myVerificationPubKey, myVerificationPrivKey ) { /* Validate verification public key. */ if (_.isObject(myVerificationPubKey) && typeof myVerificationPubKey.toString) { myVerificationPubKey = myVerificationPubKey.toString() } /* Set message. */ const message = PB.Signed.create({ packet: PB.Packet.create({ session: session, number: fromPlayerNumber, fromKey: PB.VerificationKey.create({ key: myVerificationPubKey }), phase: PB.Phase.values[phase.toUpperCase()], message: PB.Message.create({ hash: PB.Hash.create({ hash: equivCheckHash }) }) }) }) /* Set message. */ const msg = Buffer.from(PB.Packet.encode(message.packet).finish()) .toString('base64') /* Set signature. */ const signature = new BetterMessage(msg, 'base64') .sign(bch.PrivateKey(myVerificationPrivKey)) /* Set message signature. */ message.signature = PB.Signature.create({ signature }) /* Return packed message. */ return packMessage(message) } /** * Broadcast Final Output Addresses */ function broadcastFinalOutputAddresses ( session, fromPlayerNumber, arrayOfOutputs, phase, myVerificationPubKey, myVerificationPrivKey ) { /* Validate verification public key. */ if (_.isObject(myVerificationPubKey) && typeof myVerificationPubKey.toString) { myVerificationPubKey = myVerificationPubKey.toString() } /* Initialize signed messages. */ const signedMessages = [] /* Loop through ALL outputs. */ for (let onePlaintextAddress of arrayOfOutputs) { /* Set message. */ const message = PB.Signed.create({ packet: PB.Packet.create({ session: session, number: fromPlayerNumber, fromKey: PB.VerificationKey.create({ key: myVerificationPubKey }), phase: PB.Phase.values[phase.toUpperCase()], message: PB.Message.create({ str: onePlaintextAddress }) }) }) /* Set message. */ const msg = Buffer.from(PB.Packet.encode(message.packet).finish()) .toString('base64') /* Set signature. */ const signature = new BetterMessage(msg, 'base64') .sign(bch.PrivateKey(myVerificationPrivKey)) /* Set message signature. */ message.signature = PB.Signature.create({ signature }) /* Add message to signed messages. */ signedMessages.push(message) } /* Return packed signed messages. */ return packMessage(signedMessages) } /** * Change Address Announcement */ function changeAddressAnnounce ( session, playerNumber, changeAddress, encryptionPublicKey, phase, verificationPublicKey, verificationPrivateKey ) { /* Validate encryption public key. */ if (_.isObject(encryptionPublicKey) && typeof encryptionPublicKey.toString) { encryptionPublicKey = encryptionPublicKey.toString() } /* Validate verification public key. */ if (_.isObject(verificationPublicKey) && typeof verificationPublicKey.toString) { verificationPublicKey = verificationPublicKey.toString() } /* Initialize message. */ let message /* Set message. */ message = PB.Signed.create({ packet: PB.Packet.create({ session: session, number: playerNumber, fromKey: PB.VerificationKey.create({ key: verificationPublicKey }), phase: PB.Phase.values[phase.toUpperCase()], message: PB.Message.create({ address: PB.Address.create({ address: changeAddress }), key: PB.VerificationKey.create({ key: encryptionPublicKey }) }) }) }) /* Set message. */ const msg = Buffer.from(PB.Packet.encode(message.packet).finish()) .toString('base64') /* Set signature. */ const signature = new BetterMessage(msg, 'base64') .sign(bch.PrivateKey(verificationPrivateKey)) /* Set message signature. */ message.signature = PB.Signature.create({ signature }) /* Return packed message. */ return packMessage(message) } /** * Pack Message * * Encode (pack) a message into a prototype buffer (protobuf) object. */ function packMessage (oneOrMorePackets) { oneOrMorePackets = _.isArray(oneOrMorePackets) ? oneOrMorePackets : [oneOrMorePackets] /* Set packets. */ const packets = PB.Packets.create({ packet: oneOrMorePackets }) /* Set message buffer. */ const messageBuffer = Buffer.from(PB.Packets.encode(packets).finish()) /* Initialize length suffix. */ const lengthSuffix = Buffer.alloc(4) /* Set length suffix. */ lengthSuffix.writeUIntBE(messageBuffer.length, 0, 4) /* Set message components. */ const messageComponents = [magic, lengthSuffix, messageBuffer] /* Set full message. */ const fullMessage = Buffer.concat(messageComponents) /* Return message object. */ return { unpacked: packets, packed: fullMessage, components: messageToBuffers(fullMessage) } } /** * Check Packet Signature */ function checkPacketSignature (oneSignedPacket) { /* Initialize verification key. */ const verificationKey = oneSignedPacket.packet.fromKey.key /* Set signature. */ const signature = Buffer.from(oneSignedPacket.signature.signature).toString('base64') /* Set packet. */ const packet = PB.Packet.encode(oneSignedPacket.packet) /* Set public key. */ const pubkey = new bch.PublicKey(verificationKey) /* Set address. */ const address = pubkey.toAddress().toString() /* Set message. */ const message = Buffer.from(packet.finish()).toString('base64') // debug('checkPacketSignature', // oneSignedPacket, // verificationKey, // signature, // packet, // pubkey, // address, // message // ) /* Initialize result. */ let result = false try { /* Set result. */ result = new BetterMessage(message, 'base64').verify(address, signature) } catch (someError) { console.error('Error checking signature:', someError) // eslint-disable-line no-console } /* Return result. */ return result } /* { reason: < enum string citing reason for blame accusation >, accused: < verification key in hex format of player who 's being accused >, invalid: < an array of protobuf packets that provide evidence of fault >, hash: < hash provided by accused which differs from our own >, keypair: { key: < private key >, public: < public key > } } Possible Ban Reasons: INSUFFICIENTFUNDS = 0 DOUBLESPEND = 1 EQUIVOCATIONFAILURE = 2 SHUFFLEFAILURE = 3 SHUFFLEANDEQUIVOCATIONFAILURE = 4 INVALIDSIGNATURE = 5 MISSINGOUTPUT = 6 LIAR = 7 INVALIDFORMAT = 8 */ /** * Blame Message */ function blameMessage ( options, mySessionid, myPlayerNumber, myVerificationPublicKey, myVerificationPrivateKey ) { /* Validate verification public key. */ if (_.isObject(myVerificationPublicKey) && typeof myVerificationPublicKey.toString) { myVerificationPublicKey = myVerificationPublicKey.toString() } /* Validate verification private key. */ if (_.isObject(myVerificationPrivateKey) && typeof myVerificationPrivateKey.toString) { myVerificationPrivateKey = myVerificationPrivateKey.toString() } /* Set blame message. */ const blameMessage = _.reduce(_.keys(options), function (msg, oneOptionName) { switch (oneOptionName) { case 'reason': msg.packet.message.blame.reason = PB.Reason.values[ options.reason ? options.reason.toUpperCase() : 'NONE' ] break case 'accused': msg.packet.message.blame.accused = PB.VerificationKey.create({ key: options.accused }) break case 'invalid': // msg.packet.message.blame.invalue = PB.Invalid.create({ invalid: invalidPackets }) msg.packet.message.blame.invalue = PB.Invalid.create({ invalid: options.invalidPackets }) break case 'hash': msg.packet.message.hash = PB.Hash.create({ hash: options.hash }) break case 'keypair': msg.packet.message.blame.key = PB.DecryptionKey.create({ key: options.keypair.key, public: options.keypair.public }) break // case '': // msg.packet.message. // break default: break } /* Return message. */ return msg }, PB.Signed.create({ packet: PB.Packet.create({ session: mySessionid, number: myPlayerNumber, fromKey: PB.VerificationKey.create({ key: myVerificationPublicKey }), message: PB.Message.create({ blame: PB.Blame.create({ }) }), phase: PB.Phase.values['BLAME'] }) })) /* Set message. */ const msg = Buffer.from(PB.Packet.encode(blameMessage.packet).finish()) .toString('base64') /* Set blame message signature. */ blameMessage.signature = PB.Signature.create({ signature: new BetterMessage(msg, 'base64') .sign(bch.PrivateKey(myVerificationPrivateKey)) }) debug('Compiled blame message:', blameMessage) /* Return packed message. */ return packMessage(blameMessage) } /* Export module. */ module.exports = { PB, broadcastSignatureAndUtxo, broadcastEquivCheck, broadcastFinalOutputAddresses, forwardEncryptedOutputs, messageToBuffers, decodeAndClassify, registration, broadcastTransactionInput, changeAddressAnnounce, packMessage, blameMessage, checkPacketSignature }