token

Cyber-GE

第 1 章 · 约 15 分钟阅读

区块链与以太坊基础

Blockchain Fundamentals

第 1 章:区块链与以太坊基础

智能合约运行在区块链上。在编写第一行 Solidity 代码之前,理解区块链的核心原理是必要的——它决定了你的代码如何被执行、存储和验证。


区块链概述

区块链是一种分布式账本技术,所有参与节点都持有完整的数据副本。与传统数据库不同,区块链没有中央服务器,数据分散存储在全球数千个节点上。这种架构通过密码学和共识机制实现了三个关键特性:数据一致性、不可篡改性和去中心化。

数据一致性意味着所有节点最终会看到相同的数据状态。当一笔交易被确认后,全网所有节点都会更新到相同的状态。不可篡改性是指历史记录一旦写入,就无法被修改或删除。去中心化则确保没有单一实体能够控制整个网络。

这些特性直接影响智能合约的设计。你无法"修复"已部署的合约代码——如果发现 bug,只能部署新版本并迁移数据。你无法删除链上数据——即使是错误的数据也会永久保存。每次状态变更都需要付费——这就是 Gas 机制存在的原因。理解这些约束是写出安全、高效合约的前提。

区块结构

以太坊的每个区块由区块头(Block Header)和区块体(Block Body)两部分组成。

区块结构

区块头包含了区块的元数据。parentHash 是前一区块的哈希值,它将区块链接成一条链。stateRoot 是世界状态树的根哈希,记录了所有账户的当前状态。transactionsRootreceiptsRoot 分别是交易树和收据树的根哈希。number 是区块高度,从创世区块的 0 开始递增。timestamp 是出块时间戳。gasLimit 是区块能容纳的最大 Gas 总量,gasUsed 是实际使用的 Gas。baseFeePerGas 是 EIP-1559 引入的基础费用。

区块体包含了实际的交易数据。transactions 数组存储本区块包含的所有交易。在以太坊转向 PoS 后,还增加了 withdrawals 数组用于记录验证者的提款。

parentHash 是不可篡改性的核心机制。每个区块都包含前一区块的哈希值,如果攻击者试图修改历史交易,该区块的哈希会改变,导致后续所有区块的 parentHash 失效。要使篡改生效,攻击者必须重新计算从被修改区块到最新区块的所有哈希。在 PoS 机制下,这需要控制超过三分之二的质押 ETH,经济上完全不可行。

交易结构

交易是改变区块链状态的唯一方式。无论是转账 ETH、调用合约函数还是部署新合约,都必须通过交易完成。

每笔交易包含以下字段:nonce 是发送者的交易序号,从 0 开始严格递增。to 是接收地址,部署合约时为空。value 是转账金额,单位是 wei(1 ETH = 10^18 wei)。data 是调用数据,包含函数选择器和参数,部署合约时则是合约字节码。gasLimit 是用户愿意支付的最大 Gas 数量。maxFeePerGasmaxPriorityFeePerGas 是 EIP-1559 引入的费用参数。signature 是发送者的 ECDSA 签名。

nonce 的作用值得特别说明。nonce 必须严格递增,如果你发送 nonce=5 的交易,但 nonce=4 还未确认,交易会卡在内存池中等待。这个机制防止了交易重放攻击——攻击者无法复制一笔已确认的交易再次广播,因为 nonce 已经被使用过了。但这也意味着一笔卡住的交易会阻塞后续所有交易,解决方法是用相同的 nonce 发送一笔新交易来替换它。


以太坊账户模型

以太坊采用账户模型,而非比特币的 UTXO 模型。账户模型更直观,也更适合智能合约,因为它天然支持状态存储。每个账户都有一个余额和一个 nonce,合约账户还有代码和存储。

以太坊账户模型

以太坊有两种账户类型:外部账户(EOA)和合约账户。

外部账户由私钥控制。用户通过私钥签名来授权交易,任何拥有私钥的人都可以控制该账户。EOA 只有两个字段:nonce 记录已发送的交易数,balance 记录 ETH 余额。EOA 可以主动发起交易,可以持有和转移 ETH。

合约账户由代码逻辑控制。合约没有私钥,它的行为完全由部署时的代码决定。合约账户有四个字段:nonce 记录该合约创建的子合约数,balance 记录 ETH 余额,codeHash 是合约代码的哈希,storageRoot 是存储树的根哈希。合约不能主动发起交易,只能被调用后执行代码。

这里有一个核心约束需要理解:合约不能"自己运行"。没有 cron job,没有定时任务,没有后台进程。如果你的业务逻辑需要定期执行,比如每天结算利息或每周分发奖励,必须依赖外部服务来触发。Chainlink Automation 和 Gelato 是常用的解决方案,它们会在满足条件时调用你的合约函数。

地址生成机制

地址生成机制

EOA 地址由公钥派生。首先用椭圆曲线算法从私钥生成公钥,然后对公钥进行 keccak256 哈希,取结果的后 20 字节作为地址。这个过程是单向的——从地址无法反推出公钥或私钥。

合约地址有两种生成方式。传统的 CREATE 方式下,地址取决于部署者地址和部署者的 nonce。这意味着同一个部署者在不同时间部署相同的代码,会得到不同的地址。

CREATE2 是确定性部署方式,地址取决于部署者地址、一个 salt 值和合约字节码的哈希。这允许在部署前就计算出合约地址。CREATE2 的实际应用非常广泛:工厂模式可以预测子合约地址,反事实部署允许先使用地址再部署合约,跨链部署可以确保相同的代码在不同链上有相同的地址。Uniswap V2 的交易对合约就使用 CREATE2,确保 ETH/USDC 交易对在任何链上都有相同的地址。


EVM 执行模型

EVM(Ethereum Virtual Machine)是以太坊的运行时环境。所有智能合约最终都被编译为 EVM 字节码执行。理解 EVM 的架构对于编写高效合约和调试问题至关重要。

EVM 是一个基于栈的虚拟机。与基于寄存器的虚拟机不同,EVM 的所有操作都在栈上进行。栈的最大深度是 1024,每个栈项是 256 位。这个设计简化了虚拟机的实现,但也带来了一些限制,比如著名的"Stack too deep"错误。

EVM 数据区域

EVM 有四个主要的数据区域。Stack 是操作栈,所有计算都在这里进行,访问是免费的。Memory 是临时内存,在函数调用期间有效,按字节寻址,线性扩展,成本较低。Storage 是永久存储,数据保存在区块链上,按 256 位的槽寻址,成本最高。Calldata 是只读的调用数据区域,存储函数参数,成本最低。

Gas 成本的排序是:Calldata < Stack < Memory < Storage。这个排序直接影响了 Gas 优化策略。如果一个存储变量在函数中被多次读取,应该先缓存到内存变量中,避免重复的 SLOAD 操作。

EVM 有三个关键特性。确定性执行意味着相同的输入必须产生相同的输出,这是共识机制的基础。因此合约不能使用随机数、系统时间(除了 block.timestamp)或外部 API。隔离性意味着每个合约在独立的执行环境中运行,只能通过消息调用与其他合约交互。原子性意味着交易要么完全成功,要么完全回滚,不存在中间状态。

栈深度限制是一个常见的坑。虽然栈最大深度是 1024,但实际可用约 1000 层,因为一些内部操作需要预留空间。深度递归或过多的局部变量会导致"Stack too deep"编译错误。解决方法包括减少局部变量数量、将变量打包到结构体中、或者将函数拆分成多个小函数。


Gas 机制详解

Gas 是 EVM 的计算资源计量单位。每个操作都有固定的 Gas 成本,用户为交易支付 Gas 费用。这个机制有两个目的:防止滥用和激励验证者。

防止滥用是指无限循环会消耗完 Gas 后自动停止,而不是让网络陷入死循环。如果没有 Gas 机制,恶意用户可以部署一个无限循环的合约,让所有节点永远执行下去。激励验证者是指 Gas 费用作为验证者打包交易的收入,确保有人愿意运行节点。

Gas 机制

不同操作的 Gas 成本差异很大。每笔交易有 21,000 的基础费用,这是固定成本。存储操作是最昂贵的:写入新存储槽(SSTORE 从 0 到非 0)需要 20,000 Gas,更新已有存储槽需要 5,000 Gas,清空存储槽会退还 4,800 Gas。读取存储槽(SLOAD)的成本取决于是否是首次访问:冷访问需要 2,100 Gas,热访问只需要 100 Gas。内存操作(MLOAD/MSTORE)只需要 3 Gas。算术运算(ADD/SUB/MUL)需要 3-5 Gas。调用外部合约(CALL)在冷地址时需要 2,600 Gas。

冷/热访问的概念来自 EIP-2929(柏林升级)。首次访问某个地址或存储槽称为"冷访问",成本较高;同一交易内的后续访问称为"热访问",成本大幅降低。这影响了 Gas 优化策略:如果一个存储变量在函数中被多次读取,应该在函数开始时缓存到内存变量中,后续使用内存变量而非重复读取存储。

EIP-1559 费用模型

2021 年伦敦升级引入了新的 Gas 定价机制。在旧模型中,用户设置一个 Gas 价格,出价高的交易优先被打包,这导致了激烈的价格竞拍和剧烈的价格波动。

新模型将费用分为两部分:baseFee 和 priorityFee。baseFee 由协议根据网络拥堵程度动态调整,每个区块最多变化 12.5%。当区块使用量超过目标(50%)时,baseFee 上涨;低于目标时,baseFee 下降。baseFee 会被销毁,不会给验证者。priorityFee 是用户给验证者的小费,用于激励验证者优先打包自己的交易。

实际费用的计算公式是:(baseFee + priorityFee) × gasUsed。用户设置 maxFeePerGas 作为愿意支付的最高价格,如果 baseFee + priorityFee 超过这个值,交易会等待 baseFee 下降。

这个模型的优势是 Gas 价格更可预测,因为 baseFee 的变化是渐进的。同时 ETH 有了通缩机制,因为 baseFee 被销毁。在网络繁忙时,大量 ETH 被销毁,可能超过新发行的 ETH,导致总供应量下降。


开发环境搭建

现代 Solidity 开发推荐使用 Foundry 工具链。Foundry 由 Rust 编写,编译速度快,测试功能强大,已经成为专业开发者的首选工具。

Foundry 包含三个主要工具:forge 是编译器和测试框架,cast 是命令行交互工具,anvil 是本地测试网络。安装非常简单,只需要运行官方的安装脚本。

# 安装 Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup

安装完成后可以验证版本。

# 验证安装
forge --version   # 编译器和测试框架
cast --version    # 命令行交互工具
anvil --version   # 本地测试网络

创建新项目使用 forge init 命令。

# 初始化新项目
forge init my-project
cd my-project
# 项目结构: src/(合约) test/(测试) script/(部署) foundry.toml(配置)

项目结构包含四个目录:src 存放合约源码,test 存放测试文件,script 存放部署脚本,foundry.toml 是配置文件。

下面是一个简单的计数器合约,演示了状态变量、函数和事件的基本用法。

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

contract Counter {
    uint256 public count;  // 状态变量,存储在链上
    
    event CountChanged(uint256 newCount);  // 事件,用于通知前端
    
    function increment() external {
        count++;
        emit CountChanged(count);
    }
    
    function decrement() external {
        require(count > 0, "Counter: cannot decrement below zero");
        count--;
        emit CountChanged(count);
    }
}

编译、测试和部署的命令如下。

forge build    # 编译合约
forge test     # 运行测试
anvil          # 启动本地测试网络
# 部署到本地网络(使用 Anvil 默认测试账户)
forge create src/Counter.sol:Counter --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

上面的私钥是 Anvil 默认测试账户,仅用于本地开发。永远不要在命令行中使用真实私钥,生产环境应使用硬件钱包或密钥管理服务。


总结

本章建立了智能合约开发的概念基础。区块链通过密码学保证数据一致性和不可篡改性,以太坊的账户模型支持智能合约的状态存储,EVM 提供了确定性的执行环境,Gas 机制防止资源滥用并激励验证者。

这些底层原理直接影响智能合约的设计决策。不可变性意味着需要升级模式来修复 bug。Gas 成本意味着需要优化存储访问。被动执行意味着需要外部触发机制。公开透明意味着不能存储敏感数据。

下一章将深入 Solidity 语言本身——数据类型、函数、修饰符等核心语法。


参考文献

Qián - The Creative

卦辞 · Judgment

"元亨利贞。"

象曰 · Image

天行健,君子以自强不息。

今日启示 · Insight

创造力与领导力的时刻。保持正直,大事可成。