/home/runner/actions-runner/_work/fuel-vm/fuel-vm/fuel-tx/src/contract.rs
Line | Count | Source (jump to first uncovered line) |
1 | | use crate::{ |
2 | | StorageSlot, |
3 | | Transaction, |
4 | | ValidityError, |
5 | | }; |
6 | | |
7 | | use derivative::Derivative; |
8 | | use fuel_crypto::Hasher; |
9 | | use fuel_merkle::{ |
10 | | binary::root_calculator::MerkleRootCalculator as BinaryMerkleTree, |
11 | | sparse::{ |
12 | | in_memory::MerkleTree as SparseMerkleTree, |
13 | | MerkleTreeKey, |
14 | | }, |
15 | | }; |
16 | | use fuel_types::{ |
17 | | fmt_truncated_hex, |
18 | | Bytes32, |
19 | | ContractId, |
20 | | Salt, |
21 | | }; |
22 | | |
23 | | use alloc::vec::Vec; |
24 | | use core::iter; |
25 | | |
26 | | /// The target size of Merkle tree leaves in bytes. Contract code will will be divided |
27 | | /// into chunks of this size and pushed to the Merkle tree. |
28 | | /// |
29 | | /// See https://github.com/FuelLabs/fuel-specs/blob/master/src/identifiers/contract-id.md |
30 | | const LEAF_SIZE: usize = 16 * 1024; |
31 | | /// In the event that contract code cannot be divided evenly by the `LEAF_SIZE`, the |
32 | | /// remainder must be padded to the nearest multiple of 8 bytes. Padding is achieved by |
33 | | /// repeating the `PADDING_BYTE`. |
34 | | const PADDING_BYTE: u8 = 0u8; |
35 | | const MULTIPLE: usize = 8; |
36 | | |
37 | | #[derive(Default, Derivative, Clone, PartialEq, Eq, Hash)] |
38 | 0 | #[derivative(Debug)] |
39 | 0 | #[derive( |
40 | 0 | serde::Serialize, |
41 | 0 | serde::Deserialize, |
42 | 0 | fuel_types::canonical::Deserialize, |
43 | 0 | fuel_types::canonical::Serialize, |
44 | 0 | )] |
45 | 0 | /// Deployable representation of a contract code. |
46 | 0 | pub struct Contract( |
47 | 0 | #[derivative(Debug(format_with = "fmt_truncated_hex::<16>"))] Vec<u8>, |
48 | 0 | ); |
49 | | |
50 | | impl Contract { |
51 | | /// The `ContractId` of the contract with empty bytecode, zero salt, and empty state |
52 | | /// root. |
53 | | pub const EMPTY_CONTRACT_ID: ContractId = ContractId::new([ |
54 | | 55, 187, 13, 108, 165, 51, 58, 230, 74, 109, 215, 229, 33, 69, 82, 120, 81, 4, |
55 | | 85, 54, 172, 30, 84, 115, 226, 164, 0, 99, 103, 189, 154, 243, |
56 | | ]); |
57 | | |
58 | | /// Number of bytes in the contract's bytecode |
59 | 0 | pub fn len(&self) -> usize { |
60 | 0 | self.0.len() |
61 | 0 | } |
62 | | |
63 | | /// Check if the contract's bytecode is empty |
64 | 0 | pub fn is_empty(&self) -> bool { |
65 | 0 | self.0.is_empty() |
66 | 0 | } |
67 | | |
68 | | /// Calculate the code root of the contract, using [`Self::root_from_code`]. |
69 | 1.47k | pub fn root(&self) -> Bytes32 { |
70 | 1.47k | Self::root_from_code(self) |
71 | 1.47k | } |
72 | | |
73 | | /// Calculate the code root from a contract. |
74 | | /// |
75 | | /// <https://github.com/FuelLabs/fuel-specs/blob/master/src/identifiers/contract-id.md> |
76 | 5.77k | pub fn root_from_code<B>(bytes: B) -> Bytes32 |
77 | 5.77k | where |
78 | 5.77k | B: AsRef<[u8]>, |
79 | 5.77k | { |
80 | 5.77k | let mut tree = BinaryMerkleTree::new(); |
81 | 5.77k | bytes.as_ref().chunks(LEAF_SIZE).for_each(|leaf| { |
82 | 5.77k | // If the bytecode is not a multiple of LEAF_SIZE, the final leaf |
83 | 5.77k | // should be zero-padded rounding up to the nearest multiple of 8 |
84 | 5.77k | // bytes. |
85 | 5.77k | let len = leaf.len(); |
86 | 5.77k | if len == LEAF_SIZE || len % MULTIPLE == 05.74k { Branch (86:16): [True: 26, False: 1.12k]
Branch (86:36): [True: 53, False: 1.07k]
Branch (86:16): [True: 8, False: 354]
Branch (86:36): [True: 58, False: 296]
Branch (86:16): [True: 0, False: 2.48k]
Branch (86:36): [True: 2.37k, False: 117]
Branch (86:16): [True: 0, False: 1.09k]
Branch (86:36): [True: 419, False: 678]
Branch (86:16): [True: 0, False: 634]
Branch (86:36): [True: 315, False: 319]
Branch (86:16): [True: 0, False: 46]
Branch (86:36): [True: 19, False: 27]
|
87 | 3.26k | tree.push(leaf); |
88 | 3.26k | } else { |
89 | 2.51k | let padding_size = len.next_multiple_of(MULTIPLE); |
90 | 2.51k | let mut padded_leaf = [PADDING_BYTE; LEAF_SIZE]; |
91 | 2.51k | padded_leaf[0..len].clone_from_slice(leaf); |
92 | 2.51k | tree.push(padded_leaf[..padding_size].as_ref()); |
93 | 2.51k | } |
94 | 5.77k | }); |
95 | 5.77k | |
96 | 5.77k | tree.root().into() |
97 | 5.77k | } |
98 | | |
99 | | /// Calculate the root of the initial storage slots for this contract |
100 | 1.42k | pub fn initial_state_root<'a, I>(storage_slots: I) -> Bytes32 |
101 | 1.42k | where |
102 | 1.42k | I: Iterator<Item = &'a StorageSlot>, |
103 | 1.42k | { |
104 | 1.42k | let storage_slots = storage_slots |
105 | 4.14k | .map(|slot| (*slot.key(), slot.value())) |
106 | 4.14k | .map(|(key, data)| (MerkleTreeKey::new(key), data)); |
107 | 1.42k | let root = SparseMerkleTree::root_from_set(storage_slots); |
108 | 1.42k | root.into() |
109 | 1.42k | } |
110 | | |
111 | | /// The default state root value without any entries |
112 | 44 | pub fn default_state_root() -> Bytes32 { |
113 | 44 | Self::initial_state_root(iter::empty()) |
114 | 44 | } |
115 | | |
116 | | /// Calculate and return the contract id, provided a salt, code root and state root. |
117 | | /// |
118 | | /// <https://github.com/FuelLabs/fuel-specs/blob/master/src/identifiers/contract-id.md> |
119 | 1.41k | pub fn id(&self, salt: &Salt, root: &Bytes32, state_root: &Bytes32) -> ContractId { |
120 | 1.41k | let mut hasher = Hasher::default(); |
121 | 1.41k | |
122 | 1.41k | hasher.input(ContractId::SEED); |
123 | 1.41k | hasher.input(salt); |
124 | 1.41k | hasher.input(root); |
125 | 1.41k | hasher.input(state_root); |
126 | 1.41k | |
127 | 1.41k | ContractId::from(*hasher.digest()) |
128 | 1.41k | } |
129 | | } |
130 | | |
131 | | impl From<Vec<u8>> for Contract { |
132 | 105 | fn from(c: Vec<u8>) -> Self { |
133 | 105 | Self(c) |
134 | 105 | } |
135 | | } |
136 | | |
137 | | impl From<&[u8]> for Contract { |
138 | 2.00k | fn from(c: &[u8]) -> Self { |
139 | 2.00k | Self(c.into()) |
140 | 2.00k | } |
141 | | } |
142 | | |
143 | | impl From<&mut [u8]> for Contract { |
144 | 0 | fn from(c: &mut [u8]) -> Self { |
145 | 0 | Self(c.into()) |
146 | 0 | } |
147 | | } |
148 | | |
149 | | impl From<Contract> for Vec<u8> { |
150 | 0 | fn from(c: Contract) -> Vec<u8> { |
151 | 0 | c.0 |
152 | 0 | } |
153 | | } |
154 | | |
155 | | impl AsRef<[u8]> for Contract { |
156 | 23.3k | fn as_ref(&self) -> &[u8] { |
157 | 23.3k | self.0.as_ref() |
158 | 23.3k | } |
159 | | } |
160 | | |
161 | | impl AsMut<[u8]> for Contract { |
162 | 0 | fn as_mut(&mut self) -> &mut [u8] { |
163 | 0 | self.0.as_mut() |
164 | 0 | } |
165 | | } |
166 | | |
167 | | impl TryFrom<&Transaction> for Contract { |
168 | | type Error = ValidityError; |
169 | | |
170 | 0 | fn try_from(tx: &Transaction) -> Result<Self, Self::Error> { |
171 | 0 | match tx { |
172 | 0 | Transaction::Create(create) => TryFrom::try_from(create), |
173 | | _ => { |
174 | 0 | Err(ValidityError::TransactionOutputContainsContractCreated { index: 0 }) |
175 | | } |
176 | | } |
177 | 0 | } |
178 | | } |
179 | | |
180 | | #[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] |
181 | | #[cfg(test)] |
182 | | mod tests { |
183 | | use super::*; |
184 | | use fuel_types::{ |
185 | | bytes::WORD_SIZE, |
186 | | Bytes64, |
187 | | }; |
188 | | use itertools::Itertools; |
189 | | use quickcheck_macros::quickcheck; |
190 | | use rand::{ |
191 | | rngs::StdRng, |
192 | | RngCore, |
193 | | SeedableRng, |
194 | | }; |
195 | | use rstest::rstest; |
196 | | |
197 | | // safe-guard against breaking changes to the code root calculation for valid |
198 | | // sizes of bytecode (multiples of instruction size in bytes (half-word)) |
199 | 11 | #[rstest] |
200 | 11 | fn code_root_snapshot( |
201 | 11 | #[values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100)] instructions: usize, |
202 | 11 | ) { |
203 | 11 | let mut rng = StdRng::seed_from_u64(100); |
204 | 11 | let code_len = instructions * WORD_SIZE / 2; |
205 | 11 | let mut code = alloc::vec![0u8; code_len]; |
206 | 11 | rng.fill_bytes(code.as_mut_slice()); |
207 | 11 | |
208 | 11 | // compute root |
209 | 11 | let root = Contract::root_from_code(code); |
210 | | |
211 | | // take root snapshot |
212 | | insta::with_settings!( |
213 | | {snapshot_suffix => format!("instructions-{instructions}")}, |
214 | | { |
215 | | insta::assert_debug_snapshot!(root); |
216 | | } |
217 | | ); |
218 | 11 | } |
219 | | |
220 | | // validate code_root is always equivalent to contract.root |
221 | 1 | #[quickcheck] |
222 | 100 | fn contract_root_matches_code_root(instructions: u8) -> bool { |
223 | 100 | let mut rng = StdRng::seed_from_u64(100); |
224 | 100 | let code_len = instructions as usize * WORD_SIZE / 2; |
225 | 100 | let mut code = alloc::vec![0u8; code_len]; |
226 | 100 | rng.fill_bytes(code.as_mut_slice()); |
227 | 100 | let contract = Contract::from(code.clone()); |
228 | 100 | // compute root |
229 | 100 | let code_root = Contract::root_from_code(code); |
230 | 100 | let contract_root = contract.root(); |
231 | 100 | code_root == contract_root |
232 | 100 | } |
233 | | |
234 | 2 | #[rstest] |
235 | 2 | fn state_root_snapshot( |
236 | 2 | #[values(Vec::new(), vec![Bytes64::new([1u8; 64])])] state_slot_bytes: Vec< |
237 | 2 | Bytes64, |
238 | 2 | >, |
239 | 2 | ) { |
240 | 2 | let slots: Vec<StorageSlot> = |
241 | 2 | state_slot_bytes.iter().map(Into::into).collect_vec(); |
242 | 2 | let state_root = Contract::initial_state_root(&mut slots.iter()); |
243 | | // take root snapshot |
244 | | insta::with_settings!( |
245 | | {snapshot_suffix => format!("state-root-{}", slots.len())}, |
246 | | { |
247 | | insta::assert_debug_snapshot!(state_root); |
248 | | } |
249 | | ); |
250 | 2 | } |
251 | | |
252 | | #[test] |
253 | 1 | fn default_state_root_snapshot() { |
254 | 1 | let default_root = Contract::default_state_root(); |
255 | 1 | insta::assert_debug_snapshot!(default_root); |
256 | 1 | } |
257 | | |
258 | | #[test] |
259 | 1 | fn multi_leaf_state_root_snapshot() { |
260 | 1 | let mut rng = StdRng::seed_from_u64(0xF00D); |
261 | 1 | // 5 full leaves and a partial 6th leaf with 4 bytes of data |
262 | 1 | let partial_leaf_size = 4; |
263 | 1 | let code_len = 5 * LEAF_SIZE + partial_leaf_size; |
264 | 1 | let mut code = alloc::vec![0u8; code_len]; |
265 | 1 | rng.fill_bytes(code.as_mut_slice()); |
266 | 1 | |
267 | 1 | // compute root |
268 | 1 | let root = Contract::root_from_code(code); |
269 | | |
270 | | // take root snapshot |
271 | | insta::with_settings!( |
272 | | {snapshot_suffix => "multi-leaf-state-root"}, |
273 | | { |
274 | | insta::assert_debug_snapshot!(root); |
275 | | } |
276 | | ); |
277 | 1 | } |
278 | | |
279 | 7 | #[rstest] |
280 | | #[case(0)] |
281 | | #[case(1)] |
282 | | #[case(8)] |
283 | | #[case(500)] |
284 | | #[case(1000)] |
285 | | #[case(1024)] |
286 | | #[case(1025)] |
287 | 7 | fn partial_leaf_state_root(#[case] partial_leaf_size: usize) { |
288 | 7 | let mut rng = StdRng::seed_from_u64(0xF00D); |
289 | 7 | let code_len = partial_leaf_size; |
290 | 7 | let mut code = alloc::vec![0u8; code_len]; |
291 | 7 | rng.fill_bytes(code.as_mut_slice()); |
292 | 7 | |
293 | 7 | // Compute root |
294 | 7 | let root = Contract::root_from_code(code.clone()); |
295 | | |
296 | | // Compute expected root |
297 | 7 | let expected_root = { |
298 | 7 | let mut tree = BinaryMerkleTree::new(); |
299 | 7 | |
300 | 7 | // Push partial leaf with manual padding. |
301 | 7 | // We start by generating an n-byte array, where n is the code |
302 | 7 | // length rounded to the nearest multiple of 8, and each byte is the |
303 | 7 | // PADDING_BYTE by default. The leaf is generated by copying the |
304 | 7 | // remaining data bytes into the start of this array. |
305 | 7 | let sz = partial_leaf_size.next_multiple_of(8); |
306 | 7 | if sz > 0 { Branch (306:16): [True: 6, False: 1]
|
307 | 6 | let mut padded_leaf = vec![PADDING_BYTE; sz]; |
308 | 6 | padded_leaf[0..code_len].clone_from_slice(&code); |
309 | 6 | tree.push(&padded_leaf); |
310 | 6 | }1 |
311 | 7 | tree.root().into() |
312 | 7 | }; |
313 | 7 | |
314 | 7 | assert_eq!(root, expected_root); |
315 | 7 | } |
316 | | |
317 | 7 | #[rstest] |
318 | | #[case(0)] |
319 | | #[case(1)] |
320 | | #[case(8)] |
321 | | #[case(500)] |
322 | | #[case(1000)] |
323 | | #[case(1024)] |
324 | | #[case(1025)] |
325 | 7 | fn multi_leaf_state_root(#[case] partial_leaf_size: usize) { |
326 | 7 | let mut rng = StdRng::seed_from_u64(0xF00D); |
327 | 7 | // 3 full leaves and a partial 4th leaf |
328 | 7 | let code_len = 3 * LEAF_SIZE + partial_leaf_size; |
329 | 7 | let mut code = alloc::vec![0u8; code_len]; |
330 | 7 | rng.fill_bytes(code.as_mut_slice()); |
331 | 7 | |
332 | 7 | // Compute root |
333 | 7 | let root = Contract::root_from_code(code.clone()); |
334 | | |
335 | | // Compute expected root |
336 | 7 | let expected_root = { |
337 | 7 | let mut tree = BinaryMerkleTree::new(); |
338 | 7 | |
339 | 7 | let leaves = code.chunks(LEAF_SIZE).collect::<Vec<_>>(); |
340 | 7 | tree.push(leaves[0]); |
341 | 7 | tree.push(leaves[1]); |
342 | 7 | tree.push(leaves[2]); |
343 | 7 | |
344 | 7 | // Push partial leaf with manual padding. |
345 | 7 | // We start by generating an n-byte array, where n is the code |
346 | 7 | // length rounded to the nearest multiple of 8, and each byte is the |
347 | 7 | // PADDING_BYTE by default. The leaf is generated by copying the |
348 | 7 | // remaining data bytes into the start of this array. |
349 | 7 | let sz = partial_leaf_size.next_multiple_of(8); |
350 | 7 | if sz > 0 { Branch (350:16): [True: 6, False: 1]
|
351 | 6 | let mut padded_leaf = vec![PADDING_BYTE; sz]; |
352 | 6 | padded_leaf[0..partial_leaf_size].clone_from_slice(leaves[3]); |
353 | 6 | tree.push(&padded_leaf); |
354 | 6 | }1 |
355 | 7 | tree.root().into() |
356 | 7 | }; |
357 | 7 | |
358 | 7 | assert_eq!(root, expected_root); |
359 | 7 | } |
360 | | |
361 | | #[test] |
362 | 1 | fn empty_contract_id() { |
363 | 1 | let contract = Contract::from(vec![]); |
364 | 1 | let salt = Salt::zeroed(); |
365 | 1 | let root = contract.root(); |
366 | 1 | let state_root = Contract::default_state_root(); |
367 | 1 | |
368 | 1 | let calculated_id = contract.id(&salt, &root, &state_root); |
369 | 1 | assert_eq!(calculated_id, Contract::EMPTY_CONTRACT_ID) |
370 | 1 | } |
371 | | } |