@@ -38,9 +38,10 @@ impl BuildHasher for U64BuildHasher {
3838
3939pub type U64HashMap < V > = HashMap < u64 , V , U64BuildHasher > ;
4040
41- // TODO: Correctly define this
42- const MAX_PUBLIC_OUTPUT_COMMIT_SIZE : u64 = 1024 ;
43- const PUBLIC_OUTPUT_START_INDEX : u64 = 0 ;
41+ /// Total cap on public output bytes across all `commit_public_output` calls.
42+ /// The COMMIT AIR concatenates calls via the running `x254` index, so this
43+ /// is enforced as a running-total budget rather than a per-call limit.
44+ pub const MAX_PUBLIC_OUTPUT_TOTAL_SIZE : u64 = 1024 * 1024 ;
4445/// Maximum size of the private input memory region (in bytes).
4546pub const MAX_PRIVATE_INPUT_SIZE : u64 = 6700000 ;
4647/// Fixed high address where private input is mapped. Guest programs can read
@@ -50,19 +51,30 @@ pub const MAX_PRIVATE_INPUT_SIZE: u64 = 6700000;
5051pub const PRIVATE_INPUT_START_INDEX : u64 = 0xFF000000 ;
5152
5253#[ derive( Default , Debug ) ]
53- pub struct Memory ( U64HashMap < [ u8 ; 4 ] > ) ;
54+ pub struct Memory {
55+ cells : U64HashMap < [ u8 ; 4 ] > ,
56+ /// Bytes committed to public output via `commit_public_output`. The
57+ /// COMMIT AIR doesn't write to a fixed memory region (it streams bytes
58+ /// onto the Commit bus by `index`), so this buffer is purely the
59+ /// executor's view used by `read_return_value` and CLI display.
60+ public_output : Vec < u8 > ,
61+ }
5462
5563impl Memory {
5664 pub fn load_byte ( & self , address : u64 ) -> u8 {
5765 let aligned_address = address - address % 4 ;
58- let value = self . 0 . get ( & aligned_address) . cloned ( ) . unwrap_or_default ( ) ;
66+ let value = self
67+ . cells
68+ . get ( & aligned_address)
69+ . cloned ( )
70+ . unwrap_or_default ( ) ;
5971 value[ ( address % 4 ) as usize ]
6072 }
6173
6274 pub fn store_byte ( & mut self , address : u64 , value : u8 ) {
6375 let aligned_address = address - address % 4 ;
6476 let entry = self
65- . 0
77+ . cells
6678 . entry ( aligned_address)
6779 . or_insert_with ( || [ 0 , 0 , 0 , 0 ] ) ;
6880 entry[ ( address % 4 ) as usize ] = value;
@@ -72,7 +84,7 @@ impl Memory {
7284 if !address. is_multiple_of ( 4 ) {
7385 return Err ( MemoryError :: UnalignedAccess ) ;
7486 }
75- let bytes = self . 0 . get ( & address) . cloned ( ) . unwrap_or_default ( ) ;
87+ let bytes = self . cells . get ( & address) . cloned ( ) . unwrap_or_default ( ) ;
7688 Ok ( u32:: from_le_bytes ( bytes) )
7789 }
7890
@@ -81,7 +93,7 @@ impl Memory {
8193 return Err ( MemoryError :: UnalignedAccess ) ;
8294 }
8395 let bytes = value. to_le_bytes ( ) ;
84- self . 0 . insert ( address, bytes) ;
96+ self . cells . insert ( address, bytes) ;
8597 Ok ( ( ) )
8698 }
8799
@@ -90,8 +102,8 @@ impl Memory {
90102 if !address. is_multiple_of ( 8 ) {
91103 return Err ( MemoryError :: UnalignedAccess ) ;
92104 }
93- let low_bytes = self . 0 . get ( & address) . cloned ( ) . unwrap_or_default ( ) ;
94- let high_bytes = self . 0 . get ( & ( address + 4 ) ) . cloned ( ) . unwrap_or_default ( ) ;
105+ let low_bytes = self . cells . get ( & address) . cloned ( ) . unwrap_or_default ( ) ;
106+ let high_bytes = self . cells . get ( & ( address + 4 ) ) . cloned ( ) . unwrap_or_default ( ) ;
95107 let low = u32:: from_le_bytes ( low_bytes) as u64 ;
96108 let high = u32:: from_le_bytes ( high_bytes) as u64 ;
97109 Ok ( low | ( high << 32 ) )
@@ -104,8 +116,8 @@ impl Memory {
104116 }
105117 let low = ( value & 0xFFFFFFFF ) as u32 ;
106118 let high = ( value >> 32 ) as u32 ;
107- self . 0 . insert ( address, low. to_le_bytes ( ) ) ;
108- self . 0 . insert ( address + 4 , high. to_le_bytes ( ) ) ;
119+ self . cells . insert ( address, low. to_le_bytes ( ) ) ;
120+ self . cells . insert ( address + 4 , high. to_le_bytes ( ) ) ;
109121 Ok ( ( ) )
110122 }
111123
@@ -117,7 +129,11 @@ impl Memory {
117129 ) ;
118130 }
119131 let aligned_address = address - address % 4 ;
120- let bytes = self . 0 . get ( & aligned_address) . cloned ( ) . unwrap_or_default ( ) ;
132+ let bytes = self
133+ . cells
134+ . get ( & aligned_address)
135+ . cloned ( )
136+ . unwrap_or_default ( ) ;
121137 let value = & bytes[ ( address % 4 ) as usize ..( address % 4 ) as usize + 2 ] ;
122138 Ok ( u16:: from_le_bytes (
123139 value. try_into ( ) . map_err ( |_| MemoryError :: LoadHalf ) ?,
@@ -130,7 +146,7 @@ impl Memory {
130146 }
131147 let aligned_address = address - address % 4 ;
132148 let entry = self
133- . 0
149+ . cells
134150 . entry ( aligned_address)
135151 . or_insert_with ( || [ 0 , 0 , 0 , 0 ] ) ;
136152 let bytes = value. to_le_bytes ( ) ;
@@ -139,19 +155,25 @@ impl Memory {
139155 Ok ( ( ) )
140156 }
141157
158+ /// Append `length` bytes from guest memory starting at `address` to the
159+ /// public output. The COMMIT AIR concatenates calls via the running
160+ /// `x254` index, and the trace builder accumulates `commit_ops` into
161+ /// `VmProof.public_output`; this method maintains the executor's view
162+ /// of the same byte stream so `read_return_value` matches.
142163 pub fn commit_public_output ( & mut self , address : u64 , length : u64 ) -> Result < ( ) , MemoryError > {
143- if length > MAX_PUBLIC_OUTPUT_COMMIT_SIZE {
164+ let new_total = ( self . public_output . len ( ) as u64 )
165+ . checked_add ( length)
166+ . ok_or ( MemoryError :: CommitSizeExceeded ) ?;
167+ if new_total > MAX_PUBLIC_OUTPUT_TOTAL_SIZE {
144168 return Err ( MemoryError :: CommitSizeExceeded ) ;
145169 }
146- self . store_word ( PUBLIC_OUTPUT_START_INDEX , length as u32 ) ?;
147- let inputs = self . load_bytes ( address, length) ;
148- self . set_bytes_aligned ( PUBLIC_OUTPUT_START_INDEX + 4 , & inputs) ?;
170+ let bytes = self . load_bytes ( address, length) ?;
171+ self . public_output . extend_from_slice ( & bytes) ;
149172 Ok ( ( ) )
150173 }
151174
152175 pub fn read_return_value ( & self ) -> Result < Vec < u8 > , MemoryError > {
153- let size = self . load_word ( PUBLIC_OUTPUT_START_INDEX ) ?;
154- Ok ( self . load_bytes ( PUBLIC_OUTPUT_START_INDEX + 4 , size as u64 ) )
176+ Ok ( self . public_output . clone ( ) )
155177 }
156178
157179 /// Pre-loads private input bytes at `PRIVATE_INPUT_START_INDEX` as a
@@ -164,23 +186,29 @@ impl Memory {
164186 if inputs. len ( ) as u64 > MAX_PRIVATE_INPUT_SIZE {
165187 return Err ( MemoryError :: PrivateInputSizeExceeded ) ;
166188 }
167- self . store_word ( PRIVATE_INPUT_START_INDEX , inputs. len ( ) as u32 ) ?;
189+ let len_u32 =
190+ u32:: try_from ( inputs. len ( ) ) . map_err ( |_| MemoryError :: PrivateInputSizeExceeded ) ?;
191+ self . store_word ( PRIVATE_INPUT_START_INDEX , len_u32) ?;
168192 self . set_bytes_aligned ( PRIVATE_INPUT_START_INDEX + 4 , & inputs) ?;
169193 Ok ( ( ) )
170194 }
171195
172- pub fn load_bytes ( & self , mut addr : u64 , len : u64 ) -> Vec < u8 > {
173- let mut result = Vec :: with_capacity ( len as usize ) ;
174- let end = addr + len;
196+ pub fn load_bytes ( & self , mut addr : u64 , len : u64 ) -> Result < Vec < u8 > , MemoryError > {
197+ let end = addr. checked_add ( len) . ok_or ( MemoryError :: AddressOverflow ) ?;
198+ let len_usize = usize:: try_from ( len) . map_err ( |_| MemoryError :: AllocationFailed ) ?;
199+ let mut result = Vec :: new ( ) ;
200+ result
201+ . try_reserve_exact ( len_usize)
202+ . map_err ( |_| MemoryError :: AllocationFailed ) ?;
175203 while addr < end {
176204 let aligned = addr - ( addr % 4 ) ;
177- let bytes = self . 0 . get ( & aligned) . cloned ( ) . unwrap_or_default ( ) ;
205+ let bytes = self . cells . get ( & aligned) . cloned ( ) . unwrap_or_default ( ) ;
178206 let offset = ( addr % 4 ) as usize ;
179207 let take = std:: cmp:: min ( 4 - offset, ( end - addr) as usize ) ;
180208 result. extend_from_slice ( & bytes[ offset..offset + take] ) ;
181209 addr += take as u64 ;
182210 }
183- result
211+ Ok ( result)
184212 }
185213
186214 /// Helper method to store a given input at an aligned address. It may also overwrite existing bytes with zero if inputs is not divisible by 4
@@ -192,7 +220,7 @@ impl Memory {
192220 for chunk in inputs. chunks ( 4 ) {
193221 let mut bytes = [ 0u8 ; 4 ] ;
194222 bytes[ ..chunk. len ( ) ] . copy_from_slice ( chunk) ;
195- self . 0 . insert ( addr, bytes) ;
223+ self . cells . insert ( addr, bytes) ;
196224 addr += 4 ;
197225 }
198226 Ok ( ( ) )
@@ -209,6 +237,10 @@ pub enum MemoryError {
209237 CommitSizeExceeded ,
210238 #[ error( "Private input size exceeded" ) ]
211239 PrivateInputSizeExceeded ,
240+ #[ error( "Address range exceeds u64::MAX" ) ]
241+ AddressOverflow ,
242+ #[ error( "Failed to allocate memory for load_bytes" ) ]
243+ AllocationFailed ,
212244}
213245
214246#[ cfg( test) ]
@@ -234,7 +266,7 @@ mod tests {
234266 }
235267
236268 #[ test]
237- fn test_commit_public_output_overwrites ( ) {
269+ fn test_commit_public_output_appends ( ) {
238270 let mut memory = Memory :: default ( ) ;
239271 memory. store_byte ( 0x100 , b'a' ) ;
240272 memory. store_byte ( 0x101 , b'b' ) ;
@@ -248,19 +280,78 @@ mod tests {
248280 . commit_public_output ( 0x104 , 2 )
249281 . expect ( "second commit should succeed" ) ;
250282
251- // Overwrite semantics: second commit replaces first
283+ // Append semantics: calls concatenate (EF zkVM IO interface).
252284 assert_eq ! (
253285 memory
254286 . read_return_value( )
255287 . expect( "public output should be readable" ) ,
256- b"cd " . to_vec( )
288+ b"abcd " . to_vec( )
257289 ) ;
258290 }
259291
260292 #[ test]
261- fn test_commit_public_output_size_exceeded ( ) {
293+ fn test_commit_public_output_empty_is_ok ( ) {
294+ let mut memory = Memory :: default ( ) ;
295+ memory
296+ . commit_public_output ( 0 , 0 )
297+ . expect ( "zero-length commit should succeed" ) ;
298+ assert ! (
299+ memory
300+ . read_return_value( )
301+ . expect( "public output should be readable" )
302+ . is_empty( )
303+ ) ;
304+ }
305+
306+ #[ test]
307+ fn test_commit_public_output_address_overflow ( ) {
308+ let mut memory = Memory :: default ( ) ;
309+ let err = memory
310+ . commit_public_output ( u64:: MAX , 2 )
311+ . expect_err ( "address overflow must error, not panic" ) ;
312+ assert ! ( matches!( err, super :: MemoryError :: AddressOverflow ) ) ;
313+ }
314+
315+ #[ test]
316+ fn test_load_bytes_huge_len_returns_alloc_error ( ) {
317+ let memory = Memory :: default ( ) ;
318+ // A multi-petabyte allocation request from a guest must fail cleanly,
319+ // not abort the host process via OOM. `addr=0` and `len=1<<50` keep
320+ // `checked_add` happy so the path reaches the allocation.
321+ let huge = 1u64 << 50 ;
322+ let err = memory
323+ . load_bytes ( 0 , huge)
324+ . expect_err ( "huge alloc must error, not abort" ) ;
325+ assert ! ( matches!( err, super :: MemoryError :: AllocationFailed ) ) ;
326+ }
327+
328+ #[ test]
329+ fn test_load_bytes_overflow_errors ( ) {
330+ let memory = Memory :: default ( ) ;
331+ let err = memory
332+ . load_bytes ( u64:: MAX , 2 )
333+ . expect_err ( "address overflow must error, not panic" ) ;
334+ assert ! ( matches!( err, super :: MemoryError :: AddressOverflow ) ) ;
335+ }
336+
337+ #[ test]
338+ fn test_commit_public_output_total_cap ( ) {
262339 let mut memory = Memory :: default ( ) ;
263- let err = memory. commit_public_output ( 0x100 , 1025 ) ;
264- assert ! ( err. is_err( ) ) ;
340+ // Seed enough source bytes for two 512 KB writes.
341+ let chunk = vec ! [ 0xAB ; 512 * 1024 ] ;
342+ memory
343+ . set_bytes_aligned ( 0x1_0000 , & chunk)
344+ . expect ( "seed should succeed" ) ;
345+
346+ memory
347+ . commit_public_output ( 0x1_0000 , 512 * 1024 )
348+ . expect ( "first 512 KB commit should succeed" ) ;
349+ memory
350+ . commit_public_output ( 0x1_0000 , 512 * 1024 )
351+ . expect ( "second 512 KB commit should succeed (total = 1 MB)" ) ;
352+
353+ // One more byte exceeds the 1 MB total cap.
354+ let err = memory. commit_public_output ( 0x1_0000 , 1 ) . unwrap_err ( ) ;
355+ assert ! ( matches!( err, super :: MemoryError :: CommitSizeExceeded ) ) ;
265356 }
266357}
0 commit comments