/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use std::collections::HashMap;
use darkfi::{blockchain::HeaderHash, Error, Result};
use darkfi_sdk::{
crypto::{
pasta_prelude::PrimeField,
smt::{PoseidonFp, SparseMerkleTree, StorageAdapter, SMT_FP_DEPTH},
MerkleTree, SecretKey,
},
error::{ContractError, ContractResult},
pasta::pallas,
};
use darkfi_serial::{deserialize, serialize};
use num_bigint::BigUint;
use sled_overlay::{sled, SledDbOverlay, SledDbOverlayStateDiff};
use tracing::error;
pub const SLED_SCANNED_BLOCKS_TREE: &[u8] = b"_scanned_blocks";
pub const SLED_STATE_INVERSE_DIFF_TREE: &[u8] = b"_state_inverse_diff";
pub const SLED_MERKLE_TREES_TREE: &[u8] = b"_merkle_trees";
pub const SLED_MONEY_SMT_TREE: &[u8] = b"_money_smt";
/// Structure holding all sled trees that define the blockchain cache.
#[derive(Clone)]
pub struct Cache {
/// Main pointer to the sled db connection
pub sled_db: sled::Db,
/// The `sled` tree storing the scanned blocks from the blockchain,
/// where the key is the height number, and the value is the blocks'
/// hash.
pub scanned_blocks: sled::Tree,
/// The `sled` tree storing each blocks' full database state inverse
/// changes, where the key is the block height number, and the value
/// is the serialized database inverse diff.
pub state_inverse_diff: sled::Tree,
/// The `sled` tree storing the merkle trees of the blockchain,
/// where the key is the tree name, and the value is the serialized
/// merkle tree itself.
pub merkle_trees: sled::Tree,
/// The `sled` tree storing the Sparse Merkle Tree of the Money
/// contract.
// TODO: this could be a map of trees so more contracts can open
// SMTs if needed
pub money_smt: sled::Tree,
// TODO: Perhaps we should also move transactions history here
}
impl Cache {
/// Instantiate a new `Cache` with the given `sled` database.
pub fn new(db: &sled::Db) -> Result {
let scanned_blocks = db.open_tree(SLED_SCANNED_BLOCKS_TREE)?;
let state_inverse_diff = db.open_tree(SLED_STATE_INVERSE_DIFF_TREE)?;
let merkle_trees = db.open_tree(SLED_MERKLE_TREES_TREE)?;
let money_smt = db.open_tree(SLED_MONEY_SMT_TREE)?;
Ok(Self {
sled_db: db.clone(),
scanned_blocks,
state_inverse_diff,
merkle_trees,
money_smt,
})
}
/// Execute an atomic sled batch corresponding to inserts to the
/// merkle trees tree. For each record, the bytes slice is used as
/// the key, and the serialized merkle tree is used as value.
pub fn insert_merkle_trees(&self, trees: &[(&[u8], &MerkleTree)]) -> Result<()> {
let mut batch = sled::Batch::default();
for (key, tree) in trees {
batch.insert(*key, serialize(*tree));
}
self.merkle_trees.apply_batch(batch)?;
Ok(())
}
/// Insert a `u32` and a block inverse diff into store's inverse
/// diffs tree. The block height is used as the key, and the
/// serialized database inverse diff is used as value.
pub fn insert_state_inverse_diff(
&self,
height: &u32,
diff: &SledDbOverlayStateDiff,
) -> Result<()> {
self.state_inverse_diff.insert(height.to_be_bytes(), serialize(diff))?;
Ok(())
}
/// Fetch given block height number from the store's state inverse
/// diffs tree. The function will fail if the block height number
/// was not found.
pub fn get_state_inverse_diff(&self, height: &u32) -> Result {
match self.state_inverse_diff.get(height.to_be_bytes())? {
Some(found) => Ok(deserialize(&found)?),
None => Err(Error::BlockStateInverseDiffNotFound(*height)),
}
}
}
/// Overlay structure over a [`Cache`] instance.
pub struct CacheOverlay(pub SledDbOverlay);
impl CacheOverlay {
/// Instantiate a new `CacheOverlay` over the given [`Cache`] instance.
pub fn new(cache: &Cache) -> Result {
// Here we configure all our cache sled trees to be protected in the overlay
let protected_trees = vec![
SLED_SCANNED_BLOCKS_TREE,
SLED_STATE_INVERSE_DIFF_TREE,
SLED_MERKLE_TREES_TREE,
SLED_MONEY_SMT_TREE,
];
let mut overlay = SledDbOverlay::new(&cache.sled_db, protected_trees);
// Open all our cache sled trees in the overlay
overlay.open_tree(SLED_SCANNED_BLOCKS_TREE, true)?;
overlay.open_tree(SLED_STATE_INVERSE_DIFF_TREE, true)?;
overlay.open_tree(SLED_MERKLE_TREES_TREE, true)?;
overlay.open_tree(SLED_MONEY_SMT_TREE, true)?;
Ok(Self(overlay))
}
/// Insert a `u32`, a block hash and an optional signing key into
/// overlay's scanned blocks tree. The block height is used as the
/// key, while the serialized blockhash and key strings are used as
/// the value.
pub fn insert_scanned_block(
&mut self,
height: &u32,
hash: &HeaderHash,
signing_key: &Option,
) -> Result<()> {
let block_signing_key = match signing_key {
Some(key) => key.to_string(),
None => String::from("-"),
};
self.0.insert(
SLED_SCANNED_BLOCKS_TREE,
&height.to_be_bytes(),
&serialize(&(hash.to_string(), block_signing_key)),
)?;
Ok(())
}
}
pub type CacheSmt = SparseMerkleTree<
'static,
SMT_FP_DEPTH,
{ SMT_FP_DEPTH + 1 },
pallas::Base,
PoseidonFp,
CacheSmtStorage,
>;
pub struct CacheSmtStorage {
pub overlay: CacheOverlay,
tree: Vec,
}
impl CacheSmtStorage {
pub fn new(overlay: CacheOverlay, tree: &[u8]) -> Self {
Self { overlay, tree: tree.to_vec() }
}
pub fn snapshot(&self) -> Result> {
let mut smt = HashMap::new();
for record in self.overlay.0.iter(&self.tree)? {
let (key, value) = record?;
let mut repr = [0; 32];
repr.copy_from_slice(&value);
let Some(value) = pallas::Base::from_repr(repr).into() else {
return Err(Error::ParseFailed(
"[cache::CacheSmtStorage::snapshot] Value conversion failed",
))
};
smt.insert(BigUint::from_bytes_le(&key), value);
}
Ok(smt)
}
}
impl StorageAdapter for CacheSmtStorage {
type Value = pallas::Base;
fn put(&mut self, key: BigUint, value: pallas::Base) -> ContractResult {
if let Err(e) = self.overlay.0.insert(&self.tree, &key.to_bytes_le(), &value.to_repr()) {
error!(target: "cache::StorageAdapter::put", "Inserting key {key:?}, value {value:?} into DB failed: {e}");
return Err(ContractError::SmtPutFailed)
}
Ok(())
}
fn get(&self, key: &BigUint) -> Option {
let value = match self.overlay.0.get(&self.tree, &key.to_bytes_le()) {
Ok(v) => v,
Err(e) => {
error!(target: "cache::StorageAdapter::get", "Fetching key {key:?} from DB failed: {e}");
return None
}
};
let value = value?;
let mut repr = [0; 32];
repr.copy_from_slice(&value);
pallas::Base::from_repr(repr).into()
}
fn del(&mut self, key: &BigUint) -> ContractResult {
if let Err(e) = self.overlay.0.remove(&self.tree, &key.to_bytes_le()) {
error!(target: "cache::StorageAdapter::del", "Removing key {key:?} from DB failed: {e}");
return Err(ContractError::SmtDelFailed)
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use darkfi::{zk::halo2::Field, Result};
use darkfi_sdk::{
crypto::smt::{gen_empty_nodes, util::FieldHasher, PoseidonFp, SparseMerkleTree},
pasta::pallas,
};
use rand::rngs::OsRng;
use sled_overlay::sled;
use crate::cache::{Cache, CacheOverlay, CacheSmtStorage, SLED_MONEY_SMT_TREE};
#[test]
fn test_cache_smt() -> Result<()> {
// Setup cache and its overlay
let sled_db = sled::Config::new().temporary(true).open()?;
let cache = Cache::new(&sled_db)?;
let overlay = CacheOverlay::new(&cache)?;
// Setup SMT
const HEIGHT: usize = 3;
let hasher = PoseidonFp::new();
let empty_leaf = pallas::Base::ZERO;
let empty_nodes = gen_empty_nodes::<{ HEIGHT + 1 }, _, _>(&hasher, empty_leaf);
let store = CacheSmtStorage::new(overlay, SLED_MONEY_SMT_TREE);
let mut smt = SparseMerkleTree::::new(
store,
hasher.clone(),
&empty_nodes,
);
// Verify database is empty
assert!(cache.money_smt.is_empty());
let leaves = vec![
(pallas::Base::from(1), pallas::Base::random(&mut OsRng)),
(pallas::Base::from(2), pallas::Base::random(&mut OsRng)),
(pallas::Base::from(3), pallas::Base::random(&mut OsRng)),
];
smt.insert_batch(leaves.clone()).unwrap();
let hash1 = leaves[0].1;
let hash2 = leaves[1].1;
let hash3 = leaves[2].1;
let hash = |l, r| hasher.hash([l, r]);
let hash01 = hash(empty_nodes[3], hash1);
let hash23 = hash(hash2, hash3);
let hash0123 = hash(hash01, hash23);
let root = hash(hash0123, empty_nodes[1]);
assert_eq!(root, smt.root());
// Now try to construct a membership proof for leaf 3
let pos = leaves[2].0;
let path = smt.prove_membership(&pos);
assert_eq!(path.path[0], empty_nodes[1]);
assert_eq!(path.path[1], hash01);
assert_eq!(path.path[2], hash2);
assert_eq!(hash23, hash(path.path[2], hash3));
assert_eq!(hash0123, hash(path.path[1], hash(path.path[2], hash3)));
assert_eq!(root, hash(hash(path.path[1], hash(path.path[2], hash3)), path.path[0]));
assert!(path.verify(&root, &hash3, &pos));
// Grab the overlay diff
let diff = smt.store.overlay.0.diff(&[])?;
// Apply the overlay
smt.store.overlay.0.apply_diff(&diff)?;
// Verify database contains keys
assert!(!cache.money_smt.is_empty());
// We are now going to rollback the changes
smt.store.overlay.0.apply_diff(&diff.inverse())?;
// Verify database is empty again
assert!(cache.money_smt.is_empty());
Ok(())
}
}