aboutsummaryrefslogblamecommitdiffstats
path: root/include/Merkle.h
blob: ba8018adea280d7873ab3049d34d601a4467600b (plain) (tree)
1
2
3
4
5
6
7
8






                            
                   




                 
                                                                            
  



                                                                               
  









                                                                                

                                                                                  
                                                                            

                                                                  





                                                                               

                                                                            
                                                                        



                                                                         

                                                        
  




                                                                              



              










                                                                                




                                   

                                                             
                                                                      




                                                                            

                                                     


                                                          

       


                                                                       

                                                     















                                                                  

       
                                                       


                                                                             
                                                                           
                                          
       



                                                                

        



                       

                     
                                       

      
                       


                            


                         
                                        








                                                                                   





                                                                                     




                                                                                    
 




                                         
                                    




                                                                 

                                                            
 






                                                                 
                                                     
 



                                                       
                                                    
 


                                                                      


                 
#pragma once

#include "IStorageBackend.h"

#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>

/**
 * @file Merkle.h
 * @brief Generic sorted content-addressed B-tree over a templated key type.
 *
 * 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.
 *
 * 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 / Remove produce new nodes only along the modified path; old nodes
 * remain valid for any snapshot that still holds their root 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_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.
 *   - Leaf entries are sorted ascending by key.
 *   - Internal entries are sorted ascending by min_key.
 *
 * **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 or overwrite a `(key, payload)` mapping.
     * @param store     KV backend for node serialisation.
     * @param root_hex  Current root hash (empty string = empty tree).
     * @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 Key& key,
                              const Hash32& payload);

    /**
     * @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 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 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 entries. Only the nodes on the active path are loaded;
     * sibling subtrees are never touched.
     */
    static std::vector<Entry> Page(IStorageBackend& store,
                                    const std::string& root_hex,
                                    size_t offset,
                                    size_t limit);

private:
    struct LeafEntry {
        Key    key;
        Hash32 payload;
    };

    struct LeafNode {
        std::vector<LeafEntry> entries;
    };

    struct ChildEntry {
        uint64_t leaf_count;
        Key      min_key;
        Hash32   child_hash;
    };

    struct InternalNode {
        std::vector<ChildEntry> entries;
    };

    // Wire format
    static std::vector<uint8_t> encode_leaf(const LeafNode& n);
    static std::vector<uint8_t> encode_internal(const InternalNode& n);
    static LeafNode             decode_leaf(const std::vector<uint8_t>& bytes);
    static InternalNode         decode_internal(const std::vector<uint8_t>& bytes);
    static bool                 is_leaf_bytes(const std::vector<uint8_t>& bytes);

    // 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);

    /** @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);

    struct InsertResult {
        std::string new_hash;
        uint64_t    leaf_count = 0;
        bool        did_split  = false;
        uint64_t    split_leaf_count = 0;
        Key         split_min_key{};
        std::string split_hash;
    };

    static InsertResult insert_into(IStorageBackend& store,
                                     const std::string& node_hex,
                                     const Key& key,
                                     const Hash32& payload);

    struct RemoveResult {
        std::string new_hash;
        uint64_t    leaf_count = 0;
    };

    static RemoveResult remove_from(IStorageBackend& store,
                                     const std::string& node_hex,
                                     const Key& key);

    static void page_from(IStorageBackend& store,
                           const std::string& node_hex,
                           size_t& offset,
                           size_t limit,
                           std::vector<Entry>& out);

    static std::optional<Hash32> get_from(IStorageBackend& store,
                                          const std::string& node_hex,
                                          const Key& key);
};

} // namespace nt