@@ -22,6 +22,20 @@ impl Display for Error {
2222#[ cfg( feature = "std" ) ]
2323impl std:: error:: Error for Error { }
2424
25+ /// File-backed mmap storage for Merkle tree nodes.
26+ ///
27+ /// After `spill_nodes_to_disk()`, the in-memory node vector is freed and
28+ /// node access goes through this mmap instead.
29+ #[ cfg( feature = "disk-spill" ) ]
30+ pub ( crate ) struct MmapNodeBacking {
31+ mmap : memmap2:: Mmap ,
32+ /// Owns the file descriptor backing the mmap. Dropping it would close
33+ /// the descriptor and invalidate the mmap.
34+ _file : std:: fs:: File ,
35+ node_count : usize ,
36+ node_size : usize ,
37+ }
38+
2539/// The struct for the Merkle tree, consisting of the root and the nodes.
2640/// A typical tree would look like this
2741/// root
@@ -31,11 +45,14 @@ impl std::error::Error for Error {}
3145/// leaf 1 leaf 2 leaf 3 leaf 4
3246/// The bottom leafs correspond to the hashes of the elements, while each upper
3347/// layer contains the hash of the concatenation of the daughter nodes.
34- #[ derive( Clone ) ]
48+ #[ cfg_attr ( not ( feature = "disk-spill" ) , derive( Clone ) ) ]
3549#[ cfg_attr( feature = "serde" , derive( serde:: Serialize , serde:: Deserialize ) ) ]
3650pub struct MerkleTree < B : IsMerkleTreeBackend > {
3751 pub root : B :: Node ,
3852 nodes : Vec < B :: Node > ,
53+ #[ cfg( feature = "disk-spill" ) ]
54+ #[ cfg_attr( feature = "serde" , serde( skip) ) ]
55+ mmap_backing : Option < MmapNodeBacking > ,
3956}
4057
4158const ROOT : usize = 0 ;
@@ -78,14 +95,44 @@ where
7895 Some ( MerkleTree {
7996 root : nodes[ ROOT ] . clone ( ) ,
8097 nodes,
98+ #[ cfg( feature = "disk-spill" ) ]
99+ mmap_backing : None ,
81100 } )
82101 }
83102
103+ /// Total number of nodes in the tree (inner + leaves).
104+ #[ inline]
105+ fn node_count ( & self ) -> usize {
106+ #[ cfg( feature = "disk-spill" ) ]
107+ if let Some ( ref backing) = self . mmap_backing {
108+ return backing. node_count ;
109+ }
110+ self . nodes . len ( )
111+ }
112+
113+ /// Access a node by index, returning a reference.
114+ ///
115+ /// Returns `None` if `idx` is out of bounds.
116+ #[ inline]
117+ fn node_get ( & self , idx : usize ) -> Option < & B :: Node > {
118+ #[ cfg( feature = "disk-spill" ) ]
119+ if let Some ( ref backing) = self . mmap_backing {
120+ if idx < backing. node_count {
121+ // SAFETY: spill_nodes_to_disk writes self.nodes as contiguous bytes
122+ // to this mmap and asserts align_of::<B::Node>() == 1 at compile time.
123+ let ptr = unsafe { backing. mmap . as_ptr ( ) . add ( idx * backing. node_size ) } ;
124+ return Some ( unsafe { & * ( ptr as * const B :: Node ) } ) ;
125+ }
126+ return None ;
127+ }
128+ self . nodes . get ( idx)
129+ }
130+
84131 /// Returns a Merkle proof for the element/s at position pos
85132 /// For example, give me an inclusion proof for the 3rd element in the
86133 /// Merkle tree
87134 pub fn get_proof_by_pos ( & self , pos : usize ) -> Option < Proof < B :: Node > > {
88- let pos = pos + self . nodes . len ( ) / 2 ;
135+ let pos = pos + self . node_count ( ) / 2 ;
89136 let Ok ( merkle_path) = self . build_merkle_path ( pos) else {
90137 return None ;
91138 } ;
@@ -101,12 +148,12 @@ where
101148 /// Returns the Merkle path for the element/s for the leaf at position pos
102149 fn build_merkle_path ( & self , pos : usize ) -> Result < Vec < B :: Node > , Error > {
103150 // Pre-allocate based on tree depth (log2 of tree size)
104- let tree_depth = ( self . nodes . len ( ) + 1 ) . ilog2 ( ) as usize ;
151+ let tree_depth = ( self . node_count ( ) + 1 ) . ilog2 ( ) as usize ;
105152 let mut merkle_path = Vec :: with_capacity ( tree_depth) ;
106153 let mut pos = pos;
107154
108155 while pos != ROOT {
109- let Some ( node) = self . nodes . get ( sibling_index ( pos) ) else {
156+ let Some ( node) = self . node_get ( sibling_index ( pos) ) else {
110157 // out of bounds, exit returning the current merkle_path
111158 return Err ( Error :: OutOfBounds ) ;
112159 } ;
@@ -141,7 +188,7 @@ where
141188 return Err ( Error :: EmptyPositionList ) ;
142189 }
143190
144- let num_leaves = ( self . nodes . len ( ) + 1 ) . div_ceil ( 2 ) ;
191+ let num_leaves = ( self . node_count ( ) + 1 ) . div_ceil ( 2 ) ;
145192
146193 // Validate all positions are within bounds
147194 for & pos in pos_list {
@@ -154,15 +201,19 @@ where
154201 // of the leaves.
155202 let leaf_positions = pos_list
156203 . iter ( )
157- . map ( |pos| pos + self . nodes . len ( ) / 2 )
204+ . map ( |pos| pos + self . node_count ( ) / 2 )
158205 . collect :: < Vec < usize > > ( ) ;
159206 // We get the positions of the nodes for the batch proof.
160207 let batch_auth_path_positions = self . get_batch_auth_path_positions ( & leaf_positions) ;
161208
162209 // We get the nodes for the batch proof.
163210 let batch_auth_path_nodes = batch_auth_path_positions
164211 . iter ( )
165- . map ( |pos| self . nodes [ * pos] . clone ( ) )
212+ . map ( |pos| {
213+ self . node_get ( * pos)
214+ . expect ( "batch auth path position in bounds" )
215+ . clone ( )
216+ } )
166217 . collect ( ) ;
167218
168219 Ok ( BatchProof {
@@ -188,7 +239,7 @@ where
188239 let mut obtainable: BTreeSet < usize > = leaf_positions. iter ( ) . cloned ( ) . collect ( ) ;
189240
190241 // Number of levels in tree
191- let num_levels = ( self . nodes . len ( ) + 1 ) . ilog2 ( ) ;
242+ let num_levels = ( self . node_count ( ) + 1 ) . ilog2 ( ) ;
192243
193244 // Iter lefevel-by-level from leaves to root.
194245 for _ in 0 ..num_levels - 1 {
@@ -217,4 +268,57 @@ where
217268 // This makes the proof ordered from bottom (nodes closer to leaves) to top (nodes loser to root).
218269 auth_path_set. into_iter ( ) . rev ( ) . collect ( )
219270 }
271+
272+ /// Write tree nodes to a temp file, mmap it, and free the in-memory vector.
273+ /// Node access methods read from the mmap after this call.
274+ #[ cfg( feature = "disk-spill" ) ]
275+ pub fn spill_nodes_to_disk ( & mut self ) -> std:: io:: Result < ( ) >
276+ where
277+ B :: Node : Copy ,
278+ {
279+ const {
280+ assert ! (
281+ align_of:: <B :: Node >( ) == 1 ,
282+ "B::Node must have alignment 1 for mmap safety"
283+ )
284+ }
285+ use std:: io:: Write ;
286+
287+ if self . nodes . is_empty ( ) {
288+ return Ok ( ( ) ) ;
289+ }
290+
291+ let node_size = core:: mem:: size_of :: < B :: Node > ( ) ;
292+ let node_count = self . nodes . len ( ) ;
293+ let total_bytes = node_count * node_size;
294+
295+ let file = tempfile:: tempfile ( ) ?;
296+ file. set_len ( total_bytes as u64 ) ?;
297+ {
298+ let mut writer = std:: io:: BufWriter :: new ( & file) ;
299+ // SAFETY: B::Node is a plain byte array ([u8; N]), so casting
300+ // the contiguous Vec to a byte slice is valid.
301+ let bytes = unsafe {
302+ core:: slice:: from_raw_parts ( self . nodes . as_ptr ( ) as * const u8 , total_bytes)
303+ } ;
304+ writer. write_all ( bytes) ?;
305+ writer. flush ( ) ?;
306+ }
307+
308+ // SAFETY: tempfile() creates an anonymous file with no filesystem path,
309+ // so no other process can open or modify it.
310+ let mmap = unsafe { memmap2:: MmapOptions :: new ( ) . map ( & file) ? } ;
311+
312+ // Free the heap allocation
313+ self . nodes = Vec :: new ( ) ;
314+
315+ self . mmap_backing = Some ( MmapNodeBacking {
316+ mmap,
317+ _file : file,
318+ node_count,
319+ node_size,
320+ } ) ;
321+
322+ Ok ( ( ) )
323+ }
220324}
0 commit comments