以太坊-基础数据结构解析

以太坊基础配置

以太坊的基础配置用于服务于链,启动以太坊节点,则需要将链配置载入

https://github.com/ethereum/go-ethereum#

params/config.go 中:

// ChainConfig 是决定区块链设置的核心配置。
//
// ChainConfig 以每个区块为基础存储在数据库中。这意味着
// 任何由其创世区块标识的网络,都可以拥有自己的
// 一套配置选项。
type ChainConfig struct {
    ChainID *big.Int `json:"chainId"` // chainId 标识当前链,用于重放攻击保护。

    HomesteadBlock *big.Int `json:"homesteadBlock,omitempty"` // Homestead 硬分叉切换区块(nil = 无硬分叉,0 = 已是 Homestead)

    DAOForkBlock   *big.Int `json:"daoForkBlock,omitempty"`   // TheDAO 硬分叉切换区块(nil = 无硬分叉)
    DAOForkSupport bool     `json:"daoForkSupport,omitempty"` // 节点是否支持或反对 DAO 硬分叉

    // EIP150 实现了 Gas 价格变化 (https://github.com/ethereum/EIPs/issues/150)
    EIP150Block *big.Int `json:"eip150Block,omitempty"` // EIP150 硬分叉区块(nil = 无硬分叉)
    EIP155Block *big.Int `json:"eip155Block,omitempty"` // EIP155 硬分叉区块
    EIP158Block *big.Int `json:"eip158Block,omitempty"` // EIP158 硬分叉区块

    ByzantiumBlock      *big.Int `json:"byzantiumBlock,omitempty"`      // Byzantium 切换区块(nil = 无硬分叉,0 = 已在 Byzantium)
    ConstantinopleBlock *big.Int `json:"constantinopleBlock,omitempty"` // Constantinople 切换区块(nil = 无硬分叉,0 = 已激活)
    PetersburgBlock     *big.Int `json:"petersburgBlock,omitempty"`     // Petersburg 切换区块(nil = 与 Constantinople 相同)
    IstanbulBlock       *big.Int `json:"istanbulBlock,omitempty"`       // Istanbul 切换区块(nil = 无硬分叉,0 = 已在 Istanbul)
    MuirGlacierBlock    *big.Int `json:"muirGlacierBlock,omitempty"`    // Eip-2384(难度炸弹延迟)切换区块(nil = 无硬分叉,0 = 已激活)
    BerlinBlock         *big.Int `json:"berlinBlock,omitempty"`         // Berlin 切换区块(nil = 无硬分叉,0 = 已在 Berlin)
    LondonBlock         *big.Int `json:"londonBlock,omitempty"`         // London 切换区块(nil = 无硬分叉,0 = 已在 London)
    ArrowGlacierBlock   *big.Int `json:"arrowGlacierBlock,omitempty"`   // Eip-4345(难度炸弹延迟)切换区块(nil = 无硬分叉,0 = 已激活)
    GrayGlacierBlock    *big.Int `json:"grayGlacierBlock,omitempty"`    // Eip-5133(难度炸弹延迟)切换区块(nil = 无硬分叉,0 = 已激活)
    MergeNetsplitBlock  *big.Int `json:"mergeNetsplitBlock,omitempty"`  // 在合并(The Merge)后用于网络拆分的虚拟硬分叉

    // 硬分叉调度已从区块改为时间戳

    ShanghaiTime *uint64 `json:"shanghaiTime,omitempty"` // Shanghai 切换时间(nil = 无硬分叉,0 = 已在 Shanghai)
    CancunTime   *uint64 `json:"cancunTime,omitempty"`   // Cancun 切换时间(nil = 无硬分叉,0 = 已在 Cancun)
    PragueTime   *uint64 `json:"pragueTime,omitempty"`   // Prague 切换时间(nil = 无硬分叉,0 = 已在 Prague)
    OsakaTime    *uint64 `json:"osakaTime,omitempty"`    // Osaka 切换时间(nil = 无硬分叉,0 = 已在 Osaka)
    VerkleTime   *uint64 `json:"verkleTime,omitempty"`   // Verkle 切换时间(nil = 无硬分叉,0 = 已在 Verkle)
    BPO1Time     *uint64 `json:"bpo1Time,omitempty"`     // BPO1 切换时间(nil = 无硬分叉,0 = 已在 BPO1)
    BPO2Time     *uint64 `json:"bpo2Time,omitempty"`     // BPO2 切换时间(nil = 无硬分叉,0 = 已在 BPO2)
    BPO3Time     *uint64 `json:"bpo3Time,omitempty"`     // BPO3 切换时间(nil = 无硬分叉,0 = 已在 BPO3)
    BPO4Time     *uint64 `json:"bpo4Time,omitempty"`     // BPO4 切换时间(nil = 无硬分叉,0 = 已在 BPO4)
    BPO5Time     *uint64 `json:"bpo5Time,omitempty"`     // BPO5 切换时间(nil = 无硬分叉,0 = 已在 BPO5)

    // TerminalTotalDifficulty 是网络达到的总难度量,它会触发共识升级。
    TerminalTotalDifficulty *big.Int `json:"terminalTotalDifficulty,omitempty"`

    DepositContractAddress common.Address `json:"depositContractAddress,omitempty"`

    // EnableVerkleAtGenesis 是一个标志,指定网络是否从创世区块开始使用
    // Verkle 树。如果设置为 true,创世状态将使用 Verkle 树提交,
    // 从而无需后续进行任何 Verkle 转换。
    //
    // 这只是 Verkle 开发网络测试的临时标志,其中 Verkle 从
    // 创世区块激活,并且配置的激活日期已经过去。
    //
    // 在生产网络(主网和公共测试网)中,Verkle 激活
    // 总是发生在创世区块之后,使此标志在
    // 这些情况下无关紧要。
    EnableVerkleAtGenesis bool `json:"enableVerkleAtGenesis,omitempty"`

    // 各种共识引擎
    Ethash             *EthashConfig       `json:"ethash,omitempty"`
    Clique             *CliqueConfig       `json:"clique,omitempty"`
    BlobScheduleConfig *BlobScheduleConfig `json:"blobSchedule,omitempty"`
}

ChainConfig 结构体概述

ChainConfig 是决定区块链设置的核心配置。它以每个区块为基础存储在数据库中,这意味着任何由其创世区块标识的网络(无论是主网、测试网还是私有链),都可以拥有自己的一套配置选项,从而实现不同网络间的差异化协议和功能。

字段解释

  1. ChainID *big.Int

    解释:ChainID(链ID)标识当前区块链网络,并且是用于防止交易重放攻击的关键机制。当用户签署一笔交易时,交易数据中会包含该 ChainID。如果这笔交易被复制到另一个具有不同 ChainID 的网络上,该交易将因 ChainID 不匹配而被视为无效,从而保护用户资产。

  2. HomesteadBlock *big.Int

    解释:这是 Homestead 硬分叉的切换区块高度。

    • nil:表示该链没有发生 Homestead 硬分叉,或者说还未达到 Homestead 阶段。
    • 0:表示该链从创世区块开始就已经处于 Homestead 阶段。

    背景:Homestead 是以太坊主网的第二次重大升级,引入了一些重要的改进,例如取消了未使用的交易类型、改进了 Gas 算法等。意味着从此高度开始,新区块受 homested 版本共识规则约束。 因涉及共识变更,如果希望继续接受新区块则必须升级以太坊程序,属于区块链硬分叉。 如果不愿意接受共识变更,则可以独立使用新的 ChainID 继续原共识,且必须独立维护版本。

  3. DAOForkBlock *big.Int 解释:这是 TheDAO 硬分叉的切换区块高度。

    • nil:表示该链没有发生 TheDAO 硬分叉。

    背景:TheDAO 硬分叉是以太坊历史上最著名的事件之一。2016年,一个名为 TheDAO 的去中心化自治组织合约被黑客攻击,导致大量以太币被盗。社区就是否“回滚”交易记录以追回资产进行了激烈辩论,最终以太坊社区投票决定进行硬分叉,分叉后的链成为了当前的以太坊主网,而未回滚的链则成为了以太坊经典 (Ethereum Classic)。

  4. DAOForkSupport bool 解释:指示当前节点是支持还是反对 DAO 硬分叉。这是一个历史性的标志,在 DAO 硬分叉期间用于区分支持和反对该分叉的节点行为。

  5. EIP150Block *big.Int 解释:EIP-150 硬分叉的切换区块高度。

    • nil:表示该链没有启用 EIP-150。

    背景:EIP-150 引入了 Gas 价格的变化(例如,通过调整 Gas 成本来限制拒绝服务攻击),旨在提高网络的稳定性。

  6. EIP155Block *big.Int 解释:EIP-155 硬分叉的切换区块高度。 背景:EIP-155 进一步增强了交易的重放保护,它强制交易签名包含 ChainID,使得不同区块链网络之间的交易更难被混淆和重放。

  7. EIP158Block *big.Int 解释:EIP-158 硬分叉的切换区块高度。

    背景:EIP-158 引入了状态清理规则,使得空账户(无余额且无交易记录的账户)可以被移除,有助于修剪区块链状态树的大小。

  8. ByzantiumBlock *big.Int 解释:Byzantium(拜占庭)硬分叉的切换区块高度。

    • nil:表示无此硬分叉。
    • 0:表示该链从创世区块开始就已在 Byzantium 阶段。

    背景:Byzantium 是以太坊大都會(Metropolis)升级的第一阶段,引入了多种改进,包括交易收据中的状态根、预编译合约的更新等。

  9. ConstantinopleBlock *big.Int 解释:Constantinople(君士坦丁堡)硬分叉的切换区块高度。

    • nil:表示无此硬分叉。
    • 0:表示该链从创世区块开始就已激活此硬分叉。

    背景:Constantinople 是大都會升级的第二阶段,引入了更多的 EIP,包括更高效的虚拟机指令、难度炸弹的延迟等。

  10. PetersburgBlock *big.Int 解释:Petersburg(圣彼得堡)硬分叉的切换区块高度。

    • nil:PetersburgBlock 是一个独立的硬分叉,虽然它在以太坊主网上与 ConstantinopleBlock 在同一区块高度激活,但它们是两个不同的配置项。Petersburg 的目的是移除 Constantinople 中一个被发现有安全漏洞的 EIP。因此,一个网络可以只激活其中一个,或者在同一区块激活两个。如果 PetersburgBlock 为 nil,则意味着 Petersburg 规则不生效。

    背景:Petersburg 可以被视为对 Constantinople 的紧急修复,主要处理了一些安全漏洞,并且也延迟了难度炸弹。

  11. IstanbulBlock *big.Int 解释:Istanbul(伊斯坦布尔)硬分叉的切换区块高度。

    • nil:表示无此硬分叉。
    • 0:表示该链从创世区块开始就已在 Istanbul 阶段。 背景:Istanbul 再次延迟了难度炸弹,并引入了费用模型优化、预编译合约更新等 EIP。
  12. MuirGlacierBlock *big.Int 解释:EIP-2384(难度炸弹延迟)切换区块高度。

    • nil:表示无此硬分叉。
    • 0:表示该链从创世区块开始就已激活此硬分叉。

    背景:专门用于再次延迟以太坊的“难度炸弹”,确保在 PoS Transition 之前网络可以平稳运行。

  13. BerlinBlock *big.Int 解释:Berlin(柏林)硬分叉的切换区块高度。

    • nil:表示无此硬分叉。
    • 0:表示该链从创世区块开始就已在 Berlin 阶段。

    背景:Berlin 主要优化了 Gas 成本和一些安全补丁。

  14. LondonBlock *big.Int 解释:London(伦敦)硬分叉的切换区块高度。

    • nil:表示无此硬分叉。
    • 0:表示该链从创世区块开始就已在 London 阶段。 背景:London 引入了 EIP-1559,改变了交易费用机制,引入了基础费用燃烧(base fee burning)和最大优先级费用(max priority fee),旨在使 Gas 费用更可预测。
  15. ArrowGlacierBlock *big.Int 解释:EIP-4345(难度炸弹延迟)切换区块高度。

    • nil:表示无此硬分叉。
    • 0:表示该链从创世区块开始就已激活此硬分叉。

    背景:又一次用于延迟难度炸弹,为以太坊合并做准备。

  16. GrayGlacierBlock *big.Int 解释:EIP-5133(难度炸弹延迟)切换区块高度。

    • nil:表示无此硬分叉。
    • 0:表示该链从创世区块开始就已激活此硬分叉。

    背景:再次延迟难度炸弹,确保以太坊的 PoW 链在合并之前有足够长的时间。

  17. MergeNetsplitBlock *big.Int 解释:为了防止网络分裂。在“合并”(The Merge)期间,如果一个节点未能成功切换到权益证明(PoS)链,它可能会继续遵循旧的工作量证明(PoW)规则。MergeNetsplitBlock 标志着一个区块,在此之后,任何只支持 PoW 的节点都应停止处理区块。这确保了旧的 PoW 链不会意外地继续下去,从而防止了网络分裂,保障了平稳过渡。它不是一个“虚拟硬分叉”,而是一个停止 PoW 链的信号。 背景:The Merge 是以太坊从工作量证明 (PoW) 过渡到权益证明 (PoS) 的关键事件。

以太坊兼容或基于以太坊协议的区块链网络

链ID (Chain ID) 名称 (Name) 简称 (Short Name) 链 (Chain) 网络 (Network) 网络ID (Network ID)
1 以太坊主网 (Ethereum Mainnet) eth (eth) ETH (ETH) 主网 (mainnet) 1
2 Expanse 网络 (Expanse Network) exp (exp) EXP (EXP) 主网 (mainnet) 1
3 以太坊测试网 Ropsten (Ethereum Testnet Ropsten) rop (rop) ETH (ETH) Ropsten (ropsten) 3
4 以太坊测试网 Rinkeby (Ethereum Testnet Rinkeby) rin (rin) ETH (ETH) Rinkeby (rinkeby) 4
5 以太坊测试网 Görli (Ethereum Testnet Görli) gor (gor) ETH (ETH) Goerli (goerli) 5
6 以太坊经典测试网 Kotti (Ethereum Classic Testnet Kotti) kot (kot) ETC (ETC) Kotti (kotti) 6
8 Ubiq 网络主网 (Ubiq Network Mainnet) ubq (ubq) UBQ (UBQ) 主网 (mainnet) 1
9 Ubiq 网络测试网 (Ubiq Network Testnet) tubq (tubq) UBQ (UBQ) 主网 (mainnet) 2
28 以太坊社交 (Ethereum Social) etsc (etsc) ETSC (ETSC) 主网 (mainnet) 1
30 RSK 主网 (RSK Mainnet) rsk (rsk) RSK (RSK) 主网 (mainnet) 775
31 RSK 测试网 (RSK Testnet) trsk (trsk) RSK (RSK) 测试网 (testnet) 8052
42 以太坊测试网 Kovan (Ethereum Testnet Kovan) kov (kov) ETH (ETH) Kovan (kovan) 42
60 GoChain (GoChain) go (go) GO (GO) 主网 (mainnet) 60
61 以太坊经典主网 (Ethereum Classic Mainnet) etc (etc) ETC (ETC) 主网 (mainnet) 1
62 以太坊经典测试网 (Ethereum Classic Testnet) tetc (tetc) ETC (ETC) 测试网 (testnet) 2
64 Ellaism (Ellaism) ella (ella) ELLA (ELLA) 主网 (mainnet) 1
76 Mix (Mix) mix (mix) MIX (MIX) 主网 (mainnet) 1
77 POA Network Sokol (POA Network Sokol) poa (poa) POA (POA) Sokol (sokol) 1
88 TomoChain (TomoChain) tomo (tomo) TOMO (TOMO) 主网 (mainnet) 88
99 POA Network Core (POA Network Core) skl (skl) POA (POA) Core (core) 2
100 xDAI 链 (xDAI Chain) xdai (xdai) XDAI (XDAI) 主网 (mainnet) 1
101 Webchain (Webchain) web (web) WEB (WEB) 主网 (mainnet) 37129
101 EtherInc (EtherInc) eti (eti) ETI (ETI) 主网 (mainnet) 1
820 Callisto 主网 (Callisto Mainnet) clo (clo) CLO (CLO) 主网 (mainnet) 1
821 Callisto 测试网 (Callisto Testnet) tclo (tclo) CLO (CLO) 测试网 (testnet) 2
1620 Atheios (Atheios) ath (ath) ATH (ATH) 主网 (mainnet) 11235813
1856 Teslafunds (Teslafunds) tsf (tsf) TSF (TSF) 主网 (mainnet) 1
1987 EtherGem (EtherGem) egem (egem) EGEM (EGEM) 主网 (mainnet) 1987
2018 EOS Classic (EOS Classic) eosc (eosc) EOSC (EOSC) 主网 (mainnet) 1
24484 Webchain (在区块 xxxxxxx 之后) (Webchain (after block xxxxxxx)) web (web) WEB (WEB) 主网 (mainnet) 37129
31102 Ethersocial Network (Ethersocial Network) esn (esn) ESN (ESN) 主网 (mainnet) 1
200625 Akaroma (Akaroma) aka (aka) AKA (AKA) 主网 (mainnet) 200625
246529 ARTIS sigma1 (ARTIS sigma1) ats (ats) ARTIS (ARTIS) sigma1 (sigma1) 246529
246785 ARTIS tau1 (ARTIS tau1) ats (ats) ARTIS (ARTIS) tau1 (tau1) 246785
1313114 Ether-1 (Ether-1) etho (etho) ETHO (ETHO) 主网 (mainnet) 1313114
7762959 Musicoin (Musicoin) music (music) MUSIC (MUSIC) 主网 (mainnet) 7762959
18289463 IOLite (IOLite) ilt (ilt) ILT (ILT) 主网 (mainnet) 18289463
3125659152 Pirl (Pirl) pirl (pirl) PIRL (PIRL) 主网 (mainnet) 3125659152
385 Lisinski (Lisinski) lisinski (lisinski) CRO (CRO) 主网 (mainnet) 385
108 ThunderCore 主网 (ThunderCore Mainnet) TT (TT) TT (TT) 主网 (mainnet) 108
18 ThunderCore 测试网 (ThunderCore Testnet) TST (TST) TST (TST) 测试网 (testnet) 18
11 Metadium 主网 (Metadium Mainnet) meta (meta) META (META) 主网 (mainnet) 11
12 Metadium 测试网 (Metadium Testnet) kal (kal) META (META) 测试网 (testnet) 12
13371337 PepChain Churchill (PepChain Churchill) tpep (tpep) PEP (PEP) 测试网 (testnet) 13371337

创世区块

如果要搭建以太坊私有链的话,了解创世配置是配置的,示例配置:

这个配置是一个 以太坊创世区块(Genesis Block) 的定义。创世区块是区块链的第一个区块(区块号为 0),它没有前一个区块,并定义了网络启动时的初始状态和一系列网络规则。

下面是对每个字段的详细解释:

最外层(创世区块的头部和初始状态)

这些字段定义了创世区块的基本属性,类似于任何一个以太坊区块的头部,但针对创世区块有特殊的初始值。

  • nonce: “0x42”
    • 含义: 一个64位的哈希值,矿工挖矿时尝试找到的随机数。对于创世区块,它是一个任意的初始值,0x42 是一个常见的惯例,可以理解为一个固定的魔术数字或占位符。
  • timestamp: “0x0”
    • 含义: 区块被挖出的时间戳(Unix时间)。对于创世区块,通常设为 0x0 (格林威治标准时间 1970/1/1 00:00:00),或者一个非常早的时间。
  • extraData: “0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa”
    • 含义: 矿工可以包含的任意数据。对于以太坊主网的创世区块,这个值包含了拉丁语的 “GenesiS” 和一个笑脸表情的编码,是一个非常有标志性的字段。
  • gasLimit: “0x1388”
    • 含义: 区块中所有交易允许消耗的最大 Gas 量。在创世区块中,它是一个初始上限(0x1388 十进制为 5000)。未来的区块会动态调整这个值。
  • difficulty: “0x400000000”
    • 含义: 挖出当前区块的难度值。对于创世区块,它被设置为一个初始值,既不能太低导致轻易挖出,也不能太高导致无法启动。0x400000000 是以太坊主网创世区块的初始难度。
  • mixHash: “0x0000000000000000000000000000000000000000000000000000000000000000”
    • 含义: 与 nonce 一起,用于工作量证明(PoW)的哈希值。对于创世区块,因为没有上一个区块参与 PoW 计算,通常设为全零。
  • coinbase: “0x0000000000000000000000000000000000000000”
    • 含义: 接收当前区块奖励(如果存在)的矿工地址。对于创世区块,通常设为零地址,因为创世区块本身没有挖矿奖励。
  • number: “0x0”
    • 含义: 区块号。创世区块始终是区块 0
  • gasUsed: “0x0”
    • 含义: 当前区块中所有交易实际消耗的总 Gas 量。对于创世区块,没有包含交易,所以是 0x0
  • parentHash: “0x0000000000000000000000000000000000000000000000000000000000000000”
    • 含义: 上一个区块的哈希值。由于创世区块是链上的第一个区块,它没有父区块,因此 parentHash 设为全零。

config 对象(网络配置和硬分叉时间表)

这个对象定义了网络的关键配置参数,包括 Chain ID 和一系列硬分叉(Hard Fork)的激活区块号。这些通常与以太坊主网的硬分叉历史保持一致。

  • chainId: 1
    • 含义: 网络的唯一标识符。1 是以太坊主网的 Chain ID。
  • homesteadBlock: 1150000
    • 含义: Homestead 硬分叉激活的区块号。Homestead 是以太坊的第二个主要版本。
  • daoForkBlock: 1920000
    • 含义: DAO 硬分叉激活的区块号。这是因为 DAO 攻击事件而进行的一次社群驱动的强制性协议升级。
  • daoForkSupport: true
    • 含义: 指示客户端是否支持 DAO 硬分叉。true 表示客户端将遵循 DAO 硬分叉后的链(即以太坊主网)。如果为 false,则会遵循 ETC(以太坊经典)链。
  • eip150Block: 2463000
    • 含义: EIP-150(Tangerine Whistle Hardfork,橘皮哨音硬分叉)激活的区块号。此硬分叉修改了某些操作码的 Gas 成本。
  • eip150Hash: “0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0”
    • 含义: EIP-150 硬分叉激活区块的哈希值。这是一个额外的校验,确保客户端在正确的链上执行硬分叉。
  • eip155Block: 2675000
    • 含义: EIP-155(Spurious Dragon Hardfork,虚假龙硬分叉)激活的区块号。引入了交易回放保护。
  • eip158Block: 2675000
    • 含义: EIP-158(Spurious Dragon Hardfork,虚假龙硬分叉的一部分)激活的区块号。此硬分叉改变了空账户的状态清除规则。它通常与 EIP-155 在同一区块激活。
  • byzantiumBlock: 4370000
    • 含义: Byzantium(拜占庭)硬分叉激活的区块号。引入了一些重要的协议改进,如改进的难度调整算法和预编译合约。
  • constantinopleBlock: 7280000
    • 含义: Constantinople(君士坦丁堡)硬分叉激活的区块号。包含延迟难度炸弹、新的操作码等。
  • petersburgBlock: 7280000
    • 含义: Petersburg(彼得堡)硬分叉激活的区块号。这是一个紧急更新,与君士坦丁堡硬分叉在同一区块激活,主要用于修复君士坦丁堡中发现的安全漏洞。
  • ethash: {}
    • 含义: Ethash 工作量证明算法的特定配置。一个空对象表示使用默认的 Ethash 参数,这对于标准以太坊链是常见的。

alloc 对象(初始账户分配)

这个对象定义了在链启动时,有哪些账户拥有多少初始资金。

  • "000d836201318ec6899a67540690382780743280":
    • balance: “0xad78ebc5ac6200000”
      • 含义: 一个以太坊地址 (0x000d836...),在链启动时被分配了十六进制表示的 Wei 余额。
      • 0xad78ebc5ac6200000 转换为十进制是 500,000,000,000,000,000,000 Wei。由于 1 Ether = 10^18 Wei,这意味着这个地址拥有 500,000,000 (5亿) ETH
  • "001762430ea9c3a26e5749afdb70da5f78ddbb8c":
    • balance: “0xad78ebc5ac6200000”
      • 含义: 另一个以太坊地址 (0x0017624...),同样被分配了 500,000,000 (5亿) ETH 的初始余额。

总结: alloc 部分列出了在创始区块时预分配大量 Ether 的两个账户。这些账户在以太坊主网的创世区块中扮演了重要角色,代表了在初始众筹中分配的资金。

账户

比特币使用 “UTXO” 余额模型,以太坊使用“账户”余额模型。 以太坊丰富了账户内容,除余额外还能自定义存放任意多数据。 并利用账户数据的可维护性,构建智能合约账户。

以太坊账户分为外部账户(EOAs)和合约账户(contract account)

外部账户

EOAs-外部账户(external owned accouts)是通过私钥创建的账户。

特点:

  1. 拥有以太余额。
  2. 能发送交易,包括转账和执行合约代码。
  3. 被私钥控制。
  4. 没有相关的可执行代码。

合约账户

含有合约代码的账户。 被外部账户或者合约创建,合约在创建时被自动分配到一个账户地址, 用于存储合约代码以及合约部署或执行过程中产生的存储数据。 合约账户地址是通过SHA3哈希算法产生,而非私钥。

因为没有私钥,因此无人可以拿合约账户当做外部账户使用。 只能通过外部账户来驱动合约执行合约代码。

因为合约由其他账户创建,因此将创建者地址和该交易的随机数进行哈希后截取部分生成。

下面是代码实现:

// crypto/crypto.go
// CreateAddress 根据给定的发送者地址(或父地址)和 nonce 创建一个以太坊地址。
func CreateAddress(b common.Address, nonce uint64) common.Address {
    // 使用 RLP 编码将发送者地址 'b' 和 'nonce' 序列化为一个字节数组。
    // 这是标准的以太坊地址创建机制之一,通常用于计算合约地址:
    // 新地址 = Keccak256(RLP编码(发送者地址, nonce))[12:]
    data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})

    // 计算 RLP 编码数据 'data' 的 Keccak-256 哈希值。
    // Keccak-256 哈希会生成一个 32 字节的结果。
    // 以太坊地址是该 32 字节哈希的最后 20 字节。
    // 例如,如果哈希是 `0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` (32字节)
    // 那么 `[12:]` 会截取出 `0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` (最后 20 字节)。
    return common.BytesToAddress(Keccak256(data)[12:])
}

需要注意的是,在EIP1014中提出的另一种生成合约地址的算法。 其目的是为状态通道提供便利,通过确定内容输出稳定的合约地址。 在部署合约前就可以知道确切的合约地址。 下面是算法方法:keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]

// CreateAddress2 根据给定的发送者地址(或父地址)、初始合约代码哈希和一个盐值来创建以太坊地址。
// 这个函数实现了 EIP-1014 中定义的 CREATE2 操作码的地址计算逻辑。
func CreateAddress2(b common.Address, salt [32]byte, inithash []byte) common.Address {
    // CREATE2 地址的计算公式如下:
    // 新地址 = Keccak256(0xff + 发送者地址字节 + 盐值字节 + 初始合约代码哈希)[12:]
    //
    // - 0xff: 一个固定的单字节前缀,用于防止与 CREATE 操作码的地址计算冲突,并明确标识这是一个 CREATE2 地址。
    // - b.Bytes(): 发送者的 20 字节地址。
    // - salt[:]: 一个 32 字节的盐值,用于确保地址的唯一性和可预测性。
    //   通过更改盐值,即使初始代码相同,也可以多次部署到不同的地址。
    // - inithash: 初始合约代码的 Keccak-256 哈希值。它通常是部署前合约字节码的哈希。
    //
    // Keccak256 函数会将所有传入的字节数组连接起来,然后计算它们的 Keccak-256 哈希。
    // 最终的以太坊地址是这个 32 字节哈希的最后 20 字节(通过 [12:] 切片获取)。
    return common.BytesToAddress(Keccak256([]byte{0xff}, b.Bytes(), salt[:], inithash)[12:])
}

合约账户特点:

  • 拥有以太余额。
  • 有相关的可执行代码(合约代码)。
  • 合约代码能够被交易或者其他合约消息调用。
  • 合约代码被执行时可再调用其他合约代码。
  • 合约代码被执行时可执行复杂运算,可永久地改变合约内部的数据存储。

账户数据结构

以太坊数据以账户为单位组织,账户数据的变更引起账户状态变化。 从而引起以太坊状态变化(关于以太坊状态,后续另写文章介绍)。

在程序逻辑上两类账户的数据结构一致:

// core/state/state_object.go
// StateAccount 是以太坊共识层中账户的表示。
// 这些对象存储在主账户 Merkle Patricia Tries (MPT) 中。
type StateAccount struct {
    Nonce    uint64 // 账户的交易计数(对于外部账户)或已创建合约的计数(对于合约账户)。
    Balance  *uint256.Int // 账户中持有的 Wei 数量。使用 uint256.Int 表示大整数。
    Root     common.Hash // 存储 Merkle Patricia Trie 的根哈希。这个 Trie 存储了合约账户的所有存储变量和数据。
    CodeHash []byte // 合约账户的字节码的 Keccak-256 哈希。对于外部账户,此字段为空(Keccak256(nil))。
}

由于外部账户无内部存储数据和合约代码,因此外部账户数据中 StateRootHashCodeHash 是一个空默认值。一旦属于空默认值,则不会存储对应物理数据库中。 在程序逻辑上,存在code则为合约账户。 即 CodeHash 为空值时,账户是一个外部账户,否则是合约账户。

graph TD
    subgraph LevelDB
        direction LR
        subgraph Account
            A[Account] --- Nonce(Nonce)
            A --- Balance(Balance)
            A --- CodeHash(CodeHash)
            A --- StateRootHash(StateRootHash)

            style Account fill:#fff,stroke:#333,stroke-width:2px,rx:5px,ry:5px
            style Nonce fill:#fff,stroke:#333,stroke-width:1px
            style Balance fill:#fff,stroke:#333,stroke-width:1px
            style CodeHash fill:#fff,stroke:#333,stroke-width:1px
            style StateRootHash fill:#fff,stroke:#333,stroke-width:1px
        end

        Code[["` Code`"]]
        style Code fill:#fff,stroke:#333,stroke-width:2px,rx:5px,ry:5px

        subgraph State_Tree["State Tree"]
            S1(( )) --> S2(( ))
            S1(( )) --> S3(( ))
            S2(( )) --> S4(( ))
            S3(( )) --> S5(( ))
            S3(( )) --> S6(( ))
            S6(( )) --> S7(( ))
            S6(( )) --> S8(( ))

            style S1 fill:#333,stroke:#333,stroke-width:1px
            style S2 fill:#333,stroke:#333,stroke-width:1px
            style S3 fill:#333,stroke:#333,stroke-width:1px
            style S4 fill:#333,stroke:#333,stroke-width:1px
            style S5 fill:#333,stroke:#333,stroke-width:1px
            style S6 fill:#333,stroke:#333,stroke-width:1px
            style S7 fill:#333,stroke:#333,stroke-width:1px
            style S8 fill:#333,stroke:#333,stroke-width:1px
            style State_Tree fill:#fff,stroke:#333,stroke-width:2px,rx:5px,ry:5px
        end

        CodeHash ---> Code
        StateRootHash ---> S1

        linkStyle 9 stroke-width:2px,fill:none,stroke:black;
        linkStyle 10 stroke-width:2px,fill:none,stroke:black;
    end
    style LevelDB fill:#eef,stroke:#333,stroke-width:2px,ry:30px,rx:30px,border-radius:50%/10% leveledb-shape;
graph TD
    subgraph LevelDB
        direction LR
        subgraph Account
            A[Account] --- Nonce(Nonce)
            A --- Balance(Balance)
            A --- CodeHash(CodeHash)
            A --- StateRootHash(StateRootHash)

            style Account fill:#fff,stroke:#333,stroke-width:2px,rx:5px,ry:5px
            style Nonce fill:#fff,stroke:#333,stroke-width:1px
            style Balance fill:#fff,stroke:#333,stroke-width:1px
            style CodeHash fill:#fff,stroke:#333,stroke-width:1px
            style StateRootHash fill:#fff,stroke:#333,stroke-width:1px
        end

        Code[["` Code`"]]
        style Code fill:#fff,stroke:#333,stroke-width:2px,rx:5px,ry:5px

        subgraph State_Tree["State Tree"]
            S1(( )) --> S2(( ))
            S1(( )) --> S3(( ))
            S2(( )) --> S4(( ))
            S3(( )) --> S5(( ))
            S3(( )) --> S6(( ))
            S6(( )) --> S7(( ))
            S6(( )) --> S8(( ))

            style S1 fill:#333,stroke:#333,stroke-width:1px
            style S2 fill:#333,stroke:#333,stroke-width:1px
            style S3 fill:#333,stroke:#333,stroke-width:1px
            style S4 fill:#333,stroke:#333,stroke-width:1px
            style S5 fill:#333,stroke:#333,stroke-width:1px
            style S6 fill:#333,stroke:#333,stroke-width:1px
            style S7 fill:#333,stroke:#333,stroke-width:1px
            style S8 fill:#333,stroke:#333,stroke-width:1px
            style State_Tree fill:#fff,stroke:#333,stroke-width:2px,rx:5px,ry:5px
        end

        CodeHash ---> Code
        StateRootHash ---> S1

        linkStyle 9 stroke-width:2px,fill:none,stroke:black;
        linkStyle 10 stroke-width:2px,fill:none,stroke:black;
    end
    style LevelDB fill:#eef,stroke:#333,stroke-width:2px,ry:30px,rx:30px,border-radius:50%/10% leveledb-shape;
graph TD
    subgraph LevelDB
        direction LR
        subgraph Account
            A[Account] --- Nonce(Nonce)
            A --- Balance(Balance)
            A --- CodeHash(CodeHash)
            A --- StateRootHash(StateRootHash)

            style Account fill:#fff,stroke:#333,stroke-width:2px,rx:5px,ry:5px
            style Nonce fill:#fff,stroke:#333,stroke-width:1px
            style Balance fill:#fff,stroke:#333,stroke-width:1px
            style CodeHash fill:#fff,stroke:#333,stroke-width:1px
            style StateRootHash fill:#fff,stroke:#333,stroke-width:1px
        end

        Code[["` Code`"]]
        style Code fill:#fff,stroke:#333,stroke-width:2px,rx:5px,ry:5px

        subgraph State_Tree["State Tree"]
            S1(( )) --> S2(( ))
            S1(( )) --> S3(( ))
            S2(( )) --> S4(( ))
            S3(( )) --> S5(( ))
            S3(( )) --> S6(( ))
            S6(( )) --> S7(( ))
            S6(( )) --> S8(( ))

            style S1 fill:#333,stroke:#333,stroke-width:1px
            style S2 fill:#333,stroke:#333,stroke-width:1px
            style S3 fill:#333,stroke:#333,stroke-width:1px
            style S4 fill:#333,stroke:#333,stroke-width:1px
            style S5 fill:#333,stroke:#333,stroke-width:1px
            style S6 fill:#333,stroke:#333,stroke-width:1px
            style S7 fill:#333,stroke:#333,stroke-width:1px
            style S8 fill:#333,stroke:#333,stroke-width:1px
            style State_Tree fill:#fff,stroke:#333,stroke-width:2px,rx:5px,ry:5px
        end

        CodeHash ---> Code
        StateRootHash ---> S1

        linkStyle 9 stroke-width:2px,fill:none,stroke:black;
        linkStyle 10 stroke-width:2px,fill:none,stroke:black;
    end
    style LevelDB fill:#eef,stroke:#333,stroke-width:2px,ry:30px,rx:30px,border-radius:50%/10% leveledb-shape;

上图是以太坊账户数据存储结构,账户内部实际只存储关键数据,而合约代码以及合约自身数据则通过对应的哈希值关联。 因为每个账户对象,将作为一个以太坊账户树的一个叶子数据存储, 不能太大。

  1. LevelDB:

    • 最外层最大的容器代表 LevelDB,这是一个高性能的键值存储数据库,常被以太坊客户端(如 Geth、OpenEthereum)用作底层数据存储。所有以太坊的状态数据、交易数据、区块数据等最终都会以键值对的形式存储在 LevelDB 中。
  2. Account (账户):

    • Account 是以太坊中的核心概念,分为两类:外部账户(Externally Owned Account, EOA)和合约账户(Contract Account)。图中展示的是一个通用的账户结构,其中包含了四个主要字段:
      • Nonce: 一个非负整数。
        • 对于 EOA,它表示该账户已发送的交易数量。
        • 对于合约账户,它表示该合约创建的新合约数量。
        • Nonce 主要用于防止交易重放攻击,并确保交易的有序执行。
      • Balance: 该账户持有的以太币(ETH)数量,以 Wei(ETH 的最小单位)计。
      • CodeHash: 这是账户代码的 Keccak-256 哈希值。
        • 如果是一个 EOA,CodeHash 通常是空字符串的哈希(或一个特定全零哈希)。
        • 如果是一个合约账户,CodeHash 指向实际的智能合约字节码(Code)。箭头从 CodeHash 指向 Code,表示 CodeHashCode 的一个引用或标识。
      • StateRootHash: 这是账户的存储状态(Storage State)的根哈希。
        • 它是一个 Merkle Patricia Trie(更准确地说是 Merkle-Patricia-Trie 的根哈希)的根哈希值。这个 Trie 存储了合约的所有存储变量。
        • 如果是一个 EOA,StateRootHash 将是一个空 Trie 的哈希。
        • 如果是一个合约账户,StateRootHash 指向该合约的私有存储空间,也就是图中的 State Tree。箭头从 StateRootHash 指向 State Tree 的根节点,表示 StateRootHashState Tree 的引用。
  3. Code (代码):

    • 图中的 Code 块代表智能合约的实际字节码。当一个合约被部署时,其字节码会被存储到 LevelDB 中,并通过 CodeHash 引用。
  4. State Tree (状态树):

    • 图中的树形结构代表一个 Merkle Patricia Trie,我们称之为存储状态树。这个树用于存储智能合约的所有状态变量(即键值对数据)。
    • StateRootHash 是这棵树的根哈希。这棵树的每个节点(分支节点、叶子节点)都存储在 LevelDB 中,并通过其哈希值引用。通过验证 StateRootHash,可以高效地验证合约的某个特定存储变量是否存在以及它的值是什么(Merkle Proof)。

账户 Nonce 起始值是 0,后续每触发一次账户执行则 Nonce 值计加一次。这样的附加好处是,一般可将 Nonce 当做账户的交易次数计数器使用,特别是对于合约账户可以准确的记录合约被调用次数。

代码在这里:

// core/state_transition.go

st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)

Balance则记录该账户所拥有的 ETH 数量,称为账户余额,这里的余额的单位是 Wei 。

转账操作:

// core/evm.go
// 执行余额增减的底层、原子性操作。它不关心转账是否合法,只负责在给定vm.StateDB(表示区块链状态数据库)上,将指定amount从sender的账户中减去,并加到recipient的账户中。
// vm.StateDB: 这是EVM管理账户状态(包括余额、nonce、存储等)的核心接口或结构
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) {
    db.SubBalance(sender, amount)
    db.AddBalance(recipient, amount)
}

实际执行转账之前的检查,验证调用者是否有足够的余额进行转账:

// core/vm/evm.go
// evm.Context: EVM的执行上下文,包含了当前的区块链状态、配置、调用信息等。
// 如果余额不足,EVM会立即停止执行当前操作,返回ErrInsufficientBalance错误
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
    return nil, common.Address{}, gas, ErrInsufficientBalance
}

实际转账操作的代码:

// core/vm/evm.go
evm.Context.Transfer(evm.StateDB, caller, address, value)

交易(Transaction)

交易(Transaction)是指由一个外部账户转移一定资产给某个账户, 或者发出一个消息指令到某个智能合约。

在以太坊网络中,交易执行属于一个事务。具有原子性、一致性、隔离性、持久性特点。

  • 原子性: 是不可分割的最小执行单位,要么做,要么不做。
  • 一致性: 同一笔交易执行,必然是将以太坊账本从一个一致性状态变到另一个一致性状态。
  • 隔离性: 交易执行途中不会受其他交易干扰。
  • 持久性: 一旦交易提交,则对以太坊账本的改变是永久性的。后续的操作不会对其有任何影响。

因为是事务型,因此我们需确保在执行事务前让交易符合一些设计要求。

  • 交易必须唯一,能区分不同交易且同一笔交易不能重复提交到账本中。
  • 交易内容不得变化,每个节点收到的交易都必须一致,交易执行时账本状态变化也是一致的。
  • 交易必须被合法签名,只有已正确签名的交易才能被执行。
  • 交易不能占用过多系统资源,影响其他交易执行。

交易数据结构

下图是以太坊交易的数据结构,根据用途主要划分为4个部分

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 685.1875 134" width="685.1875" height="134"><!-- svg-source:excalidraw --><metadata></metadata><defs><style class="style-fonts">
      @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAVcABEAAAAADiwAAAUDAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKhTiEQQsGAAE2AiQDCAQgBYJeByAMFBuCDFEEGwcQ0jxP9uMgJzs14TRO0ITicbrYzikLoc7HKWK3tpPx8LRGe39mTjSJahZDxEPkGqFQL1RK3U4oRJpqot5erW0fz1TdH6qZRxEN9RKl0o52ZCuBlGH497SXzUmt1K8HKCOznIh0krId4HYH4Xv+/6+1iXPnr7ZdpIuXxufMw0qllBV7mFsTCwVSJFlTC8UjXgmJGpllbrRRMbDi9/hMJtAOiNokRDSthhZyh2dyNZApvwPaWQRwfM5v4qqBJCGeZ1GVrZCNSNEohw8Zmk25dC/ZhNDxkw7J63oNkgu4PhPFq3cOUkM6xdiaXsY54i6aG5lTZAxFjbbj13WNS0+2ITYjryt4kxVLF5n2a/fuESf3WdQvK4fDD+3y0yd6JpYQ45MYjWfLTFpiYoHXM3kJEzRPJyotbRvJGDnfJ2MJHHf6tExn7t5bDch9I1I3702QunWPp7tthTdj0G579ExZQ5Rx5D4mcDpbTN2M/xKP1bCURZpnSq1yi9btJ5TNUwlKieM6n+az4XHNZwklTlGDuoHHKd1Z/Z4VsvPCs9jDxvK8VuAo4WZ1FE03igw4nkDTStpAOVt2I4Cbp5Z2g8mjXU0+3cj+8P6W/QZcSYoMuL4TpcQpYlA38B4YpFDkQ2A7TjH6ulkDL7OXhIElsPRnK2QOnauDB77GswrUqRJIbf//As8tCwkiSQyJEKe/VCHL6HIdCig1hVFVSFYTNJIWOqFNf1wh6+kGHUaYNKNRzUi2EKwShE3A9IcsKuQl+rKOFaxqK0ZdQynXHxFA1izpw9W1DBIoaME/YhigC/f/aguPy+EaF3L57AWdSJ5Dm4WZcsD+/GZZOpYpKXysUepVDY/UIpJEzRUtMSMRRZhd3TklE2lykA4klnpm7yAqAPuHMvfGZQAyA+25bXsYXb0OdrEulEaKH9ad6Sg97ICy1zFkchZnwcaNiC0howE3u4i0zZGHZcRUAFH+Q5W0CjPllW3dnQGiLfZk+n5ZOWp+9ChGJvkLjY81D6emr82UNJ62JlNRdB2PbPPDoenrpIo0RYCjBsju0TGNgP0tyRJGBlsknqT8hrPyd3UnN+LYoc73GsmK3J529RjHY9wbTkBGWo0c3vVemMrYfzPvxEGXCIO7/yhReHdq32henBvMk9UOny8NFfn5uZcRBk/rUw1HbNvh5d7646U8zSn9sfesadEgPzw002NcgXRBLn+PeYD5nX6B7xy/ECff3d8e/YzDqlFE89kdbL5VVIVOU5Ms3ljxK27MLES6Ba5hZLCO7z3X3FavKK5xfTuKJXtPX78jFw+fre1jpLCBj5JTtbV2vsfheA1PHmC1K6anXpPBbAX1dOKheia09zUzmkf2pDkHyvkxn9qvnUM8YCHfiZLM1n+SL+ZuiqkhvnmM04/1HwwNI0Aq87182KyCSwXLrs3jTvwvw1w/wduyH0YA+DTAlNr/robK5Zc3QLM0w/cdLCr/Advpt82C7yW98neLojelX2apiLwNw+8f1+QBoSgtWPCt4kOXT3zSqYrPVpCVBY5v8IKcG+3XA/dMdKzMdUxURTUMbO1MrCwhKwMzM6+YgZ6BhS7uRjY6UWthilw4N6HevRe1xdwpsKpF5hXyTVGthaPDmG3PRq9rYn+KXNQ25tG4gmtHDorrbue5teiVtUvyiZGx/crbSfdFOS1JTUJ+dvWaSVNlbjHCCLrQWmIRAA==); }
      @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,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); }
      @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAWkABEAAAAADpwAAAVKAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAAPAgKCY4uERAKhhyFBQsIAAE2AiQDCgQgBYJeByAMFBvQDFGUJ85Sso/EdIO9TAaS4lz4nQ+Pm/Z+hLJARUkb1iZ1qMmZpVQFUnHZ6OaZuLHT9NQNBNntBAeKMJCLl6VRiSaQ//9zGpnbsqysI1IVrr8nr2RsjelP8Q2QJLAwKMym5BywArL1UT1TetIvD9sCjEyGcfk3DgSAEZimQCzd+ns4mJkR2VZQAT0YAHC7Adi4hqZfJJvBAlSyDJBB8mlID/0miuhAkxjI08Oggsq248a1ZGk/PegeBHoQmxsZcuiDNwCKpf4HiFgAFAAaQAAaBD0MYqGbIKDE+ntUafte7twOgniKRYDxKz8Q3zUYSxyXCdmoaMS9UoOcCzCCbqhP0EDiRTGrS3aRxgQNVLwGYpUSNNDxos1FR9nKHBGKqIpqbqsq2sTOplYXE0Vy9kdtqpIkulBeXaILFSW5blSE6FCbolydoIE5w8rsUBXRJnaLrd1S5/92jLHx+fRaOrrEUepwOWXBdaOsCJIkZrkeLHFk67IgKQqjV/eWspqe6+LfFx79OqvepT+By02pTymqOuiLkHCXqgpqhBJcqhEY9hsHNShvpaOyNOIsmV9wRkgC9UVI9lOKIClygoZF8fnljiwZ3JLC3N8tDr1dLyQDjFZD7j0VshfdW4MPfC0+Tj/1RgGZnP8Lv1v0JwQIgQiyBubepJB5erCGEAiWELOaQVxMCBXCIFrDci8p5HB6hIZIRFkizRoNYgwhVrDAarXk3vpxCjmenqAhEUmWRLMmQ3rKPAA6vJ8Cj7jdaAAooLvDGyAUQGyIu5ngLg/oBS+jyTMiisE3Zk3130mEmJZ54dwbcbsMUTtw/tUbK91rnPz2/p1BB18PFRb6XpMGqn3sbNUh07Wnkpv5fW+0zG5ZF/hRJQw7t3Jq84dNQdxBUvUN/GvmovceDanYH7+di95J3rAz9SO7zBtlvVnJ55b1DTIfwe1WlUHA9EbSLoObN8iZPtureLAnvEJSWkhjZXZ9VUtoY409nGsiVVLn0ICKqp1BIfOmmMpsMi/sM1Tayetq5k4i7DI0kUb1aNUhQ98dwTvgrAh0co14zbAu494jzbDf4AOK/YL2CMhovP4yJVu3We6lzI1vZc/zb/GWfaba5OpP79HNrPNlV/zt2f3TwodhcuvyBd6SZD+29S016Kcz15kbZKvlf7Ft21P7XlN9oSEyafP7Pp7fRC1piGYYSSvf9DXOHbCsPyB8+Kts3WpJJlep/LKDun91Vz06dOVB8mr5Gd5ygE9pW2zXmH05B7L3v6VnYveZtl2Ukj2Lti70srqrHuTZ1atCn91qzSV/Lri9+h3TVRmcz1q3ZS+/9bkgwhadXMDYbFhN+vqO1FLBro8MOyipj6ht00d/ujl5kpjmSjIyVoGN6a6mkqn9ls/n46O3tpkO5BVh8NwREijUoyKSHifFxgOmbQu+wWySNx3+PDtkTa9nl4UznhJbboyIYamkTVRpdPw+0zaOB5tyLxECI5A2kCeYfesjPEFfos2WmMBvnmjwvvY3vYH+ErxvHvs+8mV8Ehe11f2p28L8Sw8B0OtVqj4t+5jfNlz/f3sBgbhKyaC/hE2nn6a+QUw3WGqqjQJSPVCNBmEWTVu93YUJAjwepuAFLUwjkXqc2Q6zmOTHOt84wKFLkwG9mnQpd4sqbYaN6DKgnyhNohQpriFr06JNn2ZcF6WLNydTJWdl7LTx10fdYhi5qcHApCjlhO7kbDJV5oTF6mrpEjTrMvoWnaJ1vqstLsPJDmPMM4dl0vIPGDQV36VDp9HjZ970VlEBh2x2NOJS7kHJ5jTEtgyhCAA3gDgAAA==); }
      @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAWcABEAAAAADoQAAAVEAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAAPAgKCY4uERAKhgSEaQsIAAE2AiQDCgQgBYJeByAMFBu0DMiOw3Q7K4sLSZlv+fDfaX9ubjIRC89EFRfptk6pJFM5XcjTdRZCnqdP1h8cHj5vbzzg8WCE03X3oe6GXp4kEFidp1m1lievnyAgIxGFy37d1MX4GDMb3rsAoSMSBoVJlIwiUHHRp0KkdKTPPaZD1EU1q/iXDwSABhiWgKjV6t+jQqxYxF1Z3w4FpAAQCgEwspTS18k+yFZhVUfzgvRsEXlJAUgIA0o8oJICjtRHlRclc/7IMbogtADoAuybUnLms/dAIpP8D4Dd0obYTgFYUJA0VYokMAMEksXV16jFleeB434QeEMyWOSvvIG0LkLT7LuLkD28SEJbRbjLFDSgfb3pIkgay1b43UHSny5CkiaCpHDpImgaWxmkjspWn41nBVaoGRbYSnZ8YDgodaDsfNCIwGeyQbSFnw2iPbhgCR/jbhnh+fx0EdItvFImMnazE4u9E1Ko/yY8liytjg1SZ7OvxRfc4I4Jlrj5GI5jK4KPN/uCj7tjOJ5PF8FcUqbDa/2R14W8nUkRTYoNVJsixRAvCLU2G8fWCEKMYOPtI0UC6S/pFDF7OXVUiGRDc/fEBhsXg5ywehMfw/HudBHKtLo2X4W7FuZ4S/W5iRV+haCGxqs2X6uRdXS9AQOMHkPQpJcISLThf+G9RTPBIoQh3BtmfoRGjqRHGYhGjCfarbEgxhHihQSw3gTzOY1spdsM2OHw2N3qBDGRkCQkI8WbbH6qRk6jpxvIQKYnw61ZUDF7HAB1LpcgEgmFUACIwSidov89RHKhxaQoqwATadRrw6UOTbcadVr9XMHzrud2fMwRtePYE+90vKOe9DoOIfKdp3IeJnfey+w75xBOfmkHhw6Hvzul/VbcWOjw1laZztaZHme/cPxDz5kZHW03qPdJWi5FvX/SY9NwQwn9cZ4h0j8glh7f2Pn42yUHtxx/7z1ve3JHzAOTMzxn1I5j6MiXwqxJleedhnTs8qspPJ40pHJG06fb7ycxz5HtzutNwPeotblLJdglo42dk+QqsndB2CuuQ9jQyOe97DlMNlodW0wOqpx3XzD0RJzl3bfzTO5jnN4wDv7ctKoraXdx7aJFqw58EOH6VT3S3cVWRM00rzC7TkUEah8uZJJPRcyJ6tf9EN54vv+jB49f+4f/YoprrAxcjUzKE1+JuvjBVi3jcrbdDvwQ4bKVqEoDU4HX2U0w/TqtrF779SJS6ueWbQx/mWzKV877vFB8k46ytnnxgzRSOThBZs4hiNmY5O9n0Y03XZtSP4hMPhnprGs4llTfLZt5IvKAvrmcec2d4vrXQS7PJx+cigi8YG4mRdWL868kRryadkj2VODah3Im+Vnj1aDJnRL4f4u6VEkeCvwV7vo+ju7fWpp8IiLwsjXrrbhNU/fffHHk1UciAg8a9z195ZZZtuV92a23aoonjQyMRXQVfdZ78/u/FTOuUOn+QmSyJCMw9QWpjd8Jf22fXH/cxDiHpvDPvIvGJj96Qd3Xpy/4TaGm30Pa+xd+td+Nr1IdB0Jfh5Kl/9KFABSidkHt/7FN+ttI6P+fD4HAq8XCoN9j5Uinkp9Qm0HSc3wSoLwcnSiIVDmc4sMym8Di2WwJdBCzKTIIO6Xj2TKsYFKM9XqAj9+A+eYY4NemnMeIRRbzm28eVq4M2bLN4jZiyIi5Btl5rDxpWDmyQrbsSvr8EuUWMTWQY/4gK3uD5qxwMMf0lmLT4+VJN8hvySVsVj1Yvqpa2eCYpcT9Fy1jNX2+BVbm9xszbsnOaxc9l1XPp4oXhVdVaIEsY6KkFiIeFqg6kAo=); }
      @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAWkABEAAAAADnQAAAVMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAARAgKCY4uERAKhWiEYgsKAAE2AiQDDAQgBYJeByAMFBu3DMiOk6UGkuLP5x83/fvyAl6nGtqGqs2M8xuoOZCK+ja6eTpxYfuWr64Q/+7uEwQUYdrWU2jePOCW//9TF5mb3xaMHdM4fk/eCcZjTB77p6z9y1ZRzKGnUFUYyUGNsXQVa3mMQ3qiLcP0AifgVR4QAHogxpiagoS/jxZGRkSw5pdDDRYA3G4AaBlK6YtkBxRTgWki/WhzGXNLDTBECUosoIAa++Ynyw7z3DJ7P+1394N2ReyoZ8nxD94AGAXzPwCeAsCK4gwDCqJZpGmeun6V8j36/ez9bnc89/LIZsOwBQRWXAED/ZYfSBoy9MWOG4RsE2XiXi9DaHdBD1pXmyyDJPG8pUNwkfpkGUySDJJgSpZBk3iri0ZbSx2RIi/xUk6zxFv59oZmFxsdcuWdWiQxlXehLB28C+UxucwiZ05pEcXFyTLYOQRbK4mwie8cLTqZqP86tCqS8tA90JhiR4nD5RQ4l1kQOZOJt7geLHa4HhQ4kygmy1DuksOOlR1B+4WqTplAKtUzqDLGJC5KUq420gTHSBInRYp6QpmAhs0NMgYPp9EWmTiLqyuc0OOwG7i8VuScQ4RkGZqkvDKHReBMpk152tuNrg6rJR30Que8h0H2pHvZ8IaP5Z3ia5olRHP+LVy37EcwSP4IEP7OBxrkIHqwjRBwVkhQjUAOJYRJ4eBFuPMmgxxBj7QRhWgrKqgxQI4lxEnxSBDxzica5CR6so0UpFopQU0D+dP7AND39ZQgxO0GBQCweMShM39eSeYBtQwHggUr+LG2S5KUHOYV1qCNOUQCK12vlu/RxuwLeeMNe/kdhHB36mYX2M7IUXcQrjg5ei+C7qNR5bqowkl9rRRgrUDcu9UmOoOP0BfQeC7kUOVMbOzsR9Fn4TzgcNqqbGKP1CgFnCMztQEzOv+PzqL6GGx2fUWTrRFNqxENj8xw51+w2/YRbn9jbcUbpnlHfezW0cPSjpQ/9VrO9Iedsz2de++rbdLy14n2xKvmHW5Orj/yxmGdbjb4tdfus2xrYK8UYLubkPK6FXvhNByovoNUvmoAbqfWmdg3mBdfUet4IIL//+PFF6MsqrgHAzyYyu9vmTSa7wpctN0nyl59lauaJEKkas1kkrmUDD32+W4hYZclYn9wnnf8oaBdPZvPkPVTU7M+inXTnHbnzvFVRwJ3vRVYVVRAtwTcuFC29sQS5sfs1ZoHYjL6kwI0r671oz3sQ+FFRcrV/2s0G/FRgLiMzv14bcQ6NkZjDNrhVu0yKa6temhJDrm5W3Fp2x9keh9dfeRxsue46iAeWSFEKtYdjbBYyzIPB+2622dHa96izOjMH4iQsDs+7VZfX++jH1ZN2OsX5xxqStrN6gV1Gy7Xl9Y9P3/1Ysv1sRXzBxXvBC+SghqLQvcc5h/hjArJM7/vQsefZVz0pGpN1ryGKPW+0P6Kdq0yYP/y1YOJzNkuesLr4DVPwxUzjbP6vOfFMsr2wmiPxw55+f7vDwAAAUCN8TYmeG+d19Lf1Dr6JQC8efr7KAD4JDF6l/tTdzz7Lx0AoAYzouT/WMv+1pL6/7cJQGAxYjr8SyzyFVbgDJb5BrEl/fozgIF1kgqVKAirAZDg7TSLwODxLAaekLMoUkjz2fYsBcb5KqX2fIBDhwZ9ujXoUCaTTYtBQzr06cWbI0W6dEsIWjRp0aMR+ry5kpjOkObqJaz9/WGZBpFqiNGneekzVKW5OsP8kuLDtc2VrFGH4V3EeL68xVyVQt1mpOeSg+OYp/XpN5G3Q5t2w/Mv7nRLXj6HLHYUVmHufmm6ZcR1iTAYoK1HIg==); }
      @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,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); }</style></defs><rect x="0" y="0" width="685.1875" height="134" fill="#f5faff"></rect><g stroke-linecap="round" transform="translate(515.19140625 10) rotate(0 79.998046875 57)"><path d="M0 0 C60.73 2.05, 121.01 1.22, 160 0 M0 0 C43.29 -0.24, 89.13 -0.57, 160 0 M160 0 C159.83 27.73, 159.57 52.3, 160 114 M160 0 C159.62 36.13, 158.89 72.33, 160 114 M160 114 C118.59 115.32, 79.14 115.3, 0 114 M160 114 C116.41 113.74, 73.85 114.73, 0 114 M0 114 C1.48 72.27, 1.77 28.39, 0 0 M0 114 C0.61 77.99, -0.35 40.66, 0 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(555.189453125 15) rotate(0 40 12.5)"><text x="40" y="17.619999999999997" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">交易签名</text></g><g stroke-linecap="round" transform="translate(517.93359375 46.04296875) rotate(0 26.44921875 19.23046875)"><path d="M1.89 -1.98 L50.99 -0.29 L52.73 37.29 L-1.14 39.39" stroke="none" stroke-width="0" fill="#eaeaea"></path><path d="M0 0 C16.62 -1.49, 35.26 -1.66, 52.9 0 M0 0 C18.35 -0.73, 38.72 0.11, 52.9 0 M52.9 0 C53.37 13.78, 53.55 31.31, 52.9 38.46 M52.9 0 C52.31 8.96, 52.86 18.94, 52.9 38.46 M52.9 38.46 C33.29 36.92, 14.55 38.46, 0 38.46 M52.9 38.46 C38.06 38.85, 21.31 39.64, 0 38.46 M0 38.46 C0.04 22.95, -0.91 9.44, 0 0 M0 38.46 C0.01 25.27, 0.35 12.99, 0 0" stroke="#b3b3b3" stroke-width="2" fill="none"></path></g><g transform="translate(537.022819519043 52.7734375) rotate(0 7.359992980957031 12.5)"><text x="7.359992980957031" y="17.619999999999997" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" fill="#b3b3b3" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">R</text></g><g stroke-linecap="round" transform="translate(573.06640625 45.30859375) rotate(0 25.8046875 18.7734375)"><path d="M-0.17 -1.17 L50.47 0.93 L53.58 36.79 L1.51 37.78" stroke="none" stroke-width="0" fill="#eaeaea"></path><path d="M0 0 C12.61 2.06, 23.34 -1.02, 51.61 0 M0 0 C13.42 -0.01, 25.26 0.08, 51.61 0 M51.61 0 C50.79 7.99, 50.01 16.19, 51.61 37.55 M51.61 0 C52.24 8.26, 52.23 16.41, 51.61 37.55 M51.61 37.55 C40.82 38.91, 32.1 39.07, 0 37.55 M51.61 37.55 C37.51 38.72, 22.33 38.07, 0 37.55 M0 37.55 C0.53 26.26, -1.68 17.83, 0 0 M0 37.55 C0.94 28.48, -0.96 21.25, 0 0" stroke="#b3b3b3" stroke-width="2" fill="none"></path></g><g transform="translate(592.6511001586914 51.58203125) rotate(0 6.219993591308594 12.5)"><text x="6.219993591308594" y="17.619999999999997" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" fill="#b3b3b3" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">S</text></g><g stroke-linecap="round" transform="translate(626.33984375 45.9609375) rotate(0 22.103515625 17.5)"><path d="M0.98 -0.38 L44.96 0.12 L45.14 35.52 L0.98 34.5" stroke="none" stroke-width="0" fill="#eaeaea"></path><path d="M0 0 C16.23 0.71, 33.88 1.12, 44.21 0 M0 0 C13.45 0.1, 27.83 -0.28, 44.21 0 M44.21 0 C43.28 9.56, 43.52 19.57, 44.21 35 M44.21 0 C43.85 10.13, 44.23 20.38, 44.21 35 M44.21 35 C27.64 34.87, 11.97 35.97, 0 35 M44.21 35 C27.09 34.89, 10.47 35.14, 0 35 M0 35 C-0.69 27.57, -0.63 21.06, 0 0 M0 35 C0.12 27.68, -0.12 21.17, 0 0" stroke="#b3b3b3" stroke-width="2" fill="none"></path></g><g transform="translate(642.5233612060547 50.9609375) rotate(0 5.9199981689453125 12.5)"><text x="5.9199981689453125" y="17.619999999999997" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" fill="#b3b3b3" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">V</text></g><g stroke-linecap="round" transform="translate(270.72265625 12.567863869326231) rotate(0 122.48400814995091 55.23487050567374)"><path d="M0 0 C82.13 2.18, 162.49 0.97, 244.97 0 M0 0 C52.54 1.13, 105.46 1.5, 244.97 0 M244.97 0 C244.38 39.36, 244.57 80.78, 244.97 110.47 M244.97 0 C245.55 44.28, 246.65 88.04, 244.97 110.47 M244.97 110.47 C158.4 109.68, 73.16 108.1, 0 110.47 M244.97 110.47 C193.45 108.1, 142.68 107.94, 0 110.47 M0 110.47 C-1.01 86.74, 1.09 67.26, 0 0 M0 110.47 C0.1 82, 0.15 52, 0 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(306.8616708086423 17.56786386932623) rotate(0 86.34499359130865 15.423349530380108)"><text x="86.3449935913086" y="21.7407534980238" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="24.677359248608173px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">交易目标和消息</text></g><g stroke-linecap="round" transform="translate(274.28956225908075 42.9301952085753) rotate(0 23.645922748764008 27.145095173468974)"><path d="M0.85 0.01 L47.05 0.74 L46.1 55.26 L0.7 53.03" stroke="none" stroke-width="0" fill="#f5dcdf"></path><path d="M0 0 C14.56 -0.82, 30.99 0.29, 47.29 0 M0 0 C12.25 -0.25, 22.4 -0.1, 47.29 0 M47.29 0 C49.16 17.04, 47.04 30.28, 47.29 54.29 M47.29 0 C48.24 20.13, 47.26 39.61, 47.29 54.29 M47.29 54.29 C30.92 53.23, 16.43 53.91, 0 54.29 M47.29 54.29 C35.87 53.67, 26.26 53.24, 0 54.29 M0 54.29 C-1 42.61, -0.02 29.78, 0 0 M0 54.29 C0.37 35.8, -0.08 17.05, 0 0" stroke="#db8f96" stroke-width="2" fill="none"></path></g><g transform="translate(283.7132407451494 54.651940851664165) rotate(0 14.222244262695312 15.423349530380108)"><text x="14.222244262695312" y="21.7407534980238" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="24.677359248608173px" fill="#db8f96" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">to</text></g><g stroke-linecap="round" transform="translate(326.3191076716097 44.1971446315556) rotate(0 46.29896737150983 27.145095173468974)"><path d="M-1.2 0.97 L93.3 -1.26 L92.81 53.13 L-1.35 52.54" stroke="none" stroke-width="0" fill="#f5dcdf"></path><path d="M0 0 C22.28 1.13, 46.52 1.11, 92.6 0 M0 0 C24.98 0.08, 51 1.07, 92.6 0 M92.6 0 C93.89 22.22, 94.18 42.29, 92.6 54.29 M92.6 0 C92.43 17.46, 91.47 33.6, 92.6 54.29 M92.6 54.29 C66.24 56.2, 41.04 52.39, 0 54.29 M92.6 54.29 C65.61 54.21, 40.74 53.37, 0 54.29 M0 54.29 C0.97 35.88, -1.06 15.34, 0 0 M0 54.29 C0.18 34.75, -0.37 15.58, 0 0" stroke="#db8f96" stroke-width="2" fill="none"></path></g><g transform="translate(343.2484613956585 55.91889027464447) rotate(0 29.369613647460938 15.423349530380108)"><text x="29.369613647460938" y="21.7407534980238" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="24.677359248608173px" fill="#db8f96" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">value</text></g><g stroke-linecap="round" transform="translate(421.74131508228993 42.83246902823919) rotate(0 45.219332904383236 27.145095173468974)"><path d="M0.21 -1.16 L89.09 -1.75 L88.81 53.93 L1.29 55.2" stroke="none" stroke-width="0" fill="#f5dcdf"></path><path d="M0 0 C31.98 -2.39, 64.82 -0.85, 90.44 0 M0 0 C27.54 -0.97, 53.18 -0.18, 90.44 0 M90.44 0 C91.24 19.68, 90.29 41.36, 90.44 54.29 M90.44 0 C89.64 16.57, 89.98 34.05, 90.44 54.29 M90.44 54.29 C54.46 53.26, 17.9 52.28, 0 54.29 M90.44 54.29 C59.86 54.07, 28.7 54.47, 0 54.29 M0 54.29 C1.47 33.58, 2.15 11.9, 0 0 M0 54.29 C-0.53 38.47, 0.42 21.06, 0 0" stroke="#db8f96" stroke-width="2" fill="none"></path></g><g transform="translate(437.2579883797396 54.554214671328054) rotate(0 29.702659606933594 15.423349530380108)"><text x="29.702659606933594" y="21.7407534980238" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="24.677359248608173px" fill="#db8f96" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">input</text></g><g stroke-linecap="round" transform="translate(93.54077800850155 10.05069613420119) rotate(0 88.93273599574923 56.244118304231336)"><path d="M0 0 C65.98 -2.04, 133.76 0.17, 177.87 0 M0 0 C67.49 -0.61, 135.99 -0.11, 177.87 0 M177.87 0 C175.16 23.95, 175.29 49.73, 177.87 112.49 M177.87 0 C176.71 22.02, 176.22 45.67, 177.87 112.49 M177.87 112.49 C107.23 115.69, 37.17 115.79, 0 112.49 M177.87 112.49 C133.82 114.49, 89.3 113.56, 0 112.49 M0 112.49 C0.55 80.79, -1.68 47.9, 0 0 M0 112.49 C-0.05 85.09, 1.4 58.5, 0 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(116.5935396390164 15.05069613420119) rotate(0 65.87997436523438 13.726320262303602)"><text x="65.87997436523438" y="19.348621041743154" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="21.96211241968576px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">交易燃料设置</text></g><g stroke-linecap="round" transform="translate(96.47418980050736 44.707330203621154) rotate(0 58.43980851675758 24.15832366165432)"><path d="M1.92 -1.68 L115.96 -1.52 L118.54 48.79 L1.34 49.23" stroke="none" stroke-width="0" fill="#dce6f2"></path><path d="M0 0 C33.12 0.1, 63.32 -1.85, 116.88 0 M0 0 C30.73 -1.14, 62.87 -1.09, 116.88 0 M116.88 0 C114.52 12.66, 116.21 28.97, 116.88 48.32 M116.88 0 C117.59 11.29, 117.36 23.45, 116.88 48.32 M116.88 48.32 C69.95 51.27, 26.22 50.37, 0 48.32 M116.88 48.32 C70.72 48.69, 24.2 48.8, 0 48.32 M0 48.32 C-0.86 30.78, -1.45 14.32, 0 0 M0 48.32 C-0.64 35, 0.21 20.86, 0 0" stroke="#a6c3e1" stroke-width="2" fill="none"></path></g><g transform="translate(110.01681203796807 55.139333602971874) rotate(0 44.897186279296875 13.726320262303602)"><text x="44.897186279296875" y="19.348621041743154" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="21.96211241968576px" fill="#a6c3e1" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">gasPrice</text></g><g stroke-linecap="round" transform="translate(214.85385861433463 45.0241161045592) rotate(0 27.89874593313209 24.15832366165432)"><path d="M1.66 0.47 L57.14 0.91 L54.65 46.41 L-1.51 49.86" stroke="none" stroke-width="0" fill="#dce6f2"></path><path d="M0 0 C19.01 0.42, 39.63 -1.49, 55.8 0 M0 0 C11.75 -0.31, 24.42 -0.52, 55.8 0 M55.8 0 C54.56 8.74, 56.66 21.73, 55.8 48.32 M55.8 0 C57.09 13.19, 57.14 24.86, 55.8 48.32 M55.8 48.32 C35.9 49.42, 12.26 48.33, 0 48.32 M55.8 48.32 C37.33 47.96, 18.27 48.01, 0 48.32 M0 48.32 C0.06 38.37, 1.33 25.19, 0 0 M0 48.32 C0.71 31.46, -0.71 15.52, 0 0" stroke="#a6c3e1" stroke-width="2" fill="none"></path></g><g transform="translate(224.37209612461515 55.45611950390992) rotate(0 18.380508422851562 13.726320262303602)"><text x="18.380508422851562" y="19.348621041743154" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="21.96211241968576px" fill="#a6c3e1" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">gas</text></g><g stroke-linecap="round" transform="translate(10 44.73828125) rotate(0 40.875 22)"><path d="M-1.51 1.54 L80.31 1.46 L83.7 42.26 L-1.42 44.06" stroke="none" stroke-width="0" fill="#fff"></path><path d="M0 0 C15.68 0.29, 33.19 2.16, 81.75 0 M0 0 C16.34 -0.47, 32.28 0.58, 81.75 0 M81.75 0 C81.82 19.39, 82.62 37.02, 81.75 44 M81.75 0 C82.6 11.95, 82.57 22.02, 81.75 44 M81.75 44 C59.17 44.21, 33.39 43.72, 0 44 M81.75 44 C61.15 43.1, 42.83 43.74, 0 44 M0 44 C-0.53 34.16, -1.82 19.56, 0 0 M0 44 C0.31 34.61, -0.94 23.35, 0 0" stroke="#000" stroke-width="2" fill="none"></path></g><g transform="translate(23.945022583007812 54.23828125) rotate(0 26.929977416992188 12.5)"><text x="26.929977416992188" y="17.619999999999997" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" fill="#000" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">nonce</text></g></svg>
  • nonce: 这是一个从0开始计数的整数,表示从发送者账户发出的交易序号。该字段用于防止重放攻击(确保每笔交易只能被网络处理一次)以及保证交易顺序。
  • gasPrice: gas 费。
  • gas: 为gasLimit,即你为这笔交易设定的愿意消耗的最大燃料数量。交易总费用 = 实际消耗的 gas × gasPrice
  • to: 接收方地址
    • 普通账户地址:这通常是一笔简单的以太币转账。
    • 智能合约地址:这意味着你想要与这个智能合约进行交互,比如调用它的一个函数。
    • 空地址 (Null):如果 to 字段为空,表示这笔交易的目的是创建并部署一个新的智能合约。
  • value: 转账金额
  • input: 一个可选字段,用于携带额外的数据。
    • 调用智能合约:当 to 是一个智能合约地址时,input 字段里包含了你想调用的函数名称以及需要传入的参数,这些信息会经过特定编码。例如,调用一个代币合约的 transfer(address, amount) 函数,相关信息就在这里。
    • 创建智能合约:当 to 为空时,input 字段里包含的是编译后的智能合约字节码。
  • 后面的部分是交易发送方对交易的签名结果,可以利用交易内容和签名结果反向推导出签名者,即交易发送方地址。

交易对象

交易对象定义为一个Transation对象。

// core/types/transaction.go
// Transaction 是一个以太坊交易。
type Transaction struct {
    // 交易的共识内容 (即交易的核心数据)
    inner TxData
    // 本地节点首次看到该交易的时间 (用于防止垃圾交易/spam)
    time  time.Time

    // ---- 以下为缓存字段,用于优化性能 ----

    // 缓存的交易哈希,使用原子指针以支持并发访问
    hash atomic.Pointer[common.Hash]
    // 缓存的交易大小,使用原子类型以支持并发访问
    size atomic.Uint64
    // 缓存的交易发送方地址 (从签名中恢复而来),
    // 使用 sigCache 避免重复进行昂贵的签名恢复操作
    from atomic.Pointer[sigCache]
}

其中TxData是一个接口,以太坊网络自诞生以来,交易的格式并非一成不变。随着以太坊的升级(如柏林硬分叉、伦敦硬分叉),引入了新的交易类型来支持新功能(如访问列表、EIP-1559 的动态 Gas 费)。比如:

// core/types/tx_dynamic_fee.go
// DynamicFeeTx 代表一个 EIP-1559 交易。
type DynamicFeeTx struct {
    ChainID    *big.Int // 链 ID
    Nonce      uint64   // 交易序号
    GasTipCap  *big.Int // Gas 小费上限 (也称为 maxPriorityFeePerGas)
    GasFeeCap  *big.Int // Gas 费用上限 (也称为 maxFeePerGas)
    Gas        uint64   // Gas 上限 (Gas Limit)
    To         *common.Address `rlp:"nil"` // 接收方地址 (如果为 nil,则表示创建合约)
    Value      *big.Int // 转账金额 (单位: Wei)
    Data       []byte   // 交易数据 (例如,合约调用的输入)
    AccessList AccessList // 访问列表

    // 签名值
    V *big.Int // 签名的 V 值
    R *big.Int // 签名的 R 值
    S *big.Int // 签名的 S 值
}

另外,Transaction还定义了三个缓存项:交易哈希值(hash)、交易大小(size)和交易发送方(from)。缓存的原因是使用频次高且CPU计算量大。

第一个是交易哈希,采用了**懒加载(Lazy Evaluation)缓存(Caching)**的策略。这意味着哈希值只在第一次被请求时才会进行计算,计算结果会被缓存起来,后续的调用将直接返回缓存的结果,避免了重复的、昂贵的哈希计算

// core/types/transaction.go
// 返回交易hash
func (tx *Transaction) Hash() common.Hash {
    // 缓存检查(“快”路径)
    // 如果 不是 nil,说明此交易的哈希值之前已经被计算并缓存了。
    if hash := tx.hash.Load(); hash != nil {
        return *hash
    }

    // 准备计算(“慢”路径)
    var h common.Hash
    // tx.Type() 返回交易类型
    // LegacyTxType: 代表“传统交易”,这是以太坊最初的交易格式
    if tx.Type() == LegacyTxType {
        h = rlpHash(tx.inner)
    } else {
        // EIP-2718 之后引入的新型交易
        h = prefixedRlpHash(tx.Type(), tx.inner)
    }
    tx.hash.Store(&h)
    return h
}

第二个缓存是交易大小,交易大小是指交易信息进行RLP编码后的数据大小。代表交易网络传输大小、代表交易占区块大小、代表交易存储大小。 每笔交易进入交易池都需要检查交易大小是否超过 32KB。推送交易数据给其他节点时也需结合交易大小,在不超过网络消息最大限制(默认10MB)下分包推送数据。为避免重复计算开销,在第一次计算后便缓存。

// core/types/transaction.go
// Size 返回交易编码后的真实存储大小。
// 它会通过对交易进行编码来计算大小,或者直接返回一个先前已缓存的值。
func (tx *Transaction) Size() uint64 {
    if size := tx.size.Load(); size > 0 {
        return size
    }

    // 缓存未命中,则进行编码并缓存结果。
    // 注意,我们依赖于一个假设:即所有 tx.inner 的值都已经是 RLP 编码的!
    c := writeCounter(0)
    rlp.Encode(&c, &tx.inner)
    size := uint64(c)

    // 对于 blob 交易,需要加上其 blob 内容(sidecar)以及对 [交易+sidecar] 整体进行编码时外层列表的大小。
    if sc := tx.BlobTxSidecar(); sc != nil {
        size += rlp.ListSize(sc.encodedSize())
    }

    // 对于类型化交易,其编码结果还包含一个前置的类型字节。
    if tx.Type() != LegacyTxType {
        size += 1
    }

    tx.size.Store(size)
    return size
}

rlp 是以太坊定义的一套区块链数据编码解码协议,而非采用常见的 gzip、json、Protobuf 编码格式。目的是为了尽可能地压缩数据,毕竟区块链数据结构中只有常见的几种数据类型,不需要复杂的协议涉及,即可满足要求。

最后一个缓存是交易的发送方(from),交易的发送方,是根据签名反向计算过程,同样是CPU密集型运算。为了保证交易合法性,程序中到处都有涉及取交易发送方地址和校验发送方的合法性。只有正确的签名才能得到发送方地址。因此对交易发送方也进行缓存。

// core/types/transaction_signing.go
// Sender 使用 secp256k1 椭圆曲线从签名 (V, R, S) 中恢复出发送者地址。
// 如果恢复失败或签名不正确,则会返回一个错误。
//
// Sender 方法可能会缓存地址,使其可以在不同的签名方法中被复用。
// 如果缓存中记录的签名者(signer)与当前调用所使用的签名者不匹配,那么该缓存将失效。
func Sender(signer Signer, tx *Transaction) (common.Address, error) {
    if sigCache := tx.from.Load(); sigCache != nil {
        // 如果先前用于派生地址的签名者与当前使用的签名者
        // 不一致,则使缓存失效。
        if sigCache.signer.Equal(signer) {
            return sigCache.from, nil
        }
    }

    addr, err := signer.Sender(tx)
    if err != nil {
        return common.Address{}, err
    }
    tx.from.Store(&sigCache{signer: signer, from: addr})
    return addr, nil
}

特殊指出是需要一个Signer进行解签名,同时通过signer获取Sender,合法时缓存并返回。但在使用缓存内容时,还需要检查前后两个 Signer 是否一致,因为不一样的Signer 算法不一样,获得的交易签名者也不相同。

需要注意,上面三个缓存使用都在有一个前提条件:交易对象一旦创建,交易内容不得修改。这也是为何交易对象中单独定义在私有的 txdata 中,而非直接定义在 Transaction 中的原因之一

交易对象方法

交易对象 Transtion 除对外提供交易内容访问外,也定义了一些辅助方法。

  1. ChainId()Protected()
// core/types/transaction.go

// ChainId 返回交易的链 ID。
func (tx *Transaction) ChainId() *big.Int {
    return tx.inner.chainID()
}

// Protected 用于判断该交易是否受重放保护 (replay-protected)。
func (tx *Transaction) Protected() bool {
    switch tx := tx.inner.(type) {
    case *LegacyTx:
        // 对于遗留(Legacy)类型的交易,只有当 V 字段不为 nil 且其值符合重放保护的格式时,才算受保护。
        return tx.V != nil && isProtectedV(tx.V)
    default:
        // 所有其他新类型的交易都默认受重放保护。
        return true
    }
}
  1. RLP 接口实现方法
// core/types/transaction.go

// EncodeRLP 实现了 rlp.Encoder 接口。
func (tx *Transaction) EncodeRLP(w io.Writer) error {
    // 检查交易类型是否为传统的 LegacyTxType
    if tx.Type() == LegacyTxType {
        // 如果是遗留类型的交易,直接对其内部数据结构进行 RLP 编码。
        return rlp.Encode(w, tx.inner)
    }
    
    // 如果不是遗留类型,那么它就是一个 EIP-2718 类型的交易信封 (typed TX envelope)。
    // 从缓冲池中获取一个 bytes.Buffer 以提高性能。
    buf := encodeBufferPool.Get().(*bytes.Buffer)
    // 使用 defer 确保在函数退出时将缓冲区放回池中,以便复用。
    defer encodeBufferPool.Put(buf)
    buf.Reset()

    // 调用内部方法将类型化交易编码到缓冲区中。
    if err := tx.encodeTyped(buf); err != nil {
        return err
    }
    
    // 对于类型化交易,需要将编码后的数据(包含交易类型前缀和 payload)
    // 作为一个字节串,再次进行 RLP 编码后写入 io.Writer。
    return rlp.Encode(w, buf.Bytes())
}

Go语言中只要对象有实现某个接口的所有方法,则认为该对象属于某接口类型。这里Transaction 实现了rlp包中的EncoderDecoder两个接口。意味在进行RLP编码解码时,将通过自定义实现的两个方法进行。RLP编码解码交易,实际是将交易内容 txdata 进行编码解码。同时在解码交易时,缓存交易大小。

//rlp/encode.go

// Decoder 接口由那些需要自定义 RLP 解码规则或需要将数据解码到私有字段的类型来实现。
//
// DecodeRLP 方法应该从给定的 Stream 中读取一个值。虽然读取少于或多于一个值并非禁止,
// 但这样做可能会引起混淆。
type Decoder interface {
    DecodeRLP(*Stream) error
}

// Encoder 接口由那些需要自定义编码规则或希望编码私有字段的类型来实现。
type Encoder interface {
    // EncodeRLP 方法应将其接收者(receiver)的 RLP 编码写入 w (io.Writer)。
    // 如果该方法的实现是一个指针方法,那么即使接收者为 nil 指针,该方法也可能被调用。
    //
    // 实现时应确保生成有效的 RLP。目前,写入的数据不会被校验,
    // 但未来的版本可能会加入校验。推荐只写入一个 RLP 值,
    // 但也允许写入多个值或不写入任何值。
    EncodeRLP(io.Writer) error
}
  1. JSON接口实现
// core/types/transaction_marshalling.go
// MarshalJSON 将交易(Transaction)序列化为包含哈希的 JSON 格式。
func (tx *Transaction) MarshalJSON() ([]byte, error) {
    var enc txJSON
    // 以下字段适用于所有交易类型。
    enc.Hash = tx.Hash()
    enc.Type = hexutil.Uint64(tx.Type())

    // 其他字段根据交易类型有条件地设置。
    switch itx := tx.inner.(type) {
    // 情况一:传统交易 (LegacyTx)
    case *LegacyTx:
        enc.Nonce = (*hexutil.Uint64)(&itx.Nonce)
        enc.To = tx.To()
        enc.Gas = (*hexutil.Uint64)(&itx.Gas)
        enc.GasPrice = (*hexutil.Big)(itx.GasPrice)
        enc.Value = (*hexutil.Big)(itx.Value)
        enc.Input = (*hexutil.Bytes)(&itx.Data)
        enc.V = (*hexutil.Big)(itx.V)
        enc.R = (*hexutil.Big)(itx.R)
        enc.S = (*hexutil.Big)(itx.S)
        // 如果是受 EIP-155 保护的交易,则包含链 ID
        if tx.Protected() {
            enc.ChainID = (*hexutil.Big)(tx.ChainId())
        }

    // 情况二:带访问列表的交易 (AccessListTx, EIP-2930)
    case *AccessListTx:
    //...

    // 情况三:动态费用交易 (DynamicFeeTx, EIP-1559)
    case *DynamicFeeTx:
    //...

    // 情况四:Blob 交易 (BlobTx, EIP-4844)
    case *BlobTx:
    //...
    // 情况五:设置代码交易 (SetCodeTx, EIP-7702)
    case *SetCodeTx:
    //...
    }
    return json.Marshal(&enc)
}

func (tx *Transaction) UnmarshalJSON(input []byte) error {
    //...
    // Now set the inner transaction.
    tx.setDecoded(inner, 0)

    return nil
}

下面是一个交易JSON格式数据示例。

交易回执

以太坊作为智能合约平台。每一笔交易作为消息在以太坊虚拟机中执行时,均会获得一个交易回执信息(Receipt)。就像在银行转账后,可以获得关于这笔转账的交易电子回单。

回执信息分为三部分:共识信息、交易信息、区块信息。

Status CumulativeGasUsed Logs Bloom TxHash ContractAddress GasUsed BlockHash BlockNumber TransactionIndex 共识内容 交易信息 区块信息

Receipt结构体

// core/types/receipt.go
// Receipt 代表一笔交易的执行结果。
type Receipt struct {
    // 共识字段:这些字段由以太坊黄皮书(Yellow Paper)定义,所有节点必须达成一致。
    Type              uint8  `json:"type,omitempty"`                 // 交易类型(如 Legacy, AccessList, DynamicFee 等)
    PostState         []byte `json:"root"`                           // 【已废弃】拜占庭分叉前使用的状态根,现由 Status 替代
    Status            uint64 `json:"status"`                         // 交易状态:1=成功,0=失败(拜占庭后引入)
    CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"` // 本交易执行后区块累计消耗的 Gas 总量
    Bloom             Bloom  `json:"logsBloom" gencodec:"required"`  // 日志的布隆过滤器,用于快速检索日志事件
    Logs              []*Log `json:"logs" gencodec:"required"`       // 本次交易产生的日志列表

    // 实现字段:这些字段由 Geth 在处理交易时添加,不属于黄皮书共识规范。
    TxHash            common.Hash    `json:"transactionHash" gencodec:"required"` // 交易哈希
    ContractAddress   common.Address `json:"contractAddress"`                    // 若为合约创建交易,此为新合约地址
    GasUsed           uint64         `json:"gasUsed" gencodec:"required"`        // 本次交易实际消耗的 Gas
    EffectiveGasPrice *big.Int       `json:"effectiveGasPrice"`                  // 本次交易实际支付的 Gas 价格(含小费和基础费),为兼容旧版未加 required 标签
    BlobGasUsed       uint64         `json:"blobGasUsed,omitempty"`              // 本次交易使用的 Blob Gas(EIP-4844 引入)
    BlobGasPrice      *big.Int       `json:"blobGasPrice,omitempty"`             // 本次交易支付的每单位 Blob Gas 价格

    // 包含信息:这些字段提供该收据对应交易被包含在哪个区块中的上下文信息。
    BlockHash        common.Hash `json:"blockHash,omitempty"`     // 所在区块的哈希
    BlockNumber      *big.Int    `json:"blockNumber,omitempty"`   // 所在区块高度
    TransactionIndex uint        `json:"transactionIndex"`        // 交易在区块中的索引位置(第几笔交易)
}

代码中对交易回执的构建部分:

// core/types/receipt.go
// NewReceipt 创建一个基础交易收据,复制初始字段。
// 已弃用:请改用结构体字面量创建收据。
func NewReceipt(root []byte, failed bool, cumulativeGasUsed uint64) *Receipt {
    r := &Receipt{
        Type:              LegacyTxType,
        PostState:         common.CopyBytes(root),  // 拜占庭分叉前的状态根
        CumulativeGasUsed: cumulativeGasUsed,
    }
    if failed {
        r.Status = ReceiptStatusFailed          // 0 = 交易失败
    } else {
        r.Status = ReceiptStatusSuccessful      // 1 = 交易成功
    }
    return r
}

交易回执内容介绍

交易回执共识信息

在校验区块是否合法时,这部分信息也参与校验。这些信息参与校验的原因是确保交易必须在区块中的固定顺序中执行,且记录了交易执行后的状态信息。这样可强化交易顺序。

  1. status: 表示是否成功,1表示成功。在高度1035301前,并非1或0,而是 StateRoot,表示此交易执行完毕后的以太坊状态。
  2. CumulativeGasUsed:: 区块中已执行的交易累计消耗的Gas,包含当前交易。
  3. Logs: 当前交易执行所产生的智能合约事件列表。
  4. Bloom:是从 Logs 中提取的事件布隆过滤器,用于快速检测某主题的事件是否存在于Logs中。

关于这些信息是如何参与共识校验的,参与校验的仅仅是回执哈希,而回执哈希计算只包含这些信息。

  1. 执行交易生成回执​:当节点收到一个新区块时,它会重新执行区块中的每笔交易(基于当前状态),并独立生成每个交易的回执。回执中的 status和其他字段取决于交易执行结果(例如,如果交易失败,status为 0)。
  2. 构建回执的 MPT​:节点将所有交易回执组织成一个 Merkle Patricia Trie(MPT)。每个回执被编码为 RLP 格式,并作为 MPT 的叶子节点。MPT 的根哈希被计算出来,称为 receiptsRoot。
  3. 验证 receiptsRoot​:节点将计算出的 receiptsRoot与区块头中的 receiptsRoot字段进行比较:
    • 如果两者匹配,说明所有节点对交易回执的共识一致,区块有效。
    • 如果不匹配,说明交易执行结果有分歧(例如,回执中的 status或其它字段不一致),区块被拒绝,共识无法达成。
// core/types/block_validator.go
// ValidateState 验证状态转换后发生的各种变更是否合法,
// 包括:使用的 Gas 数量、收据树根(receipts root)、状态根(state root)等。
func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, res *ProcessResult, stateless bool) error {
    ...
    // 验证收据 Trie 的根哈希(R = (Tr [[H1, R1], ... [Hn, Rn]])) 是否匹配区块头
    receiptSha := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil))
    if receiptSha != header.ReceiptHash {
        return fmt.Errorf("收据根哈希无效(远程:%x,本地:%x)", header.ReceiptHash, receiptSha)
    }
    ...
}

函数 types.DeriveSha 用于为交易回执列表(或其他可派生列表)生成 Merkle Trie 根哈希。它将列表中每个元素的 RLP 编码作为“值”,以“索引的 RLP 编码”作为“键”,构建一棵 Merkle Trie,并最终计算其根哈希。

与早期实现不同,当前版本为适配内存高效的 StackTrie必须按“键的字典序”插入数据。由于 RLP 编码的特性(例如索引 0 的编码 0x80 在字典序上大于 0x01 ~ 0x7f),插入顺序被调整为:

  1. 先插入索引 10x7f
  2. 再插入索引 0
  3. 最后插入索引 0x80 到列表末尾。

这确保了键值按字典序递增,满足 StackTrie 的要求。虽然插入顺序与原始索引顺序不同,但最终生成的 Trie 根哈希与规范一致,不影响共识。

// core/types/hashing.go
// DeriveSha 为区块头中的交易列表、收据列表或提款列表生成对应的 Merkle Trie 哈希根。
// 适用于:交易根(transactions root)、收据根(receipts root)、提款根(withdrawals root)等。
func DeriveSha(list DerivableList, hasher TrieHasher) common.Hash {
    // 重置哈希器,确保无残留状态
    hasher.Reset()

    // 从缓冲池中获取一个 bytes.Buffer 用于临时编码,避免重复分配内存
    valueBuf := encodeBufferPool.Get().(*bytes.Buffer)
    defer encodeBufferPool.Put(valueBuf) // 使用后归还缓冲池

    // StackTrie 要求键值必须按“哈希字典序”递增插入,但 `list` 提供的顺序(按索引 0,1,2...)
    // 并不符合该顺序(因为 RLP 编码后,0 的字节序大于 0x80~0xff)。
    // 因此,此处采用特殊插入顺序,确保键(索引的 RLP 编码)按字典序递增:
    //   1. 先插入索引 1 ~ 0x7f(RLP 单字节编码,字典序递增)
    //   2. 再插入索引 0(RLP 编码为 0x80,字典序大于 0x7f)
    //   3. 最后插入索引 0x80 ~ Len-1(RLP 多字节编码,字典序递增)
    //
    // 注意:此处忽略 hasher.Update 可能返回的错误。因为一旦出错,
    // hasher 内部状态已损坏,后续 Hash() 会返回错误或无效哈希 —— 这本身就是一种“失败信号”。

    var indexBuf []byte // 用于临时存储索引的 RLP 编码

    // 阶段一:插入索引 1 到 0x7f(含)
    for i := 1; i < list.Len() && i <= 0x7f; i++ {
        indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i)) // 将索引 i 编码为 RLP
        value := encodeForDerive(list, i, valueBuf)          // 获取第 i 项的 RLP 编码值
        hasher.Update(indexBuf, value)                       // 插入到 Trie 哈希器
    }

    // 阶段二:插入索引 0(RLP 编码为 0x80,字典序大于 0x7f)
    if list.Len() > 0 {
        indexBuf = rlp.AppendUint64(indexBuf[:0], 0)
        value := encodeForDerive(list, 0, valueBuf)
        hasher.Update(indexBuf, value)
    }

    // 阶段三:插入索引 0x80 到 Len-1(RLP 编码为多字节,字典序递增)
    for i := 0x80; i < list.Len(); i++ {
        indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i))
        value := encodeForDerive(list, i, valueBuf)
        hasher.Update(indexBuf, value)
    }

    // 最终计算并返回 Trie 根哈希
    return hasher.Hash()
}
// core/types/hashing.go
func encodeForDerive(list DerivableList, i int, buf *bytes.Buffer) []byte {
    buf.Reset()
    list.EncodeIndex(i, buf)
    // It's really unfortunate that we need to perform this copy.
    // StackTrie holds onto the values until Hash is called, so the values
    // written to it must not alias.
    return common.CopyBytes(buf.Bytes())
}

type DerivableList interface {
    Len() int
    EncodeIndex(int, *bytes.Buffer)
}

Receipts 实现了 DerivableList 接口

// core/types/receipt.go
// EncodeIndex 将第i个收据编码至写入器w。
func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) {
    r := rs[i]
    data := &receiptRLP{r.statusEncoding(), r.CumulativeGasUsed, r.Bloom, r.Logs}
    if r.Type == LegacyTxType { // 传统交易类型
        rlp.Encode(w, data)
        return
    }
    w.WriteByte(r.Type)
    switch r.Type {
    case AccessListTxType, DynamicFeeTxType, BlobTxType, SetCodeTxType: // 访问列表交易类型/动态费用交易类型/Blob交易类型/设置代码交易类型
        rlp.Encode(w, data)
    default:
        // 对于不支持的交易类型,不写入任何内容。由于该方法用于DeriveSha,
        // 错误会在匹配派生哈希与区块时被捕获
    }
}

而receiptRLP仅仅包含上面提到的参与共识校验的内容:

// core/types/receipt.go
// receiptRLP 是收据的共识编码结构。
type receiptRLP struct {
    PostStateOrStatus []byte  // 交易后状态或状态码
    CumulativeGasUsed uint64  // 累计燃气消耗量
    Bloom             Bloom   // 布隆过滤器(日志摘要)
    Logs              []*Log  // 日志条目列表
}

交易回执交易信息

这部分信息记录的是关于回执所对应的交易信息,有:

  • TxHash: 交易回执所对应的交易哈希。
  • ContractAddress: 当这笔交易是部署新合约时,记录新合约的地址。
  • GasUsed: 这笔交易执行所消耗的Gas燃料。

这些信息不参与共识的原因是这三项信息已经在其他地方校验。

  • TxHash: 区块有校验交易集的正确性。
  • ContractAddress: 如果是新合约,实际上已经提交到以太坊状态 State 中。
  • GasUsed: 已属于CumulativeGasUsed的一部分。
// core/types/receipt.go
// DeriveFields 根据共识数据和上下文信息(如包含的区块和交易)填充收据的计算字段。
func (r *Receipt) DeriveFields(signer Signer, context DeriveReceiptContext) {
    ...
    r.TxHash = context.Tx.Hash()
    r.GasUsed = context.GasUsed
    ...
    // 合约地址可以从交易本身中推导出来
    if context.Tx.To() == nil {
        // 获取签名者开销较大,仅在确实需要时执行
        from, _ := Sender(signer, context.Tx)
        r.ContractAddress = crypto.CreateAddress(from, context.Tx.Nonce())
    } else {
        r.ContractAddress = common.Address{}
    }
    ...
}

交易回执区块信息

这部分信息完全是为了方便外部读取交易回执,不但知道交易执行情况,还能方便的指定该交易属于哪个区块中第几笔交易。

  • BlockHash: 交易所在区块哈希。
  • BlockNumber: 交易所在区块高度。
  • TransactionIndex: 交易在区块中的序号。

交易回执构造

交易回执是在以太坊虚拟机处理完交易后,根据结果整理出的交易执行结果信息。反映了交易执行前后以太坊变化以及交易执行状态。

构造部分前面说了,就是NewReceipt,但是已经弃用了。

这里是完整的交易回执构造代码:

// core/state_processor.go
// MakeReceipt 根据交易的执行结果,生成对应的交易回执(Receipt)对象。
func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, usedGas uint64, root []byte) *types.Receipt {

    // 创建一个新的交易回执,记录交易执行后的中间状态根(仅旧版本使用)和累计消耗 Gas。
    receipt := &types.Receipt{
        Type:              tx.Type(),      // 交易类型(如 Legacy, AccessList, DynamicFee, BlobTx)
        PostState:         root,           // 【已废弃】拜占庭前用于记录状态根,现由 Status 替代
        CumulativeGasUsed: usedGas,        // 本交易执行后,区块内累计消耗的总 Gas
    }

    // 根据执行结果设置交易状态:成功或失败
    if result.Failed() {
        receipt.Status = types.ReceiptStatusFailed       // 0 — 执行失败
    } else {
        receipt.Status = types.ReceiptStatusSuccessful   // 1 — 执行成功
    }

    // 记录交易哈希和本交易实际消耗的 Gas
    receipt.TxHash = tx.Hash()
    receipt.GasUsed = result.UsedGas

    // 如果是 Blob 交易(EIP-4844),记录使用的 Blob Gas 和每单位 Blob 的价格
    if tx.Type() == types.BlobTxType {
        receipt.BlobGasUsed = uint64(len(tx.BlobHashes()) * params.BlobTxBlobGasPerBlob)
        receipt.BlobGasPrice = evm.Context.BlobBaseFee // 当前区块的 blob 基础费用
    }

    // 如果是合约创建交易(To 为 nil),计算并记录新创建的合约地址
    if tx.To() == nil {
        receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce())
    }

    // 获取本次交易产生的所有日志(Logs),并构建布隆过滤器(Bloom Filter)
    receipt.Logs = statedb.GetLogs(tx.Hash(), blockNumber.Uint64(), blockHash, blockTime)
    receipt.Bloom = types.CreateBloom(receipt) // 基于日志内容生成布隆过滤器

    // 填充区块上下文信息,便于索引和查询
    receipt.BlockHash = blockHash
    receipt.BlockNumber = blockNumber
    receipt.TransactionIndex = uint(statedb.TxIndex()) // 交易在区块中的位置索引

    return receipt
}

交易回执存储

交易回执作为交易执行中间产物,为了方便快速获取某笔交易的执行明细。以太坊中有跟随区块存储时实时存储交易回执。但为了降低存储量,只存储了必要内容。

首先,在存储时,将交易回执对象转换为精简内容。精简内容是专门为存储定义的一个结构ReceiptForStorage。存储时将交易回执集进行RLP编码存储。

// core/rawdb/accessors_chain.go
// WriteReceipts 将属于某个区块的所有交易回执(receipts)写入数据库。
func WriteReceipts(db ethdb.KeyValueWriter, hash common.Hash, number uint64, receipts types.Receipts) {

    // 将回执列表转换为“存储专用格式”,并序列化为 RLP 字节流
    storageReceipts := make([]*types.ReceiptForStorage, len(receipts))
    for i, receipt := range receipts {
        // 将 *Receipt 类型强制转换为 *ReceiptForStorage(存储优化格式)
        storageReceipts[i] = (*types.ReceiptForStorage)(receipt)
    }

    bytes, err := rlp.EncodeToBytes(storageReceipts)
    if err != nil {
        // 如果序列化失败,记录致命错误并终止程序(因为回执必须可序列化)
        log.Crit("区块回执序列化失败", "err", err)
    }

    // 将序列化后的回执字节数据写入数据库,键由区块号和哈希共同生成
    if err := db.Put(blockReceiptsKey(number, hash), bytes); err != nil {
        // 如果数据库写入失败,记录致命错误并终止程序(因为回执必须持久化)
        log.Crit("区块回执存储失败", "err", err)
    }
}

所以看存储了哪些内容,只需要看 ReceiptForStorageEncodeRLP 方法:

// core/types/receipt.go
// EncodeRLP 实现了 rlp.Encoder 接口,将 ReceiptForStorage 的所有内容字段“扁平化”编码为 RLP 流。
// 该编码格式专为存储优化,省略了 TxHash、BlockHash 等可推导字段,仅保留核心共识数据。
func (r *ReceiptForStorage) EncodeRLP(_w io.Writer) error {
    // 创建一个带缓冲的 RLP 编码器,提升写入效率
    w := rlp.NewEncoderBuffer(_w)

    // 开始编码最外层的列表(对应 Receipt 的顶层结构)
    outerList := w.List()

    // 1. 写入状态字段(Status 或 PostState,取决于分叉)
    //    → 调用 (*Receipt)(r).statusEncoding() 获取兼容旧版的编码值
    w.WriteBytes((*Receipt)(r).statusEncoding())

    // 2. 写入累计 Gas 消耗量
    w.WriteUint64(r.CumulativeGasUsed)

    // 3. 开始编码日志列表(Logs)
    logList := w.List()
    for _, log := range r.Logs {
        // 逐条编码每条日志(Log 也实现了 EncodeRLP)
        if err := log.EncodeRLP(w); err != nil {
            return err
        }
    }
    w.ListEnd(logList) // 结束日志列表

    // 结束最外层列表
    w.ListEnd(outerList)

    // 将缓冲区内容刷新到原始 Writer(如文件或网络流)
    return w.Flush()
}

根据EncodeRLP方法实现,可以得出在存储时仅仅存储了部分内容:StatusCumulativeGasUsedLogs