diff --git a/CODEOWNERS b/CODEOWNERS index d9fb2d8b5..84ee4eb07 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,6 +4,7 @@ # Pick two reviewers for each top level directory: # the owner and someone from the eng-reviewers team. /.github/ @smrz2001 @ceramicnetwork/eng-reviewers +/anchor-service/ @AaronGoldman @ceramicnetwork/eng-reviewers /api-server/ @dav1do @ceramicnetwork/eng-reviewers /api/ @dav1do @ceramicnetwork/eng-reviewers /beetle/ @nathanielc @ceramicnetwork/eng-reviewers diff --git a/Cargo.lock b/Cargo.lock index abac55ec0..3f2d672e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1404,6 +1404,22 @@ dependencies = [ "shlex", ] +[[package]] +name = "ceramic-anchor-service" +version = "0.34.0" +dependencies = [ + "anyhow", + "async-trait", + "ceramic-core", + "ceramic-event", + "cid 0.11.1", + "expect-test", + "indexmap 2.4.0", + "serde", + "serde_ipld_dagjson", + "tokio", +] + [[package]] name = "ceramic-api" version = "0.34.0" @@ -1554,7 +1570,7 @@ dependencies = [ "hex", "ipld-core", "iroh-bitswap", - "itertools 0.12.1", + "itertools 0.13.0", "multibase 0.9.1", "multihash 0.19.1", "multihash-codetable", diff --git a/Cargo.toml b/Cargo.toml index 6386e4f24..333c933c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "api", "api-server", + "anchor-service", "car", "core", "event", @@ -53,6 +54,7 @@ bs58 = "0.4" bytecheck = "0.6.7" bytes = "1.1" bytesize = "1.1" +ceramic-anchor-service = { path = "./anchor-service" } ceramic-api = { path = "./api" } ceramic-api-server = { path = "./api-server" } ceramic-car = { path = "./car" } @@ -101,6 +103,7 @@ http-serde = "1.1" humansize = "2" hyper = { version = "0.14", features = ["full"] } ignore = "0.4.18" +indexmap = "2.3.0" indicatif = "0.17.1" integer-encoding = "3.0" ipld-core = "0.4" @@ -110,7 +113,7 @@ iroh-p2p = { version = "0.2.0", path = "./beetle/iroh-p2p" } iroh-rpc-client = { path = "./beetle/iroh-rpc-client" } iroh-rpc-types = { path = "./beetle/iroh-rpc-types" } iroh-util = { path = "./beetle/iroh-util" } -itertools = "0.12.0" +itertools = "0.13.0" k256 = "0.13" keyed_priority_queue = "0.4.1" lazy_static = "1.4" @@ -226,6 +229,9 @@ authors = [ "Danny Browning ", "Nathaniel Cook ", "Aaron D Goldman ", + "Mohsin Zaidi <@smrz2001>", + "David Estes <@dav1do>", + "Spencer T Brody <@stbrody>" ] license = "Apache-2.0/MIT" repository = "https://github.com/3box/rust-ceramic" diff --git a/Makefile b/Makefile index 950e5004a..0b435c3da 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ MANUAL_DEPLOY ?= false TEST_SELECTOR ?= . .PHONY: all -all: build check-fmt check-clippy check-deps test +all: check-deps check-fmt check-clippy build test .PHONY: build build: diff --git a/anchor-service/Cargo.toml b/anchor-service/Cargo.toml new file mode 100644 index 000000000..34bfb9fde --- /dev/null +++ b/anchor-service/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ceramic-anchor-service" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +publish = false + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +ceramic-core.workspace = true +ceramic-event.workspace = true +cid.workspace = true +expect-test.workspace = true +indexmap.workspace = true +serde.workspace = true +serde_ipld_dagjson.workspace = true +tokio.workspace = true + +[features] +test-network = [] diff --git a/anchor-service/src/anchor.rs b/anchor-service/src/anchor.rs new file mode 100644 index 000000000..40fee3fc3 --- /dev/null +++ b/anchor-service/src/anchor.rs @@ -0,0 +1,90 @@ +use cid::Cid; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +use ceramic_event::unvalidated::{Proof, RawTimeEvent}; + +/// AnchorRequest for a Data Event on a Stream +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AnchorRequest { + /// The CID of the stream + pub id: Cid, + /// The CID of the Event to be anchored + pub prev: Cid, +} + +/// Merkle tree node +pub type MerkleNode = Vec>; + +/// A collection of Merkle tree nodes. +#[derive(Default)] +pub struct MerkleNodes { + /// This is a map from CIDs to Merkle Tree nodes that have those CIDs. + /// We are using an IndexMap to keep the block in insert order. + /// This keeps the remote and local block together for easier debugging. + nodes: IndexMap, +} + +impl MerkleNodes { + /// Extend one map of MerkleNodes with another + pub fn extend(&mut self, other: MerkleNodes) { + self.nodes.extend(other.nodes); + } + + /// Insert a new MerkleNode into the map + pub fn insert(&mut self, key: Cid, value: MerkleNode) { + self.nodes.insert(key, value); + } + + /// Return an iterator over the MerkleNodes + pub fn iter(&self) -> indexmap::map::Iter { + self.nodes.iter() + } +} + +impl std::fmt::Debug for MerkleNodes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map() + .entries(self.nodes.iter().map(|(k, v)| { + ( + format!("{:?}", k), + v.iter().map(|x| format!("{:?}", x)).collect::>(), + ) + })) + .finish() + } +} + +/// A list of Time Events +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TimeEvents { + /// The list of Time Events + pub events: Vec, +} + +impl std::fmt::Debug for TimeEvents { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(self.events.iter()).finish() + } +} + +/// TimeEvents, MerkleNodes, and Proof emitted from anchoring +pub struct TimeEventBatch { + /// The intermediate Merkle Tree Nodes + pub merkle_nodes: MerkleNodes, + /// The anchor proof + pub proof: Proof, + /// The Time Events + pub time_events: TimeEvents, +} + +impl std::fmt::Debug for TimeEventBatch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TimeEventBatch") + .field("merkle_nodes", &self.merkle_nodes) + .field("proof", &self.proof) + .field("time_events", &self.time_events) + .finish() + } +} diff --git a/anchor-service/src/anchor_batch.rs b/anchor-service/src/anchor_batch.rs new file mode 100644 index 000000000..84dcf7ebb --- /dev/null +++ b/anchor-service/src/anchor_batch.rs @@ -0,0 +1,59 @@ +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; + +use crate::{ + anchor::{AnchorRequest, TimeEventBatch}, + merkle_tree::{build_merkle_tree, MerkleTree}, + time_event::build_time_events, + transaction_manager::{Receipt, TransactionManager}, +}; + +/// ceramic_anchor_service::Store is responsible for fetching AnchorRequests and storing TimeEvents. +#[async_trait] +pub trait Store: Send + Sync { + /// Get a batch of AnchorRequests. + async fn get_anchor_requests(&self) -> Result>; + /// Store a batch of TimeEvents. + async fn put_time_events(&self, batch: TimeEventBatch) -> Result<()>; +} + +/// An AnchorService is responsible for anchoring batches of AnchorRequests and storing TimeEvents generated based on +/// the requests and the anchor proof. +pub struct AnchorService { + tx_manager: Arc, +} + +impl AnchorService { + /// Create a new AnchorService. + pub fn new(tx_manager: Arc) -> Self { + Self { tx_manager } + } + + /// Anchor a batch of requests using a Transaction Manager: + /// - Build a MerkleTree from the anchor requests + /// - Anchor the root of the tree and obtain a proof from the Transaction Manager + /// - Build TimeEvents from the anchor requests and the proof + /// + /// This function will block until the proof is obtained from the Transaction Manager. + pub async fn anchor_batch(&self, anchor_requests: &[AnchorRequest]) -> Result { + let MerkleTree { + root_cid, + nodes, + count, + } = build_merkle_tree(anchor_requests)?; + let Receipt { + proof, + detached_time_event, + mut remote_merkle_nodes, + } = self.tx_manager.make_proof(root_cid).await?; + let time_events = build_time_events(anchor_requests, &detached_time_event, count)?; + remote_merkle_nodes.extend(nodes); + Ok(TimeEventBatch { + merkle_nodes: remote_merkle_nodes, + proof, + time_events, + }) + } +} diff --git a/anchor-service/src/lib.rs b/anchor-service/src/lib.rs new file mode 100644 index 000000000..00ab28e87 --- /dev/null +++ b/anchor-service/src/lib.rs @@ -0,0 +1,11 @@ +//! This crate include all the machinery needed for building Merkle Trees from Anchor Requests and then the Time Events +//! corresponding to those requests once the root of the tree has been anchored. +#![warn(missing_docs)] +mod anchor; +mod anchor_batch; +mod merkle_tree; +mod time_event; +mod transaction_manager; + +pub use anchor_batch::{AnchorService, Store}; +pub use transaction_manager::{DetachedTimeEvent, Receipt, TransactionManager}; diff --git a/anchor-service/src/merkle_tree.rs b/anchor-service/src/merkle_tree.rs new file mode 100644 index 000000000..7cba30512 --- /dev/null +++ b/anchor-service/src/merkle_tree.rs @@ -0,0 +1,83 @@ +use crate::anchor::{AnchorRequest, MerkleNode, MerkleNodes}; +use anyhow::{anyhow, Result}; +use ceramic_core::SerializeExt; +use cid::Cid; + +pub struct MerkleTree { + pub root_cid: Cid, + pub nodes: MerkleNodes, + pub count: u64, +} + +/// Make a tree using Merkle mountain range +/// Ref: https://eprint.iacr.org/2021/038.pdf +pub fn build_merkle_tree(anchor_requests: &[AnchorRequest]) -> Result { + // For size zero trees return an error + if anchor_requests.is_empty() { + return Err(anyhow!("no requests to anchor")); + } + // The roots of the sub-trees with full power of 2 trees. + // They are in the places in the array where the 1s are in the count u64. + // e.g. 13 = 0b1110 + // root + // / \ + // / \ / \ + // / \ / \ / \ / \ + // /\ /\ / \ / \ / \ / \ 12 13 + // 0 1 2 3 4 5 6 7 8 9 10 11 + // + // here the peeks are [none, 12..13, 8..11, 0..7] + // place values 1's, 2's, 4's, 8's + let mut peaks: Vec> = vec![None; 64]; + + // The nodes in the Merkle Map[node_cid, [left_cid, right_cid]] + let mut nodes = MerkleNodes::default(); + + // insert all the `anchor_request.prev`s into the peaks. + for anchor_request in anchor_requests { + let mut new_node_cid: Cid = anchor_request.prev; + for peek in peaks.iter_mut() { + // walk the place values + match peek { + None => { + // if the place values peek is empty put the cid there + *peek = Some(new_node_cid); + // we found a place to put it. we are done + break; + } + Some(old_node_cid) => { + // if the place values peek is occupied add the old cid to the new cid and carry to the next place values peek + let merged_node: MerkleNode; + (new_node_cid, merged_node) = merge_nodes(*old_node_cid, new_node_cid)?; + // remember the generated nodes + nodes.insert(new_node_cid, merged_node); + // clear the place value peek we took old node from + *peek = None; + } + } + } + } + + // Roll up the peaks into a root. + // Since each tree is larger then all the preceding trees combined + // we just walk the non empty peeks putting the last peek on the left. + let mut peaks_iter = peaks.into_iter().flatten(); + let mut right_cid = peaks_iter.next().expect("should never be empty"); + for left_cid in peaks_iter { + let merged_node: MerkleNode; + (right_cid, merged_node) = merge_nodes(left_cid, right_cid)?; + nodes.insert(right_cid, merged_node); + } + + Ok(MerkleTree { + root_cid: right_cid, + count: anchor_requests.len() as u64, + nodes, + }) +} + +/// Accepts the CIDs of two blocks and returns the CID of the CBOR list that includes both CIDs. +pub(crate) fn merge_nodes(left: Cid, right: Cid) -> Result<(Cid, MerkleNode)> { + let merkle_node = vec![Some(left), Some(right)]; + Ok((merkle_node.to_cid()?, merkle_node)) +} diff --git a/anchor-service/src/time_event.rs b/anchor-service/src/time_event.rs new file mode 100644 index 000000000..cbbb67a41 --- /dev/null +++ b/anchor-service/src/time_event.rs @@ -0,0 +1,171 @@ +use anyhow::{anyhow, Result}; + +use ceramic_event::unvalidated::RawTimeEvent; + +use crate::{ + anchor::{AnchorRequest, TimeEvents}, + DetachedTimeEvent, +}; + +pub fn build_time_events( + anchor_requests: &[AnchorRequest], + detached_time_event: &DetachedTimeEvent, + count: u64, +) -> Result { + let events = anchor_requests + .iter() + .enumerate() + .map(|(index, anchor_request)| { + let local_path = index_to_path(index.try_into()?, count)?; + let remote_path = detached_time_event.path.as_str(); + Ok(RawTimeEvent::new( + anchor_request.id, + anchor_request.prev, + detached_time_event.proof, + format!("{}/{}", remote_path, local_path) + .trim_matches('/') + .to_owned(), + )) + }) + .collect::>>()?; + + Ok(TimeEvents { events }) +} + +pub fn index_to_path(index: u64, count: u64) -> Result { + // we want to find the path to the index in a tree length. + // first find the sub-tree then append the path in the sub-tree + // + // e.g. index_to_path(index: 10, count: 14) => Ok("1/0/1/0") + // + // 14 = 0b1110 + // 10 = 0b1010 + // [root] + // / [\] + // / \ [/] \ + // / \ / \ / [\] / \ + // /\ /\ / \ / \ / \ [/] \ 12 13 + // 0 1 2 3 4 5 6 7 8 9 [10] 11 + // + // find the tree the index is in. + // MSB of 14 is 8; 10 > 8; -= 8; go right "/1" + // MSB of 6 is 4; 2 !> 4; -= 4; go left "/0" + // append the remaining bits of index as path in the sub-tree. + // 2 is 0b10 so right "/1" then left "/0" + // final {"path": "1/0/1/0"} for index 10 of length 14. + if index >= count { + return Err(anyhow!("index({}) >= count({})", index, count)); + } + + let mut path: Vec<_> = Vec::new(); + let mut length = count; + let mut index = index; + + // The purpose of this while loop is to figure out which subtree the index is in. + let mut top_power_of_2 = Default::default(); + while length > 0 { + top_power_of_2 = 1 << (63 - length.leading_zeros()); + if top_power_of_2 == length { + break; + } + if index < top_power_of_2 { + // the index is in the left tree + path.push('0'); + break; + } else { + // the index is in the right tree + path.push('1'); + length -= top_power_of_2; + index -= top_power_of_2; + } + } + + // The purpose of this for loop is to figure out the specified index's location in the subtree. + // Adding the top power of two forces the binary of the number to always start with 1. We can then subtract the + // top power of two to strip the leading 1. This leaves us with all the leading zeros. + path.append( + format!("{:b}", index + top_power_of_2)[1..] + .chars() + .collect::>() + .as_mut(), + ); + Ok(path + .iter() + .map(|c| c.to_string()) + .collect::>() + .join("/")) +} + +/// Tests to ensure that the merge function is working as expected. +#[cfg(test)] +mod tests { + use super::*; + use cid::Cid; + use expect_test::expect; + + #[tokio::test] + async fn test_time_event() { + let id = + Cid::try_from("baeabeifu7qd7bpy4z6vdo7jff6kg3uiwolqtofhut7nrhx6wuhpb2wqxtq").unwrap(); + let prev = + Cid::try_from("baeabeifu7qd7bpy4z6vdo7jff6kg3uiwolqtofhut7nrhx6wuhpb2wqxtq").unwrap(); + let proof = + Cid::try_from("bafyreidq247kfkizr3k6wlvx43lt7gro2dno7vzqepmnqt26agri4opzqu").unwrap(); + let detached_time_event = DetachedTimeEvent { + path: "".to_owned(), + proof, + }; + let anchor_requests = vec![AnchorRequest { id, prev }]; + let time_event = build_time_events(&anchor_requests, &detached_time_event, 1); + expect![[r#"{"events":[{"id":{"/":"baeabeifu7qd7bpy4z6vdo7jff6kg3uiwolqtofhut7nrhx6wuhpb2wqxtq"},"prev":{"/":"baeabeifu7qd7bpy4z6vdo7jff6kg3uiwolqtofhut7nrhx6wuhpb2wqxtq"},"proof":{"/":"bafyreidq247kfkizr3k6wlvx43lt7gro2dno7vzqepmnqt26agri4opzqu"},"path":""}]}"#]] + .assert_eq(&String::from_utf8(serde_ipld_dagjson::to_vec(&time_event.unwrap()).unwrap()).unwrap()); + } + + #[tokio::test] + async fn test_index_to_path() { + // index: 0, count: 1 + expect![""].assert_eq(&index_to_path(0, 1).unwrap()); + + // index: 0 - 1, count: 2 + expect!["0"].assert_eq(&index_to_path(0, 2).unwrap()); + expect!["1"].assert_eq(&index_to_path(1, 2).unwrap()); + + // index: 0 - 2, count: 3 + expect!["0/0"].assert_eq(&index_to_path(0, 3).unwrap()); + expect!["0/1"].assert_eq(&index_to_path(1, 3).unwrap()); + expect!["1"].assert_eq(&index_to_path(2, 3).unwrap()); + + // index 0 - 3, count: 4 + expect!["0/0"].assert_eq(&index_to_path(0, 4).unwrap()); + expect!["0/1"].assert_eq(&index_to_path(1, 4).unwrap()); + expect!["1/0"].assert_eq(&index_to_path(2, 4).unwrap()); + expect!["1/1"].assert_eq(&index_to_path(3, 4).unwrap()); + + // '1/' 10 > 8, 14 + // '1/0/' 2 > 4, 6 + // '1/0/' 0b10 + // '1/0/1/0' + expect!["1/0/1/0"].assert_eq(&index_to_path(10, 14).unwrap()); + + // '0/' 500_000 < 524288, 1_000_000 + // '0/' 0b1111010000100100000 + // '0/1/1/1/1/0/1/0/0/0/0/1/0/0/1/0/0/0/0/0/' + expect!["0/1/1/1/1/0/1/0/0/0/0/1/0/0/1/0/0/0/0/0"] + .assert_eq(&index_to_path(500_000, 1_000_000).unwrap()); + + // '1/' 999_999 > 524288, 1_000_000 + // '1/1/' 475_711 > 262_144, 475_712 + // '1/1/1/' 213_567 > 131072, 213_568 + // '1/1/1/1/' 82_495 > 65_536, 82_496 + // '1/1/1/1/1/' 16_959 > 16_384, 16_960 + // '1/1/1/1/1/1/' 575 > 512, 576 + // '1/1/1/1/1/1/0/' 63 !> 64, 64 + // '1/1/1/1/1/1/0/' 0b111111 + // '1/1/1/1/1/1/0/1/1/1/1/1/1/' + expect!["1/1/1/1/1/1/1/1/1/1/1/1"].assert_eq(&index_to_path(999_999, 1_000_000).unwrap()); + expect!["0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0"] + .assert_eq(&index_to_path(0, 1_000_000).unwrap()); + expect!["0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/1"] + .assert_eq(&index_to_path(1, 1_000_000).unwrap()); + } +} diff --git a/anchor-service/src/transaction_manager.rs b/anchor-service/src/transaction_manager.rs new file mode 100644 index 000000000..cbc5c7730 --- /dev/null +++ b/anchor-service/src/transaction_manager.rs @@ -0,0 +1,62 @@ +use anyhow::Result; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use ceramic_core::Cid; +use ceramic_event::unvalidated::Proof; + +use crate::anchor::MerkleNodes; + +/// A receipt containing a blockchain proof CID, the path prefix to the CID in the anchored Merkle tree and the +/// corresponding Merkle tree nodes. +pub struct Receipt { + /// the proof for block from the remote anchoring service + pub proof: Proof, + /// the path through the remote Merkle tree + pub detached_time_event: DetachedTimeEvent, + /// the Merkle tree nodes from the remote anchoring service + pub remote_merkle_nodes: MerkleNodes, +} + +impl std::fmt::Debug for Receipt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut merkle_tree_nodes: Vec<_> = self + .remote_merkle_nodes + .iter() + .map(|(k, v)| format!("{:?}: {:?}", k, v)) + .collect(); + merkle_tree_nodes.sort(); + f.debug_struct("Receipt") + .field("proof", &self.proof) + .field("detached_time_event", &self.detached_time_event) + .field("remote_merkle_nodes", &merkle_tree_nodes) + .finish() + } +} + +/// A detached time event containing the path through the Merkle tree and the CID of the anchor proof block. This can be +/// used to build Time Events. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DetachedTimeEvent { + /// The path through the Merkle Tree + pub path: String, + /// The CID of the anchor proof block + pub proof: Cid, +} + +impl std::fmt::Debug for DetachedTimeEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DetachedTimeEvent") + .field("path", &self.path) + .field("proof", &format!("{:?}", &self.proof)) + .finish() + } +} + +/// Interface for the transaction manager that accepts a root CID and returns a proof. +#[async_trait] +pub trait TransactionManager: Send + Sync { + /// Accepts a root CID and returns a proof. + async fn make_proof(&self, root: Cid) -> Result; +} diff --git a/cspell.json b/cspell.json index 723741af7..8a493a3d9 100644 --- a/cspell.json +++ b/cspell.json @@ -49,6 +49,7 @@ "libp2p", "listen_addrs", "mdns", + "Merkle", "meshsub", "minicbor", "mockall", diff --git a/event-svc/src/store/sql/entities/event_block.rs b/event-svc/src/store/sql/entities/event_block.rs index dd53759e7..88797c404 100644 --- a/event-svc/src/store/sql/entities/event_block.rs +++ b/event-svc/src/store/sql/entities/event_block.rs @@ -45,7 +45,7 @@ impl ReconEventBlockRaw { }), |blocks| { blocks - .group_by(|(key, _)| key.clone()) + .chunk_by(|(key, _)| key.clone()) .into_iter() .map(|(key, group)| { ( diff --git a/event/src/lib.rs b/event/src/lib.rs index 435376659..a50a387f3 100644 --- a/event/src/lib.rs +++ b/event/src/lib.rs @@ -1,6 +1,7 @@ //! # Ceramic Event //! Implementation of ceramic event protocol, with appropriate compatibilility with js-ceramic #![warn(missing_docs)] + mod bytes; /// Unvalidated event types pub mod unvalidated; diff --git a/event/src/unvalidated/event.rs b/event/src/unvalidated/event.rs index 7c6b526ee..c0475d846 100644 --- a/event/src/unvalidated/event.rs +++ b/event/src/unvalidated/event.rs @@ -332,9 +332,13 @@ impl TimeEvent { /// Raw Time Event as it is encoded in the protocol. #[derive(Serialize, Deserialize)] pub struct RawTimeEvent { + /// The CID of the init event of the stream id: Cid, + /// The CID of the Data event that is being anchored in the chain prev: Cid, + /// The CID of the proof block that tells us how to query the chain proof: Cid, + /// path from the root in the proof block to the prev in the merkle tree path: String, }