Skip to content

Commit

Permalink
feat: add function for generating bytecode identifier (#6674)
Browse files Browse the repository at this point in the history
## Description

Depends on #6522
Related FuelLabs/forc.pub#16

Adds a function, `get_bytecode_id` that generates a sha256 hash of the
bytecode with the configurables section of the bytecode removed. This
will be used for indexing and lookups of the corresponding ABIs for
contracts, predicates, and scripts in the package registry (forc.pub).

## Checklist

- [ ] I have linked to any relevant issues.
- [ ] I have commented my code, particularly in hard-to-understand
areas.
- [ ] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [ ] If my change requires substantial documentation changes, I have
[requested support from the DevRel
team](https://github.com/FuelLabs/devrel-requests/issues/new/choose)
- [ ] I have added tests that prove my fix is effective or that my
feature works.
- [ ] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [ ] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [ ] I have requested a review from the relevant team or maintainers.

---------

Co-authored-by: Vaivaswatha Nagaraj <[email protected]>
Co-authored-by: IGI-111 <[email protected]>
Co-authored-by: Marcos Henrich <[email protected]>
Co-authored-by: Joshua Batty <[email protected]>
  • Loading branch information
5 people authored Nov 19, 2024
1 parent b823691 commit 6245ee6
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 12 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions forc-util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ clap = { workspace = true, features = ["cargo", "derive", "env"] }
dirs.workspace = true
fd-lock.workspace = true
forc-tracing.workspace = true
fuel-asm.workspace = true
fuel-tx = { workspace = true, optional = true }
hex.workspace = true
paste.workspace = true
regex.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
serial_test.workspace = true
sha2.workspace = true
sway-core.workspace = true
sway-error.workspace = true
sway-types.workspace = true
Expand Down
170 changes: 170 additions & 0 deletions forc-util/src/bytecode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use anyhow::anyhow;
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;

// The index of the beginning of the half-word (4 bytes) that contains the configurables section offset.
const CONFIGURABLES_OFFSET_INSTR_LO: usize = 4;
// The index of the end of the half-word (4 bytes) that contains the configurables section offset.
const CONFIGURABLES_OFFSET_INSTR_HI: usize = 5;
// The count of the beginning half-words that contain the configurables section offset.
const CONFIGURABLES_OFFSET_PREAMBLE: usize = CONFIGURABLES_OFFSET_INSTR_HI + 1;

/// A tuple of an instruction and its corresponding bytes. Useful when needing to access the raw bytes
/// of an instruction that is parsed as [fuel_asm::InvalidOpcode], such as metadata in the preamble.
pub type InstructionWithBytes = (
Result<fuel_asm::Instruction, fuel_asm::InvalidOpcode>,
Vec<u8>,
);

/// An iterator over each [fuel_asm::Instruction] or [fuel_asm::InvalidOpcode] with its corresponding bytes.
pub struct InstructionWithBytesIterator {
buf_reader: BufReader<File>,
}

impl InstructionWithBytesIterator {
/// Return a new iterator for each instruction parsed from raw bytes.
pub fn new(buf_reader: BufReader<File>) -> Self {
InstructionWithBytesIterator { buf_reader }
}
}

impl Iterator for InstructionWithBytesIterator {
type Item = InstructionWithBytes;

fn next(&mut self) -> Option<InstructionWithBytes> {
let mut buffer = [0; fuel_asm::Instruction::SIZE];
// Read the next instruction into the buffer
match self.buf_reader.read_exact(&mut buffer) {
Ok(_) => fuel_asm::from_bytes(buffer)
.next()
.map(|inst| (inst, buffer.to_vec())),
Err(_) => None,
}
}
}

/// Parses a bytecode file into an iterator of instructions and their corresponding bytes.
pub fn parse_bytecode_to_instructions<P>(path: P) -> anyhow::Result<InstructionWithBytesIterator>
where
P: AsRef<Path> + Clone,
{
let f = File::open(path.clone())
.map_err(|_| anyhow!("{}: file not found", path.as_ref().to_string_lossy()))?;
let buf_reader = BufReader::new(f);

Ok(InstructionWithBytesIterator::new(buf_reader))
}

/// Gets the bytecode ID from a bytecode file. The bytecode ID is the hash of the bytecode after removing the
/// condigurables section, if any.
pub fn get_bytecode_id<P>(path: P) -> anyhow::Result<String>
where
P: AsRef<Path> + Clone,
{
let mut instructions = parse_bytecode_to_instructions(path.clone())?;

// Collect the first six instructions into a temporary vector
let mut first_six_instructions = Vec::with_capacity(CONFIGURABLES_OFFSET_PREAMBLE);
for _ in 0..CONFIGURABLES_OFFSET_PREAMBLE {
if let Some(instruction) = instructions.next() {
first_six_instructions.push(instruction);
} else {
return Err(anyhow!("Incomplete bytecode"));
}
}

let (lo_instr, low_raw) = &first_six_instructions[CONFIGURABLES_OFFSET_INSTR_LO];
let (hi_instr, hi_raw) = &first_six_instructions[CONFIGURABLES_OFFSET_INSTR_HI];

if let Err(fuel_asm::InvalidOpcode) = lo_instr {
if let Err(fuel_asm::InvalidOpcode) = hi_instr {
// Now assemble the configurables offset.
let configurables_offset = usize::from_be_bytes([
low_raw[0], low_raw[1], low_raw[2], low_raw[3], hi_raw[0], hi_raw[1], hi_raw[2],
hi_raw[3],
]);

// Hash the first six instructions
let mut hasher = Sha256::new();
for (_, raw) in first_six_instructions {
hasher.update(raw);
}

// Continue hashing the remaining instructions up to the configurables section offset.
instructions
.take(
configurables_offset / fuel_asm::Instruction::SIZE
- CONFIGURABLES_OFFSET_PREAMBLE,
) // Minus 6 because we already hashed the first six
.for_each(|(_, raw)| {
hasher.update(raw);
});

let hash_result = hasher.finalize();
let bytecode_id = format!("{:x}", hash_result);
return Ok(bytecode_id);
}
}

Err(anyhow!("Configurables section offset not found"))
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_get_bytecode_id_happy() {
let bytecode_id: String =
get_bytecode_id("tests/fixtures/bytecode/debug-counter.bin").expect("bytecode id");
assert_eq!(
bytecode_id,
"e65aa988cae1041b64dc2d85e496eed0e8a1d8105133bd313c17645a1859d53b".to_string()
);

let bytecode_id =
get_bytecode_id("tests/fixtures/bytecode/release-counter.bin").expect("bytecode id");
assert_eq!(
bytecode_id,
"42ae8352cbc892d7c7621f1d6fb42b072a08ba5968508d49f54991668d4ea141".to_string()
);

let bytecode_id =
get_bytecode_id("tests/fixtures/bytecode/debug-configurable_constants.bin")
.expect("bytecode id");
assert_eq!(
bytecode_id,
"babc3d9dcac8d48dee1e5aeb3340ff098d3c1ab8b0a28341d9291d8ff757199e".to_string()
);

let bytecode_id =
get_bytecode_id("tests/fixtures/bytecode/release-configurable_constants.bin")
.expect("bytecode id");
assert_eq!(
bytecode_id,
"2adfb515b66763fd29391bdba012921d045a0be83d89be5492bcaacc429695e9".to_string()
);
}

#[test]
fn test_get_bytecode_id_missing_configurable_offset() {
let result = get_bytecode_id(
"tests/fixtures/bytecode/debug-configurable_constants-missing-offset.bin",
);
assert_eq!(
result.unwrap_err().to_string().as_str(),
"Configurables section offset not found"
);
}

#[test]
fn test_get_bytecode_id_bad_path() {
let result = get_bytecode_id("tests/fixtures/bytecode/blahblahblahblah.bin");
assert_eq!(
result.unwrap_err().to_string().as_str(),
"tests/fixtures/bytecode/blahblahblahblah.bin: file not found"
);
}
}
1 change: 1 addition & 0 deletions forc-util/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use sway_error::{
use sway_types::{LineCol, LineColRange, SourceEngine, Span};
use sway_utils::constants;

pub mod bytecode;
pub mod fs_locking;
pub mod restricted;

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
14 changes: 2 additions & 12 deletions forc/src/cli/commands/parse_bytecode.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use anyhow::anyhow;
use clap::Parser;
use forc_util::bytecode::parse_bytecode_to_instructions;
use forc_util::ForcResult;
use std::fs::{self, File};
use std::io::Read;
use term_table::row::Row;
use term_table::table_cell::{Alignment, TableCell};
use tracing::info;
Expand All @@ -21,15 +19,7 @@ pub(crate) struct Command {
}

pub(crate) fn exec(command: Command) -> ForcResult<()> {
let mut f = File::open(&command.file_path)
.map_err(|_| anyhow!("{}: file not found", command.file_path))?;
let metadata = fs::metadata(&command.file_path)
.map_err(|_| anyhow!("{}: file not found", command.file_path))?;
let mut buffer = vec![0; metadata.len() as usize];
f.read_exact(&mut buffer).expect("buffer overflow");

let instructions = fuel_asm::from_bytes(buffer.iter().cloned())
.zip(buffer.chunks(fuel_asm::Instruction::SIZE));
let instructions = parse_bytecode_to_instructions(&command.file_path)?;

let mut table = term_table::Table::new();
table.separate_rows = false;
Expand Down

0 comments on commit 6245ee6

Please sign in to comment.