diff --git a/public/dashjoin.js b/public/dashjoin.js index dd66431..9d7e38b 100644 --- a/public/dashjoin.js +++ b/public/dashjoin.js @@ -3,13 +3,13 @@ var DashJoin = ('object' === typeof module && exports) || {}; 'use strict'; let DashP2P = window.DashP2P || require('dashp2p'); + let DashTx = window.DashTx || require('dashtx'); const DV_LITTLE_ENDIAN = true; const DENOM_LOWEST = 100001; const PREDENOM_MIN = DENOM_LOWEST + 193; const COLLATERAL = 10000; // DENOM_LOWEST / 10 - const PAYLOAD_SIZE_MAX = 4 * 1024 * 1024; let STANDARD_DENOMINATIONS_MAP = { // 0.00100001 @@ -24,11 +24,25 @@ var DashJoin = ('object' === typeof module && exports) || {}; 0b00000001: 1000010000, }; + // Note: "mask" may be a misnomer. The spec seems to be more of an ID, + // but the implementation makes it look more like a mask... + let STANDARD_DENOMINATION_MASKS = { + // 0.00100001 + 100001: 0b00010000, + // 0.01000010 + 1000010: 0b00001000, + // 0.10000100 + 10000100: 0b00000100, + // 1.00001000 + 100001000: 0b00000010, + // 10.00010000 + 1000010000: 0b00000001, + }; + // https://github.com/dashpay/dash/blob/v19.x/src/coinjoin/coinjoin.h#L39 // const COINJOIN_ENTRY_MAX_SIZE = 9; // real // const COINJOIN_ENTRY_MAX_SIZE = 2; // just for testing right now - DashJoin.PAYLOAD_SIZE_MAX = PAYLOAD_SIZE_MAX; DashJoin.DENOM_LOWEST = DENOM_LOWEST; DashJoin.COLLATERAL = COLLATERAL; DashJoin.PREDENOM_MIN = PREDENOM_MIN; @@ -59,6 +73,17 @@ var DashJoin = ('object' === typeof module && exports) || {}; return 0; }; + Sizes.DSQ = 142; + Sizes.SENDDSQ = 1; // 1-byte bool + Sizes.DENOM = 4; // 32-bit uint + Sizes.PROTX = 32; + Sizes.TIME = 8; // 64-bit uint + Sizes.READY = 1; // 1-byte bool + Sizes.SIG = 97; + + // Sizes.DSSU = 16; + // Sizes.SESSION_ID = 4; + /** * Turns on or off DSQ messages (necessary for CoinJoin, but off by default) * @param {Object} opts @@ -72,43 +97,144 @@ var DashJoin = ('object' === typeof module && exports) || {}; send = true, }) { const command = 'senddsq'; - const SENDDSQ_SIZE = 1; // 1-byte bool + let [bytes, payload] = DashP2P.packers._alloc(message, Sizes.SENDDSQ); - if (!message) { - let dsqSize = DashP2P.sizes.HEADER_SIZE + SENDDSQ_SIZE; - message = new Uint8Array(dsqSize); + let sendByte = [0x01]; + if (!send) { + sendByte = [0x00]; } + payload.set(sendByte, 0); + + void DashP2P.packers.message({ network, command, bytes }); + return bytes; + }; - let payload = message.subarray(DashP2P.sizes.HEADER_SIZE); - if (send) { - payload.set([0x01], 0); - } else { - payload.set([0x00], 0); + /** + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Uint8Array?} [opts.message] + * @param {Uint32} opts.denomination + * @param {Uint8Array} opts.collateralTx + */ + Packers.dsa = function ({ network, message, denomination, collateralTx }) { + const command = 'dsa'; + let dsaSize = Sizes.DENOM + collateralTx.length; + let [bytes, payload] = DashP2P.packers._alloc(message, dsaSize); + + //@ts-ignore - numbers can be used as map keys + let denomMask = STANDARD_DENOMINATION_MASKS[denomination]; + if (!denomMask) { + throw new Error( + `contact your local Dash representative to vote for denominations of '${denomination}'`, + ); } - void DashP2P.packers.message({ network, command, bytes: message }); - return message; - // return { message, payload }; + let dv = new DataView(payload.buffer); + let offset = 0; + + dv.setUint32(offset, denomMask, DV_LITTLE_ENDIAN); + offset += Sizes.DENOM; + + payload.set(collateralTx, offset); + + void DashP2P.packers.message({ network, command, bytes }); + return bytes; }; - Sizes.DSQ_SIZE = 142; - // DSQ stuff?? - Sizes.DENOM = 4; - Sizes.PROTX = 32; - Sizes.TIME = 8; - Sizes.READY = 1; - Sizes.SIG = 97; - // + /** + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Uint8Array?} [opts.message] + * @param {Array} opts.inputs + * @param {Array} opts.outputs + * @param {Uint8Array} opts.collateralTx + */ + Packers.dsi = function ({ network, message, inputs, collateralTx, outputs }) { + const command = 'dsi'; + + let neutered = []; + for (let input of inputs) { + let _input = { + txId: input.txId || input.txid, + txid: input.txid || input.txId, + outputIndex: input.outputIndex, + }; + neutered.push(_input); + } + + let inputsHex = DashTx.serializeInputs(inputs); + let inputHex = inputsHex.join(''); + let outputsHex = DashTx.serializeOutputs(outputs); + let outputHex = outputsHex.join(''); + + let dsiSize = collateralTx.length; + dsiSize += inputHex.length / 2; + dsiSize += outputHex.length / 2; + + let [bytes, payload] = DashP2P.packers._alloc(message, dsiSize); - // Sizes.DSSU_SIZE = 16; - // Sizes.SESSION_ID_SIZE = 4; + let offset = 0; + { + let j = 0; + for (let i = 0; i < inputHex.length; i += 2) { + let end = i + 2; + let hex = inputHex.slice(i, end); + payload[j] = parseInt(hex, 16); + j += 1; + } + offset += inputHex.length / 2; + } + + payload.set(collateralTx, offset); + offset += collateralTx.length; + + { + let outputsPayload = payload.subarray(offset); + let j = 0; + for (let i = 0; i < outputHex.length; i += 2) { + let end = i + 2; + let hex = outputHex.slice(i, end); + outputsPayload[j] = parseInt(hex, 16); + j += 1; + } + offset += outputHex.length / 2; + } + + void DashP2P.packers.message({ network, command, bytes }); + return bytes; + }; + + /** + * @param {Object} opts + * @param {Uint8Array?} [opts.message] + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Array} [opts.inputs] + */ + Packers.dss = function ({ network, message, inputs }) { + const command = 'dss'; + + if (!inputs?.length) { + // TODO make better + throw new Error('you must provide some inputs'); + } + + let txInputsHex = DashTx.serializeInputs(inputs); + let txInputHex = txInputsHex.join(''); + + let dssSize = txInputHex.length / 2; + let [bytes, payload] = DashP2P.packers._alloc(message, dssSize); + void DashP2P.utils.hexToPayload(txInputHex, payload); + + void DashP2P.packers.message({ network, command, bytes }); + return bytes; + }; /** * @param {Uint8Array} bytes */ Parsers.dsq = function (bytes) { - if (bytes.length !== Sizes.DSQ_SIZE) { - let msg = `developer error: 'dsq' must be ${Sizes.DSQ_SIZE} bytes, but received ${bytes.length}`; + if (bytes.length !== Sizes.DSQ) { + let msg = `developer error: 'dsq' must be ${Sizes.DSQ} bytes, but received ${bytes.length}`; throw new Error(msg); } let dv = new DataView(bytes.buffer); @@ -160,40 +286,8 @@ var DashJoin = ('object' === typeof module && exports) || {}; return dsqMessage; }; - Utils.hexToBytes = function (hex) { - let bufLen = hex.length / 2; - let u8 = new Uint8Array(bufLen); - - let i = 0; - let index = 0; - let lastIndex = hex.length - 2; - for (;;) { - if (i > lastIndex) { - break; - } - - let h = hex.slice(i, i + 2); - let b = parseInt(h, 16); - u8[index] = b; - - i += 2; - index += 1; - } - - return u8; - }; - - Utils.bytesToHex = function (u8) { - /** @type {Array} */ - let hex = []; - - u8.forEach(function (b) { - let h = b.toString(16).padStart(2, '0'); - hex.push(h); - }); - - return hex.join(''); - }; + // Utils.hexToBytes = DashTx.utils.hexToBytes; + // Utils.bytesToHex = DashTx.utils.bytesToHex; Utils._evonodeMapToList = function (evonodesMap) { console.log('[debug] get evonode list...'); diff --git a/public/dashp2p.js b/public/dashp2p.js index ce0c464..eafd3e8 100644 --- a/public/dashp2p.js +++ b/public/dashp2p.js @@ -19,6 +19,9 @@ var DashP2P = ('object' === typeof module && exports) || {}; message: 'promise stream closed', }; + const PAYLOAD_SIZE_MAX = 4 * 1024 * 1024; + DashP2P.PAYLOAD_SIZE_MAX = PAYLOAD_SIZE_MAX; + let SIZES = { // header MAGIC_BYTES: 4, @@ -57,7 +60,7 @@ var DashP2P = ('object' === typeof module && exports) || {}; let Utils = {}; DashP2P.create = function () { - const HEADER_SIZE = Sizes.HEADER_SIZE; + const HEADER_SIZE = Sizes.HEADER; let p2p = {}; p2p.state = 'header'; @@ -324,8 +327,9 @@ var DashP2P = ('object' === typeof module && exports) || {}; SIZES.COMMAND_NAME + // 12 SIZES.PAYLOAD_SIZE + // 4 SIZES.CHECKSUM; // 4 - Sizes.HEADER_SIZE = TOTAL_HEADER_SIZE; // 24 - Sizes.PING_SIZE = SIZES.NONCE; // same as pong + Sizes.HEADER = TOTAL_HEADER_SIZE; // 24 + Sizes.PING = SIZES.NONCE; // same as pong + Sizes.VERACK = 0; Packers.PROTOCOL_VERSION = 70227; Packers.NETWORKS = {}; @@ -435,16 +439,16 @@ var DashP2P = ('object' === typeof module && exports) || {}; } let payloadLength = payload?.byteLength || 0; - let messageSize = Sizes.HEADER_SIZE + payloadLength; + let messageSize = Sizes.HEADER + payloadLength; let offset = 0; let embeddedPayload = false; let message = bytes; if (message) { if (!payload) { - payload = message.subarray(Sizes.HEADER_SIZE); + payload = message.subarray(Sizes.HEADER); payloadLength = payload.byteLength; - messageSize = Sizes.HEADER_SIZE + payloadLength; + messageSize = Sizes.HEADER + payloadLength; embeddedPayload = true; } } else { @@ -477,7 +481,7 @@ var DashP2P = ('object' === typeof module && exports) || {}; message.set(payloadSizeBytes, offset); offset += SIZES.PAYLOAD_SIZE; - let checksum = Packers.checksum(payload); + let checksum = Packers._checksum(payload); message.set(checksum, offset); offset += SIZES.CHECKSUM; @@ -487,44 +491,33 @@ var DashP2P = ('object' === typeof module && exports) || {}; return message; }; - Packers.verack = function ({ network = 'mainnet' }) { - let verackBytes = Packers.message({ - network: network, - command: 'verack', - payload: null, - }); - return verackBytes; - }; - /** - * In this case the only bytes are the nonce - * Use a .subarray(offset) to define an offset. - * (a manual offset will not work consistently, and .byteOffset is context-sensitive) - * @param {Object} opts - * @param {NetworkName} opts.network - "mainnet", "testnet", etc - * @param {Uint8Array?} [opts.message] - * @param {Uint8Array} opts.nonce + * Returns a correctly-sized buffer and subarray into the payload + * @param {Uint8Array} bytes + * @param {Uint16} payloadSize */ - Packers.pong = function ({ network = 'mainnet', message = null, nonce }) { - const command = 'pong'; - - if (!message) { - let pongSize = Sizes.HEADER_SIZE + Sizes.PING_SIZE; - message = new Uint8Array(pongSize); + Packers._alloc = function (bytes, payloadSize) { + let messageSize = DashP2P.sizes.HEADER + payloadSize; + if (!bytes) { + bytes = new Uint8Array(messageSize); + } else if (bytes.length !== messageSize) { + if (bytes.length < messageSize) { + let msg = `the provided buffer is only ${bytes.length} bytes, but at least ${messageSize} are needed`; + throw new Error(msg); + } + bytes = bytes.subarray(0, messageSize); } - let nonceBytes = message.subarray(Sizes.HEADER_SIZE); - nonceBytes.set(nonce, 0); + let payload = bytes.subarray(DashP2P.sizes.HEADER); - void Packers.message({ network, command, bytes: message }); - return message; + return [bytes, payload]; }; /** * First 4 bytes of SHA256(SHA256(payload)) in internal byte order. * @param {Uint8Array} payload */ - Packers.checksum = function (payload) { + Packers._checksum = function (payload) { // TODO this should be node-specific in node for performance reasons if (Crypto.createHash) { let hash = Crypto.createHash('sha256').update(payload).digest(); @@ -549,6 +542,7 @@ var DashP2P = ('object' === typeof module && exports) || {}; /* (it's simply very complex, okay?) */ Packers.version = function ({ network = 'mainnet', + message, protocol_version = Packers.PROTOCOL_VERSION, // alias of addr_trans_services //services, @@ -564,6 +558,8 @@ var DashP2P = ('object' === typeof module && exports) || {}; relay = null, mnauth_challenge = null, }) { + const command = 'version'; + if (!Array.isArray(addr_recv_services)) { throw new Error('"addr_recv_services" must be an array'); } @@ -590,7 +586,7 @@ var DashP2P = ('object' === typeof module && exports) || {}; sizes.mnauthChallenge = SIZES.MNAUTH_CHALLENGE_NONEMPTY; sizes.mnConnection = SIZES.MN_CONNECTION_NONEMPTY; - let TOTAL_SIZE = + let versionSize = SIZES.VERSION + SIZES.SERVICES + SIZES.TIMESTAMP + @@ -602,14 +598,15 @@ var DashP2P = ('object' === typeof module && exports) || {}; SIZES.ADDR_TRANS_PORT + SIZES.NONCE + SIZES.USER_AGENT_BYTES + - sizes.userAgentString + + sizes.userAgentString + // calc SIZES.START_HEIGHT + - sizes.relay + - sizes.mnauthChallenge + - sizes.mnConnection; - let payload = new Uint8Array(TOTAL_SIZE); - // Protocol version + sizes.relay + // calc + sizes.mnauthChallenge + // calc + sizes.mnConnection; // calc + let [bytes, payload] = Packers._alloc(message, versionSize); + + // Protocol version //@ts-ignore - protocol_version has a default value let versionBytes = Utils._uint32ToBytesLE(protocol_version); payload.set(versionBytes, 0); @@ -761,12 +758,41 @@ var DashP2P = ('object' === typeof module && exports) || {}; // payload.set([0x01], MNAUTH_CONNECTION_OFFSET); // } - let versionMessage = Packers.message({ - network: network, - command: 'version', - payload: payload, - }); - return versionMessage; + void Packers.message({ network, command, bytes }); + return bytes; + }; + + /** + * No payload, just an ACK + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Uint8Array?} [opts.message] - preallocated bytes + */ + Packers.verack = function ({ network = 'mainnet', message }) { + const command = 'verack'; + let [bytes] = Packers._alloc(message, Sizes.VERACK); + + void Packers.message({ network, command, bytes }); + return bytes; + }; + + /** + * In this case the only bytes are the nonce + * Use a .subarray(offset) to define an offset. + * (a manual offset will not work consistently, and .byteOffset is context-sensitive) + * @param {Object} opts + * @param {NetworkName} opts.network - "mainnet", "testnet", etc + * @param {Uint8Array?} [opts.message] + * @param {Uint8Array} opts.nonce + */ + Packers.pong = function ({ network = 'mainnet', message = null, nonce }) { + const command = 'pong'; + let [bytes, payload] = Packers._alloc(message, Sizes.PING); + + payload.set(nonce, 0); + + void Packers.message({ network, command, bytes }); + return bytes; }; /** @@ -781,8 +807,8 @@ var DashP2P = ('object' === typeof module && exports) || {}; * @param {Uint8Array} bytes */ Parsers.header = function (bytes) { - if (bytes.length < Sizes.HEADER_SIZE) { - let msg = `developer error: header should be ${Sizes.HEADER_SIZE}+ bytes (optional payload), not ${bytes.length}`; + if (bytes.length < Sizes.HEADER) { + let msg = `developer error: header should be ${Sizes.HEADER}+ bytes (optional payload), not ${bytes.length}`; throw new Error(msg); } let dv = new DataView(bytes.buffer); @@ -825,6 +851,30 @@ var DashP2P = ('object' === typeof module && exports) || {}; }; Parsers.SIZES = SIZES; + /** + * @param {String} hex + * @param {Uint8Array} payload + */ + Utils.hexToPayload = function (hex, payload) { + let i = 0; + let index = 0; + let lastIndex = hex.length - 2; + for (;;) { + if (i > lastIndex) { + break; + } + + let h = hex.slice(i, i + 2); + let b = parseInt(h, 16); + payload[index] = b; + + i += 2; + index += 1; + } + + return payload; + }; + Utils.EventStream = {}; /** @param {String} events */