[Note: This lecture is based on material from the official ethereum developer documentation, the website solidity-by-example, the blog post of Preethi Kasireddy.] and the two books "Mastering Ethereum" and "The Senior Solidity Engineer's"
In the last lecture we looked at the problem of common coin. Common coin is important because it can let us solve problems like leader election. Leader election is a building block for constructing blockchains.
We looked at centralized trusted sources of randomness like NIST random beacons.
We built a common coin protocol using the commit and reveal method.
Reminder about the commit and reveal method: It works in two phases.
The common randomness would then be the xor of all random values revealed. However, there was a problem. A party can bias the coin by having the ability to not reveal their committed value. To force everyone to reveal their committed value, we proposed using economic incentives. Also, we noted that ethereum uses this method for its consensus algorithm.
We also talked about a solution that involves Multi-Party-Computation MPC. In that solution, the group can reveal the committed value on behalf of the party, even if the party refuses to reveal its committed value.
In the lab yesterday, we introduced secret sharing and MPC. In the rest of the semester, we will further expand on MPC. However, for this lecture, we will step back and look at ethereum.
The Ethereum blockchain is essentially a global computer where everyone agrees on the state of this computer. The ethereum state can only be changed by users issuing transactions.
[Source: Preethi Kasireddy's blog post]
The global state is replicated on all nodes that are in the ethereum network. Just like bitcoin transactions, ethereum transactions are grouped into blocks and broadcasted into the network. Any person can use the global computer by issuing transactions and paying the required fees in ether. Every few seconds a leader gets elected, that leader chooses what transactions goes in the next block.
[Source: Preethi Kasireddy's blog post]
Just like bitcoin, each block references the hash of the previous block.
In comparison to Bitcoin, Ethereum shares several similarities such as a peer-to-peer network that connects users, a consensus algorithm for synchronization of state updates that is Byzantine fault-tolerant, and the utilization of cryptographic primitives like digital signatures and hashes. Additionally, Ethereum also has its own digital currency, known as ether.
However, Ethereum's primary objective is not to function as a digital currency payment network, and ether is not intended to be a conventional currency. Instead, ether is designed to be a utility currency that pays for the use of the Ethereum world computer.
In contrast to Bitcoin, Ethereum has a Turing Complete language and a complex full virtual machine. It is not restricted to a limited scripting language like Bitcoin's Script. Ethereum can operate as a general-purpose computer and execute code with more complexity. Bitcoin's Script language, on the other hand, is deliberately limited to simple true/false evaluations of spending conditions.
Transactions within each block are ordered in a strict manner. All network participants have a consensus on the precise number and history of blocks and work towards consolidating current live transaction requests into the subsequent block.
After a block is assembled by a validator or leader who is randomly selected, it is transmitted to the entire network. Each node adds this block to the end of its blockchain, and a new validator is picked to construct the following block. Ethereum's "proof-of-stake" protocol specifies the exact process of block assembly and the commitment/consensus process.
[Source: Ethereum docs]
Note that the state is not actually included in the block. Only a commitment to the state is included in the block (subsequently the blockchain).
Ethereum elects a random leader to pick the next block using a common coin protocol. They use the commit and reveal method, they punish any bad behavior economically.
In more details:
You can think of Ethereum as a state machine. Ethereum's state is a large data structure which holds not only all accounts and balances, but a machine state, which can change from block to block according to a pre-defined set of rules, and which can execute arbitrary machine code. The specific rules of changing state from block to block are defined by the EVM.
[Source Ethereum docs]
Every transaction causes the EVM to run which causes the global state to change. The order of the transaction is very important! The EVM is single threaded.
The EVM executes as a stack machine with a depth of 1024 items. Each item is a 256-bits (mainly to facilitate native hashing and elliptic curve operations). The EVM has powerful opcode like jump opertations that allows it to read from anywhere in the stack (in contrast to bitcoin that doesn't have this). This extra power makes the EVM to be turing complete. A list of the available opcodes can be find here.
The EVM stores all in-memory values on a stack, and has several addressable data components:
Why can't someone run an endless loop and by that do a denial of service attack? or store very big things on ethereum?
Each operation in ethereum costs gas. Different operations have different prices. The sender of the transaction must pay for running each opration. In case they run out of gas, the transaction would not be executed and the state would roll back to the previous state. The sender must buy gas and estimate before hand how much gas it will cost to run the transaction.
The global State of Ethereum is made of many accounts. Each account has a state associated with it and a 20-byte address.
Ethereum has two account types:
Both account types have the ability to:
Source: Ethereum docs
Difference between Contract accounts and Externally-owned account (EOA):
[Source: Preethi Kasireddy's blog post]
Smart contracts are just contract accounts that has been deployed on the ethereum network.
To deploy a contract, an Externally-owned account (EOA) should upload the opcodes as part of a deployment transaction. An account is then created in the global state and the code is stored there. Once deployed, the code of a smart contract cannot change. Unlike with traditional software, the only way to modify a smart contract is to deploy a new instance.
Subsequent calls by an Externally-owned account (EOA) to the smart contract will make the EVM load the code and it's proper state and execute the proper code.
Note that a smart contract can call another smart contract.
While it is possible to write EVM bytecodes, most people use high level languages. The high level languages are then compiled to EVM bytecode.
Ethereum has many high level pragramming languages:
In this lecture, we are going to focus on Solidity because it is the most used. Solidity is an object oriented imperative programming language with a syntax similar to Python and C++.
Source solidity-by-example
Reading and Writing to a State Variable
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract SimpleStorage {
// State variable to store a number
uint public num;
// You need to send a transaction to write to a state variable.
function set(uint _num) public {
num = _num;
}
// view declares that no state will be changed.
// public is a visibility modifier, that makes the function
// accessible/callable from both inside and outside the contract.
// This means that the function can be accessed by the contract itself,
// as well as by external accounts,
// such as other contracts or user-owned accounts.
// You can read from a state variable without sending a transaction.
function get() public view returns (uint) {
return num;
}
}
Data Locations - Storage, Memory and Calldata
Variables are declared as either storage, memory or calldata to explicitly specify the location of the data.
storage - variable is a state variable (store on blockchain)
memory - variable is in memory and it exists while a function is being called
calldata - special data location that contains function arguments
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract DataLocations {
uint[] public arr;
mapping(uint => address) map;
struct MyStruct {
uint foo;
}
mapping(uint => MyStruct) myStructs;
function f() public {
// call _f with state variables
_f(arr, map, myStructs[1]);
// get a struct from a mapping
MyStruct storage myStruct = myStructs[1];
// create a struct in memory
MyStruct memory myMemStruct = MyStruct(0);
}
function _f(
uint[] storage _arr,
mapping(uint => address) storage _map,
MyStruct storage _myStruct
) internal {
// do something with storage variables
}
// You can return memory variables
function g(uint[] memory _arr) public returns (uint[] memory) {
// do something with memory array
}
function h(uint[] calldata _arr) external {
// do something with calldata array
}
}
ERC20 is a standard interface for fungible tokens on the Ethereum blockchain. It is a set of rules that define how a token contract should behave and interact with other contracts and wallets on the Ethereum network.
// Here is the interface for ERC20.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/token/ERC20/IERC20.sol
interface IERC20 {
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address recipient, uint amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint amount) external returns (bool);
function transferFrom(
address sender,
address recipient,
uint amount
) external returns (bool);
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
}
Note that the external keyword is a visibility modifier that can be applied to functions and state variables.
When a function is declared as external, it can only be called from outside the contract, and not from within the contract or any derived contracts. This means that the function can only be accessed by external accounts, such as other contracts or user-owned accounts, and not by the contract itself.
An example token implementing ERC20:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "./IERC20.sol";
// The word is for doing inheritance
contract ERC20 is IERC20 {
uint public totalSupply;
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;
string public name = "Solidity by Example";
string public symbol = "SOLBYEX";
uint8 public decimals = 18;
function transfer(address recipient, uint amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
function approve(address spender, uint amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(
address sender,
address recipient,
uint amount
) external returns (bool) {
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}
function mint(uint amount) external {
balanceOf[msg.sender] += amount;
totalSupply += amount;
emit Transfer(address(0), msg.sender, amount);
}
function burn(uint amount) external {
balanceOf[msg.sender] -= amount;
totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
}
ERC721 is a standard interface for non-fungible tokens on the Ethereum blockchain. Each token is unique, each token has a unique id and has an owner. This is reflected by the interfact functions:
interface IERC721 is IERC165 {
function balanceOf(address owner) external view returns (uint balance);
//notice the
function ownerOf(uint tokenId) external view returns (address owner);
function safeTransferFrom(address from, address to, uint tokenId) external;
function safeTransferFrom(
address from,
address to,
uint tokenId,
bytes calldata data
) external;
function transferFrom(address from, address to, uint tokenId) external;
function approve(address to, uint tokenId) external;
function getApproved(uint tokenId) external view returns (address operator);
function setApprovalForAll(address operator, bool _approved) external;
function isApprovedForAll(
address owner,
address operator
) external view returns (bool);
}