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