Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(vm): space efficient opcode implementations #481

Merged
merged 2 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

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

19 changes: 7 additions & 12 deletions crates/cfg/src/core/graph.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use alloy::primitives::U256;
use eyre::{OptionExt, Result};
use heimdall_common::utils::strings::encode_hex_reduced;
use heimdall_vm::ext::exec::VMTrace;
use heimdall_vm::{
core::opcodes::{OpCodeInfo, JUMPDEST},
ext::exec::VMTrace,
};
use petgraph::{matrix_graph::NodeIndex, Graph};

/// convert a symbolic execution [`VMTrace`] into a [`Graph`] of blocks, illustrating the
Expand All @@ -18,12 +21,7 @@ pub fn build_cfg(

// add the current operations to the cfg
for operation in &vm_trace.operations {
let opcode_name = operation
.last_instruction
.opcode_details
.as_ref()
.ok_or_eyre("failed to get opcode details for instruction")?
.name;
let opcode_name = OpCodeInfo::from(operation.last_instruction.opcode).name();

let assembly = format!(
"{} {} {}",
Expand Down Expand Up @@ -63,11 +61,8 @@ pub fn build_cfg(
.first()
.ok_or_eyre("failed to get first operation")?
.last_instruction
.opcode_details
.as_ref()
.ok_or_eyre("failed to get opcode details")?
.name ==
"JUMPDEST",
.opcode ==
JUMPDEST,
)?;
}

Expand Down
19 changes: 10 additions & 9 deletions crates/decompile/src/utils/heuristics/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use alloy::primitives::U256;
use eyre::eyre;
use heimdall_common::utils::strings::find_balanced_encapsulator;
use heimdall_vm::core::{
opcodes::{OpCodeInfo, CALLDATALOAD, ISZERO},
types::{byte_size_to_type, convert_bitmask},
vm::State,
};
Expand Down Expand Up @@ -57,7 +58,7 @@ pub fn argument_heuristic(
// if this is a bitwise mask operation on CALLDATALOAD, we can use it to determine the
// size (and consequently type) of the variable
if let Some(calldataload_op) =
state.last_instruction.input_operations.iter().find(|op| op.opcode.code == 0x35)
state.last_instruction.input_operations.iter().find(|op| op.opcode == CALLDATALOAD)
{
// this is a bitwise mask, we can use it to determine the size of the variable
let (mask_size_bytes, _potential_types) = convert_bitmask(&state.last_instruction);
Expand All @@ -71,7 +72,7 @@ pub fn argument_heuristic(
debug!(
"instruction {} ({}) indicates argument {} is masked to {} bytes",
state.last_instruction.instruction,
state.last_instruction.opcode_details.as_ref().expect("impossible").name,
OpCodeInfo::from(state.last_instruction.opcode).name(),
arg_index,
mask_size_bytes
);
Expand Down Expand Up @@ -120,22 +121,22 @@ pub fn argument_heuristic(
}

// if the any input op is ISZERO(x), this is a boolean return
if return_memory_operations.iter().any(|x| x.operation.opcode.name == "ISZERO") {
if return_memory_operations.iter().any(|x| x.operation.opcode == ISZERO) {
function.returns = Some(String::from("bool"));
}
// if the input op is any of the following, it is a uint256 return
// this is because these push numeric values onto the stack
else if return_memory_operations.iter().any(|x| {
[0x31, 0x34, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x58, 0x5a]
.contains(&x.operation.opcode.code)
.contains(&x.operation.opcode)
}) {
function.returns = Some(String::from("uint256"));
}
// if the input op is any of the following, it is an address return
// this is because these push address values onto the stack
else if return_memory_operations
.iter()
.any(|x| [0x30, 0x32, 0x33, 0x41].contains(&x.operation.opcode.code))
.any(|x| [0x30, 0x32, 0x33, 0x41].contains(&x.operation.opcode))
{
function.returns = Some(String::from("address"));
}
Expand Down Expand Up @@ -215,7 +216,7 @@ pub fn argument_heuristic(
debug!(
"instruction {} ({}) indicates argument {} may be a numeric type",
state.last_instruction.instruction,
state.last_instruction.opcode_details.as_ref().expect("impossible").name,
OpCodeInfo::from(state.last_instruction.opcode).name(),
arg_index
);

Expand All @@ -238,7 +239,7 @@ pub fn argument_heuristic(
debug!(
"instruction {} ({}) indicates argument {} may be a bytes type",
state.last_instruction.instruction,
state.last_instruction.opcode_details.as_ref().expect("impossible").name,
OpCodeInfo::from(state.last_instruction.opcode).name(),
arg_index
);

Expand All @@ -250,7 +251,7 @@ pub fn argument_heuristic(
0x15 => {
// if this is a boolean check on CALLDATALOAD, we can add boolean to the potential types
if let Some(calldataload_op) =
state.last_instruction.input_operations.iter().find(|op| op.opcode.code == 0x35)
state.last_instruction.input_operations.iter().find(|op| op.opcode == CALLDATALOAD)
{
// yulify the calldataload operation, and find the associated argument index
// this MUST exist, as we have already inserted it in the CALLDATALOAD heuristic
Expand All @@ -261,7 +262,7 @@ pub fn argument_heuristic(
debug!(
"instruction {} ({}) indicates argument {} may be a boolean",
state.last_instruction.instruction,
state.last_instruction.opcode_details.clone().expect("impossible").name,
OpCodeInfo::from(state.last_instruction.opcode).name(),
arg_index
);

Expand Down
27 changes: 9 additions & 18 deletions crates/decompile/src/utils/heuristics/modifiers.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use eyre::eyre;
use heimdall_vm::core::{
opcodes::{WrappedInput, WrappedOpcode},
vm::State,
use heimdall_vm::{
core::{
opcodes::{OpCodeInfo, JUMPI},
vm::State,
},
w_callvalue, w_iszero,
};
use tracing::debug;

Expand All @@ -27,12 +29,7 @@ pub fn modifier_heuristic(
state: &State,
_: &mut AnalyzerState,
) -> Result<(), Error> {
let opcode_name = state
.last_instruction
.opcode_details
.as_ref()
.ok_or(Error::Eyre(eyre!("opcode_details is None")))?
.name;
let opcode_name = OpCodeInfo::from(state.last_instruction.opcode).name();

// if any instruction is non-pure, the function is non-pure
if function.pure && NON_PURE_OPCODES.contains(&state.last_instruction.opcode) {
Expand All @@ -54,15 +51,9 @@ pub fn modifier_heuristic(

// if the instruction is a JUMPI with non-zero CALLVALUE requirement, the function is
// non-payable exactly: ISZERO(CALLVALUE())
// TODO: in the future, i want a way to abstract this to a more general form. maybe with
// macros(?). i.e. `iszero!(callvalue!())`
if function.payable &&
state.last_instruction.opcode == 0x57 &&
state.last_instruction.input_operations[1] ==
WrappedOpcode::new(
0x15,
vec![WrappedInput::Opcode(WrappedOpcode::new(0x34, vec![]))],
)
state.last_instruction.opcode == JUMPI &&
state.last_instruction.input_operations[1] == w_iszero!(w_callvalue!())
{
debug!(
"conditional at instruction {} indicates a non-payable function",
Expand Down
9 changes: 2 additions & 7 deletions crates/decompile/src/utils/heuristics/solidity.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use alloy::primitives::U256;
use alloy_dyn_abi::{DynSolType, DynSolValue};
use heimdall_common::utils::strings::encode_hex_reduced;
use heimdall_vm::core::vm::State;
use heimdall_vm::core::{opcodes::OpCodeInfo, vm::State};

use crate::{
core::analyze::AnalyzerState,
Expand Down Expand Up @@ -188,12 +188,7 @@ pub fn solidity_heuristic(
function.logic.push(format!(
"(bool success, bytes memory ret0) = address({}).{}{}(abi.encode({}));",
address,
instruction
.opcode_details
.as_ref()
.expect("impossible")
.name
.to_lowercase(),
OpCodeInfo::from(instruction.opcode).name().to_lowercase(),
modifier,
calldata
.iter()
Expand Down
6 changes: 3 additions & 3 deletions crates/decompile/src/utils/heuristics/yul.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use heimdall_common::utils::strings::encode_hex_reduced;
use heimdall_vm::core::vm::State;
use heimdall_vm::core::{opcodes::OpCodeInfo, vm::State};

use crate::{
core::analyze::AnalyzerState,
Expand All @@ -24,7 +24,7 @@ pub fn yul_heuristic(
function.memory.insert(key, StorageFrame { operation });
function.logic.push(format!(
"{}({}, {})",
instruction.opcode_details.as_ref().expect("impossible").name.to_lowercase(),
OpCodeInfo::from(instruction.opcode).name().to_lowercase(),
encode_hex_reduced(key),
instruction.input_operations[1].yulify()
));
Expand Down Expand Up @@ -80,7 +80,7 @@ pub fn yul_heuristic(
0xff | 0xA0 | 0xA1 | 0xA2 | 0xA3 | 0xA4 => {
function.logic.push(format!(
"{}({})",
instruction.opcode_details.as_ref().expect("impossible").name.to_lowercase(),
OpCodeInfo::from(instruction.opcode).name().to_lowercase(),
instruction
.input_operations
.iter()
Expand Down
10 changes: 5 additions & 5 deletions crates/disassemble/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::time::Instant;
use crate::{error::Error, interfaces::DisassemblerArgs};
use eyre::eyre;
use heimdall_common::utils::strings::encode_hex;
use heimdall_vm::core::opcodes::Opcode;
use heimdall_vm::core::opcodes::OpCodeInfo;
use tracing::{debug, info};

pub async fn disassemble(args: DisassemblerArgs) -> Result<String, Error> {
Expand All @@ -21,13 +21,13 @@ pub async fn disassemble(args: DisassemblerArgs) -> Result<String, Error> {
// iterate over the bytecode, disassembling each instruction
let start_disassemble_time = Instant::now();
while program_counter < contract_bytecode.len() {
let operation = Opcode::new(contract_bytecode[program_counter]);
let opcode = contract_bytecode[program_counter];
let mut pushed_bytes = String::new();

// handle PUSH0 -> PUSH32, which require us to push the next N bytes
// onto the stack
if operation.code >= 0x5f && operation.code <= 0x7f {
let byte_count_to_push: u8 = operation.code - 0x5f;
if (0x5f..=0x7f).contains(&opcode) {
let byte_count_to_push: u8 = opcode - 0x5f;
pushed_bytes = match contract_bytecode
.get(program_counter + 1..program_counter + 1 + byte_count_to_push as usize)
{
Expand All @@ -45,7 +45,7 @@ pub async fn disassemble(args: DisassemblerArgs) -> Result<String, Error> {
} else {
format!("{:06x}", program_counter)
},
operation.name,
OpCodeInfo::from(opcode).name(),
pushed_bytes
)
.as_str(),
Expand Down
7 changes: 6 additions & 1 deletion crates/vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ thiserror = "1.0.50"
tracing = "0.1.40"
eyre = "0.6.12"
heimdall-common.workspace = true
alloy = { version = "0.1.3", features = ["full", "rpc-types-debug", "rpc-types-trace"] }
alloy = { version = "0.1.3", features = [
"full",
"rpc-types-debug",
"rpc-types-trace",
] }
hashbrown = "0.14.5"
paste = "1.0.15"

[features]
step-tracing = []
Expand Down
Loading
Loading