forc_util/
bytecode.rs

1use anyhow::anyhow;
2use sha2::{Digest, Sha256};
3use std::fs::File;
4use std::io::{BufReader, Read};
5use std::path::Path;
6
7// The index of the beginning of the half-word (4 bytes) that contains the configurables section offset.
8const CONFIGURABLES_OFFSET_INSTR_LO: usize = 4;
9// The index of the end of the half-word (4 bytes) that contains the configurables section offset.
10const CONFIGURABLES_OFFSET_INSTR_HI: usize = 5;
11// The count of the beginning half-words that contain the configurables section offset.
12const CONFIGURABLES_OFFSET_PREAMBLE: usize = CONFIGURABLES_OFFSET_INSTR_HI + 1;
13
14/// A tuple of an instruction and its corresponding bytes. Useful when needing to access the raw bytes
15/// of an instruction that is parsed as [fuel_asm::InvalidOpcode], such as metadata in the preamble.
16pub type InstructionWithBytes = (
17    Result<fuel_asm::Instruction, fuel_asm::InvalidOpcode>,
18    Vec<u8>,
19);
20
21/// An iterator over each [fuel_asm::Instruction] or [fuel_asm::InvalidOpcode] with its corresponding bytes.
22pub struct InstructionWithBytesIterator {
23    buf_reader: BufReader<File>,
24}
25
26impl InstructionWithBytesIterator {
27    /// Return a new iterator for each instruction parsed from raw bytes.
28    pub fn new(buf_reader: BufReader<File>) -> Self {
29        InstructionWithBytesIterator { buf_reader }
30    }
31}
32
33impl Iterator for InstructionWithBytesIterator {
34    type Item = InstructionWithBytes;
35
36    fn next(&mut self) -> Option<InstructionWithBytes> {
37        let mut buffer = [0; fuel_asm::Instruction::SIZE];
38        // Read the next instruction into the buffer
39        match self.buf_reader.read_exact(&mut buffer) {
40            Ok(_) => fuel_asm::from_bytes(buffer)
41                .next()
42                .map(|inst| (inst, buffer.to_vec())),
43            Err(_) => None,
44        }
45    }
46}
47
48/// Parses a bytecode file into an iterator of instructions and their corresponding bytes.
49pub fn parse_bytecode_to_instructions<P>(path: P) -> anyhow::Result<InstructionWithBytesIterator>
50where
51    P: AsRef<Path> + Clone,
52{
53    let f = File::open(path.clone())
54        .map_err(|_| anyhow!("{}: file not found", path.as_ref().to_string_lossy()))?;
55    let buf_reader = BufReader::new(f);
56
57    Ok(InstructionWithBytesIterator::new(buf_reader))
58}
59
60/// Gets the bytecode ID from a bytecode file. The bytecode ID is the hash of the bytecode after removing the
61/// condigurables section, if any.
62pub fn get_bytecode_id<P>(path: P) -> anyhow::Result<String>
63where
64    P: AsRef<Path> + Clone,
65{
66    let mut instructions = parse_bytecode_to_instructions(path.clone())?;
67
68    // Collect the first six instructions into a temporary vector
69    let mut first_six_instructions = Vec::with_capacity(CONFIGURABLES_OFFSET_PREAMBLE);
70    for _ in 0..CONFIGURABLES_OFFSET_PREAMBLE {
71        if let Some(instruction) = instructions.next() {
72            first_six_instructions.push(instruction);
73        } else {
74            return Err(anyhow!("Incomplete bytecode"));
75        }
76    }
77
78    let (lo_instr, low_raw) = &first_six_instructions[CONFIGURABLES_OFFSET_INSTR_LO];
79    let (hi_instr, hi_raw) = &first_six_instructions[CONFIGURABLES_OFFSET_INSTR_HI];
80
81    if let Err(fuel_asm::InvalidOpcode) = lo_instr {
82        if let Err(fuel_asm::InvalidOpcode) = hi_instr {
83            // Now assemble the configurables offset.
84            let configurables_offset = usize::from_be_bytes([
85                low_raw[0], low_raw[1], low_raw[2], low_raw[3], hi_raw[0], hi_raw[1], hi_raw[2],
86                hi_raw[3],
87            ]);
88
89            // Hash the first six instructions
90            let mut hasher = Sha256::new();
91            for (_, raw) in first_six_instructions {
92                hasher.update(raw);
93            }
94
95            // Continue hashing the remaining instructions up to the configurables section offset.
96            instructions
97                .take(
98                    configurables_offset / fuel_asm::Instruction::SIZE
99                        - CONFIGURABLES_OFFSET_PREAMBLE,
100                ) // Minus 6 because we already hashed the first six
101                .for_each(|(_, raw)| {
102                    hasher.update(raw);
103                });
104
105            let hash_result = hasher.finalize();
106            let bytecode_id = format!("{:x}", hash_result);
107            return Ok(bytecode_id);
108        }
109    }
110
111    Err(anyhow!("Configurables section offset not found"))
112}
113
114#[cfg(test)]
115mod test {
116    use super::*;
117
118    #[test]
119    fn test_get_bytecode_id_happy() {
120        // These binary files were generated from `examples/configurable_constants` and `examples/counter`
121        // using `forc build` and `forc build --release` respectively.
122        let bytecode_id: String =
123            get_bytecode_id("tests/fixtures/bytecode/debug-counter.bin").expect("bytecode id");
124        assert_eq!(
125            bytecode_id,
126            "e65aa988cae1041b64dc2d85e496eed0e8a1d8105133bd313c17645a1859d53b".to_string()
127        );
128
129        let bytecode_id =
130            get_bytecode_id("tests/fixtures/bytecode/release-counter.bin").expect("bytecode id");
131        assert_eq!(
132            bytecode_id,
133            "42ae8352cbc892d7c7621f1d6fb42b072a08ba5968508d49f54991668d4ea141".to_string()
134        );
135
136        let bytecode_id =
137            get_bytecode_id("tests/fixtures/bytecode/debug-configurable_constants.bin")
138                .expect("bytecode id");
139        assert_eq!(
140            bytecode_id,
141            "babc3d9dcac8d48dee1e5aeb3340ff098d3c1ab8b0a28341d9291d8ff757199e".to_string()
142        );
143
144        let bytecode_id =
145            get_bytecode_id("tests/fixtures/bytecode/release-configurable_constants.bin")
146                .expect("bytecode id");
147        assert_eq!(
148            bytecode_id,
149            "2adfb515b66763fd29391bdba012921d045a0be83d89be5492bcaacc429695e9".to_string()
150        );
151    }
152
153    #[test]
154    fn test_get_bytecode_id_missing_configurable_offset() {
155        // This bytecode file was generated from `examples/configurable_constants` using an older version of the
156        // compiler that did not include the configurables section offset in the preamble.
157        let result = get_bytecode_id(
158            "tests/fixtures/bytecode/debug-configurable_constants-missing-offset.bin",
159        );
160        assert_eq!(
161            result.unwrap_err().to_string().as_str(),
162            "Configurables section offset not found"
163        );
164    }
165
166    #[test]
167    fn test_get_bytecode_id_bad_path() {
168        let result = get_bytecode_id("tests/fixtures/bytecode/blahblahblahblah.bin");
169        assert_eq!(
170            result.unwrap_err().to_string().as_str(),
171            "tests/fixtures/bytecode/blahblahblahblah.bin: file not found"
172        );
173    }
174}