Skip to content

4.5 Code Geth Transactions and EVM

A transaction roughly includes (I'm not trying to be accurate here, for correct schema see spec)

  • sender
  • receiver
  • value
  • input

When a block gets included or built, the execution engine runs the transactions and update the state. When the receiver is empty, it is likely a smart contract creation request; and the 'input' is expected to be the contract code. The 'receiver' can be either a wallet address or a contract address. If it is a contract address, the code that is deployed to the address will be executed.

Smart Contract#

Smart contract is some logic that gets executed on chain on Ethereum. Compared to other language or compute environment, smart contract has some 'irreversible' constraint, e.g. once you send the token, you cannot get it back. Also it does not have the notion of 'time' as it only relies on the clock of Ethereum, namely slot and epoch. If you have not tried writing any Solidity before, I highly recommend to follow the CryptoZombies tutorial. It gives a sense of what smart contract is.

Smart contracts written in Solidity will get compiled to bytecode that represents 'Opcode', such as ADD, SUB. This video with timestamp is also a great source in explaining how EVM Opcodes work. For this note, I will focus on how smart contracts get on to Ethereum and get executed in EVM.

Smart Contract Deployment#

After writing smart contract and testing locally, the final step is to deploy it to testnet or mainnet. Deploying smart contract is essentially making JSON-RPC calls as this example 'Deploying a contract using JSON_RPC'. The most important step is:

curl --data '{"jsonrpc":"2.0","method": "eth_sendTransaction", "params": [{"from": "0x9b1d35635cc34752ca54713bb99d38614f63c955", "gas": "0x1c31e", "data": "0x6060604052341561000f57600080fd5b60eb8061001d6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063c6888fa1146044575b600080fd5b3415604e57600080fd5b606260048080359060200190919050506078565b6040518082815260200191505060405180910390f35b60007f24abdb5865df5079dcc5ac590ff6f01d5c16edbc5fab4e195d9febd1114503da600783026040518082815260200191505060405180910390a16007820290509190505600a165627a7a7230582040383f19d9f65246752244189b02f56e8d0980ed44e7a56c0b200458caad20bb0029"}], "id": 6}' -H "Content-Type: application/json" localhost:8545
{"id":6,"jsonrpc":"2.0","result":"0xe1f3095770633ab2b18081658bad475439f6a08c902d0915903bafff06e6febf"}

where
* eth_sendTransaction is one of the JSON-RPC methods, also refer to 4.2 Code - Geth - JSON-RPC
* result is the compiled contract bytes.
* to is left out because this is a contract deployment request, not to send a token to a different account. In https://github.com/ethereum/go-ethereum/blob/v1.11.2/core/state_processor.go L:132
if msg.To() == nil {
    receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce())
}

A Smart Contract Deployment Example on Etherscan#

Take the famous Uniswap as an example, this is the address of Uniswap Router on Ethereum:

0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD

1. By copying it to Etherscan, you get to see its overview: https://etherscan.io/address/0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
2. Click on the 'Contract' tab, scroll to the bottom and look for 'Contract Creation Code'. This is the result field when making the eth_sendTransaction call to deploy the contract.
3. Click 'Decompile Bytecode' and it will show the solidity code similar to its source code.
4. Click 'Switch to Opcodes View', it will show
PUSH1 0xa0
PUSH1 0x40
...

5. Scroll back to the top and click 'txn' in the 'More Info' box, as shown in Uniswap-Etherscan.png
This is the deployment transaction of the contract.
6. Click to expand 'More Details' and look for 'Input Data'. It is the same as the 'Contract Creation Code' in step 2.

Transfer or Contract Execution in EVM#

Continuing the 4.4 Code - Geth - Block Building#Block Execution
1. https://github.com/ethereum/go-ethereum/blob/v1.11.2/core/state_processor.go, applyTransaction -> ApplyMessage(evm, msg, gp)
2. https://github.com/ethereum/go-ethereum/blob/v1.11.2/core/state_transition.go, ApplyMessage -> TransitionDb:

if contractCreation {
     ret, _, st.gas, vmerr = st.evm.Create(sender, st.data, st.gas, st.value)
} else {
     // Increment the nonce for the next transaction
     st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
     ret, st.gas, vmerr = st.evm.Call(sender, st.to(), st.data, st.gas, st.value)
}

3. https://github.com/ethereum/go-ethereum/blob/v1.11.2/core/vm/runtime/runtime.go, Create or Call
4. https://github.com/ethereum/go-ethereum/blob/v1.11.2/core/vm/evm.go, func (evm *EVM) create or func (evm *EVM) Call
Be it a wallet address or contract address, always transfer the value:
evm.Context.Transfer(evm.StateDB, caller.Address(), address, value)

But if the account is a wallet address, then it has no code, so the contract execution will be skipped:
code := evm.StateDB.GetCode(addr)
/* if the address is a wallet address */
if len(code) == 0 {
    ret, err = nil, nil // gas is unchanged
} else {
    // ...
    /* otherwise run the smart contract */
    ret, err = evm.interpreter.Run(contract, input, false)
    // ...
}

5. https://github.com/ethereum/go-ethereum/blob/v1.11.2/core/vm/interpreter.go, where the main logic is:
op = contract.GetOp(pc)
operation := in.table[op]
res, err = operation.execute(&pc, in, callContext)

Gas Estimation#

The JSON-RPC method eth_estimateGas allows users to estimate the gas cost given the transaction. Obviously, the more computation the higher gas cost. The calculation is done by calling ApplyMessage to one node. It should be very accurate and already consumes computing power, then why is it called 'estimate'? My take is that:

  • The one specific node might not be up-to-date with the latest gas cost
  • Consuming computing power of one node isn't that bad; a real transaction will be broadcasted and executed in all nodes.

zkEVM#

Refer to this article: https://linea.mirror.xyz/qD18IaQ4BROn_Y40EBMTUTdJHYghUtdECscSWyMvm8M

I include some highlights:

Meanwhile, the program’s execution trace is compiled to an “arithmetic circuit” for proving (ie. translated into a series of mathematical statements). This allows the full node to generate a zero-knowledge proof that confirms the program is executed correctly.

Connecting to the EIP-4844, Zk-rollups should be able to generate such execution proof and store it in the 'blobs_sidecar', which is not sent to the execution engine on the Layer-1.

As the proof circuit validates the computational integrity of execution, other peers on the network don’t need to re-execute the program to validate the proposed output. Nodes only need to check the zero-knowledge proofs to confirm that the zkEVM’s new state (after executing the program) is correct.

While Layer-2's saves Layer-1 by executing transactions else where, with zk it will be possible to only need 1 node to execute transactions. Therefore it can also scale Layer-1.