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

feat(levm): opcodes revert invalid selfdestruct #946

Merged
merged 61 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
4f2c616
implementation of transaction report in execute and call
JereSalo Oct 22, 2024
e9f304f
pseudo-fix tests
JereSalo Oct 22, 2024
5f46de1
fix gas error
JereSalo Oct 22, 2024
30deafe
fix one more testt
JereSalo Oct 22, 2024
bce759d
fix one more panicking test
JereSalo Oct 22, 2024
1111e34
fix call test (chaning returndata)
JereSalo Oct 22, 2024
b0e2302
refactor of return data offset and size
JereSalo Oct 22, 2024
e40e726
refactor of return data of subcontext and fix some tests
JereSalo Oct 22, 2024
e31c4b5
fix nested call test
JereSalo Oct 22, 2024
d148400
adapt comparison with revm to new execute() interface
JereSalo Oct 22, 2024
69d63e6
change calldata for returndata in benchmarks
JereSalo Oct 22, 2024
d7358b8
change name of transaction report
JereSalo Oct 22, 2024
d1f2b33
fix clippy lint
JereSalo Oct 22, 2024
443f5f4
run cargo fmt
JereSalo Oct 22, 2024
50e89fb
add gas_refunded behavior in call (even though it is not implemented …
JereSalo Oct 22, 2024
5ff9b3f
change gas refunds logic
JereSalo Oct 22, 2024
807bc5f
run cargo fmt
JereSalo Oct 22, 2024
8091e47
change some comments
JereSalo Oct 22, 2024
a5d9b73
run cargo fmt, again
JereSalo Oct 22, 2024
49fc47b
uncomment opcodes
JereSalo Oct 23, 2024
3fae9f7
Revert "uncomment opcodes"
JereSalo Oct 23, 2024
2d50eed
uncomment opcodes
JereSalo Oct 23, 2024
fa95f81
merge branch main
JereSalo Oct 23, 2024
f624276
merge last changes
JereSalo Oct 23, 2024
2b1808b
add basic code for opcodes
JereSalo Oct 23, 2024
850dd42
change some comments
JereSalo Oct 23, 2024
0774ec2
make some changes in gas cost
JereSalo Oct 23, 2024
5d5f6a5
add test for invalid opcode
JereSalo Oct 23, 2024
3583b6d
add functionalities to revert
JereSalo Oct 23, 2024
9022e5d
fix gas consumption problem
JereSalo Oct 23, 2024
a54c3a8
change transact return type
JereSalo Oct 23, 2024
3b5a41e
add test for revert opcode
JereSalo Oct 23, 2024
7f7e43e
make little change in test
JereSalo Oct 23, 2024
1310116
move revert behavior to execute
JereSalo Oct 23, 2024
e23c962
add oneliner and run cargo fmt
JereSalo Oct 23, 2024
4b811c0
remove comment
JereSalo Oct 23, 2024
8b69f6d
add one more test for revert
JereSalo Oct 23, 2024
74ec5e5
add some comments
JereSalo Oct 23, 2024
57b7722
add little docs for Callframe
JereSalo Oct 23, 2024
aa2273c
add docs comments
JereSalo Oct 23, 2024
6037206
fix clippy lint and run cargo fmt
JereSalo Oct 23, 2024
365df7b
remove unused import
JereSalo Oct 23, 2024
16544d3
Merge branch 'levm/feat/transaction_report' into levm/feat/revert_inv…
JereSalo Oct 23, 2024
bf5b19c
merge main into branch
JereSalo Oct 24, 2024
e5d6e13
delete comment
JereSalo Oct 24, 2024
3b2ba4d
make changes to selfdestruct account management
JereSalo Oct 25, 2024
251292a
run cargo fmt
JereSalo Oct 26, 2024
3ab97ce
Merge branch 'main' into levm/feat/revert_invalid_selfdestruct
JereSalo Oct 26, 2024
8ed0b97
abstraction for restoring state
JereSalo Oct 29, 2024
cd40809
change popping in revert
JereSalo Oct 29, 2024
732f2e3
run cargo fmt
JereSalo Oct 29, 2024
6f6d60e
merge main and fix problems
JereSalo Nov 1, 2024
d50356a
merge main into branch
JereSalo Nov 1, 2024
16f5ce1
run cargo fmt
JereSalo Nov 1, 2024
a07b506
add accounts to cache in selfdestruct and fix possible issues
JereSalo Nov 1, 2024
b81d569
run cargo fmt
JereSalo Nov 1, 2024
5155c96
make some changes to call opcode
JereSalo Nov 2, 2024
f2efa72
run cargo fmt
JereSalo Nov 2, 2024
024f118
add comments clarifying state of revert
JereSalo Nov 4, 2024
796ceb1
Merge branch 'main' into levm/feat/revert_invalid_selfdestruct
JereSalo Nov 4, 2024
362ff24
fix issues with create opcode
JereSalo Nov 4, 2024
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
3 changes: 3 additions & 0 deletions crates/vm/levm/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ pub mod gas_cost {
pub const CODECOPY_DYNAMIC_BASE: U256 = U256([3, 0, 0, 0]);
pub const GASPRICE: U256 = U256([2, 0, 0, 0]);
pub const EXTCODECOPY_DYNAMIC_BASE: U256 = U256([3, 0, 0, 0]);
pub const SELFDESTRUCT_STATIC: U256 = U256([5000, 0, 0, 0]);
pub const SELFDESTRUCT_DYNAMIC: U256 = U256([25000, 0, 0, 0]);
pub const COLD_ADDRESS_ACCESS_COST: U256 = U256([2600, 0, 0, 0]);
}

// Costs in gas for call opcodes (in wei)
Expand Down
4 changes: 3 additions & 1 deletion crates/vm/levm/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ pub enum VMError {
OverflowInArithmeticOp,
FatalError,
InvalidTransaction,
RevertOpcode,
InvalidOpcode,
MissingBlobHashes,
BlobHashIndexOutOfBounds,
RevertOpcode,
SenderAccountDoesNotExist,
SenderAccountShouldNotHaveBytecode,
SenderBalanceShouldContainTransferValue,
Expand All @@ -38,6 +39,7 @@ pub enum ResultReason {
Stop,
Revert,
Return,
SelfDestruct,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
113 changes: 113 additions & 0 deletions crates/vm/levm/src/opcode_handlers/system.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
constants::{call_opcode, SUCCESS_FOR_RETURN},
errors::ResultReason,
vm::Account,
};

use super::*;
Expand Down Expand Up @@ -254,4 +255,116 @@ impl VM {
current_call_frame,
)
}

// REVERT operation
pub fn op_revert(
&mut self,
current_call_frame: &mut CallFrame,
) -> Result<OpcodeSuccess, VMError> {
// Description: Gets values from stack, calculates gas cost and sets return data.
// Returns: VMError RevertOpcode if executed correctly.
// Notes:
// The reversion of changes is made in the generic_call().
// Changes are not "reverted" if it is the first callframe, they are just not commited.

let offset = current_call_frame
.stack
.pop()?
.try_into()
.map_err(|_| VMError::VeryLargeNumber)?;

let size = current_call_frame
.stack
.pop()?
.try_into()
.map_err(|_| VMError::VeryLargeNumber)?;
JereSalo marked this conversation as resolved.
Show resolved Hide resolved

let gas_cost = current_call_frame.memory.expansion_cost(offset + size)?;

self.increase_consumed_gas(current_call_frame, gas_cost)?;

current_call_frame.returndata = current_call_frame.memory.load_range(offset, size).into();

Err(VMError::RevertOpcode)
}

/// ### INVALID operation
/// Reverts consuming all gas, no return data.
pub fn op_invalid(&mut self) -> Result<OpcodeSuccess, VMError> {
Err(VMError::InvalidOpcode)
}

// SELFDESTRUCT operation
pub fn op_selfdestruct(
&mut self,
current_call_frame: &mut CallFrame,
) -> Result<OpcodeSuccess, VMError> {
// Sends all ether in the account to the target address
// Steps:
// 1. Pop the target address from the stack
// 2. Get current account and: Store the balance in a variable, set it's balance to 0
// 3. Get the target account, checking if it is empty and if it is cold. Update gas cost accordingly.
// 4. Add the balance of the current account to the target account
// 5. Register account to be destroyed in accrued substate.

// Notes:
// If context is Static, return error.
// If executed in the same transaction a contract was created, the current account is registered to be destroyed
if current_call_frame.is_static {
return Err(VMError::OpcodeNotAllowedInStaticContext);
}

// Gas costs variables
let static_gas_cost = gas_cost::SELFDESTRUCT_STATIC;
let dynamic_gas_cost = gas_cost::SELFDESTRUCT_DYNAMIC;
let _cold_gas_cost = gas_cost::COLD_ADDRESS_ACCESS_COST;
let mut gas_cost = static_gas_cost; // This will be updated later

// 1. Pop the target address from the stack
let target_address = Address::from_low_u64_be(current_call_frame.stack.pop()?.low_u64());

// 2. Get current account and: Store the balance in a variable, set it's balance to 0
let current_account_balance = self
.db
.accounts
.get(&current_call_frame.to)
.unwrap()
.balance;
self.db
.accounts
.get_mut(&current_call_frame.to)
.unwrap()
.balance = U256::zero();
Copy link
Contributor

@ilitteri ilitteri Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle these unwraps

Copy link
Contributor Author

@JereSalo JereSalo Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the new PR that implements Database changes the db works differently and if an account "doesn't exist" then it will return an empty account so there will be no need to handle this kind of errors. Maybe after merging that one to main I can make changes to this PR so that it fits the new database implementation.
Also, I will have to access Cache first, and if data is not cached then I will make a request to the database. That will also let me implement gas cost for cold and warm addresses.


// 3 & 4. Get target account and add the balance of the current account to it
// TODO: If address is cold, there is an additional cost of 2600. AFAIK accessList has not been implemented yet.
if self.db.account_is_empty(&target_address) {
gas_cost += dynamic_gas_cost;
self.db.accounts.insert(
target_address,
Account::default().with_balance(current_account_balance),
);
} else {
let target_account = self.db.accounts.get_mut(&target_address).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.


target_account.balance += current_account_balance;
}

// 5. Register account to be destroyed in accrued substate IF executed in the same transaction a contract was created
// This is temporary because it is not necessarily right but we should just check if the current account was created in this transaction.
if self
.accrued_substate
.created_contract_addresses
.contains(&current_call_frame.to)
{
self.accrued_substate
.self_destruct_set
.insert(current_call_frame.to);
}
// Those accounts should be destroyed at the end of the transaction.

self.increase_consumed_gas(current_call_frame, gas_cost)?;

Ok(OpcodeSuccess::Result(ResultReason::SelfDestruct))
}
}
9 changes: 6 additions & 3 deletions crates/vm/levm/src/opcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ pub enum Opcode {
DELEGATECALL = 0xF4,
CREATE2 = 0xF5,
STATICCALL = 0xFA,
// REVERT = 0xFD,
// INVALID = 0xFE,
// SELFDESTRUCT = 0xFF,
REVERT = 0xFD,
INVALID = 0xFE,
SELFDESTRUCT = 0xFF,
}

impl Copy for Opcode {}
Expand Down Expand Up @@ -321,6 +321,9 @@ impl From<u8> for Opcode {
0xF5 => Opcode::CREATE2,
0xF4 => Opcode::DELEGATECALL,
0xFA => Opcode::STATICCALL,
0xFD => Opcode::REVERT,
0xFE => Opcode::INVALID,
0xFF => Opcode::SELFDESTRUCT,
_ => panic!("Unknown opcode: 0x{:02X}", byte),
}
}
Expand Down
12 changes: 6 additions & 6 deletions crates/vm/levm/src/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ pub enum Operation {
DelegateCall,
Create2,
StaticCall,
// Revert,
// Invalid,
// SelfDestruct,
Revert,
Invalid,
SelfDestruct,
}

impl Operation {
Expand Down Expand Up @@ -202,9 +202,9 @@ impl Operation {
Operation::DelegateCall => Bytes::copy_from_slice(&[Opcode::DELEGATECALL as u8]),
Operation::Create2 => Bytes::copy_from_slice(&[Opcode::CREATE2 as u8]),
Operation::StaticCall => Bytes::copy_from_slice(&[Opcode::STATICCALL as u8]),
// Operation::Revert => Bytes::copy_from_slice(&[Opcode::REVERT as u8]),
// Operation::Invalid => Bytes::copy_from_slice(&[Opcode::INVALID as u8]),
// Operation::SelfDestruct => Bytes::copy_from_slice(&[Opcode::SELFDESTRUCT as u8]),
Operation::Revert => Bytes::copy_from_slice(&[Opcode::REVERT as u8]),
Operation::Invalid => Bytes::copy_from_slice(&[Opcode::INVALID as u8]),
Operation::SelfDestruct => Bytes::copy_from_slice(&[Opcode::SELFDESTRUCT as u8]),
}
}
}
39 changes: 27 additions & 12 deletions crates/vm/levm/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl Account {

pub type Storage = HashMap<U256, H256>;

#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Db {
pub accounts: HashMap<Address, Account>,
// contracts: HashMap<B256, Bytecode>,
Expand Down Expand Up @@ -149,12 +149,18 @@ impl Db {

Ok(self.accounts.get(address).unwrap())
}

pub fn account_is_empty(&self, address: &Address) -> bool {
!self.accounts.contains_key(address)
}
}

#[derive(Debug, Clone, Default)]
// TODO: https://github.com/lambdaclass/ethereum_rust/issues/604
pub struct Substate {
pub warm_addresses: HashSet<Address>,
pub created_contract_addresses: HashSet<Address>, // Maybe not necessary
pub self_destruct_set: HashSet<Address>,
}

#[derive(Debug, Default, Clone)]
Expand Down Expand Up @@ -273,10 +279,13 @@ impl VM {
}

pub fn execute(&mut self, current_call_frame: &mut CallFrame) -> TransactionReport {
// let mut current_call_frame = self
// .call_frames
// .pop()
// .expect("Fatal Error: This should not happen"); // if this happens during execution, we are cooked 💀
// Backup of Database, Substate and Gas Refunds if sub-context is reverted
let (backup_db, backup_substate, backup_refunded_gas) = (
self.db.clone(),
self.accrued_substate.clone(),
self.env.refunded_gas,
);

loop {
let opcode = current_call_frame.next_opcode().unwrap_or(Opcode::STOP);
let op_result: Result<OpcodeSuccess, VMError> = match opcode {
Expand Down Expand Up @@ -372,6 +381,10 @@ impl VM {
Opcode::EXTCODESIZE => self.op_extcodesize(current_call_frame),
Opcode::EXTCODECOPY => self.op_extcodecopy(current_call_frame),
Opcode::EXTCODEHASH => self.op_extcodehash(current_call_frame),
Opcode::REVERT => self.op_revert(current_call_frame),
Opcode::INVALID => self.op_invalid(),
Opcode::SELFDESTRUCT => self.op_selfdestruct(current_call_frame),

_ => Err(VMError::OpcodeNotFound),
};

Expand All @@ -394,19 +407,23 @@ impl VM {
Err(error) => {
self.call_frames.push(current_call_frame.clone());

// CONSUME ALL GAS UNLESS THE ERROR IS FROM REVERT OPCODE
// Unless error is from Revert opcode, all gas is consumed
if error != VMError::RevertOpcode {
let left_gas = current_call_frame.gas_limit - current_call_frame.gas_used;
current_call_frame.gas_used += left_gas;
self.env.consumed_gas += left_gas;
}

// Restore previous state
(self.accrued_substate, self.db, self.env.refunded_gas) =
(backup_substate, backup_db, backup_refunded_gas);
JereSalo marked this conversation as resolved.
Show resolved Hide resolved

return TransactionReport {
result: TxResult::Revert(error),
new_state: self.db.accounts.clone(),
gas_used: current_call_frame.gas_used.low_u64(),
gas_refunded: self.env.refunded_gas.low_u64(),
output: current_call_frame.returndata.clone(),
output: current_call_frame.returndata.clone(), // Bytes::new() if error is not RevertOpcode
logs: current_call_frame.logs.clone(),
created_address: None,
};
Expand Down Expand Up @@ -536,7 +553,7 @@ impl VM {
// self.call_frames.push(new_call_frame.clone());
let tx_report = self.execute(&mut new_call_frame);

current_call_frame.gas_used += tx_report.gas_used.into(); // We add the gas used by the sub-context to the current one after it's execution.
current_call_frame.gas_used += tx_report.gas_used.into(); // Add gas used by the sub-context to the current one after it's execution.
current_call_frame.logs.extend(tx_report.logs);
current_call_frame
.memory
Expand All @@ -550,10 +567,8 @@ impl VM {
.stack
.push(U256::from(SUCCESS_FOR_CALL))?;
}
TxResult::Revert(_error) => {
// Behavior for revert between contexts goes here if necessary
// It is also possible to differentiate between RevertOpcode error and other kinds of revert.

TxResult::Revert(_) => {
// Push 0 to stack
current_call_frame.stack.push(U256::from(REVERT_FOR_CALL))?;
}
}
Expand Down
66 changes: 66 additions & 0 deletions crates/vm/levm/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4340,3 +4340,69 @@ fn extcodehash_non_existing_account() {
);
assert_eq!(vm.env.consumed_gas, 23603.into());
}

#[test]
fn invalid_opcode() {
let operations = [Operation::Invalid, Operation::Stop];

let mut vm = new_vm_with_ops(&operations);

let mut current_call_frame = vm.call_frames.pop().unwrap();
let tx_report = vm.execute(&mut current_call_frame);

assert!(matches!(
tx_report.result,
TxResult::Revert(VMError::InvalidOpcode)
));
}

// Revert Opcode has correct output and result
#[test]
fn revert_opcode() {
let ops = vec![
Operation::Push((32, U256::from(0xA))), // value
Operation::Push((32, U256::from(0xFF))), // offset
Operation::Mstore,
Operation::Push((32, U256::from(32))), // size
Operation::Push((32, U256::from(0xFF))), // offset
Operation::Revert,
];

let mut vm = new_vm_with_ops(&ops);

let mut current_call_frame = vm.call_frames.pop().unwrap();
let tx_report = vm.execute(&mut current_call_frame);

assert_eq!(U256::from_big_endian(&tx_report.output), U256::from(0xA));
assert!(matches!(
tx_report.result,
TxResult::Revert(VMError::RevertOpcode)
));
}

// Store something in the database, then revert. Database should be like it was before the store.
#[test]
fn revert_sstore() {
let key = U256::from(80);
let value = U256::from(100);
let sender_address = Address::from_low_u64_be(3000);
let operations = vec![
Operation::Push((1, value)),
Operation::Push((1, key)),
Operation::Sstore,
Operation::Revert,
];

let mut vm = new_vm_with_ops(&operations);
vm.current_call_frame_mut().code_address = sender_address;
vm.db.accounts.insert(sender_address, Account::default());

let mut current_call_frame = vm.call_frames.pop().unwrap();

// Db state before the SSTORE
let db_backup = vm.db.clone();

vm.execute(&mut current_call_frame);

assert_eq!(vm.db, db_backup);
}
Loading