Core transaction processing logic

With lots of TODOs to fill in afterward; this is just a start.
This commit is contained in:
Daniel Lubarov 2022-07-28 15:46:36 -07:00
parent 1763b6bc37
commit cc61c7211c
21 changed files with 537 additions and 11 deletions

View File

@ -17,9 +17,6 @@ pub struct CpuColumnsView<T: Copy> {
/// Filter. 1 if the row is part of bootstrapping the kernel code, 0 otherwise.
pub is_bootstrap_kernel: T,
/// Filter. 1 if the row is part of bootstrapping a contract's code, 0 otherwise.
pub is_bootstrap_contract: T,
/// Filter. 1 if the row corresponds to a cycle of execution and 0 otherwise.
/// Lets us re-use columns in non-cycle rows.
pub is_cpu_cycle: T,

View File

@ -11,6 +11,15 @@ pub static KERNEL: Lazy<Kernel> = Lazy::new(combined_kernel);
pub(crate) fn combined_kernel() -> Kernel {
let files = vec![
include_str!("asm/core/bootloader.asm"),
include_str!("asm/core/create.asm"),
include_str!("asm/core/create_addresses.asm"),
include_str!("asm/core/intrinsic_gas.asm"),
include_str!("asm/core/nonce.asm"),
include_str!("asm/core/process_txn.asm"),
include_str!("asm/core/terminate.asm"),
include_str!("asm/core/transfer.asm"),
include_str!("asm/core/util.asm"),
include_str!("asm/curve/bn254/curve_add.asm"),
include_str!("asm/curve/bn254/curve_mul.asm"),
include_str!("asm/curve/bn254/moddiv.asm"),
@ -33,7 +42,6 @@ pub(crate) fn combined_kernel() -> Kernel {
include_str!("asm/rlp/read_to_memory.asm"),
include_str!("asm/storage/read.asm"),
include_str!("asm/storage/write.asm"),
include_str!("asm/transactions/process_normalized.asm"),
include_str!("asm/transactions/router.asm"),
include_str!("asm/transactions/type_0.asm"),
include_str!("asm/transactions/type_1.asm"),

View File

@ -0,0 +1,11 @@
// Loads some prover-provided contract code into the code segment of memory,
// then hashes the code and returns the hash.
global bootload_contract:
// stack: retdest
// TODO
// stack: code_hash, retdest
SWAP1
JUMP

View File

@ -0,0 +1,59 @@
// Handlers for call-like operations, namely CALL, CALLCODE, STATICCALL and DELEGATECALL.
// Creates a new sub context and executes the code of the given account.
global call:
// stack: gas, address, value, args_offset, args_size, ret_offset, ret_size
%address
%stack (self, gas, address, value)
// These are (should_transfer_value, value, static, gas, sender, storage, code_addr)
-> (1, value, 0, gas, self, address, address)
%jump(call_common)
// Creates a new sub context as if calling itself, but with the code of the
// given account. In particular the storage remains the same.
global call_code:
// stack: gas, address, value, args_offset, args_size, ret_offset, ret_size
%address
%stack (self, gas, address, value)
// These are (should_transfer_value, value, static, gas, sender, storage, code_addr)
-> (1, value, 0, gas, self, self, address)
%jump(call_common)
// Creates a new sub context and executes the code of the given account.
// Equivalent to CALL, except that it does not allow any state modifying
// instructions or sending ETH in the sub context. The disallowed instructions
// are CREATE, CREATE2, LOG0, LOG1, LOG2, LOG3, LOG4, SSTORE, SELFDESTRUCT and
// CALL if the value sent is not 0.
global static_all:
// stack: gas, address, args_offset, args_size, ret_offset, ret_size
%address
%stack (self, gas, address)
// These are (should_transfer_value, value, static, gas, sender, storage, code_addr)
-> (0, 0, 1, gas, self, address, address)
%jump(call_common)
// Creates a new sub context as if calling itself, but with the code of the
// given account. In particular the storage, the current sender and the current
// value remain the same.
global delegate_call:
// stack: gas, address, args_offset, args_size, ret_offset, ret_size
%address
%sender
%callvalue
%stack (self, sender, value, gas, address)
// These are (should_transfer_value, value, static, gas, sender, storage, code_addr)
-> (0, value, 0, gas, sender, self, address)
%jump(call_common)
call_common:
// stack: should_transfer_value, value, static, gas, sender, storage, code_addr, args_offset, args_size, ret_offset, ret_size
// TODO: Set all the appropriate metadata fields...
%create_context
// stack: new_ctx, after_call
// Now, switch to the new context and go to usermode with PC=0.
SET_CONTEXT
PUSH 0
EXIT_KERNEL
after_call:
// TODO: Set RETURNDATA etc.

View File

@ -0,0 +1,84 @@
// Create a new contract account with the traditional address scheme, i.e.
// address = KEC(RLP(sender, nonce))[12:]
// This can be used both for the CREATE instruction and for contract-creation
// transactions.
//
// Pre stack: CODE_ADDR, code_len, retdest
// Post stack: address
// Note: CODE_ADDR refers to a (context, segment, offset) tuple.
global create:
// stack: sender, endowment, CODE_ADDR, code_len, retdest
DUP1 %get_nonce
// stack: nonce, sender, endowment, CODE_ADDR, code_len, retdest
// Call get_create_address and have it return to create_inner.
%stack (nonce, sender)
-> (sender, nonce, create_inner, sender)
%jump(get_create_address)
// CREATE2; see EIP-1014. Address will be
// address = KEC(0xff || sender || salt || code_hash)[12:]
//
// Pre stack: sender, endowment, salt, CODE_ADDR, code_len, retdest
// Post stack: address
// Note: CODE_ADDR refers to a (context, segment, offset) tuple.
global create2:
// stack: sender, endowment, salt, CODE_ADDR, code_len, retdest
// Call get_create2_address and have it return to create_inner.
%stack (sender, endowment, salt) -> (salt, sender, endowment)
// stack: salt, sender, endowment, CODE_ADDR, code_len, retdest
DUP7 DUP7 DUP7 DUP7 // CODE_ADDR and code_len
// stack: CODE_ADDR, code_len, salt, sender, endowment, CODE_ADDR, code_len, retdest
PUSH create_inner
// stack: create_inner, CODE_ADDR, code_len, salt, sender, endowment, CODE_ADDR, code_len, retdest
SWAP5 // create_inner <-> salt
// stack: salt, CODE_ADDR, code_len, create_inner, sender, endowment, CODE_ADDR, code_len, retdest
DUP7 // sender
// stack: sender, salt, CODE_ADDR, code_len, create_inner, sender, endowment, CODE_ADDR, code_len, retdest
%jump(get_create2_address)
// Pre stack: address, sender, endowment, CODE_ADDR, code_len, retdest
// Post stack: address
// Note: CODE_ADDR refers to a (context, segment, offset) tuple.
create_inner:
// stack: address, sender, endowment, CODE_ADDR, code_len, retdest
%stack (address, sender, endowment)
-> (sender, address, endowment, sender, address)
// TODO: Need to handle insufficient balance failure.
%transfer_eth
// stack: sender, address, CODE_ADDR, code_len, retdest
%increment_nonce
// stack: address, CODE_ADDR, code_len, retdest
%create_context
// stack: new_ctx, address, CODE_ADDR, code_len, retdest
%stack (new_ctx, address, src_ctx, src_segment, src_offset, code_len)
-> (new_ctx, @SEGMENT_CODE, 0,
src_ctx, src_segment, src_offset,
code_len, run_constructor,
new_ctx, address)
%jump(memcpy)
run_constructor:
// stack: new_ctx, address, retdest
// At this point, the initialization code has been loaded.
// Save our return address in memory, so we'll be in `after_constructor`
// after the new context returns.
// Note: We can't use %mstore_context_metadata because we're writing to
// memory owned by the new context, not the current one.
%stack (new_ctx) -> (new_ctx, @SEGMENT_CONTEXT_METADATA,
@CTX_METADATA_PARENT_PC, after_constructor, new_ctx)
MSTORE_GENERAL
// stack: new_ctx, address, retdest
// Now, switch to the new context and go to usermode with PC=0.
SET_CONTEXT
// stack: (empty, since we're in the new context)
PUSH 0
EXIT_KERNEL
after_constructor:
// stack: address, retdest
// TODO: If code was returned, store it in the account.
SWAP1
JUMP

View File

@ -0,0 +1,27 @@
// Computes the address of a contract based on the conventional scheme, i.e.
// address = KEC(RLP(sender, nonce))[12:]
//
// Pre stack: sender, nonce, retdest
// Post stack: address
global get_create_address:
// stack: sender, nonce, retdest
// TODO: Replace with actual implementation.
%pop2
PUSH 123
SWAP1
JUMP
// Computes the address for a contract based on the CREATE2 rule, i.e.
// address = KEC(0xff || sender || salt || code_hash)[12:]
//
// Pre stack: sender, salt, CODE_ADDR, code_len, retdest
// Post stack: address
//
// Note: CODE_ADDR is a (context, segment, offset) tuple.
global get_create2_address:
// stack: sender, salt, CODE_ADDR, code_len, retdest
// TODO: Replace with actual implementation.
%pop6
PUSH 123
SWAP1
JUMP

View File

@ -0,0 +1,65 @@
// After the transaction data has been parsed into a normalized set of fields
// (see NormalizedTxnField), this routine processes the transaction.
global intrinsic_gas:
// stack: retdest
// Calculate the number of zero and nonzero bytes in the txn data.
PUSH 0 // zeros = 0
PUSH 0 // i = 0
count_zeros_loop:
// stack: i, zeros, retdest
DUP1
%mload_txn_field(@TXN_FIELD_DATA_LEN)
EQ
// stack: i == data.len, i, zeros, retdest
%jumpi(count_zeros_finish)
// stack: i, zeros, retdest
DUP1
%mload_kernel(@SEGMENT_TXN_DATA)
ISZERO
// stack: data[i] == 0, i, zeros
%stack (data_i_is_zero, i, zeros) -> (data_i_is_zero, zeros, i)
ADD
// stack: zeros', i, retdest
SWAP1
// stack: i, zeros', retdest
%add_const(1)
// stack: i', zeros', retdest
%jump(count_zeros_loop)
count_zeros_finish:
// stack: i, zeros, retdest
POP
// stack: zeros, retdest
DUP1
// stack: zeros, zeros, retdest
%mload_txn_field(@TXN_FIELD_DATA_LEN)
// stack: data.len, zeros, zeros, retdest
SUB
// stack: nonzeros, zeros, retdest
%mul_const(@GAS_TXDATANONZERO)
// stack: gas_nonzeros, zeros, retdest
SWAP1
%mul_const(@GAS_TXDATAZERO)
// stack: gas_zeros, gas_nonzeros, retdest
ADD
// stack: gas_txndata, retdest
%is_contract_creation
%mul_const(@GAS_TXCREATE)
// stack: gas_creation, gas_txndata, retdest
PUSH @GAS_TRANSACTION
// stack: gas_txn, gas_creation, gas_txndata, retdest
// TODO: Add num_access_list_addresses * GAS_ACCESSLISTADDRESS
// TODO: Add num_access_list_slots * GAS_ACCESSLISTSTORAGE
ADD
ADD
// stack: total_gas, retdest
SWAP1
JUMP

View File

@ -0,0 +1,28 @@
// Increment the nonce of the given account.
// Pre stack: address, retdest
// Post stack: (empty)
global get_nonce:
// stack: address, retdest
// TODO: Replace with actual implementation.
JUMP
// Convenience macro to call get_nonce and return where we left off.
%macro get_nonce
%stack (address) -> (address, %%after)
%jump(get_nonce)
%%after:
%endmacro
global increment_nonce:
// stack: address, retdest
// TODO: Replace with actual implementation.
POP
JUMP
// Convenience macro to call increment_nonce and return where we left off.
%macro increment_nonce
%stack (address) -> (address, %%after)
%jump(increment_nonce)
%%after:
%endmacro

View File

@ -0,0 +1,39 @@
// After the transaction data has been parsed into a normalized set of fields
// (see NormalizedTxnField), this routine processes the transaction.
global process_normalized_txn:
// stack: (empty)
PUSH validate
%jump(intrinsic_gas)
validate:
// stack: intrinsic_gas
// TODO: Check gas >= intrinsic_gas.
// TODO: Check sender_balance >= intrinsic_gas + value.
buy_gas:
// TODO: Deduct gas from sender (some may be refunded later).
increment_nonce:
// TODO: Increment nonce.
process_based_on_type:
%is_contract_creation
%jumpi(process_contract_creation_txn)
%jump(process_message_txn)
process_contract_creation_txn:
// stack: (empty)
// Push the code address & length onto the stack, then call `create`.
%mload_txn_field(@TXN_FIELD_DATA_LEN)
// stack: code_len
PUSH 0
// stack: code_offset, code_len
PUSH @SEGMENT_TXN_DATA
// stack: code_segment, code_offset, code_len
PUSH 0 // context
// stack: CODE_ADDR, code_len
%jump(create)
process_message_txn:
// TODO

View File

@ -0,0 +1,56 @@
// Handlers for operations which terminate the current context, namely STOP,
// RETURN, SELFDESTRUCT, REVERT, and exceptions such as stack underflow.
global stop:
// TODO: Set parent context's CTX_METADATA_RETURNDATA_SIZE to 0.
%jump(terminate_common)
global return:
// TODO: Set parent context's CTX_METADATA_RETURNDATA_SIZE.
// TODO: Copy returned memory to parent context's RETURNDATA (but not if we're returning from a constructor?)
// TODO: Copy returned memory to parent context's memory (as specified in their call instruction)
%jump(terminate_common)
global selfdestruct:
%consume_gas_const(@GAS_SELFDESTRUCT)
// TODO
%jump(terminate_common)
global revert:
// TODO
%jump(terminate_common)
// The execution is in an exceptional halt-ing state if
// - there is insufficient gas
// - the instruction is invalid
// - there are insufficient stack items
// - a JUMP/JUMPI destination is invalid
// - the new stack size would be larger than 1024, or
// - state modification is attempted during a static call
global exception:
// TODO
%jump(terminate_common)
terminate_common:
// stack: success
// We want to move the success flag from our (child) context's stack to the
// parent context's stack. We will write it to memory, specifically
// SEGMENT_KERNEL_GENERAL[0], then load it after the context switch.
PUSH 0
// stack: 0, success
%mstore_kernel(@SEGMENT_KERNEL_GENERAL)
// stack: (empty)
// Go back to the parent context.
%mload_context_metadata(@CTX_METADATA_PARENT_CONTEXT)
SET_CONTEXT
// stack: (empty)
// Load the success flag that we stored in SEGMENT_KERNEL_GENERAL[0].
PUSH 0
%mload_kernel(@SEGMENT_KERNEL_GENERAL)
// stack: success
// JUMP to the parent IP.
%mload_context_metadata(@CTX_METADATA_PARENT_PC)
JUMP

View File

@ -0,0 +1,16 @@
// Transfers some ETH from one address to another. The amount is given in wei.
// Pre stack: from, to, amount, retdest
// Post stack: (empty)
global transfer_eth:
// stack: from, to, amount, retdest
// TODO: Replace with actual implementation.
%pop3
JUMP
// Convenience macro to call transfer_eth and return where we left off.
%macro transfer_eth
%stack (from, to, amount) -> (from, to, amount, %%after)
%jump(transfer_eth)
%%after:
%endmacro

View File

@ -0,0 +1,32 @@
// Return the next context ID, and record the old context ID in the new one's
// @CTX_METADATA_PARENT_CONTEXT field. Does not actually enter the new context.
%macro create_context
%next_context_id
GET_CONTEXT
%stack (ctx, next_ctx)
-> (next_ctx, @SEGMENT_NORMALIZED_TXN, @CTX_METADATA_PARENT_CONTEXT,
ctx, next_ctx)
MSTORE_GENERAL
// stack: next_ctx
%endmacro
// Get and increment @GLOBAL_METADATA_LARGEST_CONTEXT to determine the next context ID.
%macro next_context_id
// stack: (empty)
%mload_global_metadata(@GLOBAL_METADATA_LARGEST_CONTEXT)
%add_const(1)
// stack: new_ctx
DUP1
%mstore_global_metadata(@GLOBAL_METADATA_LARGEST_CONTEXT)
// stack: new_ctx
%endmacro
// Returns whether the current transaction is a contract creation transaction.
%macro is_contract_creation
// stack: (empty)
%mload_txn_field(@TXN_FIELD_TO)
// stack: to
ISZERO
// If there is no "to" field, then this is a contract creation.
// stack: to == 0
%endmacro

View File

@ -0,0 +1,8 @@
// Computes the Keccak256 hash of some arbitrary bytes in memory.
// The given memory values should be in the range of a byte.
//
// Pre stack: ADDR, len, retdest
// Post stack: hash
global keccak_general:
// stack: ADDR, len
// TODO

View File

@ -33,3 +33,15 @@
%mload_current(@SEGMENT_CONTEXT_METADATA)
// stack: (empty)
%endmacro
%macro address
%mload_context_metadata(0) // TODO: Read proper field.
%endmacro
%macro sender
%mload_context_metadata(0) // TODO: Read proper field.
%endmacro
%macro callvalue
%mload_context_metadata(0) // TODO: Read proper field.
%endmacro

View File

@ -1,5 +0,0 @@
// After the transaction data has been parsed into a normalized set of fields
// (see TxnField), this routine processes the transaction.
global process_normalized_txn:
// TODO

View File

@ -134,7 +134,12 @@ impl<'a> Interpreter<'a> {
}
pub(crate) fn get_txn_field(&self, field: NormalizedTxnField) -> U256 {
self.memory.context_memory[0].segments[Segment::TxnFields as usize].content[field as usize]
self.memory.context_memory[0].segments[Segment::TxnFields as usize].get(field as usize)
}
pub(crate) fn set_txn_field(&mut self, field: NormalizedTxnField, value: U256) {
self.memory.context_memory[0].segments[Segment::TxnFields as usize]
.set(field as usize, value);
}
pub(crate) fn get_txn_data(&self) -> &[U256] {

View File

@ -0,0 +1,54 @@
use anyhow::Result;
use crate::cpu::kernel::aggregator::KERNEL;
use crate::cpu::kernel::interpreter::Interpreter;
#[test]
fn test_get_create_address() -> Result<()> {
let get_create_address = KERNEL.global_labels["get_create_address"];
// TODO: Replace with real data once we have a real implementation.
let retaddr = 0xdeadbeefu32.into();
let nonce = 5.into();
let sender = 0.into();
let expected_addr = 123.into();
let initial_stack = vec![retaddr, nonce, sender];
let mut interpreter = Interpreter::new_with_kernel(get_create_address, initial_stack);
interpreter.run()?;
assert_eq!(interpreter.stack(), &[expected_addr]);
Ok(())
}
#[test]
fn test_get_create2_address() -> Result<()> {
let get_create2_address = KERNEL.global_labels["get_create2_address"];
// TODO: Replace with real data once we have a real implementation.
let retaddr = 0xdeadbeefu32.into();
let code_len = 0.into();
let code_offset = 0.into();
let code_segment = 0.into();
let code_context = 0.into();
let salt = 5.into();
let sender = 0.into();
let expected_addr = 123.into();
let initial_stack = vec![
retaddr,
code_len,
code_offset,
code_segment,
code_context,
salt,
sender,
];
let mut interpreter = Interpreter::new_with_kernel(get_create2_address, initial_stack);
interpreter.run()?;
assert_eq!(interpreter.stack(), &[expected_addr]);
Ok(())
}

View File

@ -0,0 +1,27 @@
use anyhow::Result;
use crate::cpu::kernel::aggregator::KERNEL;
use crate::cpu::kernel::interpreter::Interpreter;
use crate::cpu::kernel::txn_fields::NormalizedTxnField;
const GAS_TX: u32 = 21_000;
const GAS_TXCREATE: u32 = 32_000;
#[test]
fn test_intrinsic_gas() -> Result<()> {
let intrinsic_gas = KERNEL.global_labels["intrinsic_gas"];
// Contract creation transaction.
let initial_stack = vec![0xdeadbeefu32.into()];
let mut interpreter = Interpreter::new_with_kernel(intrinsic_gas, initial_stack.clone());
interpreter.run()?;
assert_eq!(interpreter.stack(), vec![(GAS_TX + GAS_TXCREATE).into()]);
// Message transaction.
let mut interpreter = Interpreter::new_with_kernel(intrinsic_gas, initial_stack);
interpreter.set_txn_field(NormalizedTxnField::To, 123.into());
interpreter.run()?;
assert_eq!(interpreter.stack(), vec![GAS_TX.into()]);
Ok(())
}

View File

@ -0,0 +1,2 @@
mod create_addresses;
mod intrinsic_gas;

View File

@ -1,3 +1,4 @@
mod core;
mod curve_ops;
mod ecrecover;
mod exp;

View File

@ -18,7 +18,7 @@ pub(crate) enum Segment {
/// General purpose kernel memory, used by various kernel functions.
/// In general, calling a helper function can result in this memory being clobbered.
KernelGeneral = 7,
/// Contains normalized transaction fields; see `TxnField`.
/// Contains normalized transaction fields; see `NormalizedTxnField`.
TxnFields = 8,
/// Contains the data field of a transaction.
TxnData = 9,