Introduction

Introduction

These tutorials aim to give developers a solid foundation on how to interact and build with GroveDB so they may begin using it for their own applications.

GroveDB is the first database to enable cryptographic proofs for complex queries. It was built for Dash Platform by Dash Core Group but can be easily plugged into other systems and applications as well. The design, a hierarchical grove of Merkle AVL trees interconnected via root hashes and references, is largely inspired by the paper Database Outsourcing with Hierarchical Authenticated Data Structures.

Setup

Setup

Clone the GroveDB repo.

git clone https://github.com/dashpay/grovedb.git

Open and build the tutorial code:


cd grovedb/tutorials
cargo build
                    

To run a tutorial, do:

cargo run --bin <tutorial name>

Valid tutorial names are: open, insert, delete, query-simple, query-complex, proofs.

Open

Open

GroveDB uses the open() function to open an existing instance of GroveDB at the given path in your filesystem. If there is no existing instance, a new one will be created with an empty root tree. The only argument for open() is the filesystem path.

The following code can be run with cargo run --bin open


use grovedb::GroveDB
    fn main() {
    // Specify the path where you want to set up the GroveDB instance
    let path = String::from("../tutorial-storage");
                              
    // Open a new GroveDB at the path
    GroveDb::open(&path).unwrap();
                            
    // Print to the terminal
    println!("Opened {:?}", path);
}
                   

A folder called “storage” should have appeared in your grovedb-tutorials directory, populated with some files. GroveDB is set up and ready for us to insert data.

Insert

Insert

GroveDB uses the insert() function to insert items into storage. By default, inserting overwrites values unless the value is a tree. The function takes five arguments as shown below.


/// Insert Element into GroveDB
pub fn insert<'p, P>(
   &self,
   // Tree path to the subtree where the key-value should be inserted
   path: P,
   // The key
   key: &'p [u8],
   // The value
   element: Element,
   // Insert options
   options: Option<InsertOptions>,
   // Transaction that the insert operation should be included in
   transaction: TransactionArg,
) -> CostResult<(), Error>
                    

This tutorial inserts two key-values (KVs) into the root tree of the GroveDB instance created in Open Tutorial (or a new instance if you skipped the open tutorial). The keys and values of both KVs are strings. Then, for the purpose of showing they are there, it uses the get() function to retrieve the values and print them to the terminal.

The following code can be run with cargo run --bin insert.


use grovedb::Element;
use grovedb::GroveDb;

fn main() {
    // Specify a path and open GroveDB at the path as db
    let path = String::from("../tutorial-storage");
    let db = GroveDb::open(path).unwrap();

    // Define key-values for insertion
    let key1 = b"hello";
    let val1 = b"world";
    let key2 = b"grovedb";
    let val2 = b"rocks";

    // Insert key-value 1 into the root tree
    db.insert([], key1, Element::Item(val1.to_vec(), None), None, None)
        .unwrap()
        .expect("successful key1 insert");

    // Insert key-value 2 into the root tree
    db.insert([], key2, Element::Item(val2.to_vec(), None), None, None)
        .unwrap()
        .expect("successful key2 insert");

    // At this point the Items are fully inserted into the database.
    // No other steps are required.

    // To show that the Items are there, we will use the get()
    // function to get them from the RocksDB backing store.

    // Get value 1
    let result1 = db.get([], key1, None).unwrap();

    // Get value 2
    let result2 = db.get([], key2, None).unwrap();

    // Print the values to terminal
    println!("{:?}", result1);
    println!("{:?}", result2);
}
                    

The terminal should output:


Ok(item: [hex: 776f726c64, str: world])
Ok(item: [hex: 726f636b73, str: rocks])
                    

This tells us that at the given keys exist items with the shown hex and str values.

Element Enum

Element Enum

In the previous tutorial, we inserted strings as the values. However, values may be any of the five variants of the Element enum:


pub enum Element {
    /// An ordinary value
    Item(Vec<u8>, Option<ElementFlags>),
    /// A reference to an object by its path
    Reference(ReferencePathType, MaxReferenceHop, Option<ElementFlags>),
    /// A subtree, contains a prefixed key representing the root of the
    /// subtree.
    Tree(Option<Vec<u8>>, Option<ElementFlags>),
    /// Signed integer value that can be totaled in a sum tree
    SumItem(SumValue, Option<ElementFlags>),
    /// Same as Element::Tree but underlying Merk sums values of it's summable
    /// nodes
    SumTree(Option<Vec<u8>>, SumValue, Option<ElementFlags>),
}                            
                    

For example, instead of using let val1 = b"world", we could have put let val1 = Element::Tree(None, None) and then put val1 in the insert function instead of Element::Item(val1.to_vec(), None).

In that case, the output would have returned:


Ok(tree: None)
Ok(item: [hex: 726f636b73, str: rocks])
                    

Element Functions

Element Functions

Instead of passing Element::Item() or some other Element enum to the insert function, we could also pass one of the functions implemented in Element, new_item(). Given an item value, the new_item function returns Element::Item(item_value, None). It’s useful because it allows developers to set a default value for the flags. It looks like this:


impl Element {
    /// Set element to an item with a default flag value
    pub fn new_item(item_value: Vec<u8>) -> Self {
        Element::Item(item_value, None)
    }
}                            
                    

And would be used like so:


// Insert key-value 1 into the root tree
db.insert([], key1, Element::new_item(val1.to_vec()), None, None)
   .unwrap()
   .expect("successful root tree leaf insert");
                 

In this case the default flags are None.

There are a few other functions implemented in Element that can be used similarly. The Element::empty_tree() function is quite useful as well.

Delete

Delete

GroveDB uses the delete() function to delete key-values from storage. It takes four arguments as shown below.


// Delete Element from GroveDB
pub fn delete<'p, P>(
    &self,
    // Tree path to where the key is located ([] for root)
    path: P,
    // Key to be deleted
    key: &'p [u8],
    // Delete options
    options: Option<DeleteOptions>,
    // Transaction that the delete operation should be included in
    transaction: TransactionArg,
) -> CostResult<(), Error>
                    

This tutorial inserts two key-values in the same way as the Insert Tutorial, uses get() to check if the values are there, deletes them, and checks if they're there again.

The following code can be run with cargo run --bin delete.


use grovedb::GroveDb;
use grovedb::Element;

fn main() {
    // Specify a path and open GroveDB at the path as db
    let path = String::from("../tutorial-storage");
    let db = GroveDb::open(path).unwrap();

    // Define key-values for insertion
    let key1 = b"hello";
    let val1 = b"world";
    let key2 = b"grovedb";
    let val2 = b"rocks";

    // Insert key-value 1 into the root tree
    db.insert([], key1, Element::Item(val1.to_vec(), None), None, None)
        .unwrap()
        .expect("successful key1 insert");

    // Insert key-value 2 into the root tree
    db.insert([], key2, Element::Item(val2.to_vec(), None), None, None)
        .unwrap()
        .expect("successful key2 insert");

    // Check the key-values are there
    let result1 = db.get([], key1, None).unwrap();
    let result2 = db.get([], key2, None).unwrap();
    println!("Before deleting, we have key1: {:?}", result1);
    println!("Before deleting, we have key2: {:?}", result2);

    // Delete the values
    db.delete([], key1, None, None)
        .unwrap()
        .expect("successfully deleted key1");
    db.delete([], key2, None, None)
        .unwrap()
        .expect("successfully deleted key2");

    // Check the key-values again
    let result3 = db.get([], key1, None).unwrap();
    let result4 = db.get([], key2, None).unwrap();
    println!("After deleting, we have key1: {:?}", result3);
    println!("After deleting, we have key2: {:?}", result4);
}
                    

The terminal should output:


Before deleting, we have key1 Ok(item: [hex: 776f726c64, str: world])
Before deleting, we have key2 Ok(item: [hex: 726f636b73, str: rocks])
After deleting, we have key1 Err(PathKeyNotFound("key not found in Merk for get: 68656c6c6f"))
After deleting, we have key2 Err(PathKeyNotFound("key not found in Merk for get: 67726f76656462"))
                    

Query

Query

GroveDB generally uses the query() function to perform queries on storage. It takes four arguments as shown below


impl GroveDb {
    /// Returns given path query results
    pub fn query(
        &self,
        path_query: &PathQuery,
        allow_cache: bool,
        result_type: QueryResultType,
        transaction: TransactionArg,
    ) -> CostResult<(QueryResultElements, u16), Error>
}                            
                    

Explanations

Explanations

GroveDB queries can be very complex. This section gives explanations of each component of a path query. Deep understanding of each component isn’t necessary to follow and learn from the tutorials, so, if you’d rather learn by doing, feel free to skip it.

A path query, which is the first argument of query(), has a path and a sized query as parameters. The path points to the highest-level subtree you want to query. You can traverse and recurse into lower-level subtrees within a path query using subqueries, as will be explained later.


/// Path query
pub struct PathQuery {
    /// Path
    pub path: Vec<Vec<u8>>,
    /// Query
    pub query: SizedQuery,
}
                    

A sized query, which is the second argument of path queries, takes a query, limit, and offset as parameters. The limit is an integer which specifies the maximum number of results to return in the final result set. The offset is an integer which specifies how many items at the beginning of the raw result set to exclude from the final result set. In other words, the limit specifies a cut-off at the end of the raw result set, and offset specifies a cut-off at the beginning of the raw result set which, after applied, will compose the final result set. As implied, there is an ordering of the raw (and final) result set, which is defined in the other, first, parameter of a sized query: query.


/// Sized query
pub struct SizedQuery {
    /// Query
    pub query: Query,
    /// Limit
    pub limit: Option<u16>,
    /// Offset
    pub offset: Option<u16>,
}
                    

A query takes a vector of query items as its first argument. A query item is either a key, set of keys, or a range of keys. Items in these keys or ranges of keys will be added to the raw result set. Subtrees in these keys or ranges of keys are optionally handled with the next two parameters: the default subquery branch and the conditional subquery branch.


/// A `Query` represents one or more keys or ranges of keys, which can be 
/// used to resolve a proof which will include all of the requested values.
pub struct Query {
    /// Items
    pub items: Vec<QueryItem>,
    /// Default subquery branch
    pub default_subquery_branch: SubqueryBranch,
    /// Conditional subquery branches
    pub conditional_subquery_branches: Option<IndexMap<QueryItem, SubqueryBranch>>,
    /// Left to right?
    pub left_to_right: bool,
}

/// A `QueryItem` represents a key or range of keys to be included in a proof.
pub enum QueryItem {
    Key(Vec<u8>),
    Range(Range<Vec<u8>>),
    RangeInclusive(RangeInclusive<Vec<u8>>),
    RangeFull(RangeFull),
    RangeFrom(RangeFrom<Vec<u8>>),
    RangeTo(RangeTo<Vec<u8>>),
    RangeToInclusive(RangeToInclusive<Vec<u8>>),
    RangeAfter(RangeFrom<Vec<u8>>),
    RangeAfterTo(Range<Vec<u8>>),
    RangeAfterToInclusive(RangeInclusive<Vec<u8>>),
}
                    

A default subquery branch has two parameters: a subquery path and a subquery. The subquery path is a path that is applied to all the subtrees in the result set from the higher-level query we just mentioned. The subquery is a query of the same type as the higher-level query which is applied to the subtrees at the end of the subquery path. The result set of the subquery branch is added to the overall result set. Since subqueries are the same as queries, they can recurse, so you can have subqueries within subqueries. Both subquery path and subquery are optional parameters of a subquery branch. If no subquery path is defined, the subquery applies to all the subtrees in the higher-level query result set. If no subquery is defined, all the elements from the subquery path subtrees are added to the result set.


/// Subquery branch
pub struct SubqueryBranch {
    /// Subquery path
    pub subquery_path: Option<Path>,
    /// Subquery
    pub subquery: Option<Box<Query>>,
}
                    

A conditional subquery branch is the same as default subquery branches, but takes an additional argument for query items, which again are keys or ranges of keys. The subquery branch is only applied to the subtrees which meet the condition of matching the query items.

Finally, the last parameter of query is left_to_right, which is a boolean that defines the order of the result set. Left to right means lower to higher in terms of integers, or alphabetically in terms of strings.

See the documentation for more details.

Get

Get

The previous tutorials used a function get() to retrieve items from storage. Getting only allows for the retrieval of one item, the key must be specified, and cryptographic proofs of gets aren’t supported. Queries, on the other hand, can return many values all at once, the keys don’t need to be provided, and query results can be cryptographically proven.


/// Get an element from the backing store
pub fn get<'p, P>(
    &self,
    path: P,
    key: &'p [u8],
    transaction: TransactionArg,
) -> CostResult<Element, Error>
                    

Simple Query

Simple Query

This tutorial populates GroveDB with values 0-99 in a subtree within a subtree within the root tree:


/// Root
///    SUBTREE1
///       SUBTREE2
///          Values 0-99
                    

It then constructs and executes a query to retrieve a subset of the items and prints the query result to the terminal.

The following code can be run with cargo run --bin query-simple.


use grovedb::operations::insert::InsertOptions;
use grovedb::Element;
use grovedb::GroveDb;
use grovedb::{PathQuery, Query};

const KEY1: &[u8] = b"key1";
const KEY2: &[u8] = b"key2";

// Allow insertions to overwrite trees
// This is necessary so the tutorial can be rerun easily
const INSERT_OPTIONS: Option<InsertOptions> = Some(InsertOptions {
    validate_insertion_does_not_override: false,
    validate_insertion_does_not_override_tree: false,
    base_root_storage_is_free: true,
});

fn main() {
    // Specify the path where the GroveDB instance exists.
    let path = String::from("../tutorial-storage");

    // Open GroveDB at the path.
    let db = GroveDb::open(path).unwrap();

    // Populate GroveDB with values. This function is defined below.
    populate(&db);

    // Define the path to the subtree we want to query.
    let path = vec![KEY1.to_vec(), KEY2.to_vec()];

    // Instantiate a new query.
    let mut query = Query::new();

    // Insert a range of keys to the query that we would like returned.
    // In this case, we are asking for keys 30 through 34.
    query.insert_range(30_u8.to_be_bytes().to_vec()..35_u8.to_be_bytes().to_vec());

    // Put the query into a new unsized path query.
    let path_query = PathQuery::new_unsized(path, query.clone());

    // Execute the query and collect the result items in "elements".
    let (elements, _) = db
        .query_item_value(&path_query, true, None)
        .unwrap()
        .expect("expected successful get_path_query");

    // Print result items to terminal.
    println!("{:?}", elements);
}

fn populate(db: &GroveDb) {
    // Put an empty subtree into the root tree nodes at KEY1.
    // Call this SUBTREE1.
    db.insert([], KEY1, Element::empty_tree(), INSERT_OPTIONS, None)
        .unwrap()
        .expect("successful SUBTREE1 insert");

    // Put an empty subtree into subtree1 at KEY2.
    // Call this SUBTREE2.
    db.insert([KEY1], KEY2, Element::empty_tree(), INSERT_OPTIONS, None)
        .unwrap()
        .expect("successful SUBTREE2 insert");

    // Populate SUBTREE2 with values 0 through 99 under keys 0 through 99.
    for i in 0u8..100 {
        let i_vec = (i as u8).to_be_bytes().to_vec();
        db.insert(
            [KEY1, KEY2],
            &i_vec,
            Element::new_item(i_vec.clone()),
            INSERT_OPTIONS,
            None,
        )
        .unwrap()
        .expect("successfully inserted values");
    }
}
                    

The terminal should output:


[[30], [31], [32], [33], [34]]
                    

Complex Query

Complex Query

This tutorial populates GroveDB with the following tree structure:


/// Root
///   SUBTREE1
///      SUBTREE2
///         Values 0-49 except random number 1
///         SUBTREE3 at random number 1
///            Values 50-74 except random number 2
///            SUBTREE4 at random number 2
///               SUBTREE5
///                  Values 75-99
                    

It then queries SUBTREE2 for keys 20-30. Call this QUERY1.

It applies a conditional subquery to QUERY1, which says if any keys 20-25 are subtrees, navigate the subquery path and execute the subquery. Call this SUBQUERY1. No path is specified for SUBQUERY1, so the subquery is applied directly to the subtrees. In this case, that is just SUBTREE3. The subquery asks for keys 60 and 70 from SUBTREE3.

Say SUBTREE3 is located at key 22. The result set now looks like this:


[20,21,60,70,23,24,25,26,27,28,29,30]
                  

It then applies a conditional subquery to SUBQUERY1, which says if key 60 is a subtree, navigate the subquery path and execute the subquery. Call this SUBQUERY2. SUBQUERY2 specifies to navigate through SUBTREE4 and execute the subquery on SUBTREE5. The subquery asks for keys 90 through 94 from SUBTREE5.

Say SUBTREE4 is located at key 60. The result set now looks like this:


[20,21,90,91,92,93,94,70,23,24,25,26,27,28,29,30]
                  

QUERY1, SUBQUERY1, and SUBQUERY2 are then all put into a sized query, which sets a limit of 10 and an offset of 3. Limit of 10 means no more than 10 items can be included in the results. Offset of 3 means the first 3 pre-sized query results are omitted from the post-sized query results.

The final result set is:


[91,92,93,94,70,23,24,25,26,27]
                  

The sized query is then passed to the path query and the path query is executed.

The following code can be run with cargo run --bin query-complex.


use grovedb::operations::insert::InsertOptions;
use grovedb::Element;
use grovedb::GroveDb;
use grovedb::{PathQuery, Query, QueryItem, SizedQuery};
use rand::Rng;

const KEY1: &[u8] = b"key1";
const KEY2: &[u8] = b"key2";
const KEY3: &[u8] = b"key3";

// Allow insertions to overwrite trees
// This is necessary so the tutorial can be rerun easily
const INSERT_OPTIONS: Option<InsertOptions> = Some(InsertOptions {
    validate_insertion_does_not_override: false,
    validate_insertion_does_not_override_tree: false,
    base_root_storage_is_free: true,
});

fn main() {
    // Specify the path where the GroveDB instance exists.
    let path = String::from("../tutorial-storage");

    // Open GroveDB at the path.
    let db = GroveDb::open(path).unwrap();

    // Populate GroveDB with values. This function is defined below.
    populate(&db);

    // Define the path to the highest-level subtree we want to query.
    let path = vec![KEY1.to_vec(), KEY2.to_vec()];

    // Instantiate new queries.
    let mut query = Query::new();
    let mut subquery = Query::new();
    let mut subquery2 = Query::new();

    // Insert query items into the queries.
    // Query 20-30 at path.
    query.insert_range(20_u8.to_be_bytes().to_vec()..31_u8.to_be_bytes().to_vec());
    // If any 20-30 are subtrees and meet the subquery condition,
    // follow the path and query 60, 70 from there.
    subquery.insert_keys(vec![vec![60], vec![70]]);
    // If either 60, 70 are subtrees and meet the subquery condition,
    // follow the path and query 90-94 from there.
    subquery2.insert_range(90_u8.to_be_bytes().to_vec()..95_u8.to_be_bytes().to_vec());

    // Add subquery branches.
    // If 60 is a subtree, navigate through SUBTREE4 and run subquery2 on SUBTREE5.
    subquery.add_conditional_subquery(QueryItem::Key(vec![60]), Some(vec!(KEY3.to_vec())), Some(subquery2));
    // If anything up to and including 25 is a subtree, run subquery on it. No path.
    query.add_conditional_subquery(
        QueryItem::RangeToInclusive(std::ops::RangeToInclusive { end: vec![25] }),
        None,
        Some(subquery),
    );

    // Put the query into a sized query. Limit the result set to 10,
    // and impose an offset of 3.
    let sized_query = SizedQuery::new(query, Some(10), Some(3));

    // Put the sized query into a new path query.
    let path_query = PathQuery::new(path, sized_query.clone());

    // Execute the path query and collect the result items in "elements".
    let (elements, _) = db
        .query_item_value(&path_query, true, None)
        .unwrap()
        .expect("expected successful get_path_query");

    // Print result items to terminal.
    println!("{:?}", elements);
}

fn populate(db: &GroveDb) {
    // Put an empty subtree into the root tree nodes at KEY1.
    // Call this SUBTREE1.
    db.insert([], KEY1, Element::empty_tree(), INSERT_OPTIONS, None)
        .unwrap()
        .expect("successful SUBTREE1 insert");

    // Put an empty subtree into subtree1 at KEY2.
    // Call this SUBTREE2.
    db.insert([KEY1], KEY2, Element::empty_tree(), INSERT_OPTIONS, None)
        .unwrap()
        .expect("successful SUBTREE2 insert");

    // Populate SUBTREE2 with values 0 through 49 under keys 0 through 49.
    for i in 0u8..50 {
        let i_vec = (i as u8).to_be_bytes().to_vec();
        db.insert(
            [KEY1, KEY2],
            &i_vec,
            Element::new_item(i_vec.clone()),
            INSERT_OPTIONS,
            None,
        )
        .unwrap()
        .expect("successfully inserted values in SUBTREE2");
    }

    // Set random_numbers
    let mut rng = rand::thread_rng();
    let rn1: &[u8] = &(rng.gen_range(15..26) as u8).to_be_bytes();
    let rn2: &[u8] = &(rng.gen_range(60..62) as u8).to_be_bytes();

    // Overwrite key rn1 with a subtree
    // Call this SUBTREE3
    db.insert(
        [KEY1, KEY2],
        &rn1,
        Element::empty_tree(),
        INSERT_OPTIONS,
        None,
    )
    .unwrap()
    .expect("successful SUBTREE3 insert");

    // Populate SUBTREE3 with values 50 through 74 under keys 50 through 74
    for i in 50u8..75 {
        let i_vec = (i as u8).to_be_bytes().to_vec();
        db.insert(
            [KEY1, KEY2, rn1],
            &i_vec,
            Element::new_item(i_vec.clone()),
            INSERT_OPTIONS,
            None,
        )
        .unwrap()
        .expect("successfully inserted values in SUBTREE3");
    }

    // Overwrite key rn2 with a subtree
    // Call this SUBTREE4
    db.insert(
        [KEY1, KEY2, rn1],
        &rn2,
        Element::empty_tree(),
        INSERT_OPTIONS,
        None,
    )
    .unwrap()
    .expect("successful SUBTREE4 insert");

    // Put an empty subtree into SUBTREE4 at KEY3.
    // Call this SUBTREE5.
    db.insert([KEY1, KEY2, rn1, rn2], KEY3, Element::empty_tree(), INSERT_OPTIONS, None)
        .unwrap()
        .expect("successful SUBTREE5 insert");

    // Populate SUBTREE5 with values 75 through 99 under keys 75 through 99
    for i in 75u8..99 {
        let i_vec = (i as u8).to_be_bytes().to_vec();
        db.insert(
            [KEY1, KEY2, rn1, rn2, KEY3],
            &i_vec,
            Element::new_item(i_vec.clone()),
            INSERT_OPTIONS,
            None,
        )
        .unwrap()
        .expect("successfully inserted values in SUBTREE5");
    }
}
                  

The terminal output depends on which random numbers were generated. A few of the possibilities are shown below.

Random number 1 does not fall between 20-25:


[[22], [23], [24], [25], [26], [27], [28], [29]]
                  

Random number 1 is 23, random number 2 is not 60:


[[60], [70], [24], [25], [26], [27], [28], [29]]
                  

Random number 1 is 21, random number 2 is 60:


[[92], [93], [94], [70], [22], [23], [24], [25], [26], [27]]
                  

Proofs

Proofs

GroveDB generally uses the prove_query() function to generate query proofs, and the verify_query() function to verify query proofs. prove_query() just takes a path query as an argument and verify_query() takes a proof as well as a path query.

This tutorial populates and queries GroveDB the same as Simple Query Tutorial, then generates a proof for the query, uses it to calculate the root hash, and then compares that root hash to the GroveDB root hash.

The following code can be run with cargo run --bin proofs.


use grovedb::GroveDb;
use grovedb::{ Query, PathQuery };
use grovedb::operations::insert::InsertOptions;
use grovedb::Element;

const KEY1: &[u8] = b"key1";
const KEY2: &[u8] = b"key2";

// Allow insertions to overwrite trees
// This is necessary so the tutorial can be rerun easily
const INSERT_OPTIONS: Option<InsertOptions> = Some(InsertOptions {
    validate_insertion_does_not_override: false,
    validate_insertion_does_not_override_tree: false,
    base_root_storage_is_free: true,
});

fn main() {
    // Specify the path to the previously created GroveDB instance
    let path = String::from("../tutorial-storage");

    // Open GroveDB as db
    let db = GroveDb::open(path).unwrap();

    // Populate GroveDB with values. This function is defined below.
    populate(&db);

    // Define the path to the subtree we want to query.
    let path = vec![KEY1.to_vec(), KEY2.to_vec()];

    // Instantiate a new query.
    let mut query = Query::new();

    // Insert a range of keys to the query that we would like returned.
    query.insert_range(30_u8.to_be_bytes().to_vec()..35_u8.to_be_bytes().to_vec());

    // Put the query into a new unsized path query.
    let path_query = PathQuery::new_unsized(path, query.clone());

    // Execute the query and collect the result items in "elements".
    let (_elements, _) = db
        .query_item_value(&path_query, true, None)
        .unwrap()
        .expect("expected successful get_path_query");

    // Generate proof.
    let proof = db.prove_query(&path_query).unwrap().unwrap();

    // Get hash from query proof and print to terminal along with GroveDB root hash.
    let (hash, _result_set) = GroveDb::verify_query(&proof, &path_query).unwrap();

    // See if the query proof hash matches the GroveDB root hash
    println!("Does the hash generated from the query proof match the GroveDB root hash?");
    if hash == db.root_hash(None).unwrap().unwrap() {
        println!("Yes");
    } else { println!("No"); };
}

fn populate(db: &GroveDb) {
    // Put an empty subtree into the root tree nodes at KEY1.
    // Call this SUBTREE1.
    db.insert([], KEY1, Element::empty_tree(), INSERT_OPTIONS, None)
        .unwrap()
        .expect("successful SUBTREE1 insert");

    // Put an empty subtree into subtree1 at KEY2.
    // Call this SUBTREE2.
    db.insert([KEY1], KEY2, Element::empty_tree(), INSERT_OPTIONS, None)
        .unwrap()
        .expect("successful SUBTREE2 insert");

    // Populate SUBTREE2 with values 0 through 99 under keys 0 through 99.
    for i in 0u8..100 {
        let i_vec = (i as u8).to_be_bytes().to_vec();
        db.insert(
            [KEY1, KEY2],
            &i_vec,
            Element::new_item(i_vec.clone()),
            INSERT_OPTIONS,
            None,
        )
        .unwrap()
        .expect("successfully inserted values");
    }
}
                    

The terminal should output:


Does the hash generated from the query proof match the GroveDB root hash?
Yes
                    

Now you know that your query results are complete, correct, and fresh.