|
| 1 | +use crate::{ |
| 2 | + context::{PublicContext, UnconstrainedContext}, |
| 3 | + history::public_storage::PublicStorageHistoricalRead, |
| 4 | + oracle, |
| 5 | +}; |
| 6 | +use dep::protocol_types::{ |
| 7 | + address::AztecAddress, block_header::BlockHeader, hash::poseidon2_hash, traits::Packable, |
| 8 | +}; |
| 9 | + |
| 10 | +/// A struct that allows for efficient reading of value `T` from public storage in private. |
| 11 | +/// |
| 12 | +/// The efficient reads are achieved by verifying large values through a single hash check |
| 13 | +/// and then proving inclusion only of the hash in public storage. This reduces the number |
| 14 | +/// of required tree inclusion proofs from `N` to 1. |
| 15 | +/// |
| 16 | +/// # Type Parameters |
| 17 | +/// - `T`: The underlying type being wrapped, must implement `Packable<N>` |
| 18 | +/// - `N`: The number of field elements required to pack values of type `T` |
| 19 | +pub struct WithHash<T, let N: u32> { |
| 20 | + value: T, |
| 21 | + packed: [Field; N], |
| 22 | + hash: Field, |
| 23 | +} |
| 24 | + |
| 25 | +impl<T, let N: u32> WithHash<T, N> |
| 26 | +where |
| 27 | + T: Packable<N> + Eq, |
| 28 | +{ |
| 29 | + pub fn new(value: T) -> Self { |
| 30 | + let packed = value.pack(); |
| 31 | + Self { value, packed, hash: poseidon2_hash(packed) } |
| 32 | + } |
| 33 | + |
| 34 | + pub fn get_value(self) -> T { |
| 35 | + self.value |
| 36 | + } |
| 37 | + |
| 38 | + pub fn get_hash(self) -> Field { |
| 39 | + self.hash |
| 40 | + } |
| 41 | + |
| 42 | + pub fn public_storage_read(context: PublicContext, storage_slot: Field) -> T { |
| 43 | + context.storage_read(storage_slot) |
| 44 | + } |
| 45 | + |
| 46 | + pub unconstrained fn unconstrained_public_storage_read( |
| 47 | + context: UnconstrainedContext, |
| 48 | + storage_slot: Field, |
| 49 | + ) -> T { |
| 50 | + context.storage_read(storage_slot) |
| 51 | + } |
| 52 | + |
| 53 | + pub fn historical_public_storage_read( |
| 54 | + header: BlockHeader, |
| 55 | + address: AztecAddress, |
| 56 | + storage_slot: Field, |
| 57 | + ) -> T { |
| 58 | + let historical_block_number = header.global_variables.block_number as u32; |
| 59 | + |
| 60 | + // We could simply produce historical inclusion proofs for each field in `packed`, but that would require one |
| 61 | + // full sibling path per storage slot (since due to kernel siloing the storage is not contiguous). Instead, we |
| 62 | + // get an oracle to provide us the values, and instead we prove inclusion of their hash, which is both a much |
| 63 | + // smaller proof (a single slot), and also independent of the size of T (except in that we need to pack and hash T). |
| 64 | + let hint = WithHash::new( |
| 65 | + /// Safety: We verify that a hash of the hint/packed data matches the stored hash. |
| 66 | + unsafe { |
| 67 | + oracle::storage::storage_read(address, storage_slot, historical_block_number) |
| 68 | + }, |
| 69 | + ); |
| 70 | + |
| 71 | + let hash = header.public_storage_historical_read(storage_slot + N as Field, address); |
| 72 | + |
| 73 | + if hash != 0 { |
| 74 | + assert_eq(hash, hint.get_hash(), "Hint values do not match hash"); |
| 75 | + } else { |
| 76 | + // The hash slot can only hold a zero if it is uninitialized. Therefore, the hints must then be zero |
| 77 | + // (i.e. the default value for public storage) as well. |
| 78 | + assert_eq( |
| 79 | + hint.get_value(), |
| 80 | + T::unpack(std::mem::zeroed()), |
| 81 | + "Non-zero hint for zero hash", |
| 82 | + ); |
| 83 | + }; |
| 84 | + |
| 85 | + hint.get_value() |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +impl<T, let N: u32> Packable<N + 1> for WithHash<T, N> |
| 90 | +where |
| 91 | + T: Packable<N>, |
| 92 | +{ |
| 93 | + fn pack(self) -> [Field; N + 1] { |
| 94 | + let mut result: [Field; N + 1] = std::mem::zeroed(); |
| 95 | + for i in 0..N { |
| 96 | + result[i] = self.packed[i]; |
| 97 | + } |
| 98 | + result[N] = self.hash; |
| 99 | + |
| 100 | + result |
| 101 | + } |
| 102 | + |
| 103 | + fn unpack(packed: [Field; N + 1]) -> Self { |
| 104 | + let mut value_packed: [Field; N] = std::mem::zeroed(); |
| 105 | + for i in 0..N { |
| 106 | + value_packed[i] = packed[i]; |
| 107 | + } |
| 108 | + let hash = packed[N]; |
| 109 | + |
| 110 | + Self { value: T::unpack(value_packed), packed: value_packed, hash } |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +mod test { |
| 115 | + use crate::{ |
| 116 | + oracle::random::random, |
| 117 | + test::{ |
| 118 | + helpers::{cheatcodes, test_environment::TestEnvironment}, |
| 119 | + mocks::mock_struct::MockStruct, |
| 120 | + }, |
| 121 | + utils::with_hash::WithHash, |
| 122 | + }; |
| 123 | + use dep::protocol_types::hash::poseidon2_hash; |
| 124 | + use dep::std::{mem, test::OracleMock}; |
| 125 | + |
| 126 | + global storage_slot: Field = 47; |
| 127 | + |
| 128 | + #[test] |
| 129 | + unconstrained fn create_and_recover() { |
| 130 | + let value = MockStruct { a: 5, b: 3 }; |
| 131 | + let value_with_hash = WithHash::new(value); |
| 132 | + let recovered = WithHash::unpack(value_with_hash.pack()); |
| 133 | + |
| 134 | + assert_eq(recovered.value, value); |
| 135 | + assert_eq(recovered.packed, value.pack()); |
| 136 | + assert_eq(recovered.hash, poseidon2_hash(value.pack())); |
| 137 | + } |
| 138 | + |
| 139 | + #[test] |
| 140 | + unconstrained fn read_uninitialized_value() { |
| 141 | + let mut env = TestEnvironment::new(); |
| 142 | + |
| 143 | + let block_header = env.private().historical_header; |
| 144 | + let address = env.contract_address(); |
| 145 | + |
| 146 | + let result = WithHash::<MockStruct, _>::historical_public_storage_read( |
| 147 | + block_header, |
| 148 | + address, |
| 149 | + storage_slot, |
| 150 | + ); |
| 151 | + |
| 152 | + // We should get zeroed value |
| 153 | + let expected: MockStruct = mem::zeroed(); |
| 154 | + assert_eq(result, expected); |
| 155 | + } |
| 156 | + |
| 157 | + #[test] |
| 158 | + unconstrained fn read_initialized_value() { |
| 159 | + let mut env = TestEnvironment::new(); |
| 160 | + |
| 161 | + let value = MockStruct { a: 5, b: 3 }; |
| 162 | + let value_with_hash = WithHash::new(value); |
| 163 | + |
| 164 | + // We write the value with hash to storage |
| 165 | + cheatcodes::direct_storage_write( |
| 166 | + env.contract_address(), |
| 167 | + storage_slot, |
| 168 | + value_with_hash.pack(), |
| 169 | + ); |
| 170 | + |
| 171 | + // We advance block by 1 because env.private() currently returns context at latest_block - 1 |
| 172 | + env.advance_block_by(1); |
| 173 | + |
| 174 | + let result = WithHash::<MockStruct, _>::historical_public_storage_read( |
| 175 | + env.private().historical_header, |
| 176 | + env.contract_address(), |
| 177 | + storage_slot, |
| 178 | + ); |
| 179 | + |
| 180 | + assert_eq(result, value); |
| 181 | + } |
| 182 | + |
| 183 | + #[test(should_fail_with = "Non-zero hint for zero hash")] |
| 184 | + unconstrained fn test_bad_hint_uninitialized_value() { |
| 185 | + let mut env = TestEnvironment::new(); |
| 186 | + |
| 187 | + env.advance_block_to(6); |
| 188 | + |
| 189 | + let value_packed = MockStruct { a: 1, b: 1 }.pack(); |
| 190 | + |
| 191 | + let block_header = env.private().historical_header; |
| 192 | + let address = env.contract_address(); |
| 193 | + |
| 194 | + // Mock the oracle to return a non-zero hint/packed value |
| 195 | + let _ = OracleMock::mock("storageRead") |
| 196 | + .with_params(( |
| 197 | + address.to_field(), storage_slot, block_header.global_variables.block_number as u32, |
| 198 | + value_packed.len(), |
| 199 | + )) |
| 200 | + .returns(value_packed) |
| 201 | + .times(1); |
| 202 | + |
| 203 | + // This should revert because the hint value is non-zero and the hash is zero (default value of storage) |
| 204 | + let _ = WithHash::<MockStruct, _>::historical_public_storage_read( |
| 205 | + block_header, |
| 206 | + address, |
| 207 | + storage_slot, |
| 208 | + ); |
| 209 | + } |
| 210 | + |
| 211 | + #[test(should_fail_with = "Hint values do not match hash")] |
| 212 | + unconstrained fn test_bad_hint_initialized_value() { |
| 213 | + let mut env = TestEnvironment::new(); |
| 214 | + |
| 215 | + let value_packed = MockStruct { a: 5, b: 3 }.pack(); |
| 216 | + |
| 217 | + // We write the value to storage |
| 218 | + cheatcodes::direct_storage_write(env.contract_address(), storage_slot, value_packed); |
| 219 | + |
| 220 | + // Now we write incorrect hash to the hash storage slot |
| 221 | + let incorrect_hash = random(); |
| 222 | + let hash_storage_slot = storage_slot + (value_packed.len() as Field); |
| 223 | + cheatcodes::direct_storage_write( |
| 224 | + env.contract_address(), |
| 225 | + hash_storage_slot, |
| 226 | + [incorrect_hash], |
| 227 | + ); |
| 228 | + |
| 229 | + // We advance block by 1 because env.private() currently returns context at latest_block - 1 |
| 230 | + env.advance_block_by(1); |
| 231 | + |
| 232 | + let _ = WithHash::<MockStruct, _>::historical_public_storage_read( |
| 233 | + env.private().historical_header, |
| 234 | + env.contract_address(), |
| 235 | + storage_slot, |
| 236 | + ); |
| 237 | + } |
| 238 | +} |
0 commit comments