aboutsummaryrefslogtreecommitdiffstats
path: root/src/Merkle.cpp
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2026-05-13 01:59:22 -0300
committerGitHub <noreply@github.com>2026-05-13 01:59:22 -0300
commitaf6073dedffd1a16aff7a283ded50545dc40b179 (patch)
treea4b8fbfb4842f5ac27bf7fa641555de3fac6b7f3 /src/Merkle.cpp
parentc1b70cfe745d935bded3053fba8c1f9573aa07aa (diff)
downloadRNT-af6073dedffd1a16aff7a283ded50545dc40b179.tar.gz
RNT-af6073dedffd1a16aff7a283ded50545dc40b179.zip
Extern handles for embedded runtime
Diffstat (limited to '')
-rw-r--r--src/Merkle.cpp488
1 files changed, 488 insertions, 0 deletions
diff --git a/src/Merkle.cpp b/src/Merkle.cpp
new file mode 100644
index 0000000..56ab349
--- /dev/null
+++ b/src/Merkle.cpp
@@ -0,0 +1,488 @@
+#include "Merkle.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cstring>
+#include <numeric>
+#include <stdexcept>
+
+namespace nt {
+
+// ── Wire format constants ────────────────────────────────────────────────────
+
+static constexpr uint8_t TAG_LEAF = 0x4C; // 'L'
+static constexpr uint8_t TAG_INTERNAL = 0x49; // 'I'
+
+// ── Byte-order helpers ───────────────────────────────────────────────────────
+
+static void write_u32(std::vector<uint8_t>& buf, uint32_t v)
+{
+ buf.push_back(static_cast<uint8_t>(v >> 24));
+ buf.push_back(static_cast<uint8_t>(v >> 16));
+ buf.push_back(static_cast<uint8_t>(v >> 8));
+ buf.push_back(static_cast<uint8_t>(v ));
+}
+
+static void write_u64(std::vector<uint8_t>& buf, uint64_t v)
+{
+ buf.push_back(static_cast<uint8_t>(v >> 56));
+ buf.push_back(static_cast<uint8_t>(v >> 48));
+ buf.push_back(static_cast<uint8_t>(v >> 40));
+ buf.push_back(static_cast<uint8_t>(v >> 32));
+ buf.push_back(static_cast<uint8_t>(v >> 24));
+ buf.push_back(static_cast<uint8_t>(v >> 16));
+ buf.push_back(static_cast<uint8_t>(v >> 8));
+ buf.push_back(static_cast<uint8_t>(v ));
+}
+
+static uint32_t read_u32(const uint8_t* p)
+{
+ return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16)
+ | (uint32_t(p[2]) << 8) | uint32_t(p[3]);
+}
+
+static uint64_t read_u64(const uint8_t* p)
+{
+ return (uint64_t(p[0]) << 56) | (uint64_t(p[1]) << 48)
+ | (uint64_t(p[2]) << 40) | (uint64_t(p[3]) << 32)
+ | (uint64_t(p[4]) << 24) | (uint64_t(p[5]) << 16)
+ | (uint64_t(p[6]) << 8) | uint64_t(p[7]);
+}
+
+// ── Hex / binary conversion ──────────────────────────────────────────────────
+
+Merkle::Hash32 Merkle::hex_to_bin(const std::string& hex)
+{
+ if (hex.size() != 64)
+ throw std::invalid_argument(
+ "Merkle: hash must be exactly 64 hex characters, got " +
+ std::to_string(hex.size()));
+
+ Hash32 out{};
+ auto nibble = [](char c) -> uint8_t {
+ if (c >= '0' && c <= '9') return static_cast<uint8_t>(c - '0');
+ if (c >= 'a' && c <= 'f') return static_cast<uint8_t>(c - 'a' + 10);
+ if (c >= 'A' && c <= 'F') return static_cast<uint8_t>(c - 'A' + 10);
+ throw std::invalid_argument(std::string("Merkle: invalid hex character '") + c + "'");
+ };
+ for (size_t i = 0; i < 32; ++i)
+ out[i] = static_cast<uint8_t>((nibble(hex[2*i]) << 4) | nibble(hex[2*i+1]));
+ return out;
+}
+
+std::string Merkle::bin_to_hex(const Hash32& bin)
+{
+ static const char HEX[] = "0123456789abcdef";
+ std::string out(64, '0');
+ for (size_t i = 0; i < 32; ++i)
+ {
+ out[2*i] = HEX[(bin[i] >> 4) & 0xF];
+ out[2*i+1] = HEX[bin[i] & 0xF];
+ }
+ return out;
+}
+
+// ── Node serialisation ───────────────────────────────────────────────────────
+
+std::vector<uint8_t> Merkle::encode_leaf(const LeafNode& n)
+{
+ std::vector<uint8_t> buf;
+ buf.reserve(1 + 4 + n.hashes.size() * 32);
+ buf.push_back(TAG_LEAF);
+ write_u32(buf, static_cast<uint32_t>(n.hashes.size()));
+ for (const auto& h : n.hashes)
+ buf.insert(buf.end(), h.begin(), h.end());
+ return buf;
+}
+
+std::vector<uint8_t> Merkle::encode_internal(const InternalNode& n)
+{
+ // Each entry: 8 (leaf_count) + 32 (min_hash) + 32 (child_hash) = 72 bytes
+ std::vector<uint8_t> buf;
+ buf.reserve(1 + 4 + n.entries.size() * 72);
+ buf.push_back(TAG_INTERNAL);
+ write_u32(buf, static_cast<uint32_t>(n.entries.size()));
+ for (const auto& e : n.entries)
+ {
+ write_u64(buf, e.leaf_count);
+ buf.insert(buf.end(), e.min_hash.begin(), e.min_hash.end());
+ buf.insert(buf.end(), e.child_hash.begin(), e.child_hash.end());
+ }
+ return buf;
+}
+
+Merkle::LeafNode Merkle::decode_leaf(const std::vector<uint8_t>& bytes)
+{
+ uint32_t count = read_u32(bytes.data() + 1);
+ LeafNode n;
+ n.hashes.reserve(count);
+ const uint8_t* p = bytes.data() + 5;
+ for (uint32_t i = 0; i < count; ++i, p += 32)
+ {
+ Hash32 h;
+ std::memcpy(h.data(), p, 32);
+ n.hashes.push_back(h);
+ }
+ return n;
+}
+
+Merkle::InternalNode Merkle::decode_internal(const std::vector<uint8_t>& bytes)
+{
+ uint32_t count = read_u32(bytes.data() + 1);
+ InternalNode n;
+ n.entries.reserve(count);
+ const uint8_t* p = bytes.data() + 5;
+ for (uint32_t i = 0; i < count; ++i, p += 72)
+ {
+ ChildEntry e;
+ e.leaf_count = read_u64(p);
+ std::memcpy(e.min_hash.data(), p + 8, 32);
+ std::memcpy(e.child_hash.data(), p + 40, 32);
+ n.entries.push_back(e);
+ }
+ return n;
+}
+
+bool Merkle::is_leaf_bytes(const std::vector<uint8_t>& bytes)
+{
+ return !bytes.empty() && bytes[0] == TAG_LEAF;
+}
+
+// ── Storage helpers ──────────────────────────────────────────────────────────
+
+std::vector<uint8_t> Merkle::load_node(IStorageBackend& store,
+ const std::string& hash_hex)
+{
+ auto opt = store.Get(hash_hex);
+ if (!opt)
+ throw std::runtime_error("Merkle: node not found: " + hash_hex);
+ return std::move(*opt);
+}
+
+std::string Merkle::store_leaf(IStorageBackend& store, const LeafNode& n)
+{
+ return store.Put(encode_leaf(n));
+}
+
+std::string Merkle::store_internal(IStorageBackend& store, const InternalNode& n)
+{
+ return store.Put(encode_internal(n));
+}
+
+Merkle::Hash32 Merkle::subtree_min(IStorageBackend& store, const std::string& node_hex)
+{
+ auto bytes = load_node(store, node_hex);
+ if (is_leaf_bytes(bytes))
+ {
+ auto leaf = decode_leaf(bytes);
+ assert(!leaf.hashes.empty() && "Merkle invariant: leaf node must never be empty");
+ return leaf.hashes.front();
+ }
+ auto node = decode_internal(bytes);
+ assert(!node.entries.empty() && "Merkle invariant: internal node must never be empty");
+ return node.entries.front().min_hash;
+}
+
+// ── Routing helper ───────────────────────────────────────────────────────────
+
+size_t Merkle::route(const InternalNode& node, const Hash32& target)
+{
+ assert(!node.entries.empty() && "Merkle invariant: internal node must never be empty");
+ assert(std::is_sorted(node.entries.begin(), node.entries.end(),
+ [](const ChildEntry& a, const ChildEntry& b) {
+ return a.min_hash < b.min_hash;
+ }) && "Merkle invariant: internal node entries must be sorted by min_hash");
+
+ // Rightmost child whose min_hash ≤ target.
+ // Defaults to 0 (leftmost) when target < all min_hashes.
+ size_t idx = 0;
+ for (size_t i = 1; i < node.entries.size(); ++i)
+ {
+ if (node.entries[i].min_hash <= target)
+ idx = i;
+ else
+ break;
+ }
+ return idx;
+}
+
+// ── Insert ───────────────────────────────────────────────────────────────────
+
+std::string Merkle::Insert(IStorageBackend& store,
+ const std::string& root_hex,
+ const std::string& hash_hex)
+{
+ const Hash32 tuple_hash = hex_to_bin(hash_hex);
+
+ if (root_hex.empty())
+ {
+ LeafNode leaf;
+ leaf.hashes.push_back(tuple_hash);
+ return store_leaf(store, leaf);
+ }
+
+ auto result = insert_into(store, root_hex, tuple_hash);
+ if (!result.did_split)
+ return result.new_hash;
+
+ // Root split: wrap both halves in a new internal root.
+ Hash32 left_min = subtree_min(store, result.new_hash);
+ Hash32 right_min = result.split_min_hash;
+
+ InternalNode new_root;
+
+ ChildEntry left;
+ left.leaf_count = result.leaf_count;
+ left.min_hash = left_min;
+ left.child_hash = hex_to_bin(result.new_hash);
+ new_root.entries.push_back(left);
+
+ ChildEntry right;
+ right.leaf_count = result.split_leaf_count;
+ right.min_hash = right_min;
+ right.child_hash = hex_to_bin(result.split_hash);
+ new_root.entries.push_back(right);
+
+ return store_internal(store, new_root);
+}
+
+Merkle::InsertResult Merkle::insert_into(IStorageBackend& store,
+ const std::string& node_hex,
+ const Hash32& tuple_hash)
+{
+ auto bytes = load_node(store, node_hex);
+
+ // ── Leaf node ───────────────────────────────────────────────────────────
+ if (is_leaf_bytes(bytes))
+ {
+ auto leaf = decode_leaf(bytes);
+
+ auto pos = std::lower_bound(leaf.hashes.begin(), leaf.hashes.end(), tuple_hash);
+ if (pos != leaf.hashes.end() && *pos == tuple_hash)
+ {
+ // Already present — no-op.
+ InsertResult r;
+ r.new_hash = node_hex;
+ r.leaf_count = leaf.hashes.size();
+ return r;
+ }
+
+ leaf.hashes.insert(pos, tuple_hash);
+
+ if (leaf.hashes.size() <= B)
+ {
+ InsertResult r;
+ r.new_hash = store_leaf(store, leaf);
+ r.leaf_count = leaf.hashes.size();
+ return r;
+ }
+
+ // Split leaf at midpoint.
+ size_t mid = leaf.hashes.size() / 2;
+ LeafNode left_leaf, right_leaf;
+ left_leaf.hashes = { leaf.hashes.begin(), leaf.hashes.begin() + static_cast<ptrdiff_t>(mid) };
+ right_leaf.hashes = { leaf.hashes.begin() + static_cast<ptrdiff_t>(mid), leaf.hashes.end() };
+
+ InsertResult r;
+ r.new_hash = store_leaf(store, left_leaf);
+ r.leaf_count = left_leaf.hashes.size();
+ r.did_split = true;
+ r.split_leaf_count = right_leaf.hashes.size();
+ r.split_min_hash = right_leaf.hashes.front();
+ r.split_hash = store_leaf(store, right_leaf);
+ return r;
+ }
+
+ // ── Internal node ───────────────────────────────────────────────────────
+ auto node = decode_internal(bytes);
+
+ size_t child_idx = route(node, tuple_hash);
+ auto child_result = insert_into(store,
+ bin_to_hex(node.entries[child_idx].child_hash),
+ tuple_hash);
+
+ node.entries[child_idx].leaf_count = child_result.leaf_count;
+ node.entries[child_idx].child_hash = hex_to_bin(child_result.new_hash);
+
+ // Update min_hash for the modified child — only child 0 can have its min
+ // decrease (routing guarantees min_hash[i>0] ≤ inserted hash, so inserting
+ // below min_hash[i>0] is impossible).
+ if (child_idx == 0)
+ node.entries[0].min_hash = subtree_min(store, child_result.new_hash);
+
+ uint64_t total = 0;
+ for (const auto& e : node.entries) total += e.leaf_count;
+
+ if (!child_result.did_split)
+ {
+ InsertResult r;
+ r.new_hash = store_internal(store, node);
+ r.leaf_count = total;
+ return r;
+ }
+
+ // Insert the right sibling immediately after child_idx.
+ ChildEntry sibling;
+ sibling.leaf_count = child_result.split_leaf_count;
+ sibling.min_hash = child_result.split_min_hash;
+ sibling.child_hash = hex_to_bin(child_result.split_hash);
+ node.entries.insert(node.entries.begin() + static_cast<ptrdiff_t>(child_idx) + 1, sibling);
+ total += child_result.split_leaf_count;
+
+ if (node.entries.size() <= B)
+ {
+ InsertResult r;
+ r.new_hash = store_internal(store, node);
+ r.leaf_count = total;
+ return r;
+ }
+
+ // Split this internal node.
+ size_t mid = node.entries.size() / 2;
+ InternalNode left_node, right_node;
+ left_node.entries = { node.entries.begin(), node.entries.begin() + static_cast<ptrdiff_t>(mid) };
+ right_node.entries = { node.entries.begin() + static_cast<ptrdiff_t>(mid), node.entries.end() };
+
+ uint64_t left_count = 0, right_count = 0;
+ for (const auto& e : left_node.entries) left_count += e.leaf_count;
+ for (const auto& e : right_node.entries) right_count += e.leaf_count;
+
+ InsertResult r;
+ r.new_hash = store_internal(store, left_node);
+ r.leaf_count = left_count;
+ r.did_split = true;
+ r.split_leaf_count = right_count;
+ r.split_min_hash = right_node.entries.front().min_hash;
+ r.split_hash = store_internal(store, right_node);
+ return r;
+}
+
+// ── Remove ───────────────────────────────────────────────────────────────────
+
+std::string Merkle::Remove(IStorageBackend& store,
+ const std::string& root_hex,
+ const std::string& hash_hex)
+{
+ if (root_hex.empty()) return "";
+ const Hash32 tuple_hash = hex_to_bin(hash_hex);
+ auto result = remove_from(store, root_hex, tuple_hash);
+ if (result.new_hash.empty()) return "";
+
+ // Collapse: if the root is an internal node with a single child, skip it.
+ auto bytes = load_node(store, result.new_hash);
+ if (!is_leaf_bytes(bytes))
+ {
+ auto node = decode_internal(bytes);
+ if (node.entries.size() == 1)
+ return bin_to_hex(node.entries[0].child_hash);
+ }
+ return result.new_hash;
+}
+
+Merkle::RemoveResult Merkle::remove_from(IStorageBackend& store,
+ const std::string& node_hex,
+ const Hash32& tuple_hash)
+{
+ auto bytes = load_node(store, node_hex);
+
+ // ── Leaf node ───────────────────────────────────────────────────────────
+ if (is_leaf_bytes(bytes))
+ {
+ auto leaf = decode_leaf(bytes);
+ auto it = std::lower_bound(leaf.hashes.begin(), leaf.hashes.end(), tuple_hash);
+ if (it == leaf.hashes.end() || *it != tuple_hash)
+ return { node_hex, leaf.hashes.size() }; // not found — no change
+
+ leaf.hashes.erase(it);
+ if (leaf.hashes.empty())
+ return { "", 0 };
+ return { store_leaf(store, leaf), leaf.hashes.size() };
+ }
+
+ // ── Internal node ───────────────────────────────────────────────────────
+ auto node = decode_internal(bytes);
+
+ size_t child_idx = route(node, tuple_hash);
+ auto child_result = remove_from(store,
+ bin_to_hex(node.entries[child_idx].child_hash),
+ tuple_hash);
+
+ if (child_result.new_hash.empty())
+ {
+ node.entries.erase(node.entries.begin() + static_cast<ptrdiff_t>(child_idx));
+ if (node.entries.empty()) return { "", 0 };
+ }
+ else
+ {
+ node.entries[child_idx].leaf_count = child_result.leaf_count;
+ node.entries[child_idx].child_hash = hex_to_bin(child_result.new_hash);
+ // The min of any child might have changed if we removed its minimum entry.
+ node.entries[child_idx].min_hash = subtree_min(store, child_result.new_hash);
+ }
+
+ uint64_t total = 0;
+ for (const auto& e : node.entries) total += e.leaf_count;
+ return { store_internal(store, node), total };
+}
+
+// ── Page ─────────────────────────────────────────────────────────────────────
+
+std::vector<std::string> Merkle::Page(IStorageBackend& store,
+ const std::string& root_hex,
+ size_t offset,
+ size_t limit)
+{
+ if (root_hex.empty() || limit == 0) return {};
+ std::vector<Hash32> raw;
+ raw.reserve(limit);
+ size_t skip = offset;
+ page_from(store, root_hex, skip, limit, raw);
+ std::vector<std::string> out;
+ out.reserve(raw.size());
+ for (const auto& h : raw) out.push_back(bin_to_hex(h));
+ return out;
+}
+
+void Merkle::page_from(IStorageBackend& store,
+ const std::string& node_hex,
+ size_t& offset,
+ size_t limit,
+ std::vector<Hash32>& out)
+{
+ auto bytes = load_node(store, node_hex);
+
+ if (is_leaf_bytes(bytes))
+ {
+ auto leaf = decode_leaf(bytes);
+ const size_t available = leaf.hashes.size();
+
+ if (offset >= available)
+ {
+ offset -= available;
+ return;
+ }
+
+ for (size_t i = offset; i < available && out.size() < limit; ++i)
+ out.push_back(leaf.hashes[i]);
+
+ offset = 0;
+ return;
+ }
+
+ auto node = decode_internal(bytes);
+
+ for (const auto& entry : node.entries)
+ {
+ if (out.size() >= limit) break;
+
+ if (offset >= entry.leaf_count)
+ {
+ offset -= entry.leaf_count;
+ continue;
+ }
+
+ page_from(store, bin_to_hex(entry.child_hash), offset, limit, out);
+ }
+}
+
+} // namespace nt