Skip to content

Commit

Permalink
feat: sload opcode
Browse files Browse the repository at this point in the history
  • Loading branch information
enitrat committed Oct 4, 2023
1 parent 31195a2 commit c7d9492
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 72 deletions.
9 changes: 7 additions & 2 deletions crates/evm/src/errors.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const RETURNDATA_OUT_OF_BOUNDS_ERROR: felt252 = 'KKT: ReturnDataOutOfBounds';
// JUMP
const INVALID_DESTINATION: felt252 = 'KKT: invalid JUMP destination';

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

#[derive(Drop, Copy, PartialEq)]
enum EVMError {
StackError: felt252,
Expand All @@ -22,7 +25,8 @@ enum EVMError {
ReturnDataError: felt252,
JumpError: felt252,
NotImplemented,
UnknownOpcode: u8
UnknownOpcode: u8,
SyscallFailed: felt252
}


Expand All @@ -36,7 +40,8 @@ impl EVMErrorIntoU256 of Into<EVMError, u256> {
EVMError::JumpError(error_message) => error_message.into(),
EVMError::NotImplemented => 'NotImplemented'.into(),
// TODO: refactor with dynamic strings once supported
EVMError::UnknownOpcode => 'UnknownOpcode'.into()
EVMError::UnknownOpcode => 'UnknownOpcode'.into(),
EVMError::SyscallFailed(error_message) => error_message.into()
}
}
}
19 changes: 18 additions & 1 deletion crates/evm/src/helpers.cairo
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use evm::errors::{EVMError, TYPE_CONVERSION_ERROR};
use starknet::StorageBaseAddress;
use utils::traits::Felt252TryIntoStorageBaseAddress;

trait TryIntoResult<T, U> {
fn try_into_result(self: T) -> Result<U, EVMError>;
Expand All @@ -11,7 +13,22 @@ impl U256TryIntoResultU32 of TryIntoResult<u256, u32> {
fn try_into_result(self: u256) -> Result<u32, EVMError> {
match self.try_into() {
Option::Some(value) => Result::Ok(value),
Option::None(_) => Result::Err(EVMError::TypeConversionError(TYPE_CONVERSION_ERROR))
Option::None => Result::Err(EVMError::TypeConversionError(TYPE_CONVERSION_ERROR))
}
}
}

impl U256TryIntoResultStorageBaseAddress of TryIntoResult<u256, StorageBaseAddress> {
/// Converts a u256 into a Result<u32, EVMError>
/// If the u256 is larger than MAX_U32, it returns an error.
/// Otherwise, it returns the casted value.
fn try_into_result(self: u256) -> Result<StorageBaseAddress, EVMError> {
let res_felt: felt252 = self
.try_into()
.ok_or(EVMError::TypeConversionError(TYPE_CONVERSION_ERROR))?;
let res_sba: StorageBaseAddress = res_felt
.try_into()
.ok_or(EVMError::TypeConversionError(TYPE_CONVERSION_ERROR))?;
Result::Ok(res_sba)
}
}
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
3 changes: 3 additions & 0 deletions crates/evm/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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
22 changes: 19 additions & 3 deletions crates/evm/src/stack.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
//! ```
use debug::PrintTrait;
use evm::errors::{EVMError, STACK_OVERFLOW, STACK_UNDERFLOW};
use evm::helpers::U256TryIntoResultU32;
use evm::helpers::{U256TryIntoResultU32, U256TryIntoResultStorageBaseAddress};
use nullable::{nullable_from_box, NullableTrait};
use starknet::EthAddress;
use starknet::{StorageBaseAddress, EthAddress};

use utils::constants;
use utils::i256::i256;
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 @@ -132,6 +133,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
80 changes: 80 additions & 0 deletions crates/evm/src/storage_journal.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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.
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; }
}
}
}

/// 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; }
}
}
}
}
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');
}
31 changes: 30 additions & 1 deletion crates/evm/src/tests/test_stack.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,10 @@ mod push {

#[cfg(test)]
mod pop {
use evm::errors::{EVMError, STACK_UNDERFLOW};
use evm::errors::{EVMError, STACK_UNDERFLOW, TYPE_CONVERSION_ERROR};
use starknet::storage_base_address_const;
use super::StackTrait;
use utils::traits::StorageBaseAddressPartialEq;

#[test]
#[available_gas(950000)]
Expand Down Expand Up @@ -268,6 +270,33 @@ mod pop {
'should return StackUnderflow'
);
}

#[test]
#[available_gas(2000000000)]
fn test_pop_sba_ok() {
let mut stack = StackTrait::new();
stack.push(0x01).unwrap();

let res = stack.pop_sba();
let expected_val: starknet::StorageBaseAddress = storage_base_address_const::<0x01>();
assert(res.unwrap() == expected_val, 'wrong result');
}

#[test]
#[available_gas(200000)]
fn test_pop_sba_err() {
let mut stack = StackTrait::new();
stack
.push(0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00)
.unwrap(); // Higher than max possible StorageBaseAddress value

let res = stack.pop_sba();
assert(res.is_err(), 'should return Err');
assert(
res.unwrap_err() == EVMError::TypeConversionError(TYPE_CONVERSION_ERROR),
'should ret TypeConversionError'
);
}
}

#[cfg(test)]
Expand Down
15 changes: 15 additions & 0 deletions crates/utils/src/hash.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
impl HashStateExtImpl of HashStateExtTrait<HashState> {
/// A variant of poseidon hash that computes a value that fits in a Starknet StorageBaseAddress.
#[inline(always)]
fn finalize_250(self: HashState) -> felt252 {
let r = if self.odd {
let (r, _, _) = hades_permutation(self.s0, self.s1 + 1, self.s2);
r
} else {
let (r, _, _) = hades_permutation(self.s0 + 1, self.s1, self.s2);
r
};

r & 0x3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
}
}
Loading

0 comments on commit c7d9492

Please sign in to comment.