Skip to content

Commit

Permalink
feat: sload opcode (#396)
Browse files Browse the repository at this point in the history
* feat: sload opcode

* tests: StorageBaseAddress partialEq

* tests: journal

* chore: fmt

* refactor: moved evm::helpers content

* chore: fmt

* chore: fix build utils

* fix: inconsistent ref error caused by generic parameters
  • Loading branch information
enitrat authored Oct 6, 2023
1 parent 4cc738e commit ea08eb8
Show file tree
Hide file tree
Showing 20 changed files with 460 additions and 92 deletions.
5 changes: 5 additions & 0 deletions crates/evm/src/errors.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const INVALID_DESTINATION: felt252 = 'KKT: invalid JUMP destination';
// EVM STATE
const WRITE_IN_STATIC_CONTEXT: felt252 = 'KKT: WriteInStaticContext';

// STARKNET_SYSCALLS
const READ_SYSCALL_FAILED: felt252 = 'KKT: read syscall failed';

#[derive(Drop, Copy, PartialEq)]
enum EVMError {
StackError: felt252,
Expand All @@ -26,6 +29,7 @@ enum EVMError {
JumpError: felt252,
NotImplemented,
UnknownOpcode: u8,
SyscallFailed: felt252,
WriteInStaticContext: felt252
}

Expand All @@ -41,6 +45,7 @@ impl EVMErrorIntoU256 of Into<EVMError, u256> {
EVMError::NotImplemented => 'NotImplemented'.into(),
// TODO: refactor with dynamic strings once supported
EVMError::UnknownOpcode => 'UnknownOpcode'.into(),
EVMError::SyscallFailed(error_message) => error_message.into(),
EVMError::WriteInStaticContext(error_message) => error_message.into(),
}
}
Expand Down
17 changes: 0 additions & 17 deletions crates/evm/src/helpers.cairo

This file was deleted.

23 changes: 21 additions & 2 deletions crates/evm/src/instructions/memory_operations.cairo
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! Stack Memory Storage and Flow Operations.
use evm::errors::{EVMError, INVALID_DESTINATION};
use evm::errors::{EVMError, INVALID_DESTINATION, READ_SYSCALL_FAILED};
use evm::machine::{Machine, MachineCurrentContextTrait};
use evm::memory::MemoryTrait;
use evm::stack::StackTrait;
use evm::storage_journal::JournalTrait;
use starknet::Store;

#[generate_trait]
impl MemoryOperation of MemoryOperationTrait {
Expand Down Expand Up @@ -140,7 +142,24 @@ impl MemoryOperation of MemoryOperationTrait {
/// Load from storage.
/// # Specification: https://www.evm.codes/#54?fork=shanghai
fn exec_sload(ref self: Machine) -> Result<(), EVMError> {
Result::Err(EVMError::NotImplemented)
let key = self.stack.pop_sba()?;
match self.storage_journal.read(key) {
Option::Some(value) => {
self.stack.push(value)?;
return Result::Ok(());
},
Option::None => {
match Store::<u256>::read(0, key) {
Result::Ok(value) => {
self.stack.push(value)?;
return Result::Ok(());
},
Result::Err(_) => {
return Result::Err(EVMError::SyscallFailed(READ_SYSCALL_FAILED));
},
}
}
}
}

/// 0x5A - GAS operation
Expand Down
6 changes: 3 additions & 3 deletions crates/evm/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ mod errors;
// Kakarot main module
mod execution;

// Helpers module
mod helpers;

// instructions module
mod instructions;

Expand All @@ -28,6 +25,9 @@ mod model;
// Stack module
mod stack;

// Storage journal module
mod storage_journal;

// tests
#[cfg(test)]
mod tests;
16 changes: 2 additions & 14 deletions crates/evm/src/machine.cairo
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
use evm::storage_journal::Journal;
use evm::{
context::{
ExecutionContext, ExecutionContextTrait, DefaultBoxExecutionContext, CallContext,
CallContextTrait, Status, Event
},
stack::{Stack, StackTrait}, memory::{Memory, MemoryTrait}
};
use starknet::{EthAddress, ContractAddress};


/// The Journal tracks the changes applied to storage during the execution of a transaction.
/// Local changes tracks the changes applied inside a single execution context.
/// Global changes tracks the changes applied in the entire transaction.
/// Upon exiting an execution context, local changes must be finalized into global changes
/// Upon exiting the transaction, global changes must be finalized into storage updates.
#[derive(Destruct, Default)]
struct Journal {
local_changes: Felt252Dict<felt252>,
local_keys: Array<felt252>,
global_changes: Felt252Dict<felt252>,
global_keys: Array<felt252>
}
use starknet::{EthAddress, ContractAddress};

#[derive(Destruct)]
struct Machine {
Expand Down
25 changes: 21 additions & 4 deletions crates/evm/src/stack.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
//! let value = stack.pop()?;
//! ```
use debug::PrintTrait;
use evm::errors::{EVMError, STACK_OVERFLOW, STACK_UNDERFLOW};
use evm::helpers::U256TryIntoResultU32;
use evm::errors::{EVMError, STACK_OVERFLOW, STACK_UNDERFLOW, TYPE_CONVERSION_ERROR};
use nullable::{nullable_from_box, NullableTrait};
use starknet::EthAddress;
use starknet::{StorageBaseAddress, EthAddress};

use utils::constants;
use utils::i256::i256;
use utils::traits::{Felt252TryIntoStorageBaseAddress, TryIntoResult};


#[derive(Destruct, Default)]
Expand All @@ -38,6 +38,7 @@ trait StackTrait {
fn pop_usize(ref self: Stack) -> Result<usize, EVMError>;
fn pop_i256(ref self: Stack) -> Result<i256, EVMError>;
fn pop_eth_address(ref self: Stack) -> Result<EthAddress, EVMError>;
fn pop_sba(ref self: Stack) -> Result<StorageBaseAddress, EVMError>;
fn pop_n(ref self: Stack, n: usize) -> Result<Array<u256>, EVMError>;
fn peek(ref self: Stack) -> Option<u256>;
fn peek_at(ref self: Stack, index: usize) -> Result<u256, EVMError>;
Expand All @@ -54,10 +55,10 @@ impl StackImpl of StackTrait {
Default::default()
}

#[inline(always)]
/// Sets the current active segment for the `Stack` instance.
/// Active segment are implementation-specific concepts that reflect
/// the execution context being currently executed.
#[inline(always)]
fn set_active_segment(ref self: Stack, active_segment: usize) {
self.active_segment = active_segment;
}
Expand Down Expand Up @@ -114,6 +115,7 @@ impl StackImpl of StackTrait {
#[inline(always)]
fn pop_usize(ref self: Stack) -> Result<usize, EVMError> {
let item: u256 = self.pop()?;
// item.try_into().ok_or(EVMError::TypeConversionError(TYPE_CONVERSION_ERROR))
let item: usize = item.try_into_result()?;
Result::Ok(item)
}
Expand All @@ -132,6 +134,21 @@ impl StackImpl of StackTrait {
Result::Ok(item)
}

/// Calls `Stack::pop` and converts it to a StorageBaseAddress
///
/// # Errors
///
/// Returns `EVMError::StackError` with appropriate message
/// In case:
/// - Stack is empty
/// - Type conversion failed
#[inline(always)]
fn pop_sba(ref self: Stack) -> Result<StorageBaseAddress, EVMError> {
let item: u256 = self.pop()?;
let item: StorageBaseAddress = item.try_into_result()?;
Result::Ok(item)
}

/// Calls `Stack::pop` and converts it to usize
///
/// # Errors
Expand Down
86 changes: 86 additions & 0 deletions crates/evm/src/storage_journal.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use nullable::{match_nullable, FromNullableResult};
use starknet::{StorageBaseAddress, Store, storage_base_address_from_felt252};
use utils::helpers::ArrayExtensionTrait;
use utils::traits::{StorageBaseAddressPartialEq, StorageBaseAddressIntoFelt252};
/// The Journal tracks the changes applied to storage during the execution of a transaction.
/// Local changes tracks the changes applied inside a single execution context.
/// Global changes tracks the changes applied in the entire transaction.
/// Upon exiting an execution context, local changes must be finalized into global changes
/// Upon exiting the transaction, global changes must be finalized into storage updates.
#[derive(Destruct, Default)]
struct Journal {
local_changes: Felt252Dict<Nullable<u256>>,
local_keys: Array<StorageBaseAddress>,
global_changes: Felt252Dict<Nullable<u256>>,
global_keys: Array<StorageBaseAddress>
}

#[generate_trait]
impl JournalImpl of JournalTrait {
/// Reads a value from the journal. Starts by looking for the value in the local changes. If the value is not found, looks for it in the global changes.
#[inline(always)]
fn read(ref self: Journal, storage_address: StorageBaseAddress) -> Option<u256> {
match match_nullable(self.local_changes.get(storage_address.into())) {
FromNullableResult::Null => {
match match_nullable(self.global_changes.get(storage_address.into())) {
FromNullableResult::Null => { Option::None },
FromNullableResult::NotNull(value) => { Option::Some(value.unbox()) }
}
},
FromNullableResult::NotNull(value) => Option::Some(value.unbox()),
}
}

/// Writes a value to the journal.
/// Values written to the journal are not written to storage until the journal is totally finalized at the end of the transaction.
#[inline(always)]
fn write(ref self: Journal, storage_address: StorageBaseAddress, value: u256) {
self.local_changes.insert(storage_address.into(), NullableTrait::new(value));
self.local_keys.append_unique(storage_address);
}

/// Finalizes the local changes in the journal by copying them to the global changes and keys.
/// Local changes are relative to a specific execution context. `finalize_local` must be called upon returning from an execution context.
/// Dropping the tracking of local keys effectively "resets" the journal,
/// without modifying the underlying dict. Reading from the journal will still
/// return the local changes first, but they will never be out of sync with global
/// changes, unless there was a modification more recently.
fn finalize_local(ref self: Journal) {
let mut local_keys = self.local_keys.span();
loop {
match local_keys.pop_front() {
Option::Some(key) => {
let key = *key;
let value = self.local_changes.get(key.into());
self.global_changes.insert(key.into(), value);
self.global_keys.append_unique(key);
},
Option::None => { break; }
}
};
self.local_keys = Default::default();
}

/// Finalizes the global changes in the journal by writing them to the storage to be stored permanently onchain.
/// Global changes are relative the the execution of an entire transaction. `finalize_global` must be called upon finishing the transaction.
fn finalize_global(ref self: Journal) {
let mut global_keys = self.global_keys.span();
loop {
match global_keys.pop_front() {
Option::Some(key) => {
let key = *key;
let value = self.global_changes.get(key.into());
match match_nullable(value) {
FromNullableResult::Null => {},
FromNullableResult::NotNull(value) => {
let value = value.unbox();
Store::write(0, key, value);
}
};
},
Option::None => { break; }
}
};
self.global_keys = Default::default();
}
}
3 changes: 3 additions & 0 deletions crates/evm/src/tests.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ mod test_memory;
#[cfg(test)]
mod test_stack;

#[cfg(test)]
mod test_storage_journal;

#[cfg(test)]
mod test_utils;
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ use evm::instructions::{MemoryOperationTrait, EnvironmentInformationTrait};
use evm::machine::{Machine, MachineCurrentContextTrait};
use evm::memory::{InternalMemoryTrait, MemoryTrait};
use evm::stack::StackTrait;
use evm::storage_journal::{JournalTrait};
use evm::tests::test_utils::{
setup_machine, setup_machine_with_bytecode, setup_machine_with_calldata, evm_address, callvalue
};
use integer::BoundedInt;

use starknet::EthAddressIntoFelt252;
use starknet::{EthAddressIntoFelt252, storage_base_address_const, Store};
use utils::helpers::{u256_to_bytes_array};
use utils::traits::{EthAddressIntoU256};
use utils::traits::{EthAddressIntoU256, StorageBaseAddressIntoU256};


#[test]
Expand Down Expand Up @@ -498,3 +499,43 @@ fn test_exec_jumpi_inside_pushn() {
assert(result.is_err(), 'invalid jump dest');
assert(result.unwrap_err() == EVMError::JumpError(INVALID_DESTINATION), 'invalid jump dest');
}

#[test]
#[available_gas(20000000)]
fn test_exec_sload_from_journal() {
// Given
let mut machine = setup_machine();
let key = storage_base_address_const::<0x10>();
let value = 0x02;
machine.storage_journal.write(key, value);

machine.stack.push(key.into());

// When
let result = machine.exec_sload();

// Then
assert(result.is_ok(), 'should have succeeded');
assert(machine.stack.len() == 1, 'stack should have one element');
assert(machine.stack.pop().unwrap() == value, 'sload failed');
}

#[test]
#[available_gas(20000000)]
fn test_exec_sload_from_storage() {
// Given
let mut machine = setup_machine();
let key = storage_base_address_const::<0x10>();
let value = 0x02;
Store::<u256>::write(0, key, value);

machine.stack.push(key.into());

// When
let result = machine.exec_sload();

// Then
assert(result.is_ok(), 'should have succeeded');
assert(machine.stack.len() == 1, 'stack should have one element');
assert(machine.stack.pop().unwrap() == value, 'sload failed');
}
1 change: 0 additions & 1 deletion crates/evm/src/tests/test_memory.cairo
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use core::dict::Felt252DictTrait;
use evm::memory::{MemoryTrait, InternalMemoryTrait, MemoryPrintTrait};
use integer::BoundedInt;
use utils::constants::{POW_2_8, POW_2_56, POW_2_64, POW_2_120};
Expand Down
Loading

0 comments on commit ea08eb8

Please sign in to comment.