From 76734a5672377a1d09c584fe607650ee48da4076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ja=C3=ABl=20Champagne=20Gareau?= Date: Thu, 3 Sep 2026 00:45:04 -0400 Subject: [PATCH 1/2] build: bump opendht minimum version to 3.0 version 3.0 introduced the persistance API, which we need. --- CMakeLists.txt | 2 +- README.md | 2 +- configure.ac | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c5892e..357f9ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") ############################ # Find required packages # ############################ -find_package(opendht 1.2.0 REQUIRED) +find_package(opendht 3.0.0 REQUIRED) find_package(CURLpp REQUIRED) find_package(glibmm REQUIRED) find_package(B64 REQUIRED) diff --git a/README.md b/README.md index 16ad1f0..0482680 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Milis Linux: mps kur dpaste (https://github.com/milisarge/malfs-milis/blob/maste ## Dependencies -- [OpenDHT](https://github.com/savoirfairelinux/opendht/) (minimal version: 1.2.0) +- [OpenDHT](https://github.com/savoirfairelinux/opendht/) (minimal version: 3.0.0) - [msgpack-c](https://github.com/msgpack/msgpack-c) - [gpgmepp](https://github.com/KDE/gpgmepp) - [json.hpp](https://github.com/nlohmann/json) (required version for CMake: 2.1.1) diff --git a/configure.ac b/configure.ac index 3ff8a0d..00d5bcd 100644 --- a/configure.ac +++ b/configure.ac @@ -18,7 +18,7 @@ AS_IF([test "x$enable_debug" = "xyes"], AC_PROG_CXX AC_PROG_RANLIB -PKG_CHECK_MODULES([OpenDHT], [opendht >= 1.2]) +PKG_CHECK_MODULES([OpenDHT], [opendht >= 3.0.0]) PKG_CHECK_MODULES([CURLPP], [curlpp]) PKG_CHECK_MODULES([GLIBMM], [glibmm-2.4]) From d17666f7c72326c9701b9763159b2c57b6c625d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ja=C3=ABl=20Champagne=20Gareau?= Date: Thu, 3 Sep 2026 00:47:23 -0400 Subject: [PATCH 2/2] perf: add a DHT cache machanism --- README.md | 8 ++++++ src/node.cpp | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/node.h | 33 +++++++++++++++++------ 3 files changed, 108 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0482680..088a08a 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,14 @@ Milis Linux: mps kur dpaste (https://github.com/milisarge/malfs-milis/blob/maste - [catch](https://github.com/catchorg/Catch2) for unit tests - getopt (util-linux) +## Caching + +To avoid the multi-second DHT cold start on every invocation, `dpaste` caches +the DHT identity and the node state (routing table) on disk. The cache lives +in `${XDG_CACHE_HOME}/dpaste` (usually `~/.cache/dpaste`) and can be relocated +with the `DPASTE_CACHE_DIR` environment variable. The state is refreshed at +the end of every run; the first run remains slow, subsequent ones start warm. + ## Pastebin over DHT A DHT is efficient and requires no infrastructure. In practice, you can always diff --git a/src/node.cpp b/src/node.cpp index dcbeca2..25752a4 100644 --- a/src/node.cpp +++ b/src/node.cpp @@ -21,8 +21,14 @@ #include #include #include +#include +#include +#include +#include +#include #include +#include #include "node.h" @@ -30,6 +36,75 @@ namespace dpaste { const constexpr char* Node::DPASTE_USER_TYPE; +namespace { + +/** + * Directory holding the on-disk caches (identity + node state). + * Can be overridden with the DPASTE_CACHE_DIR environment variable + * (e.g. for tests). Defaults to ${XDG_CACHE_HOME}/dpaste. + */ +std::string cacheDir() { + const char* env = std::getenv("DPASTE_CACHE_DIR"); + std::string dir = env and *env ? env : Glib::get_user_cache_dir() + "/dpaste"; + std::error_code ec; + std::filesystem::create_directories(dir, ec); + return dir; +} + +} /* anonymous namespace */ + +dht::crypto::Identity Node::loadIdentity() { + /* Only try the cache if the files exist: a missing cache is the normal + * first-run case and shouldn't print a scary error. */ + std::ifstream key_file(identity_path_ + ".pem"); + if (key_file.good()) { + try { + auto id = dht::crypto::loadIdentity(identity_path_); + if (id.first and id.second) + return id; + } catch (const std::exception& e) { + std::cerr << "dpaste: cached identity is corrupt (" << e.what() << "), generating a new one." << std::endl; + } + } + + auto id = dht::crypto::generateIdentity(); + try { + /* Write to temporary files first, then rename, so concurrent dpaste + * processes never leave a half-written identity in the cache. */ + auto tmp_path = identity_path_ + ".tmp"; + dht::crypto::saveIdentity(id, tmp_path); + std::rename((tmp_path + ".pem").c_str(), (identity_path_ + ".pem").c_str()); + std::rename((tmp_path + ".crt").c_str(), (identity_path_ + ".crt").c_str()); + } catch (const std::exception& e) { + std::cerr << "dpaste: failed to cache identity: " << e.what() << std::endl; + } + return id; +} + +void Node::run(uint16_t port, std::string bootstrap_hostname, std::string bootstrap_port) { + if (running_) + return; + + auto dir = cacheDir(); + identity_path_ = dir + "/identity"; + nodes_path_ = dir + "/nodes"; + + /* Load (or generate and cache) the identity so we don't pay for RSA key + * generation on every run. */ + auto identity = loadIdentity(); + + /* Ask OpenDHT to load its state (routing table) on start and save it on + * shutdown; this turns the multi-second DHT cold start into a warm one. */ + dht::DhtRunner::Config config; + config.dht_config.id = identity; + config.dht_config.node_config.persist_path = nodes_path_; + config.threaded = true; + node_.run(port, config); + + node_.bootstrap(bootstrap_hostname, bootstrap_port); + running_ = true; +} + bool Node::paste(const std::string& code, dht::Blob&& blob, dht::DoneCallbackSimple&& cb) { auto v = std::make_shared(std::forward(blob)); v->user_type = DPASTE_USER_TYPE; diff --git a/src/node.h b/src/node.h index 48f1f75..71af14e 100644 --- a/src/node.h +++ b/src/node.h @@ -54,15 +54,22 @@ class Node { static const constexpr char* DPASTE_USER_TYPE = "dpaste"; Node() {} - virtual ~Node () {} - - void run(uint16_t port = 0, std::string bootstrap_hostname = DEFAULT_BOOTSTRAP_NODE, std::string bootstrap_port = DEFAULT_BOOTSTRAP_PORT) { + virtual ~Node() { + /* Persist the node state (routing table) on disk so the next run + * starts warm instead of cold-bootstrapping the DHT. */ if (running_) - return; - node_.run(port, dht::crypto::generateIdentity(), true); - node_.bootstrap(bootstrap_hostname, bootstrap_port); - running_ = true; - }; + stop(); + } + + /** + * Start the DHT node. The identity and the node state are cached on disk + * (see DPASTE_CACHE_DIR below) so subsequent runs connect much faster. + * + * @param port Local port to bind (0 for random). + * @param bootstrap_hostname Hostname of the bootstrap node. + * @param bootstrap_port Port of the bootstrap node. + */ + void run(uint16_t port = 0, std::string bootstrap_hostname = DEFAULT_BOOTSTRAP_NODE, std::string bootstrap_port = DEFAULT_BOOTSTRAP_PORT); void stop() { std::condition_variable cv; @@ -116,9 +123,19 @@ class Node { private: + /** + * Load the DHT identity from the on-disk cache, generating and caching a + * new one if the cache is missing or corrupt. + */ + dht::crypto::Identity loadIdentity(); + dht::DhtRunner node_; bool running_ {false}; + /* on-disk cache locations (identity + DHT node state) */ + std::string identity_path_ {}; + std::string nodes_path_ {}; + std::uniform_int_distribution codeDist_; std::mt19937_64 rand_; };