From 10374352c62e9fc81bfde12922b8b172320df76c Mon Sep 17 00:00:00 2001 From: Ravin Perera <33562092+ravinsp@users.noreply.github.com> Date: Thu, 4 Jun 2020 13:51:53 +0530 Subject: [PATCH] hpfs integration. (#94) --- CMakeLists.txt | 45 +- README.md | 6 +- examples/echo_contract/contract.js | 4 +- examples/file_contract/contract.js | 26 + examples/hpclient/file-client.js | 153 ++ src/comm/comm_client.cpp | 151 +- src/comm/comm_server.cpp | 664 ++++----- src/conf.cpp | 10 +- src/conf.hpp | 6 +- src/cons/cons.cpp | 1426 +++++++++---------- src/cons/cons.hpp | 19 +- src/cons/ledger_handler.cpp | 2 +- src/cons/state_handler.cpp | 78 +- src/cons/state_handler.hpp | 8 +- src/fbschema/common_helpers.cpp | 166 +-- src/fbschema/common_helpers.hpp | 6 +- src/fbschema/ledger_helpers.cpp | 2 +- src/fbschema/p2pmsg_helpers.cpp | 8 +- src/fbschema/p2pmsg_helpers.hpp | 6 +- src/hpfs/h32.cpp | 74 + src/hpfs/h32.hpp | 36 + src/hpfs/hpfs.cpp | 178 +++ src/hpfs/hpfs.hpp | 18 + src/main.cpp | 41 +- src/p2p/p2p.cpp | 347 ++--- src/p2p/p2p.hpp | 10 +- src/pchheader.hpp | 1 + src/proc.cpp | 1010 +++++++------ src/proc.hpp | 20 +- src/statefs/hasher.cpp | 82 -- src/statefs/hasher.hpp | 40 - src/statefs/hashmap_builder.cpp | 505 ------- src/statefs/hashmap_builder.hpp | 38 - src/statefs/hashtree_builder.cpp | 299 ---- src/statefs/hashtree_builder.hpp | 54 - src/statefs/state_common.cpp | 61 - src/statefs/state_common.hpp | 67 - src/statefs/state_monitor/fusefs.cpp | 1380 ------------------ src/statefs/state_monitor/fusefs.hpp | 9 - src/statefs/state_monitor/state_monitor.cpp | 582 -------- src/statefs/state_monitor/state_monitor.hpp | 78 - src/statefs/state_restore.cpp | 200 --- src/statefs/state_restore.hpp | 28 - src/statefs/state_store.cpp | 357 ----- src/statefs/state_store.hpp | 35 - src/usr/usr.cpp | 319 +++-- src/util.cpp | 276 ++-- src/util.hpp | 2 + test/bin/hpfs | Bin 0 -> 174232 bytes test/bin/libb2.so.1 | Bin 0 -> 181504 bytes test/local-cluster/Dockerfile | 9 +- test/local-cluster/cluster-create.sh | 2 +- test/vm-cluster/cluster.sh | 4 +- test/vm-cluster/setup-hp.sh | 5 +- test/vm-cluster/setup-vm.sh | 4 +- 55 files changed, 2774 insertions(+), 6183 deletions(-) create mode 100644 examples/file_contract/contract.js create mode 100644 examples/hpclient/file-client.js create mode 100644 src/hpfs/h32.cpp create mode 100644 src/hpfs/h32.hpp create mode 100644 src/hpfs/hpfs.cpp create mode 100644 src/hpfs/hpfs.hpp delete mode 100644 src/statefs/hasher.cpp delete mode 100644 src/statefs/hasher.hpp delete mode 100644 src/statefs/hashmap_builder.cpp delete mode 100644 src/statefs/hashmap_builder.hpp delete mode 100644 src/statefs/hashtree_builder.cpp delete mode 100644 src/statefs/hashtree_builder.hpp delete mode 100644 src/statefs/state_common.cpp delete mode 100644 src/statefs/state_common.hpp delete mode 100644 src/statefs/state_monitor/fusefs.cpp delete mode 100644 src/statefs/state_monitor/fusefs.hpp delete mode 100644 src/statefs/state_monitor/state_monitor.cpp delete mode 100644 src/statefs/state_monitor/state_monitor.hpp delete mode 100644 src/statefs/state_restore.cpp delete mode 100644 src/statefs/state_restore.hpp delete mode 100644 src/statefs/state_store.cpp delete mode 100644 src/statefs/state_store.hpp create mode 100755 test/bin/hpfs create mode 100755 test/bin/libb2.so.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 189683a5..125bcde0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY build) link_directories(/usr/local/lib) -# We have 3 executable build outputs: appbill, hpstatemon and hpcore +# We have 2 executable build outputs: appbill and hpcore #-------appbill------- @@ -30,29 +30,6 @@ add_executable(appbill src/bill/appbill.cpp ) - -#-------hpstatemon------- - -add_executable(hpstatemon - src/statefs/state_monitor/fusefs.cpp - src/statefs/state_monitor/state_monitor.cpp - src/statefs/hasher.cpp - src/statefs/state_common.cpp -) -target_link_libraries(hpstatemon - libfuse3.so.3 - libsodium.a - libboost_system.a - libboost_thread.a - libboost_filesystem.a - libboost_stacktrace_backtrace.a - backtrace - pthread - crypto - ${CMAKE_DL_LIBS} # Needed for stacktrace support -) - - #-------hpcore------- add_executable(hpcore @@ -62,12 +39,8 @@ add_executable(hpcore src/hplog.cpp src/proc.cpp src/bill/corebill.cpp - src/statefs/hasher.cpp - src/statefs/state_common.cpp - src/statefs/hashmap_builder.cpp - src/statefs/hashtree_builder.cpp - src/statefs/state_restore.cpp - src/statefs/state_store.cpp + src/hpfs/h32.cpp + src/hpfs/hpfs.cpp src/comm/comm_session.cpp src/comm/comm_server.cpp src/comm/comm_client.cpp @@ -98,27 +71,23 @@ target_link_libraries(hpcore ${CMAKE_DL_LIBS} # Needed for stacktrace support ) add_dependencies(hpcore - hpstatemon appbill ) add_custom_command(TARGET hpcore POST_BUILD # COMMAND strip ./build/hpcore - # COMMAND strip ./build/hpstatemon # COMMAND strip ./build/appbill - COMMAND cp ./test/bin/websocketd ./build/websocketd - COMMAND cp ./test/bin/websocat ./build/websocat + COMMAND cp ./test/bin/websocketd ./test/bin/websocat ./test/bin/hpfs ./build/ ) -target_precompile_headers(hpstatemon PUBLIC src/pchheader.hpp) -target_precompile_headers(hpcore REUSE_FROM hpstatemon) +target_precompile_headers(hpcore PUBLIC src/pchheader.hpp) # Create docker image from hpcore build output with 'make docker' # Requires docker to be runnable without 'sudo' add_custom_target(docker COMMAND mkdir -p ./test/local-cluster/bin - COMMAND cp ./build/hpcore ./build/hpstatemon ./build/appbill ./test/local-cluster/bin/ - COMMAND cp ./test/bin/fusermount3 ./test/bin/libfuse3.so.3 ./test/bin/websocketd ./test/bin/websocat ./test/local-cluster/bin/ + COMMAND cp ./build/hpcore ./build/appbill ./test/local-cluster/bin/ + COMMAND cp ./test/bin/fusermount3 ./test/bin/libfuse3.so.3 ./test/bin/libb2.so.1 ./test/bin/websocketd ./test/bin/websocat ./test/bin/hpfs ./test/local-cluster/bin/ COMMAND docker build -t hpcore:latest ./test/local-cluster ) set_target_properties(docker PROPERTIES EXCLUDE_FROM_ALL TRUE) diff --git a/README.md b/README.md index 033fe6a8..833058be 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A C++ version of hotpocket designed for production envrionments, original prototype here: https://github.com/codetsunami/hotpocket -[Hot Pocket Wiki](https://github.com/HotPocketDev/core/wiki/Hot-Pocket-Wiki) +[Hot Pocket Wiki](https://github.com/HotPocketDev/core/wiki) ## Libraries * Crypto - Libsodium https://github.com/jedisct1/libsodium @@ -81,7 +81,7 @@ This will update your linker library cache and avoid potential issues when runni 1. Run `make` (Hot Pocket binary will be created as `./build/hpcore`) 1. Refer to [Running Hot Pocket](https://github.com/HotPocketDev/core/wiki/Running-Hot-Pocket) in the Wiki. -Refer to [Hot Pocket Wiki](https://github.com/HotPocketDev/core/wiki/Hot-Pocket-Wiki) for more info. +Refer to [Hot Pocket Wiki](https://github.com/HotPocketDev/core/wiki) for more info. ## Code structure Code is divided into subsystems via namespaces. @@ -102,4 +102,4 @@ Code is divided into subsystems via namespaces. **util::** Contains shared data structures/helper functions used by multiple subsystems. -**statefs::** Fuse-based state filesystem monitoring and contract state maintenance subsystem. \ No newline at end of file +**hpfs::** [hpfs](https://github.com/HotPocketDev/hpfs) state management client helpers. \ No newline at end of file diff --git a/examples/echo_contract/contract.js b/examples/echo_contract/contract.js index a2154485..65c2dfea 100644 --- a/examples/echo_contract/contract.js +++ b/examples/echo_contract/contract.js @@ -9,7 +9,7 @@ const fs = require('fs') let hpargs = JSON.parse(fs.readFileSync(0, 'utf8')); // We just save execution args as an example state file change. -fs.appendFileSync("state/exects.txt", "ts:" + hpargs.ts + "\n"); +fs.appendFileSync("exects.txt", "ts:" + hpargs.ts + "\n"); Object.keys(hpargs.usrfd).forEach(function (key, index) { let userfds = hpargs.usrfd[key]; @@ -17,7 +17,7 @@ Object.keys(hpargs.usrfd).forEach(function (key, index) { if (userinput.length > 0) { // Append user input to a state file. - fs.appendFileSync("state/userinputs.txt", userinput + "\n"); + fs.appendFileSync("userinputs.txt", userinput + "\n"); fs.writeSync(userfds[1], "Echoing: " + userinput); } }); diff --git a/examples/file_contract/contract.js b/examples/file_contract/contract.js new file mode 100644 index 00000000..3c212817 --- /dev/null +++ b/examples/file_contract/contract.js @@ -0,0 +1,26 @@ +process.on('uncaughtException', (err) => { + console.error('There was an uncaught error', err) +}) +const fs = require('fs') + +//console.log("===File contract started==="); +//console.log("Contract args received from hp: " + input); + +let hpargs = JSON.parse(fs.readFileSync(0, 'utf8')); + +// We just save execution args as an example state file change. +fs.appendFileSync("exects.txt", "ts:" + hpargs.ts + "\n"); + +Object.keys(hpargs.usrfd).forEach(function (key, index) { + let userfds = hpargs.usrfd[key]; + let fileContent = fs.readFileSync(userfds[0]); + + if (fileContent.length > 0) { + // Save the content into a new file. + var fileName = new Date().getTime().toString(); + fs.writeFileSync(fileName, fileContent); + fs.writeSync(userfds[1], "Saved file (len: " + fileContent.length / 1024 + " KB)"); + } +}); + +//console.log("===File contract ended==="); \ No newline at end of file diff --git a/examples/hpclient/file-client.js b/examples/hpclient/file-client.js new file mode 100644 index 00000000..7bf12c64 --- /dev/null +++ b/examples/hpclient/file-client.js @@ -0,0 +1,153 @@ +const fs = require('fs') +const ws_api = require('ws'); +const sodium = require('libsodium-wrappers') +const readline = require('readline') + +// sodium has a trigger when it's ready, we will wait and execute from there +sodium.ready.then(main).catch((e) => { console.log(e) }) + + +function main() { + + var keys = sodium.crypto_sign_keypair() + + + // check for client keys + if (!fs.existsSync('.hp_client_keys')) { + keys.privateKey = sodium.to_hex(keys.privateKey) + keys.publicKey = sodium.to_hex(keys.publicKey) + fs.writeFileSync('.hp_client_keys', JSON.stringify(keys)) + } else { + keys = JSON.parse(fs.readFileSync('.hp_client_keys')) + keys.privateKey = Uint8Array.from(Buffer.from(keys.privateKey, 'hex')) + keys.publicKey = Uint8Array.from(Buffer.from(keys.publicKey, 'hex')) + } + + + var server = 'wss://localhost:8080' + + if (process.argv.length == 3) server = 'wss://localhost:' + process.argv[2] + + if (process.argv.length == 4) server = 'wss://' + process.argv[2] + ':' + process.argv[3] + + var ws = new ws_api(server, { + rejectUnauthorized: false + }) + + /* anatomy of a public challenge + { + version: '0.1', + type: 'public_challenge', + challenge: '' + } + */ + + + // if the console ctrl + c's us we should close ws gracefully + process.once('SIGINT', function (code) { + console.log('SIGINT received...'); + ws.close() + }); + + function create_input_container(inp) { + + let hexInp = inp.toString('hex'); + console.log("hex " + hexInp.length); + + let inp_container = { + nonce: (new Date()).getTime().toString(), + input: hexInp, + max_ledger_seqno: 9999999 + } + let inp_container_bytes = JSON.stringify(inp_container); + let sig_bytes = sodium.crypto_sign_detached(inp_container_bytes, keys.privateKey); + + let signed_inp_container = { + type: "contract_input", + content: inp_container_bytes.toString('hex'), + sig: Buffer.from(sig_bytes).toString('hex') + } + + return JSON.stringify(signed_inp_container); + } + + function create_status_request() { + let statreq = { type: 'stat' } + return JSON.stringify(statreq); + } + + function handle_public_challange(m) { + let pkhex = 'ed' + Buffer.from(keys.publicKey).toString('hex'); + console.log('My public key is: ' + pkhex); + + // sign the challenge and send back the response + var sigbytes = sodium.crypto_sign_detached(m.challenge, keys.privateKey); + var response = { + type: 'challenge_resp', + challenge: m.challenge, + sig: Buffer.from(sigbytes).toString('hex'), + pubkey: pkhex + } + + ws.send(JSON.stringify(response)) + + // start listening for stdin + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + console.log("Ready to accept inputs.") + + // Capture user input from the console. + var input_pump = () => { + rl.question('', (inp) => { + + let msgtosend = ""; + + if (inp == "stat") + msgtosend = create_status_request(); + else { + var fileContent = fs.readFileSync(inp); + msgtosend = create_input_container(fileContent); + + console.log("Sending file (len: " + fileContent.length / 1024 + " KB)"); + } + + ws.send(msgtosend) + + input_pump() + }) + } + input_pump(); + } + + ws.on('message', (data) => { + + try { + m = JSON.parse(data) + } catch (e) { + console.log("Exception: " + data); + return + } + + if (m.type == 'public_challenge') { + handle_public_challange(m); + } + else if (m.type == 'contract_output') { + console.log("Contract says: " + Buffer.from(m.content, 'hex').toString()); + } + else if (m.type == 'request_status_result') { + if (m.status != "accepted") + console.log("Input status: " + m.status); + } + else { + console.log(m); + } + + }); + + ws.on('close', () => { + console.log('Server disconnected.'); + }); +} diff --git a/src/comm/comm_client.cpp b/src/comm/comm_client.cpp index 78227e5b..658495d8 100644 --- a/src/comm/comm_client.cpp +++ b/src/comm/comm_client.cpp @@ -7,90 +7,89 @@ namespace comm { -int comm_client::start(std::string_view host, const uint16_t port, const uint64_t (&metric_thresholds)[4], const uint64_t max_msg_size) -{ - return start_websocat_process(host, port); -} - -void comm_client::stop() -{ - if (read_fd > 0) - close(read_fd); - if (write_fd > 0) - close(write_fd); - - if (websocat_pid > 0) - kill(websocat_pid, SIGINT); // Kill websocat. -} - -int comm_client::start_websocat_process(std::string_view host, const uint16_t port) -{ - // setup pipe I/O - if (pipe(read_pipe) < 0 || pipe(write_pipe) < 0) + int comm_client::start(std::string_view host, const uint16_t port, const uint64_t (&metric_thresholds)[4], const uint64_t max_msg_size) { - LOG_ERR << errno << ": websocat pipe creation failed."; - return -1; + return start_websocat_process(host, port); } - const pid_t pid = fork(); - - if (pid > 0) + void comm_client::stop() { - // HotPocket process. - websocat_pid = pid; - - read_fd = read_pipe[0]; - write_fd = write_pipe[1]; - - // Close unused fds by us. - close(write_pipe[0]); - close(read_pipe[1]); - - // Wait for some time and check if websocat is still running properly. - util::sleep(20); - int pid_status; - waitpid(websocat_pid, &pid_status, WNOHANG); - if (WIFEXITED(pid_status)) // This means websocat has exited. - { + if (read_fd > 0) close(read_fd); + if (write_fd > 0) close(write_fd); + + if (websocat_pid > 0) + util::kill_process(websocat_pid, false); // Kill websocat. + } + + int comm_client::start_websocat_process(std::string_view host, const uint16_t port) + { + // setup pipe I/O + if (pipe(read_pipe) < 0 || pipe(write_pipe) < 0) + { + LOG_ERR << errno << ": websocat pipe creation failed."; return -1; } + + const pid_t pid = fork(); + + if (pid > 0) + { + // HotPocket process. + + read_fd = read_pipe[0]; + write_fd = write_pipe[1]; + + // Close unused fds by us. + close(write_pipe[0]); + close(read_pipe[1]); + + // Wait for some time and check if websocat is still running properly. + util::sleep(20); + if (kill(pid, 0) == -1) + { + close(read_fd); + close(write_fd); + return -1; + } + + websocat_pid = pid; + } + else if (pid == 0) + { + // Websocat process. + close(write_pipe[1]); //parent write + close(read_pipe[0]); //parent read + + dup2(write_pipe[0], STDIN_FILENO); //child read + close(write_pipe[0]); + dup2(read_pipe[1], STDOUT_FILENO); //child write + close(read_pipe[1]); + + std::string url = std::string("wss://").append(host).append(":").append(std::to_string(port)); + + // Fill process args. + char *execv_args[] = { + conf::ctx.websocat_exe_path.data(), + url.data(), + (char *)"-k", // Accept invalid certificates + (char *)"-b", // Binary mode + (char *)"-E", // Close on EOF + (char *)"-q", // Quiet mode + NULL}; + + const int ret = execv(execv_args[0], execv_args); + LOG_ERR << errno << ": websocat process execv failed."; + exit(1); + } + else + { + LOG_ERR << "fork() failed when starting websocat process."; + return -1; + } + + return 0; } - else if (pid == 0) - { - // Websocat process. - close(write_pipe[1]); //parent write - close(read_pipe[0]); //parent read - - dup2(write_pipe[0], STDIN_FILENO); //child read - close(write_pipe[0]); - dup2(read_pipe[1], STDOUT_FILENO); //child write - close(read_pipe[1]); - - std::string url = std::string("wss://").append(host).append(":").append(std::to_string(port)); - - // Fill process args. - char *execv_args[] = { - conf::ctx.websocat_exe_path.data(), - url.data(), - (char *)"-k", // Accept invalid certificates - (char *)"-b", // Binary mode - (char *)"-E", // Close on EOF - (char *)"-q", // Quiet mode - NULL}; - - const int ret = execv(execv_args[0], execv_args); - LOG_ERR << errno << ": websocat process execv failed."; - exit(1); - } - else - { - LOG_ERR << "fork() failed when starting websocat process."; - return -1; - } - - return 0; -} } // namespace comm diff --git a/src/comm/comm_server.cpp b/src/comm/comm_server.cpp index d1f64aa7..5cf35a9d 100644 --- a/src/comm/comm_server.cpp +++ b/src/comm/comm_server.cpp @@ -12,391 +12,397 @@ namespace comm { -int comm_server::start( - const uint16_t port, const char *domain_socket_name, const SESSION_TYPE session_type, const bool is_binary, const bool use_size_header, - const uint64_t (&metric_thresholds)[4], const std::set &req_known_remotes, const uint64_t max_msg_size) -{ - int accept_fd = open_domain_socket(domain_socket_name); - if (accept_fd > 0) + int comm_server::start( + const uint16_t port, const char *domain_socket_name, const SESSION_TYPE session_type, const bool is_binary, const bool use_size_header, + const uint64_t (&metric_thresholds)[4], const std::set &req_known_remotes, const uint64_t max_msg_size) { - watchdog_thread = std::thread( - &comm_server::connection_watchdog, this, accept_fd, session_type, is_binary, - std::ref(metric_thresholds), req_known_remotes, max_msg_size); - return start_websocketd_process(port, domain_socket_name, is_binary, use_size_header); - } - - return -1; -} - -int comm_server::open_domain_socket(const char *domain_socket_name) -{ - int fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd == -1) - { - LOG_ERR << errno << ": Domain socket open error"; - return -1; - } - - sockaddr_un addr; - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - - strncpy(addr.sun_path, domain_socket_name, sizeof(addr.sun_path) - 1); - unlink(domain_socket_name); - - if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) - { - LOG_ERR << errno << ": Domain socket bind error"; - return -1; - } - - if (listen(fd, 5) == -1) - { - LOG_ERR << errno << ": Domain socket listen error"; - return -1; - } - - // Set non-blocking behaviour. - // We do this so the accept() call returns immediately without blocking the listening thread. - int flags = fcntl(fd, F_GETFL); - fcntl(fd, F_SETFL, flags | O_NONBLOCK); - - return fd; // This is the fd we should call accept() on. -} - -void comm_server::connection_watchdog( - const int accept_fd, const SESSION_TYPE session_type, const bool is_binary, - const uint64_t (&metric_thresholds)[4], const std::set &req_known_remotes, const uint64_t max_msg_size) -{ - util::mask_signal(); - - // Map with read fd to connected session mappings. - std::unordered_map sessions; - // Map with read fd to connected comm client mappings. - std::unordered_map outbound_clients; - - // Counter to track when to initiate outbound client connections. - int16_t loop_counter = -1; - - while (true) - { - if (should_stop_listening) - break; - - // Prepare poll fd list. - const size_t fd_count = sessions.size() + 1; //+1 for the inclusion of accept_fd - pollfd pollfds[fd_count]; - if (poll_fds(pollfds, accept_fd, sessions) == -1) + int accept_fd = open_domain_socket(domain_socket_name); + if (accept_fd > 0) { - util::sleep(10); - continue; + watchdog_thread = std::thread( + &comm_server::connection_watchdog, this, accept_fd, session_type, is_binary, + std::ref(metric_thresholds), req_known_remotes, max_msg_size); + return start_websocketd_process(port, domain_socket_name, is_binary, use_size_header); } - util::sleep(10); + return -1; + } - // Accept any new incoming connection if available. - check_for_new_connection(sessions, accept_fd, session_type, is_binary, metric_thresholds); - - if (!req_known_remotes.empty()) + int comm_server::open_domain_socket(const char *domain_socket_name) + { + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { - // Restore any missing outbound connections every 500 iterations (including the first iteration). - if (loop_counter == -1 || loop_counter == 500) + LOG_ERR << errno << ": Domain socket open error"; + return -1; + } + + sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + + strncpy(addr.sun_path, domain_socket_name, sizeof(addr.sun_path) - 1); + unlink(domain_socket_name); + + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) + { + LOG_ERR << errno << ": Domain socket bind error"; + return -1; + } + + if (listen(fd, 5) == -1) + { + LOG_ERR << errno << ": Domain socket listen error"; + return -1; + } + + // Set non-blocking behaviour. + // We do this so the accept() call returns immediately without blocking the listening thread. + int flags = fcntl(fd, F_GETFL); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + return fd; // This is the fd we should call accept() on. + } + + void comm_server::connection_watchdog( + const int accept_fd, const SESSION_TYPE session_type, const bool is_binary, + const uint64_t (&metric_thresholds)[4], const std::set &req_known_remotes, const uint64_t max_msg_size) + { + util::mask_signal(); + + // Map with read fd to connected session mappings. + std::unordered_map sessions; + // Map with read fd to connected comm client mappings. + std::unordered_map outbound_clients; + + // Counter to track when to initiate outbound client connections. + int16_t loop_counter = -1; + + while (true) + { + if (should_stop_listening) + break; + + // Prepare poll fd list. + const size_t fd_count = sessions.size() + 1; //+1 for the inclusion of accept_fd + pollfd pollfds[fd_count]; + if (poll_fds(pollfds, accept_fd, sessions) == -1) { - loop_counter = 0; - maintain_known_connections(sessions, outbound_clients, req_known_remotes, session_type, is_binary, max_msg_size, metric_thresholds); + util::sleep(10); + continue; } - loop_counter++; - } - const size_t sessions_count = sessions.size(); + util::sleep(10); - // Loop through all fds and read any data. - for (size_t i = 1; i <= sessions_count; i++) - { - const short result = pollfds[i].revents; - const int fd = pollfds[i].fd; + // Accept any new incoming connection if available. + check_for_new_connection(sessions, accept_fd, session_type, is_binary, metric_thresholds); - const auto iter = sessions.find(fd); - if (iter != sessions.end()) + if (!req_known_remotes.empty()) { - comm_session &session = iter->second; - bool should_disconnect = (session.state == SESSION_STATE::CLOSED); - - if (!should_disconnect) + // Restore any missing outbound connections every 500 iterations (including the first iteration). + if (loop_counter == -1 || loop_counter == 500) { - if (result & POLLIN) - should_disconnect = (session.attempt_read(max_msg_size) == -1); - - if (result & (POLLERR | POLLHUP | POLLRDHUP | POLLNVAL)) - should_disconnect = true; + loop_counter = 0; + maintain_known_connections(sessions, outbound_clients, req_known_remotes, session_type, is_binary, max_msg_size, metric_thresholds); } + loop_counter++; + } - if (should_disconnect) + const size_t sessions_count = sessions.size(); + + // Loop through all fds and read any data. + for (size_t i = 1; i <= sessions_count; i++) + { + const short result = pollfds[i].revents; + const int fd = pollfds[i].fd; + + const auto iter = sessions.find(fd); + if (iter != sessions.end()) { - // If this is an outbound session, cleanup the corresponding comm client as well. - if (!session.is_inbound) + comm_session &session = iter->second; + bool should_disconnect = (session.state == SESSION_STATE::CLOSED); + + if (!should_disconnect) { - const auto client_itr = outbound_clients.find(fd); - client_itr->second.stop(); - outbound_clients.erase(client_itr); + if (result & POLLIN) + should_disconnect = (session.attempt_read(max_msg_size) == -1); + + if (result & (POLLERR | POLLHUP | POLLRDHUP | POLLNVAL)) + should_disconnect = true; } - session.close(); - sessions.erase(fd); + if (should_disconnect) + { + // If this is an outbound session, cleanup the corresponding comm client as well. + if (!session.is_inbound) + { + const auto client_itr = outbound_clients.find(fd); + client_itr->second.stop(); + outbound_clients.erase(client_itr); + } + + session.close(); + sessions.erase(fd); + } + } + } + } + + // If we reach this point that means we are shutting down. + + // Close all sessions and clients + for (auto &[fd, session] : sessions) + session.close(false); + for (auto &[fd, client] : outbound_clients) + client.stop(); + + LOG_INFO << (session_type == SESSION_TYPE::USER ? "User" : "Peer") << " listener stopped."; + } + + int comm_server::poll_fds(pollfd *pollfds, const int accept_fd, const std::unordered_map &sessions) + { + const short poll_events = POLLIN | POLLRDHUP; + pollfds[0].fd = accept_fd; + + auto iter = sessions.begin(); + for (size_t i = 1; i <= sessions.size(); i++) + { + pollfds[i].fd = iter->first; + pollfds[i].events = poll_events; + iter++; + } + + if (poll(pollfds, sessions.size() + 1, 10) == -1) //10ms timeout + { + LOG_ERR << errno << ": Poll failed."; + return -1; + } + + return 0; + } + + void comm_server::check_for_new_connection( + std::unordered_map &sessions, const int accept_fd, + const SESSION_TYPE session_type, const bool is_binary, const uint64_t (&metric_thresholds)[4]) + { + // Accept new client connection (if available) + int client_fd = accept(accept_fd, NULL, NULL); + if (client_fd == -1 && errno != EAGAIN) + { + LOG_ERR << errno << ": Domain socket accept error"; + } + else if (client_fd > 0) + { + // New client connected. + const std::string ip = get_cgi_ip(client_fd); + + if (corebill::is_banned(ip)) + { + LOG_DBG << "Dropping connection for banned host " << ip; + close(client_fd); + } + else + { + comm_session session(ip, client_fd, client_fd, session_type, is_binary, true, metric_thresholds); + if (session.on_connect() == 0) + sessions.try_emplace(client_fd, std::move(session)); + } + } + } + + void comm_server::maintain_known_connections( + std::unordered_map &sessions, std::unordered_map &outbound_clients, + const std::set &req_known_remotes, const SESSION_TYPE session_type, const bool is_binary, + const uint64_t max_msg_size, const uint64_t (&metric_thresholds)[4]) + { + // Find already connected known remote parties list + std::set known_remotes; + for (const auto &[fd, session] : sessions) + { + if (session.state != SESSION_STATE::CLOSED && !session.known_ipport.first.empty()) + known_remotes.emplace(session.known_ipport); + } + + for (const auto &ipport : req_known_remotes) + { + if (should_stop_listening) + break; + + // Check if we are already connected to this remote party. + if (known_remotes.find(ipport) != known_remotes.end()) + continue; + + std::string_view host = ipport.first; + const uint16_t port = ipport.second; + LOG_DBG << "Trying to connect " << host << ":" << std::to_string(port); + + comm::comm_client client; + if (client.start(host, port, metric_thresholds, conf::cfg.peermaxsize) == -1) + { + LOG_ERR << "Outbound connection attempt failed"; + } + else + { + comm::comm_session session(host, client.read_fd, client.write_fd, comm::SESSION_TYPE::PEER, is_binary, false, metric_thresholds); + session.known_ipport = ipport; + if (session.on_connect() == 0) + { + sessions.try_emplace(client.read_fd, std::move(session)); + outbound_clients.emplace(client.read_fd, std::move(client)); + known_remotes.emplace(ipport); } } } } - // If we reach this point that means we are shutting down. - - // Close all sessions and clients - for (auto &[fd, session] : sessions) - session.close(false); - for (auto &[fd, client] : outbound_clients) - client.stop(); - - LOG_INFO << (session_type == SESSION_TYPE::USER ? "User" : "Peer") << " listener stopped."; -} - -int comm_server::poll_fds(pollfd *pollfds, const int accept_fd, const std::unordered_map &sessions) -{ - const short poll_events = POLLIN | POLLRDHUP; - pollfds[0].fd = accept_fd; - - auto iter = sessions.begin(); - for (size_t i = 1; i <= sessions.size(); i++) + int comm_server::start_websocketd_process(const uint16_t port, const char *domain_socket_name, const bool is_binary, const bool use_size_header) { - pollfds[i].fd = iter->first; - pollfds[i].events = poll_events; - iter++; - } + // setup pipe for firewall + int firewall_pipe[2]; // parent to child pipe - if (poll(pollfds, sessions.size() + 1, 10) == -1) //10ms timeout - { - LOG_ERR << errno << ": Poll failed."; - return -1; - } - - return 0; -} - -void comm_server::check_for_new_connection( - std::unordered_map &sessions, const int accept_fd, - const SESSION_TYPE session_type, const bool is_binary, const uint64_t (&metric_thresholds)[4]) -{ - // Accept new client connection (if available) - int client_fd = accept(accept_fd, NULL, NULL); - if (client_fd == -1 && errno != EAGAIN) - { - LOG_ERR << errno << ": Domain socket accept error"; - } - else if (client_fd > 0) - { - // New client connected. - const std::string ip = get_cgi_ip(client_fd); - - if (corebill::is_banned(ip)) + if (pipe(firewall_pipe)) { - LOG_DBG << "Dropping connection for banned host " << ip; - close(client_fd); + LOG_ERR << errno << ": pipe() call failed for firewall"; } else { - comm_session session(ip, client_fd, client_fd, session_type, is_binary, true, metric_thresholds); - if (session.on_connect() == 0) - sessions.try_emplace(client_fd, std::move(session)); + firewall_out = firewall_pipe[1]; } - } -} -void comm_server::maintain_known_connections( - std::unordered_map &sessions, std::unordered_map &outbound_clients, - const std::set &req_known_remotes, const SESSION_TYPE session_type, const bool is_binary, - const uint64_t max_msg_size, const uint64_t (&metric_thresholds)[4]) -{ - // Find already connected known remote parties list - std::set known_remotes; - for (const auto &[fd, session] : sessions) - { - if (session.state != SESSION_STATE::CLOSED && !session.known_ipport.first.empty()) - known_remotes.emplace(session.known_ipport); - } + const pid_t pid = fork(); - for (const auto &ipport : req_known_remotes) - { - if (should_stop_listening) - break; - - // Check if we are already connected to this remote party. - if (known_remotes.find(ipport) != known_remotes.end()) - continue; - - std::string_view host = ipport.first; - const uint16_t port = ipport.second; - LOG_DBG << "Trying to connect " << host << ":" << std::to_string(port); - - comm::comm_client client; - if (client.start(host, port, metric_thresholds, conf::cfg.peermaxsize) == -1) + if (pid > 0) { - LOG_ERR << "Outbound connection attempt failed"; + // HotPocket process. + + // Close the child reading end of the pipe in the parent + if (firewall_out > 0) + close(firewall_pipe[0]); + + // Wait for some time and check if websocketd is still running properly. + util::sleep(20); + if (kill(pid, 0) == -1) + return -1; + + websocketd_pid = pid; } - else + else if (pid == 0) { - comm::comm_session session(host, client.read_fd, client.write_fd, comm::SESSION_TYPE::PEER, is_binary, false, metric_thresholds); - session.known_ipport = ipport; - if (session.on_connect() == 0) + // Websocketd process. + // We are using websocketd forked repo: https://github.com/codetsunami/websocketd + + if (firewall_out > 0) { - sessions.try_emplace(client.read_fd, std::move(session)); - outbound_clients.emplace(client.read_fd, std::move(client)); - known_remotes.emplace(ipport); + // Close parent writing end of the pipe in the child + close(firewall_pipe[1]); + // Override stdin in the child's file table + dup2(firewall_pipe[0], 0); } + + // Override stdout in the child's file table with /dev/null + // int null_fd = open("/dev/null", O_WRONLY); + // if (null_fd) + // dup2(null_fd, 1); + + // Fill process args. + char *execv_args[] = { + conf::ctx.websocketd_exe_path.data(), + (char *)"--port", + std::to_string(port).data(), + (char *)"--ssl", + (char *)"--sslcert", + conf::ctx.tls_cert_file.data(), + (char *)"--sslkey", + conf::ctx.tls_key_file.data(), + (char *)(is_binary ? "--binary=true" : "--binary=false"), + (char *)(use_size_header ? "--sizeheader=true" : "--sizeheader=false"), + (char *)"--loglevel=error", + (char *)"nc", // netcat (OpenBSD) is used for domain socket redirection. + (char *)"-U", // Use UNIX domain socket + (char *)domain_socket_name, + NULL}; + + const int ret = execv(execv_args[0], execv_args); + LOG_ERR << errno << ": websocketd process execv failed."; + exit(1); } - } -} - -int comm_server::start_websocketd_process(const uint16_t port, const char *domain_socket_name, const bool is_binary, const bool use_size_header) -{ - // setup pipe for firewall - int firewall_pipe[2]; // parent to child pipe - - if (pipe(firewall_pipe)) - { - LOG_ERR << errno << ": pipe() call failed for firewall"; - } - else - { - firewall_out = firewall_pipe[1]; - } - - const pid_t pid = fork(); - - if (pid > 0) - { - // HotPocket process. - websocketd_pid = pid; - - // Close the child reading end of the pipe in the parent - if (firewall_out > 0) - close(firewall_pipe[0]); - } - else if (pid == 0) - { - // Websocketd process. - // We are using websocketd forked repo: https://github.com/codetsunami/websocketd - - if (firewall_out > 0) + else { - // Close parent writing end of the pipe in the child - close(firewall_pipe[1]); - // Override stdin in the child's file table - dup2(firewall_pipe[0], 0); + LOG_ERR << "fork() failed when starting websocketd process."; + return -1; } - // Override stdout in the child's file table with /dev/null - // int null_fd = open("/dev/null", O_WRONLY); - // if (null_fd) - // dup2(null_fd, 1); - - // Fill process args. - char *execv_args[] = { - conf::ctx.websocketd_exe_path.data(), - (char *)"--port", - std::to_string(port).data(), - (char *)"--ssl", - (char *)"--sslcert", - conf::ctx.tls_cert_file.data(), - (char *)"--sslkey", - conf::ctx.tls_key_file.data(), - (char *)(is_binary ? "--binary=true" : "--binary=false"), - (char *)(use_size_header? "--sizeheader=true" : "--sizeheader=false"), - (char *)"--loglevel=error", - (char *)"nc", // netcat (OpenBSD) is used for domain socket redirection. - (char *)"-U", // Use UNIX domain socket - (char *)domain_socket_name, - NULL}; - - const int ret = execv(execv_args[0], execv_args); - LOG_ERR << errno << ": websocketd process execv failed."; - exit(1); + return 0; } - else + + void comm_server::firewall_ban(std::string_view ip, const bool unban) { - LOG_ERR << "fork() failed when starting websocketd process."; - return -1; + if (firewall_out < 0) + return; + + iovec iov[]{ + {(void *)(unban ? "r" : "a"), 1}, + {(void *)ip.data(), ip.length()}}; + writev(firewall_out, iov, 2); } - return 0; -} - -void comm_server::firewall_ban(std::string_view ip, const bool unban) -{ - if (firewall_out < 0) - return; - - iovec iov[]{ - {(void *)(unban ? "r" : "a"), 1}, - {(void *)ip.data(), ip.length()}}; - writev(firewall_out, iov, 2); -} - -/** + /** * If the fd supplied was produced by accept()ing unix domain socket connection * the process at the other end is inspected for CGI environment variables * and the REMOTE_ADDR variable is returned as std::string, otherwise empty string */ -std::string comm_server::get_cgi_ip(const int fd) -{ - socklen_t length; - ucred uc; - length = sizeof(struct ucred); - - // Ask the operating system for information about the other process - if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &uc, &length) == -1) + std::string comm_server::get_cgi_ip(const int fd) { - LOG_ERR << errno << ": Could not retrieve PID from unix domain socket"; - return ""; - } + socklen_t length; + ucred uc; + length = sizeof(struct ucred); - // Open /proc//environ for that process - std::stringstream ss; - ss << "/proc/" << uc.pid << "/environ"; - std::string fn = ss.str(); - - const int envfd = open(fn.c_str(), O_RDONLY); - if (!envfd) - { - LOG_ERR << errno << ": Could not open environ block for process on other end of unix domain socket PID=" << uc.pid; - return ""; - } - - // Read environ block - char envblock[0x7fff]; - const ssize_t bytes_read = read(envfd, envblock, 0x7fff); //0x7fff bytes is an operating system size limit for this block - close(envfd); - - // Find the REMOTE_ADDR entry. Envrion block delimited by \0 - for (char *upto = envblock, *last = envblock; upto - envblock < bytes_read; ++upto) - { - if (*upto == '\0') + // Ask the operating system for information about the other process + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &uc, &length) == -1) { - if (upto - last > 12 && strncmp(last, "REMOTE_ADDR=", 12) == 0) - return std::string((const char *)(last + 12)); - last = upto + 1; + LOG_ERR << errno << ": Could not retrieve PID from unix domain socket"; + return ""; } + + // Open /proc//environ for that process + std::stringstream ss; + ss << "/proc/" << uc.pid << "/environ"; + std::string fn = ss.str(); + + const int envfd = open(fn.c_str(), O_RDONLY); + if (!envfd) + { + LOG_ERR << errno << ": Could not open environ block for process on other end of unix domain socket PID=" << uc.pid; + return ""; + } + + // Read environ block + char envblock[0x7fff]; + const ssize_t bytes_read = read(envfd, envblock, 0x7fff); //0x7fff bytes is an operating system size limit for this block + close(envfd); + + // Find the REMOTE_ADDR entry. Envrion block delimited by \0 + for (char *upto = envblock, *last = envblock; upto - envblock < bytes_read; ++upto) + { + if (*upto == '\0') + { + if (upto - last > 12 && strncmp(last, "REMOTE_ADDR=", 12) == 0) + return std::string((const char *)(last + 12)); + last = upto + 1; + } + } + + LOG_ERR << "Could not find REMOTE_ADDR variable in /proc/" << uc.pid << "/environ"; + return ""; } - LOG_ERR << "Could not find REMOTE_ADDR variable in /proc/" << uc.pid << "/environ"; - return ""; -} + void comm_server::stop() + { + should_stop_listening = true; + watchdog_thread.join(); -void comm_server::stop() -{ - should_stop_listening = true; - watchdog_thread.join(); - - if (websocketd_pid > 0) - kill(websocketd_pid, SIGINT); // Kill websocketd. -} + if (websocketd_pid > 0) + util::kill_process(websocketd_pid, false); // Kill websocketd. + } } // namespace comm diff --git a/src/conf.cpp b/src/conf.cpp index b3a9b66d..7a9633f6 100644 --- a/src/conf.cpp +++ b/src/conf.cpp @@ -79,8 +79,7 @@ int create_contract() boost::filesystem::create_directories(ctx.config_dir); boost::filesystem::create_directories(ctx.hist_dir); - boost::filesystem::create_directories(ctx.state_dir); - boost::filesystem::create_directories(ctx.state_hist_dir); + boost::filesystem::create_directories(ctx.state_rw_dir); //Create config file with default settings. @@ -136,9 +135,9 @@ void set_contract_dir_paths(std::string exepath, std::string basedir) basedir = util::realpath(basedir); ctx.exe_dir = boost::filesystem::path(util::realpath(exepath)).parent_path().string(); - ctx.statemon_exe_path = ctx.exe_dir + "/" + "hpstatemon"; ctx.websocketd_exe_path = ctx.exe_dir + "/" + "websocketd"; ctx.websocat_exe_path = ctx.exe_dir + "/" + "websocat"; + ctx.hpfs_exe_path = ctx.exe_dir + "/" + "hpfs"; ctx.contract_dir = basedir; ctx.config_dir = basedir + "/cfg"; @@ -147,7 +146,7 @@ void set_contract_dir_paths(std::string exepath, std::string basedir) ctx.tls_cert_file = ctx.config_dir + "/tlscert.pem"; ctx.hist_dir = basedir + "/hist"; ctx.state_dir = basedir + "/state"; - ctx.state_hist_dir = basedir + "/statehist"; + ctx.state_rw_dir = ctx.state_dir + "/rw"; ctx.log_dir = basedir + "/log"; } @@ -507,12 +506,11 @@ int validate_config() */ int validate_contract_dir_paths() { - const std::string paths[7] = { + const std::string paths[6] = { ctx.contract_dir, ctx.config_file, ctx.hist_dir, ctx.state_dir, - ctx.state_hist_dir, ctx.tls_key_file, ctx.tls_cert_file}; diff --git a/src/conf.hpp b/src/conf.hpp index e6fe21b2..84066c6f 100644 --- a/src/conf.hpp +++ b/src/conf.hpp @@ -27,14 +27,14 @@ struct contract_ctx { std::string command; // The CLI command issued to launch HotPocket std::string exe_dir; // Hot Pocket executable dir. - std::string statemon_exe_path; // State monitor executable file path. std::string websocketd_exe_path; // Websocketd executable file path. std::string websocat_exe_path; // Websocketd executable file path. + std::string hpfs_exe_path; // hpfs executable file path. std::string contract_dir; // Contract base directory full path std::string hist_dir; // Contract ledger history dir full path - std::string state_dir; // Contract executing state dir full path (This is the fuse mount point) - std::string state_hist_dir; // Contract state history dir full path + std::string state_dir; // Contract state maintenence path (hpfs path) + std::string state_rw_dir; // Contract executation read/write state path. std::string log_dir; // Contract log dir full path std::string config_dir; // Contract config dir full path std::string config_file; // Full path to the contract config file diff --git a/src/cons/cons.cpp b/src/cons/cons.cpp index e9edae0a..eb8ea0d3 100644 --- a/src/cons/cons.cpp +++ b/src/cons/cons.cpp @@ -10,11 +10,11 @@ #include "../hplog.hpp" #include "../crypto.hpp" #include "../proc.hpp" +#include "../hpfs/h32.hpp" +#include "../hpfs/hpfs.hpp" #include "ledger_handler.hpp" #include "state_handler.hpp" #include "cons.hpp" -#include "../statefs/state_common.hpp" -#include "../statefs/state_store.hpp" namespace p2pmsg = fbschema::p2pmsg; namespace jusrmsg = jsonschema::usrmsg; @@ -22,939 +22,907 @@ namespace jusrmsg = jsonschema::usrmsg; namespace cons { -/** + /** * Voting thresholds for consensus stages. */ -constexpr float STAGE1_THRESHOLD = 0.5; -constexpr float STAGE2_THRESHOLD = 0.65; -constexpr float STAGE3_THRESHOLD = 0.8; -constexpr float MAJORITY_THRESHOLD = 0.8; + constexpr float STAGE1_THRESHOLD = 0.5; + constexpr float STAGE2_THRESHOLD = 0.65; + constexpr float STAGE3_THRESHOLD = 0.8; + constexpr float MAJORITY_THRESHOLD = 0.8; -consensus_context ctx; + consensus_context ctx; -int init() -{ - //set start stage - ctx.stage = 0; + bool init_success = false; - //load lcl details from lcl history. - ledger_history ldr_hist = load_ledger(); - ctx.led_seq_no = ldr_hist.led_seq_no; - ctx.lcl = ldr_hist.lcl; - ctx.ledger_cache.swap(ldr_hist.cache); - - hasher::B2H root_hash = hasher::B2H_empty; - if (statefs::compute_hash_tree(root_hash, true) == -1) - return -1; - - LOG_INFO << "Initial state: " << root_hash; - - std::string str_root_hash(reinterpret_cast(&root_hash), hasher::HASH_SIZE); - str_root_hash.swap(ctx.curr_hash_state); - - if (!ctx.ledger_cache.empty()) + int init() { - ctx.prev_hash_state = ctx.ledger_cache.rbegin()->second.state; - } - else - { - ctx.prev_hash_state = ctx.curr_hash_state; + //set start stage + ctx.stage = 0; + + //load lcl details from lcl history. + ledger_history ldr_hist = load_ledger(); + ctx.led_seq_no = ldr_hist.led_seq_no; + ctx.lcl = ldr_hist.lcl; + ctx.ledger_cache.swap(ldr_hist.cache); + + if (hpfs::get_root_hash(ctx.curr_state_hash) == -1) + return -1; + + LOG_INFO << "Initial state: " << ctx.curr_state_hash; + + // We allocate 1/5 of the round time to each stage expect stage 3. For stage 3 we allocate 2/5. + // Stage 3 is allocated an extra stage_time unit becayse a node needs enough time to + // catch up from lcl/state desync. + ctx.stage_time = conf::cfg.roundtime / 5; + ctx.stage_reset_wait_threshold = conf::cfg.roundtime / 10; + + init_success = true; + return 0; } - ctx.state_syncing_thread = std::thread(&run_state_sync_iterator); - - // We allocate 1/5 of the round time to each stage expect stage 3. For stage 3 we allocate 2/5. - // Stage 3 is allocated an extra stage_time unit becayse a node needs enough time to - // catch up from lcl/state desync. - ctx.stage_time = conf::cfg.roundtime / 5; - ctx.stage_reset_wait_threshold = conf::cfg.roundtime / 10; - - return 0; -} - -/** + /** * Cleanup any resources. */ -void deinit() -{ - ctx.is_shutting_down = true; - ctx.state_syncing_thread.join(); -} - -void consensus() -{ - // A consensus round consists of 4 stages (0,1,2,3). - // For a given stage, this function may get visited multiple times due to time-wait conditions. - - uint64_t stage_start = 0; - if (!wait_and_proceed_stage(stage_start)) - return; // This means the stage has been reset. - - // Get the latest current time. - ctx.time_now = stage_start; - std::list collected_proposals; - - // Throughout consensus, we move over the incoming proposals collected via the network so far into - // the candidate proposal set (move and append). This is to have a private working set for the consensus - // and avoid threading conflicts with network incoming proposals. + void deinit() { - std::lock_guard lock(p2p::ctx.collected_msgs.proposals_mutex); - collected_proposals.splice(collected_proposals.end(), p2p::ctx.collected_msgs.proposals); - } + if (init_success) + { - //Copy collected propsals to candidate set of proposals. - //Add propsals of new nodes and replace proposals from old nodes to reflect current status of nodes. - for (const auto &proposal : collected_proposals) - { - auto prop_itr = ctx.candidate_proposals.find(proposal.pubkey); - if (prop_itr != ctx.candidate_proposals.end()) - { - ctx.candidate_proposals.erase(prop_itr); - ctx.candidate_proposals.emplace(proposal.pubkey, std::move(proposal)); - } - else - { - ctx.candidate_proposals.emplace(proposal.pubkey, std::move(proposal)); } } - // Throughout consensus, we move over the incoming npl messages collected via the network so far into - // the candidate npl message set (move and append). This is to have a private working set for the consensus - // and avoid threading conflicts with network incoming npl messages. - { - std::lock_guard lock(p2p::ctx.collected_msgs.npl_messages_mutex); - for (const auto &npl : p2p::ctx.collected_msgs.npl_messages) - { - const fbschema::p2pmsg::Container *container = fbschema::p2pmsg::GetContainer(npl.data()); - // Only the npl messages with a valid lcl will be passed down to the contract. lcl should match the previous round's lcl - if (fbschema::flatbuff_bytes_to_sv(container->lcl()) != ctx.lcl) - continue; - ctx.candidate_npl_messages.push_back(std::move(npl)); + int run_consensus() + { + while (true) + { + if (consensus() == -1) + return -1; } - p2p::ctx.collected_msgs.npl_messages.clear(); + + return 0; } - LOG_DBG << "Started stage " << std::to_string(ctx.stage); - - if (ctx.stage == 0) // Stage 0 means begining of a consensus round. + int consensus() { - // Broadcast non-unl proposals (NUP) containing inputs from locally connected users. - broadcast_nonunl_proposal(); - //util::sleep(conf::cfg.roundtime / 10); + // A consensus round consists of 4 stages (0,1,2,3). + // For a given stage, this function may get visited multiple times due to time-wait conditions. - // Verify and transfer user inputs from incoming NUPs onto consensus candidate data. - verify_and_populate_candidate_user_inputs(); + uint64_t stage_start = 0; + if (!wait_and_proceed_stage(stage_start)) + return 0; // This means the stage has been reset. - // In stage 0 we create a novel proposal and broadcast it. - const p2p::proposal stg_prop = create_stage0_proposal(); - broadcast_proposal(stg_prop); - } - else // Stage 1, 2, 3 - { - purify_candidate_proposals(); + // Get the latest current time. + ctx.time_now = stage_start; + std::list collected_proposals; - // Initialize vote counters - vote_counter votes; - - // check if we're ahead/behind of consensus lcl - bool is_lcl_desync, should_request_history; - std::string majority_lcl; - check_lcl_votes(is_lcl_desync, should_request_history, majority_lcl, votes); - - if (is_lcl_desync) + // Throughout consensus, we move over the incoming proposals collected via the network so far into + // the candidate proposal set (move and append). This is to have a private working set for the consensus + // and avoid threading conflicts with network incoming proposals. { - ctx.is_lcl_syncing = true; + std::lock_guard lock(p2p::ctx.collected_msgs.proposals_mutex); + collected_proposals.splice(collected_proposals.end(), p2p::ctx.collected_msgs.proposals); + } - if (should_request_history) + //Copy collected propsals to candidate set of proposals. + //Add propsals of new nodes and replace proposals from old nodes to reflect current status of nodes. + for (const auto &proposal : collected_proposals) + { + auto prop_itr = ctx.candidate_proposals.find(proposal.pubkey); + if (prop_itr != ctx.candidate_proposals.end()) { - LOG_INFO << "Syncing lcl. Curr lcl:" << cons::ctx.lcl.substr(0, 15) << " majority:" << majority_lcl.substr(0, 15); + ctx.candidate_proposals.erase(prop_itr); + ctx.candidate_proposals.emplace(proposal.pubkey, std::move(proposal)); + } + else + { + ctx.candidate_proposals.emplace(proposal.pubkey, std::move(proposal)); + } + } + // Throughout consensus, we move over the incoming npl messages collected via the network so far into + // the candidate npl message set (move and append). This is to have a private working set for the consensus + // and avoid threading conflicts with network incoming npl messages. + { + std::lock_guard lock(p2p::ctx.collected_msgs.npl_messages_mutex); + for (const auto &npl : p2p::ctx.collected_msgs.npl_messages) + { + const fbschema::p2pmsg::Container *container = fbschema::p2pmsg::GetContainer(npl.data()); + // Only the npl messages with a valid lcl will be passed down to the contract. lcl should match the previous round's lcl + if (fbschema::flatbuff_bytes_to_sv(container->lcl()) != ctx.lcl) + continue; - // TODO: If we are in a lcl fork condition try to rollback state with the help of - // state_restore to rollback state checkpoints before requesting new state. + ctx.candidate_npl_messages.push_back(std::move(npl)); + } + p2p::ctx.collected_msgs.npl_messages.clear(); + } - // Handle minority going forward when boostrapping cluster. - // Here we are mimicking invalid min ledger scenario. - if (majority_lcl == GENESIS_LEDGER) + LOG_DBG << "Started stage " << std::to_string(ctx.stage); + + if (ctx.stage == 0) // Stage 0 means begining of a consensus round. + { + // Broadcast non-unl proposals (NUP) containing inputs from locally connected users. + broadcast_nonunl_proposal(); + //util::sleep(conf::cfg.roundtime / 10); + + // Verify and transfer user inputs from incoming NUPs onto consensus candidate data. + verify_and_populate_candidate_user_inputs(); + + // In stage 0 we create a novel proposal and broadcast it. + const p2p::proposal stg_prop = create_stage0_proposal(); + broadcast_proposal(stg_prop); + } + else // Stage 1, 2, 3 + { + purify_candidate_proposals(); + + // Initialize vote counters + vote_counter votes; + + // check if we're ahead/behind of consensus lcl + bool is_lcl_desync, should_request_history; + std::string majority_lcl; + check_lcl_votes(is_lcl_desync, should_request_history, majority_lcl, votes); + + if (is_lcl_desync) + { + ctx.is_lcl_syncing = true; + + if (should_request_history) { - ctx.last_requested_lcl = majority_lcl; - p2p::history_response res; - res.error = p2p::LEDGER_RESPONSE_ERROR::INVALID_MIN_LEDGER; - handle_ledger_history_response(std::move(res)); + LOG_INFO << "Syncing lcl. Curr lcl:" << cons::ctx.lcl.substr(0, 15) << " majority:" << majority_lcl.substr(0, 15); + + // TODO: If we are in a lcl fork condition try to rollback state with the help of + // state_restore to rollback state checkpoints before requesting new state. + + // Handle minority going forward when boostrapping cluster. + // Here we are mimicking invalid min ledger scenario. + if (majority_lcl == GENESIS_LEDGER) + { + ctx.last_requested_lcl = majority_lcl; + p2p::history_response res; + res.error = p2p::LEDGER_RESPONSE_ERROR::INVALID_MIN_LEDGER; + handle_ledger_history_response(std::move(res)); + } + else + { + //create history request message and request history from a random peer. + send_ledger_history_request(ctx.lcl, majority_lcl); + } } - else + } + else + { + const bool lcl_syncing_just_finished = ctx.is_lcl_syncing; + ctx.is_lcl_syncing = false; + + if (lcl_syncing_just_finished) + ; //TODO: Check and compare majotiry state and start state sync. + bool is_state_syncing = false; + + if (!is_state_syncing) { - //create history request message and request history from a random peer. - send_ledger_history_request(ctx.lcl, majority_lcl); + conf::change_operating_mode(conf::OPERATING_MODE::PROPOSER); + + // In stage 1, 2, 3 we vote for incoming proposals and promote winning votes based on thresholds. + const p2p::proposal stg_prop = create_stage123_proposal(votes); + + broadcast_proposal(stg_prop); + + if (ctx.stage == 3) + { + if (apply_ledger(stg_prop) == -1) + return -1; + + // node has finished a consensus round (all 4 stages). + LOG_INFO << "****Stage 3 consensus reached**** (lcl:" << ctx.lcl.substr(0, 15) + << " state:" << ctx.curr_state_hash << ")"; + } } } } - else - { - const bool lcl_syncing_just_finished = ctx.is_lcl_syncing; - ctx.is_lcl_syncing = false; - if (lcl_syncing_just_finished || ctx.stage == 1 || (ctx.stage == 3 && ctx.is_state_syncing)) - check_state(votes); - - if (!ctx.is_state_syncing) - { - conf::change_operating_mode(conf::OPERATING_MODE::PROPOSER); - - // In stage 1, 2, 3 we vote for incoming proposals and promote winning votes based on thresholds. - const p2p::proposal stg_prop = create_stage123_proposal(votes); - - broadcast_proposal(stg_prop); - - if (ctx.stage == 3) - { - apply_ledger(stg_prop); - - // node has finished a consensus round (all 4 stages). - LOG_INFO << "****Stage 3 consensus reached**** (lcl:" << ctx.lcl.substr(0, 15) - << " state:" << *reinterpret_cast(cons::ctx.curr_hash_state.c_str()) << ")"; - } - } - } + // Node has finished a consensus stage. Transition to next stage. + ctx.stage = (ctx.stage + 1) % 4; + return 0; } - // Node has finished a consensus stage. Transition to next stage. - ctx.stage = (ctx.stage + 1) % 4; -} - -/** + /** * Cleanup any outdated proposals from the candidate set. */ -void purify_candidate_proposals() -{ - auto itr = ctx.candidate_proposals.begin(); - while (itr != ctx.candidate_proposals.end()) + void purify_candidate_proposals() { - const p2p::proposal &cp = itr->second; - - // only consider recent proposals and proposals from previous stage and current stage. - if ((ctx.time_now - cp.timestamp < conf::cfg.roundtime * 4) && cp.stage >= (ctx.stage - 1)) + auto itr = ctx.candidate_proposals.begin(); + while (itr != ctx.candidate_proposals.end()) { - ++itr; + const p2p::proposal &cp = itr->second; - bool self = cp.pubkey == conf::cfg.pubkey; - LOG_DBG << "Proposal [stage" << std::to_string(cp.stage) - << "] users:" << cp.users.size() - << " hinp:" << cp.hash_inputs.size() - << " hout:" << cp.hash_outputs.size() - << " ts:" << std::to_string(cp.time) - << " lcl:" << cp.lcl.substr(0, 15) - << " state:" << *reinterpret_cast(cp.curr_hash_state.c_str()) - << " self:" << self; - } - else - { - ctx.candidate_proposals.erase(itr++); + // only consider recent proposals and proposals from previous stage and current stage. + if ((ctx.time_now - cp.timestamp < conf::cfg.roundtime * 4) && cp.stage >= (ctx.stage - 1)) + { + ++itr; + + bool self = cp.pubkey == conf::cfg.pubkey; + LOG_DBG << "Proposal [stage" << std::to_string(cp.stage) + << "] users:" << cp.users.size() + << " hinp:" << cp.hash_inputs.size() + << " hout:" << cp.hash_outputs.size() + << " ts:" << std::to_string(cp.time) + << " lcl:" << cp.lcl.substr(0, 15) + << " state:" << cp.curr_state_hash + << " self:" << self; + } + else + { + ctx.candidate_proposals.erase(itr++); + } } } -} -/** + /** * Syncrhonise the stage/round time for fixed intervals and reset the stage. * @return True if consensus can proceed in the current round. False if stage is reset. */ -bool wait_and_proceed_stage(uint64_t &stage_start) -{ - // Here, nodes try to synchronise nodes stages using network clock. - // We devide universal time to windows of equal size of roundtime. Each round must be synced with the - // start of a window. - - const uint64_t now = util::get_epoch_milliseconds(); - - // Rrounds are divided into windows of roundtime. - // This gets the start time of current round window. Stage 0 must start in the next window. - const uint64_t current_round_start = (((uint64_t)(now / conf::cfg.roundtime)) * conf::cfg.roundtime); - - if (ctx.stage == 0) + bool wait_and_proceed_stage(uint64_t &stage_start) { - // Stage 0 must start in the next round window. - stage_start = current_round_start + conf::cfg.roundtime; - const int64_t to_wait = stage_start - now; + // Here, nodes try to synchronise nodes stages using network clock. + // We devide universal time to windows of equal size of roundtime. Each round must be synced with the + // start of a window. - LOG_DBG << "Waiting " << std::to_string(to_wait) << "ms for next round stage 0"; - util::sleep(to_wait); - return true; - } - else - { - stage_start = current_round_start + (ctx.stage * ctx.stage_time); + const uint64_t now = util::get_epoch_milliseconds(); - // Compute stage time wait. - // Node wait between stages to collect enough proposals from previous stages from other nodes. - const int64_t to_wait = stage_start - now; + // Rrounds are divided into windows of roundtime. + // This gets the start time of current round window. Stage 0 must start in the next window. + const uint64_t current_round_start = (((uint64_t)(now / conf::cfg.roundtime)) * conf::cfg.roundtime); - // If a node doesn't have enough time (eg. due to network delay) to recieve/send reliable stage proposals for next stage, - // it will continue particapating in this round, otherwise will join in next round. - if (to_wait < ctx.stage_reset_wait_threshold) //todo: self claculating/adjusting network delay + if (ctx.stage == 0) { - LOG_DBG << "Missed stage " << std::to_string(ctx.stage) << " window. Resetting to stage 0"; - ctx.stage = 0; - return false; - } - else - { - LOG_DBG << "Waiting " << std::to_string(to_wait) << "ms for stage " << std::to_string(ctx.stage); + // Stage 0 must start in the next round window. + stage_start = current_round_start + conf::cfg.roundtime; + const int64_t to_wait = stage_start - now; + + LOG_DBG << "Waiting " << std::to_string(to_wait) << "ms for next round stage 0"; util::sleep(to_wait); return true; } - } -} + else + { + stage_start = current_round_start + (ctx.stage * ctx.stage_time); -/** + // Compute stage time wait. + // Node wait between stages to collect enough proposals from previous stages from other nodes. + const int64_t to_wait = stage_start - now; + + // If a node doesn't have enough time (eg. due to network delay) to recieve/send reliable stage proposals for next stage, + // it will continue particapating in this round, otherwise will join in next round. + if (to_wait < ctx.stage_reset_wait_threshold) //todo: self claculating/adjusting network delay + { + LOG_DBG << "Missed stage " << std::to_string(ctx.stage) << " window. Resetting to stage 0"; + ctx.stage = 0; + return false; + } + else + { + LOG_DBG << "Waiting " << std::to_string(to_wait) << "ms for stage " << std::to_string(ctx.stage); + util::sleep(to_wait); + return true; + } + } + } + + /** * Broadcasts any inputs from locally connected users via an NUP. * @return 0 for successful broadcast. -1 for failure. */ -void broadcast_nonunl_proposal() -{ - std::lock_guard lock(p2p::ctx.collected_msgs.nonunl_proposals_mutex); - - if (usr::ctx.users.empty()) - return; - - // Construct NUP. - p2p::nonunl_proposal nup; - - for (auto &[sid, user] : usr::ctx.users) + void broadcast_nonunl_proposal() { - std::list usermsgs; - usermsgs.splice(usermsgs.end(), user.submitted_inputs); + std::lock_guard lock(p2p::ctx.collected_msgs.nonunl_proposals_mutex); - // We should create an entry for each user pubkey, even if the user has no inputs. This is - // because this data map will be used to track connected users as well in addition to inputs. - nup.user_messages.try_emplace(user.pubkey, std::move(usermsgs)); + if (usr::ctx.users.empty()) + return; + + // Construct NUP. + p2p::nonunl_proposal nup; + + for (auto &[sid, user] : usr::ctx.users) + { + std::list usermsgs; + usermsgs.splice(usermsgs.end(), user.submitted_inputs); + + // We should create an entry for each user pubkey, even if the user has no inputs. This is + // because this data map will be used to track connected users as well in addition to inputs. + nup.user_messages.try_emplace(user.pubkey, std::move(usermsgs)); + } + + flatbuffers::FlatBufferBuilder fbuf(1024); + p2pmsg::create_msg_from_nonunl_proposal(fbuf, nup); + p2p::broadcast_message(fbuf, true); + + LOG_DBG << "NUP sent." + << " users:" << nup.user_messages.size(); } - flatbuffers::FlatBufferBuilder fbuf(1024); - p2pmsg::create_msg_from_nonunl_proposal(fbuf, nup); - p2p::broadcast_message(fbuf, true); - - LOG_DBG << "NUP sent." - << " users:" << nup.user_messages.size(); -} - -/** + /** * Verifies the user signatures and populate non-expired user inputs from collected * non-unl proposals (if any) into consensus candidate data. */ -void verify_and_populate_candidate_user_inputs() -{ - // Lock the user sessions. - std::lock_guard users_lock(usr::ctx.users_mutex); - - // Lock the list so any network activity is blocked. - std::lock_guard nups_lock(p2p::ctx.collected_msgs.nonunl_proposals_mutex); - for (const p2p::nonunl_proposal &p : p2p::ctx.collected_msgs.nonunl_proposals) + void verify_and_populate_candidate_user_inputs() { - for (const auto &[pubkey, umsgs] : p.user_messages) + // Lock the user sessions. + std::lock_guard users_lock(usr::ctx.users_mutex); + + // Lock the list so any network activity is blocked. + std::lock_guard nups_lock(p2p::ctx.collected_msgs.nonunl_proposals_mutex); + for (const p2p::nonunl_proposal &p : p2p::ctx.collected_msgs.nonunl_proposals) { - // Locate this user's socket session in case we need to send any status messages regarding user inputs. - const comm::comm_session *session = usr::get_session_by_pubkey(pubkey); - - // Populate user list with this user's pubkey. - ctx.candidate_users.emplace(pubkey); - - // Keep track of total input length to verify against remaining balance. - // We only process inputs in the submitted order that can be satisfied with the remaining account balance. - size_t total_input_len = 0; - bool appbill_balance_exceeded = false; - - for (const usr::user_submitted_message &umsg : umsgs) + for (const auto &[pubkey, umsgs] : p.user_messages) { - const char *reject_reason = NULL; - const std::string sig_hash = crypto::get_hash(umsg.sig); + // Locate this user's socket session in case we need to send any status messages regarding user inputs. + const comm::comm_session *session = usr::get_session_by_pubkey(pubkey); - // Check for duplicate messages using hash of the signature. - if (ctx.recent_userinput_hashes.try_emplace(sig_hash)) + // Populate user list with this user's pubkey. + ctx.candidate_users.emplace(pubkey); + + // Keep track of total input length to verify against remaining balance. + // We only process inputs in the submitted order that can be satisfied with the remaining account balance. + size_t total_input_len = 0; + bool appbill_balance_exceeded = false; + + for (const usr::user_submitted_message &umsg : umsgs) { - // Verify the signature of the message content. - if (crypto::verify(umsg.content, umsg.sig, pubkey) == 0) + const char *reject_reason = NULL; + const std::string sig_hash = crypto::get_hash(umsg.sig); + + // Check for duplicate messages using hash of the signature. + if (ctx.recent_userinput_hashes.try_emplace(sig_hash)) { - std::string nonce; - std::string input; - uint64_t maxledgerseqno; - jusrmsg::extract_input_container(nonce, input, maxledgerseqno, umsg.content); - - // Ignore the input if our ledger has passed the input TTL. - if (maxledgerseqno > ctx.led_seq_no) + // Verify the signature of the message content. + if (crypto::verify(umsg.content, umsg.sig, pubkey) == 0) { - if (!appbill_balance_exceeded) - { - // Hash is prefixed with the nonce to support user-defined sort order. - std::string hash = std::move(nonce); - // Append the hash of the message signature to get the final hash. - hash.append(sig_hash); + std::string nonce; + std::string input; + uint64_t maxledgerseqno; + jusrmsg::extract_input_container(nonce, input, maxledgerseqno, umsg.content); - // Keep checking the subtotal of inputs extracted so far with the appbill account balance. - total_input_len += input.length(); - if (verify_appbill_check(pubkey, total_input_len)) + // Ignore the input if our ledger has passed the input TTL. + if (maxledgerseqno > ctx.led_seq_no) + { + if (!appbill_balance_exceeded) { - ctx.candidate_user_inputs.try_emplace( - hash, - candidate_user_input(pubkey, std::move(input), maxledgerseqno)); + // Hash is prefixed with the nonce to support user-defined sort order. + std::string hash = std::move(nonce); + // Append the hash of the message signature to get the final hash. + hash.append(sig_hash); + + // Keep checking the subtotal of inputs extracted so far with the appbill account balance. + total_input_len += input.length(); + if (verify_appbill_check(pubkey, total_input_len)) + { + ctx.candidate_user_inputs.try_emplace( + hash, + candidate_user_input(pubkey, std::move(input), maxledgerseqno)); + } + else + { + // Abandon processing further inputs from this user when we find out + // an input cannot be processed with the account balance. + appbill_balance_exceeded = true; + reject_reason = jusrmsg::REASON_APPBILL_BALANCE_EXCEEDED; + } } else { - // Abandon processing further inputs from this user when we find out - // an input cannot be processed with the account balance. - appbill_balance_exceeded = true; reject_reason = jusrmsg::REASON_APPBILL_BALANCE_EXCEEDED; } } else { - reject_reason = jusrmsg::REASON_APPBILL_BALANCE_EXCEEDED; + LOG_DBG << "User message bad max ledger seq expired."; + reject_reason = jusrmsg::REASON_MAX_LEDGER_EXPIRED; } } else { - LOG_DBG << "User message bad max ledger seq expired."; - reject_reason = jusrmsg::REASON_MAX_LEDGER_EXPIRED; + LOG_DBG << "User message bad signature."; + reject_reason = jusrmsg::REASON_BAD_SIG; } } else { - LOG_DBG << "User message bad signature."; - reject_reason = jusrmsg::REASON_BAD_SIG; + LOG_DBG << "Duplicate user message."; + reject_reason = jusrmsg::REASON_DUPLICATE_MSG; } - } - else - { - LOG_DBG << "Duplicate user message."; - reject_reason = jusrmsg::REASON_DUPLICATE_MSG; - } - // Send the request status result if this user is connected to us. - if (session != NULL) - { - usr::send_request_status_result(*session, - reject_reason == NULL ? jusrmsg::STATUS_ACCEPTED : jusrmsg::STATUS_REJECTED, - reject_reason == NULL ? "" : reject_reason, - jusrmsg::MSGTYPE_CONTRACT_INPUT, - jusrmsg::origin_data_for_contract_input(umsg.sig)); + // Send the request status result if this user is connected to us. + if (session != NULL) + { + usr::send_request_status_result(*session, + reject_reason == NULL ? jusrmsg::STATUS_ACCEPTED : jusrmsg::STATUS_REJECTED, + reject_reason == NULL ? "" : reject_reason, + jusrmsg::MSGTYPE_CONTRACT_INPUT, + jusrmsg::origin_data_for_contract_input(umsg.sig)); + } } } } + p2p::ctx.collected_msgs.nonunl_proposals.clear(); } - p2p::ctx.collected_msgs.nonunl_proposals.clear(); -} -/** + /** * Executes the appbill and verifies whether the user has enough account balance to process the provided input. * @param pubkey User binary pubkey. * @param input_len Total bytes length of user input. * @return Whether the user is allowed to process the input or not. */ -bool verify_appbill_check(std::string_view pubkey, const size_t input_len) -{ - // If appbill not enabled always green light the input. - if (conf::cfg.appbill.empty()) - return true; - - // execute appbill in --check mode to verify this user can submit a packet/connection to the network - // todo: this can be made more efficient, appbill --check can process 7 at a time - - // Fill appbill args - const int len = conf::cfg.runtime_appbill_args.size() + 4; - char *execv_args[len]; - for (int i = 0; i < conf::cfg.runtime_appbill_args.size(); i++) - execv_args[i] = conf::cfg.runtime_appbill_args[i].data(); - char option[] = "--check"; - execv_args[len - 4] = option; - // add the hex encoded public key as the last parameter - std::string hexpubkey; - util::bin2hex(hexpubkey, reinterpret_cast(pubkey.data()), pubkey.size()); - std::string inputsize = std::to_string(input_len); - execv_args[len - 3] = hexpubkey.data(); - execv_args[len - 2] = inputsize.data(); - execv_args[len - 1] = NULL; - - int pid = fork(); - if (pid == 0) + bool verify_appbill_check(std::string_view pubkey, const size_t input_len) { - // before execution chdir into a valid the latest state data directory that contains an appbill.table - chdir(statefs::current_ctx.data_dir.c_str()); - int ret = execv(execv_args[0], execv_args); - LOG_ERR << "Appbill process execv failed: " << ret; - return false; - } - else - { - // app bill in check mode takes a very short period of time to execute, typically 1ms - // so we will blocking wait for it here - int status = 0; - waitpid(pid, &status, 0); //todo: check error conditions here - status = WEXITSTATUS(status); - if (status != 128 && status != 0) - { - // this user's key passed appbill + // If appbill not enabled always green light the input. + if (conf::cfg.appbill.empty()) return true; + + // execute appbill in --check mode to verify this user can submit a packet/connection to the network + // todo: this can be made more efficient, appbill --check can process 7 at a time + + // Fill appbill args + const int len = conf::cfg.runtime_appbill_args.size() + 4; + char *execv_args[len]; + for (int i = 0; i < conf::cfg.runtime_appbill_args.size(); i++) + execv_args[i] = conf::cfg.runtime_appbill_args[i].data(); + char option[] = "--check"; + execv_args[len - 4] = option; + // add the hex encoded public key as the last parameter + std::string hexpubkey; + util::bin2hex(hexpubkey, reinterpret_cast(pubkey.data()), pubkey.size()); + std::string inputsize = std::to_string(input_len); + execv_args[len - 3] = hexpubkey.data(); + execv_args[len - 2] = inputsize.data(); + execv_args[len - 1] = NULL; + + int pid = fork(); + if (pid == 0) + { + // before execution chdir into a valid the latest state data directory that contains an appbill.table + chdir(conf::ctx.state_rw_dir.c_str()); + int ret = execv(execv_args[0], execv_args); + LOG_ERR << "Appbill process execv failed: " << ret; + return false; } else { - // user's key did not pass, do not add to user input candidates - LOG_DBG << "Appbill validation failed " << hexpubkey << " return code was " << status; - return false; + // app bill in check mode takes a very short period of time to execute, typically 1ms + // so we will blocking wait for it here + int status = 0; + waitpid(pid, &status, 0); //todo: check error conditions here + status = WEXITSTATUS(status); + if (status != 128 && status != 0) + { + // this user's key passed appbill + return true; + } + else + { + // user's key did not pass, do not add to user input candidates + LOG_DBG << "Appbill validation failed " << hexpubkey << " return code was " << status; + return false; + } } } -} -p2p::proposal create_stage0_proposal() -{ - // The proposal we are going to emit in stage 0. - p2p::proposal stg_prop; - stg_prop.time = ctx.time_now; - stg_prop.stage = 0; - stg_prop.lcl = ctx.lcl; - stg_prop.curr_hash_state = ctx.curr_hash_state; - - // Populate the proposal with set of candidate user pubkeys. - for (const std::string &pubkey : ctx.candidate_users) - stg_prop.users.emplace(pubkey); - - // We don't need candidate_users anymore, so clear it. It will be repopulated during next consensus round. - ctx.candidate_users.clear(); - - // Populate the proposal with hashes of user inputs. - for (const auto &[hash, cand_input] : ctx.candidate_user_inputs) - stg_prop.hash_inputs.emplace(hash); - - // Populate the proposal with hashes of user outputs. - for (const auto &[hash, cand_output] : ctx.candidate_user_outputs) - stg_prop.hash_outputs.emplace(hash); - - // todo: generate stg_prop hash and check with ctx.novel_proposal, we are sending same proposal again. - - return stg_prop; -} - -p2p::proposal create_stage123_proposal(vote_counter &votes) -{ - // The proposal to be emited at the end of this stage. - p2p::proposal stg_prop; - stg_prop.stage = ctx.stage; - - // we always vote for our current lcl and state regardless of what other peers are saying - // if there's a fork condition we will either request history and state from - // our peers or we will halt depending on level of consensus on the sides of the fork - stg_prop.lcl = ctx.lcl; - stg_prop.curr_hash_state = ctx.curr_hash_state; - - // Vote for rest of the proposal fields by looking at candidate proposals. - for (const auto &[pubkey, cp] : ctx.candidate_proposals) + p2p::proposal create_stage0_proposal() { - // Vote for times. - // Everyone votes on an arbitrary time, as long as its within the round time and not in the future. - if (ctx.time_now > cp.time && (ctx.time_now - cp.time) < conf::cfg.roundtime) - increment(votes.time, cp.time); + // The proposal we are going to emit in stage 0. + p2p::proposal stg_prop; + stg_prop.time = ctx.time_now; + stg_prop.stage = 0; + stg_prop.lcl = ctx.lcl; + stg_prop.curr_state_hash = ctx.curr_state_hash; - // Vote for user pubkeys. - for (const std::string &pubkey : cp.users) - increment(votes.users, pubkey); - - // Vote for user inputs (hashes). Only vote for the inputs that are in our candidate_inputs set. - for (const std::string &hash : cp.hash_inputs) - if (ctx.candidate_user_inputs.count(hash) > 0) - increment(votes.inputs, hash); - - // Vote for contract outputs (hashes). Only vote for the outputs that are in our candidate_outputs set. - for (const std::string &hash : cp.hash_outputs) - if (ctx.candidate_user_outputs.count(hash) > 0) - increment(votes.outputs, hash); - } - - const float_t vote_threshold = get_stage_threshold(ctx.stage); - - // todo: check if inputs being proposed by another node are actually spoofed inputs - // from a user locally connected to this node. - - // if we're at proposal stage 1 we'll accept any input and connection that has 1 or more vote. - - // Add user pubkeys which have votes over stage threshold to proposal. - for (const auto &[pubkey, numvotes] : votes.users) - if (numvotes >= vote_threshold || (ctx.stage == 1 && numvotes > 0)) + // Populate the proposal with set of candidate user pubkeys. + for (const std::string &pubkey : ctx.candidate_users) stg_prop.users.emplace(pubkey); - // Add inputs which have votes over stage threshold to proposal. - for (const auto &[hash, numvotes] : votes.inputs) - if (numvotes >= vote_threshold || (ctx.stage == 1 && numvotes > 0)) + // We don't need candidate_users anymore, so clear it. It will be repopulated during next consensus round. + ctx.candidate_users.clear(); + + // Populate the proposal with hashes of user inputs. + for (const auto &[hash, cand_input] : ctx.candidate_user_inputs) stg_prop.hash_inputs.emplace(hash); - // Add outputs which have votes over stage threshold to proposal. - for (const auto &[hash, numvotes] : votes.outputs) - if (numvotes >= vote_threshold) + // Populate the proposal with hashes of user outputs. + for (const auto &[hash, cand_output] : ctx.candidate_user_outputs) stg_prop.hash_outputs.emplace(hash); - // time is voted on a simple sorted (highest to lowest) and majority basis, since there will always be disagreement. - int32_t highest_time_vote = 0; - for (auto itr = votes.time.rbegin(); itr != votes.time.rend(); ++itr) - { - const uint64_t time = itr->first; - const int32_t numvotes = itr->second; + // todo: generate stg_prop hash and check with ctx.novel_proposal, we are sending same proposal again. - if (numvotes > highest_time_vote) - { - highest_time_vote = numvotes; - stg_prop.time = time; - } + return stg_prop; } - return stg_prop; -} + p2p::proposal create_stage123_proposal(vote_counter &votes) + { + // The proposal to be emited at the end of this stage. + p2p::proposal stg_prop; + stg_prop.stage = ctx.stage; -/** + // we always vote for our current lcl and state regardless of what other peers are saying + // if there's a fork condition we will either request history and state from + // our peers or we will halt depending on level of consensus on the sides of the fork + stg_prop.lcl = ctx.lcl; + stg_prop.curr_state_hash = ctx.curr_state_hash; + + // Vote for rest of the proposal fields by looking at candidate proposals. + for (const auto &[pubkey, cp] : ctx.candidate_proposals) + { + // Vote for times. + // Everyone votes on an arbitrary time, as long as its within the round time and not in the future. + if (ctx.time_now > cp.time && (ctx.time_now - cp.time) < conf::cfg.roundtime) + increment(votes.time, cp.time); + + // Vote for user pubkeys. + for (const std::string &pubkey : cp.users) + increment(votes.users, pubkey); + + // Vote for user inputs (hashes). Only vote for the inputs that are in our candidate_inputs set. + for (const std::string &hash : cp.hash_inputs) + if (ctx.candidate_user_inputs.count(hash) > 0) + increment(votes.inputs, hash); + + // Vote for contract outputs (hashes). Only vote for the outputs that are in our candidate_outputs set. + for (const std::string &hash : cp.hash_outputs) + if (ctx.candidate_user_outputs.count(hash) > 0) + increment(votes.outputs, hash); + } + + const float_t vote_threshold = get_stage_threshold(ctx.stage); + + // todo: check if inputs being proposed by another node are actually spoofed inputs + // from a user locally connected to this node. + + // if we're at proposal stage 1 we'll accept any input and connection that has 1 or more vote. + + // Add user pubkeys which have votes over stage threshold to proposal. + for (const auto &[pubkey, numvotes] : votes.users) + if (numvotes >= vote_threshold || (ctx.stage == 1 && numvotes > 0)) + stg_prop.users.emplace(pubkey); + + // Add inputs which have votes over stage threshold to proposal. + for (const auto &[hash, numvotes] : votes.inputs) + if (numvotes >= vote_threshold || (ctx.stage == 1 && numvotes > 0)) + stg_prop.hash_inputs.emplace(hash); + + // Add outputs which have votes over stage threshold to proposal. + for (const auto &[hash, numvotes] : votes.outputs) + if (numvotes >= vote_threshold) + stg_prop.hash_outputs.emplace(hash); + + // time is voted on a simple sorted (highest to lowest) and majority basis, since there will always be disagreement. + int32_t highest_time_vote = 0; + for (auto itr = votes.time.rbegin(); itr != votes.time.rend(); ++itr) + { + const uint64_t time = itr->first; + const int32_t numvotes = itr->second; + + if (numvotes > highest_time_vote) + { + highest_time_vote = numvotes; + stg_prop.time = time; + } + } + + return stg_prop; + } + + /** * Broadcasts the given proposal to all connected peers. * @return 0 on success. -1 if no peers to broadcast. */ -void broadcast_proposal(const p2p::proposal &p) -{ - flatbuffers::FlatBufferBuilder fbuf(1024); - p2pmsg::create_msg_from_proposal(fbuf, p); + void broadcast_proposal(const p2p::proposal &p) + { + flatbuffers::FlatBufferBuilder fbuf(1024); + p2pmsg::create_msg_from_proposal(fbuf, p); - // In observer mode, we only send out the proposal to ourselves. - if (conf::cfg.current_mode == conf::OPERATING_MODE::OBSERVER) - p2p::send_message_to_self(fbuf); - else - p2p::broadcast_message(fbuf, true); + // In observer mode, we only send out the proposal to ourselves. + if (conf::cfg.current_mode == conf::OPERATING_MODE::OBSERVER) + p2p::send_message_to_self(fbuf); + else + p2p::broadcast_message(fbuf, true); - // LOG_DBG << "Proposed [stage" << std::to_string(p.stage) - // << "] users:" << p.users.size() - // << " hinp:" << p.hash_inputs.size() - // << " hout:" << p.hash_outputs.size() - // << " ts:" << std::to_string(p.time); -} + // LOG_DBG << "Proposed [stage" << std::to_string(p.stage) + // << "] users:" << p.users.size() + // << " hinp:" << p.hash_inputs.size() + // << " hout:" << p.hash_outputs.size() + // << " ts:" << std::to_string(p.time); + } -/** + /** * Check our LCL is consistent with the proposals being made by our UNL peers lcl_votes. */ -void check_lcl_votes(bool &is_desync, bool &should_request_history, std::string &majority_lcl, vote_counter &votes) -{ - int32_t total_lcl_votes = 0; - - for (const auto &[pubkey, cp] : ctx.candidate_proposals) + void check_lcl_votes(bool &is_desync, bool &should_request_history, std::string &majority_lcl, vote_counter &votes) { - increment(votes.lcl, cp.lcl); - total_lcl_votes++; - } + int32_t total_lcl_votes = 0; - is_desync = false; - should_request_history = false; - - if (total_lcl_votes < (MAJORITY_THRESHOLD * conf::cfg.unl.size())) - { - LOG_DBG << "Not enough peers proposing to perform consensus. votes:" << std::to_string(total_lcl_votes) << " needed:" << std::to_string(MAJORITY_THRESHOLD * conf::cfg.unl.size()); - is_desync = true; - - //Not enough nodes are propsing. So Node is switching to Proposer if it's in observer mode. - conf::change_operating_mode(conf::OPERATING_MODE::PROPOSER); - - return; - } - - int32_t winning_votes = 0; - for (const auto [lcl, votes] : votes.lcl) - { - if (votes > winning_votes) + for (const auto &[pubkey, cp] : ctx.candidate_proposals) { - winning_votes = votes; - majority_lcl = lcl; + increment(votes.lcl, cp.lcl); + total_lcl_votes++; + } + + is_desync = false; + should_request_history = false; + + if (total_lcl_votes < (MAJORITY_THRESHOLD * conf::cfg.unl.size())) + { + LOG_DBG << "Not enough peers proposing to perform consensus. votes:" << std::to_string(total_lcl_votes) << " needed:" << std::to_string(MAJORITY_THRESHOLD * conf::cfg.unl.size()); + is_desync = true; + + //Not enough nodes are propsing. So Node is switching to Proposer if it's in observer mode. + conf::change_operating_mode(conf::OPERATING_MODE::PROPOSER); + + return; + } + + int32_t winning_votes = 0; + for (const auto [lcl, votes] : votes.lcl) + { + if (votes > winning_votes) + { + winning_votes = votes; + majority_lcl = lcl; + } + } + + //if winning lcl is not matched node lcl, + //that means vote is not on the consensus ledger. + //Should request history from a peer. + if (ctx.lcl != majority_lcl) + { + LOG_DBG << "We are not on the consensus ledger, requesting history from a random peer"; + is_desync = true; + + //Node is not in sync with current lcl ->switch to observer mode. + conf::change_operating_mode(conf::OPERATING_MODE::OBSERVER); + + should_request_history = true; + return; + } + + if (winning_votes < MAJORITY_THRESHOLD * ctx.candidate_proposals.size()) + { + // potential fork condition. + LOG_DBG << "No consensus on lcl. Possible fork condition. won:" << std::to_string(winning_votes) << " total:" << std::to_string(ctx.candidate_proposals.size()); + is_desync = true; + return; } } - //if winning lcl is not matched node lcl, - //that means vote is not on the consensus ledger. - //Should request history from a peer. - if (ctx.lcl != majority_lcl) - { - LOG_DBG << "We are not on the consensus ledger, requesting history from a random peer"; - is_desync = true; - - //Node is not in sync with current lcl ->switch to observer mode. - conf::change_operating_mode(conf::OPERATING_MODE::OBSERVER); - - should_request_history = true; - return; - } - - if (winning_votes < MAJORITY_THRESHOLD * ctx.candidate_proposals.size()) - { - // potential fork condition. - LOG_DBG << "No consensus on lcl. Possible fork condition. won:" << std::to_string(winning_votes) << " total:" << std::to_string(ctx.candidate_proposals.size()); - is_desync = true; - return; - } -} - -/** + /** * Returns the consensus percentage threshold for the specified stage. * @param stage The consensus stage [1, 2, 3] */ -float_t get_stage_threshold(const uint8_t stage) -{ - switch (stage) + float_t get_stage_threshold(const uint8_t stage) { - case 1: - return cons::STAGE1_THRESHOLD * conf::cfg.unl.size(); - case 2: - return cons::STAGE2_THRESHOLD * conf::cfg.unl.size(); - case 3: - return cons::STAGE3_THRESHOLD * conf::cfg.unl.size(); + switch (stage) + { + case 1: + return cons::STAGE1_THRESHOLD * conf::cfg.unl.size(); + case 2: + return cons::STAGE2_THRESHOLD * conf::cfg.unl.size(); + case 3: + return cons::STAGE3_THRESHOLD * conf::cfg.unl.size(); + } + return -1; } - return -1; -} -/** + /** * Finalize the ledger after consensus. * @param cons_prop The proposal that reached consensus. */ -void apply_ledger(const p2p::proposal &cons_prop) -{ - const std::tuple new_lcl = save_ledger(cons_prop); - ctx.led_seq_no = std::get<0>(new_lcl); - ctx.lcl = std::get<1>(new_lcl); - ctx.prev_hash_state = ctx.curr_hash_state; - - // After the current ledger seq no is updated, we remove any newly expired inputs from candidate set. + int apply_ledger(const p2p::proposal &cons_prop) { - auto itr = ctx.candidate_user_inputs.begin(); - while (itr != ctx.candidate_user_inputs.end()) + const std::tuple new_lcl = save_ledger(cons_prop); + ctx.led_seq_no = std::get<0>(new_lcl); + ctx.lcl = std::get<1>(new_lcl); + + // After the current ledger seq no is updated, we remove any newly expired inputs from candidate set. { - if (itr->second.maxledgerseqno <= ctx.led_seq_no) - ctx.candidate_user_inputs.erase(itr++); - else - ++itr; + auto itr = ctx.candidate_user_inputs.begin(); + while (itr != ctx.candidate_user_inputs.end()) + { + if (itr->second.maxledgerseqno <= ctx.led_seq_no) + ctx.candidate_user_inputs.erase(itr++); + else + ++itr; + } } + + // Send any output from the previous consensus round to locally connected users. + dispatch_user_outputs(cons_prop); + + proc::contract_bufmap_t useriobufmap; + + proc::contract_iobuf_pair nplbufpair; + nplbufpair.inputs.splice(nplbufpair.inputs.end(), ctx.candidate_npl_messages); + + feed_user_inputs_to_contract_bufmap(useriobufmap, cons_prop); + + if (run_contract_binary(cons_prop.time, useriobufmap, nplbufpair) == -1) + return -1; + + extract_user_outputs_from_contract_bufmap(useriobufmap); + broadcast_npl_output(nplbufpair.output); + return 0; } - // Send any output from the previous consensus round to locally connected users. - dispatch_user_outputs(cons_prop); - - // This will hold a list of file blocks that was updated by the contract process. - // We then feed this information to state tracking logic. - proc::contract_fblockmap_t updated_blocks; - - proc::contract_bufmap_t useriobufmap; - - proc::contract_iobuf_pair nplbufpair; - nplbufpair.inputs.splice(nplbufpair.inputs.end(), ctx.candidate_npl_messages); - - feed_user_inputs_to_contract_bufmap(useriobufmap, cons_prop); - - run_contract_binary(cons_prop.time, useriobufmap, nplbufpair, updated_blocks); - - extract_user_outputs_from_contract_bufmap(useriobufmap); - broadcast_npl_output(nplbufpair.output); -} - -/** + /** * Dispatch any consensus-reached outputs to matching users if they are connected to us locally. * @param cons_prop The proposal that achieved consensus. */ -void dispatch_user_outputs(const p2p::proposal &cons_prop) -{ - std::lock_guard lock(usr::ctx.users_mutex); - - for (const std::string &hash : cons_prop.hash_outputs) + void dispatch_user_outputs(const p2p::proposal &cons_prop) { - const auto cu_itr = ctx.candidate_user_outputs.find(hash); - const bool hashfound = (cu_itr != ctx.candidate_user_outputs.end()); - if (!hashfound) - { - LOG_ERR << "Output required but wasn't in our candidate outputs map, this will potentially cause desync."; - // todo: consider fatal - } - else - { - // Send matching outputs to locally connected users. + std::lock_guard lock(usr::ctx.users_mutex); - candidate_user_output &cand_output = cu_itr->second; - - // Find the user session by user pubkey. - const auto sess_itr = usr::ctx.sessionids.find(cand_output.userpubkey); - if (sess_itr != usr::ctx.sessionids.end()) // match found + for (const std::string &hash : cons_prop.hash_outputs) + { + const auto cu_itr = ctx.candidate_user_outputs.find(hash); + const bool hashfound = (cu_itr != ctx.candidate_user_outputs.end()); + if (!hashfound) { - const auto user_itr = usr::ctx.users.find(sess_itr->second); // sess_itr->second is the session id. - if (user_itr != usr::ctx.users.end()) // match found - { - std::string outputtosend; - outputtosend.swap(cand_output.output); - - std::string msg; - jusrmsg::create_contract_output_container(msg, outputtosend); - - const usr::connected_user &user = user_itr->second; - user.session.send(msg); - } + LOG_ERR << "Output required but wasn't in our candidate outputs map, this will potentially cause desync."; + // todo: consider fatal } + else + { + // Send matching outputs to locally connected users. - // now we can safely delete this candidate output. - ctx.candidate_user_outputs.erase(cu_itr); + candidate_user_output &cand_output = cu_itr->second; + + // Find the user session by user pubkey. + const auto sess_itr = usr::ctx.sessionids.find(cand_output.userpubkey); + if (sess_itr != usr::ctx.sessionids.end()) // match found + { + const auto user_itr = usr::ctx.users.find(sess_itr->second); // sess_itr->second is the session id. + if (user_itr != usr::ctx.users.end()) // match found + { + std::string outputtosend; + outputtosend.swap(cand_output.output); + + std::string msg; + jusrmsg::create_contract_output_container(msg, outputtosend); + + const usr::connected_user &user = user_itr->second; + user.session.send(msg); + } + } + + // now we can safely delete this candidate output. + ctx.candidate_user_outputs.erase(cu_itr); + } } } -} -/** + /** * Check state against the winning and canonical state * @param votes The voting table. */ -void check_state(vote_counter &votes) -{ - std::string majority_state; - - for (const auto &[pubkey, cp] : ctx.candidate_proposals) + void check_state(vote_counter &votes) { - increment(votes.state, cp.curr_hash_state); - } + hpfs::h32 majority_state = hpfs::h32_empty; - int32_t winning_votes = 0; - for (const auto [state, votes] : votes.state) - { - if (votes > winning_votes) + for (const auto &[pubkey, cp] : ctx.candidate_proposals) { - winning_votes = votes; - majority_state = state; + increment(votes.state, cp.curr_state_hash); + } + + int32_t winning_votes = 0; + for (const auto [state, votes] : votes.state) + { + if (votes > winning_votes) + { + winning_votes = votes; + majority_state = state; + } } } - if (ctx.is_state_syncing) - { - std::lock_guard lock(cons::ctx.state_syncing_mutex); - hasher::B2H root_hash = hasher::B2H_empty; - int ret = statefs::compute_hash_tree(root_hash); - std::string str_root_hash(reinterpret_cast(&root_hash), hasher::HASH_SIZE); - str_root_hash.swap(ctx.curr_hash_state); - } - - // We do not initiate state sync in stage 3 because the majority state is likely to get changed soon. - if (ctx.stage < 3 && majority_state != ctx.curr_hash_state) - { - if (ctx.state_sync_lcl != ctx.lcl) - { - // Switch to observer mode to avoid sending out proposals till the state is synced - conf::change_operating_mode(conf::OPERATING_MODE::OBSERVER); - - const hasher::B2H majority_state_hash = *reinterpret_cast(majority_state.c_str()); - LOG_INFO << "Syncing state. Curr state:" << *reinterpret_cast(ctx.curr_hash_state.c_str()) << " majority:" << majority_state_hash; - - start_state_sync(majority_state_hash); - - ctx.is_state_syncing = true; - ctx.state_sync_lcl = ctx.lcl; - } - } - else if (majority_state == ctx.curr_hash_state && ctx.is_state_syncing) - { - LOG_INFO << "State sync complete. state:" << *reinterpret_cast(ctx.curr_hash_state.c_str()); - - ctx.is_state_syncing = false; - ctx.state_sync_lcl.clear(); - } -} - -/** + /** * Transfers consensus-reached inputs into the provided contract buf map so it can be fed into the contract process. * @param bufmap The contract bufmap which needs to be populated with inputs. * @param cons_prop The proposal that achieved consensus. */ -void feed_user_inputs_to_contract_bufmap(proc::contract_bufmap_t &bufmap, const p2p::proposal &cons_prop) -{ - // Populate the buf map with all currently connected users regardless of whether they have inputs or not. - // This is in case the contract wanted to emit some data to a user without needing any input. - for (const std::string &pubkey : cons_prop.users) - bufmap.try_emplace(pubkey, proc::contract_iobuf_pair()); - - for (const std::string &hash : cons_prop.hash_inputs) + void feed_user_inputs_to_contract_bufmap(proc::contract_bufmap_t &bufmap, const p2p::proposal &cons_prop) { - // For each consensus input hash, we need to find the actual input content to feed the contract. - const auto itr = ctx.candidate_user_inputs.find(hash); - const bool hashfound = (itr != ctx.candidate_user_inputs.end()); - if (!hashfound) + // Populate the buf map with all currently connected users regardless of whether they have inputs or not. + // This is in case the contract wanted to emit some data to a user without needing any input. + for (const std::string &pubkey : cons_prop.users) + bufmap.try_emplace(pubkey, proc::contract_iobuf_pair()); + + for (const std::string &hash : cons_prop.hash_inputs) { - LOG_ERR << "input required but wasn't in our candidate inputs map, this will potentially cause desync."; - // TODO: consider fatal - } - else - { - // Populate the input content into the bufmap. + // For each consensus input hash, we need to find the actual input content to feed the contract. + const auto itr = ctx.candidate_user_inputs.find(hash); + const bool hashfound = (itr != ctx.candidate_user_inputs.end()); + if (!hashfound) + { + LOG_ERR << "input required but wasn't in our candidate inputs map, this will potentially cause desync."; + // TODO: consider fatal + } + else + { + // Populate the input content into the bufmap. - candidate_user_input &cand_input = itr->second; + candidate_user_input &cand_input = itr->second; - std::string inputtofeed; - inputtofeed.swap(cand_input.input); + std::string inputtofeed; + inputtofeed.swap(cand_input.input); - proc::contract_iobuf_pair &bufpair = bufmap[cand_input.userpubkey]; - bufpair.inputs.push_back(std::move(inputtofeed)); + proc::contract_iobuf_pair &bufpair = bufmap[cand_input.userpubkey]; + bufpair.inputs.push_back(std::move(inputtofeed)); - // Remove the input from the candidate set because we no longer need it. - //LOG_DBG << "candidate input deleted."; - ctx.candidate_user_inputs.erase(itr); + // Remove the input from the candidate set because we no longer need it. + //LOG_DBG << "candidate input deleted."; + ctx.candidate_user_inputs.erase(itr); + } } } -} -/** + /** * Reads any outputs the contract has produced on the provided buf map and transfers them to candidate outputs * for the next consensus round. * @param bufmap The contract bufmap containing the outputs produced by the contract. */ -void extract_user_outputs_from_contract_bufmap(proc::contract_bufmap_t &bufmap) -{ - for (auto &[pubkey, bufpair] : bufmap) + void extract_user_outputs_from_contract_bufmap(proc::contract_bufmap_t &bufmap) { - if (!bufpair.output.empty()) + for (auto &[pubkey, bufpair] : bufmap) { - std::string output; - output.swap(bufpair.output); + if (!bufpair.output.empty()) + { + std::string output; + output.swap(bufpair.output); - const std::string hash = crypto::get_hash(pubkey, output); - ctx.candidate_user_outputs.try_emplace( - std::move(hash), - candidate_user_output(pubkey, std::move(output))); + const std::string hash = crypto::get_hash(pubkey, output); + ctx.candidate_user_outputs.try_emplace( + std::move(hash), + candidate_user_output(pubkey, std::move(output))); + } } } -} -void broadcast_npl_output(std::string &output) -{ - if (!output.empty()) + void broadcast_npl_output(std::string &output) { - p2p::npl_message npl; - npl.data.swap(output); + if (!output.empty()) + { + p2p::npl_message npl; + npl.data.swap(output); - flatbuffers::FlatBufferBuilder fbuf(1024); - p2pmsg::create_msg_from_npl_output(fbuf, npl, ctx.lcl); - p2p::broadcast_message(fbuf, false); + flatbuffers::FlatBufferBuilder fbuf(1024); + p2pmsg::create_msg_from_npl_output(fbuf, npl, ctx.lcl); + p2p::broadcast_message(fbuf, false); + } } -} -/** + /** * Executes the smart contract with the specified time and provided I/O buf maps. * @param time_now The time that must be passed on to the contract. * @param useriobufmap The contract bufmap which holds user I/O buffers. */ -void run_contract_binary(const int64_t time_now, proc::contract_bufmap_t &useriobufmap, proc::contract_iobuf_pair &nplbufpair, proc::contract_fblockmap_t &state_updates) -{ - // todo:implement exchange of hpsc bufs - proc::contract_iobuf_pair hpscbufpair; - proc::exec_contract( - proc::contract_exec_args(time_now, useriobufmap, nplbufpair, hpscbufpair, state_updates)); -} + int run_contract_binary(const int64_t time_now, proc::contract_bufmap_t &useriobufmap, proc::contract_iobuf_pair &nplbufpair) + { + // todo:implement exchange of hpsc bufs + proc::contract_iobuf_pair hpscbufpair; + return proc::exec_contract( + proc::contract_exec_args(time_now, useriobufmap, nplbufpair, hpscbufpair), + ctx.curr_state_hash); + } -/** + /** * Increment voting table counter. * @param counter The counter map in which a vote should be incremented. * @param candidate The candidate whose vote should be increased by 1. */ -template -void increment(std::map &counter, const T &candidate) -{ - if (counter.count(candidate)) - counter[candidate]++; - else - counter.try_emplace(candidate, 1); -} + template + void increment(std::map &counter, const T &candidate) + { + if (counter.count(candidate)) + counter[candidate]++; + else + counter.try_emplace(candidate, 1); + } } // namespace cons diff --git a/src/cons/cons.hpp b/src/cons/cons.hpp index 71cbf13d..c84b621d 100644 --- a/src/cons/cons.hpp +++ b/src/cons/cons.hpp @@ -6,6 +6,7 @@ #include "../proc.hpp" #include "../p2p/p2p.hpp" #include "../usr/user_input.hpp" +#include "../hpfs/h32.hpp" #include "ledger_handler.hpp" #include "state_handler.hpp" @@ -73,8 +74,7 @@ struct consensus_context uint64_t time_now = 0; std::string lcl; uint64_t led_seq_no = 0; - std::string curr_hash_state; - std::string prev_hash_state; + hpfs::h32 curr_state_hash; //Map of closed ledgers(only lrdgername[sequnece_number-hash], state hash) with sequence number as map key. //contains closed ledgers from latest to latest - MAX_LEDGER_SEQUENCE. @@ -88,11 +88,6 @@ struct consensus_context uint16_t stage_time = 0; // Time allocated to a consensus stage. uint16_t stage_reset_wait_threshold = 0; // Minimum stage wait time to reset the stage. - bool is_state_syncing = false; - std::string state_sync_lcl; - std::thread state_syncing_thread; - std::mutex state_syncing_mutex; - bool is_shutting_down = false; consensus_context() @@ -108,7 +103,7 @@ struct vote_counter std::map users; std::map inputs; std::map outputs; - std::map state; + std::map state; }; extern consensus_context ctx; @@ -117,7 +112,9 @@ int init(); void deinit(); -void consensus(); +int run_consensus(); + +int consensus(); void purify_candidate_proposals(); @@ -145,7 +142,7 @@ uint64_t get_ledger_time_resolution(const uint64_t time); uint64_t get_stage_time_resolution(const uint64_t time); -void apply_ledger(const p2p::proposal &proposal); +int apply_ledger(const p2p::proposal &proposal); void dispatch_user_outputs(const p2p::proposal &cons_prop); @@ -157,7 +154,7 @@ void extract_user_outputs_from_contract_bufmap(proc::contract_bufmap_t &bufmap); void broadcast_npl_output(std::string &output); -void run_contract_binary(const int64_t time_now, proc::contract_bufmap_t &useriobufmap, proc::contract_iobuf_pair &nplbufpair, proc::contract_fblockmap_t &state_updates); +int run_contract_binary(const int64_t time_now, proc::contract_bufmap_t &useriobufmap, proc::contract_iobuf_pair &nplbufpair); template void increment(std::map &counter, const T &candidate); diff --git a/src/cons/ledger_handler.cpp b/src/cons/ledger_handler.cpp index 5d7b8081..12c14367 100644 --- a/src/cons/ledger_handler.cpp +++ b/src/cons/ledger_handler.cpp @@ -61,7 +61,7 @@ const std::tuple save_ledger(const p2p::proposal &p ledger_cache_entry c; c.lcl = file_name; - c.state = proposal.curr_hash_state; + c.state = proposal.curr_state_hash.to_string_view(); cons::ctx.ledger_cache.emplace(led_seq_no, std::move(c)); //Remove old ledgers that exceeds max sequence range. diff --git a/src/cons/state_handler.cpp b/src/cons/state_handler.cpp index 51714f8a..40cc7b98 100644 --- a/src/cons/state_handler.cpp +++ b/src/cons/state_handler.cpp @@ -5,7 +5,6 @@ #include "../p2p/p2p.hpp" #include "../pchheader.hpp" #include "../cons/cons.hpp" -#include "../statefs/state_store.hpp" #include "../hplog.hpp" #include "../util.hpp" @@ -24,7 +23,7 @@ std::list candidate_state_responses; std::list pending_requests; // List of submitted requests we are awaiting responses for, keyed by expected response hash. -std::unordered_map submitted_requests; +std::unordered_map submitted_requests; /** * Sends a state request to a random peer. @@ -33,7 +32,7 @@ std::unordered_map submit * @param block_id The requested block id. Only relevant if requesting a file block. Otherwise -1. * @param expected_hash The expected hash of the requested data. The peer will ignore the request if their hash is different. */ -void request_state_from_peer(const std::string &path, const bool is_file, const int32_t block_id, const hasher::B2H expected_hash) +void request_state_from_peer(const std::string &path, const bool is_file, const int32_t block_id, const hpfs::h32 expected_hash) { p2p::state_request sr; sr.parent_path = path; @@ -59,8 +58,8 @@ int create_state_response(flatbuffers::FlatBufferBuilder &fbuf, const p2p::state // Vector to hold the block bytes. Normally block size is constant BLOCK_SIZE (4MB), but the // last block of a file may have a smaller size. std::vector block; - if (statefs::get_block(block, sr.parent_path, sr.block_id, sr.expected_hash) == -1) - return -1; + + // TODO: get block p2p::block_response resp; resp.path = sr.parent_path; @@ -76,17 +75,19 @@ int create_state_response(flatbuffers::FlatBufferBuilder &fbuf, const p2p::state if (sr.is_file) { std::vector existing_block_hashmap; - if (statefs::get_block_hash_map(existing_block_hashmap, sr.parent_path, sr.expected_hash) == -1) - return -1; + + // TODO: get block hash list + // TODO: get file length + std::size_t file_length = 0; - fbschema::p2pmsg::create_msg_from_filehashmap_response(fbuf, sr.parent_path, existing_block_hashmap, statefs::get_file_length(sr.parent_path), sr.expected_hash, ctx.lcl); + fbschema::p2pmsg::create_msg_from_filehashmap_response(fbuf, sr.parent_path, existing_block_hashmap, file_length, sr.expected_hash, ctx.lcl); } else { // If the state request is for a directory we need to reply with the file system entries and their hashes inside that dir. std::unordered_map existing_fs_entries; - if (statefs::get_fs_entry_hashes(existing_fs_entries, sr.parent_path, sr.expected_hash) == -1) - return -1; + + // TODO: get fs entry hashes fbschema::p2pmsg::create_msg_from_fsentry_response(fbuf, sr.parent_path, existing_fs_entries, sr.expected_hash, ctx.lcl); } @@ -100,7 +101,7 @@ int create_state_response(flatbuffers::FlatBufferBuilder &fbuf, const p2p::state * @param state_hash_to_request Peer's expected state hash. If peer doesn't have this as its state hash the * request will be ignord. */ -void start_state_sync(const hasher::B2H state_hash_to_request) +void start_state_sync(const hpfs::h32 state_hash_to_request) { { std::lock_guard lock(p2p::ctx.collected_msgs.state_response_mutex); @@ -108,7 +109,6 @@ void start_state_sync(const hasher::B2H state_hash_to_request) } { - std::lock_guard lock(cons::ctx.state_syncing_mutex); candidate_state_responses.clear(); pending_requests.clear(); submitted_requests.clear(); @@ -133,8 +133,6 @@ int run_state_sync_iterator() util::sleep(SYNC_LOOP_WAIT); // TODO: Also bypass peer session handler state responses if we're not syncing. - if (!ctx.is_state_syncing) - continue; { std::lock_guard lock(p2p::ctx.collected_msgs.state_response_mutex); @@ -144,8 +142,6 @@ int run_state_sync_iterator() candidate_state_responses.splice(candidate_state_responses.end(), p2p::ctx.collected_msgs.state_response); } - std::lock_guard lock(cons::ctx.state_syncing_mutex); - for (auto &response : candidate_state_responses) { if (ctx.is_shutting_down) @@ -155,7 +151,7 @@ int run_state_sync_iterator() const fbschema::p2pmsg::State_Response_Message *resp_msg = content->message_as_State_Response_Message(); // Check whether we are actually waiting for this response's hash. If not, ignore it. - hasher::B2H response_hash = fbschema::flatbuff_bytes_to_hash(resp_msg->hash()); + hpfs::h32 response_hash = fbschema::flatbuff_bytes_to_hash(resp_msg->hash()); const auto pending_resp_itr = submitted_requests.find(response_hash); if (pending_resp_itr == submitted_requests.end()) continue; @@ -250,15 +246,17 @@ int handle_fs_entry_response(const fbschema::p2pmsg::Fs_Entry_Response *fs_entry std::string_view root_path_sv = fbschema::flatbuff_str_to_sv(fs_entry_resp->path()); std::string root_path_str(root_path_sv.data(), root_path_sv.size()); - if (!statefs::is_dir_exists(root_path_str)) - { - statefs::create_dir(root_path_str); - } - else - { - if (statefs::get_fs_entry_hashes(existing_fs_entries, std::move(root_path_str), hasher::B2H_empty) == -1) - return -1; - } + // TODO: Create state path dir if not exist. + // TODO: Get existing fs entries hash map. + // if (!statefs::is_dir_exists(root_path_str)) + // { + // statefs::create_dir(root_path_str); + // } + // else + // { + // if (statefs::get_fs_entry_hashes(existing_fs_entries, std::move(root_path_str), hpfs::h32_empty) == -1) + // return -1; + // } // Request more info on fs entries that exist on both sides but are different. for (const auto &[path, fs_entry] : existing_fs_entries) @@ -281,13 +279,13 @@ int handle_fs_entry_response(const fbschema::p2pmsg::Fs_Entry_Response *fs_entry // If there was an entry that does not exist on other side, delete it from this node. if (fs_entry.is_file) { - if (statefs::delete_file(path) == -1) - return -1; + //if (statefs::delete_file(path) == -1) + // return -1; } else { - if (statefs::delete_dir(path) == -1) - return -1; + //if (statefs::delete_dir(path) == -1) + // return -1; } } } @@ -310,14 +308,14 @@ int handle_file_hashmap_response(const fbschema::p2pmsg::File_HashMap_Response * const std::string path_str(path_sv.data(), path_sv.size()); std::vector existing_block_hashmap; - if (statefs::get_block_hash_map(existing_block_hashmap, path_str, hasher::B2H_empty) == -1) - return -1; + //if (statefs::get_block_hash_map(existing_block_hashmap, path_str, hpfs::h32_empty) == -1) + // return -1; - const hasher::B2H *existing_hashes = reinterpret_cast(existing_block_hashmap.data()); - auto existing_hash_count = existing_block_hashmap.size() / hasher::HASH_SIZE; + const hpfs::h32 *existing_hashes = reinterpret_cast(existing_block_hashmap.data()); + auto existing_hash_count = existing_block_hashmap.size() / sizeof(hpfs::h32); - const hasher::B2H *resp_hashes = reinterpret_cast(file_resp->hash_map()->data()); - auto resp_hash_count = file_resp->hash_map()->size() / hasher::HASH_SIZE; + const hpfs::h32 *resp_hashes = reinterpret_cast(file_resp->hash_map()->data()); + auto resp_hash_count = file_resp->hash_map()->size() / sizeof(hpfs::h32); auto insert_itr = pending_requests.begin(); @@ -335,8 +333,8 @@ int handle_file_hashmap_response(const fbschema::p2pmsg::File_HashMap_Response * if (existing_hash_count > resp_hash_count) { - if (statefs::truncate_file(path_str, file_resp->file_length()) == -1) - return -1; + //if (statefs::truncate_file(path_str, file_resp->file_length()) == -1) + // return -1; } else if (existing_hash_count < resp_hash_count) { @@ -354,8 +352,8 @@ int handle_file_block_response(const fbschema::p2pmsg::Block_Response *block_msg { p2p::block_response block_resp = fbschema::p2pmsg::create_block_response_from_msg(*block_msg); - if (statefs::write_block(block_resp.path, block_resp.block_id, block_resp.data.data(), block_resp.data.size()) == -1) - return -1; + //if (statefs::write_block(block_resp.path, block_resp.block_id, block_resp.data.data(), block_resp.data.size()) == -1) + // return -1; return 0; } diff --git a/src/cons/state_handler.hpp b/src/cons/state_handler.hpp index 01955255..47963fd3 100644 --- a/src/cons/state_handler.hpp +++ b/src/cons/state_handler.hpp @@ -4,7 +4,7 @@ #include "../pchheader.hpp" #include "../p2p/p2p.hpp" #include "../fbschema/p2pmsg_content_generated.h" -#include "../statefs/hasher.hpp" +#include "../hpfs/h32.hpp" namespace cons { @@ -22,7 +22,7 @@ struct backlog_item BACKLOG_ITEM_TYPE type; std::string path; int32_t block_id = -1; // Only relevant if type=BLOCK - hasher::B2H expected_hash; + hpfs::h32 expected_hash; // No. of cycles that this item has been waiting in pending state. // Used by pending_responses list to increase wait count. @@ -33,9 +33,9 @@ extern std::list candidate_state_responses; int create_state_response(flatbuffers::FlatBufferBuilder &fbuf, const p2p::state_request &sr); -void request_state_from_peer(const std::string &path, const bool is_file, const int32_t block_id, const hasher::B2H expected_hash); +void request_state_from_peer(const std::string &path, const bool is_file, const int32_t block_id, const hpfs::h32 expected_hash); -void start_state_sync(const hasher::B2H state_hash_to_request); +void start_state_sync(const hpfs::h32 state_hash_to_request); int run_state_sync_iterator(); diff --git a/src/fbschema/common_helpers.cpp b/src/fbschema/common_helpers.cpp index 51a6e73e..0008f5bf 100644 --- a/src/fbschema/common_helpers.cpp +++ b/src/fbschema/common_helpers.cpp @@ -3,124 +3,124 @@ namespace fbschema { -//---Conversion helpers from flatbuffers data types to std data types---// + //---Conversion helpers from flatbuffers data types to std data types---// -/** + /** * Returns string_view from flat buffer data pointer and length. */ -std::string_view flatbuff_bytes_to_sv(const uint8_t *data, const flatbuffers::uoffset_t length) -{ - const char *signature_content_str = reinterpret_cast(data); - return std::string_view(signature_content_str, length); -} + std::string_view flatbuff_bytes_to_sv(const uint8_t *data, const flatbuffers::uoffset_t length) + { + const char *signature_content_str = reinterpret_cast(data); + return std::string_view(signature_content_str, length); + } -/** + /** * Returns string_view from Flat Buffer vector of bytes. */ -std::string_view flatbuff_bytes_to_sv(const flatbuffers::Vector *buffer) -{ - return flatbuff_bytes_to_sv(buffer->Data(), buffer->size()); -} + std::string_view flatbuff_bytes_to_sv(const flatbuffers::Vector *buffer) + { + return flatbuff_bytes_to_sv(buffer->Data(), buffer->size()); + } -/** + /** * Returns return string_view from Flat Buffer string. */ -std::string_view flatbuff_str_to_sv(const flatbuffers::String *buffer) -{ - return flatbuff_bytes_to_sv(buffer->Data(), buffer->size()); -} + std::string_view flatbuff_str_to_sv(const flatbuffers::String *buffer) + { + return flatbuff_bytes_to_sv(buffer->Data(), buffer->size()); + } -/** + /** * Returns hash from Flat Buffer vector of bytes. */ -hasher::B2H flatbuff_bytes_to_hash(const flatbuffers::Vector *buffer) -{ - return *reinterpret_cast(buffer->data()); -} + hpfs::h32 flatbuff_bytes_to_hash(const flatbuffers::Vector *buffer) + { + return *reinterpret_cast(buffer->data()); + } -/** + /** * Returns set from Flatbuffer vector of ByteArrays. */ -const std::set flatbuf_bytearrayvector_to_stringlist(const flatbuffers::Vector> *fbvec) -{ - std::set set; - for (auto el : *fbvec) - set.emplace(std::string(flatbuff_bytes_to_sv(el->array()))); - return set; -} + const std::set flatbuf_bytearrayvector_to_stringlist(const flatbuffers::Vector> *fbvec) + { + std::set set; + for (auto el : *fbvec) + set.emplace(std::string(flatbuff_bytes_to_sv(el->array()))); + return set; + } -/** + /** * Returns a map from Flatbuffer vector of key value pairs. */ -const std::unordered_map -flatbuf_pairvector_to_stringmap(const flatbuffers::Vector> *fbvec) -{ - std::unordered_map map; - map.reserve(fbvec->size()); - for (auto el : *fbvec) - map.emplace(flatbuff_bytes_to_sv(el->key()), flatbuff_bytes_to_sv(el->value())); - return map; -} + const std::unordered_map + flatbuf_pairvector_to_stringmap(const flatbuffers::Vector> *fbvec) + { + std::unordered_map map; + map.reserve(fbvec->size()); + for (auto el : *fbvec) + map.emplace(flatbuff_bytes_to_sv(el->key()), flatbuff_bytes_to_sv(el->value())); + return map; + } -//---Conversion helpers from std data types to flatbuffers data types---// -//---These are used in constructing Flatbuffer messages using builders---// + //---Conversion helpers from std data types to flatbuffers data types---// + //---These are used in constructing Flatbuffer messages using builders---// -/** + /** * Returns Flatbuffer bytes vector from string_view. */ -const flatbuffers::Offset> -sv_to_flatbuff_bytes(flatbuffers::FlatBufferBuilder &builder, std::string_view sv) -{ - return builder.CreateVector(reinterpret_cast(sv.data()), sv.size()); -} + const flatbuffers::Offset> + sv_to_flatbuff_bytes(flatbuffers::FlatBufferBuilder &builder, std::string_view sv) + { + return builder.CreateVector(reinterpret_cast(sv.data()), sv.size()); + } -/** + /** * Returns Flatbuffer string from string_view. */ -const flatbuffers::Offset -sv_to_flatbuff_str(flatbuffers::FlatBufferBuilder &builder, std::string_view sv) -{ - return builder.CreateString(sv); -} + const flatbuffers::Offset + sv_to_flatbuff_str(flatbuffers::FlatBufferBuilder &builder, std::string_view sv) + { + return builder.CreateString(sv); + } -/** + /** * Returns Flatbuffer bytes vector from hash. */ -const flatbuffers::Offset> -hash_to_flatbuff_bytes(flatbuffers::FlatBufferBuilder &builder, const hasher::B2H hash) -{ - return builder.CreateVector(reinterpret_cast(&hash), hasher::HASH_SIZE); -} + const flatbuffers::Offset> + hash_to_flatbuff_bytes(flatbuffers::FlatBufferBuilder &builder, const hpfs::h32 hash) + { + return builder.CreateVector(reinterpret_cast(&hash), sizeof(hpfs::h32)); + } -/** + /** * Returns Flatbuffer vector of ByteArrays from given set of strings. */ -const flatbuffers::Offset>> -stringlist_to_flatbuf_bytearrayvector(flatbuffers::FlatBufferBuilder &builder, const std::set &set) -{ - std::vector> fbvec; - fbvec.reserve(set.size()); - for (std::string_view str : set) - fbvec.push_back(CreateByteArray(builder, sv_to_flatbuff_bytes(builder, str))); - return builder.CreateVector(fbvec); -} + const flatbuffers::Offset>> + stringlist_to_flatbuf_bytearrayvector(flatbuffers::FlatBufferBuilder &builder, const std::set &set) + { + std::vector> fbvec; + fbvec.reserve(set.size()); + for (std::string_view str : set) + fbvec.push_back(CreateByteArray(builder, sv_to_flatbuff_bytes(builder, str))); + return builder.CreateVector(fbvec); + } -/** + /** * Returns Flatbuffer vector of key value pairs from given map. */ -const flatbuffers::Offset>> -stringmap_to_flatbuf_bytepairvector(flatbuffers::FlatBufferBuilder &builder, const std::unordered_map &map) -{ - std::vector> fbvec; - fbvec.reserve(map.size()); - for (auto const &[key, value] : map) + const flatbuffers::Offset>> + stringmap_to_flatbuf_bytepairvector(flatbuffers::FlatBufferBuilder &builder, const std::unordered_map &map) { - fbvec.push_back(CreateBytesKeyValuePair( - builder, - sv_to_flatbuff_bytes(builder, key), - sv_to_flatbuff_bytes(builder, value))); + std::vector> fbvec; + fbvec.reserve(map.size()); + for (auto const &[key, value] : map) + { + fbvec.push_back(CreateBytesKeyValuePair( + builder, + sv_to_flatbuff_bytes(builder, key), + sv_to_flatbuff_bytes(builder, value))); + } + return builder.CreateVector(fbvec); } - return builder.CreateVector(fbvec); -} } // namespace fbschema \ No newline at end of file diff --git a/src/fbschema/common_helpers.hpp b/src/fbschema/common_helpers.hpp index dd305cd5..1505f6c3 100644 --- a/src/fbschema/common_helpers.hpp +++ b/src/fbschema/common_helpers.hpp @@ -2,8 +2,8 @@ #define _HP_FBSCHEMA_COMMON_HELPERS_ #include "../pchheader.hpp" +#include "../hpfs/h32.hpp" #include "common_schema_generated.h" -#include "../statefs/hasher.hpp" namespace fbschema { @@ -19,7 +19,7 @@ std::string_view flatbuff_bytes_to_sv(const flatbuffers::Vector *buffer std::string_view flatbuff_str_to_sv(const flatbuffers::String *buffer); -hasher::B2H flatbuff_bytes_to_hash(const flatbuffers::Vector *buffer); +hpfs::h32 flatbuff_bytes_to_hash(const flatbuffers::Vector *buffer); const std::set flatbuf_bytearrayvector_to_stringlist(const flatbuffers::Vector> *fbvec); @@ -36,7 +36,7 @@ const flatbuffers::Offset sv_to_flatbuff_str(flatbuffers::FlatBufferBuilder &builder, std::string_view sv); const flatbuffers::Offset> -hash_to_flatbuff_bytes(flatbuffers::FlatBufferBuilder &builder, hasher::B2H hash); +hash_to_flatbuff_bytes(flatbuffers::FlatBufferBuilder &builder, hpfs::h32 hash); const flatbuffers::Offset>> stringlist_to_flatbuf_bytearrayvector(flatbuffers::FlatBufferBuilder &builder, const std::set &set); diff --git a/src/fbschema/ledger_helpers.cpp b/src/fbschema/ledger_helpers.cpp index 700a73a6..90f814c3 100644 --- a/src/fbschema/ledger_helpers.cpp +++ b/src/fbschema/ledger_helpers.cpp @@ -19,7 +19,7 @@ const std::string_view create_ledger_from_proposal(flatbuffers::FlatBufferBuilde seq_no, p.time, sv_to_flatbuff_bytes(builder, p.lcl), - sv_to_flatbuff_bytes(builder, p.curr_hash_state), + sv_to_flatbuff_bytes(builder, p.curr_state_hash.to_string_view()), stringlist_to_flatbuf_bytearrayvector(builder, p.users), stringlist_to_flatbuf_bytearrayvector(builder, p.hash_inputs), stringlist_to_flatbuf_bytearrayvector(builder, p.hash_outputs)); diff --git a/src/fbschema/p2pmsg_helpers.cpp b/src/fbschema/p2pmsg_helpers.cpp index 358eea04..765fd1a8 100644 --- a/src/fbschema/p2pmsg_helpers.cpp +++ b/src/fbschema/p2pmsg_helpers.cpp @@ -215,7 +215,7 @@ const p2p::proposal create_proposal_from_msg(const Proposal_Message &msg, const p.time = msg.time(); p.stage = msg.stage(); p.lcl = flatbuff_bytes_to_sv(lcl); - p.curr_hash_state = flatbuff_bytes_to_sv(msg.curr_state_hash()); + p.curr_state_hash = flatbuff_bytes_to_sv(msg.curr_state_hash()); if (msg.users()) p.users = flatbuf_bytearrayvector_to_stringlist(msg.users()); @@ -365,7 +365,7 @@ void create_msg_from_proposal(flatbuffers::FlatBufferBuilder &container_builder, stringlist_to_flatbuf_bytearrayvector(builder, p.users), stringlist_to_flatbuf_bytearrayvector(builder, p.hash_inputs), stringlist_to_flatbuf_bytearrayvector(builder, p.hash_outputs), - sv_to_flatbuff_bytes(builder, p.curr_hash_state)); + sv_to_flatbuff_bytes(builder, p.curr_state_hash.to_string_view())); const flatbuffers::Offset message = CreateContent(builder, Message_Proposal_Message, proposal.Union()); builder.Finish(message); // Finished building message content to get serialised content. @@ -477,7 +477,7 @@ void create_msg_from_state_request(flatbuffers::FlatBufferBuilder &container_bui * @param expected_hash The exptected hash of the requested path. * @param lcl Lcl to be include in the container msg. */ -void create_msg_from_fsentry_response(flatbuffers::FlatBufferBuilder &container_builder, const std::string_view path, std::unordered_map &fs_entries, hasher::B2H expected_hash, std::string_view lcl) +void create_msg_from_fsentry_response(flatbuffers::FlatBufferBuilder &container_builder, const std::string_view path, std::unordered_map &fs_entries, hpfs::h32 expected_hash, std::string_view lcl) { flatbuffers::FlatBufferBuilder builder(1024); @@ -507,7 +507,7 @@ void create_msg_from_fsentry_response(flatbuffers::FlatBufferBuilder &container_ * @param hashmap Hashmap of the file * @param lcl Lcl to be include in the container msg. */ -void create_msg_from_filehashmap_response(flatbuffers::FlatBufferBuilder &container_builder, std::string_view path, std::vector &hashmap, std::size_t file_length, hasher::B2H expected_hash, std::string_view lcl) +void create_msg_from_filehashmap_response(flatbuffers::FlatBufferBuilder &container_builder, std::string_view path, std::vector &hashmap, std::size_t file_length, hpfs::h32 expected_hash, std::string_view lcl) { // todo:get a average propsal message size and allocate content builder based on that. flatbuffers::FlatBufferBuilder builder(1024); diff --git a/src/fbschema/p2pmsg_helpers.hpp b/src/fbschema/p2pmsg_helpers.hpp index 45986a18..9e789418 100644 --- a/src/fbschema/p2pmsg_helpers.hpp +++ b/src/fbschema/p2pmsg_helpers.hpp @@ -5,7 +5,7 @@ #include "p2pmsg_container_generated.h" #include "p2pmsg_content_generated.h" #include "../p2p/p2p.hpp" -#include "../statefs/hasher.hpp" +#include "../hpfs/h32.hpp" namespace fbschema::p2pmsg { @@ -57,9 +57,9 @@ void create_msg_from_npl_output(flatbuffers::FlatBufferBuilder &container_builde void create_msg_from_state_request(flatbuffers::FlatBufferBuilder &container_builder, const p2p::state_request &hr, std::string_view lcl); void create_msg_from_fsentry_response(flatbuffers::FlatBufferBuilder &container_builder, const std::string_view path, - std::unordered_map &fs_entries, hasher::B2H expected_hash, std::string_view lcl); + std::unordered_map &fs_entries, hpfs::h32 expected_hash, std::string_view lcl); -void create_msg_from_filehashmap_response(flatbuffers::FlatBufferBuilder &container_builder, std::string_view path, std::vector &hashmap, std::size_t file_length, hasher::B2H expected_hash, std::string_view lcl); +void create_msg_from_filehashmap_response(flatbuffers::FlatBufferBuilder &container_builder, std::string_view path, std::vector &hashmap, std::size_t file_length, hpfs::h32 expected_hash, std::string_view lcl); void create_msg_from_block_response(flatbuffers::FlatBufferBuilder &container_builder, p2p::block_response &block_resp, std::string_view lcl); diff --git a/src/hpfs/h32.cpp b/src/hpfs/h32.cpp new file mode 100644 index 00000000..78c07e1c --- /dev/null +++ b/src/hpfs/h32.cpp @@ -0,0 +1,74 @@ +#include "h32.hpp" + +/** + * Based on https://github.com/codetsunami/file-ptracer/blob/master/merkle.cpp + */ +namespace hpfs +{ + /** + * Helper functions for working with 32 byte hash type h32. + */ + + h32 h32_empty; + + bool h32::operator==(const h32 rhs) const + { + return this->data[0] == rhs.data[0] && this->data[1] == rhs.data[1] && this->data[2] == rhs.data[2] && this->data[3] == rhs.data[3]; + } + + bool h32::operator!=(const h32 rhs) const + { + return this->data[0] != rhs.data[0] || this->data[1] != rhs.data[1] || this->data[2] != rhs.data[2] || this->data[3] != rhs.data[3]; + } + + void h32::operator^=(const h32 rhs) + { + this->data[0] ^= rhs.data[0]; + this->data[1] ^= rhs.data[1]; + this->data[2] ^= rhs.data[2]; + this->data[3] ^= rhs.data[3]; + } + + std::string_view h32::to_string_view() const + { + return std::string_view(reinterpret_cast(this), sizeof(hpfs::h32)); + } + + h32 &h32::operator=(std::string_view sv) + { + memcpy(this->data, sv.data(), sizeof(h32)); + return *this; + } + + // Comparison operator for std::map key support. + bool h32::operator<(const h32 rhs) const + { + // Here we do not actually care about true comparison value. + // We just need the comparison to return consistent result based on + // a fixed criteria. + return this->data[0] < rhs.data[0]; + } + + std::ostream &operator<<(std::ostream &output, const h32 &h) + { + const uint8_t *buf = reinterpret_cast(&h); + for (int i = 0; i < 8; i++) + output << std::hex << std::setfill('0') << std::setw(2) << (int)buf[i]; + + return output; + } + + // Helper class to support std::map/std::unordered_map custom hashing function. + // This is needed to use B2H as the std map container key. + size_t h32_std_key_hasher::operator()(const h32 h) const + { + // Compute individual hash values. http://stackoverflow.com/a/1646913/126995 + size_t res = 17; + res = res * 31 + std::hash()(h.data[0]); + res = res * 31 + std::hash()(h.data[1]); + res = res * 31 + std::hash()(h.data[2]); + res = res * 31 + std::hash()(h.data[3]); + return res; + } + +} // namespace hpfs \ No newline at end of file diff --git a/src/hpfs/h32.hpp b/src/hpfs/h32.hpp new file mode 100644 index 00000000..8a75edaf --- /dev/null +++ b/src/hpfs/h32.hpp @@ -0,0 +1,36 @@ +#ifndef _HP_HPFS_H32_ +#define _HP_HPFS_H32_ + +#include "../pchheader.hpp" + +namespace hpfs +{ + + // blake2b hash is 32 bytes which we store as 4 quad words + // Originally from https://github.com/codetsunami/file-ptracer/blob/master/merkle.cpp + struct h32 + { + uint64_t data[4]; + + bool operator==(const h32 rhs) const; + bool operator!=(const h32 rhs) const; + void operator^=(const h32 rhs); + std::string_view to_string_view() const; + h32 &operator=(std::string_view sv); + bool operator<(const h32 rhs) const; + }; + extern h32 h32_empty; + + std::ostream &operator<<(std::ostream &output, const h32 &h); + + // Helper class to support std::map/std::unordered_map custom hashing function. + // This is needed to use B2H as the std map container key. + class h32_std_key_hasher + { + public: + size_t operator()(const h32 h) const; + }; + +} // namespace hpfs + +#endif \ No newline at end of file diff --git a/src/hpfs/hpfs.cpp b/src/hpfs/hpfs.cpp new file mode 100644 index 00000000..20752804 --- /dev/null +++ b/src/hpfs/hpfs.cpp @@ -0,0 +1,178 @@ +#include "hpfs.hpp" +#include "h32.hpp" +#include "../conf.hpp" +#include "../hplog.hpp" +#include "../util.hpp" + +namespace hpfs +{ + pid_t merge_pid = 0; + + bool init_success = false; + + int init() + { + LOG_INFO << "Starting hpfs merge process..."; + if (start_merge_process() == -1) + return -1; + + LOG_INFO << "Started hpfs merge process. pid:" << merge_pid; + init_success = true; + return 0; + } + + void deinit() + { + if (init_success) + { + LOG_INFO << "Stopping hpfs merge process... pid:" << merge_pid; + if (merge_pid > 0 && util::kill_process(merge_pid, true) == 0) + LOG_INFO << "Stopped hpfs merge process."; + } + } + + int start_merge_process() + { + const pid_t pid = fork(); + + if (pid > 0) + { + // HotPocket process. + // Check if process is still running. + util::sleep(20); + if (kill(pid, 0) == -1) + return -1; + + merge_pid = pid; + } + else if (pid == 0) + { + // hpfs process. + // Fill process args. + char *execv_args[] = { + conf::ctx.hpfs_exe_path.data(), + (char *)"merge", + conf::ctx.state_dir.data(), + NULL}; + + const int ret = execv(execv_args[0], execv_args); + LOG_ERR << errno << ": hpfs merge process execv failed."; + exit(1); + } + else + { + LOG_ERR << errno << ": fork() failed when starting hpfs merge process."; + return -1; + } + + return 0; + } + + int start_fs_session(pid_t &session_pid, std::string &mount_dir, + const char *mode, const bool hash_map_enabled) + { + const pid_t pid = fork(); + + if (pid > 0) + { + // HotPocket process. + + // If the mound dir is not specified, assign a mount dir based on hpfs process id. + if (mount_dir.empty()) + mount_dir = std::string(conf::ctx.state_dir) + .append("/") + .append(std::to_string(pid)); + + // Wait until hpfs is initialized properly. + bool hpfs_initialized = false; + uint8_t retry_count = 0; + do + { + util::sleep(20); + + // Check if process is still running. + if (kill(pid, 0) == -1) + break; + + // If hpfs is initialized, the inode no. of the mounted root dir is always 1. + struct stat st; + hpfs_initialized = (stat(mount_dir.c_str(), &st) == 0 && st.st_ino == 1); + + } while (!hpfs_initialized && ++retry_count < 100); + + // Kill the process if hpfs couldn't be initialized after the wait period. + if (!hpfs_initialized) + { + LOG_ERR << "Couldn't initialize hpfs session."; + util::kill_process(pid, true); + return -1; + } + + session_pid = pid; + } + else if (pid == 0) + { + // hpfs process. + + // If the mound dir is not specified, assign a mount dir based on hpfs process id. + const pid_t self_pid = getpid(); + if (mount_dir.empty()) + mount_dir = std::string(conf::ctx.state_dir) + .append("/") + .append(std::to_string(self_pid)); + + // Fill process args. + char *execv_args[] = { + conf::ctx.hpfs_exe_path.data(), + (char *)mode, // hpfs mode: rw | ro + conf::ctx.state_dir.data(), + mount_dir.data(), + (char *)(hash_map_enabled ? "hmap=true" : "hmap-false"), + NULL}; + + const int ret = execv(execv_args[0], execv_args); + LOG_ERR << errno << ": hpfs session process execv failed."; + exit(1); + } + else + { + LOG_ERR << errno << ": fork() failed when starting hpfs session process."; + return -1; + } + + return 0; + } + + int get_root_hash(h32 &hash) + { + pid_t pid; + std::string mount_dir; + if (start_fs_session(pid, mount_dir, "ro", true) == -1) + return -1; + + int res = get_hash(hash, mount_dir, "/"); + util::kill_process(pid, true); + + return res; + } + + int get_hash(h32 &hash, const std::string_view mount_dir, const std::string_view vpath) + { + std::string path = std::string(mount_dir).append(vpath).append("::hpfs.hmap.hash"); + int fd = open(path.c_str(), O_RDONLY); + if (fd == -1) + { + LOG_ERR << errno << ": Error opening hash file."; + return -1; + } + int res = read(fd, &hash, sizeof(h32)); + close(fd); + if (res == -1) + { + LOG_ERR << errno << ": Error reading hash file."; + return -1; + } + return 0; + } + +} // namespace hpfs \ No newline at end of file diff --git a/src/hpfs/hpfs.hpp b/src/hpfs/hpfs.hpp new file mode 100644 index 00000000..41361b84 --- /dev/null +++ b/src/hpfs/hpfs.hpp @@ -0,0 +1,18 @@ +#ifndef _HP_HPFS_HPFS_ +#define _HP_HPFS_HPFS_ + +#include "../pchheader.hpp" +#include "h32.hpp" + +namespace hpfs +{ + int init(); + void deinit(); + int start_merge_process(); + int start_fs_session(pid_t &session_pid, std::string &mount_dir, + const char *mode, const bool hash_map_enabled); + int get_root_hash(h32 &hash); + int get_hash(h32 &hash, const std::string_view mount_dir, const std::string_view vpath); +} // namespace hpfs + +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 58f0e463..bc718766 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,7 +11,7 @@ #include "usr/usr.hpp" #include "p2p/p2p.hpp" #include "cons/cons.hpp" -#include "statefs/state_common.hpp" +#include "hpfs/hpfs.hpp" /** * Parses CLI args and extracts hot pocket command and parameters given. @@ -68,6 +68,7 @@ void deinit() p2p::deinit(); cons::deinit(); proc::deinit(); + hpfs::deinit(); hplog::deinit(); } @@ -82,18 +83,18 @@ void signal_handler(int signum) namespace boost { -inline void assertion_failed_msg(char const *expr, char const *msg, char const *function, char const * /*file*/, long /*line*/) -{ - LOG_ERR << "Expression '" << expr << "' is false in function '" << function << "': " << (msg ? msg : "<...>") << ".\n" - << "Backtrace:\n" - << boost::stacktrace::stacktrace() << '\n'; - std::abort(); -} + inline void assertion_failed_msg(char const *expr, char const *msg, char const *function, char const * /*file*/, long /*line*/) + { + LOG_ERR << "Expression '" << expr << "' is false in function '" << function << "': " << (msg ? msg : "<...>") << ".\n" + << "Backtrace:\n" + << boost::stacktrace::stacktrace() << '\n'; + std::abort(); + } -inline void assertion_failed(char const *expr, char const *function, char const *file, long line) -{ - ::boost::assertion_failed_msg(expr, 0 /*nullptr*/, function, file, line); -} + inline void assertion_failed(char const *expr, char const *function, char const *file, long line) + { + ::boost::assertion_failed_msg(expr, 0 /*nullptr*/, function, file, line); + } } // namespace boost /** @@ -187,18 +188,22 @@ int main(int argc, char **argv) LOG_INFO << "Operating mode: " << (conf::cfg.startup_mode == conf::OPERATING_MODE::OBSERVER ? "Observer" : "Proposer"); - statefs::init(conf::ctx.state_hist_dir); - - if (p2p::init() != 0 || usr::init() != 0 || cons::init() != 0) + if (hpfs::init() != 0 || p2p::init() != 0 || usr::init() != 0 || cons::init() != 0) + { + deinit(); return -1; + } // After initializing primary subsystems, register the SIGINT handler. signal(SIGINT, signal_handler); - while (true) - cons::consensus(); + if (cons::run_consensus() == -1) + { + LOG_ERR << "Error occured in consensus."; + deinit(); + return -1; + } - // Free resources. deinit(); } } diff --git a/src/p2p/p2p.cpp b/src/p2p/p2p.cpp index dc349a76..e11af096 100644 --- a/src/p2p/p2p.cpp +++ b/src/p2p/p2p.cpp @@ -11,213 +11,220 @@ namespace p2p { -// Holds global connected-peers and related objects. -connected_context ctx; + // Holds global connected-peers and related objects. + connected_context ctx; -/** + bool init_success = false; + + /** * Initializes the p2p subsystem. Must be called once during application startup. * @return 0 for successful initialization. -1 for failure. */ -int init() -{ - //Entry point for p2p which will start peer connections to other nodes - return start_peer_connections(); -} + int init() + { + //Entry point for p2p which will start peer connections to other nodes + if (start_peer_connections() == -1) + return -1; -/** + init_success = true; + return 0; + } + + /** * Cleanup any running processes. */ -void deinit() -{ - ctx.listener.stop(); -} - -int start_peer_connections() -{ - const uint64_t metric_thresholds[] = {conf::cfg.peermaxcpm, conf::cfg.peermaxdupmpm, conf::cfg.peermaxbadsigpm, conf::cfg.peermaxbadmpm}; - if (ctx.listener.start( - conf::cfg.peerport, ".sock-peer", comm::SESSION_TYPE::PEER, true, false, metric_thresholds, conf::cfg.peers, conf::cfg.peermaxsize) == -1) - return -1; - - LOG_INFO << "Started listening for peer connections on " << std::to_string(conf::cfg.peerport); - return 0; -} - -int resolve_peer_challenge(comm::comm_session &session, const peer_challenge_response &challenge_resp) -{ - - // Compare the response challenge string with the original issued challenge. - if (session.issued_challenge != challenge_resp.challenge) + void deinit() { - LOG_DBG << "Peer challenge response, challenge invalid."; - return -1; + if (init_success) + ctx.listener.stop(); } - // Verify the challenge signature. - if (crypto::verify( - challenge_resp.challenge, - challenge_resp.signature, - challenge_resp.pubkey) != 0) + int start_peer_connections() { - LOG_DBG << "Peer challenge response signature verification failed."; - return -1; - } + const uint64_t metric_thresholds[] = {conf::cfg.peermaxcpm, conf::cfg.peermaxdupmpm, conf::cfg.peermaxbadsigpm, conf::cfg.peermaxbadmpm}; + if (ctx.listener.start( + conf::cfg.peerport, ".sock-peer", comm::SESSION_TYPE::PEER, true, false, metric_thresholds, conf::cfg.peers, conf::cfg.peermaxsize) == -1) + return -1; - // Converting the binary pub key into hexa decimal string this will be used as the key in storing peer sessions - std::string pubkeyhex; - util::bin2hex(pubkeyhex, reinterpret_cast(challenge_resp.pubkey.data()), challenge_resp.pubkey.length()); - - const int res = challenge_resp.pubkey.compare(conf::cfg.pubkey); - - // If pub key is same as our (self) pub key, then this is the loopback connection to ourselves. - // Hence we must keep the connection but only one of two sessions must be added to peer_connections. - // If pub key is greater than our id (< 0), then we should give priority to any existing inbound connection - // from the same peer and drop the outbound connection. - // If pub key is lower than our id (> 0), then we should give priority to any existing outbound connection - // from the same peer and drop the inbound connection. - - std::lock_guard lock(ctx.peer_connections_mutex); - - const auto iter = p2p::ctx.peer_connections.find(pubkeyhex); - if (iter == p2p::ctx.peer_connections.end()) - { - // Add the new connection straight away, if we haven't seen it before. - session.uniqueid.swap(pubkeyhex); - session.challenge_status = comm::CHALLENGE_VERIFIED; - p2p::ctx.peer_connections.try_emplace(session.uniqueid, &session); + LOG_INFO << "Started listening for peer connections on " << std::to_string(conf::cfg.peerport); return 0; } - else if (res == 0) // New connection is self (There can be two sessions for self (inbound/outbound)) + + int resolve_peer_challenge(comm::comm_session &session, const peer_challenge_response &challenge_resp) { - session.is_self = true; - session.uniqueid.swap(pubkeyhex); - session.challenge_status = comm::CHALLENGE_VERIFIED; - return 0; - } - else // New connection is not self but with same pub key. - { - comm::comm_session &ex_session = *iter->second; - // We don't allow duplicate connections to the same peer to same direction. - if (ex_session.is_inbound != session.is_inbound) + + // Compare the response challenge string with the original issued challenge. + if (session.issued_challenge != challenge_resp.challenge) { - // Decide whether we need to replace existing session with new session. - const bool replace_needed = ((res < 0 && !ex_session.is_inbound) || (res > 0 && ex_session.is_inbound)); - if (replace_needed) - { - // If we happen to replace a peer session with known IP, transfer required details to the new session. - if (session.known_ipport.first.empty()) - session.known_ipport.swap(ex_session.known_ipport); - session.uniqueid.swap(pubkeyhex); - session.challenge_status = comm::CHALLENGE_VERIFIED; - - ex_session.close(false); - p2p::ctx.peer_connections.erase(iter); // remove existing session. - p2p::ctx.peer_connections.try_emplace(session.uniqueid, &session); // add new session. - - LOG_DBG << "Replacing existing connection [" << session.uniqueid << "]"; - return 0; - } - else if (ex_session.known_ipport.first.empty() || !session.known_ipport.first.empty()) - { - // If we have any known ip-port info from the new session, transfer them to the existing session. - ex_session.known_ipport.swap(session.known_ipport); - } + LOG_DBG << "Peer challenge response, challenge invalid."; + return -1; } - // Reaching this point means we don't need the new session. - LOG_DBG << "Rejecting new peer connection because existing connection takes priority [" << pubkeyhex << "]"; - return -1; - } -} + // Verify the challenge signature. + if (crypto::verify( + challenge_resp.challenge, + challenge_resp.signature, + challenge_resp.pubkey) != 0) + { + LOG_DBG << "Peer challenge response signature verification failed."; + return -1; + } -/** + // Converting the binary pub key into hexa decimal string this will be used as the key in storing peer sessions + std::string pubkeyhex; + util::bin2hex(pubkeyhex, reinterpret_cast(challenge_resp.pubkey.data()), challenge_resp.pubkey.length()); + + const int res = challenge_resp.pubkey.compare(conf::cfg.pubkey); + + // If pub key is same as our (self) pub key, then this is the loopback connection to ourselves. + // Hence we must keep the connection but only one of two sessions must be added to peer_connections. + // If pub key is greater than our id (< 0), then we should give priority to any existing inbound connection + // from the same peer and drop the outbound connection. + // If pub key is lower than our id (> 0), then we should give priority to any existing outbound connection + // from the same peer and drop the inbound connection. + + std::lock_guard lock(ctx.peer_connections_mutex); + + const auto iter = p2p::ctx.peer_connections.find(pubkeyhex); + if (iter == p2p::ctx.peer_connections.end()) + { + // Add the new connection straight away, if we haven't seen it before. + session.uniqueid.swap(pubkeyhex); + session.challenge_status = comm::CHALLENGE_VERIFIED; + p2p::ctx.peer_connections.try_emplace(session.uniqueid, &session); + return 0; + } + else if (res == 0) // New connection is self (There can be two sessions for self (inbound/outbound)) + { + session.is_self = true; + session.uniqueid.swap(pubkeyhex); + session.challenge_status = comm::CHALLENGE_VERIFIED; + return 0; + } + else // New connection is not self but with same pub key. + { + comm::comm_session &ex_session = *iter->second; + // We don't allow duplicate connections to the same peer to same direction. + if (ex_session.is_inbound != session.is_inbound) + { + // Decide whether we need to replace existing session with new session. + const bool replace_needed = ((res < 0 && !ex_session.is_inbound) || (res > 0 && ex_session.is_inbound)); + if (replace_needed) + { + // If we happen to replace a peer session with known IP, transfer required details to the new session. + if (session.known_ipport.first.empty()) + session.known_ipport.swap(ex_session.known_ipport); + session.uniqueid.swap(pubkeyhex); + session.challenge_status = comm::CHALLENGE_VERIFIED; + + ex_session.close(false); + p2p::ctx.peer_connections.erase(iter); // remove existing session. + p2p::ctx.peer_connections.try_emplace(session.uniqueid, &session); // add new session. + + LOG_DBG << "Replacing existing connection [" << session.uniqueid << "]"; + return 0; + } + else if (ex_session.known_ipport.first.empty() || !session.known_ipport.first.empty()) + { + // If we have any known ip-port info from the new session, transfer them to the existing session. + ex_session.known_ipport.swap(session.known_ipport); + } + } + + // Reaching this point means we don't need the new session. + LOG_DBG << "Rejecting new peer connection because existing connection takes priority [" << pubkeyhex << "]"; + return -1; + } + } + + /** * Broadcasts the given message to all currently connected outbound peers. * @param msg Peer outbound message to be broadcasted. * @param send_to_self Whether to also send the message to self (this node). */ -void broadcast_message(const flatbuffers::FlatBufferBuilder &fbuf, const bool send_to_self) -{ - if (ctx.peer_connections.size() == 0) + void broadcast_message(const flatbuffers::FlatBufferBuilder &fbuf, const bool send_to_self) { - LOG_DBG << "No peers to broadcast (not even self). Waiting until at least one peer connects."; - while (ctx.peer_connections.size() == 0) - util::sleep(100); + if (ctx.peer_connections.size() == 0) + { + LOG_DBG << "No peers to broadcast (not even self). Waiting until at least one peer connects."; + while (ctx.peer_connections.size() == 0) + util::sleep(100); + } + + //Broadcast while locking the peer_connections. + std::lock_guard lock(ctx.peer_connections_mutex); + + for (const auto &[k, session] : ctx.peer_connections) + { + if (!send_to_self && session->is_self) + continue; + + std::string_view msg = std::string_view( + reinterpret_cast(fbuf.GetBufferPointer()), fbuf.GetSize()); + session->send(msg); + } } - //Broadcast while locking the peer_connections. - std::lock_guard lock(ctx.peer_connections_mutex); - - for (const auto &[k, session] : ctx.peer_connections) - { - if (!send_to_self && session->is_self) - continue; - - std::string_view msg = std::string_view( - reinterpret_cast(fbuf.GetBufferPointer()), fbuf.GetSize()); - session->send(msg); - } -} - -/** + /** * Sends the given message to self (this node). * @param msg Peer outbound message to be sent to self. */ -void send_message_to_self(const flatbuffers::FlatBufferBuilder &fbuf) -{ - //Send while locking the peer_connections. - std::lock_guard lock(p2p::ctx.peer_connections_mutex); - - // Find the peer session connected to self. - const auto peer_itr = ctx.peer_connections.find(conf::cfg.pubkeyhex); - if (peer_itr != ctx.peer_connections.end()) + void send_message_to_self(const flatbuffers::FlatBufferBuilder &fbuf) { - std::string_view msg = std::string_view( - reinterpret_cast(fbuf.GetBufferPointer()), fbuf.GetSize()); + //Send while locking the peer_connections. + std::lock_guard lock(p2p::ctx.peer_connections_mutex); - const comm::comm_session *session = peer_itr->second; - session->send(msg); - } -} - -/** - * Sends the given message to a random peer (except self). - * @param msg Peer outbound message to be sent to peer. - */ -void send_message_to_random_peer(const flatbuffers::FlatBufferBuilder &fbuf) -{ - //Send while locking the peer_connections. - std::lock_guard lock(p2p::ctx.peer_connections_mutex); - - const size_t connected_peers = ctx.peer_connections.size(); - if (connected_peers == 0) - { - LOG_DBG << "No peers to send (not even self)."; - return; - } - else if (connected_peers == 1 && ctx.peer_connections.begin()->second->is_self) - { - LOG_DBG << "Only self is connected."; - return; - } - - while (true) - { - // Initialize random number generator with current timestamp. - const int random_peer_index = (rand() % connected_peers); // select a random peer index. - auto it = ctx.peer_connections.begin(); - std::advance(it, random_peer_index); //move iterator to point to random selected peer. - - //send message to selected peer. - const comm::comm_session *session = it->second; - if (!session->is_self) // Exclude self peer. + // Find the peer session connected to self. + const auto peer_itr = ctx.peer_connections.find(conf::cfg.pubkeyhex); + if (peer_itr != ctx.peer_connections.end()) { std::string_view msg = std::string_view( reinterpret_cast(fbuf.GetBufferPointer()), fbuf.GetSize()); + const comm::comm_session *session = peer_itr->second; session->send(msg); - break; } } -} + + /** + * Sends the given message to a random peer (except self). + * @param msg Peer outbound message to be sent to peer. + */ + void send_message_to_random_peer(const flatbuffers::FlatBufferBuilder &fbuf) + { + //Send while locking the peer_connections. + std::lock_guard lock(p2p::ctx.peer_connections_mutex); + + const size_t connected_peers = ctx.peer_connections.size(); + if (connected_peers == 0) + { + LOG_DBG << "No peers to send (not even self)."; + return; + } + else if (connected_peers == 1 && ctx.peer_connections.begin()->second->is_self) + { + LOG_DBG << "Only self is connected."; + return; + } + + while (true) + { + // Initialize random number generator with current timestamp. + const int random_peer_index = (rand() % connected_peers); // select a random peer index. + auto it = ctx.peer_connections.begin(); + std::advance(it, random_peer_index); //move iterator to point to random selected peer. + + //send message to selected peer. + const comm::comm_session *session = it->second; + if (!session->is_self) // Exclude self peer. + { + std::string_view msg = std::string_view( + reinterpret_cast(fbuf.GetBufferPointer()), fbuf.GetSize()); + + session->send(msg); + break; + } + } + } } // namespace p2p \ No newline at end of file diff --git a/src/p2p/p2p.hpp b/src/p2p/p2p.hpp index f601d415..81ba3f09 100644 --- a/src/p2p/p2p.hpp +++ b/src/p2p/p2p.hpp @@ -7,7 +7,7 @@ #include "../comm/comm_session.hpp" #include "../usr/user_input.hpp" #include "peer_session_handler.hpp" -#include "../statefs/hasher.hpp" +#include "../hpfs/h32.hpp" #include "../conf.hpp" namespace p2p @@ -20,7 +20,7 @@ struct proposal uint64_t time; uint8_t stage; std::string lcl; - std::string curr_hash_state; + hpfs::h32 curr_state_hash; std::set users; std::set hash_inputs; std::set hash_outputs; @@ -75,14 +75,14 @@ struct state_request std::string parent_path; // The requested file or dir path. bool is_file; // Whether the path is a file or dir. int32_t block_id; // Block id of the file if we are requesting for file block. Otherwise -1. - hasher::B2H expected_hash; // The expected hash of the requested result. + hpfs::h32 expected_hash; // The expected hash of the requested result. }; // Represents state file system entry. struct state_fs_hash_entry { bool is_file; // Whether this is a file or dir. - hasher::B2H hash; // Hash of the file or dir. + hpfs::h32 hash; // Hash of the file or dir. }; // Represents a file block data resposne. @@ -91,7 +91,7 @@ struct block_response std::string path; // Path of the file. uint32_t block_id; // Id of the block where the data belongs to. std::string_view data; // The block data. - hasher::B2H hash; // Hash of the bloc data. + hpfs::h32 hash; // Hash of the bloc data. }; struct message_collection diff --git a/src/pchheader.hpp b/src/pchheader.hpp index 01090149..8926a262 100644 --- a/src/pchheader.hpp +++ b/src/pchheader.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include diff --git a/src/proc.cpp b/src/proc.cpp index 7c32889c..ba897e56 100644 --- a/src/proc.cpp +++ b/src/proc.cpp @@ -4,226 +4,178 @@ #include "fbschema/common_helpers.hpp" #include "fbschema/p2pmsg_container_generated.h" #include "fbschema/p2pmsg_content_generated.h" -#include "statefs/hasher.hpp" -#include "statefs/state_common.hpp" -#include "statefs/hashtree_builder.hpp" #include "proc.hpp" -#include "cons/cons.hpp" +#include "hpfs/hpfs.hpp" namespace proc { -// Enum used to differenciate pipe fds maintained for SC I/O pipes. -enum FDTYPE -{ - // Used by Smart Contract to read input sent by Hot Pocket - SCREAD = 0, - // Used by Hot Pocket to write input to the smart contract. - HPWRITE = 1, - // Used by Hot Pocket to read output from the smart contract. - HPREAD = 2, - // Used by Smart Contract to write output back to Hot Pocket. - SCWRITE = 3 -}; + // Enum used to differenciate pipe fds maintained for SC I/O pipes. + enum FDTYPE + { + // Used by Smart Contract to read input sent by Hot Pocket + SCREAD = 0, + // Used by Hot Pocket to write input to the smart contract. + HPWRITE = 1, + // Used by Hot Pocket to read output from the smart contract. + HPREAD = 2, + // Used by Smart Contract to write output back to Hot Pocket. + SCWRITE = 3 + }; -// Map of user pipe fds (map key: user public key) -contract_fdmap_t userfds; + // Map of user pipe fds (map key: user public key) + contract_fdmap_t userfds; -// Pipe fds for NPL <--> messages. -std::vector nplfds; + // Pipe fds for NPL <--> messages. + std::vector nplfds; -// Pipe fds for HP <--> messages. -std::vector hpscfds; + // Pipe fds for HP <--> messages. + std::vector hpscfds; -// Holds the contract process id (if currently executing). -pid_t contract_pid; + // Holds the contract process id (if currently executing). + pid_t contract_pid; -// Holds the state monitor process id (if currently executing). -pid_t statemon_pid; + // Holds the hpfs rw process id (if currently executing). + pid_t hpfs_pid; -const char *FINDMNT_COMMAND = "findmnt --noheadings "; + const char *FINDMNT_COMMAND = "findmnt --noheadings "; -/** + /** * Executes the contract process and passes the specified arguments. * @return 0 on successful process creation. -1 on failure or contract process is already running. */ -int exec_contract(const contract_exec_args &args) -{ - // Setup io pipes and feed all inputs to them. - create_iopipes_for_fdmap(userfds, args.userbufs); - create_iopipes(nplfds); - create_iopipes(hpscfds); - - if (feed_inputs(args) != 0) - return -1; - - // Start the state monitor before starting the contract process. - if (start_state_monitor() != 0) - return -1; - - const pid_t pid = fork(); - if (pid > 0) + int exec_contract(const contract_exec_args &args, hpfs::h32 &state_hash) { - // HotPocket process. - contract_pid = pid; + // Setup io pipes and feed all inputs to them. + create_iopipes_for_fdmap(userfds, args.userbufs); + create_iopipes(nplfds); + create_iopipes(hpscfds); - // Close all fds unused by HP process. - close_unused_fds(true); + if (feed_inputs(args) != 0) + return -1; - // Wait for child process (contract process) to complete execution. - const int presult = await_process_execution(contract_pid); - LOG_DBG << "Contract process ended."; + // Start the hpfs rw session before starting the contract process. + if (start_hpfs_rw_session() != 0) + return -1; - contract_pid = 0; - if (presult != 0) + const pid_t pid = fork(); + if (pid > 0) { - LOG_ERR << "Contract process exited with non-normal status code: " << presult; + // HotPocket process. + contract_pid = pid; + + // Close all fds unused by HP process. + close_unused_fds(true); + + // Wait for child process (contract process) to complete execution. + const int presult = await_process_execution(contract_pid); + LOG_DBG << "Contract process ended."; + + contract_pid = 0; + if (presult != 0) + { + LOG_ERR << "Contract process exited with non-normal status code: " << presult; + return -1; + } + + if (stop_hpfs_rw_session(state_hash) != 0) + return -1; + + // After contract execution, collect contract outputs. + if (fetch_outputs(args) != 0) + return -1; + } + else if (pid == 0) + { + // Contract process. + // Set up the process environment and overlay the contract binary program with execv(). + + // Close all fds unused by SC process. + close_unused_fds(false); + + // Write the contract input message from HotPocket to the stdin (0) of the contract process. + write_contract_args(args); + + LOG_DBG << "Starting contract process..."; + + const bool using_appbill = !conf::cfg.appbill.empty(); + int len = conf::cfg.runtime_binexec_args.size() + 1; + if (using_appbill) + len += conf::cfg.runtime_appbill_args.size(); + + // Fill process args. + char *execv_args[len]; + int j = 0; + if (using_appbill) + for (int i = 0; i < conf::cfg.runtime_appbill_args.size(); i++, j++) + execv_args[i] = conf::cfg.runtime_appbill_args[i].data(); + + for (int i = 0; i < conf::cfg.runtime_binexec_args.size(); i++, j++) + execv_args[j] = conf::cfg.runtime_binexec_args[i].data(); + execv_args[len - 1] = NULL; + + chdir(conf::ctx.state_rw_dir.c_str()); + + int ret = execv(execv_args[0], execv_args); + LOG_ERR << errno << ": Contract process execv failed."; + exit(1); + } + else + { + LOG_ERR << "fork() failed when starting contract process."; return -1; } - if (stop_state_monitor() != 0) - return -1; - - // After contract execution, collect contract outputs. - if (fetch_outputs(args) != 0) - return -1; - } - else if (pid == 0) - { - // Contract process. - // Set up the process environment and overlay the contract binary program with execv(). - - // Close all fds unused by SC process. - close_unused_fds(false); - - // Write the contract input message from HotPocket to the stdin (0) of the contract process. - write_contract_args(args); - - LOG_DBG << "Starting contract process..."; - - const bool using_appbill = !conf::cfg.appbill.empty(); - int len = conf::cfg.runtime_binexec_args.size() + 1; - if (using_appbill) - len += conf::cfg.runtime_appbill_args.size(); - - // Fill process args. - char *execv_args[len]; - int j = 0; - if (using_appbill) - for (int i = 0; i < conf::cfg.runtime_appbill_args.size(); i++, j++) - execv_args[i] = conf::cfg.runtime_appbill_args[i].data(); - - for (int i = 0; i < conf::cfg.runtime_binexec_args.size(); i++, j++) - execv_args[j] = conf::cfg.runtime_binexec_args[i].data(); - execv_args[len - 1] = NULL; - - int ret = execv(execv_args[0], execv_args); - LOG_ERR << errno << ": Contract process execv failed."; - exit(1); - } - else - { - LOG_ERR << "fork() failed when starting contract process."; - return -1; + return 0; } - return 0; -} - -/** - * Blocks the calling thread until the specified process compelted exeution (if running). + /** + * Blocks the calling thread until the specified process completed exeution (if running). * @return 0 if process exited normally or exit code of process if abnormally exited. */ -int await_process_execution(pid_t pid) -{ - if (pid > 0) + int await_process_execution(pid_t pid) { - int scstatus; - waitpid(pid, &scstatus, 0); - if (!WIFEXITED(scstatus)) - return WEXITSTATUS(scstatus); - } - return 0; -} - -/** - * Mounts the fuse file system at the contract state dir by starting the state monitor process. - * State monitor will automatically create a state history checkpoint as well. - */ -int start_state_monitor() -{ - pid_t pid = fork(); - if (pid > 0) - { - // HP process. - statemon_pid = pid; - - // Give enough time for the state monitor to start. - // We wait until Fuse filesystem is mounted for max number of retries. - uint16_t retry_count = 0; - std::string findmnt_command = FINDMNT_COMMAND + conf::ctx.state_dir; - while (retry_count < 50) + if (pid > 0) { - util::sleep(10); - int ret = system(findmnt_command.c_str()); - if (WEXITSTATUS(ret) == 0) // Success. Fuse fs has been mounted. - return 0; - retry_count++; + int scstatus; + waitpid(pid, &scstatus, 0); + if (!WIFEXITED(scstatus)) + return WEXITSTATUS(scstatus); } - - // We waited enough time for Fuse fs to be mounted but no luck. - return -1; + return 0; } - else if (pid == 0) - { - // State monitor process. - LOG_DBG << "Starting state monitor..."; - // Fill process args. - char *execv_args[4]; - execv_args[0] = conf::ctx.statemon_exe_path.data(); - execv_args[1] = conf::ctx.state_hist_dir.data(); - execv_args[2] = conf::ctx.state_dir.data(); - execv_args[3] = NULL; - - int ret = execv(execv_args[0], execv_args); - LOG_ERR << errno << ": State monitor execv failed."; - exit(1); - } - else if (pid < 0) - { - LOG_ERR << "fork() failed when starting state monitor."; - return -1; - } -} - -/** - * Terminate the state monitor and update the latest state hash tree. + /** + * Starts the hpfs read/write state filesystem. */ -int stop_state_monitor() -{ - kill(statemon_pid, SIGINT); + int start_hpfs_rw_session() + { + LOG_DBG << "Starting hpfs rw session..."; + if (hpfs::start_fs_session(hpfs_pid, conf::ctx.state_rw_dir, "rw", true) == -1) + return -1; - // Wait for state monitor process to complete execution after the SIGINT. - const int presult = await_process_execution(statemon_pid); - LOG_DBG << "State monitor stopped."; + LOG_DBG << "hpfs rw session started. pid:" << hpfs_pid; + } - statemon_pid = 0; + /** + * Stops the hpfs state filesystem. + */ + int stop_hpfs_rw_session(hpfs::h32 &state_hash) + { + // Read the root hash. + if (hpfs::get_hash(state_hash, conf::ctx.state_rw_dir, "/") == -1) + return -1; - if (presult != 0) - LOG_ERR << "State monitor process exited with non-normal status code: " << presult; + LOG_DBG << "Stopping hpfs rw session... pid:" << hpfs_pid; + if (util::kill_process(hpfs_pid, true) == -1) + return -1; - // Update the hash tree. - hasher::B2H statehash = hasher::B2H_empty; - statefs::hashtree_builder htree_builder(statefs::get_state_dir_context()); - if (htree_builder.generate(statehash) != 0) - return -1; + hpfs_pid = 0; + LOG_DBG << "hpfs rw session stopped."; + return 0; + } - cons::ctx.curr_hash_state = std::string(reinterpret_cast(&statehash), hasher::HASH_SIZE); - return 0; -} - -/** + /** * Writes the contract args (JSON) into the stdin of the contract process. * Args format: * { @@ -236,200 +188,200 @@ int stop_state_monitor() * "unl":[ "pkhex", ... ] * } */ -int write_contract_args(const contract_exec_args &args) -{ - // Populate the json string with contract args. - // We don't use a JSON parser here because it's lightweight to contrstuct the - // json string manually. - - std::ostringstream os; - os << "{\"version\":\"" << util::HP_VERSION - << "\",\"pubkey\":\"" << conf::cfg.pubkeyhex - << "\",\"ts\":" << args.timestamp - << ",\"hpfd\":[" << hpscfds[FDTYPE::SCREAD] << "," << hpscfds[FDTYPE::SCWRITE] - << "],\"usrfd\":{"; - - fdmap_json_to_stream(userfds, os); - - os << "},\"nplfd\":[" << nplfds[FDTYPE::SCREAD] << "," << nplfds[FDTYPE::SCWRITE] - << "],\"unl\":["; - - for (auto nodepk = conf::cfg.unl.begin(); nodepk != conf::cfg.unl.end(); nodepk++) + int write_contract_args(const contract_exec_args &args) { - if (nodepk != conf::cfg.unl.begin()) - os << ","; // Trailing comma separator for previous element. + // Populate the json string with contract args. + // We don't use a JSON parser here because it's lightweight to contrstuct the + // json string manually. - // Convert binary nodepk into hex. - std::string pubkeyhex; - util::bin2hex( - pubkeyhex, - reinterpret_cast((*nodepk).data()), - (*nodepk).length()); + std::ostringstream os; + os << "{\"version\":\"" << util::HP_VERSION + << "\",\"pubkey\":\"" << conf::cfg.pubkeyhex + << "\",\"ts\":" << args.timestamp + << ",\"hpfd\":[" << hpscfds[FDTYPE::SCREAD] << "," << hpscfds[FDTYPE::SCWRITE] + << "],\"usrfd\":{"; - os << "\"" << pubkeyhex << "\""; + fdmap_json_to_stream(userfds, os); + + os << "},\"nplfd\":[" << nplfds[FDTYPE::SCREAD] << "," << nplfds[FDTYPE::SCWRITE] + << "],\"unl\":["; + + for (auto nodepk = conf::cfg.unl.begin(); nodepk != conf::cfg.unl.end(); nodepk++) + { + if (nodepk != conf::cfg.unl.begin()) + os << ","; // Trailing comma separator for previous element. + + // Convert binary nodepk into hex. + std::string pubkeyhex; + util::bin2hex( + pubkeyhex, + reinterpret_cast((*nodepk).data()), + (*nodepk).length()); + + os << "\"" << pubkeyhex << "\""; + } + + os << "]}"; + + // Get the json string that should be written to contract input pipe. + const std::string json = os.str(); + + // Establish contract input pipe. + int stdinpipe[2]; + if (pipe(stdinpipe) != 0) + { + LOG_ERR << "Failed to create pipe to the contract process."; + return -1; + } + + // Redirect pipe read-end to the contract std input so the + // contract process can read from our pipe. + dup2(stdinpipe[0], STDIN_FILENO); + close(stdinpipe[0]); + + // Write the json message and close write fd. + if (write(stdinpipe[1], json.data(), json.size()) == -1) + { + LOG_ERR << "Failed to write to stdin of contract process."; + return -1; + } + close(stdinpipe[1]); + + return 0; } - os << "]}"; - - // Get the json string that should be written to contract input pipe. - const std::string json = os.str(); - - // Establish contract input pipe. - int stdinpipe[2]; - if (pipe(stdinpipe) != 0) + int feed_inputs(const contract_exec_args &args) { - LOG_ERR << "Failed to create pipe to the contract process."; - return -1; + // Write any hp or npl input messages to hp->sc and npl->sc pipe. + if (write_contract_hp_npl_inputs(args) != 0) + { + return -1; + } + + // Write any verified (consensus-reached) user inputs to user pipes. + if (write_contract_fdmap_inputs(userfds, args.userbufs) != 0) + { + cleanup_fdmap(userfds); + LOG_ERR << "Failed to write user inputs to contract."; + return -1; + } + + return 0; } - // Redirect pipe read-end to the contract std input so the - // contract process can read from our pipe. - dup2(stdinpipe[0], STDIN_FILENO); - close(stdinpipe[0]); - - // Write the json message and close write fd. - if (write(stdinpipe[1], json.data(), json.size()) == -1) + int fetch_outputs(const contract_exec_args &args) { - LOG_ERR << "Failed to write to stdin of contract process."; - return -1; - } - close(stdinpipe[1]); + if (read_contract_hp_npl_outputs(args) != 0) + { + return -1; + } - return 0; -} + if (read_contract_fdmap_outputs(userfds, args.userbufs) != 0) + { + LOG_ERR << "Error reading User output from the contract."; + return -1; + } -int feed_inputs(const contract_exec_args &args) -{ - // Write any hp or npl input messages to hp->sc and npl->sc pipe. - if (write_contract_hp_npl_inputs(args) != 0) - { - return -1; + nplfds.clear(); + userfds.clear(); + return 0; } - // Write any verified (consensus-reached) user inputs to user pipes. - if (write_contract_fdmap_inputs(userfds, args.userbufs) != 0) - { - cleanup_fdmap(userfds); - LOG_ERR << "Failed to write user inputs to contract."; - return -1; - } - - return 0; -} - -int fetch_outputs(const contract_exec_args &args) -{ - if (read_contract_hp_npl_outputs(args) != 0) - { - return -1; - } - - if (read_contract_fdmap_outputs(userfds, args.userbufs) != 0) - { - LOG_ERR << "Error reading User output from the contract."; - return -1; - } - - nplfds.clear(); - userfds.clear(); - return 0; -} - -/** + /** * Writes any hp input messages to the contract. */ -int write_contract_hp_npl_inputs(const contract_exec_args &args) -{ - if (write_iopipe(hpscfds, args.hpscbufs.inputs) != 0) + int write_contract_hp_npl_inputs(const contract_exec_args &args) { - LOG_ERR << "Error writing HP inputs to SC"; - return -1; + if (write_iopipe(hpscfds, args.hpscbufs.inputs) != 0) + { + LOG_ERR << "Error writing HP inputs to SC"; + return -1; + } + + if (write_npl_iopipe(nplfds, args.nplbuff.inputs) != 0) + { + LOG_ERR << "Error writing NPL inputs to SC"; + return -1; + } + + return 0; } - if (write_npl_iopipe(nplfds, args.nplbuff.inputs) != 0) - { - LOG_ERR << "Error writing NPL inputs to SC"; - return -1; - } - - return 0; -} - -/** + /** * Read all HP output messages produced by the contract process and store them in * the buffer for later processing. * * @return 0 on success. -1 on failure. */ -int read_contract_hp_npl_outputs(const contract_exec_args &args) -{ - // Clear the input buffers because we are sure the contract has finished reading from - // that mapped memory portion. - args.hpscbufs.inputs.clear(); - - if (read_iopipe(hpscfds, args.hpscbufs.output) != 0) // hpscbufs.second is the output buffer. + int read_contract_hp_npl_outputs(const contract_exec_args &args) { - LOG_ERR << "Error reading HP output from the contract."; - return -1; + // Clear the input buffers because we are sure the contract has finished reading from + // that mapped memory portion. + args.hpscbufs.inputs.clear(); + + if (read_iopipe(hpscfds, args.hpscbufs.output) != 0) // hpscbufs.second is the output buffer. + { + LOG_ERR << "Error reading HP output from the contract."; + return -1; + } + + if (read_iopipe(nplfds, args.nplbuff.output) != 0) // hpscbufs.second is the output buffer. + { + LOG_ERR << "Error reading NPL output from the contract."; + return -1; + } + + return 0; } - if (read_iopipe(nplfds, args.nplbuff.output) != 0) // hpscbufs.second is the output buffer. - { - LOG_ERR << "Error reading NPL output from the contract."; - return -1; - } - - return 0; -} - -/** + /** * Common helper function to write json output of fdmap to given ostream. * @param fdmap Any pubkey->fdlist map. (eg. userfds, nplfds) * @param os An output stream. */ -void fdmap_json_to_stream(const contract_fdmap_t &fdmap, std::ostringstream &os) -{ - for (auto itr = fdmap.begin(); itr != fdmap.end(); itr++) + void fdmap_json_to_stream(const contract_fdmap_t &fdmap, std::ostringstream &os) { - if (itr != fdmap.begin()) - os << ","; // Trailing comma separator for previous element. + for (auto itr = fdmap.begin(); itr != fdmap.end(); itr++) + { + if (itr != fdmap.begin()) + os << ","; // Trailing comma separator for previous element. - // Get the hex pubkey. - std::string_view pubkey = itr->first; // Pubkey in binary format. - std::string pubkeyhex; - util::bin2hex( - pubkeyhex, - reinterpret_cast(pubkey.data()), - pubkey.length()); + // Get the hex pubkey. + std::string_view pubkey = itr->first; // Pubkey in binary format. + std::string pubkeyhex; + util::bin2hex( + pubkeyhex, + reinterpret_cast(pubkey.data()), + pubkey.length()); - // Write hex pubkey and fds. - os << "\"" << pubkeyhex << "\":[" - << itr->second[FDTYPE::SCREAD] << "," - << itr->second[FDTYPE::SCWRITE] << "]"; + // Write hex pubkey and fds. + os << "\"" << pubkeyhex << "\":[" + << itr->second[FDTYPE::SCREAD] << "," + << itr->second[FDTYPE::SCWRITE] << "]"; + } } -} -/** + /** * Creates io pipes for all pubkeys specified in bufmap. * @param fdmap A map which has public key and a vector as fd list for that public key. * @param bufmap A map which has a public key and input/output buffer lists for that public key. * @return 0 on success. -1 on failure. */ -int create_iopipes_for_fdmap(contract_fdmap_t &fdmap, contract_bufmap_t &bufmap) -{ - for (auto &[pubkey, buflist] : bufmap) + int create_iopipes_for_fdmap(contract_fdmap_t &fdmap, contract_bufmap_t &bufmap) { - std::vector fds = std::vector(); - if (create_iopipes(fds) != 0) - return -1; + for (auto &[pubkey, buflist] : bufmap) + { + std::vector fds = std::vector(); + if (create_iopipes(fds) != 0) + return -1; - fdmap.emplace(pubkey, std::move(fds)); + fdmap.emplace(pubkey, std::move(fds)); + } + + return 0; } - return 0; -} - -/** + /** * Common function to create the pipes and write buffer inputs to the fdmap. * We take mutable parameters since the internal entries in the maps will be * modified (eg. fd close, buffer clear). @@ -438,19 +390,19 @@ int create_iopipes_for_fdmap(contract_fdmap_t &fdmap, contract_bufmap_t &bufmap) * @param bufmap A map which has a public key and input/output buffer lists for that public key. * @return 0 on success. -1 on failure. */ -int write_contract_fdmap_inputs(contract_fdmap_t &fdmap, contract_bufmap_t &bufmap) -{ - // Loop through input buffers for each pubkey. - for (auto &[pubkey, buflist] : bufmap) + int write_contract_fdmap_inputs(contract_fdmap_t &fdmap, contract_bufmap_t &bufmap) { - if (write_iopipe(fdmap[pubkey], buflist.inputs) != 0) - return -1; + // Loop through input buffers for each pubkey. + for (auto &[pubkey, buflist] : bufmap) + { + if (write_iopipe(fdmap[pubkey], buflist.inputs) != 0) + return -1; + } + + return 0; } - return 0; -} - -/** + /** * Common function to read all outputs produced by the contract process and store them in * output buffers for later processing. * @@ -458,274 +410,274 @@ int write_contract_fdmap_inputs(contract_fdmap_t &fdmap, contract_bufmap_t &bufm * @param bufmap A map which has a public key and input/output buffer pair for that public key. * @return 0 on success. -1 on failure. */ -int read_contract_fdmap_outputs(contract_fdmap_t &fdmap, contract_bufmap_t &bufmap) -{ - for (auto &[pubkey, bufpair] : bufmap) + int read_contract_fdmap_outputs(contract_fdmap_t &fdmap, contract_bufmap_t &bufmap) { - // Clear the input buffer because we are sure the contract has finished reading from - // the inputs' mapped memory portion. - bufpair.inputs.clear(); + for (auto &[pubkey, bufpair] : bufmap) + { + // Clear the input buffer because we are sure the contract has finished reading from + // the inputs' mapped memory portion. + bufpair.inputs.clear(); - // Get fds for the pubkey. - std::vector &fds = fdmap[pubkey]; + // Get fds for the pubkey. + std::vector &fds = fdmap[pubkey]; - if (read_iopipe(fds, bufpair.output) != 0) // bufpair.second is the output buffer. - return -1; + if (read_iopipe(fds, bufpair.output) != 0) // bufpair.second is the output buffer. + return -1; + } + + return 0; } - return 0; -} - -/** + /** * Common function to close any open fds in the map after an error. * @param fdmap Any pubkey->fdlist map. (eg. userfds, nplfds) */ -void cleanup_fdmap(contract_fdmap_t &fdmap) -{ - for (auto &[pubkey, fds] : fdmap) + void cleanup_fdmap(contract_fdmap_t &fdmap) { - for (int i = 0; i < 4; i++) + for (auto &[pubkey, fds] : fdmap) { - if (fds[i] > 0) - close(fds[i]); - fds[i] = 0; + for (int i = 0; i < 4; i++) + { + if (fds[i] > 0) + close(fds[i]); + fds[i] = 0; + } } } -} -/** + /** * Common function to create a pair of pipes (Hp->SC, SC->HP). * @param fds Vector to populate fd list. * @param inputbuffer Buffer to write into the HP write fd. */ -int create_iopipes(std::vector &fds) -{ - int inpipe[2]; - if (pipe(inpipe) != 0) - return -1; - - int outpipe[2]; - if (pipe(outpipe) != 0) + int create_iopipes(std::vector &fds) { - // Close the earlier created pipe. - close(inpipe[0]); - close(inpipe[1]); - return -1; + int inpipe[2]; + if (pipe(inpipe) != 0) + return -1; + + int outpipe[2]; + if (pipe(outpipe) != 0) + { + // Close the earlier created pipe. + close(inpipe[0]); + close(inpipe[1]); + return -1; + } + + // If both pipes got created, assign them to the fd vector. + fds.clear(); + fds.push_back(inpipe[0]); //SCREAD + fds.push_back(inpipe[1]); //HPWRITE + fds.push_back(outpipe[0]); //HPREAD + fds.push_back(outpipe[1]); //SCWRITE + + return 0; } - // If both pipes got created, assign them to the fd vector. - fds.clear(); - fds.push_back(inpipe[0]); //SCREAD - fds.push_back(inpipe[1]); //HPWRITE - fds.push_back(outpipe[0]); //HPREAD - fds.push_back(outpipe[1]); //SCWRITE - - return 0; -} - -/** + /** * Common function to write the given input buffer into the write fd from the HP side. * @param fds Vector of fd list. * @param inputs Buffer to write into the HP write fd. */ -int write_iopipe(std::vector &fds, std::list &inputs) -{ - // Write the inputs (if any) into the contract and close the writefd. - - const int writefd = fds[FDTYPE::HPWRITE]; - bool vmsplice_error = false; - - if (!inputs.empty()) + int write_iopipe(std::vector &fds, std::list &inputs) { - // Prepare the input memory segments to map with vmsplice. - size_t i = 0; - iovec memsegs[inputs.size()]; - for (std::string &input : inputs) + // Write the inputs (if any) into the contract and close the writefd. + + const int writefd = fds[FDTYPE::HPWRITE]; + bool vmsplice_error = false; + + if (!inputs.empty()) { - memsegs[i].iov_base = input.data(); - memsegs[i].iov_len = input.length(); - i++; + // Prepare the input memory segments to map with vmsplice. + size_t i = 0; + iovec memsegs[inputs.size()]; + for (std::string &input : inputs) + { + memsegs[i].iov_base = input.data(); + memsegs[i].iov_len = input.length(); + i++; + } + + // We use vmsplice to map (zero-copy) the inputs into the fd. + if (vmsplice(writefd, memsegs, inputs.size(), 0) == -1) + vmsplice_error = true; + + // It's important that we DO NOT clear the input buffer string until the contract + // process has actually read from the fd. Because the OS is just mapping our + // input buffer memory portion into the fd, if we clear it now, the contract process + // will get invaid bytes when reading the fd. } - // We use vmsplice to map (zero-copy) the inputs into the fd. - if (vmsplice(writefd, memsegs, inputs.size(), 0) == -1) - vmsplice_error = true; + // Close the writefd since we no longer need it. + close(writefd); + fds[FDTYPE::HPWRITE] = 0; - // It's important that we DO NOT clear the input buffer string until the contract - // process has actually read from the fd. Because the OS is just mapping our - // input buffer memory portion into the fd, if we clear it now, the contract process - // will get invaid bytes when reading the fd. + return vmsplice_error ? -1 : 0; } - // Close the writefd since we no longer need it. - close(writefd); - fds[FDTYPE::HPWRITE] = 0; - - return vmsplice_error ? -1 : 0; -} - -/** + /** * Write the given input buffer into the write fd from the HP side. * @param fds Vector of fd list. * @param inputs Buffer to write into the HP write fd. */ -int write_npl_iopipe(std::vector &fds, std::list &inputs) -{ - /** + int write_npl_iopipe(std::vector &fds, std::list &inputs) + { + /** * npl inputs are feed into the contract in a binary protocol. It follows the following pattern * |**NPL version (1 byte)**|**Reserved (1 byte)**|**Length of the message (2 bytes)**|**Public key (4 bytes)**|**Npl message data**| * Length of the message is calculated without including public key length */ - const int writefd = fds[FDTYPE::HPWRITE]; - bool vmsplice_error = false; - if (!inputs.empty()) - { - int8_t total_memsegs = inputs.size() * 3; - iovec memsegs[total_memsegs]; - size_t i = 0; - for (auto &input : inputs) + const int writefd = fds[FDTYPE::HPWRITE]; + bool vmsplice_error = false; + if (!inputs.empty()) { - int8_t pre_header_index = i * 3; - int8_t pubkey_index = pre_header_index + 1; - int8_t msg_index = pre_header_index + 2; + int8_t total_memsegs = inputs.size() * 3; + iovec memsegs[total_memsegs]; + size_t i = 0; + for (auto &input : inputs) + { + int8_t pre_header_index = i * 3; + int8_t pubkey_index = pre_header_index + 1; + int8_t msg_index = pre_header_index + 2; - // First binary representation of version, reserve and message length is constructed and feed it into - // memory segment. Then the public key and at last the message data + // First binary representation of version, reserve and message length is constructed and feed it into + // memory segment. Then the public key and at last the message data - // At the moment no data is inserted as reserve - uint8_t reserve = 0; + // At the moment no data is inserted as reserve + uint8_t reserve = 0; - //Get message container - const fbschema::p2pmsg::Container *container = fbschema::p2pmsg::GetContainer(input.data()); - const flatbuffers::Vector *container_content = container->content(); + //Get message container + const fbschema::p2pmsg::Container *container = fbschema::p2pmsg::GetContainer(input.data()); + const flatbuffers::Vector *container_content = container->content(); - uint16_t msg_length = container_content->size(); + uint16_t msg_length = container_content->size(); - /** + /** * Pre header is constructed using bit shifting. This will generate a bit pattern as explain in the example below * version = 00000001 * reserve = 00000000 * msg_length = 0000000010001101 * pre_header = 00000001000000000000000010001101 */ - uint32_t pre_header = util::MIN_NPL_INPUT_VERSION; - pre_header = pre_header << 8; - pre_header += reserve; + uint32_t pre_header = util::MIN_NPL_INPUT_VERSION; + pre_header = pre_header << 8; + pre_header += reserve; - pre_header = pre_header << 16; - pre_header += msg_length; - memsegs[pre_header_index].iov_base = &pre_header; - memsegs[pre_header_index].iov_len = 4; + pre_header = pre_header << 16; + pre_header += msg_length; + memsegs[pre_header_index].iov_base = &pre_header; + memsegs[pre_header_index].iov_len = 4; - std::string_view msg_pubkey = fbschema::flatbuff_bytes_to_sv(container->pubkey()); - memsegs[pubkey_index].iov_base = reinterpret_cast(const_cast(msg_pubkey.data())); - memsegs[pubkey_index].iov_len = msg_pubkey.size(); + std::string_view msg_pubkey = fbschema::flatbuff_bytes_to_sv(container->pubkey()); + memsegs[pubkey_index].iov_base = reinterpret_cast(const_cast(msg_pubkey.data())); + memsegs[pubkey_index].iov_len = msg_pubkey.size(); - memsegs[msg_index].iov_base = reinterpret_cast(const_cast(container_content->Data())); - memsegs[msg_index].iov_len = container_content->size(); + memsegs[msg_index].iov_base = reinterpret_cast(const_cast(container_content->Data())); + memsegs[msg_index].iov_len = container_content->size(); - i++; + i++; + } + + if (vmsplice(writefd, memsegs, total_memsegs, 0) == -1) + vmsplice_error = true; } + // It's important that we DO NOT clear the input buffer string until the contract + // process has actually read from the fd. Because the OS is just mapping our + // input buffer memory portion into the fd, if we clear it now, the contract process + // will get invaid bytes when reading the fd. - if (vmsplice(writefd, memsegs, total_memsegs, 0) == -1) - vmsplice_error = true; + // Close the writefd since we no longer need it. + close(writefd); + fds[FDTYPE::HPWRITE] = 0; + + return vmsplice_error ? -1 : 0; } - // It's important that we DO NOT clear the input buffer string until the contract - // process has actually read from the fd. Because the OS is just mapping our - // input buffer memory portion into the fd, if we clear it now, the contract process - // will get invaid bytes when reading the fd. - // Close the writefd since we no longer need it. - close(writefd); - fds[FDTYPE::HPWRITE] = 0; - - return vmsplice_error ? -1 : 0; -} - -/** + /** * Common function to read and close SC output from the pipe and populate the output list. * @param fds Vector representing the pipes fd list. * @param output The buffer to place the read output. */ -int read_iopipe(std::vector &fds, std::string &output) -{ - // Read any data that have been written by the contract process - // from the output pipe and store in the output buffer. - // Outputs will be read by the consensus process later when it wishes so. - - const int readfd = fds[FDTYPE::HPREAD]; - int bytes_available = 0; - ioctl(readfd, FIONREAD, &bytes_available); - bool vmsplice_error = false; - - if (bytes_available > 0) + int read_iopipe(std::vector &fds, std::string &output) { - output.resize(bytes_available); + // Read any data that have been written by the contract process + // from the output pipe and store in the output buffer. + // Outputs will be read by the consensus process later when it wishes so. - // Populate the user output buffer with new data from the pipe. - // We use vmsplice to map (zero-copy) the output from the fd into output bbuffer. - iovec memsegs[1]; - memsegs[0].iov_base = output.data(); - memsegs[0].iov_len = bytes_available; + const int readfd = fds[FDTYPE::HPREAD]; + int bytes_available = 0; + ioctl(readfd, FIONREAD, &bytes_available); + bool vmsplice_error = false; - if (vmsplice(readfd, memsegs, 1, 0) == -1) - vmsplice_error = true; + if (bytes_available > 0) + { + output.resize(bytes_available); + + // Populate the user output buffer with new data from the pipe. + // We use vmsplice to map (zero-copy) the output from the fd into output bbuffer. + iovec memsegs[1]; + memsegs[0].iov_base = output.data(); + memsegs[0].iov_len = bytes_available; + + if (vmsplice(readfd, memsegs, 1, 0) == -1) + vmsplice_error = true; + } + + // Close readfd fd on HP process side because we are done with contract process I/O. + close(readfd); + fds[FDTYPE::HPREAD] = 0; + + return vmsplice_error ? -1 : 0; } - // Close readfd fd on HP process side because we are done with contract process I/O. - close(readfd); - fds[FDTYPE::HPREAD] = 0; + void close_unused_fds(const bool is_hp) + { + close_unused_vectorfds(is_hp, hpscfds); - return vmsplice_error ? -1 : 0; -} + close_unused_vectorfds(is_hp, nplfds); -void close_unused_fds(const bool is_hp) -{ - close_unused_vectorfds(is_hp, hpscfds); + // Loop through user fds. + for (auto &[pubkey, fds] : userfds) + close_unused_vectorfds(is_hp, fds); + } - close_unused_vectorfds(is_hp, nplfds); - - // Loop through user fds. - for (auto &[pubkey, fds] : userfds) - close_unused_vectorfds(is_hp, fds); -} - -/** + /** * Common function for closing unused fds based on which process this gets called from. * @param is_hp Specify 'true' when calling from HP process. 'false' from SC process. * @param fds Vector of fds to close. */ -void close_unused_vectorfds(const bool is_hp, std::vector &fds) -{ - if (is_hp) + void close_unused_vectorfds(const bool is_hp, std::vector &fds) { - // Close unused fds in Hot Pocket process. - close(fds[FDTYPE::SCREAD]); - fds[FDTYPE::SCREAD] = 0; - close(fds[FDTYPE::SCWRITE]); - fds[FDTYPE::SCWRITE] = 0; - } - else - { - // Close unused fds in smart contract process. - close(fds[FDTYPE::HPREAD]); - fds[FDTYPE::HPREAD] = 0; + if (is_hp) + { + // Close unused fds in Hot Pocket process. + close(fds[FDTYPE::SCREAD]); + fds[FDTYPE::SCREAD] = 0; + close(fds[FDTYPE::SCWRITE]); + fds[FDTYPE::SCWRITE] = 0; + } + else + { + // Close unused fds in smart contract process. + close(fds[FDTYPE::HPREAD]); + fds[FDTYPE::HPREAD] = 0; - // HPWRITE fd has aleady been closed by HP process after writing - // inputs (before the fork). + // HPWRITE fd has aleady been closed by HP process after writing + // inputs (before the fork). + } } -} -/** + /** * Cleanup any running processes. */ -void deinit() -{ - if (contract_pid > 0) - kill(contract_pid, SIGINT); + void deinit() + { + if (contract_pid > 0) + util::kill_process(contract_pid, true); - if (statemon_pid > 0) - kill(statemon_pid, SIGINT); -} + if (hpfs_pid > 0) + util::kill_process(hpfs_pid, true); + } } // namespace proc diff --git a/src/proc.hpp b/src/proc.hpp index b92e2c41..66c34fb4 100644 --- a/src/proc.hpp +++ b/src/proc.hpp @@ -3,6 +3,7 @@ #include "pchheader.hpp" #include "usr/usr.hpp" +#include "hpfs/h32.hpp" #include "util.hpp" /** @@ -33,11 +34,6 @@ typedef std::unordered_map> contract_fdmap_t; // This is used to keep track of input/output buffers for a given public key (eg. user, npl) typedef std::unordered_map contract_bufmap_t; -// Common typedef for a map of updated blocks of state files by the contract process. -// This is used as a hint in updating the state merkle tree. -// filename->modified blocks -typedef std::unordered_map> contract_fblockmap_t; - /** * Holds information that should be passed into the contract process. */ @@ -54,10 +50,6 @@ struct contract_exec_args // Pair of HP<->SC JSON message buffers (mainly used for control messages). // Input buffers for HP->SC messages, Output buffers for SC->HP messages. contract_iobuf_pair &hpscbufs; - - // The map of state files that was updated with updated block ids. - // Each block id N represents Nth 4MB block of the file. - contract_fblockmap_t &state_updates; // Current HotPocket timestamp. const int64_t timestamp; @@ -66,18 +58,16 @@ struct contract_exec_args int64_t timestamp, contract_bufmap_t &userbufs, contract_iobuf_pair &nplbuff, - contract_iobuf_pair &hpscbufs, - contract_fblockmap_t &state_updates) : + contract_iobuf_pair &hpscbufs) : userbufs(userbufs), nplbuff(nplbuff), hpscbufs(hpscbufs), - state_updates(state_updates), timestamp(timestamp) { } }; -int exec_contract(const contract_exec_args &args); +int exec_contract(const contract_exec_args &args, hpfs::h32 &state_hash); void deinit(); @@ -85,9 +75,9 @@ void deinit(); int await_process_execution(pid_t pid); -int start_state_monitor(); +int start_hpfs_rw_session(); -int stop_state_monitor(); +int stop_hpfs_rw_session(hpfs::h32 &state_hash); int write_contract_args(const contract_exec_args &args); diff --git a/src/statefs/hasher.cpp b/src/statefs/hasher.cpp deleted file mode 100644 index d9c0a1f3..00000000 --- a/src/statefs/hasher.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "hasher.hpp" - -/** - * Contains hashing functions and helpers used to manipulate block hashes used in state management. - * This could also be used throughout rest of the application as well. However for now we are only - * using this for state management code base only. - * - * Based on https://github.com/codetsunami/file-ptracer/blob/master/merkle.cpp - */ -namespace hasher -{ - -// Represents empty/default B2H hash value. -B2H B2H_empty = hasher::B2H_empty; - -/** - * Helper functions for working with 32 byte hash type B2H. - */ - -bool B2H::operator==(const B2H rhs) const -{ - return this->data[0] == rhs.data[0] && this->data[1] == rhs.data[1] && this->data[2] == rhs.data[2] && this->data[3] == rhs.data[3]; -} - -bool B2H::operator!=(const B2H rhs) const -{ - return this->data[0] != rhs.data[0] || this->data[1] != rhs.data[1] || this->data[2] != rhs.data[2] || this->data[3] != rhs.data[3]; -} - -void B2H::operator^=(const B2H rhs) -{ - this->data[0] ^= rhs.data[0]; - this->data[1] ^= rhs.data[1]; - this->data[2] ^= rhs.data[2]; - this->data[3] ^= rhs.data[3]; -} - -std::ostream &operator<<(std::ostream &output, const B2H &h) -{ - output << h.data[0];// << h.data[1] << h.data[2] << h.data[3]; - return output; -} - -std::stringstream &operator<<(std::stringstream &output, const B2H &h) -{ - output << std::hex << h; - return output; -} - -// The actual hash function, note that the B2H datatype is always passed by value being only 4 quadwords. -// This function accepts two buffers to hash together in order to support common use case in state handling. -B2H hash(const void *buf1, const size_t buf1len, const void *buf2, const size_t buf2len) -{ - crypto_generichash_blake2b_state state; - crypto_generichash_blake2b_init(&state, NULL, 0, HASH_SIZE); - - crypto_generichash_blake2b_update(&state, - reinterpret_cast(buf1), buf1len); - crypto_generichash_blake2b_update(&state, - reinterpret_cast(buf2), buf2len); - B2H ret; - crypto_generichash_blake2b_final( - &state, - reinterpret_cast(&ret), - HASH_SIZE); - return ret; -} - -// Helper class to support std::map/std::unordered_map custom hashing function. -// This is needed to use B2H as the std map container key. -size_t B2H_std_key_hasher::operator()(const hasher::B2H h) const -{ - // Compute individual hash values. http://stackoverflow.com/a/1646913/126995 - size_t res = 17; - res = res * 31 + std::hash()(h.data[0]); - res = res * 31 + std::hash()(h.data[1]); - res = res * 31 + std::hash()(h.data[2]); - res = res * 31 + std::hash()(h.data[3]); - return res; -} - -} // namespace hasher \ No newline at end of file diff --git a/src/statefs/hasher.hpp b/src/statefs/hasher.hpp deleted file mode 100644 index 26006cf1..00000000 --- a/src/statefs/hasher.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef _HASHER_ -#define _HASHER_ - -#include "../pchheader.hpp" - -namespace hasher -{ - -// Hash length (32 bytes) -constexpr size_t HASH_SIZE = crypto_generichash_blake2b_BYTES; - -// blake2b hash is 32 bytes which we store as 4 quad words -// Originally from https://github.com/codetsunami/file-ptracer/blob/master/merkle.cpp -struct B2H -{ - uint64_t data[4]; - - bool operator==(const B2H rhs) const; - bool operator!=(const B2H rhs) const; - void operator^=(const B2H rhs); -}; - -extern B2H B2H_empty; - -std::ostream &operator<<(std::ostream &output, const B2H &h); -std::stringstream &operator<<(std::stringstream &output, const B2H &h); - -B2H hash(const void *buf1, const size_t buf1len, const void *buf2, const size_t buf2len); - -// Helper class to support std::map/std::unordered_map custom hashing function. -// This is needed to use B2H as the std map container key. -class B2H_std_key_hasher -{ -public: - size_t operator()(const hasher::B2H h) const; -}; - -} // namespace hasher - -#endif \ No newline at end of file diff --git a/src/statefs/hashmap_builder.cpp b/src/statefs/hashmap_builder.cpp deleted file mode 100644 index 6a2bb4d8..00000000 --- a/src/statefs/hashmap_builder.cpp +++ /dev/null @@ -1,505 +0,0 @@ -#include "../pchheader.hpp" -#include "../hplog.hpp" -#include "state_common.hpp" -#include "hashmap_builder.hpp" -#include "hasher.hpp" - -namespace statefs -{ - -/** - * Hashmap builder class is responsible for updating file hash based on the modified blocks of a file. - */ - -hashmap_builder::hashmap_builder(const state_dir_context &ctx) : ctx(ctx) -{ -} - -/** - * Generates/updates the block hash map for a file and updates the parent dir hash accordingly as well. - * @param parent_dir_hash Hash of the parent directory. This will be updated of the file hash was updated. - * @param filepath Full path to the actual state file. - * @param file_relpath The relative path to the state file from the state data directory. - * @param changed_blocks Index of changed blocks and the new hashes to be used as a hint. - * @return 0 on success. -1 on failure. - */ -int hashmap_builder::generate_hashmap_for_file(hasher::B2H &parent_dir_hash, const std::string &filepath, const std::string &file_relpath, const std::map &changed_blocks) -{ - // We attempt to avoid a full rebuild of the block hash map file when possible. - // For this optimisation, both the block hash map (.bhmap) file and the - // delta block index must exist. - - // Block index may be provided as an argument. If it is empty we attempt to read from the - // .bindex file from the state checkpoint delta. - - // If the block index exists, we generate/update the hashmap file with the aid of that. - // Block index file contains the updated blockids. If not, we simply rehash all the blocks. - - // Open the actual data file and calculate the block count. - int orifd = open(filepath.data(), O_RDONLY); - if (orifd == -1) - { - LOG_ERR << errno << ": Open failed " << filepath; - return -1; - } - const off_t file_length = lseek(orifd, 0, SEEK_END); - const uint32_t block_count = ceil((double)file_length / (double)BLOCK_SIZE); - - // Attempt to read the existing block hash map file. - std::string bhmap_file; - std::vector bhmap_data; - if (read_block_hashmap(bhmap_data, bhmap_file, file_relpath) == -1) - { - close(orifd); - return -1; - } - - hasher::B2H old_file_hash = hasher::B2H_empty; - if (!bhmap_data.empty()) - memcpy(&old_file_hash, bhmap_data.data(), hasher::HASH_SIZE); - - // Array to contain the updated block hashes. Slot 0 is for the root hash. - // Allocating hash array on the heap to avoid filling limited stack space. - std::unique_ptr hashes = std::make_unique(1 + block_count); - const size_t hashes_size = (1 + block_count) * hasher::HASH_SIZE; - - if (changed_blocks.empty()) - { - // Attempt to read the delta block index file. - std::map bindex; - uint32_t original_block_count; - if (get_delta_block_index(bindex, original_block_count, file_relpath) == -1) - { - close(orifd); - return -1; - } - - if (update_hashes_with_backup_block_hints(hashes.get(), hashes_size, file_relpath, orifd, block_count, original_block_count, bindex, bhmap_data) == -1) - { - close(orifd); - return -1; - } - } - else - { - if (update_hashes_with_changed_block_hints(hashes.get(), hashes_size, file_relpath, orifd, block_count, changed_blocks, bhmap_data) == -1) - { - close(orifd); - return -1; - } - } - - close(orifd); - - if (write_block_hashmap(bhmap_file, hashes.get(), hashes_size) == -1) - return -1; - - if (update_hashtree_entry(parent_dir_hash, !bhmap_data.empty(), old_file_hash, hashes[0], bhmap_file, file_relpath) == -1) - return -1; - - return 0; -} - -/** - * Reads the block hash map of a given data file into the provided vector. - * @param bhmap_data Vector to copy the block hash map contents. - * @param bhmap_file The full path to the block hash map file pointed to by the relative path. - * @param relpath The relative path of the actual data file. - * @return 0 on success. -1 on failure. - */ -int hashmap_builder::read_block_hashmap(std::vector &bhmap_data, std::string &bhmap_file, const std::string &relpath) -{ - bhmap_file.reserve(ctx.block_hashmap_dir.length() + relpath.length() + BLOCK_HASHMAP_EXT_LEN); - bhmap_file.append(ctx.block_hashmap_dir).append(relpath).append(BLOCK_HASHMAP_EXT); - - if (boost::filesystem::exists(bhmap_file)) - { - int hmapfd = open(bhmap_file.c_str(), O_RDONLY); - if (hmapfd == -1) - { - LOG_ERR << errno << ": Open failed " << bhmap_file; - return -1; - } - - off_t size = lseek(hmapfd, 0, SEEK_END); - bhmap_data.resize(size); - - if (pread(hmapfd, bhmap_data.data(), size, 0) == -1) - { - LOG_ERR << errno << ": Read failed " << bhmap_file; - close(hmapfd); - return -1; - } - - close(hmapfd); - } - else - { - // Create directory tree if not exist so we are able to create the hashmap files. - boost::filesystem::path hmapsubdir = boost::filesystem::path(bhmap_file).parent_path(); - if (created_bhmapsubdirs.count(hmapsubdir.string()) == 0) - { - boost::filesystem::create_directories(hmapsubdir); - created_bhmapsubdirs.emplace(hmapsubdir.string()); - } - } - - return 0; -} - -/** - * Reads the delta block index of a file. - * @param idxmap Map to copy the block index contents (block id --> hash). - * @param total_block_count Reference to hold the total block count of the original data file. - * @param file_relpath Relative path to the data file. - * @return 0 on success. -1 on failure. - */ -int hashmap_builder::get_delta_block_index(std::map &idxmap, uint32_t &total_block_count, const std::string &file_relpath) -{ - std::string bindex_file; - bindex_file.reserve(ctx.delta_dir.length() + file_relpath.length() + BLOCK_INDEX_EXT_LEN); - bindex_file.append(ctx.delta_dir).append(file_relpath).append(BLOCK_INDEX_EXT); - - if (boost::filesystem::exists(bindex_file)) - { - std::ifstream in_file(bindex_file, std::ios::binary | std::ios::ate); - std::streamsize idx_size = in_file.tellg(); - in_file.seekg(0, std::ios::beg); - - // Read the block index file into a vector. - std::vector bindex(idx_size); - if (in_file.read(bindex.data(), idx_size)) - { - // First 8 bytes contain the original file length. - off_t orifilelen; - memcpy(&orifilelen, bindex.data(), 8); - total_block_count = ceil((double)orifilelen / (double)BLOCK_SIZE); - - // Skip the first 8 bytes and loop through index entries. - for (uint32_t idx_offset = 8; idx_offset < bindex.size();) - { - // Read the block no. (4 bytes) of where this block is from in the original file. - uint32_t block_no = 0; - memcpy(&block_no, bindex.data() + idx_offset, 4); - idx_offset += 12; // Skip the cached block offset (8 bytes) - - // Read the block hash (32 bytes). - hasher::B2H hash; - memcpy(&hash, bindex.data() + idx_offset, 32); - idx_offset += 32; - - idxmap.try_emplace(block_no, hash); - } - } - else - { - LOG_ERR << errno << ": Read failed " << bindex_file; - return -1; - } - - in_file.close(); - } - - return 0; -} - -/** - * Updates the hash map with the use of delta backup block ids. - * @param hashes Pointer to the hash array to copy the block hashes after the update. - * @param hashes_size Byte length of the hashes array. - * @param relpath Relative path of the data file. - * @param orifd An open file descriptor to the data file. - * @param block_count Block count of the updated file. - * @param original_block_count Original block count before the update. - * @param bindex Delta backup block index map. - * @param bhmap_data Contents of the existing block hash map. - * @return 0 on success. -1 on failure. - */ -int hashmap_builder::update_hashes_with_backup_block_hints( - hasher::B2H *hashes, const off_t hashes_size, const std::string &relpath, const int orifd, const uint32_t block_count, - const uint32_t original_block_count, const std::map &bindex, const std::vector &bhmap_data) -{ - uint32_t nohint_blockstart = 0; - - // If both existing delta block index and block hash map is available, we can just overlay the - // changed block hashes (mentioned in the delta block index) on top of the old block hashes. - // This would prevent unncessarily hashing lot of blocks. - if (!bhmap_data.empty() && !bindex.empty()) - { - // Load old hashes. - memcpy(hashes, bhmap_data.data(), hashes_size < bhmap_data.size() ? hashes_size : bhmap_data.size()); - - // Refer to the block index and rehash the changed blocks. - for (const auto [block_id, old_hash] : bindex) - { - // If the block_id from the block index is no longer there, that means the current file is - // shorter than the previous version. So we can stop hashing at this point. - if (block_id >= block_count) - break; - - if (compute_blockhash(hashes[block_id + 1], block_id, orifd, relpath) == -1) - return -1; - } - - // If the current file has more blocks than the previous version, we need to hash those - // additional blocks as well. - if (block_count > original_block_count) - nohint_blockstart = original_block_count; - else - nohint_blockstart = block_count; // No additional blocks remaining. - } - - //Hash any additional blocks that has to be hashed without the guidance of block index. - for (uint32_t block_id = nohint_blockstart; block_id < block_count; block_id++) - { - if (compute_blockhash(hashes[block_id + 1], block_id, orifd, relpath) == -1) - return -1; - } - - // Calculate the new file hash: filehash = HASH(filename + XOR(block hashes)) - hasher::B2H filehash = hasher::B2H_empty; - for (int i = 1; i <= block_count; i++) - filehash ^= hashes[i]; - - // Rehash the file hash with filename included. - const std::string filename = boost::filesystem::path(relpath.data()).filename().string(); - filehash = hasher::hash(filename.c_str(), filename.length(), &filehash, hasher::HASH_SIZE); - - hashes[0] = filehash; - return 0; -} - -/** - * Updates the hash map with the use of list of updated block ids. - * @param hashes Pointer to the hash array to copy the block hashes after the update. - * @param hashes_size Byte length of the hashes array. - * @param relpath Relative path of the data file. - * @param orifd An open file descriptor to the data file. - * @param block_count Block count of the updated file. - * @param bindex Map of updated block ids and new hashes. - * @param bhmap_data Contents of the existing block hash map. - * @return 0 on success. -1 on failure. - */ -int hashmap_builder::update_hashes_with_changed_block_hints( - hasher::B2H *hashes, const off_t hashes_size, const std::string &relpath, const int orifd, - const uint32_t block_count, const std::map &bindex, const std::vector &bhmap_data) -{ - // If both existing delta block index and block hash map is available, we can just overlay the - // changed block hashes (mentioned in the delta block index) on top of the old block hashes. - // This would prevent unncessarily hashing lot of blocks. - - if (!bindex.empty()) - { - // Load old hashes if exists. - if (!bhmap_data.empty()) - memcpy(hashes, bhmap_data.data(), hashes_size < bhmap_data.size() ? hashes_size : bhmap_data.size()); - - // Refer to the block index and overlay the new hash into the hashes array. - for (const auto [block_id, new_hash] : bindex) - hashes[block_id + 1] = new_hash; - - // If the block hash map didn't existed, we need to calculate and fill the unchanged block hashes from the actual file. - if (bhmap_data.empty()) - { - for (uint32_t block_id = 0; block_id < block_count; block_id++) - { - if (bindex.count(block_id) == 0 && compute_blockhash(hashes[block_id + 1], block_id, orifd, relpath) == -1) - return -1; - } - } - } - else - { - // If we don't have the changed block index, we have to hash the entire file blocks again. - for (uint32_t block_id = 0; block_id < block_count; block_id++) - { - if (compute_blockhash(hashes[block_id + 1], block_id, orifd, relpath) == -1) - return -1; - } - } - - // Calculate the new file hash: filehash = HASH(filename + XOR(block hashes)) - hasher::B2H filehash = hasher::B2H_empty; - for (int i = 1; i <= block_count; i++) - filehash ^= hashes[i]; - - // Rehash the file hash with filename included. - const std::string filename = boost::filesystem::path(relpath.data()).filename().string(); - filehash = hasher::hash(filename.c_str(), filename.length(), &filehash, hasher::HASH_SIZE); - - hashes[0] = filehash; - return 0; -} - -/** - * Calculates the hash of the specified block id of a file. - * @param hash Reference to assign the calculated hash. - * @param block_id Id of the block to be hashed. - * @param filefd Open file descriptor for the state data file. - * @param relpath Relative path of the state data file. - * @return 0 on success. -1 on failure. - */ -int hashmap_builder::compute_blockhash(hasher::B2H &hash, const uint32_t block_id, const int filefd, const std::string &relpath) -{ - // Allocating block buffer on the heap to avoid filling limited stack space. - std::unique_ptr block_buf = std::make_unique(BLOCK_SIZE); - const off_t block_offset = BLOCK_SIZE * block_id; - size_t bytes_read = pread(filefd, block_buf.get(), BLOCK_SIZE, block_offset); - if (bytes_read == -1) - { - LOG_ERR << errno << ": Read failed " << relpath; - return -1; - } - - hash = hasher::hash(&block_offset, 8, block_buf.get(), bytes_read); - return 0; -} - -/** - * Saves the block hash map into the relevant .bhmap file. - * @param bhmap_file Full path to the block hash map file. - * @param hashes Pointer to the hashes array containing the root hash and block hashes. - * @param hashes_size Byte length of the hashes array. - * @return 0 on success. -1 on failure. - */ -int hashmap_builder::write_block_hashmap(const std::string &bhmap_file, const hasher::B2H *hashes, const off_t hashes_size) -{ - int hmapfd = open(bhmap_file.c_str(), O_RDWR | O_TRUNC | O_CREAT, FILE_PERMS); - if (hmapfd == -1) - { - LOG_ERR << errno << ": Open failed " << bhmap_file; - return -1; - } - - // Write the updated hash list into the block hash map file. - if (pwrite(hmapfd, hashes, hashes_size, 0) == -1) - { - LOG_ERR << errno << ": Write failed " << bhmap_file; - close(hmapfd); - return -1; - } - - if (ftruncate(hmapfd, hashes_size) == -1) - { - LOG_ERR << errno << ": Truncate failed " << bhmap_file; - close(hmapfd); - return -1; - } - - close(hmapfd); -} - -/** - * Updates a file hash and adjust parent dir hash of the hash tree. - * @param parent_dir_hash Current hash of the parent dir. This will be assigned the new hash after the update. - * @param old_bhmap_exists Whether the block hash map file already exists or not. - * @param old_file_hash Old file hash. (0000 if this is a new file) - * @param new_file_hash New file hash. - * @param bhmap_file Full path to the block hash map file. - * @param relpath Relative path to the state data file. - * @return 0 on success. -1 on failure. - */ -int hashmap_builder::update_hashtree_entry(hasher::B2H &parent_dir_hash, const bool old_bhmap_exists, const hasher::B2H old_file_hash, - const hasher::B2H new_file_hash, const std::string &bhmap_file, const std::string &relpath) -{ - std::string hardlink_dir(ctx.hashtree_dir); - const std::string relpath_dir = boost::filesystem::path(relpath).parent_path().string(); - - hardlink_dir.append(relpath_dir); - if (relpath_dir != "/") - hardlink_dir.append("/"); - - std::stringstream new_hl_path; - new_hl_path << hardlink_dir << new_file_hash << ".rh"; - - // TODO: Even though we maintain hardlinks named after the file hash, we don't actually utilize them elsewhere. - // The intention is to be able to get a hash listing of the entire directory. Such ability is useful to serve state - // requests. However since state requests need the file name along with the hash we have to resort to iterating each - // .bhmap file and reading the file hash from first 32 bytes. - - if (old_bhmap_exists) - { - // Rename the existing hard link if old block hash map existed. - // We thereby assume the old hard link also existed. - std::stringstream oldhlpath; - oldhlpath << hardlink_dir << old_file_hash << ".rh"; - if (rename(oldhlpath.str().c_str(), new_hl_path.str().c_str()) == -1) - return -1; - - // Subtract the old root hash and add the new root hash from the parent dir hash. - parent_dir_hash ^= old_file_hash; - parent_dir_hash ^= new_file_hash; - } - else - { - // Create a new hard link with new root hash as the name. - if (link(bhmap_file.c_str(), new_hl_path.str().c_str()) == -1) - return -1; - - // Add the new root hash to parent hash. - parent_dir_hash ^= new_file_hash; - } - - return 0; -} - -/** - * Removes an existing block hash map file. Caled when deleting a state data file. - * @param parent_dir_hash Current hash of the parent dir. This will be assigned the new hash after the update. - * @param Full path to the block hash map file. - * @return 0 on success. -1 on failure. - */ -int hashmap_builder::remove_hashmap_file(hasher::B2H &parent_dir_hash, const std::string &bhmap_file) -{ - if (boost::filesystem::exists(bhmap_file)) - { - int hmapfd = open(bhmap_file.data(), O_RDONLY); - if (hmapfd == -1) - { - LOG_ERR << errno << ": Open failed " << bhmap_file; - return -1; - } - - hasher::B2H filehash; - if (read(hmapfd, &filehash, hasher::HASH_SIZE) == -1) - { - LOG_ERR << errno << ": Read failed " << bhmap_file; - close(hmapfd); - return -1; - } - - // Delete the .bhmap file. - if (remove(bhmap_file.c_str()) == -1) - { - LOG_ERR << errno << ": Delete failed " << bhmap_file; - close(hmapfd); - return -1; - } - - // Delete the hardlink of the .bhmap file. - std::string hardlink_dir(ctx.hashtree_dir); - const std::string relpath = get_relpath(bhmap_file, ctx.block_hashmap_dir); - const std::string relpath_dir = boost::filesystem::path(relpath).parent_path().string(); - - hardlink_dir.append(relpath_dir); - if (relpath_dir != "/") - hardlink_dir.append("/"); - - std::stringstream hlpath; - hlpath << hardlink_dir << filehash << ".rh"; - if (remove(hlpath.str().c_str()) == -1) - { - LOG_ERR << errno << ": Delete failed for hard link " << filehash << " of " << bhmap_file; - close(hmapfd); - return -1; - } - - // XOR parent dir hash with file hash so the file hash gets removed from parent dir hash. - parent_dir_hash ^= filehash; - close(hmapfd); - } - - return 0; -} - -} // namespace statefs \ No newline at end of file diff --git a/src/statefs/hashmap_builder.hpp b/src/statefs/hashmap_builder.hpp deleted file mode 100644 index e554746f..00000000 --- a/src/statefs/hashmap_builder.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef _HP_STATEFS_HASHMAP_BUILDER_ -#define _HP_STATEFS_HASHMAP_BUILDER_ - -#include "../pchheader.hpp" -#include "hasher.hpp" -#include "state_common.hpp" - -namespace statefs -{ - -class hashmap_builder -{ -private: - const state_dir_context ctx; - // List of new block hash map sub directories created during the session. - std::unordered_set created_bhmapsubdirs; - - int read_block_hashmap(std::vector &bhmap_data, std::string &hmapfile, const std::string &relpath); - int get_delta_block_index(std::map &idxmap, uint32_t &total_block_count, const std::string &file_relpath); - int update_hashes_with_backup_block_hints( - hasher::B2H *hashes, const off_t hashes_size, const std::string &relpath, const int orifd, - const uint32_t block_count, const uint32_t original_block_count, const std::map &bindex, const std::vector &bhmap_data); - int update_hashes_with_changed_block_hints( - hasher::B2H *hashes, const off_t hashes_size, const std::string &relpath, const int orifd, - const uint32_t block_count, const std::map &bindex, const std::vector &bhmap_data); - int compute_blockhash(hasher::B2H &hash, const uint32_t block_id, const int filefd, const std::string &relpath); - int write_block_hashmap(const std::string &bhmap_file, const hasher::B2H *hashes, const off_t hashes_size); - int update_hashtree_entry(hasher::B2H &parent_dir_hash, const bool old_bhmap_exists, const hasher::B2H old_file_hash, const hasher::B2H new_file_hash, const std::string &bhmap_file, const std::string &relpath); - -public: - hashmap_builder(const state_dir_context &ctx); - int generate_hashmap_for_file(hasher::B2H &parent_dir_hash, const std::string &filepath, const std::string &file_relpath, const std::map &changed_blocks); - int remove_hashmap_file(hasher::B2H &parent_dir_hash, const std::string &filepath); -}; - -} // namespace statefs - -#endif diff --git a/src/statefs/hashtree_builder.cpp b/src/statefs/hashtree_builder.cpp deleted file mode 100644 index 30d23899..00000000 --- a/src/statefs/hashtree_builder.cpp +++ /dev/null @@ -1,299 +0,0 @@ -#include "../pchheader.hpp" -#include "hashtree_builder.hpp" -#include "state_restore.hpp" -#include "state_common.hpp" - -namespace statefs -{ - -hashtree_builder::hashtree_builder(const state_dir_context &ctx) : ctx(ctx), hmapbuilder(ctx) -{ - force_rebuild_all = false; - hint_mode = false; -} - -int hashtree_builder::generate(hasher::B2H &root_hash) -{ - // Load modified file path hints if available. - populate_hint_paths_from_idx_file(IDX_TOUCHED_FILES); - populate_hint_paths_from_idx_file(IDX_NEW_FILES); - hint_mode = !hint_paths.empty(); - - return traverse_and_generate(root_hash); -} - -int hashtree_builder::generate(hasher::B2H &root_hash, const bool force_all) -{ - force_rebuild_all = force_all; - if (force_rebuild_all) - { - boost::filesystem::remove_all(ctx.block_hashmap_dir); - boost::filesystem::remove_all(ctx.hashtree_dir); - - boost::filesystem::create_directories(ctx.block_hashmap_dir); - boost::filesystem::create_directories(ctx.hashtree_dir); - } - - return traverse_and_generate(root_hash); -} - -int hashtree_builder::generate(hasher::B2H &root_hash, const std::unordered_map> &touched_files) -{ - hint_mode = true; - file_block_index = touched_files; - for (const auto &[relpath, bindex] : touched_files) - insert_hint_path(relpath); - - return traverse_and_generate(root_hash); -} - -int hashtree_builder::traverse_and_generate(hasher::B2H &root_hash) -{ - // Load current root hash if exist. - const std::string dir_hash_file = ctx.hashtree_dir + "/" + DIR_HASH_FNAME; - root_hash = get_existing_dir_hash(dir_hash_file); - - traversel_rootdir = ctx.data_dir; - removal_mode = false; - if (update_hashtree(root_hash) != 0) - return -1; - - // If there are any remaining hint files directly under this directory, that means - // those files are no longer there. So we need to delete the corresponding .bhmap and rh files - // and adjust the directory hash accordingly. - if (hint_mode && !hint_paths.empty()) - { - traversel_rootdir = ctx.block_hashmap_dir; - removal_mode = true; - if (update_hashtree(root_hash) != 0) - return -1; - } - - return 0; -} - -int hashtree_builder::update_hashtree(hasher::B2H &root_hash) -{ - hintpath_map::iterator hint_dir_itr = hint_paths.end(); - if (!should_process_dir(hint_dir_itr, traversel_rootdir)) - return 0; - - if (update_hashtree_fordir(root_hash, traversel_rootdir, hint_dir_itr, true) != 0) - return -1; - - return 0; -} - -int hashtree_builder::update_hashtree_fordir(hasher::B2H &parent_dir_hash, const std::string &dirpath, const hintpath_map::iterator hint_dir_itr, const bool is_root_level) -{ - const std::string htree_dirpath = switch_base_path(dirpath, traversel_rootdir, ctx.hashtree_dir); - - // Load current dir hash if exist. - const std::string dir_hash_file = htree_dirpath + "/" + DIR_HASH_FNAME; - hasher::B2H dir_hash = get_existing_dir_hash(dir_hash_file); - - // Remember the dir hash before we mutate it. - hasher::B2H original_dir_hash = dir_hash; - - // Iterate files/subdirs inside this dir. - const boost::filesystem::directory_iterator itr_end; - for (boost::filesystem::directory_iterator itr(dirpath); itr != itr_end; itr++) - { - const bool is_dir = boost::filesystem::is_directory(itr->path()); - const std::string path_str = itr->path().string(); - - if (is_dir) - { - hintpath_map::iterator hint_subdir_itr = hint_paths.end(); - if (!should_process_dir(hint_subdir_itr, path_str)) - continue; - - if (update_hashtree_fordir(dir_hash, path_str, hint_subdir_itr, false) != 0) - return -1; - } - else - { - if (!should_process_file(hint_dir_itr, path_str)) - continue; - - if (process_file(dir_hash, path_str, htree_dirpath) != 0) - return -1; - } - } - - // If there are no more files in the hint dir, delete the hint dir entry as well. - if (hint_dir_itr != hint_paths.end() && hint_dir_itr->second.empty()) - hint_paths.erase(hint_dir_itr); - - // In removalmode, we check whether the dir is empty. If so we remove the dir as well. - if (removal_mode && boost::filesystem::is_empty(dirpath)) - { - // We remove the dirs if we are below root level only. - // Otherwise we only remove root dir.hash file. - if (!is_root_level) - { - boost::filesystem::remove_all(dirpath); - boost::filesystem::remove_all(htree_dirpath); - } - else - { - boost::filesystem::remove(dir_hash_file); - } - - // Subtract the original dir hash from the parent dir hash. - parent_dir_hash ^= original_dir_hash; - } - else if (dir_hash != original_dir_hash) - { - // If dir hash has changed, write it back to dir hash file. - if (save_dir_hash(dir_hash_file, dir_hash) == -1) - return -1; - - // Also update the parent dir hash by subtracting the old hash and adding the new hash. - parent_dir_hash ^= original_dir_hash; - parent_dir_hash ^= dir_hash; - } - else - { - parent_dir_hash = dir_hash; - } - - return 0; -} - -hasher::B2H hashtree_builder::get_existing_dir_hash(const std::string &dir_hash_file) -{ - // Load current dir hash if exist. - hasher::B2H dir_hash = hasher::B2H_empty; - int dir_hash_fd = open(dir_hash_file.c_str(), O_RDONLY); - if (dir_hash_fd > 0) - { - read(dir_hash_fd, &dir_hash, hasher::HASH_SIZE); - close(dir_hash_fd); - } - return dir_hash; -} - -int hashtree_builder::save_dir_hash(const std::string &dir_hash_file, hasher::B2H dir_hash) -{ - int dir_hash_fd = open(dir_hash_file.c_str(), O_RDWR | O_TRUNC | O_CREAT, FILE_PERMS); - if (dir_hash_fd == -1) - return -1; - - if (write(dir_hash_fd, &dir_hash, hasher::HASH_SIZE) == -1) - { - close(dir_hash_fd); - return -1; - } - - close(dir_hash_fd); - return 0; -} - -inline bool hashtree_builder::should_process_dir(hintpath_map::iterator &dir_itr, const std::string &dirpath) -{ - if (force_rebuild_all) - return true; - - return (hint_mode ? get_hinteddir_match(dir_itr, dirpath) : true); -} - -bool hashtree_builder::should_process_file(const hintpath_map::iterator hint_dir_itr, const std::string filepath) -{ - if (force_rebuild_all) - return true; - - if (hint_mode) - { - if (hint_dir_itr == hint_paths.end()) - return false; - - std::string relpath = get_relpath(filepath, traversel_rootdir); - - // If in removal mode, we are traversing .bhmap files. Hence we should truncate .bhmap extension - // before we search for the path in file hints. - if (removal_mode) - relpath = relpath.substr(0, relpath.length() - BLOCK_HASHMAP_EXT_LEN); - - std::unordered_set &hint_files = hint_dir_itr->second; - const auto hint_file_itr = hint_files.find(relpath); - if (hint_file_itr == hint_files.end()) - return false; - - // Erase the visiting filepath from hint files. - hint_files.erase(hint_file_itr); - } - return true; -} - -int hashtree_builder::process_file(hasher::B2H &parent_dir_hash, const std::string &filepath, const std::string &htree_dirpath) -{ - if (!removal_mode) - { - // Create directory tree if not exist so we are able to create the file root hash files (hard links). - if (created_htree_subdirs.count(htree_dirpath) == 0) - { - boost::filesystem::create_directories(htree_dirpath); - created_htree_subdirs.emplace(htree_dirpath); - } - - const std::string relpath = get_relpath(filepath, ctx.data_dir); - const std::map &changed_blocks = file_block_index[relpath]; - - if (hmapbuilder.generate_hashmap_for_file(parent_dir_hash, filepath, relpath, changed_blocks) == -1) - return -1; - } - else - { - if (hmapbuilder.remove_hashmap_file(parent_dir_hash, filepath) == -1) - return -1; - } - - return 0; -} - -void hashtree_builder::populate_hint_paths_from_idx_file(const char *const idxfile) -{ - std::ifstream in_file(std::string(ctx.delta_dir).append(idxfile)); - if (!in_file.fail()) - { - for (std::string relpath; std::getline(in_file, relpath);) - insert_hint_path(relpath); - in_file.close(); - } -} - -void hashtree_builder::insert_hint_path(const std::string &relpath) -{ - boost::filesystem::path p_relpath(relpath); - std::string parent_dir = p_relpath.parent_path().string(); - hint_paths[parent_dir].emplace(relpath); -} - -bool hashtree_builder::get_hinteddir_match(hintpath_map::iterator &match_itr, const std::string &dirpath) -{ - // First check whether there's an exact match. If not check for a partial match. - // Exact match will return the iterator. Partial match or not found will return end() iterator. - const std::string relpath = get_relpath(dirpath, traversel_rootdir); - const auto exact_match_itr = hint_paths.find(relpath); - - if (exact_match_itr != hint_paths.end()) - { - match_itr = exact_match_itr; - return true; - } - - for (auto itr = hint_paths.begin(); itr != hint_paths.end(); itr++) - { - if (strncmp(relpath.c_str(), itr->first.c_str(), relpath.length()) == 0) - { - // Partial match found. - match_itr = hint_paths.end(); - return true; - } - } - - return false; // Not found at all. -} - -} // namespace statefs \ No newline at end of file diff --git a/src/statefs/hashtree_builder.hpp b/src/statefs/hashtree_builder.hpp deleted file mode 100644 index ef2d582d..00000000 --- a/src/statefs/hashtree_builder.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef _HP_STATEFS_HASHTREE_BUILDER_ -#define _HP_STATEFS_HASHTREE_BUILDER_ - -#include "../pchheader.hpp" -#include "hasher.hpp" -#include "hashmap_builder.hpp" -#include "state_common.hpp" - -namespace statefs -{ - -typedef std::unordered_map> hintpath_map; - -class hashtree_builder -{ -private: - const state_dir_context ctx; - hashmap_builder hmapbuilder; - - // Hint path map with parent dir as key and list of file paths under each parent dir. - hintpath_map hint_paths; - bool force_rebuild_all; - bool hint_mode; - bool removal_mode; - std::string traversel_rootdir; - std::unordered_map> file_block_index; - - // List of new root hash map sub directories created during the session. - std::unordered_set created_htree_subdirs; - - int traverse_and_generate(hasher::B2H &root_hash); - int update_hashtree(hasher::B2H &root_hash); - int update_hashtree_fordir(hasher::B2H &parent_dir_hash, const std::string &relpath, const hintpath_map::iterator hint_dir_itr, const bool is_root_level); - - hasher::B2H get_existing_dir_hash(const std::string &dir_hash_file); - int save_dir_hash(const std::string &dir_hash_file, hasher::B2H dir_hash); - bool should_process_dir(hintpath_map::iterator &hint_subdir_itr, const std::string &dirpath); - bool should_process_file(const hintpath_map::iterator hint_dir_itr, const std::string filepath); - int process_file(hasher::B2H &parent_dir_hash, const std::string &filepath, const std::string &htree_dirpath); - int update_hashtree_entry(hasher::B2H &parent_dir_hash, const bool old_bhmap_exists, const hasher::B2H old_file_hash, const hasher::B2H new_file_hash, const std::string &bhmap_file, const std::string &relpath); - void populate_hint_paths_from_idx_file(const char *const idxfile); - void insert_hint_path(const std::string &relpath); - bool get_hinteddir_match(hintpath_map::iterator &match_itr, const std::string &dirpath); - -public: - hashtree_builder(const state_dir_context &ctx); - int generate(hasher::B2H &root_hash); - int generate(hasher::B2H &root_hash, const bool force_all); - int generate(hasher::B2H &root_hash, const std::unordered_map> &touched_files); -}; - -} // namespace statefs - -#endif diff --git a/src/statefs/state_common.cpp b/src/statefs/state_common.cpp deleted file mode 100644 index ba55755a..00000000 --- a/src/statefs/state_common.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include -#include -#include "state_common.hpp" - -namespace statefs -{ - -std::string state_hist_dir; -state_dir_context current_ctx; - -void init(const std::string &state_hist_dir_root) -{ - state_hist_dir = realpath(state_hist_dir_root.c_str(), NULL); - - // Initialize 0 state (current state) directory. - current_ctx = get_state_dir_context(0, true); -} - -std::string get_state_dir_root(const int16_t checkpoint_id) -{ - return state_hist_dir + "/" + std::to_string(checkpoint_id); -} - -state_dir_context get_state_dir_context(const int16_t checkpoint_id, const bool create_dirs) -{ - state_dir_context ctx; - ctx.root_dir = get_state_dir_root(checkpoint_id); - ctx.data_dir = ctx.root_dir + DATA_DIR; - ctx.block_hashmap_dir = ctx.root_dir + BHMAP_DIR; - ctx.hashtree_dir = ctx.root_dir + HTREE_DIR; - ctx.delta_dir = ctx.root_dir + DELTA_DIR; - - if (create_dirs) - { - if (!boost::filesystem::exists(ctx.data_dir)) - boost::filesystem::create_directories(ctx.data_dir); - if (!boost::filesystem::exists(ctx.block_hashmap_dir)) - boost::filesystem::create_directories(ctx.block_hashmap_dir); - if (!boost::filesystem::exists(ctx.hashtree_dir)) - boost::filesystem::create_directories(ctx.hashtree_dir); - if (!boost::filesystem::exists(ctx.delta_dir)) - boost::filesystem::create_directories(ctx.delta_dir); - } - - return ctx; -} - -std::string get_relpath(const std::string &fullpath, const std::string &base_path) -{ - std::string relpath = fullpath.substr(base_path.length(), fullpath.length() - base_path.length()); - if (relpath.empty()) - relpath = "/"; - return relpath; -} - -std::string switch_base_path(const std::string &fullpath, const std::string &from_base_path, const std::string &to_base_path) -{ - return to_base_path + get_relpath(fullpath, from_base_path); -} - -} // namespace statefs \ No newline at end of file diff --git a/src/statefs/state_common.hpp b/src/statefs/state_common.hpp deleted file mode 100644 index 09e1a268..00000000 --- a/src/statefs/state_common.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#ifndef _HP_STATEFS_STATE_COMMON_ -#define _HP_STATEFS_STATE_COMMON_ - -#include -#include -#include "hasher.hpp" - -namespace statefs -{ - -// Max number of state history checkpoints to keep. -constexpr int16_t MAX_CHECKPOINTS = 5; - -// Cache block size. -constexpr size_t BLOCK_SIZE = 4 * 1024 * 1024; // 4MB - -// Cache block index entry bytes length. -constexpr size_t BLOCK_INDEX_ENTRY_SIZE = 44; - -// Permissions used when creating block cache and index files. -constexpr int FILE_PERMS = 0644; - -const char *const BLOCK_HASHMAP_EXT = ".bhmap"; -constexpr size_t BLOCK_HASHMAP_EXT_LEN = 6; - -const char *const BLOCK_INDEX_EXT = ".bindex"; -constexpr size_t BLOCK_INDEX_EXT_LEN = 7; - -const char *const BLOCK_CACHE_EXT = ".bcache"; -constexpr size_t BLOCK_CACHE_EXT_LEN = 7; - -const char *const IDX_NEW_FILES = "/idxnew.idx"; -const char *const IDX_TOUCHED_FILES = "/idxtouched.idx"; -const char *const DIR_HASH_FNAME = "dir.hash"; - -const char *const DATA_DIR = "/data"; -const char *const BHMAP_DIR = "/bhmap"; -const char *const HTREE_DIR = "/htree"; -const char *const DELTA_DIR = "/delta"; - -/** - * Context struct to hold all state-related directory paths. - */ -struct state_dir_context -{ - std::string root_dir; // Directory holding state sub dirs. - std::string data_dir; // Directory containing smart contract data. - std::string block_hashmap_dir; // Directory containing block hash map files. - std::string hashtree_dir; // Directory containing hash tree files (dir.hash and hard links). - std::string delta_dir; // Directory containing original smart contract data. -}; - -// Container directory to contain all checkpoints. -extern std::string state_hist_dir; - -// Currently loaded state checkpoint directory context (usually checkpoint 0) -extern state_dir_context current_ctx; - -void init(const std::string &state_hist_dir_root); -std::string get_state_dir_root(const int16_t checkpoint_id); -state_dir_context get_state_dir_context(int16_t checkpoint_id = 0, bool create_dirs = false); -std::string get_relpath(const std::string &fullpath, const std::string &base_path); -std::string switch_base_path(const std::string &fullpath, const std::string &from_base_path, const std::string &to_base_path); - -} // namespace statefs - -#endif \ No newline at end of file diff --git a/src/statefs/state_monitor/fusefs.cpp b/src/statefs/state_monitor/fusefs.cpp deleted file mode 100644 index dd8584c9..00000000 --- a/src/statefs/state_monitor/fusefs.cpp +++ /dev/null @@ -1,1380 +0,0 @@ -/* - * Code copied and adopted from https://github.com/libfuse/libfuse/blob/master/example/passthrough_hp.cc - */ - -#define FUSE_USE_VERSION 35 - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#ifndef _GNU_SOURCE -#define _GNU_SOURCE -#endif - -// C includes -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// C++ includes -#include -#include -#include - -#include "../../pchheader.hpp" -#include "../state_common.hpp" -#include "state_monitor.hpp" - -using namespace std; - -// Uniquely identifies a file in the source directory tree. This could -// be simplified to just ino_t since we require the source directory -// not to contain any mountpoints. This hasn't been done yet in case -// we need to reconsider this constraint (but relaxing this would have -// the drawback that we can no longer re-use inode numbers, and thus -// readdir() would need to do a full lookup() in order to report the -// right inode number). -typedef std::pair SrcId; - -// Define a hash function for SrcId -namespace std -{ -template <> -struct hash -{ - size_t operator()(const SrcId &id) const - { - return hash{}(id.first) ^ hash{}(id.second); - } -}; -} // namespace std - -namespace helpers -{ - -int getfilepath(std::string &filepath, int parentfd, const char *filename) -{ - // Get parent directory path using the parentfd. - char proclnk[32]; - char parentpath[PATH_MAX]; - sprintf(proclnk, "/proc/self/fd/%d", parentfd); - ssize_t parentlen = readlink(proclnk, parentpath, PATH_MAX); - if (parentlen > 0) - { - // Concat parent dir path and filename to get the full path. - filepath.reserve(parentlen + strlen(filename) + 1); - filepath.append(parentpath, parentlen).append("/").append(filename); - return 0; - } - return -1; -} - -} // namespace helpers - -namespace fusefs -{ - -/* We are re-using pointers to our `struct sfs_inode` and `struct - sfs_dirp` elements as inodes and file handles. This means that we - must be able to store pointer a pointer in both a fuse_ino_t - variable and a uint64_t variable (used for file handles). */ -static_assert(sizeof(fuse_ino_t) >= sizeof(void *), - "void* must fit into fuse_ino_t"); -static_assert(sizeof(fuse_ino_t) >= sizeof(uint64_t), - "fuse_ino_t must be at least 64 bits"); - -/* Forward declarations */ -struct Inode; -static Inode &get_inode(fuse_ino_t ino); -static void forget_one(fuse_ino_t ino, uint64_t n); - -// Maps files in the source directory tree to inodes -typedef std::unordered_map InodeMap; - -struct Inode -{ - int fd{-1}; - bool is_symlink{false}; - dev_t src_dev{0}; - ino_t src_ino{0}; - uint64_t nlookup{0}; - std::mutex m; - - // Delete copy constructor and assignments. We could implement - // move if we need it. - Inode() = default; - Inode(const Inode &) = delete; - Inode(Inode &&inode) = delete; - Inode &operator=(Inode &&inode) = delete; - Inode &operator=(const Inode &) = delete; - - ~Inode() - { - if (fd > 0) - close(fd); - } -}; - -struct Fs -{ - // Must be acquired *after* any Inode.m locks. - std::mutex mutex; - InodeMap inodes; // protected by mutex - Inode root; - double timeout; - bool debug; - std::string source; - size_t blocksize; - dev_t src_dev; - bool nosplice; - bool nocache; -}; -static Fs fs{}; -static statefs::state_monitor statemonitor; - -#define FUSE_BUF_COPY_FLAGS \ - (fs.nosplice ? FUSE_BUF_NO_SPLICE : static_cast(0)) - -static Inode &get_inode(fuse_ino_t ino) -{ - if (ino == FUSE_ROOT_ID) - return fs.root; - - Inode *inode = reinterpret_cast(ino); - if (inode->fd == -1) - { - cerr << "INTERNAL ERROR: Unknown inode " << ino << endl; - abort(); - } - return *inode; -} - -static int get_fs_fd(fuse_ino_t ino) -{ - int fd = get_inode(ino).fd; - return fd; -} - -static void sfs_init(void *userdata, fuse_conn_info *conn) -{ - (void)userdata; - if (conn->capable & FUSE_CAP_EXPORT_SUPPORT) - conn->want |= FUSE_CAP_EXPORT_SUPPORT; - - if (fs.timeout && conn->capable & FUSE_CAP_WRITEBACK_CACHE) - conn->want |= FUSE_CAP_WRITEBACK_CACHE; - - if (conn->capable & FUSE_CAP_FLOCK_LOCKS) - conn->want |= FUSE_CAP_FLOCK_LOCKS; - - // Use splicing if supported. Since we are using writeback caching - // and readahead, individual requests should have a decent size so - // that splicing between fd's is well worth it. - if (conn->capable & FUSE_CAP_SPLICE_WRITE && !fs.nosplice) - conn->want |= FUSE_CAP_SPLICE_WRITE; - if (conn->capable & FUSE_CAP_SPLICE_READ && !fs.nosplice) - conn->want |= FUSE_CAP_SPLICE_READ; -} - -static void sfs_getattr(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) -{ - (void)fi; - Inode &inode = get_inode(ino); - struct stat attr; - auto res = fstatat(inode.fd, "", &attr, - AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1) - { - fuse_reply_err(req, errno); - return; - } - fuse_reply_attr(req, &attr, fs.timeout); -} - -#ifdef HAVE_UTIMENSAT -static int utimensat_empty_nofollow(Inode &inode, - const struct timespec *tv) -{ - if (inode.is_symlink) - { - /* Does not work on current kernels, but may in the future: - https://marc.info/?l=linux-kernel&m=154158217810354&w=2 */ - auto res = utimensat(inode.fd, "", tv, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1 && errno == EINVAL) - { - /* Sorry, no race free way to set times on symlink. */ - errno = EPERM; - } - return res; - } - - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", inode.fd); - - return utimensat(AT_FDCWD, procname, tv, 0); -} -#endif - -static void do_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, - int valid, struct fuse_file_info *fi) -{ - Inode &inode = get_inode(ino); - int ifd = inode.fd; - int res; - - if (valid & FUSE_SET_ATTR_MODE) - { - if (fi) - { - res = fchmod(fi->fh, attr->st_mode); - } - else - { - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", ifd); - res = chmod(procname, attr->st_mode); - } - if (res == -1) - goto out_err; - } - if (valid & (FUSE_SET_ATTR_UID | FUSE_SET_ATTR_GID)) - { - uid_t uid = (valid & FUSE_SET_ATTR_UID) ? attr->st_uid : static_cast(-1); - gid_t gid = (valid & FUSE_SET_ATTR_GID) ? attr->st_gid : static_cast(-1); - - res = fchownat(ifd, "", uid, gid, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1) - goto out_err; - } - if (valid & FUSE_SET_ATTR_SIZE) - { - if (fi) - { - res = ftruncate(fi->fh, attr->st_size); - } - else - { - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", ifd); - res = truncate(procname, attr->st_size); - } - if (res == -1) - goto out_err; - } - if (valid & (FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME)) - { - struct timespec tv[2]; - - tv[0].tv_sec = 0; - tv[1].tv_sec = 0; - tv[0].tv_nsec = UTIME_OMIT; - tv[1].tv_nsec = UTIME_OMIT; - - if (valid & FUSE_SET_ATTR_ATIME_NOW) - tv[0].tv_nsec = UTIME_NOW; - else if (valid & FUSE_SET_ATTR_ATIME) - tv[0] = attr->st_atim; - - if (valid & FUSE_SET_ATTR_MTIME_NOW) - tv[1].tv_nsec = UTIME_NOW; - else if (valid & FUSE_SET_ATTR_MTIME) - tv[1] = attr->st_mtim; - - if (fi) - res = futimens(fi->fh, tv); - else - { -#ifdef HAVE_UTIMENSAT - res = utimensat_empty_nofollow(inode, tv); -#else - res = -1; - errno = EOPNOTSUPP; -#endif - } - if (res == -1) - goto out_err; - } - return sfs_getattr(req, ino, fi); - -out_err: - fuse_reply_err(req, errno); -} - -static void sfs_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, - int valid, fuse_file_info *fi) -{ - // We use some conditions to detect truncate call. - if (fi != NULL && fi->fh > 0 && attr->st_size > 0) - statemonitor.ontruncate(fi->fh, attr->st_size); - - (void)ino; - do_setattr(req, ino, attr, valid, fi); -} - -static int do_lookup(fuse_ino_t parent, const char *name, - fuse_entry_param *e) -{ - if (fs.debug) - cerr << "DEBUG: lookup(): name=" << name - << ", parent=" << parent << endl; - memset(e, 0, sizeof(*e)); - e->attr_timeout = fs.timeout; - e->entry_timeout = fs.timeout; - - auto newfd = openat(get_fs_fd(parent), name, O_PATH | O_NOFOLLOW); - if (newfd == -1) - return errno; - - auto res = fstatat(newfd, "", &e->attr, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1) - { - auto saveerr = errno; - close(newfd); - if (fs.debug) - cerr << "DEBUG: lookup(): fstatat failed" << endl; - return saveerr; - } - - if (e->attr.st_dev != fs.src_dev) - { - cerr << "WARNING: Mountpoints in the source directory tree will be hidden." << endl; - return ENOTSUP; - } - else if (e->attr.st_ino == FUSE_ROOT_ID) - { - cerr << "ERROR: Source directory tree must not include inode " - << FUSE_ROOT_ID << endl; - return EIO; - } - - SrcId id{e->attr.st_ino, e->attr.st_dev}; - unique_lock fs_lock{fs.mutex}; - Inode *inode_p; - try - { - inode_p = &fs.inodes[id]; - } - catch (std::bad_alloc &) - { - return ENOMEM; - } - e->ino = reinterpret_cast(inode_p); - Inode &inode{*inode_p}; - - if (inode.fd != -1) - { // found existing inode - fs_lock.unlock(); - if (fs.debug) - cerr << "DEBUG: lookup(): inode " << e->attr.st_ino - << " (userspace) already known." << endl; - lock_guard g{inode.m}; - inode.nlookup++; - close(newfd); - } - else - { // no existing inode - /* This is just here to make Helgrind happy. It violates the - lock ordering requirement (inode.m must be acquired before - fs.mutex), but this is of no consequence because at this - point no other thread has access to the inode mutex */ - lock_guard g{inode.m}; - inode.src_ino = e->attr.st_ino; - inode.src_dev = e->attr.st_dev; - inode.is_symlink = S_ISLNK(e->attr.st_mode); - inode.nlookup = 1; - inode.fd = newfd; - fs_lock.unlock(); - - if (fs.debug) - cerr << "DEBUG: lookup(): created userspace inode " << e->attr.st_ino - << endl; - } - - return 0; -} - -static void sfs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) -{ - fuse_entry_param e{}; - auto err = do_lookup(parent, name, &e); - if (err == ENOENT) - { - e.attr_timeout = fs.timeout; - e.entry_timeout = fs.timeout; - e.ino = e.attr.st_ino = 0; - fuse_reply_entry(req, &e); - } - else if (err) - { - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - } - else - { - fuse_reply_entry(req, &e); - } -} - -static void mknod_symlink(fuse_req_t req, fuse_ino_t parent, - const char *name, mode_t mode, dev_t rdev, - const char *link) -{ - int res; - Inode &inode_p = get_inode(parent); - auto saverr = ENOMEM; - - if (S_ISDIR(mode)) - res = mkdirat(inode_p.fd, name, mode); - else if (S_ISLNK(mode)) - res = symlinkat(link, inode_p.fd, name); - else - res = mknodat(inode_p.fd, name, mode, rdev); - saverr = errno; - if (res == -1) - goto out; - - fuse_entry_param e; - saverr = do_lookup(parent, name, &e); - if (saverr) - goto out; - - fuse_reply_entry(req, &e); - return; - -out: - if (saverr == ENFILE || saverr == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, saverr); -} - -static void sfs_mknod(fuse_req_t req, fuse_ino_t parent, const char *name, - mode_t mode, dev_t rdev) -{ - mknod_symlink(req, parent, name, mode, rdev, nullptr); -} - -static void sfs_mkdir(fuse_req_t req, fuse_ino_t parent, const char *name, - mode_t mode) -{ - mknod_symlink(req, parent, name, S_IFDIR | mode, 0, nullptr); -} - -static void sfs_symlink(fuse_req_t req, const char *link, fuse_ino_t parent, - const char *name) -{ - mknod_symlink(req, parent, name, S_IFLNK, 0, link); -} - -static int linkat_empty_nofollow(Inode &inode, int dfd, const char *name) -{ - if (inode.is_symlink) - { - auto res = linkat(inode.fd, "", dfd, name, AT_EMPTY_PATH); - if (res == -1 && (errno == ENOENT || errno == EINVAL)) - { - /* Sorry, no race free way to hard-link a symlink. */ - errno = EOPNOTSUPP; - } - return res; - } - - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", inode.fd); - return linkat(AT_FDCWD, procname, dfd, name, AT_SYMLINK_FOLLOW); -} - -static void sfs_link(fuse_req_t req, fuse_ino_t ino, fuse_ino_t parent, - const char *name) -{ - Inode &inode = get_inode(ino); - Inode &inode_p = get_inode(parent); - fuse_entry_param e{}; - - e.attr_timeout = fs.timeout; - e.entry_timeout = fs.timeout; - - auto res = linkat_empty_nofollow(inode, inode_p.fd, name); - if (res == -1) - { - fuse_reply_err(req, errno); - return; - } - - res = fstatat(inode.fd, "", &e.attr, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW); - if (res == -1) - { - fuse_reply_err(req, errno); - return; - } - e.ino = reinterpret_cast(&inode); - { - lock_guard g{inode.m}; - inode.nlookup++; - } - - fuse_reply_entry(req, &e); - return; -} - -static void sfs_rmdir(fuse_req_t req, fuse_ino_t parent, const char *name) -{ - Inode &inode_p = get_inode(parent); - lock_guard g{inode_p.m}; - auto res = unlinkat(inode_p.fd, name, AT_REMOVEDIR); - fuse_reply_err(req, res == -1 ? errno : 0); -} - -static void sfs_rename(fuse_req_t req, fuse_ino_t parent, const char *name, - fuse_ino_t newparent, const char *newname, - unsigned int flags) -{ - Inode &inode_p = get_inode(parent); - Inode &inode_np = get_inode(newparent); - if (flags) - { - fuse_reply_err(req, EINVAL); - return; - } - - // state monitor hook. - std::string oldfilepath, newfilepath; - if (helpers::getfilepath(oldfilepath, inode_p.fd, name) == 0 && - helpers::getfilepath(newfilepath, inode_np.fd, newname) == 0) - { - statemonitor.onrename(oldfilepath, newfilepath); - } - - auto res = renameat(inode_p.fd, name, inode_np.fd, newname); - fuse_reply_err(req, res == -1 ? errno : 0); -} - -static void sfs_unlink(fuse_req_t req, fuse_ino_t parent, const char *name) -{ - Inode &inode_p = get_inode(parent); - - // state monitor hook. - std::string filepath; - if (helpers::getfilepath(filepath, inode_p.fd, name) == 0) - statemonitor.ondelete(filepath); - - auto res = unlinkat(inode_p.fd, name, 0); - fuse_reply_err(req, res == -1 ? errno : 0); -} - -static void forget_one(fuse_ino_t ino, uint64_t n) -{ - Inode &inode = get_inode(ino); - unique_lock l{inode.m}; - - if (n > inode.nlookup) - { - cerr << "INTERNAL ERROR: Negative lookup count for inode " - << inode.src_ino << endl; - abort(); - } - inode.nlookup -= n; - if (!inode.nlookup) - { - if (fs.debug) - cerr << "DEBUG: forget: cleaning up inode " << inode.src_ino << endl; - { - lock_guard g_fs{fs.mutex}; - l.unlock(); - fs.inodes.erase({inode.src_ino, inode.src_dev}); - } - } - else if (fs.debug) - cerr << "DEBUG: forget: inode " << inode.src_ino - << " lookup count now " << inode.nlookup << endl; -} - -static void sfs_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) -{ - forget_one(ino, nlookup); - fuse_reply_none(req); -} - -static void sfs_forget_multi(fuse_req_t req, size_t count, - fuse_forget_data *forgets) -{ - for (int i = 0; i < count; i++) - forget_one(forgets[i].ino, forgets[i].nlookup); - fuse_reply_none(req); -} - -static void sfs_readlink(fuse_req_t req, fuse_ino_t ino) -{ - Inode &inode = get_inode(ino); - char buf[PATH_MAX + 1]; - auto res = readlinkat(inode.fd, "", buf, sizeof(buf)); - if (res == -1) - fuse_reply_err(req, errno); - else if (res == sizeof(buf)) - fuse_reply_err(req, ENAMETOOLONG); - else - { - buf[res] = '\0'; - fuse_reply_readlink(req, buf); - } -} - -struct DirHandle -{ - DIR *dp{nullptr}; - off_t offset; - - DirHandle() = default; - DirHandle(const DirHandle &) = delete; - DirHandle &operator=(const DirHandle &) = delete; - - ~DirHandle() - { - if (dp) - closedir(dp); - } -}; - -static DirHandle *get_dir_handle(fuse_file_info *fi) -{ - return reinterpret_cast(fi->fh); -} - -static void sfs_opendir(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) -{ - Inode &inode = get_inode(ino); - auto d = new (nothrow) DirHandle; - if (d == nullptr) - { - fuse_reply_err(req, ENOMEM); - return; - } - - // Make Helgrind happy - it can't know that there's an implicit - // synchronization due to the fact that other threads cannot - // access d until we've called fuse_reply_*. - lock_guard g{inode.m}; - - auto fd = openat(inode.fd, ".", O_RDONLY); - if (fd == -1) - goto out_errno; - - // On success, dir stream takes ownership of fd, so we - // do not have to close it. - d->dp = fdopendir(fd); - if (d->dp == nullptr) - goto out_errno; - - d->offset = 0; - - fi->fh = reinterpret_cast(d); - if (fs.timeout) - { - fi->keep_cache = 1; - fi->cache_readdir = 1; - } - fuse_reply_open(req, fi); - return; - -out_errno: - auto error = errno; - delete d; - if (error == ENFILE || error == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, error); -} - -static bool is_dot_or_dotdot(const char *name) -{ - return name[0] == '.' && - (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')); -} - -static void do_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t offset, fuse_file_info *fi, int plus) -{ - auto d = get_dir_handle(fi); - Inode &inode = get_inode(ino); - lock_guard g{inode.m}; - char *p; - auto rem = size; - int err = 0, count = 0; - - if (fs.debug) - cerr << "DEBUG: readdir(): started with offset " - << offset << endl; - - auto buf = new (nothrow) char[size]; - if (!buf) - { - fuse_reply_err(req, ENOMEM); - return; - } - p = buf; - - if (offset != d->offset) - { - if (fs.debug) - cerr << "DEBUG: readdir(): seeking to " << offset << endl; - seekdir(d->dp, offset); - d->offset = offset; - } - - while (1) - { - struct dirent *entry; - errno = 0; - entry = readdir(d->dp); - if (!entry) - { - if (errno) - { - err = errno; - if (fs.debug) - warn("DEBUG: readdir(): readdir failed with"); - goto error; - } - break; // End of stream - } - d->offset = entry->d_off; - if (is_dot_or_dotdot(entry->d_name)) - continue; - - fuse_entry_param e{}; - size_t entsize; - if (plus) - { - err = do_lookup(ino, entry->d_name, &e); - if (err) - goto error; - entsize = fuse_add_direntry_plus(req, p, rem, entry->d_name, &e, entry->d_off); - - if (entsize > rem) - { - if (fs.debug) - cerr << "DEBUG: readdir(): buffer full, returning data. " << endl; - forget_one(e.ino, 1); - break; - } - } - else - { - e.attr.st_ino = entry->d_ino; - e.attr.st_mode = entry->d_type << 12; - entsize = fuse_add_direntry(req, p, rem, entry->d_name, &e.attr, entry->d_off); - - if (entsize > rem) - { - if (fs.debug) - cerr << "DEBUG: readdir(): buffer full, returning data. " << endl; - break; - } - } - - p += entsize; - rem -= entsize; - count++; - if (fs.debug) - { - cerr << "DEBUG: readdir(): added to buffer: " << entry->d_name - << ", ino " << e.attr.st_ino << ", offset " << entry->d_off << endl; - } - } - err = 0; -error: - - // If there's an error, we can only signal it if we haven't stored - // any entries yet - otherwise we'd end up with wrong lookup - // counts for the entries that are already in the buffer. So we - // return what we've collected until that point. - if (err && rem == size) - { - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - } - else - { - if (fs.debug) - cerr << "DEBUG: readdir(): returning " << count - << " entries, curr offset " << d->offset << endl; - fuse_reply_buf(req, buf, size - rem); - } - delete[] buf; - return; -} - -static void sfs_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t offset, fuse_file_info *fi) -{ - // operation logging is done in readdir to reduce code duplication - do_readdir(req, ino, size, offset, fi, 0); -} - -static void sfs_readdirplus(fuse_req_t req, fuse_ino_t ino, size_t size, - off_t offset, fuse_file_info *fi) -{ - // operation logging is done in readdir to reduce code duplication - do_readdir(req, ino, size, offset, fi, 1); -} - -static void sfs_releasedir(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) -{ - (void)ino; - auto d = get_dir_handle(fi); - delete d; - fuse_reply_err(req, 0); -} - -static void sfs_create(fuse_req_t req, fuse_ino_t parent, const char *name, - mode_t mode, fuse_file_info *fi) -{ - Inode &inode_p = get_inode(parent); - - auto fd = openat(inode_p.fd, name, - (fi->flags | O_CREAT) & ~O_NOFOLLOW, mode); - if (fd == -1) - { - auto err = errno; - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - return; - } - - fi->fh = fd; - fuse_entry_param e; - auto err = do_lookup(parent, name, &e); - if (err) - { - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - } - else - { - // state monitor hook. - statemonitor.oncreate(fd); - fuse_reply_create(req, &e, fi); - } -} - -static void sfs_fsyncdir(fuse_req_t req, fuse_ino_t ino, int datasync, - fuse_file_info *fi) -{ - (void)ino; - int res; - int fd = dirfd(get_dir_handle(fi)->dp); - if (datasync) - res = fdatasync(fd); - else - res = fsync(fd); - fuse_reply_err(req, res == -1 ? errno : 0); -} - -static void sfs_open(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) -{ - Inode &inode = get_inode(ino); - - /* With writeback cache, kernel may send read requests even - when userspace opened write-only */ - if (fs.timeout && (fi->flags & O_ACCMODE) == O_WRONLY) - { - fi->flags &= ~O_ACCMODE; - fi->flags |= O_RDWR; - } - - /* With writeback cache, O_APPEND is handled by the kernel. This - breaks atomicity (since the file may change in the underlying - filesystem, so that the kernel's idea of the end of the file - isn't accurate anymore). However, no process should modify the - file in the underlying filesystem once it has been read, so - this is not a problem. */ - if (fs.timeout && fi->flags & O_APPEND) - fi->flags &= ~O_APPEND; - - /* Unfortunately we cannot use inode.fd, because this was opened - with O_PATH (so it doesn't allow read/write access). */ - char buf[64]; - sprintf(buf, "/proc/self/fd/%i", inode.fd); - - // state monitor hook. - statemonitor.onopen(inode.fd, fi->flags); - - auto fd = open(buf, fi->flags & ~O_NOFOLLOW); - if (fd == -1) - { - auto err = errno; - if (err == ENFILE || err == EMFILE) - cerr << "ERROR: Reached maximum number of file descriptors." << endl; - fuse_reply_err(req, err); - return; - } - - fi->keep_cache = (fs.timeout != 0); - fi->fh = fd; - - fuse_reply_open(req, fi); -} - -static void sfs_release(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) -{ - (void)ino; - close(fi->fh); - - // state monitor hook. - statemonitor.onclose(fi->fh); - - fuse_reply_err(req, 0); -} - -static void sfs_flush(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi) -{ - (void)ino; - auto res = close(dup(fi->fh)); - fuse_reply_err(req, res == -1 ? errno : 0); -} - -static void sfs_fsync(fuse_req_t req, fuse_ino_t ino, int datasync, - fuse_file_info *fi) -{ - (void)ino; - int res; - if (datasync) - res = fdatasync(fi->fh); - else - res = fsync(fi->fh); - fuse_reply_err(req, res == -1 ? errno : 0); -} - -static void do_read(fuse_req_t req, size_t size, off_t off, fuse_file_info *fi) -{ - - fuse_bufvec buf = FUSE_BUFVEC_INIT(size); - buf.buf[0].flags = static_cast( - FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK); - buf.buf[0].fd = fi->fh; - buf.buf[0].pos = off; - - fuse_reply_data(req, &buf, FUSE_BUF_COPY_FLAGS); -} - -static void sfs_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, - fuse_file_info *fi) -{ - (void)ino; - do_read(req, size, off, fi); -} - -static void do_write_buf(fuse_req_t req, size_t size, off_t off, - fuse_bufvec *in_buf, fuse_file_info *fi) -{ - fuse_bufvec out_buf = FUSE_BUFVEC_INIT(size); - out_buf.buf[0].flags = static_cast( - FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK); - out_buf.buf[0].fd = fi->fh; - out_buf.buf[0].pos = off; - - auto res = fuse_buf_copy(&out_buf, in_buf, FUSE_BUF_COPY_FLAGS); - if (res < 0) - fuse_reply_err(req, -res); - else - fuse_reply_write(req, (size_t)res); -} - -static void sfs_write_buf(fuse_req_t req, fuse_ino_t ino, fuse_bufvec *in_buf, - off_t off, fuse_file_info *fi) -{ - (void)ino; - auto size{fuse_buf_size(in_buf)}; - - // state monitor hook. - statemonitor.onwrite(fi->fh, off, size); - - do_write_buf(req, size, off, in_buf, fi); -} - -static void sfs_statfs(fuse_req_t req, fuse_ino_t ino) -{ - struct statvfs stbuf; - - auto res = fstatvfs(get_fs_fd(ino), &stbuf); - if (res == -1) - fuse_reply_err(req, errno); - else - fuse_reply_statfs(req, &stbuf); -} - -#ifdef HAVE_POSIX_FALLOCATE -static void sfs_fallocate(fuse_req_t req, fuse_ino_t ino, int mode, - off_t offset, off_t length, fuse_file_info *fi) -{ - (void)ino; - if (mode) - { - fuse_reply_err(req, EOPNOTSUPP); - return; - } - - auto err = posix_fallocate(fi->fh, offset, length); - fuse_reply_err(req, err); -} -#endif - -static void sfs_flock(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi, - int op) -{ - (void)ino; - auto res = flock(fi->fh, op); - fuse_reply_err(req, res == -1 ? errno : 0); -} - -#ifdef HAVE_SETXATTR -static void sfs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name, - size_t size) -{ - char *value = nullptr; - Inode &inode = get_inode(ino); - ssize_t ret; - int saverr; - - if (inode.is_symlink) - { - /* Sorry, no race free way to getxattr on symlink. */ - saverr = ENOTSUP; - goto out; - } - - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", inode.fd); - - if (size) - { - value = new (nothrow) char[size]; - if (value == nullptr) - { - saverr = ENOMEM; - goto out; - } - - ret = getxattr(procname, name, value, size); - if (ret == -1) - goto out_err; - saverr = 0; - if (ret == 0) - goto out; - - fuse_reply_buf(req, value, ret); - } - else - { - ret = getxattr(procname, name, nullptr, 0); - if (ret == -1) - goto out_err; - - fuse_reply_xattr(req, ret); - } -out_free: - delete[] value; - return; - -out_err: - saverr = errno; -out: - fuse_reply_err(req, saverr); - goto out_free; -} - -static void sfs_listxattr(fuse_req_t req, fuse_ino_t ino, size_t size) -{ - char *value = nullptr; - Inode &inode = get_inode(ino); - ssize_t ret; - int saverr; - - if (inode.is_symlink) - { - /* Sorry, no race free way to listxattr on symlink. */ - saverr = ENOTSUP; - goto out; - } - - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", inode.fd); - - if (size) - { - value = new (nothrow) char[size]; - if (value == nullptr) - { - saverr = ENOMEM; - goto out; - } - - ret = listxattr(procname, value, size); - if (ret == -1) - goto out_err; - saverr = 0; - if (ret == 0) - goto out; - - fuse_reply_buf(req, value, ret); - } - else - { - ret = listxattr(procname, nullptr, 0); - if (ret == -1) - goto out_err; - - fuse_reply_xattr(req, ret); - } -out_free: - delete[] value; - return; -out_err: - saverr = errno; -out: - fuse_reply_err(req, saverr); - goto out_free; -} - -static void sfs_setxattr(fuse_req_t req, fuse_ino_t ino, const char *name, - const char *value, size_t size, int flags) -{ - Inode &inode = get_inode(ino); - ssize_t ret; - int saverr; - - if (inode.is_symlink) - { - /* Sorry, no race free way to setxattr on symlink. */ - saverr = ENOTSUP; - goto out; - } - - char procname[64]; - sprintf(procname, "/proc/self/fd/%i", inode.fd); - - ret = setxattr(procname, name, value, size, flags); - saverr = ret == -1 ? errno : 0; - -out: - fuse_reply_err(req, saverr); -} - -static void sfs_removexattr(fuse_req_t req, fuse_ino_t ino, const char *name) -{ - char procname[64]; - Inode &inode = get_inode(ino); - ssize_t ret; - int saverr; - - if (inode.is_symlink) - { - /* Sorry, no race free way to setxattr on symlink. */ - saverr = ENOTSUP; - goto out; - } - - sprintf(procname, "/proc/self/fd/%i", inode.fd); - ret = removexattr(procname, name); - saverr = ret == -1 ? errno : 0; - -out: - fuse_reply_err(req, saverr); -} -#endif - -static void assign_operations(fuse_lowlevel_ops &sfs_oper) -{ - sfs_oper.init = sfs_init; - sfs_oper.lookup = sfs_lookup; - sfs_oper.mkdir = sfs_mkdir; - sfs_oper.mknod = sfs_mknod; - sfs_oper.symlink = sfs_symlink; - sfs_oper.link = sfs_link; - sfs_oper.unlink = sfs_unlink; - sfs_oper.rmdir = sfs_rmdir; - sfs_oper.rename = sfs_rename; - sfs_oper.forget = sfs_forget; - sfs_oper.forget_multi = sfs_forget_multi; - sfs_oper.getattr = sfs_getattr; - sfs_oper.setattr = sfs_setattr; - sfs_oper.readlink = sfs_readlink; - sfs_oper.opendir = sfs_opendir; - sfs_oper.readdir = sfs_readdir; - sfs_oper.readdirplus = sfs_readdirplus; - sfs_oper.releasedir = sfs_releasedir; - sfs_oper.fsyncdir = sfs_fsyncdir; - sfs_oper.create = sfs_create; - sfs_oper.open = sfs_open; - sfs_oper.release = sfs_release; - sfs_oper.flush = sfs_flush; - sfs_oper.fsync = sfs_fsync; - sfs_oper.read = sfs_read; - sfs_oper.write_buf = sfs_write_buf; - sfs_oper.statfs = sfs_statfs; -#ifdef HAVE_POSIX_FALLOCATE - sfs_oper.fallocate = sfs_fallocate; -#endif - sfs_oper.flock = sfs_flock; -#ifdef HAVE_SETXATTR - sfs_oper.setxattr = sfs_setxattr; - sfs_oper.getxattr = sfs_getxattr; - sfs_oper.listxattr = sfs_listxattr; - sfs_oper.removexattr = sfs_removexattr; -#endif -} - -void maximize_fd_limit() -{ - struct rlimit lim - { - }; - auto res = getrlimit(RLIMIT_NOFILE, &lim); - if (res != 0) - { - warn("WARNING: getrlimit() failed with"); - return; - } - lim.rlim_cur = lim.rlim_max; - res = setrlimit(RLIMIT_NOFILE, &lim); - if (res != 0) - warn("WARNING: setrlimit() failed with"); -} - -/** - * Starts hosting the fuse file system along with the state monitor. - * @param arg0 First CLI argument to be passed into fuse main. - * @param state_hist_dir Hot pocket state history directory. - * @param fuse_mnt_dir Directory to mound the fuse filesystem. - * @return 0 on success. 1 on failure. - */ -int start(const char *arg0, const char *state_hist_dir, const char *fuse_mnt_dir) -{ - // We need an fd for every entry in our the filesystem that the - // kernel knows about. This is way more than most processes need, - // so try to get rid of any resource softlimit. - maximize_fd_limit(); - - // We consider this as the first run of the history dir is empty. - const bool is_first_run = boost::filesystem::is_empty(state_hist_dir); - - statefs::init(state_hist_dir); - statemonitor.ctx = statefs::get_state_dir_context(); - fs.source = statemonitor.ctx.data_dir; - - // Create a checkpoint from the second run onwards. - if (!is_first_run) - statemonitor.create_checkpoint(); - - // Initialize filesystem root - fs.root.fd = -1; - fs.root.nlookup = 9999; - fs.root.is_symlink = false; - - // This is equivalent to specifying --nocache for fuse args. If we need to support caching, - // we need to set this to 86400.0 - fs.timeout = 0; - - struct stat stat; - auto ret = lstat(fs.source.c_str(), &stat); - if (ret == -1) - err(1, "ERROR: failed to stat source (\"%s\")", fs.source.c_str()); - if (!S_ISDIR(stat.st_mode)) - errx(1, "ERROR: source is not a directory"); - fs.src_dev = stat.st_dev; - - fs.root.fd = open(fs.source.c_str(), O_PATH); - if (fs.root.fd == -1) - err(1, "ERROR: open(\"%s\", O_PATH)", fs.source.c_str()); - - // Initialize fuse - fuse_args args = FUSE_ARGS_INIT(0, nullptr); - if (fuse_opt_add_arg(&args, arg0) || - fuse_opt_add_arg(&args, "-o") || - fuse_opt_add_arg(&args, "default_permissions,fsname=hpstatefs") - /*|| fuse_opt_add_arg(&args, "-odebug")*/) - errx(3, "ERROR: Out of memory"); - - fuse_lowlevel_ops sfs_oper{}; - assign_operations(sfs_oper); - auto se = fuse_session_new(&args, &sfs_oper, sizeof(sfs_oper), &fs); - if (se == nullptr) - goto err_out1; - - if (fuse_set_signal_handlers(se) != 0) - goto err_out2; - - // Don't apply umask, use modes exactly as specified - umask(0); - - // Mount and run main loop - struct fuse_loop_config loop_config; - loop_config.clone_fd = 0; - loop_config.max_idle_threads = 10; - if (fuse_session_mount(se, fuse_mnt_dir) != 0) - goto err_out3; - - ret = fuse_session_loop_mt(se, &loop_config); - - fuse_session_unmount(se); - -err_out3: - fuse_remove_signal_handlers(se); -err_out2: - fuse_session_destroy(se); -err_out1: - fuse_opt_free_args(&args); - - return ret ? 1 : 0; -} - -} // namespace fusefs - -int main(int argc, char *argv[]) -{ - if (argc != 3) - { - std::cerr << "Incorrect arguments.\n"; - exit(1); - } - - return fusefs::start(argv[0], argv[1], argv[2]); -} - -namespace boost -{ -/** - * Global exception handler for boost exceptions. - */ -void throw_exception(std::exception const &e) -{ - std::cerr << "Boost error: " << e.what() << "\n" - << boost::stacktrace::stacktrace(); - exit(1); -} - -inline void assertion_failed_msg(char const *expr, char const *msg, char const *function, char const * /*file*/, long /*line*/) -{ - std::cerr << "Expression '" << expr << "' is false in function '" << function << "': " << (msg ? msg : "<...>") << ".\n" - << "Backtrace:\n" - << boost::stacktrace::stacktrace() << '\n'; - std::abort(); -} - -inline void assertion_failed(char const *expr, char const *function, char const *file, long line) -{ - ::boost::assertion_failed_msg(expr, 0 /*nullptr*/, function, file, line); -} -} // namespace boost \ No newline at end of file diff --git a/src/statefs/state_monitor/fusefs.hpp b/src/statefs/state_monitor/fusefs.hpp deleted file mode 100644 index 369e24fe..00000000 --- a/src/statefs/state_monitor/fusefs.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef _FUSE_FS_ -#define _FUSE_FS_ - -namespace fusefs -{ -int start(const char *arg0, const char *source, const char *mountpoint, const char *delta_dir); -} - -#endif \ No newline at end of file diff --git a/src/statefs/state_monitor/state_monitor.cpp b/src/statefs/state_monitor/state_monitor.cpp deleted file mode 100644 index 228ae9b8..00000000 --- a/src/statefs/state_monitor/state_monitor.cpp +++ /dev/null @@ -1,582 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../hasher.hpp" -#include "../state_common.hpp" -#include "state_monitor.hpp" - -namespace statefs -{ - -/** - * Creates a new checkpoint directory. This will remove the oldest checkpoint if we have - * reached MAX_CHECKPOINTS. This is called whenever fuse filesystem is run so the contract - * always runs on a new checkpoint. - */ -void state_monitor::create_checkpoint() -{ - /** - * Checkpoints are numbered 0, -1, -2, ... - * Checkpoint 0 is the latest state containing "state", "data", "delta", "bhmap", "htree" directories. - * Checkpoints -1 and lower contains only the "delta" dirs containing older state changesets. - */ - - // Shift "-1" and older checkpoints by 1 more. And then copy checkpoint 0 delta dir to "-1". - // If MAX oldest checkpoint is there, remove it and work our way upwards. - int16_t oldest_chkpnt = MAX_CHECKPOINTS * -1; - for (int16_t chkpnt = oldest_chkpnt; chkpnt <= -1; chkpnt++) - { - std::string dir = get_state_dir_root(chkpnt); - - if (boost::filesystem::exists(dir)) - { - if (chkpnt == oldest_chkpnt) - { - boost::filesystem::remove_all(dir); - } - else - { - std::string dir_shift = get_state_dir_root(chkpnt - 1); - boost::filesystem::rename(dir, dir_shift); - } - } - - if (chkpnt == -1) - { - state_dir_context ctx = get_state_dir_context(0, true); - - // Shift 0-state delta dir to -1. - std::string delta_1 = dir + DELTA_DIR; - boost::filesystem::create_directories(delta_1); - - boost::filesystem::rename(ctx.delta_dir, delta_1); - boost::filesystem::create_directories(ctx.delta_dir); - } - } - - return; -} - -/** - * Called whenever a new file is created in the fuse fs. - * @param fd The fd of the created file. - */ -void state_monitor::oncreate(const int fd) -{ - std::lock_guard lock(monitor_mutex); - - std::string filepath; - if (extract_filepath(filepath, fd) == 0) - oncreate_filepath(filepath); -} - -/** - * Called whenever a file is going to be opened. - * @param inodefd inode fd given by fuse fs. This is used to find the physical path of the file. - * @param flags Open flags. - */ -void state_monitor::onopen(const int inodefd, const int flags) -{ - std::lock_guard lock(monitor_mutex); - - // Find the actual file path which is going to be opened and add that path to tracked file info list. - std::string filepath; - if (extract_filepath(filepath, inodefd) == 0) - { - state_file_info *fi; - if (get_tracked_fileinfo(&fi, filepath) == 0) - { - // Check whether the file is going to be opened in truncate mode. - // If so cache the entire file immediately because this is the last chance we get to backup the data. - if (flags & O_TRUNC) - cache_blocks(*fi, 0, fi->original_length); - } - } -} - -/** - * Called whenever a file is being written to. - * @param fd fd of the file being written to. - * @param offset Byte offset of the write. - * @param length Number of bytes being overwritten. - */ -void state_monitor::onwrite(const int fd, const off_t offset, const size_t length) -{ - // TODO: Known issue: onwrite can get called if the client program deletes a file before - // closing the currently open file. If there were some bytes on the write buffer, the flush happens - // when the client closes the fd. By that time the fd is invalid since the file is deleted. - // However nothing happens to us as our code simply returns on invalild fd error. - - std::lock_guard lock(monitor_mutex); - - // Find the actual filepath being written to and cache the blocks to server as backup. - std::string filepath; - if (get_fd_filepath(filepath, fd) == 0) - { - state_file_info *fi; - if (get_tracked_fileinfo(&fi, filepath) == 0) - cache_blocks(*fi, offset, length); - } -} - -/** - * Called when a file is being renamed. - * We simply treat this as delete-and-create operation. - */ -void state_monitor::onrename(const std::string &old_filepath, const std::string &new_filepath) -{ - std::lock_guard lock(monitor_mutex); - - ondelete_filepath(old_filepath); - oncreate_filepath(new_filepath); -} - -/** - * Called when a file is being deleted. - */ -void state_monitor::ondelete(const std::string &filepath) -{ - std::lock_guard lock(monitor_mutex); - ondelete_filepath(filepath); -} - -/** - * Called when a file is being truncated. - */ -void state_monitor::ontruncate(const int fd, const off_t newsize) -{ - std::lock_guard lock(monitor_mutex); - - std::string filepath; - if (get_fd_filepath(filepath, fd) == 0) - { - // If truncated size is less than the original, cache the entire file. - state_file_info *fi; - if (get_tracked_fileinfo(&fi, filepath) == 0 && newsize < fi->original_length) - cache_blocks(*fi, 0, fi->original_length); - } -} - -/** - * Called when an open file is being closed. Here, we clear any tracking information we kept for this file - * and close off any related fds associated with any backup operations for this file. - */ -void state_monitor::onclose(const int fd) -{ - std::lock_guard lock(monitor_mutex); - - // fd_path_map should contain this fd already if we were tracking it. - - auto pitr = fd_path_map.find(fd); - if (pitr != fd_path_map.end()) - { - // Close any block cache/index fds we have opened for this file. - auto fitr = file_info_map.find(pitr->second); // pitr->second is the filepath string. - if (fitr != file_info_map.end()) - close_caching_fds(fitr->second); // fitr->second is the fileinfo struct. - - fd_path_map.erase(pitr); - } -} - -/** - * Extracts the full physical file path for a given fd. - * @param filepath String to assign the extracted file path. - * @param fd The file descriptor to find the filepath. - * @return 0 on successful file path extraction. -1 on failure. - */ -int state_monitor::extract_filepath(std::string &filepath, const int fd) -{ - char proclnk[32]; - sprintf(proclnk, "/proc/self/fd/%d", fd); - - filepath.resize(PATH_MAX); - ssize_t len = readlink(proclnk, filepath.data(), PATH_MAX); - if (len > 0) - { - filepath.resize(len); - return 0; - } - return -1; -} - -/** - * Find the full physical file path for a given fd using the fd map. - * @param filepath String to assign the extracted file path. - * @param fd The file descriptor to find the filepath. - * @return 0 on successful file path extraction. -1 on failure. - */ -int state_monitor::get_fd_filepath(std::string &filepath, const int fd) -{ - // Return path from the map if found. - const auto itr = fd_path_map.find(fd); - if (itr != fd_path_map.end()) - { - filepath = itr->second; - return 0; - } - - // Extract the file path and populate the fd-->filepath map. - if (extract_filepath(filepath, fd) == 0) - { - fd_path_map[fd] = filepath; - return 0; - } - - return -1; -} - -/** - * Called when a new file is going to be created. fd is not yet open at this point. - * We need to catch this and start tracking this filepath. - */ -void state_monitor::oncreate_filepath(const std::string &filepath) -{ - // Check whether we are already tracking this file path. - // Only way we could be tracking this path already is deleting an existing file and creating - // a new file with the same name. - if (file_info_map.count(filepath) == 0) - { - // Add an entry for the new file in the file info map. This information will be used to ignore - // future operations (eg. write/delete) done to this file. - state_file_info fi; - fi.is_new = true; - fi.filepath = filepath; - file_info_map[filepath] = std::move(fi); - - // Add to the list of new files added during this session. - write_new_file_entry(filepath); - } -} - -/** - * Called when a file is going to be deleted. We use this to remove any tracking information - * regarding this file and to backup the file before deletion. - */ -void state_monitor::ondelete_filepath(const std::string &filepath) -{ - state_file_info *fi; - if (get_tracked_fileinfo(&fi, filepath) == 0) - { - if (fi->is_new) - { - // If this is a new file, just remove from existing index entries. - // No need to cache the file blocks. - remove_new_file_entry(fi->filepath); - file_info_map.erase(filepath); - } - else - { - // If not a new file, cache the entire file. - cache_blocks(*fi, 0, fi->original_length); - } - } -} - -/** - * Finds the tracked state file information for the given filepath. - * @param fi Reference pointer to assign the state file info struct. - * @param filepath Full physical path of the file. - * @return 0 on successful find. -1 on failure. - */ -int state_monitor::get_tracked_fileinfo(state_file_info **fi, const std::string &filepath) -{ - // Return from filepath-->fileinfo map if found. - const auto itr = file_info_map.find(filepath); - if (itr != file_info_map.end()) - { - *fi = &itr->second; - return 0; - } - - // Initialize a new state file info struct for the given filepath. - state_file_info &fileinfo = file_info_map[filepath]; - - // We use stat() to find out the length of the file. - struct stat stat_buf; - if (stat(filepath.c_str(), &stat_buf) != 0) - { - std::cerr << errno << ": Error occured in stat() of " << filepath << "\n"; - return -1; - } - - fileinfo.original_length = stat_buf.st_size; - fileinfo.filepath = filepath; - *fi = &fileinfo; - return 0; -} - -/** - * Backs up the specified bytes range of the given file. This is called whenever a file is being - * overwritten/deleted. - * @param fi The file info struct pointing to the file to be cached. - * @param offset The start byte position for caching. - * @param length How many bytes to cache. - * @return 0 on successful execution. -1 on failure. - */ -int state_monitor::cache_blocks(state_file_info &fi, const off_t offset, const size_t length) -{ - // No caching required if this is a new file created during this session. - if (fi.is_new) - return 0; - - uint32_t original_block_count = ceil((double)fi.original_length / (double)BLOCK_SIZE); - - // Check whether we have already cached the entire file. - if (original_block_count == fi.cached_blockids.size()) - return 0; - - // Initialize fds and indexes required for caching the file. - if (prepare_caching(fi) != 0) - return -1; - - // Return if incoming write is outside any of the original blocks. - if (offset > original_block_count * BLOCK_SIZE) - return 0; - - uint32_t startblock = offset / BLOCK_SIZE; - uint32_t endblock = (offset + length) / BLOCK_SIZE; - - // std::cout << "Cache blocks: '" << fi.filepath << "' [" << offset << "," << length << "] " << startblock << "," << endblock << "\n"; - - // If this is the first time we are caching this file, write an entry to the touched file index. - // Touched file index is used by rollback to server as a guide. - if (fi.cached_blockids.empty() && write_touched_file_entry(fi.filepath) != 0) - return -1; - - for (uint32_t i = startblock; i <= endblock; i++) - { - // Skip if we have already cached this block. - if (fi.cached_blockids.count(i) > 0) - continue; - - // Read the block being replaced and send to cache file. - // Allocating block buffer on the heap to avoid filling limited stack space. - std::unique_ptr block_buf = std::make_unique(BLOCK_SIZE); - off_t block_offset = BLOCK_SIZE * i; - size_t bytes_read = pread(fi.readfd, block_buf.get(), BLOCK_SIZE, BLOCK_SIZE * i); - if (bytes_read < 0) - { - std::cerr << errno << ": Read failed " << fi.filepath << "\n"; - return -1; - } - - // No more bytes to read in this file. - if (bytes_read == 0) - return 0; - - if (write(fi.cachefd, block_buf.get(), bytes_read) < 0) - { - std::cerr << errno << ": Write to block cache failed. " << fi.filepath << "\n"; - return -1; - } - - // Append an entry (44 bytes) into the block cache index. We maintain this index to - // help random block access for external use cases. We currently do not sort this index here. - // Whoever is using the index must sort it if required. - // Entry format: [blocknum(4 bytes) | cacheoffset(8 bytes) | blockhash(32 bytes)] - - // Calculate the block hash by combining block offset with block data. - char entrybuf[BLOCK_INDEX_ENTRY_SIZE]; - hasher::B2H hash = hasher::hash(&block_offset, 8, block_buf.get(), bytes_read); - - // Original file block id. - memcpy(entrybuf, &i, 4); - // Position of the block within the cache file. - off_t cacheoffset = fi.cached_blockids.size() * BLOCK_SIZE; - memcpy(entrybuf + 4, &cacheoffset, 8); - // The block hash. - memcpy(entrybuf + 12, hash.data, 32); - if (write(fi.indexfd, entrybuf, BLOCK_INDEX_ENTRY_SIZE) < 0) - { - std::cerr << errno << ": Write to block index failed. " << fi.filepath << "\n"; - return -1; - } - - // Mark the block as cached. - fi.cached_blockids.emplace(i); - } - - return 0; -} - -/** - * Initializes fds and indexes required for caching a particular file. - * @param fi The state file info struct pointing to the file being cached. - * @return 0 on succesful initialization. -1 on failure. - */ -int state_monitor::prepare_caching(state_file_info &fi) -{ - // If readfd is greater than 0 then we take it as caching being already initialized for this file. - if (fi.readfd > 0) - return 0; - - // Open up the file using a read-only fd. This fd will be used to fetch blocks to be cached. - fi.readfd = open(fi.filepath.c_str(), O_RDONLY); - if (fi.readfd < 0) - { - std::cerr << errno << ": Open failed " << fi.filepath << "\n"; - return -1; - } - - // Get the path of the file relative to the state dir. We maintain this same reative path for the - // corresponding cache and index files in the cache dir. - std::string relpath = get_relpath(fi.filepath, ctx.data_dir); - - std::string tmppath; - tmppath.reserve(ctx.delta_dir.length() + relpath.length() + BLOCK_CACHE_EXT_LEN); - tmppath.append(ctx.delta_dir).append(relpath).append(BLOCK_CACHE_EXT); - - // Create directory tree if not exist so we are able to create the cache and index files. - boost::filesystem::path cachesubdir = boost::filesystem::path(tmppath).parent_path(); - if (created_cache_subdirs.count(cachesubdir.string()) == 0) - { - boost::filesystem::create_directories(cachesubdir); - created_cache_subdirs.emplace(cachesubdir.string()); - } - - // Create and open the block cache file. - fi.cachefd = open(tmppath.c_str(), O_WRONLY | O_APPEND | O_CREAT, FILE_PERMS); - if (fi.cachefd <= 0) - { - std::cerr << errno << ": Open failed " << tmppath << "\n"; - return -1; - } - - // Create and open the block index file. - tmppath.replace(tmppath.length() - BLOCK_CACHE_EXT_LEN, BLOCK_INDEX_EXT_LEN, BLOCK_INDEX_EXT); - fi.indexfd = open(tmppath.c_str(), O_WRONLY | O_APPEND | O_CREAT, FILE_PERMS); - if (fi.indexfd <= 0) - { - std::cerr << errno << ": Open failed " << tmppath << "\n"; - return -1; - } - - // Write first entry (8 bytes) to the index file. First entry is the length of the original file. - // This will be helpful when restoring/rolling back a file. - if (write(fi.indexfd, &fi.original_length, 8) == -1) - { - std::cerr << errno << ": Error writing to index file " << tmppath << "\n"; - return -1; - } - - return 0; -} - -/** - * Closes any open caching fds for a given file. - */ -void state_monitor::close_caching_fds(state_file_info &fi) -{ - if (fi.readfd > 0) - close(fi.readfd); - - if (fi.cachefd > 0) - close(fi.cachefd); - - if (fi.indexfd > 0) - close(fi.indexfd); - - fi.readfd = 0; - fi.cachefd = 0; - fi.indexfd = 0; -} - -/** - * Inserts a file into the modified files list of this session. - * This index is used to restore modified files during rollback. - */ -int state_monitor::write_touched_file_entry(std::string_view filepath) -{ - if (touched_fileindex_fd <= 0) - { - std::string index_file = ctx.delta_dir + IDX_TOUCHED_FILES; - touched_fileindex_fd = open(index_file.c_str(), O_WRONLY | O_APPEND | O_CREAT, FILE_PERMS); - if (touched_fileindex_fd <= 0) - { - std::cerr << errno << ": Open failed " << index_file << "\n"; - return -1; - } - } - - // Write the relative file path line to the index. - filepath = filepath.substr(ctx.data_dir.length(), filepath.length() - ctx.data_dir.length()); - write(touched_fileindex_fd, filepath.data(), filepath.length()); - write(touched_fileindex_fd, "\n", 1); - return 0; -} - -/** - * Inserts a file into the list of new files created during this session. - * This index is used in deleting new files during restore. - */ -int state_monitor::write_new_file_entry(std::string_view filepath) -{ - std::string index_file = ctx.delta_dir + IDX_NEW_FILES; - int fd = open(index_file.c_str(), O_WRONLY | O_APPEND | O_CREAT, FILE_PERMS); - if (fd <= 0) - { - std::cerr << errno << ": Open failed " << index_file << "\n"; - return -1; - } - - // Write the relative file path line to the index. - filepath = filepath.substr(ctx.data_dir.length(), filepath.length() - ctx.data_dir.length()); - write(fd, filepath.data(), filepath.length()); - write(fd, "\n", 1); - close(fd); - return 0; -} - -/** - * Scans and removes the given filepath from the new files index. - * This is called when a file added during this session gets deleted in the same session. - */ -void state_monitor::remove_new_file_entry(std::string_view filepath) -{ - filepath = filepath.substr(ctx.data_dir.length(), filepath.length() - ctx.data_dir.length()); - - // We create a copy of the new file index and transfer lines from first file - // to the second file except the line matching the given filepath. - - std::string index_file = ctx.delta_dir + IDX_NEW_FILES; - std::string index_file_tmp = ctx.delta_dir + IDX_NEW_FILES + ".tmp"; - - std::ifstream in_file(index_file); - std::ofstream outfile(index_file_tmp); - - bool lines_transferred = false; - for (std::string line; std::getline(in_file, line);) - { - if (line != filepath) // Skip the file being removed. - { - outfile << line << "\n"; - lines_transferred = true; - } - } - - in_file.close(); - outfile.close(); - - // Remove the old index. - std::remove(index_file.c_str()); - - // If no lines transferred, delete the temp file as well. - if (lines_transferred) - std::rename(index_file_tmp.c_str(), index_file.c_str()); - else - std::remove(index_file_tmp.c_str()); -} - -} // namespace statefs \ No newline at end of file diff --git a/src/statefs/state_monitor/state_monitor.hpp b/src/statefs/state_monitor/state_monitor.hpp deleted file mode 100644 index 43ed6d8b..00000000 --- a/src/statefs/state_monitor/state_monitor.hpp +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef _HP_STATEFS_STATE_MONITOR_ -#define _HP_STATEFS_STATE_MONITOR_ - -#include -#include -#include -#include -#include -#include -#include "../state_common.hpp" - -namespace statefs -{ - -/** - * Holds information about an original file in state that we are tracking. - */ -struct state_file_info -{ - bool is_new; // Whether this is a new file created during this session. - off_t original_length; // Original file length. - std::unordered_set cached_blockids; // Set of block ids cached during this session. - std::string filepath; // Actual real path of the file. (not fuse path) - int readfd; // fd used for reading the original file for caching. - int cachefd; // fd for writing into the block cache file. - int indexfd; // fd for writing into the block index file. -}; - -/** - * Invoked by fuse file system for relevent file system calls. - */ -class state_monitor -{ -private: - // Map of fd-->filepath - std::unordered_map fd_path_map; - - // Map of filepath-->fileinfo - std::unordered_map file_info_map; - - // List of new cache sub directories created during the session. - std::unordered_set created_cache_subdirs; - - // Mutex to synchronize parallel file system calls into our custom state tracking logic. - std::mutex monitor_mutex; - - // Holds the fd used to write into modified files index. This will be kept open for the entire - // life of the state monitor. - int touched_fileindex_fd = 0; - - int extract_filepath(std::string &filepath, const int fd); - int get_fd_filepath(std::string &filepath, const int fd); - void oncreate_filepath(const std::string &filepath); - void ondelete_filepath(const std::string &filepath); - int get_tracked_fileinfo(state_file_info **fileinfo, const std::string &filepath); - - int cache_blocks(state_file_info &fi, const off_t offset, const size_t length); - int prepare_caching(state_file_info &fi); - void close_caching_fds(state_file_info &fi); - int write_touched_file_entry(std::string_view filepath); - int write_new_file_entry(std::string_view filepath); - void remove_new_file_entry(std::string_view filepath); - -public: - state_dir_context ctx; - void create_checkpoint(); - void oncreate(const int fd); - void onopen(const int inodefd, const int flags); - void onwrite(const int fd, const off_t offset, const size_t length); - void onrename(const std::string &old_filepath, const std::string &new_filepath); - void ondelete(const std::string &filepath); - void ontruncate(const int fd, const off_t newsize); - void onclose(const int fd); -}; - -} // namespace statefs - -#endif \ No newline at end of file diff --git a/src/statefs/state_restore.cpp b/src/statefs/state_restore.cpp deleted file mode 100644 index 4b4ff7a5..00000000 --- a/src/statefs/state_restore.cpp +++ /dev/null @@ -1,200 +0,0 @@ -#include "../pchheader.hpp" -#include "../hplog.hpp" -#include "hasher.hpp" -#include "state_restore.hpp" -#include "hashtree_builder.hpp" -#include "state_common.hpp" - -namespace statefs -{ - -// Look at new files added and delete them if still exist. -void state_restore::delete_new_files() -{ - std::string index_file(ctx.delta_dir); - index_file.append(IDX_NEW_FILES); - - std::ifstream in_file(index_file); - for (std::string file; std::getline(in_file, file);) - { - std::string filepath(ctx.data_dir); - filepath.append(file); - - remove(filepath.c_str()); - } - - in_file.close(); -} - -// Look at touched files and restore them. -int state_restore::restore_touched_files() -{ - std::unordered_set processed; - - std::string index_file(ctx.delta_dir); - index_file.append(IDX_TOUCHED_FILES); - - std::ifstream in_file(index_file); - for (std::string file; std::getline(in_file, file);) - { - // Skip if already processed. - if (processed.count(file) > 0) - continue; - - std::vector bindex; - if (read_block_index(bindex, file) != 0) - return -1; - - if (restore_blocks(file, bindex) != 0) - return -1; - - // Add to processed file list. - processed.emplace(file); - } - - in_file.close(); - return 0; -} - -// Read the delta block index. -int state_restore::read_block_index(std::vector &buffer, std::string_view file) -{ - std::string bindex_file(ctx.delta_dir); - bindex_file.append(file).append(BLOCK_INDEX_EXT); - std::ifstream in_file(bindex_file, std::ios::binary | std::ios::ate); - std::streamsize idx_size = in_file.tellg(); - in_file.seekg(0, std::ios::beg); - - buffer.resize(idx_size); - if (!in_file.read(buffer.data(), idx_size)) - { - LOG_ERR << errno << ": Read failed " << bindex_file; - return -1; - } - - return 0; -} - -// Restore blocks mentioned in the delta block index. -int state_restore::restore_blocks(std::string_view file, const std::vector &bindex) -{ - int bcache_fd = 0, ori_file_fd = 0; - const char *idx_ptr = bindex.data(); - - // First 8 bytes of the index contains the supposed length of the original file. - off_t original_len = 0; - memcpy(&original_len, idx_ptr, 8); - - // Open block cache file. - { - std::string bcache_file(ctx.delta_dir); - bcache_file.append(file).append(BLOCK_CACHE_EXT); - bcache_fd = open(bcache_file.c_str(), O_RDONLY); - if (bcache_fd <= 0) - { - LOG_ERR << errno << ": Open failed " << bcache_file; - return -1; - } - } - - // Create or Open original file. - { - std::string original_file(ctx.data_dir); - original_file.append(file); - - // Create directory tree if not exist so we are able to create the file. - boost::filesystem::path filedir = boost::filesystem::path(original_file).parent_path(); - if (created_dirs.count(filedir.string()) == 0) - { - boost::filesystem::create_directories(filedir); - created_dirs.emplace(filedir.string()); - } - - ori_file_fd = open(original_file.c_str(), O_WRONLY | O_CREAT, FILE_PERMS); - if (ori_file_fd <= 0) - { - LOG_ERR << errno << ": Open failed " << original_file; - return -1; - } - } - - // Restore the blocks as specified in block index. - for (uint32_t idx_offset = 8; idx_offset < bindex.size();) - { - // Find the block no. of where this block is from in the original file. - uint32_t block_no = 0; - memcpy(&block_no, idx_ptr + idx_offset, 4); - idx_offset += 4; - off_t ori_file_offset = block_no * BLOCK_SIZE; - - // Find the offset where the block is located in the block cache file. - off_t bcache_offset; - memcpy(&bcache_offset, idx_ptr + idx_offset, 8); - idx_offset += 40; // Skip the hash(32) - - // Transfer the cached block to the target file. - copy_file_range(bcache_fd, &bcache_offset, ori_file_fd, &ori_file_offset, BLOCK_SIZE, 0); - } - - // If the target file is bigger than the original size, truncate it to the original size. - off_t current_len = lseek(ori_file_fd, 0, SEEK_END); - if (current_len > original_len) - ftruncate(ori_file_fd, original_len); - - close(bcache_fd); - close(ori_file_fd); - - return 0; -} - -// This is called after a rollback so the all checkpoint dirs shift by 1. -void state_restore::rewind_checkpoints() -{ - // Assuming we have restored the current state with current delta, - // we need to shift each history delta by 1 place. - - // Delete the state 0 (current) delta. - boost::filesystem::remove_all(ctx.delta_dir); - - int16_t oldest_chkpnt = (MAX_CHECKPOINTS + 1) * -1; // +1 because we maintain one extra checkpoint in case of rollbacks. - for (int16_t chkpnt = -1; chkpnt >= oldest_chkpnt; chkpnt--) - { - std::string dir = get_state_dir_root(chkpnt); - - if (boost::filesystem::exists(dir)) - { - if (chkpnt == -1) - { - // Shift -1 state delta dir to 0-state and delete -1 dir. - std::string delta_1 = dir + DELTA_DIR; - boost::filesystem::rename(delta_1, ctx.delta_dir); - boost::filesystem::remove_all(dir); - } - else - { - std::string dirshift = get_state_dir_root(chkpnt + 1); - boost::filesystem::rename(dir, dirshift); - } - } - } -} - -// Rolls back current state to previous state. -int state_restore::rollback(hasher::B2H &root_hash) -{ - ctx = get_state_dir_context(); - - delete_new_files(); - if (restore_touched_files() == -1) - return -1; - - // Update hash tree. - hashtree_builder htree_builder(ctx); - htree_builder.generate(root_hash); - - rewind_checkpoints(); - - return 0; -} - -} // namespace statefs \ No newline at end of file diff --git a/src/statefs/state_restore.hpp b/src/statefs/state_restore.hpp deleted file mode 100644 index 01ab0ea5..00000000 --- a/src/statefs/state_restore.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef _HP_STATEFS_STATE_RESTORE_ -#define _HP_STATEFS_STATE_RESTORE_ - -#include "../pchheader.hpp" -#include "hasher.hpp" -#include "state_common.hpp" - -namespace statefs -{ - -class state_restore -{ -private: - state_dir_context ctx; - std::unordered_set created_dirs; - void delete_new_files(); - int restore_touched_files(); - int read_block_index(std::vector &buffer, std::string_view file); - int restore_blocks(std::string_view file, const std::vector &bindex); - void rewind_checkpoints(); - -public: - int rollback(hasher::B2H &root_hash); -}; - -} // namespace statefs - -#endif diff --git a/src/statefs/state_store.cpp b/src/statefs/state_store.cpp deleted file mode 100644 index 51fef626..00000000 --- a/src/statefs/state_store.cpp +++ /dev/null @@ -1,357 +0,0 @@ -#include "../pchheader.hpp" -#include "hasher.hpp" -#include "state_common.hpp" -#include "hashtree_builder.hpp" -#include "state_store.hpp" -#include "../hplog.hpp" -#include "state_store.hpp" - -namespace statefs -{ - -// Map of modified/deleted files with updated blockids and hashes (if modified). -std::unordered_map> touched_files; - -/** - * Checks whether the given directory exists in the state data directory. - */ -bool is_dir_exists(const std::string &dir_relpath) -{ - const std::string full_path = current_ctx.data_dir + dir_relpath; - return boost::filesystem::exists(full_path); -} - -/** - * Retrieves the hash list of the file system entries at a given directory. - * @return 0 on success. -1 on failure. - */ -int get_fs_entry_hashes(std::unordered_map &fs_entries, const std::string &dir_relpath, const hasher::B2H expected_hash) -{ - // TODO: instead of iterating the data dir, we could simply query the hash tree directory - // listing and get the hashes using the hardlink names straight away. But then we don't have - // a way to get the file names. If we could implement a mechanism for that we could make this efficient. - - if (expected_hash != hasher::B2H_empty) - { - // Check whether the existing block hash matches expected hash. - const std::string dir_hash_path = current_ctx.hashtree_dir + dir_relpath + DIR_HASH_FNAME; - - hasher::B2H existsing_hash; - if (read_file_bytes(&existsing_hash, dir_hash_path.c_str(), 0, hasher::HASH_SIZE) == -1) - return -1; - - if (existsing_hash != expected_hash) - return -1; - } - - const std::string full_path = current_ctx.data_dir + dir_relpath; - for (const boost::filesystem::directory_entry &dentry : boost::filesystem::directory_iterator(full_path)) - { - const boost::filesystem::path p = dentry.path(); - - p2p::state_fs_hash_entry fs_entry; - fs_entry.is_file = !boost::filesystem::is_directory(p); - - std::string fsentry_relpath = dir_relpath + p.filename().string(); - - // Read the first 32 bytes of the .bhmap file or dir.hash file. - - std::string hash_path; - - if (fs_entry.is_file) - { - hash_path = current_ctx.block_hashmap_dir + fsentry_relpath + BLOCK_HASHMAP_EXT; - } - else - { - fsentry_relpath += "/"; - hash_path = current_ctx.hashtree_dir + fsentry_relpath + DIR_HASH_FNAME; - // Skip the directory if it doesn't contain the dir.hash file. - // By that we assume the directory is empty so we're not interested in it. - if (!boost::filesystem::exists(hash_path)) - continue; - } - - if (read_file_bytes(&fs_entry.hash, hash_path.c_str(), 0, hasher::HASH_SIZE) == -1) - return -1; - - fs_entries.emplace(fsentry_relpath, std::move(fs_entry)); - } - return 0; -} - -/** - * Retrieves the block hash map for a file. - * @return 0 on success. -1 on failure. - */ -int get_block_hash_map(std::vector &vec, const std::string &file_relpath, const hasher::B2H expected_hash) -{ - const std::string bhmap_path = current_ctx.block_hashmap_dir + file_relpath + BLOCK_HASHMAP_EXT; - - if (expected_hash != hasher::B2H_empty) - { - // Check whether the existing block hash matches expected hash. - - if (!boost::filesystem::exists(bhmap_path) || read_file_bytes_to_end(vec, bhmap_path.c_str(), 0) == -1) - return -1; - - // Existing hash is the first 32 bytes of bhmap contents. - hasher::B2H existing_hash = *reinterpret_cast(vec.data()); - if (existing_hash != expected_hash) - return -1; - - // Return the bhmap bytes without the first 32 bytes. - vec.erase(vec.begin(), vec.begin() + hasher::HASH_SIZE); - } - else - { - // Skip the file root hash and get the rest of the bytes. - if (boost::filesystem::exists(bhmap_path) && read_file_bytes_to_end(vec, bhmap_path.c_str(), hasher::HASH_SIZE) == -1) - return -1; - } - - return 0; -} - -/** - * Retrieves the byte length of a file. - * @return 0 on success. -1 on failure. - */ -int get_file_length(const std::string &file_relpath) -{ - std::string full_path = current_ctx.data_dir + file_relpath; - int fd = open(full_path.c_str(), O_RDONLY); - if (fd == -1) - { - LOG_ERR << errno << " Open failed " << full_path; - return -1; - } - - const off_t total_len = lseek(fd, 0, SEEK_END); - close(fd); - - return total_len; -} - -/** - * Retrieves the specified data block from a state file. - * @return Number of bytes read on success. -1 on failure. - */ -int get_block(std::vector &vec, const std::string &file_relpath, const uint32_t block_id, const hasher::B2H expected_hash) -{ - // Check whether the existing block hash matches expected hash. - if (expected_hash != hasher::B2H_empty) - { - std::string bhmap_path = current_ctx.block_hashmap_dir + file_relpath + BLOCK_HASHMAP_EXT; - hasher::B2H existing_hash = hasher::B2H_empty; - - if (read_file_bytes(&existing_hash, bhmap_path.c_str(), (block_id + 1) * hasher::HASH_SIZE, hasher::HASH_SIZE) == -1) - return -1; - - if (existing_hash != expected_hash) - return -1; - } - - std::string full_path = current_ctx.data_dir + file_relpath; - vec.resize(BLOCK_SIZE); - int read_bytes = read_file_bytes(vec.data(), full_path.c_str(), block_id * BLOCK_SIZE, BLOCK_SIZE); - - if (read_bytes == -1) - return -1; - - vec.resize(read_bytes); - return read_bytes; -} - -/** - * Creates the specified directory in the state data directory. - */ -void create_dir(const std::string &dir_relpath) -{ - const std::string full_path = current_ctx.data_dir + dir_relpath; - boost::filesystem::create_directories(full_path); -} - -/** - * Deletes all files within the specified state sub directory and marks the changes. - * @return 0 on success. -1 on failure. - */ -int delete_dir(const std::string &dir_relpath) -{ - std::string full_dir_path = current_ctx.data_dir + dir_relpath; - - const boost::filesystem::directory_iterator itr_end; - for (boost::filesystem::directory_iterator itr(full_dir_path); itr != itr_end; itr++) - { - boost::filesystem::path p = itr->path(); - - if (!boost::filesystem::is_directory(p)) - { - if (!boost::filesystem::remove(p)) - return -1; - - // Add the deleted file rel path to the touched files list. - touched_files.emplace( - get_relpath(p.string(), current_ctx.data_dir), - std::map()); - } - } - - // Finally, delete the directory itself. - boost::filesystem::remove_all(full_dir_path); - - return 0; -} - -/** - * Deletes the specified state file and marks the change. - * @return 0 on success. -1 on failure. - */ -int delete_file(const std::string &file_relpath) -{ - std::string full_path = current_ctx.data_dir + file_relpath; - if (!boost::filesystem::remove(full_path)) - return -1; - - touched_files.emplace(file_relpath, std::map()); - return 0; -} - -/** - * Truncates the specified state file to the specified length and marks the change. - * @return 0 on success. -1 on failure. - */ -int truncate_file(const std::string &file_relpath, const size_t newsize) -{ - std::string full_path = current_ctx.data_dir + file_relpath; - int fd = open(full_path.c_str(), O_WRONLY | O_CREAT, FILE_PERMS); - if (fd == -1) - { - LOG_ERR << errno << " Open failed " << full_path; - return -1; - } - - int ret = ftruncate(fd, newsize); - close(fd); - if (ret == -1) - { - LOG_ERR << errno << "Truncate failed " << full_path; - return -1; - } - - return 0; -} - -/** - * Writes the specified block to a file and marks the change. - * @param file_relpath State data relative path of the file. - * @param block_id Block id to replace/write. - * @param buf The buffer containing data to be written. - * @param len Length of the buffer. - * @return 0 on success. -1 on failure. - */ -int write_block(const std::string &file_relpath, const uint32_t block_id, const void *buf, const size_t len) -{ - std::string full_path = current_ctx.data_dir + file_relpath; - int fd = open(full_path.c_str(), O_WRONLY | O_CREAT, FILE_PERMS); - if (fd == -1) - { - LOG_ERR << errno << " Open failed " << full_path; - return -1; - } - - const off_t offset = block_id * BLOCK_SIZE; - int ret = pwrite(fd, buf, len, offset); - close(fd); - if (ret == -1) - { - LOG_ERR << errno << " Write failed " << full_path; - return -1; - } - - hasher::B2H hash = hasher::hash(&offset, 8, buf, len); - touched_files[file_relpath].emplace(block_id, hash); - - return 0; -} - -/** - * Computes the latest hash tree with any changes recorded in touched files index. - * @return 0 on success. -1 on failure. - */ -int compute_hash_tree(hasher::B2H &statehash, const bool force_all) -{ - hashtree_builder htree_builder(current_ctx); - - int ret = force_all ? htree_builder.generate(statehash, true) : htree_builder.generate(statehash, touched_files); - - touched_files.clear(); - return ret; -} - -//-----Private helper functions---------// - -/** - * Reads bytes from file into a buffer. - * @param buf Buffer to fill with the read bytes. - * @param filepath Full path to the file. - * @param start Starting offset to read. - * @param len Number of bytes to read. - * @return Number of bytes read on successful read. -1 on failure. - */ -int read_file_bytes(void *buf, const char *filepath, const off_t start, const size_t len) -{ - int fd = open(filepath, O_RDONLY); - if (fd == -1) - { - LOG_ERR << errno << " Open failed " << filepath; - return -1; - } - - int read_bytes = pread(fd, buf, len, start); - close(fd); - if (read_bytes <= 0) - { - LOG_ERR << errno << " Read failed " << filepath; - return -1; - } - - return read_bytes; -} - -/** - * Reads bytes from file into a vector. The vector size will be adjusted to the actual bytes read. - * @param vec Vector to fill with the read bytes. - * @param filepath Full path to the file. - * @param start Starting offset to read. - * @return Number of bytes read on successful read. -1 on failure. - */ -int read_file_bytes_to_end(std::vector &vec, const char *filepath, const off_t start) -{ - int fd = open(filepath, O_RDONLY); - if (fd == -1) - { - LOG_ERR << errno << " Open failed " << filepath; - return -1; - } - - const off_t total_len = lseek(fd, 0, SEEK_END); - if (total_len == -1) - return -1; - - const size_t len = total_len - start; - vec.resize(len); - - int read_bytes = pread(fd, vec.data(), len, start); - close(fd); - if (read_bytes <= 0) - { - LOG_ERR << errno << " Read failed " << filepath; - return -1; - } - vec.resize(read_bytes); - - return read_bytes; -} - -} // namespace statefs \ No newline at end of file diff --git a/src/statefs/state_store.hpp b/src/statefs/state_store.hpp deleted file mode 100644 index fd1c0edc..00000000 --- a/src/statefs/state_store.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef _HP_STATEFS_STATE_STORE_ -#define _HP_STATEFS_STATE_STORE_ - -#include "../pchheader.hpp" -#include "../p2p/p2p.hpp" -#include "hasher.hpp" - -namespace statefs -{ - -// Map of modified/deleted files with updated blockids and hashes (if modified). -extern std::unordered_map> touched_files; - -bool is_dir_exists(const std::string &dir_relpath); -int get_fs_entry_hashes(std::unordered_map &fs_entries, const std::string &dir_relpath, const hasher::B2H expected_hash); -int get_block_hash_map(std::vector &vec, const std::string &file_relpath, const hasher::B2H expected_hash); -int get_file_length(const std::string &file_relpath); -int get_block(std::vector &vec, const std::string &file_relpath, const uint32_t block_id, const hasher::B2H expected_hash); -void create_dir(const std::string &dir_relpath); -int delete_dir(const std::string &dir_relpath); -int delete_file(const std::string &file_relpath); -int truncate_file(const std::string &file_relpath, const size_t newsize); -int write_block(const std::string &file_relpath, const uint32_t block_id, const void *buf, const size_t len); -int compute_hash_tree(hasher::B2H &statehash, const bool force_all = false); - -/** - * Private helper functions. - */ - -int read_file_bytes(void *buf, const char *filepath, const off_t start, const size_t len); -int read_file_bytes_to_end(std::vector &vec, const char *filepath, const off_t start); - -} // namespace statefs - -#endif \ No newline at end of file diff --git a/src/usr/usr.cpp b/src/usr/usr.cpp index 7d5bdf47..5ee3dec8 100644 --- a/src/usr/usr.cpp +++ b/src/usr/usr.cpp @@ -15,167 +15,174 @@ namespace jusrmsg = jsonschema::usrmsg; namespace usr { -// Holds global connected-users and related objects. -connected_context ctx; + // Holds global connected-users and related objects. + connected_context ctx; -/** + bool init_success = false; + + /** * Initializes the usr subsystem. Must be called once during application startup. * @return 0 for successful initialization. -1 for failure. */ -int init() -{ - // Start listening for incoming user connections. - return start_listening(); -} + int init() + { + // Start listening for incoming user connections. + if (start_listening() == -1) + return -1; -/** + init_success = true; + return 0; + } + + /** * Cleanup any running processes. */ -void deinit() -{ - ctx.listener.stop(); -} + void deinit() + { + if (init_success) + ctx.listener.stop(); + } -/** + /** * Starts listening for incoming user websocket connections. */ -int start_listening() -{ - const uint64_t metric_thresholds[] = {conf::cfg.pubmaxcpm, 0, 0, conf::cfg.pubmaxbadmpm}; - if (ctx.listener.start( - conf::cfg.pubport, ".sock-user", comm::SESSION_TYPE::USER, true, true, metric_thresholds, std::set(), conf::cfg.pubmaxsize) == -1) - return -1; + int start_listening() + { + const uint64_t metric_thresholds[] = {conf::cfg.pubmaxcpm, 0, 0, conf::cfg.pubmaxbadmpm}; + if (ctx.listener.start( + conf::cfg.pubport, ".sock-user", comm::SESSION_TYPE::USER, true, true, metric_thresholds, std::set(), conf::cfg.pubmaxsize) == -1) + return -1; - LOG_INFO << "Started listening for user connections on " << std::to_string(conf::cfg.pubport); - return 0; -} + LOG_INFO << "Started listening for user connections on " << std::to_string(conf::cfg.pubport); + return 0; + } -/** + /** * Verifies the given message for a previously issued user challenge. * @param message Challenge response. * @param session The socket session that received the response. * @return 0 for successful verification. -1 for failure. */ -int verify_challenge(std::string_view message, comm::comm_session &session) -{ - // The received message must be the challenge response. We need to verify it. - if (session.issued_challenge.empty()) + int verify_challenge(std::string_view message, comm::comm_session &session) { - LOG_DBG << "No challenge found for the session " << session.uniqueid; - return -1; - } - - std::string userpubkeyhex; - std::string_view original_challenge = session.issued_challenge; - if (jusrmsg::verify_user_challenge_response(userpubkeyhex, message, original_challenge) == 0) - { - // Challenge signature verification successful. - - // Decode hex pubkey and get binary pubkey. We are only going to keep - // the binary pubkey due to reduced memory footprint. - std::string userpubkey; - userpubkey.resize(userpubkeyhex.length() / 2); - util::hex2bin( - reinterpret_cast(userpubkey.data()), - userpubkey.length(), - userpubkeyhex); - - // Now check whether this user public key is a duplicate. - if (ctx.sessionids.count(userpubkey) == 0) + // The received message must be the challenge response. We need to verify it. + if (session.issued_challenge.empty()) { - // All good. Unique public key. - // Promote the connection from pending-challenges to authenticated users. + LOG_DBG << "No challenge found for the session " << session.uniqueid; + return -1; + } - session.challenge_status = comm::CHALLENGE_VERIFIED; // Set as challenge verified - add_user(session, userpubkey); // Add the user to the global authed user list - session.issued_challenge.clear(); // Remove the stored challenge + std::string userpubkeyhex; + std::string_view original_challenge = session.issued_challenge; + if (jusrmsg::verify_user_challenge_response(userpubkeyhex, message, original_challenge) == 0) + { + // Challenge signature verification successful. - LOG_DBG << "User connection " << session.uniqueid << " authenticated. Public key " - << userpubkeyhex; - return 0; + // Decode hex pubkey and get binary pubkey. We are only going to keep + // the binary pubkey due to reduced memory footprint. + std::string userpubkey; + userpubkey.resize(userpubkeyhex.length() / 2); + util::hex2bin( + reinterpret_cast(userpubkey.data()), + userpubkey.length(), + userpubkeyhex); + + // Now check whether this user public key is a duplicate. + if (ctx.sessionids.count(userpubkey) == 0) + { + // All good. Unique public key. + // Promote the connection from pending-challenges to authenticated users. + + session.challenge_status = comm::CHALLENGE_VERIFIED; // Set as challenge verified + add_user(session, userpubkey); // Add the user to the global authed user list + session.issued_challenge.clear(); // Remove the stored challenge + + LOG_DBG << "User connection " << session.uniqueid << " authenticated. Public key " + << userpubkeyhex; + return 0; + } + else + { + LOG_DBG << "Duplicate user public key " << session.uniqueid; + } } else { - LOG_DBG << "Duplicate user public key " << session.uniqueid; + LOG_DBG << "Challenge verification failed " << session.uniqueid; } - } - else - { - LOG_DBG << "Challenge verification failed " << session.uniqueid; + + return -1; } - return -1; -} - -/** + /** * Processes a message sent by a connected user. This will be invoked by web socket on_message handler. * @param user The authenticated user who sent the message. * @param message The message sent by user. * @return 0 on successful processing. -1 for failure. */ -int handle_user_message(connected_user &user, std::string_view message) -{ - rapidjson::Document d; - const char *msg_type = jusrmsg::MSGTYPE_UNKNOWN; - - if (jusrmsg::parse_user_message(d, message) == 0) + int handle_user_message(connected_user &user, std::string_view message) { - const char *msg_type = d[jusrmsg::FLD_TYPE].GetString(); + rapidjson::Document d; + const char *msg_type = jusrmsg::MSGTYPE_UNKNOWN; - // Message is a contract input message. - if (d[jusrmsg::FLD_TYPE] == jusrmsg::MSGTYPE_CONTRACT_INPUT) + if (jusrmsg::parse_user_message(d, message) == 0) { - std::string contentjson; - std::string sig; - if (jusrmsg::extract_signed_input_container(contentjson, sig, d) == 0) - { - std::lock_guard lock(ctx.users_mutex); + const char *msg_type = d[jusrmsg::FLD_TYPE].GetString(); - //Add to the submitted input list. - user.submitted_inputs.push_back(user_submitted_message( - std::move(contentjson), - std::move(sig))); + // Message is a contract input message. + if (d[jusrmsg::FLD_TYPE] == jusrmsg::MSGTYPE_CONTRACT_INPUT) + { + std::string contentjson; + std::string sig; + if (jusrmsg::extract_signed_input_container(contentjson, sig, d) == 0) + { + std::lock_guard lock(ctx.users_mutex); + + //Add to the submitted input list. + user.submitted_inputs.push_back(user_submitted_message( + std::move(contentjson), + std::move(sig))); + return 0; + } + else + { + send_request_status_result(user.session, jusrmsg::STATUS_REJECTED, jusrmsg::REASON_BAD_SIG, msg_type, jusrmsg::origin_data_for_contract_input(sig)); + return -1; + } + } + else if (d[jusrmsg::FLD_TYPE] == jusrmsg::MSGTYPE_STAT) + { + std::string msg; + jusrmsg::create_status_response(msg); + user.session.send(msg); return 0; } else { - send_request_status_result(user.session, jusrmsg::STATUS_REJECTED, jusrmsg::REASON_BAD_SIG, msg_type, jusrmsg::origin_data_for_contract_input(sig)); + LOG_DBG << "Invalid user message type: " << msg_type; + send_request_status_result(user.session, jusrmsg::STATUS_REJECTED, jusrmsg::REASON_INVALID_MSG_TYPE, msg_type, ""); return -1; } } - else if (d[jusrmsg::FLD_TYPE] == jusrmsg::MSGTYPE_STAT) - { - std::string msg; - jusrmsg::create_status_response(msg); - user.session.send(msg); - return 0; - } else { - LOG_DBG << "Invalid user message type: " << msg_type; - send_request_status_result(user.session, jusrmsg::STATUS_REJECTED, jusrmsg::REASON_INVALID_MSG_TYPE, msg_type, ""); + // Bad message. + send_request_status_result(user.session, jusrmsg::STATUS_REJECTED, jusrmsg::REASON_BAD_MSG_FORMAT, msg_type, ""); return -1; } } - else - { - // Bad message. - send_request_status_result(user.session, jusrmsg::STATUS_REJECTED, jusrmsg::REASON_BAD_MSG_FORMAT, msg_type, ""); - return -1; - } -} -/** + /** * Send the specified status result via the provided session. */ -void send_request_status_result(const comm::comm_session &session, std::string_view status, std::string_view reason, std::string_view origin_type, std::string_view origin_extra_data) -{ - std::string msg; - jusrmsg::create_request_status_result(msg, status, reason, origin_type, origin_extra_data); - session.send(msg); -} + void send_request_status_result(const comm::comm_session &session, std::string_view status, std::string_view reason, std::string_view origin_type, std::string_view origin_extra_data) + { + std::string msg; + jusrmsg::create_request_status_result(msg, status, reason, origin_type, origin_extra_data); + session.send(msg); + } -/** + /** * Adds the user denoted by specified session id and public key to the global authed user list. * This should get called after the challenge handshake is verified. * @@ -183,70 +190,70 @@ void send_request_status_result(const comm::comm_session &session, std::string_v * @param pubkey User's binary public key. * @return 0 on successful additions. -1 on failure. */ -int add_user(const comm::comm_session &session, const std::string &pubkey) -{ - const std::string &sessionid = session.uniqueid; - if (ctx.users.count(sessionid) == 1) + int add_user(const comm::comm_session &session, const std::string &pubkey) { - LOG_INFO << sessionid << " already exist. Cannot add user."; - return -1; + const std::string &sessionid = session.uniqueid; + if (ctx.users.count(sessionid) == 1) + { + LOG_INFO << sessionid << " already exist. Cannot add user."; + return -1; + } + + { + std::lock_guard lock(ctx.users_mutex); + ctx.users.emplace(sessionid, usr::connected_user(session, pubkey)); + } + + // Populate sessionid map so we can lookup by user pubkey. + ctx.sessionids.try_emplace(pubkey, sessionid); + + return 0; } - { - std::lock_guard lock(ctx.users_mutex); - ctx.users.emplace(sessionid, usr::connected_user(session, pubkey)); - } - - // Populate sessionid map so we can lookup by user pubkey. - ctx.sessionids.try_emplace(pubkey, sessionid); - - return 0; -} - -/** + /** * Removes the specified public key from the global user list. * This must get called when a user disconnects from HP. * * @param sessionid User socket session id. * @return 0 on successful removals. -1 on failure. */ -int remove_user(const std::string &sessionid) -{ - const auto itr = ctx.users.find(sessionid); - - if (itr == ctx.users.end()) + int remove_user(const std::string &sessionid) { - LOG_INFO << sessionid << " does not exist. Cannot remove user."; - return -1; + const auto itr = ctx.users.find(sessionid); + + if (itr == ctx.users.end()) + { + LOG_INFO << sessionid << " does not exist. Cannot remove user."; + return -1; + } + + usr::connected_user &user = itr->second; + + { + std::lock_guard lock(ctx.users_mutex); + ctx.sessionids.erase(user.pubkey); + } + + ctx.users.erase(itr); + return 0; } - usr::connected_user &user = itr->second; - - { - std::lock_guard lock(ctx.users_mutex); - ctx.sessionids.erase(user.pubkey); - } - - ctx.users.erase(itr); - return 0; -} - -/** + /** * Finds and returns the socket session for the proided user pubkey. * @param pubkey User binary pubkey. * @return Pointer to the socket session. NULL of not found. */ -const comm::comm_session *get_session_by_pubkey(const std::string &pubkey) -{ - const auto sessionid_itr = ctx.sessionids.find(pubkey); - if (sessionid_itr != ctx.sessionids.end()) + const comm::comm_session *get_session_by_pubkey(const std::string &pubkey) { - const auto user_itr = ctx.users.find(sessionid_itr->second); - if (user_itr != ctx.users.end()) - return &user_itr->second.session; + const auto sessionid_itr = ctx.sessionids.find(pubkey); + if (sessionid_itr != ctx.sessionids.end()) + { + const auto user_itr = ctx.users.find(sessionid_itr->second); + if (user_itr != ctx.users.end()) + return &user_itr->second.session; + } + + return NULL; } - return NULL; -} - } // namespace usr \ No newline at end of file diff --git a/src/util.cpp b/src/util.cpp index ed442d9f..2b5e020c 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,84 +1,85 @@ #include "pchheader.hpp" +#include "hplog.hpp" #include "util.hpp" namespace util { -// rollover_hashset class methods + // rollover_hashset class methods -rollover_hashset::rollover_hashset(const uint32_t maxsize) -{ - this->maxsize = maxsize == 0 ? 1 : maxsize; -} + rollover_hashset::rollover_hashset(const uint32_t maxsize) + { + this->maxsize = maxsize == 0 ? 1 : maxsize; + } -/** + /** * Inserts the given hash to the list. * @return True on succesful insertion. False if hash already exists. */ -bool rollover_hashset::try_emplace(const std::string hash) -{ - const auto itr = recent_hashes.find(hash); - if (itr == recent_hashes.end()) // Not found + bool rollover_hashset::try_emplace(const std::string hash) { - // Add the new message hash to the set. - const auto [newitr, success] = recent_hashes.emplace(std::move(hash)); - - // Insert a pointer to the stored hash value to the back of the ordered list of hashes. - recent_hashes_list.push_back(&(*newitr)); - - // Remove oldest hash if exceeding max size. - if (recent_hashes_list.size() > maxsize) + const auto itr = recent_hashes.find(hash); + if (itr == recent_hashes.end()) // Not found { - const std::string &oldest_hash = *recent_hashes_list.front(); - recent_hashes.erase(oldest_hash); - recent_hashes_list.pop_front(); + // Add the new message hash to the set. + const auto [newitr, success] = recent_hashes.emplace(std::move(hash)); + + // Insert a pointer to the stored hash value to the back of the ordered list of hashes. + recent_hashes_list.push_back(&(*newitr)); + + // Remove oldest hash if exceeding max size. + if (recent_hashes_list.size() > maxsize) + { + const std::string &oldest_hash = *recent_hashes_list.front(); + recent_hashes.erase(oldest_hash); + recent_hashes_list.pop_front(); + } + + return true; // Hash was inserted successfuly. } - return true; // Hash was inserted successfuly. + return false; // Hash already exists. } - return false; // Hash already exists. -} + // ttl_set class methods. -// ttl_set class methods. - -/** + /** * If key does not exist, inserts it with the specified ttl. If key exists, * renews the expiration time to match the time-to-live from now onwards. * @param key Object to insert. * @param ttl Time to live in milliseonds. */ -void ttl_set::emplace(const std::string key, uint64_t ttl_milli) -{ - ttlmap[key] = util::get_epoch_milliseconds() + ttl_milli; -} + void ttl_set::emplace(const std::string key, uint64_t ttl_milli) + { + ttlmap[key] = util::get_epoch_milliseconds() + ttl_milli; + } -void ttl_set::erase(const std::string &key) -{ - const auto itr = ttlmap.find(key); - if (itr != ttlmap.end()) - ttlmap.erase(itr); -} + void ttl_set::erase(const std::string &key) + { + const auto itr = ttlmap.find(key); + if (itr != ttlmap.end()) + ttlmap.erase(itr); + } -/** + /** * Returns true of the key exists and not expired. Returns false if key does not exist * or has expired. */ -bool ttl_set::exists(const std::string &key) -{ - const auto itr = ttlmap.find(key); - if (itr == ttlmap.end()) // Not found - return false; + bool ttl_set::exists(const std::string &key) + { + const auto itr = ttlmap.find(key); + if (itr == ttlmap.end()) // Not found + return false; - // Check whether we are passed the expiration time (itr->second is the expiration time) - const bool expired = util::get_epoch_milliseconds() > itr->second; - if (expired) - ttlmap.erase(itr); + // Check whether we are passed the expiration time (itr->second is the expiration time) + const bool expired = util::get_epoch_milliseconds() > itr->second; + if (expired) + ttlmap.erase(itr); - return !expired; -} + return !expired; + } -/** + /** * Encodes provided bytes to hex string. * * @param encoded_string String reference to assign the hex encoded output. @@ -86,63 +87,63 @@ bool ttl_set::exists(const std::string &key) * @param bin_len Bytes length. * @return Always returns 0. */ -int bin2hex(std::string &encoded_string, const unsigned char *bin, const size_t bin_len) -{ - // Allocate the target string. - encoded_string.resize(bin_len * 2); + int bin2hex(std::string &encoded_string, const unsigned char *bin, const size_t bin_len) + { + // Allocate the target string. + encoded_string.resize(bin_len * 2); - // Get encoded string. - sodium_bin2hex( - encoded_string.data(), - encoded_string.length() + 1, // + 1 because sodium writes ending '\0' character as well. - bin, - bin_len); + // Get encoded string. + sodium_bin2hex( + encoded_string.data(), + encoded_string.length() + 1, // + 1 because sodium writes ending '\0' character as well. + bin, + bin_len); - return 0; -} + return 0; + } -/** + /** * Decodes provided hex string into bytes. * * @param decodedbuf Buffer to assign decoded bytes. * @param decodedbuf_len Decoded buffer size. * @param hex_str hex string to decode. */ -int hex2bin(unsigned char *decodedbuf, const size_t decodedbuf_len, std::string_view hex_str) -{ - const char *hex_end; - size_t bin_len; - if (sodium_hex2bin( - decodedbuf, decodedbuf_len, - hex_str.data(), - hex_str.length(), - "", &bin_len, &hex_end)) + int hex2bin(unsigned char *decodedbuf, const size_t decodedbuf_len, std::string_view hex_str) { - return -1; + const char *hex_end; + size_t bin_len; + if (sodium_hex2bin( + decodedbuf, decodedbuf_len, + hex_str.data(), + hex_str.length(), + "", &bin_len, &hex_end)) + { + return -1; + } + + return 0; } - return 0; -} - -/** + /** * Returns current time in UNIX epoch milliseconds. */ -int64_t get_epoch_milliseconds() -{ - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); -} + int64_t get_epoch_milliseconds() + { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + } -/** + /** * Sleeps the current thread for specified no. of milliseconds. */ -void sleep(const uint64_t milliseconds) -{ - std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); -} + void sleep(const uint64_t milliseconds) + { + std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); + } -/** + /** * Compare two version strings in the format of "1.12.3". * v1 < v2 -> returns -1 * v1 == v2 -> returns 0 @@ -154,56 +155,75 @@ void sleep(const uint64_t milliseconds) * syntax because istringstream doesn't support string_view. It's not worth optmising * this code as it's not being used in high-scale processing. */ -int version_compare(const std::string &x, const std::string &y) -{ - std::istringstream ix(x), iy(y); - while (ix.good() || iy.good()) + int version_compare(const std::string &x, const std::string &y) { - int cx = 0, cy = 0; - ix >> cx; - iy >> cy; + std::istringstream ix(x), iy(y); + while (ix.good() || iy.good()) + { + int cx = 0, cy = 0; + ix >> cx; + iy >> cy; - if ((!ix.eof() && !ix.good()) || (!iy.eof() && !iy.good())) - return -2; + if ((!ix.eof() && !ix.good()) || (!iy.eof() && !iy.good())) + return -2; - if (cx > cy) - return 1; - if (cx < cy) - return -1; + if (cx > cy) + return 1; + if (cx < cy) + return -1; - ix.ignore(); - iy.ignore(); + ix.ignore(); + iy.ignore(); + } + + return 0; } - return 0; -} - -/** + /** * Returns a std::string_view pointing to the rapidjson Value which is assumed * to be a string. We use this function because rapidjson does not have built-in string_view * support. Passing a non-string 'v' is not supported. */ -std::string_view getsv(const rapidjson::Value &v) -{ - return std::string_view(v.GetString(), v.GetStringLength()); -} + std::string_view getsv(const rapidjson::Value &v) + { + return std::string_view(v.GetString(), v.GetStringLength()); + } -// Provide a safe std::string overload for realpath -std::string realpath(std::string path) -{ - std::array buffer; - ::realpath(path.c_str(), buffer.data()); - buffer[PATH_MAX] = '\0'; - return buffer.data(); -} + // Provide a safe std::string overload for realpath + std::string realpath(std::string path) + { + std::array buffer; + ::realpath(path.c_str(), buffer.data()); + buffer[PATH_MAX] = '\0'; + return buffer.data(); + } -// Applies signal mask to the calling thread. -void mask_signal() -{ - sigset_t mask; - sigemptyset(&mask); - sigaddset(&mask, SIGINT); - pthread_sigmask(SIG_BLOCK, &mask, NULL); -} + // Applies signal mask to the calling thread. + void mask_signal() + { + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + pthread_sigmask(SIG_BLOCK, &mask, NULL); + } + + // Kill a process with SIGINT and wait until it stops running. + int kill_process(const pid_t pid, const bool wait) + { + if (kill(pid, SIGINT) == -1) + { + LOG_ERR << errno << ": Error issuing SIGINT to pid " << pid; + return -1; + } + + int pid_status; + if (wait && waitpid(pid, &pid_status, 0) == -1) + { + LOG_ERR << errno << ": waitpid failed."; + return -1; + } + + return 0; + } } // namespace util diff --git a/src/util.hpp b/src/util.hpp index 5728c7e9..a0162f11 100644 --- a/src/util.hpp +++ b/src/util.hpp @@ -80,6 +80,8 @@ std::string realpath(std::string path); void mask_signal(); +int kill_process(const pid_t pid, const bool wait); + } // namespace util #endif diff --git a/test/bin/hpfs b/test/bin/hpfs new file mode 100755 index 0000000000000000000000000000000000000000..0c51e627ba183042330e5f50429b0a282e81b3ee GIT binary patch literal 174232 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 J*z2wJ{{XLyC;|Wg literal 0 HcmV?d00001 diff --git a/test/bin/libb2.so.1 b/test/bin/libb2.so.1 new file mode 100755 index 0000000000000000000000000000000000000000..29a4d1f6196f5d3484a065c0ed7a627c01eec3ad GIT binary patch literal 181504 zcmeFad3;nw_BVWQca}~k=}y>WQAngAL>3beBm&ZbARP^e2<}Q)H5ed}AjDw_0+ODX z8Bk{iXZX#aGo#G>bi^4KbaYIl8^jecAc_luU`N1!3xYuMe!r*cc6U1H?|t6)`8RXyI=4=DvwTd^RW{Q!tjlg(X|S|8Rbjlk!uzsKLx$lsh8l^u_cSgR z+W2ryTj1kc70!AZMus5RM*{wi-!4TwGX$`nb-KIP!`-TS7TM0YgEw4vWXQvM?$<-H zo(@%O#No-m*L8jD)1&FECuw<2I8-r%aNYG%Z1X&;(1!K2+FgfsBB!qZpB`zNV508t zr}UDh^@F&?tI{rAdl#{wi zkE=>D2Rf?y8Md@IBf~JVJ0>`~reM|_rmcfh)JJ+3{@nQMh`%oQ>xRDz@t1)=UKiu9 zr$C0`(a&CZX5sJ1`%SJFYfrd#ef!IV-{1V$cX7YF@|u1d*LU1I|E3E+pM6KK>IthS zY?-)v!Y`MYfBpBt2bcUkd*Z)u|M>KQ&x_tBS1=k3l0eDoK+N=7ft zy7jsP!#^1vzoE8U`I@4CmY>b<=6z_!Z(@QF*OuKjHf_Z|} zB9vJ&P|9*h3GuX~<;K`+$4!tvOG{@)_0n2Q-TO3)kzzbejo+$lHJEA9gVE$svWGFP zv@@+NhNCrVr6x7pRya`v@EDbax5SB7t(Jyb(>)udCDZPNJ_eM}8<5MwbV5UnDKjx! zQ=!gj<>ksyfSNml6w?MO;;}`Aqef2|IAGvl!vb?J3xk6OMBsq~A|$|4)0Yj%G5E~I zla~$u?AS3G=OiHV;m#RnnExOD#T#8^ts6!$IIUjyTZ9RT##oJeSjq{;L|v}vQj!F( zVcf3E3%jx$XO!r2hmsO(&vs)W%Dha-nyTwq-r2J^wt9!LT;seRmTgA0rnllPD^9B7 zMvv>%?HTg4@LrAIRNNYGmG@6kc*Z<>=}Y@i6rP&1uH#X7r-j7xR1_XwGJJK`{gMac z59`W^!s{VPRi7xlcKL$OiNZ4mvaX>~_~>}n8--^)V_m)|d~}>QAquZuur#?T3UBF@ z6~$3_D+VO~t|)wWBJ@=jh1afAY8FS~^;ReNswjLriwU09QFs_s_*xr!a{p zEhL^BqwvX5c;CvS4qr|D>gzKM-|D&wTgy)0%7A05(K7f!pjvu8jJvys7ZA&Lv1Q}_ z7F@l4$+F4vPAT8VGN-n&PRjSP%qeZ$Eae3(b1EA*N_jTRoWjPnQl7>#r>LWzM5sKq;Z0jN3l#3ZS+d{3YIz4#vCaRWSOSgm?7m$SZ2uFm?q_m zS?20#G^E^>Wtv9g@n#@;wPTr6-?(4OPL?^fjXR}0abkusYc0xh`D#a;24b?WHvR;j zzPwXD|DlR5P<1;Nq3YDzTlUWy;O?{SWd)Fz3DP;Il{HjKjfRC~8Qs2rEPoM!LgFGp|nh{q69~yxXhJ3hf zi_b^dq&QU+?AxtiS)j;oZ=3}RRnDKBKdI1vZu0dsprV>PVMO!NLWj?_wD{Kh{9AqX z2NMeIw;P2k&bGit?iubYse2ym3RfJ3Q^Vs614AW+DDU9HFKCgs-kyJ3{_PY-=_~)n{2M3(1n{4M44sQ=<~c&M(0@_QFyHi$&mZ#D z?@uT+xBL7B9!F8hA=Ujk5yx9tyP5*{dhtH4_Sr=C$Qkg}{35N$-z>5euGmg=k@;ol z6)+x`_rt>d`6~{QgOHhD7Uh+C91D8-{9nJ6LE-BU$NTK%ogr{%C2;;7`BNIxD3$*! zU*Aa{#}*C}9r#xM==HhB?hH+*%Gjy-<&hU6^B*<7l}Crp@YTfi>cbu|m_E66zS^r= zd@E1&aIYCfVYaWCrF|Q~JG9%v?D%S5or{VBf2h#^MWO$PeE-QJ|1sanFHhy}%RW_G zJqx6{b@h$*Z+-t=So_LmtVVD3Wu6zgGvsey&1zmn{=>fQ1s>CI*bF?y4bAcwc~Z9( zc-$GR;H7{yu-(>Y3=!#Pewk#>}l7Q~k

@`b0%A1bOX_2lIHKP~hh9g~ZR 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 literal 0 HcmV?d00001 diff --git a/test/local-cluster/Dockerfile b/test/local-cluster/Dockerfile index 87b56dc0..8954f865 100644 --- a/test/local-cluster/Dockerfile +++ b/test/local-cluster/Dockerfile @@ -4,18 +4,19 @@ FROM node:10.17.0-buster-slim RUN apt-get update # Install netcat for websocketd <--> domain socket redirection. -RUN apt-get install -y netcat-openbsd +RUN apt-get install -y netcat-openbsd libgomp1 -# Install fuse. -# Copy fuse shared library and register it. +# Install shared libraries. +# Copy shared libraries and register it. COPY ./bin/libfuse3.so.3 /usr/local/lib/ +COPY ./bin/libb2.so.1 /usr/local/lib/ RUN ldconfig COPY ./bin/fusermount3 /usr/local/bin/ # hpcore binary is copied to /hp directory withtin the docker image. WORKDIR /hp COPY ./bin/hpcore . -COPY ./bin/hpstatemon . +COPY ./bin/hpfs . COPY ./bin/websocketd . COPY ./bin/websocat . diff --git a/test/local-cluster/cluster-create.sh b/test/local-cluster/cluster-create.sh index 4550a841..706ab5b3 100755 --- a/test/local-cluster/cluster-create.sh +++ b/test/local-cluster/cluster-create.sh @@ -47,7 +47,7 @@ do # Update contract config. node -p "JSON.stringify({...require('./tmp.json'), \ binary: '/usr/local/bin/node', \ - binargs: './bin/contract.js', \ + binargs: '/contract/bin/contract.js', \ appbill: '', \ appbillargs: '', \ peerport: ${peerport}, \ diff --git a/test/vm-cluster/cluster.sh b/test/vm-cluster/cluster.sh index b4d9131b..eddb129d 100755 --- a/test/vm-cluster/cluster.sh +++ b/test/vm-cluster/cluster.sh @@ -42,7 +42,7 @@ fi if [ $mode = "check" ]; then let nodeid=$2-1 vmip=${vmips[$nodeid]} - sshpass -f vmpass.txt ssh geveo@$vmip 'echo hpcore pid:$(pidof hpcore) hpstatemon pid:$(pidof hpstatemon) websocketd pid:$(pidof websocketd)' + sshpass -f vmpass.txt ssh geveo@$vmip 'echo hpcore pid:$(pidof hpcore) hpfs pid:$(pidof hpfs) websocketd pid:$(pidof websocketd)' exit 0 fi @@ -57,7 +57,7 @@ if [ $mode = "kill" ]; then let nodeid=$2-1 vmip=${vmips[$nodeid]} sshpass -f vmpass.txt ssh geveo@$vmip 'sudo kill $(pidof hpcore) > /dev/null 2>&1' - sshpass -f vmpass.txt ssh geveo@$vmip 'sudo kill $(pidof hpstatemon) > /dev/null 2>&1' + sshpass -f vmpass.txt ssh geveo@$vmip 'sudo kill $(pidof hpfs) > /dev/null 2>&1' sshpass -f vmpass.txt ssh geveo@$vmip 'sudo kill $(pidof websocketd) > /dev/null 2>&1' exit 0 fi diff --git a/test/vm-cluster/setup-hp.sh b/test/vm-cluster/setup-hp.sh index 3bdd7180..c0f75e66 100755 --- a/test/vm-cluster/setup-hp.sh +++ b/test/vm-cluster/setup-hp.sh @@ -13,8 +13,9 @@ fi if [ -x "$(command -v fusermount3)" ]; then echo "FUSE already installed." else - echo "Installing FUSE..." - sudo cp ./libfuse3.so.3 /usr/local/lib/ + echo "Installing FUSE and other shared libraries..." + sudo apt-get -y install libgomp1 + sudo cp ./libfuse3.so.3 ./libb2.so.1 /usr/local/lib/ sudo ldconfig sudo cp ./fusermount3 /usr/local/bin/ fi diff --git a/test/vm-cluster/setup-vm.sh b/test/vm-cluster/setup-vm.sh index d2e91d3e..bb3bb1d2 100755 --- a/test/vm-cluster/setup-vm.sh +++ b/test/vm-cluster/setup-vm.sh @@ -11,12 +11,13 @@ echo $nodeid. $vmip if [ $mode = "new" ]; then sshpass -f vmpass.txt scp $hpcore/build/hpcore \ - $hpcore/build/hpstatemon \ $hpcore/examples/echo_contract/contract.js \ ../bin/libfuse3.so.3 \ + ../bin/libb2.so.1 \ ../bin/fusermount3 \ ../bin/websocketd \ ../bin/websocat \ + ../bin/hpfs \ ./consensus-test-continuous.sh \ ./setup-hp.sh \ geveo@$vmip:~/ @@ -25,7 +26,6 @@ if [ $mode = "new" ]; then sshpass -f vmpass.txt scp geveo@$vmip:~/contract/cfg/hp.cfg ./cfg/node$nodeid.json else sshpass -f vmpass.txt scp $hpcore/build/hpcore \ - $hpcore/build/hpstatemon \ $hpcore/examples/echo_contract/contract.js \ ./consensus-test-continuous.sh \ geveo@$vmip:~/