Skip to main content

Crate hashtree_core

Crate hashtree_core 

Source
Expand description

§hashtree-core

Encrypted, content-addressed files and directories over an async Store. The core handles hashing, chunking, encryption, and immutable tree edits; storage, transport, and mutable-name discovery are separate crates.

Published API · Crate · Protocol

§Install and store a file

In a Rust application:

cargo add hashtree-core
cargo add tokio --features macros,rt-multi-thread

Save this as src/main.rs and run cargo run. No daemon, server, or Nostr identity is needed. The examples use Tokio; core streaming uses futures traits.

use hashtree_core::{
    nhash_decode, nhash_encode_full, Cid, HashTree, HashTreeConfig, MemoryStore, NHashData,
};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tree = HashTree::new(HashTreeConfig::new(Arc::new(MemoryStore::new())));
    let (cid, size) = tree.put(b"Hello, World!").await?;
    assert_eq!(size, 13); // Plaintext byte count, separate from the CID.

    // Persist the complete read capability, including the encryption key.
    let identifier = nhash_encode_full(&NHashData {
        hash: cid.hash,
        decrypt_key: cid.key,
    })?;
    let decoded = nhash_decode(&identifier)?;
    let restored = Cid { hash: decoded.hash, key: decoded.decrypt_key };
    let bytes = tree.get(&restored, Some(1024)).await?.ok_or("File is unavailable")?;
    assert_eq!(bytes, b"Hello, World!");
    println!("{}", String::from_utf8(bytes)?);
    Ok(())
}

MemoryStore lasts only for this process. The identifier does not contain the blocks or tell a reader where to fetch them. Use persistent storage and save the root identifier separately to reopen data later.

§Files, keys, and roots

  • put() returns (Cid, plaintext_size). A Cid contains a SHA-256 hash of the stored bytes and an optional 32-byte decryption key.
  • Keep both hash and key. nhash_encode_full() above preserves both; nhash_encode(&hash) preserves only the hash. For local metadata, cid.to_string() / Cid::parse() also round-trip both as hash:key hex.
  • Sharing a keyed CID grants read access to anyone who can retrieve the blocks. CHK encryption deduplicates equal content, revealing equality and allowing guesses of predictable content. It is not randomized encryption.
  • HashTreeConfig::new(store).public() selects plaintext writes. Removing the key from an encrypted CID does not decrypt its data.
  • Edits return new roots. Save the returned CID; old roots remain unchanged. Directory encryption is independent of child encryption, and a plaintext directory may expose child keys stored in its entries.

§Directories and immutable edits

Use DirEntry::from_cid() to retain the child’s key, set its plaintext size, and set LinkType::File for logical files or LinkType::Dir for directories.

use hashtree_core::{DirEntry, HashTree, HashTreeConfig, LinkType, MemoryStore};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tree = HashTree::new(HashTreeConfig::new(Arc::new(MemoryStore::new())));
    let (file, size) = tree.put(b"Hello").await?;
    let original = tree.put_directory(vec![
        DirEntry::from_cid("hello.txt", &file).with_size(size).with_link_type(LinkType::File),
    ]).await?;
    let found = tree.resolve_path(&original, "hello.txt").await?.ok_or("Missing entry")?;
    assert_eq!(tree.get(&found, Some(1024)).await?, Some(b"Hello".to_vec()));

    let (note, size) = tree.put(b"A new note").await?;
    let updated = tree.set_entry(&original, &[], "note.txt", &note, size, LinkType::File).await?;
    assert_eq!(tree.list_directory_required(&original).await?.len(), 1);
    assert_eq!(tree.list_directory_required(&updated).await?.len(), 2);
    Ok(())
}

The empty path &[] edits the root directory; &["notes", "2026"] targets a nested directory. remove_entry() and rename_entry() also return new roots. These operations do not merge concurrent writers automatically.

§Streaming and bounded reads

Add futures with cargo add futures for this example. The small chunk size makes chunking visible; the default is 2 MiB.

use futures::{io::Cursor, TryStreamExt};
use hashtree_core::{HashTree, HashTreeConfig, MemoryStore};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = HashTreeConfig::new(Arc::new(MemoryStore::new())).with_chunk_size(4);
    let tree = HashTree::new(config);
    let (cid, size) = tree.put_stream(Cursor::new(b"Hello, streaming!".to_vec())).await?;
    let mut stream = tree.get_stream(&cid);
    let mut received = 0;
    while let Some(chunk) = stream.try_next().await? {
        received += chunk.len() as u64; // Process or write each chunk here.
    }
    assert_eq!(received, size);
    assert_eq!(tree.read_file_range_cid(&cid, 7, Some(16)).await?, Some(b"streaming".to_vec()));
    Ok(())
}

put_stream() accepts futures::io::AsyncRead, not Tokio’s identically named trait; adapt a Tokio reader using tokio_util::compat with its compat feature. get(cid, Some(max_bytes)) limits assembled plaintext. For encrypted ranges, use read_file_range_cid(): start is inclusive, end is exclusive. The raw Store::get_range() instead uses an inclusive end on stored bytes.

§Storage and other crates

NeedCrate / entry point
Persistent local blockshashtree-lmdb: write/reopen example and shared-store policy
Remote blob uploads/downloadshashtree-blossom: signed HTTP client
Adaptive network readshashtree-network: route selection and verification
Records and derived indexeshashtree-collection
Mutable Nostr root nameshashtree-resolver

A custom Store implements async put, get, has, and delete and must be Send + Sync; other trait methods have defaults. It stores raw addressed bytes, not logical plaintext files. Preserve immutable hash-to-bytes mappings and verify untrusted bytes before returning/caching them. put() returning false means already present, not an upload failure. Flush buffered stores before publishing roots, and apply the backend’s durability policy. Local writes do not imply remote uploads; replicate all referenced blocks before announcing a root.

§Handle unavailable data

  • get() returns Ok(None) for a missing root, and may return Err for missing descendants, invalid encoding, decryption, storage, or size-limit failures.
  • list_directory_required() rejects unavailable directory blocks. list_directory() and list() can return empty results for unavailable roots; do not use those empty results as proof of an empty remote directory.
  • resolve_path() returns Ok(None) for an absent name/non-directory path and reports missing directory blocks as errors.
  • get_stream() can end without chunks when the root is unavailable. Check the expected plaintext length when completeness matters; zero chunks alone do not establish whether the file exists.
  • Keep transport failures separate from misses. Apply deadlines at the app or transport layer, and keep mutable-root subscriptions open until explicitly stopped.

§API and verification

The online API follows published crate versions. Select the version matching Cargo.lock. For the checkout’s API, run these commands from rust/:

cargo doc --locked -p hashtree-core --no-deps --open
cargo test --locked -p hashtree-core --doc

This README is also the crate’s generated documentation. Cargo compiles and runs its Rust examples as doctests, including in the full release/CI gate. Use the public HashTree, Cid, DirEntry, and Store APIs for app code; low-level codec/builder helpers are for protocol and storage integrations.

Re-exports§

pub use blob_route::decode_blob_reply_header;
pub use blob_route::decode_blob_request;
pub use blob_route::encode_blob_reply_header;
pub use blob_route::encode_blob_request;
pub use blob_route::BlobCodecError;
pub use blob_route::BlobReply;
pub use blob_route::BlobReplyHeader;
pub use blob_route::BlobRequest;
pub use blob_route::BlobRoute;
pub use blob_route::BlobRouteContext;
pub use blob_route::StoreBlobRoute;
pub use blob_route::BLOB_DEFAULT_HTL;
pub use blob_route::BLOB_MAX_BYTES;
pub use blob_route::BLOB_MAX_HTL;
pub use blob_route::BLOB_REPLY_HEADER_BYTES;
pub use blob_route::BLOB_REQUEST_BYTES;
pub use hashtree::decode_tree_node_by_cid;
pub use hashtree::verify_tree as hashtree_verify_tree;
pub use hashtree::HashTree;
pub use hashtree::HashTreeConfig;
pub use hashtree::HashTreeError;
pub use builder::BEP52_CHUNK_SIZE;
pub use builder::DEFAULT_CHUNK_SIZE;
pub use codec::decode_tree_node;
pub use codec::encode_and_hash;
pub use codec::encode_tree_node;
pub use codec::get_node_type;
pub use codec::is_directory_node;
pub use codec::is_tree_node;
pub use codec::try_decode_tree_node;
pub use codec::CodecError;
pub use hash::sha256;
pub use hash::verify;
pub use reader::verify_tree;
pub use reader::verify_tree_integrity;
pub use reader::ReaderError;
pub use reader::TreeEntry;
pub use reader::VerifyIntegrityResult;
pub use reader::VerifyResult;
pub use reader::WalkEntry;
pub use nhash::decode as nhash_decode_any;
pub use nhash::is_nhash;
pub use nhash::nhash_decode;
pub use nhash::nhash_encode;
pub use nhash::nhash_encode_full;
pub use nhash::DecodeResult;
pub use nhash::NHashData;
pub use nhash::NHashError;
pub use store::BufferedStore;
pub use store::MemoryStore;
pub use store::Store;
pub use store::StoreError;
pub use types::from_hex;
pub use types::hash_equals;
pub use types::to_hex;
pub use types::Cid;
pub use types::CidParseError;
pub use types::DirEntry;
pub use types::Hash;
pub use types::LinkType;
pub use types::PutResult;
pub use types::TreeNode;
pub use crypto::content_hash;
pub use crypto::could_be_encrypted;
pub use crypto::decrypt;
pub use crypto::decrypt_chk;
pub use crypto::encrypt;
pub use crypto::encrypt_chk;
pub use crypto::encrypted_size;
pub use crypto::encrypted_size_chk;
pub use crypto::generate_key;
pub use crypto::key_from_hex;
pub use crypto::key_to_hex;
pub use crypto::plaintext_size;
pub use crypto::CryptoError;
pub use crypto::EncryptionKey;
pub use visibility::xor_keys;
pub use visibility::TreeVisibility;
pub use diff::collect_hashes;
pub use diff::collect_hashes_with_progress;
pub use diff::tree_diff;
pub use diff::tree_diff_streaming;
pub use diff::tree_diff_with_old_hashes;
pub use diff::DiffStats;
pub use diff::TreeDiff;

Modules§

blob_route
One-hop blob routing values and their compact process/network wire codec.
builder
Tree builder with chunking and fanout support
codec
MessagePack encoding/decoding for tree nodes
crypto
Content Hash Key (CHK) encryption for HashTree
diff
Tree diff operations for efficient incremental updates
hash
Hashing utilities using SHA256
hashtree
HashTree - Unified merkle tree operations
nhash
Bech32-encoded identifiers for hashtree content
reader
Tree reader and traversal utilities
store
Content-addressed key-value store interfaces and implementations
types
HashTree - Simple content-addressed merkle tree
visibility
Tree visibility helpers