From 08680ee8d452378bc2dd91df195fd5d4fed3048e Mon Sep 17 00:00:00 2001 From: Ravin Perera <33562092+ravinsp@users.noreply.github.com> Date: Mon, 1 Feb 2021 22:31:28 +0530 Subject: [PATCH] hpws upgrade with websocket protocol improvements. (#232) * Updated hpws binary and header. * Improved binary encoding support in client lib. --- examples/js_client/hp-client-lib.js | 27 +- src/comm/hpws.hpp | 110 +++- test/bin/hpws | Bin 35464 -> 70080 bytes test/metrics/hp-client-lib.js | 915 ---------------------------- test/metrics/metrics.js | 23 +- 5 files changed, 129 insertions(+), 946 deletions(-) delete mode 100644 test/metrics/hp-client-lib.js diff --git a/examples/js_client/hp-client-lib.js b/examples/js_client/hp-client-lib.js index 5683b078..6e6a6179 100644 --- a/examples/js_client/hp-client-lib.js +++ b/examples/js_client/hp-client-lib.js @@ -12,6 +12,8 @@ const outputValidationPassThreshold = 0.8; const connectionCheckIntervalMs = 1000; const recentActivityThresholdMs = 3000; + const textEncoder = new TextEncoder(); + const textDecoder = new TextDecoder(); // External dependency references. let WebSocket = null; @@ -430,7 +432,7 @@ // Sign the challenge and send back the response const response = msgHelper.createUserChallengeResponse(m.challenge, serverChallenge, protocol); - ws.send(msgHelper.serializeObject(response)); + wsSend(msgHelper.serializeObject(response)); connectionStatus = 1; return true; @@ -519,9 +521,10 @@ const messageHandler = async (rcvd) => { - const data = (connectionStatus < 2 || protocol == protocols.json) ? - (isBrowser ? await rcvd.data.text() : rcvd.data) : - (isBrowser ? await rcvd.data.arrayBuffer() : rcvd.data); + // Decode the received data buffer. + // In browser, text(json) mode requires the buffer to be "decoded" to text before JSON parsing. + const isTextMode = (connectionStatus < 2 || protocol == protocols.json); + const data = (isBrowser && isTextMode) ? textDecoder.decode(rcvd.data) : rcvd.data; try { m = msgHelper.deserializeMessage(data); @@ -593,6 +596,13 @@ handshakeResolver && handshakeResolver(false); } + const wsSend = (msg) => { + if (isString(msg)) + ws.send(textEncoder.encode(msg)); + else + ws.send(msg); + } + this.isConnected = () => { return connectionStatus == 2; }; @@ -602,6 +612,9 @@ return new Promise(resolve => { ws = isBrowser ? new WebSocket(server) : new WebSocket(server, { rejectUnauthorized: false }); + if (isBrowser) + ws.binaryType = "arraybuffer"; + handshakeResolver = resolve; ws.addEventListener("error", errorHandler); ws.addEventListener("open", openHandler); @@ -633,7 +646,7 @@ // Otherwise simply wait for the previously sent request. if (statResponseResolvers.length == 1) { const msg = msgHelper.createStatusRequest(); - ws.send(msgHelper.serializeObject(msg)); + wsSend(msgHelper.serializeObject(msg)); } return p; } @@ -663,7 +676,7 @@ contractInputResolvers[sigKey] = resolve; }); - ws.send(msgHelper.serializeObject(msg)); + wsSend(msgHelper.serializeObject(msg)); return p; } @@ -673,7 +686,7 @@ return; const msg = msgHelper.createReadRequest(request); - ws.send(msgHelper.serializeObject(msg)); + wsSend(msgHelper.serializeObject(msg)); } } diff --git a/src/comm/hpws.hpp b/src/comm/hpws.hpp index fb9e84dd..b4dd0884 100644 --- a/src/comm/hpws.hpp +++ b/src/comm/hpws.hpp @@ -47,7 +47,7 @@ namespace hpws // used when waiting for messages that should already be on the pipe #define HPWS_SMALL_TIMEOUT 10 // used when waiting for server process to spawn -#define HPWS_LONG_TIMEOUT 2500 +#define HPWS_LONG_TIMEOUT 50 typedef union { @@ -138,11 +138,11 @@ namespace hpws for (int i = 0; i < 4; ++i) { munmap(buffer[i], max_buffer_size); - close(buffer_fd[i]); + ::close(buffer_fd[i]); } - close(control_line_fd[0]); - close(control_line_fd[1]); + ::close(control_line_fd[0]); + ::close(control_line_fd[1]); if (HPWS_DEBUG) fprintf(stderr, "[HPWS.HPP] child destructed pid = %d\n", child_pid); @@ -215,6 +215,21 @@ namespace hpws } } + void close() + { + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] close called\n"); + + // send the control message informing hpws that we wish to close + char buf[1] = {'c'}; + + ::write(control_line_fd[1], buf, 1); + + // wait for the process to end gracefully + int status; + printf("waitpid result: %d\n", waitpid(child_pid, &status, 0)); // add timeout here? + } + std::optional write(std::string_view to_write) { if (HPWS_DEBUG) @@ -371,8 +386,8 @@ namespace hpws // --- PARENT - close(fd[1]); - close(fd[3]); + ::close(fd[1]); + ::close(fd[3]); int child_fd[2] = {fd[0], fd[2]}; @@ -481,8 +496,8 @@ namespace hpws if (fork_child_init) fork_child_init(); - close(fd[0]); - close(fd[2]); + ::close(fd[0]); + ::close(fd[2]); // dup fd[1] into fd 3 /*if (dup2(fd[1], 3) == -1) @@ -490,8 +505,8 @@ namespace hpws if (dup2(fd[3], 4) == -1) perror("dup2 fd[3]"); */ - // close(fd[1]); - // close(fd[3]); + // ::close(fd[1]); + // ::close(fd[3]); // we're assuming all fds above 3 will have close_exec flag execv(bin_path.data(), (char *const *)argv_pass); @@ -515,11 +530,11 @@ namespace hpws for (int i = 0; i < 4; ++i) { if (fd[i] > 0) - close(fd[i]); + ::close(fd[i]); if (mapping[i] != MAP_FAILED && mapping[i] != NULL) munmap(mapping[i], max_buffer_size); if (buffer_fd[i] > -1) - close(buffer_fd[i]); + ::close(buffer_fd[i]); } return error{error_code, std::string{error_msg}}; @@ -547,9 +562,9 @@ namespace hpws if (mapping[i] != MAP_FAILED && mapping[i] != NULL) munmap(mapping[i], max_buffer_size_); if (i < 2 && child_fd[i] > -1) - close(child_fd[i]); + ::close(child_fd[i]); if (buffer_fd[i] > -1) - close(buffer_fd[i]); + ::close(buffer_fd[i]); } if (pid_child > 0) @@ -589,6 +604,13 @@ namespace hpws std::variant accept(const bool no_block = false) { + + static int calls = 0; + ++calls; + + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[0] called %d\n", calls); + #define HPWS_ACCEPT_ERROR(code, msg) \ { \ accept_cleanup(mapping, child_fd, buffer_fd, pid); \ @@ -601,6 +623,8 @@ namespace hpws // must not use pid_t here since we transfer across IPC channel as a uint32. uint32_t pid = 0; + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[1] called %d\n", calls); { struct msghdr child_msg = {0}; memset(&child_msg, 0, sizeof(child_msg)); @@ -608,13 +632,16 @@ namespace hpws child_msg.msg_control = cmsgbuf; child_msg.msg_controllen = sizeof(cmsgbuf); + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[2] called %d\n", calls); + // If no-block is specified, we first check any bytes available on control fd // before attempting to do a blocking a read. if (no_block) { struct pollfd master_pfd; master_pfd.fd = this->master_control_fd_; - master_pfd.events = POLLIN; + master_pfd.events = POLLERR | POLLHUP | POLLNVAL | POLLIN; const int master_poll_result = poll(&master_pfd, 1, HPWS_SMALL_TIMEOUT); if (master_poll_result == -1) // 1 ms timeout @@ -624,6 +651,9 @@ namespace hpws HPWS_ACCEPT_ERROR(199, "no new client available"); } + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[3] called %d\n", calls); + int bytes_read = recvmsg(this->master_control_fd_, &child_msg, 0); struct cmsghdr *cmsg = CMSG_FIRSTHDR(&child_msg); @@ -635,16 +665,25 @@ namespace hpws if (HPWS_DEBUG) fprintf(stderr, "[HPWS.HPP] On accept received SCM: child_fd[0] = %d, child_fd[1] = %d\n", child_fd[0], child_fd[1]); + + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[4] called %d\n", calls); } // read info from child control line with a timeout struct pollfd pfd; int ret; + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[5] called %d\n", calls); + pfd.fd = child_fd[0]; // expect all setup messages on the hpws->hpcore controlfd (0) pfd.events = POLLIN; ret = poll(&pfd, 1, HPWS_SMALL_TIMEOUT); // 1 ms timeout + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[6] called %d\n", calls); + // timeout or error if (ret < 1) HPWS_ACCEPT_ERROR(202, "timeout waiting for hpws accept child message"); @@ -653,11 +692,17 @@ namespace hpws if (recv(child_fd[0], (unsigned char *)(&pid), sizeof(pid), 0) < sizeof(pid)) HPWS_ACCEPT_ERROR(212, "did not receive expected 4 byte pid of child process on accept"); + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[7] called %d\n", calls); + // second thing we'll receive is IP address structure of the client addr_t buf; int bytes_read = recv(child_fd[0], (unsigned char *)(&buf), sizeof(buf), 0); + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[8] called %d\n", calls); + if (bytes_read < sizeof(buf)) HPWS_ACCEPT_ERROR(202, "received message on master control line was not sizeof(sockaddr_in6)"); @@ -669,12 +714,19 @@ namespace hpws child_msg.msg_control = cmsgbuf; child_msg.msg_controllen = sizeof(cmsgbuf); + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[9] called %d\n", calls); + int bytes_read = recvmsg(child_fd[0], &child_msg, 0); struct cmsghdr *cmsg = CMSG_FIRSTHDR(&child_msg); if (cmsg == NULL || cmsg->cmsg_type != SCM_RIGHTS) HPWS_ACCEPT_ERROR(203, "non-scm_rights message sent on accept child control line"); memcpy(&buffer_fd, CMSG_DATA(cmsg), sizeof(buffer_fd)); + + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[10] called %d\n", calls); + for (int i = 0; i < 4; ++i) { //fprintf(stderr, "scm passed buffer_fd[%d] = %d\n", i, buffer_fd[i]); @@ -685,6 +737,8 @@ namespace hpws if (mapping[i] == MAP_FAILED) HPWS_ACCEPT_ERROR(204, "could not mmap scm_rights passed buffer fd"); } + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[11] called %d\n", calls); } { struct pollfd pfd; @@ -695,10 +749,19 @@ namespace hpws if (HPWS_DEBUG) fprintf(stderr, "[HPWS.HPP] waiting for 'r' on child_fd[%d]=%d accept\n", i, child_fd[i]); pfd.fd = child_fd[i]; - pfd.events = POLLIN; + pfd.events = POLLERR | POLLHUP | POLLNVAL | POLLIN; + + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[12] called %d\n", calls); + // now we wait for a 'r' ready message or for the socket/client to die ret = poll(&pfd, 1, HPWS_LONG_TIMEOUT); // default= 1500 ms timeout + if (!(pfd.revents & POLLIN)) + HPWS_ACCEPT_ERROR(5, "could not read from client_fd"); + + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[12a] called %d - ret %d\n", calls, ret); char rbuf[2]; bytes_read = recv(child_fd[i], rbuf, sizeof(rbuf), 0); if (bytes_read < 1) @@ -713,6 +776,9 @@ namespace hpws if (HPWS_DEBUG) fprintf(stderr, "[HPWS.HPP] 'r%c' received on child_fd[%d]=%d\n", rbuf[1], i, child_fd[i]); } + + if (HPWS_DEBUG) + fprintf(stderr, "[HPWS.HPP] Accept[13] called %d\n", calls); } return client{ @@ -739,7 +805,7 @@ namespace hpws waitpid(server_pid_, &status, 0 /* should we use WNOHANG? */); } - close(master_control_fd_); + ::close(master_control_fd_); } } @@ -820,7 +886,7 @@ namespace hpws // --- PARENT - close(fd[1]); + ::close(fd[1]); int flags = fcntl(fd[0], F_GETFD, NULL); if (flags < 0) @@ -886,11 +952,11 @@ namespace hpws if (fork_child_init) fork_child_init(); - close(fd[0]); + ::close(fd[0]); // dup fd[1] into fd 3 dup2(fd[1], 3); - close(fd[1]); + ::close(fd[1]); // we're assuming all fds above 3 will have close_exec flag execv(bin_path.data(), (char *const *)argv_pass); @@ -912,9 +978,9 @@ namespace hpws waitpid(pid, &status, 0 /* should we use WNOHANG? */); } if (fd[0] > 0) - close(fd[0]); + ::close(fd[0]); if (fd[1] > 0) - close(fd[1]); + ::close(fd[1]); return error{error_code, std::string{error_msg}}; } diff --git a/test/bin/hpws b/test/bin/hpws index 4a51c2243633fd9109d576d4b1f6aa96b4237106..6f508cec2d558a7afa82e8d4b6fd3028b272bef5 100755 GIT binary patch literal 70080 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 { - - // Whether we are in Browser or NodeJs. - const isBrowser = !(typeof window === 'undefined'); - - // In browser, avoid duplicate initializations. - if (isBrowser && window.HotPocket) - return; - - const supportedHpVersion = "0.0"; - const serverChallengeSize = 16; - const outputValidationPassThreshold = 0.8; - const connectionCheckIntervalMs = 1000; - const recentActivityThresholdMs = 3000; - - // External dependency references. - let WebSocket = null; - let sodium = null; - let bson = null; - let blake3 = null; - let logLevel = 0; // 0=info, 1=error - - /*--- Included in public interface. ---*/ - const protocols = { - json: "json", - bson: "bson" // (Requires nodejs or browserified hp client library on Browser) - } - Object.freeze(protocols); - - /*--- Included in public interface. ---*/ - const events = { - disconnect: "disconnect", - contractOutput: "contractOutput", - contractReadResponse: "contractReadResponse", - connectionChange: "connectionChange", - unlChange: "unlChange" - } - Object.freeze(events); - - /*--- Included in public interface. ---*/ - const generateKeys = async (privateKeyHex = null) => { - - await initSodium(); - - if (!privateKeyHex) { - const keys = sodium.crypto_sign_keypair(); - return { - privateKey: keys.privateKey, - publicKey: keys.publicKey - } - } - else { - const binPrivateKey = hexToUint8Array(privateKeyHex); - return { - privateKey: Uint8Array.from(binPrivateKey), - publicKey: Uint8Array.from(binPrivateKey.slice(32)) - } - } - } - - /*--- Included in public interface. ---*/ - const createClient = async (servers, clientKeys, options) => { - - const defaultOptions = { - contractId: null, - contractVersion: null, - trustedServerKeys: null, - protocol: protocols.json, - requiredConnectionCount: 1, - connectionTimeoutMs: 5000 - }; - const opt = options ? { ...defaultOptions, ...options } : defaultOptions; - - if (!clientKeys) - throw "clientKeys not specified."; - if (opt.contractId == "") - throw "contractId not specified. Specify null to bypass contract id validation."; - if (opt.contractVersion == "") - throw "contractVersion not specified. Specify null to bypass contract version validation."; - if (!opt.protocol || (opt.protocol != protocols.json && opt.protocol != protocols.bson)) - throw "Valid protocol not specified."; - if (!opt.requiredConnectionCount || opt.requiredConnectionCount == 0) - throw "requiredConnectionCount must be greater than 0."; - if (!opt.connectionTimeoutMs || opt.connectionTimeoutMs == 0) - throw "Connection timeout must be greater than 0."; - - await initSodium(); - await initBlake3(); - initWebSocket(); - if (opt.protocol == protocols.bson) - initBson(); - - // Load servers and serverKeys to object keys to avoid duplicates. - - const serversLookup = {}; - servers && servers.forEach(s => { - const url = s.trim(); - if (url.length > 0) - serversLookup[url] = true - }); - if (Object.keys(serversLookup).length == 0) - throw "servers not specified."; - if (opt.requiredConnectionCount > Object.keys(serversLookup).length) - throw "requiredConnectionCount is higher than no. of servers."; - - let trustedKeysLookup = {}; - opt.trustedServerKeys && opt.trustedServerKeys.sort().forEach(k => { - const key = k.trim(); - if (key.length > 0) - trustedKeysLookup[key] = true - }); - if (Object.keys(trustedKeysLookup).length == 0) - trustedKeysLookup = null; - - return new HotPocketClient(opt.contractId, opt.contractVersion, clientKeys, serversLookup, trustedKeysLookup, opt.protocol, opt.requiredConnectionCount, opt.connectionTimeoutMs); - } - - function HotPocketClient(contractId, contractVersion, clientKeys, serversLookup, trustedKeysLookup, protocol, requiredConnectionCount, connectionTimeoutMs) { - - let emitter = new EventEmitter(); - - // The accessor function passed into connections to query latest trusted key list. - // We update the returning key list whenever we get a unl update. - const getTrustedKeys = () => trustedKeysLookup; - - // Whenever unl change is reported, update the trusted key list. - emitter.on(events.unlChange, (unl) => { - trustedKeysLookup = {}; - unl.sort().forEach(pubkey => trustedKeysLookup[pubkey] = true); - }) - - const nodes = Object.keys(serversLookup).map(s => { - return { - server: s, // Server address. - connection: null, // Hot Pocket connection (if any). - lastActivity: 0 // Last connection activity timestamp. - } - }); - - let status = 0; //0:none, 1:connected, 2:closed - - // This will get fired whenever the required connection count gets fullfilled. - let initialConnectSuccess = null; - - // Tracks when was the earliest time that we were missing some required connections. - // 0 indicates we are no missing any connections. - let connectionsMissingFrom = new Date().getTime(); - - // Checks for missing connections and attempts to establish them. - const reviewConnections = () => { - - if (status == 2) - return; - - // Check for connection changes periodically. - setTimeout(() => { - reviewConnections(); - }, connectionCheckIntervalMs); - - // Check whether we have fullfilled all required connections. - if (nodes.filter(n => n.connection && n.connection.isConnected()).length == requiredConnectionCount) { - connectionsMissingFrom = 0; - initialConnectSuccess && initialConnectSuccess(true); - initialConnectSuccess = null; - status = 1; - return; - } - - if (connectionsMissingFrom == 0) { - // Reaching here means we moved from connections-fullfilled state to missing-connections state. - connectionsMissingFrom = new Date().getTime(); - } - else if ((new Date().getTime() - connectionsMissingFrom) > connectionTimeoutMs) { - - // This means we were not able to maintain required connection count for the entire timeout period. - - liblog(1, "Missing-connections timeout reached."); - - // Close and cleanup all connections if we hit the timeout. - this.close().then(() => { - if (initialConnectSuccess) { - initialConnectSuccess(false); - initialConnectSuccess = null; - } - else { - emitter && emitter.emit(events.disconnect); - } - }); - return; - } - - // Reaching here means we should attempt to establish more connections if we have available slots. - let currentConnectionCount = nodes.filter(n => n.connection).length; - if (currentConnectionCount == requiredConnectionCount) - return; - - // Find out available slots. - // Skip nodes that are already connected or is currently establishing connection. - // Skip nodes that have recently shown some connection activity. - // Give priority to nodes that have not shown any activity recently. - const freeNodes = nodes.filter(n => !n.connection && (new Date().getTime() - n.lastActivity) > recentActivityThresholdMs); - freeNodes.sort((a, b) => a.lastActivity - b.lastActivity); // Oldest activity comes first. - - while (currentConnectionCount < requiredConnectionCount && freeNodes.length > 0) { - - // Get the next available node. - const n = freeNodes.shift(); - n.connection = new HotPocketConnection(contractId, contractVersion, clientKeys, n.server, getTrustedKeys, protocol, connectionTimeoutMs, emitter); - n.lastActivity = new Date().getTime(); - - n.connection.connect().then(success => { - if (!success) - n.connection = null; - else - emitter && emitter.emit(events.connectionChange, n.server, "add"); - }); - - n.connection.onClose = () => { - n.connection = null; - emitter && emitter.emit(events.connectionChange, n.server, "remove"); - }; - - currentConnectionCount++; - } - } - - this.connect = () => { - - if (status > 0) - return; - - reviewConnections(); - return new Promise(resolve => { - initialConnectSuccess = resolve; - }) - } - - this.close = async () => { - - if (status == 2) - return; - - status = 2; - emitter.clear(events.connectionChange); - emitter.clear(events.contractOutput); - emitter.clear(events.contractReadResponse); - - // Close all nodes connections. - await Promise.all(nodes.filter(n => n.connection).map(n => n.connection.close())); - nodes.forEach(n => n.connection = null); - } - - this.on = (event, listener) => { - emitter.on(event, listener); - } - - this.clear = (event) => { - emitter.clear(event); - } - - this.sendContractInput = async (input, nonce = null, maxLclOffset = null) => { - if (status == 2) - return; - - return await Promise.all( - nodes.filter(n => n.connection && n.connection.isConnected()) - .map(n => n.connection.sendContractInput(input, nonce, maxLclOffset))); - } - - this.sendContractReadRequest = (request) => { - if (status == 2) - return; - - nodes.filter(n => n.connection && n.connection.isConnected()).forEach(n => { - n.connection.sendContractReadRequest(request); - }); - } - } - - function HotPocketConnection(contractId, contractVersion, clientKeys, server, getTrustedKeys, protocol, connectionTimeoutMs, emitter) { - - // Create message helper with JSON protocol initially. - // After challenge handshake, we will change this to use the protocol specified by user. - const msgHelper = new MessageHelper(clientKeys, protocols.json); - - let connectionStatus = 0; // 0:none, 1:server challenge sent, 2:handshake complete. - let serverChallenge = null; // The hex challenge we have issued to the server. - let reportedContractId = null; - let reportedContractVersion = null; - let pubkey = false; // Pubkey hex (with prefix) of this connection. - - let ws = null; - let handshakeTimer = null; // Timer to track connection handshake timeout. - let handshakeResolver = null; - let closeResolver = null; - let statResponseResolvers = []; - let contractInputResolvers = {}; - - // Calcualtes the blake3 hash of all array items. - const getHash = (arr) => { - const hash = blake3.createHash(); - arr.forEach(item => hash.update(item)); - return new Uint8Array(hash.digest()); - } - - // Get root hash of the given merkle hash tree. (called recursively) - // checkHashString specifies the hash that must be checked for existance. - const getMerkleHash = (tree, checkHashString) => { - - const listToHash = []; // Collects elements to hash. - let checkHashFound = false; - - for (let elem of tree) { - - if (Array.isArray(elem)) { - // If the 'elem' is an array we should find the root hash of the array. - // Call this func recursively. If checkHash already found, pass null. - const result = getMerkleHash(elem, checkHashFound ? null : checkHashString); - if (result[0] == true) - checkHashFound = true; - - listToHash.push(result[1]); - } - else { - // 'elem' is a single hash value. We get the hash bytes depending on the data type. - // (json encoding will use hex string and bson will use buffer) - const hashBytes = isString(elem) ? hexToUint8Array(elem) : elem.buffer; - listToHash.push(hashBytes); - - // If checkHash is specified, compare the hashes. - if (checkHashString && msgHelper.stringifyValue(hashBytes) == checkHashString) - checkHashFound = true; - } - } - - // Return a tuple of whether check hash was found and the root hash of the provided merkle tree. - return [checkHashFound, getHash(listToHash)]; - } - - // Verifies whether the provided root hash has enough signatures from unl. - const validateHashSignatures = (rootHash, signatures, unlKeysLookup) => { - - const totalUnl = Object.keys(unlKeysLookup).length; - if (totalUnl == 0) { - liblog(1, "Cannot validate outputs with empty unl."); - return false; - } - - const passedKeys = {}; - - // 'signatures' is an array of pairs of [pubkey, signature] - for (pair of signatures) { - const pubkeyHex = msgHelper.stringifyValue(pair[0]); // Gets the pubkey hex to use for unl lookup key. - - // Get the signature and issuer pubkey bytes based on the data type. - // (json encoding will use hex string and bson will use buffer) - const pubkey = isString(pair[0]) ? hexToUint8Array(pair[0].substring(2)) : pair[0].buffer.slice(1); // Skip prefix byte. - const sig = isString(pair[1]) ? hexToUint8Array(pair[1]) : pair[1].buffer; - - // Check whether the pubkey is in unl and whether signature is valid. - if (!passedKeys[pubkeyHex] && unlKeysLookup[pubkeyHex] && sodium.crypto_sign_verify_detached(sig, rootHash, pubkey)) - passedKeys[pubkeyHex] = true; - } - - // Check the percentage of unl keys that passed the signature check. - const passed = Object.keys(passedKeys).length; - return ((passed / totalUnl) >= outputValidationPassThreshold); - } - - const validateOutput = (msg, trustedKeys) => { - - // Calculate combined output hash with user's pubkey. - const outputHash = getHash([[0xED], clientKeys.publicKey, ...msgHelper.spreadArrayField(msg.outputs)]); - - const result = getMerkleHash(msg.hashes, msgHelper.stringifyValue(outputHash)); - if (result[0] == true) { - const rootHash = result[1]; - - // Verify the issued signatures against the root hash. - return validateHashSignatures(rootHash, msg.unl_sig, trustedKeys); - } - - return false; - } - - const validateAndEmitUnlChange = (changedUnl) => { - // If this is currently a trusted connection, notify unl update. - const trustedKeys = getTrustedKeys(); - if (trustedKeys && trustedKeys[pubkey]) { - // Prepare sorted new unl lookup object for equality comparison. - const newUnl = {}; - changedUnl.sort().forEach(k => newUnl[k] = true); - - // Only emit unl change event if the unl has really changed. - if (JSON.stringify(trustedKeys) != JSON.stringify(newUnl)) - emitter && emitter.emit(events.unlChange, changedUnl); - } - } - - const handshakeMessageHandler = (m) => { - - if (connectionStatus == 0 && m.type == "user_challenge" && m.hp_version && m.contract_id) { - - if (m.hp_version != supportedHpVersion) { - liblog(1, `Incompatible Hot Pocket server version. Expected:${supportedHpVersion} Got:${m.hp_version}`); - return false; - } - else if (!m.contract_id) { - liblog(1, "Server did not specify contract id."); - return false; - } - else if (contractId && m.contract_id != contractId) { - liblog(1, `Contract id mismatch. Expected:${contractId} Got:${m.contract_id}`); - return false; - } - else if (!m.contract_version) { - liblog(1, "Server did not specify contract version."); - return false; - } - else if (contractVersion && m.contract_version != contractVersion) { - liblog(1, `Contract version mismatch. Expected:${contractVersion} Got:${m.contract_version}`); - return false; - } - - reportedContractId = m.contract_id; - reportedContractVersion = m.contract_version; - - // Generate the challenge we are sending to server. - serverChallenge = uint8ArrayToHex(sodium.randombytes_buf(serverChallengeSize)); - - // Sign the challenge and send back the response - const response = msgHelper.createUserChallengeResponse(m.challenge, serverChallenge, protocol); - ws.send(msgHelper.serializeObject(response)); - - connectionStatus = 1; - return true; - } - else if (connectionStatus == 1 && serverChallenge && m.type == "server_challenge_response" && m.sig && m.pubkey) { - - // Verify server challenge response. - const stringToVerify = serverChallenge + reportedContractId + reportedContractVersion; - const serverPubkeyHex = m.pubkey.substring(2); // Skip 'ed' prefix; - if (!sodium.crypto_sign_verify_detached(hexToUint8Array(m.sig), stringToVerify, hexToUint8Array(serverPubkeyHex))) { - liblog(1, `${server} challenge response verification failed.`); - return false; - } - - clearTimeout(handshakeTimer); // Cancel the handshake timeout monitor. - handshakeTimer = null; - serverChallenge = null; // Clear the sent challenge as we no longer need it. - msgHelper.useProtocol(protocol); // Here onwards, use the message protocol specified by user. - pubkey = m.pubkey; // Set this connection's public key. - connectionStatus = 2; // Handshake complete. - - // If we are still connected, report handshaking as successful. - // (If websocket disconnects, handshakeResolver will be already null) - handshakeResolver && handshakeResolver(true); - liblog(0, `Connected to ${server}`); - - validateAndEmitUnlChange(m.unl); - - return true; - } - - liblog(1, `${server} invalid message during handshake. Connection status:${connectionStatus}`); - liblog(0, m); - return false; - } - - const contractMessageHandler = (m) => { - - if (m.type == "contract_read_response") { - emitter && emitter.emit(events.contractReadResponse, msgHelper.deserializeOutput(m.content)); - } - else if (m.type == "contract_input_status") { - const sigKey = msgHelper.stringifyValue(m.input_sig); - const resolver = contractInputResolvers[sigKey]; - if (resolver) { - if (m.status == "accepted") - resolver("ok"); - else - resolver(m.reason); - delete contractInputResolvers[sigKey]; - } - } - else if (m.type == "contract_output") { - if (emitter) { - // Validate outputs if trusted keys is not null. (null means bypass validation) - const trustedKeys = getTrustedKeys(); - if (!trustedKeys || validateOutput(m, trustedKeys)) - m.outputs.forEach(output => emitter.emit(events.contractOutput, msgHelper.deserializeOutput(output))); - else - liblog(1, "Output validation failed."); - } - } - else if (m.type == "stat_response") { - statResponseResolvers.forEach(resolver => { - resolver({ - lcl: m.lcl, - lclSeqNo: m.lcl_seqno - }); - }) - statResponseResolvers = []; - } - else if (m.type == "unl_change") { - if (m.unl) { - // Convert unl pubkeys to hex string. - let unl = m.unl.map(k => msgHelper.stringifyValue(k)); - validateAndEmitUnlChange(unl); - } - } - else { - liblog(1, "Received unrecognized contract message: type:" + m.type); - return false; - } - - return true; - } - - const messageHandler = async (rcvd) => { - - const data = (connectionStatus < 2 || protocol == protocols.json) ? - (isBrowser ? await rcvd.data.text() : rcvd.data) : - (isBrowser ? await rcvd.data.arrayBuffer() : rcvd.data); - - try { - m = msgHelper.deserializeMessage(data); - } - catch (e) { - liblog(1, e); - liblog(0, "Exception deserializing: "); - liblog(0, data || rcvd); - - // If we get invalid message during handshake, close the socket. - if (connectionStatus < 2) - this.close(); - - return; - } - - let isValid = false; - if (connectionStatus < 2) - isValid = handshakeMessageHandler(m); - else if (connectionStatus == 2) - isValid = contractMessageHandler(m); - - if (!isValid) { - // If we get invalid message during handshake, close the socket. - if (connectionStatus < 2) - this.close(); - } - } - - const openHandler = () => { - ws.addEventListener("message", messageHandler); - ws.addEventListener("close", closeHandler); - - handshakeTimer = setTimeout(() => { - // If handshake does not complete within timeout, close the connection. - this.close(); - handshakeTimer = null; - }, connectionTimeoutMs); - } - - const closeHandler = () => { - - if (closeResolver) - liblog(0, "Closing connection to " + server); - else - liblog(0, "Disconnected from " + server); - - emitter = null; - - if (handshakeTimer) - clearTimeout(handshakeTimer); - - // If there are any ongoing resolvers resolve them with error output. - - handshakeResolver && handshakeResolver(false); - handshakeResolver = null; - - statResponseResolvers.forEach(resolver => resolver(null)); - statResponseResolvers = []; - - Object.values(contractInputResolvers).forEach(resolver => resolver(null)); - contractInputResolvers = {}; - - this.onClose && this.onClose(); - closeResolver && closeResolver(); - } - - const errorHandler = (e) => { - handshakeResolver && handshakeResolver(false); - } - - this.isConnected = () => { - return connectionStatus == 2; - }; - - this.connect = () => { - liblog(0, "Connecting to " + server); - return new Promise(resolve => { - - ws = isBrowser ? new WebSocket(server) : new WebSocket(server, { rejectUnauthorized: false }); - handshakeResolver = resolve; - ws.addEventListener("error", errorHandler); - ws.addEventListener("open", openHandler); - }); - } - - this.close = () => { - if (ws.readyState == WebSocket.OPEN) { - return new Promise(resolve => { - closeResolver = resolve; - ws.close(); - }); - } - else { - return Promise.resolve(); - } - } - - this.getStatus = () => { - - if (connectionStatus != 2) - return Promise.resolve(null); - - const p = new Promise(resolve => { - statResponseResolvers.push(resolve); - }); - - // If this is the only awaiting stat request, then send an actual stat request. - // Otherwise simply wait for the previously sent request. - if (statResponseResolvers.length == 1) { - const msg = msgHelper.createStatusRequest(); - ws.send(msgHelper.serializeObject(msg)); - } - return p; - } - - this.sendContractInput = async (input, nonce = null, maxLclOffset = null) => { - - if (connectionStatus != 2) - return null; - - if (!maxLclOffset) - maxLclOffset = 10; - - if (!nonce) - nonce = (new Date()).getTime().toString(); - else - nonce = nonce.toString(); - - // Acquire the current lcl and add the specified offset. - const stat = await this.getStatus(); - if (!stat) - return new Promise(resolve => resolve("ledger_status_error")); - const maxLclSeqNo = stat.lclSeqNo + maxLclOffset; - - const msg = msgHelper.createContractInput(input, nonce, maxLclSeqNo); - const sigKey = msgHelper.stringifyValue(msg.sig); - const p = new Promise(resolve => { - contractInputResolvers[sigKey] = resolve; - }); - - ws.send(msgHelper.serializeObject(msg)); - return p; - } - - this.sendContractReadRequest = (request) => { - - if (connectionStatus != 2) - return; - - const msg = msgHelper.createReadRequest(request); - ws.send(msgHelper.serializeObject(msg)); - } - } - - function MessageHelper(keys, protocol) { - - this.useProtocol = (p) => { - protocol = p; - } - - this.binaryEncode = (data) => { - return protocol == protocols.json ? - uint8ArrayToHex(data) : - (Buffer.isBuffer(data) ? data : Buffer.from(data)); - } - - this.serializeObject = (obj) => { - return protocol == protocols.json ? JSON.stringify(obj) : bson.serialize(obj); - } - - this.deserializeMessage = (m) => { - return protocol == protocols.json ? JSON.parse(m) : bson.deserialize(m); - } - - this.serializeInput = (input) => { - return protocol == protocols.json ? - (isString(input) ? input : input.toString()) : - (Buffer.isBuffer(input) ? input : Buffer.from(input)); - } - - this.deserializeOutput = (content) => { - return protocol == protocols.json ? content : content.buffer; - } - - // Used for generating strings to hold values as js object keys. - this.stringifyValue = (val) => { - if (isString(val)) - return val; - else if (val instanceof Uint8Array) - return uint8ArrayToHex(val); - else if (val.buffer) // BSON binary. - return uint8ArrayToHex(new Uint8Array(val.buffer)); - else - throw "Cannot stringify signature."; - } - - // Spreads hex/binary item array. - this.spreadArrayField = (outputs) => { - return protocol == protocols.json ? outputs : outputs.map(o => o.buffer); - } - - this.createUserChallengeResponse = (userChallenge, serverChallenge, msgProtocol) => { - // For challenge response encoding Hot Pocket always uses json. - // Challenge response will specify the protocol to use for contract messages. - const sigBytes = sodium.crypto_sign_detached(userChallenge, keys.privateKey); - - return { - type: "user_challenge_response", - sig: this.binaryEncode(sigBytes), - pubkey: "ed" + this.binaryEncode(keys.publicKey), - server_challenge: serverChallenge, - protocol: msgProtocol - } - } - - this.createContractInput = (input, nonce, maxLclSeqNo) => { - - if (input.length == 0) - return null; - - const inpContainer = { - input: this.serializeInput(input), - nonce: nonce, - max_lcl_seqno: maxLclSeqNo - } - - const serlializedInpContainer = this.serializeObject(inpContainer); - const sigBytes = sodium.crypto_sign_detached(serlializedInpContainer, keys.privateKey); - - const signedInpContainer = { - type: "contract_input", - input_container: serlializedInpContainer, - sig: this.binaryEncode(sigBytes) - } - - return signedInpContainer; - } - - this.createReadRequest = (request) => { - if (request.length == 0) - return null; - - return { - type: "contract_read_request", - content: this.serializeInput(request) - } - } - - this.createStatusRequest = () => { - return { type: "stat" }; - } - } - - function hexToUint8Array(hexString) { - return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); - } - - function uint8ArrayToHex(bytes) { - return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); - } - - function isString(obj) { - return (typeof obj === "string" || obj instanceof String); - } - - function EventEmitter() { - const registrations = {}; - - this.on = (eventName, listener) => { - if (!registrations[eventName]) - registrations[eventName] = []; - registrations[eventName].push(listener); - } - - this.emit = (eventName, ...value) => { - if (registrations[eventName]) - registrations[eventName].forEach(listener => listener(...value)); - } - - this.clear = (eventName) => { - if (eventName) - delete registrations[eventName] - else - Object.keys(registrations).forEach(k => delete registrations[k]); - } - } - - // Set sodium reference. - async function initSodium() { - - if (isBrowser) { // Browser - if (!sodium) { - sodium = window.sodium || await new Promise(resolve => { - window.sodium = { - onload: async (sodiumRef) => resolve(sodiumRef) - } - }) - } - } - else { // nodejs - if (!sodium) - sodium = require('libsodium-wrappers'); - await sodium.ready; - } - - if (!sodium) - throw "Sodium reference not found. Please include sodium js lib in browser scripts."; - } - - // Set bson reference. - function initBson() { - if (bson) // If already set, do nothing. - return; - else if (isBrowser && window.BSON) // browser - bson = window.BSON; - else if (!isBrowser) // nodejs - bson = require('bson'); - - if (!bson) - throw "BSON reference not found."; - } - - // Set WebSocket reference. - function initWebSocket() { - if (WebSocket) // If already set, do nothing. - return; - else if (isBrowser && window.WebSocket) // browser - WebSocket = window.WebSocket; - else if (!isBrowser) // nodejs - WebSocket = require('ws'); - - if (!WebSocket) - throw "WebSocket reference not found."; - } - - let blake3Resolver = null; - // Set blake3 reference. - async function initBlake3() { - if (blake3) // If already set, do nothing. - return; - else if (isBrowser && window.blake3) // browser (if blake3 already loaded) - blake3 = window.blake3; - else if (isBrowser && !window.blake3) // If blake3 not yet loaded in browser, wait for it. - blake3 = await new Promise(resolve => blake3Resolver = resolve); - else if (!isBrowser) // nodejs - blake3 = require('blake3'); - - if (!blake3) - throw "Blake3 reference not found."; - } - - function setBlake3(blake3ref) { - if (blake3Resolver) { - blake3Resolver(blake3ref) - blake3Resolver = null; - } - else { - blake3 = blake3ref; - } - } - - function setLogLevel(level) { - logLevel = level; - } - - function liblog(level, msg) { - if (level >= logLevel) - console.log(msg); - } - - if (isBrowser) { - window.HotPocket = { - generateKeys, - createClient, - events, - protocols, - setBlake3, - setLogLevel - }; - } - else { - module.exports = { - generateKeys, - createClient, - events, - protocols, - setLogLevel - }; - } -})(); \ No newline at end of file diff --git a/test/metrics/metrics.js b/test/metrics/metrics.js index 863b4456..a2273d47 100644 --- a/test/metrics/metrics.js +++ b/test/metrics/metrics.js @@ -1,7 +1,7 @@ // HotPocket test client to collect metrics. // This assumes the HotPocket server we are connecting to is hosting the echo contract. -const HotPocket = require('./hp-client-lib'); +const HotPocket = require('../../examples/js_client/hp-client-lib'); let server = 'wss://localhost:8080'; if (process.argv.length == 3) server = 'wss://localhost:' + process.argv[2]; @@ -13,10 +13,11 @@ async function main() { HotPocket.setLogLevel(1); const tests = { + "Large payload": largePayload, "Single user read requests": singleUserReadRequests, "Single user Input/Output": singleUserInputOutput, "Multi user read requests": multiUserReadRequests, - "Multi user Input/Output": multiUserInputOutput + "Multi user Input/Output": multiUserInputOutput, }; for (const test in tests) { @@ -31,6 +32,8 @@ async function main() { console.log(duration + "ms"); } + + console.log("Done."); } async function createClient() { @@ -115,4 +118,20 @@ function multiUserInputOutput() { return Promise.all(tasks); } +function largePayload() { + return new Promise(async (resolve) => { + + const payload = "A".repeat(2 * 1024 * 1024); + + const hpc = await createClient(); + hpc.on(HotPocket.events.contractOutput, (response) => { + if (response.length < payload.length) + console.log("Payload length mismatch."); + hpc.close().then(() => resolve()); + }); + + await hpc.sendContractInput(payload); + }) +} + main(); \ No newline at end of file