MCPcopy Index your code
hub / github.com/ddimaria/rust-blockchain-tutorial

github.com/ddimaria/rust-blockchain-tutorial @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
208 symbols 522 edges 35 files 39 documented · 19%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Rust Blockchain Tutorial

WORK IN PROGRESS

This repo is designed to train Rust developers on intermediate and advanced Rust development and help understand the primary concepts in Ethereum blockchain development.

Roadmap

  • [x] Ethereum Types
  • [x] Cryptography Primitives
  • [x] Chain Node
  • [x] Web3 Client
  • [x] WASM/WASI VM for Contract Execution (wasmtime)
  • [ ] Rust Smart Contracts
  • [x] Base Implementation
  • [ ] Fungible
  • [ ] Non Fungible
  • [ ] Multi Asset
  • [ ] P2P Networking between Nodes (libp2p)
  • [ ] PoS Consensus
  • [x] Persistent Disk Chain State (RocksDB)
  • [ ] Full Tutorial
  • [ ] CI

Table of Contents

Introduction

When I first entered the crypto space, I had never used Rust and was unfamiliar with blockchain technology, let alone any knowledge of Ethereum concepts. This is the tutorial I wish I had back then, and will hopefully guide Rust developers along their blockchain journey.

While the concepts explored here are based on Ethereum, there are many instances where they diverge from it. For example, structs are simplified to just show general concepts. Different hashing and consensus algorithims are implemented. The most divergent area are smart contracts. We'll explore Rust-based smart contracts that run on a WASM virtual machine. I went in this direction to keep the language choice homogenious. The overall approach is similar to Solidity, though the implementation is very different.

Ethereum Primitives

Accounts

In Ethereum, Accounts are either Externally Owned Accounts or Contract Accounts. Addresses are hex encoded: 0x71562b71999873DB5b286dF957af199Ec94617F7.

type Account = ethereum_types::Address;

For a given address, data associated with the account is stored on chain:

struct AccountData {
    nonce: u64,
    balance: u64,
    code_hash: Option<Bytes>,
}

impl AccountData {
    fn is_contract(&self) -> bool {
        self.code_hash.is_some()
    }
}

Externally Owned Accounts are simply a public address. This address is a 20 byte hash (H160), and is created by applying a hash function on the public key, taking the last 20 bytes. This is how we create an Ethereum Account in Rust:

use crypto::{keypair, public_key_address};

let (private_key, public_key) = keypair();
let address = public_key_address(&public_key);

fn public_key_address(key: &PublicKey) -> H160 {
    let public_key = key.serialize_uncompressed();
    let hash = hash(&public_key[1..]);

    Address::from_slice(&hash[12..])
}

Public keys are not stored on the chain. Since we can't derive the public key from the hash, the public key is not known until a signed transaction is validated. We'll dig a bit more into this in the Transaction section.

Contract Accounts are also just an address, but have a code hash associated with them. A contract's address is created by encoding the sender's address and their current nonce. This encoding is then hashed using a hash function, taking the last 20 bytes. This process is similiar to the Externally Owned Account creation, but the input is an encoded tuple.

use crypto::{to_address};
use web3::web3;

let account = MY_ACCOUNT_ADDRESS;
let web3 = web3::Web3::new("http://127.0.0.1:8545")?;
let nonce = web3().get_transaction_count(account).await.unwrap();
let serialized: Vec<u8> = bincode::serialize(&(account, nonce)).unwrap();
let contract_address = to_address(&serialized).unwrap();

It's important to note that addresses (accounts) are iniatiated outside of a blockchain. They can be generated in many ways, though the most common is to use a wallet. In our examples, we'll sign them offline using the provided tools in the crypto crate. Accounts are stored on the chain when they are used for the first time.

Accounts are also deterministic. That is, given the same inputs, the same address is always generated.

Nonce

Accounts have an associated nonce. A nonce is an acronym for "number only used once" and is a counter of the number of processed transactions for a given account. It is to be incremented every time a new transaction is submitted. User's must keep track of their own nonces, though wallet providers can do this as well. Anyone can query the blockchain for current nonce of an account (EOA and contract), which can be helpful for determining the next nonce to use.

The main purpose of a nonce is to make a data structure unique, so that each data structure is explicit regarding otherwise identical data being effectively different. We'll discuss nonces more when breaking down transactions and how blockchain nodes use them to preserve the order of processing submitted transactions.

Transactions

Transactions are the heart of a blockchain. Without them, the chain's state would remain unchanged. Transactions drive state changes. They are submitted externally owned accounts only (i.e. not a contract account).

pub struct Transaction {
    pub from: Address,
    pub to: Option<Address>,
    pub hash: Option<H256>,
    pub nonce: Option<U256>,
    pub value: U256,
    pub data: Option<Bytes>,
    pub gas: U256,
    pub gas_price: U256,
}

While the Transaction data structure has much more fields in Ethereum than shown above, the data subset we're using is the minimum needed to understand transactions.

  • The from portion of a transaction identifies the transaction sender. This account must already exist on the blockchain.
  • The to attribute represents the receiver of the value transferred in a transaction. It can also be a contract address, where code is executed. It is optional because it is left empty (or zero or null) to signify a transaction that deploys a contract.
  • A hash attribute contains the hash of the transaction. It's optional so that it can be calculated after the other values in the transaction are populated.
  • The nonce is the sender's next account nonce. It is the existing account nonce incremented by one. Leaving this blank will let the blockchain autoincrement on behlaf of the transaction.
  • value indicates the amount of coin to transfer from the sender to the recipient. This number can be zero for non-value-transferring transactions.
  • The data attribute can hold various pieces of data. When deploying a contract, it holds bytes of the assembled contract code. When executing a function on a contract, it holds the function name and parameters. It can also be any piece of data that the sender wants to include in the transaction.
  • gas is the total number of units that the sender is offering to pay for the transaction. We'll discuss this in more detail later.
  • The gas_price is the amount of coin (eth in Ethereum) to be paid for each unit of gas.

Kinds of Transactions

There are 3 ways that transaction can be used:


pub enum TransactionKind {
    Regular(Address, Address, U256),
    ContractDeployment(Address, Bytes),
    ContractExecution(Address, Address, Bytes),
}
  • Regular transactions are ones where value is transferred from one account to another.
  • Contract deployment transactions are used to deploy contract code to the blockchain and are without a 'to' address, where the data field is used for the contract code.
  • Contract execution transactions interact with a deployed smart contract. In this case, 'to' address is the smart contract address.

The type of transaction is derived from the values in the transaction:

fn kind(self) -> Result<TransactionKind> {
    match (self.from, self.to, self.data) {
        (from, Some(to), None) => Ok(TransactionKind::Regular(from, to, self.value)),
        (from, None, Some(data)) => Ok(TransactionKind::ContractDeployment(from, data)),
        (from, Some(to), Some(data)) => Ok(TransactionKind::ContractExecution(from, to, data)),
        _ => Err(TypeError::InvalidTransaction("kind".into())),
    }
}

Transaction Hashes

Once a transaction data structure is filled in, the hash can be calculated:

let serialized = bincode::serialize(&transaction)?;
let hash: H256 = hash(&serialized).into();

We first encode/serialize the transaction and then apply a hashing function. To keep things simple, we're using Bincode to serialize and compress the data to a binary format throughout this blockchain. Ethereum uses RLP Encoding for most of it's encoding/serialization.

Blocks

Blocks essentially containers for validated transactions. Once a set of transactions are validated, they are ready to be added to a block.

struct Block {
    number: U64,
    hash: Option<H256>,
    parent_hash: H256,
    transactions: Vec<Transaction>,
    transactions_root: H256,
    state_root: H256,
}

A Block number is an incremented unsigned integer. All blocks are in order, and there are no missing blocks. Blocks start at zero (called the Genesis Block). Similar to transactions, blocks have a hash of their values:

let serialized = bincode::serialize(&block)?;
let hash: H256 = hash(&serialized).into();
block.hash = Some(hash);

The parent_hash is the hash of the previous block. This links blocks together and are critical in blockchain validation. All transactions are within the block and are needed for the block verification process.

The transaction_root is the Merkle Root of all of the transactions in the block:

fn to_trie(transactions: &[Transaction]) -> Result<EthTrie<MemoryDB>> {
    let memdb = Arc::new(MemoryDB::new(true));
    let mut trie = EthTrie::new(memdb);

    transactions.iter().try_for_each(|transaction| {
        trie.insert(
            transaction.transaction_hash()?.as_bytes(),
            bincode::serialize(&transaction)?,
        )?
    })?;

    Ok(trie)
}

fn root_hash(transactions: &[Transaction]) -> Result<H256> {
    let mut trie = Transaction::to_trie(transactions)?;
    let root_hash = trie.root_hash()?;

    Ok(H256::from_slice(root_hash.as_bytes()))
}

The state_root is the Merkle Root of all state within the blockchain. We'll detail this more when discussing Global State.

Genesis Block

The first block in a blockchain is called the genesis block. We're using a naive implementation to create an empty block with no state:

fn genesis() -> Result<Self> {
    Block::new(U64::zero(), H256::zero(), vec![], H256::zero())
}

In Ethereum, the genesis block is created using a genisis file. This file contains configuration information and details the accounts to create and the amount of Eth they each get. This is where the initial Eth comes from, though additional Eth is created for miners. The movement to Proof of Stake in Ethereum V2 transforms miners to validators, and changes the reward mechanism. We'll talk more about this in the consensus section.

Basic Cryptography

We've talked about public and private keys, addresses, and hashes, but dive a little deeper into what they are.

Public Key Cryptography

Public Key Cryptography is asymetric encryption, where there are public and private keys (key pair). This is different from symmetric encryption that uses the same key to encrypt and decrypt.

There are differnt flavors of public key cryptography. In this project we're using Secp256k1, which works with ECDSA signatures. Secp256k1 is an elliptic curve. We're not going to get into anything that complicated here, so just picture a line that's shaped like a door knob. The public and private keys are just points on the curve.

The public key is derived from the private key. A key is just a number (unsigned 256-bit in our case). While you can derive a public key from a private key, you cannot create a private key from a public one.

Here's how the encryption works. Bob wants to send Alice a secret message. He first asks Alice for her public key, which he uses to encrypt the message. He then sends the message to Alice and she uses her private key to decr

Core symbols most depended-on inside this repo

get_account
called by 16
chain/src/account.rs
web3
called by 15
web3/src/helpers.rs
serialize
called by 11
chain/src/helpers.rs
keypair
called by 9
utils/src/crypto.rs
send_rpc
called by 8
web3/src/lib.rs
setup
called by 7
chain/src/helpers.rs
public_key_address
called by 6
utils/src/crypto.rs
hash
called by 6
utils/src/crypto.rs

Shape

Function 97
Method 87
Class 18
Enum 6

Languages

Rust100%

Modules by API surface

types/src/transaction.rs23 symbols
utils/src/crypto.rs20 symbols
chain/src/blockchain.rs20 symbols
chain/src/account.rs19 symbols
chain/src/storage.rs12 symbols
web3/src/transaction.rs10 symbols
runtime/src/contract.rs10 symbols
chain/src/method.rs10 symbols
types/src/block.rs8 symbols
web3/src/account.rs7 symbols
chain/src/logger.rs7 symbols
chain/src/transaction.rs6 symbols

For agents

$ claude mcp add rust-blockchain-tutorial \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact