How Ethereum Actually Works: Accounts, Transactions, and the Machine Under the Hood

Lesson 164min7,760 chars

Learning Objectives

  • Describe the difference between an EOA and a contract account using a real-world analogy
  • Explain why gas exists and what happens when a transaction runs out of gas
  • Identify the five core fields in an Ethereum transaction and explain each one's purpose
  • Distinguish between the EVM's three data locations (stack, memory, storage) and their relative costs
  • Initialize a Hardhat project and explain the purpose of each generated file
  • Predict whether a given transaction will succeed or fail based on its gasLimit and destination type

How Ethereum Actually Works: Accounts, Transactions, and the Machine Under the Hood

Before you write a single line of Solidity, you need to understand the machine that will execute it. Look at these two transactions. One succeeds. One burns your money and does nothing. Can you tell which is which?

solidity
// Transaction A
{to: "0xRecipient", value: 1 ether, gasLimit: 21000}
solidity
// Transaction B
{to: "0xContract", value: 1 ether, gasLimit: 21000}

Transaction A sends 1 ETH to a friend's wallet. It succeeds. Transaction B sends 1 ETH to a smart contract that runs code on receipt, but 21,000 gas is only enough for a simple transfer, not for executing contract logic. The transaction fails, you lose your gas fee, and the contract receives nothing. If you do not understand why, this lesson is for you. By the end of it, you will know exactly what happens between the moment you click "Send" and the moment a validator seals your transaction into the permanent record of the blockchain.

I have personally seen developers lose real money because they treated Ethereum like a regular API. It is not an API. It is not a database. It is a globally replicated state machine where every mistake is permanent and every computation costs money. I audited a project in 2021 where a team deployed a contract with a function that cost 14 million gas to execute. Nobody could call it. The contract was dead on arrival, and the 4 ETH they spent deploying it was gone forever. That is the kind of mistake this lesson prevents.


The Problem: Why "Just Send Money" Is Never Simple on Ethereum

If Ethereum were just a payments network, you would not need this course. Ethereum is powerful for the same reason it is dangerous: it is a general-purpose computer where money and code live side by side. When you send a transaction, you are not moving a balance from one cell to another in a spreadsheet. You are submitting a signed instruction to a global network of computers, paying for the computational resources your instruction consumes, and trusting that the rules baked into the protocol will produce the correct outcome.

Each of Ethereum's core concepts unlocks the next. Without understanding accounts, you cannot control who owns what. Without understanding transactions, you cannot predict what your code will actually do when it hits the network. Without understanding gas, you cannot estimate costs, and your users will abandon your dApp the first time MetaMask shows a $47 fee they did not expect. Without understanding the EVM, you will write Solidity that looks correct in your editor but behaves in ways you never anticipated on-chain.

Nearly every major hack in DeFi history traces back to a misunderstanding of one of these four concepts. The DAO hack in 2016 exploited how contract accounts execute code during transactions. Countless "out-of-gas" griefing attacks exploit how gas limits interact with loops. The "short address" attacks exploited how the EVM decodes transaction data. This lesson is your insurance policy against making these mistakes yourself.

⚠️ Common Pitfall: New developers assume Ethereum works like a REST API: send a request, get a response. In reality, transactions are asynchronous, irreversible, and can fail silently (consuming your gas) without reverting the way you expect. Always think of a transaction as a letter you mail, not a phone call you make.


The World-State Ledger: A Database That Never Forgets

Ethereum is, at its core, a giant key-value store that the entire world agrees on. Picture a spreadsheet with one column for the address and another for the account data. Every node in the Ethereum network holds a copy of this spreadsheet, and every 12 seconds a new batch of updates is applied to it. That batch is called a block. The chain of all historical batches is the blockchain. The current state of the spreadsheet, right now, is what Ethereum developers call the "world state."

The critical difference between Ethereum and a traditional database is immutability. When your bank updates your balance, it overwrites the old value. The previous balance is gone, or at best, buried in an internal audit log you cannot access. On Ethereum, every historical state is preserved because every block references the one before it through a cryptographic hash. You can look up what any account's balance was at block 1,000,000 or block 18,000,000. This is not a convenience feature. It is the foundation of trustlessness: anyone can independently verify the entire history.

Think of the world state as a notarized ledger at a courthouse. Every page is stamped, dated, and chained to the previous page by a wax seal. You can add new pages, but you can never tear one out or erase an entry. If someone tries to slip in a forged page, the seal will not match, and every other courthouse in the world will reject it. That is consensus: thousands of computers comparing their ledgers and refusing to accept anything that breaks the rules.

FeatureTraditional DatabaseEthereum World State
Who controls itSingle company (Oracle, AWS)No single entity, thousands of nodes
Can records be alteredYes, by the adminNo, blocks are cryptographically sealed
History availableOnly if audit logs are keptAlways, every state is reconstructible
Read accessRequires permissionPublic, anyone can query any account
Write accessAuthorized users onlyAnyone who pays gas for a valid transaction

The key insight from this table is the cost of writing. In a traditional database, writes are cheap because one server handles them. On Ethereum, every write must be processed and stored by thousands of nodes worldwide. That is why gas exists, and we will get to that. For now, burn this into your mental model: reading Ethereum is free, writing to it costs money, and once written, it is permanent.

💡 Key Insight: Ethereum's world state is not stored as a flat table. It uses a data structure called a Merkle Patricia Trie, which allows any node to prove that a specific account holds a specific balance without downloading the entire state. This is how light clients on mobile phones can verify transactions without storing 1+ TB of data.

🤔 Think about it: If every node stores the entire history, how large is the Ethereum blockchain today, and what does that mean for running your own node?

View Answer

As of early 2026, a full Ethereum archive node requires over 18 TB of storage. A "full node" that only keeps the current state plus recent blocks needs around 1 TB. This is why most developers use services like Alchemy or Infura to interact with Ethereum rather than running their own node. However, relying on a third party to read the blockchain introduces a trust assumption. In a future lesson, we will discuss how to minimize that trust.


Two Types of Citizens: EOAs and Contract Accounts

The world-state ledger tracks every account on Ethereum. But not all accounts are the same.

Every address on Ethereum is one of exactly two things: an Externally Owned Account or a Contract Account. This distinction is the single most important architectural concept in Ethereum, and misunderstanding it is the source of an entire category of bugs.

An Externally Owned Account (EOA) is controlled by a human being who holds a private key. Think of it as a personal safe deposit box at a bank. You have a key, and only you can open it. Nobody else, not even the bank (the Ethereum protocol), can move your funds without your signature. Your MetaMask wallet, your Ledger hardware wallet, your Coinbase account: these all manage EOAs. The address you see (like 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD08) is derived from the public key, which is itself derived from the private key. Lose the private key, and you lose access forever. There is no "forgot password" link on Ethereum.

A Contract Account is controlled by code, not by a human. It has no private key. Instead, it has bytecode, a program that the EVM executes whenever someone sends a transaction to its address. Think of it as a vending machine bolted to the floor of the bank lobby. It has its own balance, it follows strict rules, and it responds automatically when you interact with it. Nobody owns the vending machine in the traditional sense. It does what its code says, nothing more and nothing less. Uniswap, Aave, and OpenSea all operate through contract accounts.

Now here is where this distinction turns dangerous. A contract account can hold ETH, just like an EOA. But when you send ETH to a contract, the contract's code executes. If that code has a bug, your ETH can be trapped forever. If that code has a malicious receive() function, it can call back into your contract before your transaction finishes. That is the reentrancy attack that drained $60 million from The DAO in 2016. We will dissect that attack in Lesson 5, but I want you to feel the weight of this distinction right now.

PropertyEOA (Externally Owned Account)Contract Account
Controlled byPrivate key (human)Deployed bytecode (code)
Can initiate transactionsYesNo, can only respond to transactions
Has codeNoYes (immutable after deployment)
Has storageNo (only balance and nonce)Yes (persistent key-value storage)
Creation costFree (generate a key pair)Costs gas to deploy
Can hold ETHYesYes
ExampleYour MetaMask walletUniswap Router contract

The most revealing row in this table is "Can initiate transactions." A contract can never wake up and do something on its own. Every action on Ethereum begins with a human (or a bot, which is just software controlled by a human) signing a transaction from an EOA. The contract only runs when called. This has profound implications for design. If your contract needs to do something periodically, like distribute rewards every day, it cannot do that by itself. You need an external service (called a "keeper" or "automation bot") to send a transaction that triggers the distribution. Chainlink Automation exists precisely to solve this problem.

⚠️ Common Pitfall: Beginners often assume they can "call" a smart contract like calling a function in JavaScript. You can read from a contract for free (a "call"), but any operation that changes state requires a signed transaction from an EOA, costs gas, and takes at least one block (12 seconds) to confirm. This catches people off guard when they build their first dApp and realize the UI needs to wait for confirmation.

Deep Dive: How Ethereum Knows Whether an Address Is an EOA or a Contract

Ethereum does not have a flag that says "this is an EOA" or "this is a contract." Instead, the protocol checks the codeHash field in the account's state. If the codeHash equals the Keccak-256 hash of empty data (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470), the account is an EOA. If it holds anything else, the account is a contract. This means you can programmatically check whether an address is a contract by calling extcodesize in assembly, but be careful: during a contract's constructor execution, extcodesize returns 0 even though code is about to be deployed. Attackers have exploited this edge case in the wild.

MYTH BUSTER: "Smart contracts are smart." Common belief: smart contracts make intelligent decisions. Reality: smart contracts are neither smart nor contracts in the legal sense. They are deterministic programs that execute exactly as written, bugs and all. The word "smart" comes from Nick Szabo's 1994 paper and refers to self-executing agreements, not artificial intelligence. A smart contract with a bug will execute that bug faithfully every single time. The DAO did exactly what its code said. The code just happened to be wrong. Whenever you hear "smart contract," mentally replace it with "automated script with money."

🤔 Think about it: Can a contract account deploy another contract account? If so, who initiates the chain?

View Answer

Yes, a contract can deploy another contract using the CREATE or CREATE2 opcode. This is how factory patterns work. For example, Uniswap V2's Factory contract deploys a new Pair contract every time a new token pair is created. But trace the chain back far enough, and it always starts with an EOA. A human signed a transaction that called the Factory, which then created the Pair. The EOA is always the spark that lights the chain.

💡 Key Takeaway: Every action on Ethereum starts with a human signing a transaction from an EOA. Contract accounts are powerful but passive. They only run when called.


Anatomy of a Transaction: The Five Fields That Control Everything

You know who the players are: EOAs and contracts. Now let's look at how they communicate.

A transaction on Ethereum is a signed data structure with five core fields, and understanding each one gives you control over cost, timing, and security. Remember the letter analogy from earlier? Let's expand it. A transaction is a certified letter with a destination, contents, postage, a return address, and a tracking number. If any of these are wrong, the letter either goes to the wrong place, gets returned, or never arrives. On Ethereum, the consequences are worse: you pay postage (gas) even if the letter is rejected.

The nonce is a counter that prevents replay attacks. Every EOA has a nonce that starts at zero and increments by one with each transaction you send. If your current nonce is 42, your next transaction must have nonce 42. If you accidentally send nonce 44, it will sit in the mempool (the waiting area for unconfirmed transactions) until transactions 42 and 43 are processed first. Without the nonce, if you signed a transaction sending 1 ETH to Alice, anyone could rebroadcast that same signed transaction over and over, draining your account. The nonce makes each transaction unique.

The to field is the destination address. If you are sending ETH to a friend, this is their EOA address. If you are calling a smart contract function, this is the contract's address. If you are deploying a new contract, the to field is empty (null). That absence of a destination is how Ethereum knows you want to deploy code rather than call an existing contract.

The value field is how much ETH you are sending, denominated in wei. One ETH equals 10^18 wei. That is a 1 followed by 18 zeros. Why such a tiny unit? Because Ethereum needs precision for DeFi applications where fractions of a cent matter. When you see msg.value in Solidity, it is always in wei. I once reviewed a contract where the developer compared msg.value to 1 thinking it meant 1 ETH. It meant 1 wei, approximately $0.000000000000000003. The function's minimum payment check was essentially disabled. That is the kind of bug that costs real money.

The gasLimit and gasPrice (or in post-EIP-1559 terms, maxFeePerGas and maxPriorityFeePerGas) control how much computation you are willing to pay for and at what rate. Think of gasLimit as the maximum fare you will let a taxi meter reach. If your transaction needs 50,000 gas units of computation but you set the limit to 30,000, the EVM will execute your transaction until it hits 30,000, then revert everything it did, and you still pay for the 30,000 gas consumed. The gasPrice is how much you pay per unit. Higher rates mean validators prioritize your transaction because they earn more from it.

The data field contains the function call you want to make on the destination contract. For a simple ETH transfer, this field is empty. For a contract interaction, it contains the function selector (the first 4 bytes of the Keccak-256 hash of the function signature) followed by the ABI-encoded arguments. You do not need to construct this by hand. Libraries like ethers.js and web3.js handle the encoding. But knowing it exists helps you debug failed transactions on Etherscan, because you can decode the data field to see exactly what function was called and with what arguments.

Transaction FieldPurposeAnalogyWhat Goes Wrong If It Is Incorrect
nonceReplay protection, orderingTracking number on a certified letterTransaction stuck in mempool or rejected
toDestination addressMailing address on an envelopeETH sent to wrong address (unrecoverable)
valueAmount of ETH to send (in wei)Cash enclosed in the envelopeWrong amount, possibly exploitable
gasLimitMax computation allowedTaxi meter maximumOut-of-gas revert, fee wasted
dataFunction call + argumentsThe letter content inside the envelopeWrong function called, unexpected behavior

Focus on the last column. Every field has a failure mode, and most of them cost you money. Sending ETH to a typo address? Gone. Setting gasLimit too low? Reverted, but you still pay. Encoding the wrong function selector in the data field? You might call a completely different function than you intended. Ethereum does not have an "undo" button, a customer support line, or a dispute resolution process. The protocol executes exactly what you signed.

💡 Key Insight: After EIP-1559 (the London hard fork in August 2021), Ethereum transactions use a new fee model. Instead of a single gasPrice, you specify a maxFeePerGas (the ceiling you will pay) and a maxPriorityFeePerGas (the tip to the validator). The protocol calculates a baseFee that gets burned (destroyed), and your actual cost per gas unit is baseFee + priorityFee. This makes fees more predictable, but the gasLimit concept remains identical.

Deep Dive: What Is the Mempool and Why Should You Care?

Before a transaction is included in a block, it sits in the mempool (memory pool), a waiting area on every node. The mempool is public. Anyone can watch it. This creates an entire category of attacks called MEV (Maximal Extractable Value), where bots watch for pending transactions and insert their own transactions before or after yours to profit at your expense. For example, if you submit a large swap on Uniswap, a bot can see your pending transaction, buy the token before you (driving the price up), let your swap execute at the higher price, and then sell for a profit. This is called a "sandwich attack." Understanding the mempool is critical for building DeFi applications, and we will revisit it when we discuss security patterns.

🤔 Think about it: If you send two transactions with nonce 5 and nonce 6, but nonce 5 has a very low gas price and gets stuck, what happens to nonce 6?

View Answer

Nonce 6 will also be stuck. Ethereum processes transactions from each account strictly in nonce order. Nonce 6 cannot be mined until nonce 5 is mined first. This is why "stuck transactions" are a common problem. The solution is to send a new transaction with the same nonce 5 but a higher gas price, effectively replacing the stuck one. MetaMask calls this "Speed Up" or "Cancel" (canceling is just sending 0 ETH to yourself with the same nonce and a higher fee).

💡 Key Takeaway: A transaction is not a request. It is a signed, irreversible instruction. Know every field before you sign.


The EVM: A Calculator Inside a Vault

You have seen what a transaction contains. Now let's follow it inside the machine that executes it.

The Ethereum Virtual Machine is the runtime environment where your Solidity code actually executes, and it is deliberately primitive by design. If your laptop's CPU is a Swiss Army knife, the EVM is a single-purpose calculator sealed inside a bank vault. It can do math, read and write to storage, and send ETH. It cannot access the internet, read a file, or generate a random number. This is intentional. Every node in the network must produce the exact same result when executing your code. If the EVM could make a network request, different nodes would get different responses, and consensus would shatter.

The EVM is a stack-based machine with a 256-bit word size. If you have used an HP scientific calculator with Reverse Polish Notation, you already understand the basic execution model. Instead of writing 3 + 4, you push 3 onto the stack, then push 4, then execute the ADD opcode. The EVM pops both values, adds them, and pushes 7 back onto the stack. Your Solidity code compiles down to a sequence of these opcodes (about 140 of them in total). You rarely need to think at this level, but when you are debugging gas costs or analyzing security vulnerabilities, understanding the stack machine underneath helps enormously.

The 256-bit word size is not an accident. Keccak-256 hashes, which Ethereum uses everywhere, produce 256-bit outputs. Ethereum addresses are 160 bits (20 bytes), which fit inside a single 256-bit word. This design choice means the EVM handles large numbers naturally, essential for DeFi math. But it also means that every operation, even adding two small numbers, uses 256-bit arithmetic. There is no "int8 is faster than int256" optimization in the EVM. In fact, using uint8 in Solidity can sometimes cost more gas than uint256 because the EVM has to mask the extra bits. This surprises almost every new Solidity developer.

The EVM has three places to put data: the stack, memory, and storage. The stack is the scratchpad for computations (up to 1,024 items deep). Memory is a temporary byte array that exists only during the current transaction, like RAM. Storage is the permanent key-value store that persists between transactions, like a hard drive. The cost difference is staggering: writing to storage costs 20,000 gas for a new slot, while writing to memory costs just 3 gas per word. That is a 6,000x cost difference. Gas optimization in Solidity revolves almost entirely around minimizing storage writes.

Data LocationPersistenceCost (approximate)Analogy
StackCurrent operation only3 gas (PUSH/POP)CPU register
MemoryCurrent transaction only3 gas per word (grows quadratically)RAM
StoragePermanent, on-chain20,000 gas (new write), 5,000 gas (update)Hard drive
CalldataCurrent transaction only16 gas per non-zero byteRead-only function arguments

The quadratic memory cost is a detail most tutorials skip, but it matters. The first few words of memory are cheap. As you allocate more memory within a single transaction, the per-word cost increases quadratically. This prevents contracts from allocating gigabytes of memory in a single call. In practice, you rarely hit this limit, but it is the reason Solidity's memory keyword exists: the compiler needs to know where to allocate data so it can estimate costs.

⚠️ Common Pitfall: Beginners write loops that update storage on every iteration: balances[users[i]] += reward inside a for loop. If there are 1,000 users, that is 1,000 storage writes at 5,000 gas each, totaling 5 million gas just for the updates. Instead, accumulate results in a memory variable and write to storage once. We will practice this pattern extensively starting in Lesson 2.

Deep Dive: Why the EVM Cannot Generate Random Numbers

Every node must compute the same result for the same transaction. If the EVM had a RANDOM opcode, each node would generate a different number, and they would never agree on the state. This is why on-chain randomness is one of the hardest problems in blockchain. Early solutions used block.timestamp or blockhash as randomness sources, but miners could manipulate these values to rig outcomes. The industry standard today is Chainlink VRF (Verifiable Random Function), which uses a cryptographic proof that the random number was generated fairly. If you are building a lottery, a game, or anything that requires randomness, never use block variables. I audited a lottery contract in 2022 that used block.difficulty for randomness. A miner won the pot four times in a row before anyone noticed.

🤔 Think about it: Why is the EVM's word size 256 bits and not 64 bits like most modern CPUs?

View Answer

Ethereum needs to natively handle cryptographic hashes (256-bit Keccak), large token amounts (ETH has 18 decimal places, requiring numbers up to 10^77), and elliptic curve math for signature verification. A 64-bit word size would require multi-word arithmetic for these operations, making them slower and more gas-expensive. The 256-bit word size was a deliberate trade-off: it makes simple operations slightly less efficient but makes cryptographic and financial operations native.

💡 Key Takeaway: The EVM is deterministic, sandboxed, and expensive. Storage is permanent and costs 6,000x more than memory. Design your contracts around this reality.


Gas: The Meter That Keeps the Machine Running

The EVM runs your code. Gas determines how much of it you can afford to run, and it keeps the entire network from grinding to a halt.

Gas is Ethereum's mechanism for preventing infinite loops, pricing computation fairly, and compensating validators for their work. Without gas, a single malicious contract containing while(true) {} would force every node on the network to loop forever. This is not theoretical. It is exactly what happened in September 2016 during the Shanghai DoS attacks, when an attacker exploited underpriced opcodes to slow the network to a crawl. The Ethereum Foundation responded with two hard forks (Tangerine Whistle and Spurious Dragon) that repriced dozens of opcodes. Gas is not just an inconvenience for developers. It is a load-bearing wall of Ethereum's security architecture.

Every opcode has a fixed gas cost, and your transaction's total gas consumption is the sum of all opcodes executed. Adding two numbers (ADD) costs 3 gas. A Keccak-256 hash (SHA3) costs 30 gas plus 6 gas per word of input. Writing a new value to storage (SSTORE) costs 20,000 gas. Reading from storage (SLOAD) costs 2,100 gas (raised from 200 after the DoS attacks). These prices are defined in the Ethereum Yellow Paper and are occasionally adjusted through EIPs (Ethereum Improvement Proposals) to reflect changing hardware costs and network conditions.

The relationship between gas and ETH is indirect, and this confuses nearly everyone at first. Gas is a unit of computation, like kilowatt-hours for electricity. The gasPrice (or baseFee plus priorityFee after EIP-1559) is the exchange rate between gas units and ETH. Your total transaction cost in ETH equals gasUsed * effectiveGasPrice. If your transaction uses 50,000 gas and the effective gas price is 30 gwei (1 gwei = 10^9 wei = 0.000000001 ETH), your cost is 50,000 * 30 gwei = 1,500,000 gwei = 0.0015 ETH. At $3,000 per ETH, that is $4.50. This is why gas costs fluctuate wildly: the gas units are fixed, but the gas price in gwei changes based on network demand.

Here is the part that burns beginners. Literally. When a transaction runs out of gas, the EVM reverts all state changes made during that transaction, but you still pay for the gas consumed up to that point. This is not a bug. It is a feature. If out-of-gas transactions were free, an attacker could submit millions of expensive transactions with gasLimit set to 1, forcing validators to do work without compensation. The "you pay even if you fail" rule ensures that computation is never free, even when it is wasted.

The expensive mistake:

solidity
// Setting gasLimit too low for a contract interaction
{to: contractAddress, gasLimit: 21000, data: "0xa9059cbb..."}
// Result: OUT_OF_GAS revert. You lose the gas fee. Nothing happens.

The correct approach:

solidity
// Use estimateGas() first, then add a 20% buffer
// estimatedGas = 52,418
{to: contractAddress, gasLimit: 63000, data: "0xa9059cbb..."}
// Result: Transaction succeeds. Unused gas (10,582) is refunded.

Always estimate gas before sending a transaction, and add a safety buffer. The eth_estimateGas RPC method simulates your transaction and returns the gas needed. I add a 20% buffer because state can change between estimation and execution (another transaction might modify the contract state first, making yours more expensive). MetaMask does this automatically, which is why its gas estimates are usually slightly higher than the actual cost.

MYTH BUSTER: "A 21,000 gas limit is always enough for sending ETH." Common belief: setting gasLimit to 21,000 is fine for any ETH transfer. Reality: 21,000 gas is enough for a simple transfer between two EOAs. But if the to address is a contract account, the contract's receive() or fallback() function executes, and that code consumes additional gas. If the contract's receive function writes to storage, 21,000 gas will not cover it, and the transaction reverts. Always check whether the destination is a contract before assuming 21,000 is sufficient.

📌 Remember: Gas is consumed by computation. You pay for gas whether your transaction succeeds or fails. Unused gas is refunded. Storage operations dominate gas costs.

🤔 Think about it: If a transaction's gasLimit is 100,000 but it only uses 60,000 gas, what happens to the remaining 40,000?

View Answer

The unused 40,000 gas is refunded to your account. You only pay for gasUsed * gasPrice, not gasLimit * gasPrice. This is why setting a higher gasLimit is relatively safe: you are specifying a ceiling, not prepaying the full amount. The only risk of a very high gasLimit is that the full amount is temporarily reserved from your balance during transaction processing.

💡 Key Takeaway: Gas is the EVM's immune system. It prevents abuse, prices computation, and makes every operation economically accountable. Respect it.


Self-Assessment Rubric

Before we move to the hands-on project setup, check your understanding against these benchmarks. Be honest with yourself. If any of these feel shaky, re-read the relevant section before proceeding.

  • I can explain the difference between an EOA and a contract account using a real-world analogy
  • I can list the five core transaction fields and describe what each one does
  • I can explain why a transaction that runs out of gas still costs money
  • I can describe why the EVM cannot access the internet or generate random numbers
  • I understand why storage operations cost dramatically more than memory operations
  • I know that 21,000 gas is only sufficient for a simple EOA-to-EOA transfer

If you checked five or six of these, you are ready for the project setup. If fewer than four felt solid, spend another ten minutes with the sections above. These concepts are the bedrock of every lesson that follows. Trying to learn Solidity without understanding accounts, transactions, and gas is like trying to learn web development without understanding HTTP. You might get something working, but you will not understand why it breaks.


Deep Dive: Code Walkthrough - Exploring Ethereum with Hardhat and ethers.js

Now we translate theory into practice. The following script is a single, runnable Hardhat file that demonstrates every concept from this lesson: checking account types, inspecting transaction fields, measuring gas consumption, and observing the world state. You will be able to run this after completing the Project Update section below.

This is not toy code. Every line connects to a concept we covered above. I have added comments that reference the relevant section so you can trace the connection. Read through it once top to bottom, then run it, then come back and modify the experiments.

JavaScript
// scripts/explore-ethereum.js
// A single script that demonstrates all core Ethereum concepts from Lesson 1
// Run with: npx hardhat run scripts/explore-ethereum.js

const { ethers } = require("hardhat");

async function main() {
  // === CONCEPT 1: Accounts and the World State ===
  // Hardhat gives us 20 pre-funded test accounts (EOAs with private keys)
  const [deployer, alice, bob] = await ethers.getSigners();

  console.log("=== WORLD STATE: Account Balances ===");
  const deployerBalance = await ethers.provider.getBalance(deployer.address);
  console.log(`Deployer address: ${deployer.address}`);
  console.log(`Deployer balance: ${ethers.formatEther(deployerBalance)} ETH`);
  // Output: Deployer balance: 10000.0 ETH (Hardhat default)

  // === CONCEPT 2: EOA vs Contract Account ===
  // Check if an address has code (contract) or not (EOA)
  const deployerCode = await ethers.provider.getCode(deployer.address);
  console.log("\n=== ACCOUNT TYPES ===");
  console.log(`Deployer is EOA: ${deployerCode === "0x"}`);
  // Output: Deployer is EOA: true (no code at this address)

  // Deploy a minimal contract to demonstrate a Contract Account
  const ContractFactory = await ethers.getContractFactory("GuardianVault");
  const vault = await ContractFactory.deploy();
  await vault.waitForDeployment();
  const vaultAddress = await vault.getAddress();

  const vaultCode = await ethers.provider.getCode(vaultAddress);
  console.log(`Vault contract address: ${vaultAddress}`);
  console.log(`Vault has code: ${vaultCode !== "0x"}`);
  // Output: Vault has code: true (bytecode deployed at this address)
  console.log(`Vault code length: ${(vaultCode.length - 2) / 2} bytes`);
  // Output: Vault code length: (varies based on contract size)

  // === CONCEPT 3: Transaction Anatomy ===
  // Send a simple ETH transfer and inspect the transaction fields
  console.log("\n=== TRANSACTION ANATOMY ===");
  const tx = await deployer.sendTransaction({
    to: alice.address,
    value: ethers.parseEther("1.0"),  // 1 ETH = 10^18 wei
  });
  const receipt = await tx.wait();  // Wait for the transaction to be mined

  console.log(`Nonce: ${tx.nonce}`);
  // Output: Nonce: 1 (0 was used for contract deployment)
  console.log(`To: ${tx.to}`);
  // Output: To: <alice's address>
  console.log(`Value: ${ethers.formatEther(tx.value)} ETH`);
  // Output: Value: 1.0 ETH
  console.log(`Gas Limit: ${tx.gasLimit.toString()}`);
  // Output: Gas Limit: (estimated by ethers.js, varies)
  console.log(`Data: ${tx.data}`);
  // Output: Data: 0x (empty, because this is a simple transfer)

  // === CONCEPT 4: Gas Consumption ===
  console.log("\n=== GAS ANALYSIS ===");
  console.log(`Gas Used: ${receipt.gasUsed.toString()}`);
  // Output: Gas Used: 21000 (exact cost of a simple EOA-to-EOA transfer)
  console.log(`Effective Gas Price: ${ethers.formatUnits(receipt.gasPrice, "gwei")} gwei`);
  // Output: Effective Gas Price: (varies, Hardhat default ~1-2 gwei)

  const txCostWei = receipt.gasUsed * receipt.gasPrice;
  console.log(`Transaction Cost: ${ethers.formatEther(txCostWei)} ETH`);
  // Output: Transaction Cost: (gasUsed * gasPrice, varies)

  // === CONCEPT 5: State Changes Are Permanent ===
  console.log("\n=== STATE AFTER TRANSACTION ===");
  const aliceBalance = await ethers.provider.getBalance(alice.address);
  console.log(`Alice balance: ${ethers.formatEther(aliceBalance)} ETH`);
  // Output: Alice balance: 10001.0 ETH (started with 10000, received 1)

  const deployerNewBalance = await ethers.provider.getBalance(deployer.address);
  console.log(`Deployer balance: ${ethers.formatEther(deployerNewBalance)} ETH`);
  // Output: Deployer balance: (less than 9999.0 due to gas costs from deploy + transfer)

  // Notice: deployer lost MORE than 1 ETH because gas fees were also deducted
  const totalSpent = deployerBalance - deployerNewBalance;
  console.log(`Deployer total spent: ${ethers.formatEther(totalSpent)} ETH`);
  // Output: Deployer total spent: (slightly more than 1.0 due to gas from both txns)

  console.log("\n=== BLOCK INFO ===");
  const block = await ethers.provider.getBlock(receipt.blockNumber);
  console.log(`Block number: ${block.number}`);
  // Output: Block number: 2 (block 1 was the contract deployment)
  console.log(`Block timestamp: ${new Date(Number(block.timestamp) * 1000).toISOString()}`);
  // Output: Block timestamp: (varies each run)
  console.log(`Transactions in block: ${block.transactions.length}`);
  // Output: Transactions in block: 1

  console.log("\n--- Lesson 1 exploration complete! ---");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Let me walk you through the key moments in this script. In the first section, we grab three signers from Hardhat's built-in test network. These are EOAs, each preloaded with 10,000 ETH. We check the deployer's balance using ethers.provider.getBalance(), which reads directly from the world state. Reading is free. No transaction, no gas.

In the account types section, we use getCode() to distinguish EOAs from contracts. An EOA returns "0x" (empty bytecode). A contract returns its deployed bytecode. This is the exact same check described in the deep dive above: the protocol does not have an "account type" field. It infers the type from the presence or absence of code.

The transaction anatomy section is where the five fields become tangible. Notice that tx.data is "0x" for a simple transfer, confirming that no function is being called. The nonce is 1 because nonce 0 was consumed by the contract deployment. Every detail we discussed in theory shows up in the output.

The gas analysis section confirms the 21,000 gas rule for simple transfers. And the state section reveals the asymmetry: Alice gained exactly 1 ETH, but the deployer lost more than 1 ETH because gas fees were deducted on top of the transfer amount. This is the "writing costs money" principle made visible in your terminal.


Variations and Trade-offs: Interacting with Ethereum

There are several ways to connect to Ethereum, and your choice depends on your trust model and your budget. This is where I get opinionated, because I have seen teams make expensive mistakes by choosing the wrong infrastructure.

ApproachCostTrust LevelLatencyBest For
Run your own full node (Geth/Nethermind)$100-300/month (hardware)Trustless (you verify everything)Low (local)Production DeFi protocols
RPC provider (Alchemy, Infura, QuickNode)Free tier to $500+/monthTrust the providerMedium (network hop)Most dApps and development
Hardhat local networkFreeN/A (simulated)InstantDevelopment and testing
Light client (Helios, Lodestar)FreeTrust-minimizedMediumMobile wallets, resource-limited environments

My strong recommendation for this course, and for most development work, is Hardhat's local network. It gives you a deterministic, instant Ethereum simulation with 20 funded accounts, automatic mining, and zero cost. You do not need to sync a node, pay for an API key, or wait for block confirmations. Every transaction is mined immediately, which makes development feedback loops instantaneous. For production deployment, I prefer Alchemy over Infura because of their enhanced debugging APIs and better error messages, but we will not need that until Lesson 8.

When you are ready for testnet deployment, use Sepolia. Goerli was deprecated in 2023, and Rinkeby and Ropsten were shut down even earlier. Sepolia is the recommended testnet for application developers, while Holesky is intended for validator and infrastructure testing. I have seen outdated tutorials point students to dead testnets, wasting hours of confusion. Sepolia is the one you want.

💡 Key Takeaway: Develop on Hardhat's local network. Test on Sepolia. Deploy to mainnet. That is the pipeline, and we will follow it throughout this course.


🔨 Project Update

This is Lesson 1, so we are starting from scratch. By the end of this section, you will have a fully initialized Hardhat project with an empty GuardianVault.sol contract ready for Lesson 2. Every lesson in this course builds on the previous one. By Lesson 8, you will deploy this contract to a public testnet and verify it on Etherscan.

Step 1: Make sure you have Node.js 18+ installed. Open your terminal and run:

Bash
node --version

If you see v18.x.x or higher, you are good. If not, install Node.js from the official website or use nvm.

Step 2: Create and initialize the project. Run these commands one at a time:

Bash
mkdir guardian-vault && cd guardian-vault
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init

When Hardhat asks what you want to do, select "Create a JavaScript project". Accept the defaults for the project root and .gitignore. When asked to install dependencies, say yes.

Step 3: Replace the contents of hardhat.config.js with:

JavaScript
require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.28",
};

Step 4: Delete the sample files that Hardhat generates, then create our own:

Bash
rm contracts/Lock.sol test/Lock.js ignition/modules/Lock.js

Step 5: Create contracts/GuardianVault.sol with the following content:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// @title GuardianVault - A secure ETH vault built lesson by lesson
/// @notice This contract will grow throughout the course
contract GuardianVault {
    // Lesson 2 will add state variables and functions here
}

Step 6: Create the exploration script from the code walkthrough. Save the full script above as scripts/explore-ethereum.js.

Step 7: Compile and run.

Bash
npx hardhat compile
npx hardhat run scripts/explore-ethereum.js

Expected output from compilation:

Compiled 1 Solidity file successfully (solc-output already cached).

Expected output from the script: You will see all the sections from the code walkthrough, with real values from Hardhat's simulated network: balances of 10,000 ETH, a gas cost of 21,000 for the simple transfer, and Alice's balance at 10,001 ETH. The exact addresses and gas prices will differ on your machine, but the structure will match.

Your project structure should now look like this:

guardian-vault/
  contracts/
    GuardianVault.sol       <-- empty contract, ready for Lesson 2
  scripts/
    explore-ethereum.js     <-- the exploration script from this lesson
  test/                     <-- empty, we'll add tests in Lesson 7
  hardhat.config.js         <-- configured for Solidity 0.8.28
  package.json
  node_modules/

📌 Remember: Every file in this project will grow over the coming lessons. Do not delete anything unless I explicitly tell you to. The GuardianVault contract will accumulate state variables, functions, modifiers, and security patterns one lesson at a time.

Run the project you have built so far with npx hardhat run scripts/explore-ethereum.js and confirm you see output for all five concept sections.


Checkpoint

Test yourself on the core material before moving to the next lesson.

🤔 Question 1: You send a transaction to a contract with gasLimit set to 21,000. The contract's receive() function writes a value to storage. What happens?

View Answer

The transaction reverts with an out-of-gas error. A simple ETH transfer costs 21,000 gas, but the receive() function's SSTORE operation costs an additional 20,000+ gas. The total exceeds 21,000, so the EVM stops execution, reverts all state changes, and charges you for the 21,000 gas consumed. The contract receives nothing.

🤔 Question 2: Can you determine whether an address is an EOA or a contract account just by looking at the address string?

View Answer

No. EOA addresses and contract addresses look identical: they are both 20-byte hexadecimal strings prefixed with 0x. The only way to determine the account type is to query the blockchain for code at that address. If getCode() returns "0x", it is an EOA (or a contract that has been self-destructed, but selfdestruct is being deprecated via EIP-6780). If it returns anything else, it is a contract.

🤔 Challenge: Modify the exploration script to send ETH from Alice to the GuardianVault contract. Check whether the gas used is still 21,000. Why or why not?

Hint

Use alice.sendTransaction({ to: vaultAddress, value: ethers.parseEther("0.5") }) and check receipt.gasUsed. The answer depends on whether GuardianVault.sol has a receive() function.

View Answer

With the current empty contract (which has no receive() or fallback() function), the transaction will revert because the contract cannot accept ETH. You need to add receive() external payable {} to the contract for it to accept ETH transfers. Once you do, the gas will be slightly more than 21,000 because the EVM must execute the contract's receive function bytecode, even if the function body is empty. We will add this in Lesson 2.


Summary Table

ConceptWhat It IsWhy It MattersSecurity Implication
World StateGlobal key-value store of all accountsEverything on Ethereum is a state read or state writeAll state is public. Never store secrets in contract storage.
EOAAccount controlled by a private keyOnly EOAs can initiate transactionsLose the private key, lose everything. No recovery.
Contract AccountAccount controlled by bytecodeExecutes code when receiving transactionsBugs in code are permanent and exploitable.
NoncePer-account transaction counterPrevents replay attacks, enforces orderingStuck nonces block all subsequent transactions.
Gas LimitMaximum computation for a transactionPrevents infinite loops, bounds execution costToo low causes reverts. Too high wastes temporary lock on funds.
Gas PriceCost per unit of computation (in gwei)Determines validator priority and total tx costLow gas price means slow inclusion, vulnerable to front-running.
Data FieldABI-encoded function callTells the contract which function to executeWrong encoding calls wrong function, potentially catastrophic.
EVMStack-based virtual machine, 256-bit wordsExecutes all smart contract logicDeterministic and sandboxed. No randomness, no external calls.
Storage vs MemoryPermanent on-chain vs temporary per-transactionStorage costs 6,000x more than memoryExcessive storage writes make functions too expensive to call.

This table is your cheat sheet for the rest of the course. Every lesson builds on these nine concepts. Lesson 2 starts writing Solidity: we will declare state variables (storage), write functions that accept ETH (transactions with value), and use msg.sender (the EOA that initiated the transaction). You now have the mental model to understand exactly what every line of Solidity does under the hood.


Difficulty Fork

🟢 Too Easy? Here is your fast track.

You already understand accounts, transactions, gas, and the EVM. Key takeaways to carry forward: storage is expensive, contracts cannot initiate transactions, and the EVM is deterministic with a 256-bit word size. In Lesson 2, we write GuardianVault's first state variables and payable functions. Skim it for the security callouts but focus on the modifier patterns.

🟡 Just Right? Reinforce with a different angle.

Think of Ethereum like a global notary office. EOAs are people who walk in with their ID (private key). Contract accounts are automated kiosks bolted to the floor. Transactions are forms you fill out, sign, and pay a filing fee (gas) for. The EVM is the office clerk who processes every form identically, step by step. Practice: open Etherscan, find any transaction, and identify all five fields (nonce, to, value, gasLimit, data) in the "More Details" section. Then explain to a friend why the gas used might differ from the gas limit.

🔴 Challenge? Go deep.

Research EIP-4844 ("Proto-Danksharding") and explain how "blob" transactions add a new data availability layer to Ethereum. How do blob fees differ from regular gas fees? Why does this matter for Layer 2 rollups like Arbitrum and Optimism? Then look at the SELFDESTRUCT opcode (EIP-6780): what changed after the Dencun upgrade, and why was this change necessary for Verkle tree migration? Write a 300-word summary of how these two EIPs reshape the cost structure of deploying on Ethereum.


Next up in Lesson 2: Your First Solidity Contract. We will give GuardianVault its first real capability: accepting ETH deposits, tracking who deposited how much, and letting only the depositor withdraw. You will write your first payable function, your first mapping, and your first require statement. The world-state concepts from today will come alive as you watch your contract's storage change with every transaction.

Code Playground

JavaScript

Q&A