aboutsummaryrefslogtreecommitdiffstats
path: root/include/Merkle.h
diff options
context:
space:
mode:
Diffstat (limited to 'include/Merkle.h')
-rw-r--r--include/Merkle.h162
1 files changed, 99 insertions, 63 deletions
diff --git a/include/Merkle.h b/include/Merkle.h
index 8380093..ba8018a 100644
--- a/include/Merkle.h
+++ b/include/Merkle.h
@@ -5,105 +5,143 @@
#include <array>
#include <cstddef>
#include <cstdint>
+#include <optional>
#include <string>
#include <vector>
/**
* @file Merkle.h
- * @brief Sorted content-addressed B-tree over SHA-256 tuple hash hex strings.
+ * @brief Generic sorted content-addressed B-tree over a templated key type.
*
- * Each node is stored as an immutable blob in IStorageBackend via Put/Get.
- * All three operations load only the nodes on the path from root to the target
- * leaf — O(log_B(n)) nodes in memory at once, never the whole tree.
+ * Maps `Key → Hash32 payload` with `Key` totally ordered. Each tree node is
+ * stored as an immutable blob in IStorageBackend via Put/Get. All operations
+ * load only the nodes on the path from root to the target leaf — O(log_B(n))
+ * nodes in memory at once, never the whole tree.
*
- * **Branching factor B = 64**: leaves hold up to 64 hashes; internal nodes hold
- * up to 64 child entries. At B=64 and 1 billion tuples the tree is at most
- * 5 levels deep (~20 KB peak working set).
+ * Instantiated three times in RNT:
+ *
+ * | Level | Key | Payload (Hash32) |
+ * |---------------|----------------|-------------------------|
+ * | Branch tree | `std::string` | multigroup snapshot hash|
+ * | Multigroup | `std::string` | relation merkle root |
+ * | Relation | `nt::Hash32` | tuple hash (key=payload)|
+ *
+ * **Branching factor B = 64**. At 64 children per node and a billion leaves the
+ * tree is at most 5 levels deep (~20 KB peak working set).
*
* **Immutability**: nodes are never overwritten once written (content-addressed).
- * Insert and Remove produce new nodes only along the modified path; old nodes
+ * Insert / Remove produce new nodes only along the modified path; old nodes
* remain valid for any snapshot that still holds their root hash.
*
- * **Internal node entry layout** (leaf_count | min_hash | child_hash):
+ * **Leaf node** stores a sorted-by-key list of `(Key, Hash32 payload)` pairs,
+ * length ≤ B. For the relation level the key and payload happen to carry the
+ * same value (the tuple hash); callers pass it twice. Storing both keeps the
+ * tree shape uniform across the three levels.
+ *
+ * **Internal node entry** layout (`leaf_count | min_key | child_hash`):
* - `leaf_count` enables O(log_B(n) + limit) offset navigation for Page()
* without loading sibling subtrees.
- * - `min_hash` (smallest tuple hash in the subtree) is the routing key.
+ * - `min_key` is the smallest key in the subtree — the routing key.
* - `child_hash` is the content-addressed hash of the child node blob.
*
* **Invariants** (asserted in debug builds):
* - No leaf or internal node is ever empty.
- * - Internal node entries are sorted ascending by min_hash.
- * - All public entry points that accept a hex hash throw `std::invalid_argument`
- * when the string is not exactly 64 lowercase hex characters.
+ * - Leaf entries are sorted ascending by key.
+ * - Internal entries are sorted ascending by min_key.
*
- * @todo Add bounds checking in decode_leaf / decode_internal: validate that the
- * reported entry count does not exceed the actual blob size before reading.
- * Currently a truncated or corrupt blob causes out-of-bounds reads.
+ * **Wire format**: tagged 1-byte node kind ('L' or 'I'), then big-endian u32
+ * count, then count repetitions of either a leaf entry or a child entry.
+ * Key encoding is per-specialisation: `Hash32` is 32 raw bytes; `std::string`
+ * is `u32_be length | bytes`. Wire format is content-addressed — any change
+ * here invalidates every existing stored snapshot.
*/
namespace nt {
+/** @brief 32-byte hash, the universal payload type for all merkle levels. */
+using Hash32 = std::array<uint8_t, 32>;
+
+/** @brief Decode a 64-character lowercase-hex SHA256 string into raw bytes.
+ * Throws std::invalid_argument when the string is not exactly 64 hex chars. */
+Hash32 hex_to_bin(const std::string& hex);
+
+/** @brief Encode raw 32 bytes back into a 64-character lowercase-hex string. */
+std::string bin_to_hex(const Hash32& bin);
+
+template<typename Key>
class Merkle {
public:
static constexpr size_t B = 64;
/**
- * @brief Insert a tuple hash into the tree.
- * @param store KV backend for node serialization.
+ * @brief Insert or overwrite a `(key, payload)` mapping.
+ * @param store KV backend for node serialisation.
* @param root_hex Current root hash (empty string = empty tree).
- * @param hash_hex 64-char hex SHA-256 of the tuple to insert.
- * @return New root hash. Equal to root_hex when the hash was already present.
+ * @param key Routing/identity key.
+ * @param payload Hash this key maps to (child root one level down, or
+ * the key itself at the relation level).
+ * @return New root hash. Equal to root_hex when an insertion of an
+ * identical (key, payload) pair would be a no-op.
*/
static std::string Insert(IStorageBackend& store,
- const std::string& root_hex,
- const std::string& hash_hex);
+ const std::string& root_hex,
+ const Key& key,
+ const Hash32& payload);
/**
- * @brief Remove a tuple hash from the tree.
- * @param store KV backend.
- * @param root_hex Current root hash.
- * @param hash_hex 64-char hex SHA-256 of the tuple to remove.
- * @return New root hash. Empty string when the tree becomes empty.
+ * @brief Remove a key from the tree.
+ * @return New root hash. Empty string when the tree becomes empty.
+ * Equal to root_hex when the key is absent.
*/
static std::string Remove(IStorageBackend& store,
- const std::string& root_hex,
- const std::string& hash_hex);
+ const std::string& root_hex,
+ const Key& key);
+
+ /**
+ * @brief Look up the payload for a key.
+ * @return The payload when present, std::nullopt when absent.
+ */
+ static std::optional<Hash32> Get(IStorageBackend& store,
+ const std::string& root_hex,
+ const Key& key);
+
+ /** @brief One (key, payload) pair returned by Page(). */
+ struct Entry {
+ Key key;
+ Hash32 payload;
+ };
/**
- * @brief Page through tuple hashes in sorted order.
+ * @brief Page through entries in key-sorted order.
*
* Descends to the leaf at logical position @p offset using cumulative
* leaf_counts stored in internal nodes, then walks forward collecting at
- * most @p limit hashes. Only the nodes on the active path are loaded;
+ * most @p limit entries. Only the nodes on the active path are loaded;
* sibling subtrees are never touched.
- *
- * @param store KV backend.
- * @param root_hex Current root hash.
- * @param offset Zero-based logical tuple offset.
- * @param limit Maximum number of hashes to return.
- * @return Hex hash strings in sorted order. Empty when past the end.
*/
- static std::vector<std::string> Page(IStorageBackend& store,
- const std::string& root_hex,
- size_t offset,
- size_t limit);
+ static std::vector<Entry> Page(IStorageBackend& store,
+ const std::string& root_hex,
+ size_t offset,
+ size_t limit);
private:
- using Hash32 = std::array<uint8_t, 32>;
+ struct LeafEntry {
+ Key key;
+ Hash32 payload;
+ };
struct LeafNode {
- std::vector<Hash32> hashes; // sorted ascending, len ≤ B
+ std::vector<LeafEntry> entries;
};
- // Wire: 8-byte leaf_count | 32-byte min_hash | 32-byte child_hash
struct ChildEntry {
- uint64_t leaf_count; // number of tuple hashes in this subtree
- Hash32 min_hash; // smallest tuple hash in the subtree
- Hash32 child_hash; // content-addressed hash of the child node
+ uint64_t leaf_count;
+ Key min_key;
+ Hash32 child_hash;
};
struct InternalNode {
- std::vector<ChildEntry> entries; // sorted by min_hash, len ≤ B
+ std::vector<ChildEntry> entries;
};
// Wire format
@@ -113,34 +151,32 @@ private:
static InternalNode decode_internal(const std::vector<uint8_t>& bytes);
static bool is_leaf_bytes(const std::vector<uint8_t>& bytes);
- // Hex / binary
- static Hash32 hex_to_bin(const std::string& hex);
- static std::string bin_to_hex(const Hash32& bin);
-
// Storage helpers
static std::vector<uint8_t> load_node(IStorageBackend& store,
const std::string& hash_hex);
static std::string store_leaf(IStorageBackend& store, const LeafNode& n);
static std::string store_internal(IStorageBackend& store, const InternalNode& n);
- // Returns the minimum hash of the subtree rooted at node_hex.
- static Hash32 subtree_min(IStorageBackend& store, const std::string& node_hex);
+ /** @brief Returns the smallest key in the subtree rooted at @p node_hex. */
+ static Key subtree_min_key(IStorageBackend& store, const std::string& node_hex);
+
+ /** @brief Routes a key to the rightmost child whose min_key ≤ target. */
+ static size_t route(const InternalNode& node, const Key& target);
- // Recursive insert result: new node hash, subtree leaf count, optional split.
struct InsertResult {
std::string new_hash;
uint64_t leaf_count = 0;
bool did_split = false;
uint64_t split_leaf_count = 0;
- Hash32 split_min_hash = {};
+ Key split_min_key{};
std::string split_hash;
};
static InsertResult insert_into(IStorageBackend& store,
const std::string& node_hex,
- const Hash32& tuple_hash);
+ const Key& key,
+ const Hash32& payload);
- // Recursive remove result: new node hash (empty = node gone), leaf count.
struct RemoveResult {
std::string new_hash;
uint64_t leaf_count = 0;
@@ -148,17 +184,17 @@ private:
static RemoveResult remove_from(IStorageBackend& store,
const std::string& node_hex,
- const Hash32& tuple_hash);
+ const Key& key);
- // Page collection — offset consumed as we descend.
static void page_from(IStorageBackend& store,
const std::string& node_hex,
size_t& offset,
size_t limit,
- std::vector<Hash32>& out);
+ std::vector<Entry>& out);
- // Find which child to route into (rightmost whose min_hash ≤ target).
- static size_t route(const InternalNode& node, const Hash32& target);
+ static std::optional<Hash32> get_from(IStorageBackend& store,
+ const std::string& node_hex,
+ const Key& key);
};
} // namespace nt