第 6 章. 交易

  • ethbook
  • 发布于 4天前
  • 阅读 9

本章深入探讨了以太坊交易的结构、类型和生命周期,从传统的交易到引入访问列表、基础费用、blob gas和委托指示符的新型交易,详细解释了Nonce的作用和追踪方法,以及gas费用的机制和交易接收者的验证。此外,还介绍了交易价值、数据有效载荷、合约创建、数字签名,以及MEV对交易生命周期的影响。

第 6 章. 交易

交易是由外部拥有账户发起的签名消息,通过以太坊网络传输,并记录在以太坊区块链上。这个基本定义隐藏了许多令人惊讶和着迷的细节。看待交易的另一种方式是,它们是唯一可以触发状态变化或导致合约在 EVM 中执行的东西。以太坊是一个全局单例状态机,而交易使该状态机“运转”,改变其状态。合约不会自行运行。以太坊不会自主运行。一切都始于交易。

在本章中,我们将剖析交易,展示它们如何工作,并检查细节。请注意,本章的大部分内容是针对那些有兴趣在较低级别管理自己的交易的人,可能是因为他们正在编写钱包应用程序;如果您乐于使用现有的钱包应用程序,则不必担心这一点,但您可能会觉得这些细节很有趣!

交易的结构

首先,让我们看一下交易的基本结构,因为它在以太坊网络上被序列化和传输。每个收到序列化交易的客户端都会使用自己的内部数据结构将其存储在内存中,可能会添加网络序列化交易本身不存在的元数据。网络序列化是交易的唯一标准形式。

虽然在以太坊的早期,只有一种交易类型,但 EIP-2718 引入了一种处理不同交易类型并以不同方式处理它们的方法。特别是,每个交易都以一个字节开头,该字节指定了交易的类型:

transaction = tx_type || tx_payload

在撰写本文时(2025 年 6 月),存在五种交易类型,如表 6-1 所示。

表 6-1. EIP-2718 交易类型

类型标识符 名称
0x00 传统交易
0x01 EIP-2930 交易
0x02 EIP-1559 交易
0x03 EIP-4844 交易
0x04 EIP-7702 交易

让我们更详细地分析所有这些。

传统交易

传统交易是一个序列化的二进制消息,包含以下数据:

链 ID

您要将交易发送到的网络的链 ID。它是通过 EIP-155 添加的,作为一种简单的重放攻击保护机制。

Nonce

一个序列号,由原始 EOA 发出,用于防止消息重放。

Gas 价格

发起者愿意支付的 gas 价格(以 wei 为单位)。

Gas 限制

发起者愿意为此交易购买的最大 gas 量。请注意,您只需支付交易中实际使用的 gas 费用。Gas 限制仅表示您愿意支付的最大 gas 量。

接收者

目标以太坊地址。

Value

要发送到目的地的以太币数量。

Data

可变长度的二进制数据有效载荷。

v,r,s

原始 EOA 的 ECDSA 数字签名的三个组成部分。

交易消息的结构使用 递归长度前缀 (RLP) 编码方案进行序列化,该方案是专门为以太坊中简单、字节完美的数据序列化而创建的。以太坊中的所有数字都编码为大端整数,长度是 8 位的倍数。

请注意,此处显示了字段标签(togas limit 等),以便清晰起见,但它们不是交易序列化数据的一部分,其中包含 RLP 编码的字段值。通常,RLP 不包含任何字段分隔符或标签。RLP 的长度前缀用于标识每个字段的长度。超出定义长度的任何内容都属于结构中的下一个字段。

虽然这是实际传输的交易结构,但大多数内部表示和用户界面可视化都使用从交易或区块链派生的附加信息来修饰它。例如,您可能会注意到在标识发起者 EOA 的地址中没有“from”数据。这是因为 EOA 的公钥可以从 ECDSA 签名的 vrs 组件中派生出来。地址反过来可以从公钥派生出来。当您看到显示 from 字段的交易时,那是用于可视化交易的软件添加的。客户端软件经常添加到交易中的其他元数据包括块号(一旦发布并包含在区块链中)和交易 ID(计算出的哈希值)。同样,这些数据是从交易中派生的,并不构成交易消息本身的一部分。

EIP-2930 交易

EIP-2930 交易是第一个使用 EIP-2718 类型化交易信封的交易,交易类型为 0x01。它们基本上与之前的交易类型相同,但添加了一个名为访问列表的新字段。它是一个 (地址, 存储槽) 的数组,允许用户预先支付将要被交易访问的地址和存储槽。这样,在 EVM 中执行期间,用户支付的 gas 费用更少。

注意

更准确地说,包含在访问列表中的地址及其存储槽分别包含在 accessed_addressesaccessed_storage_keys 中,EVM 使用它们来区分热访问和冷访问。冷访问比热访问收取更多的 gas 费用。例如,如果访问的存储槽是热的,则 SLOAD 操作码收取 100 gas,否则收取 2,100 gas。

引入这种新的交易类型主要是为了解决 EIP-2929 产生的问题。EIP-2929 增加了状态访问操作码的 gas 成本,这导致一些智能合约在由于 gas 不足错误而正确处理交易时失败。通过引入访问列表,用户可以预先支付其交易将访问的地址和存储槽,从而防止这些故障。

EIP-1559 交易

EIP-1559 交易是在 2021 年 8 月 5 日的伦敦硬分叉期间引入的,交易类型为 0x02。它们通过引入一个新的协议参数:基础费用,彻底改变了以太坊的 gas 费用市场结构。

基础费用表示您需要在以太坊网络上发送交易支付的最低费用。区块 gas 限制从 1500 万增加到 3000 万 gas,并引入了区块 gas 目标,等于区块 gas 限制的一半:1500 万 gas。其想法是保持以太坊网络与之前相同的负载量,但允许区块在需要时变得更大(可能是原来的两倍)。

为了使区块的平均 gas 使用量保持在 1500 万 gas,基础费用不是固定值:它根据区块的利用率而变化。如果一个区块的 gas 使用量高于区块 gas 目标,那么基础费用就会增加;如果 gas 使用量低于 gas 目标,那么基础费用就会减少。

如图 6-1 所示,区块 gas 限制(几乎)总是在特定区块以固定的、四舍五入的值增加:1000 万、1250 万、1500 万和 3000 万。事实上,即使验证者(以及使用旧的 PoW 共识协议的矿工)可以在每个区块上略微调整 gas 目标,这直接转化为 gas 限制,区块 gas 限制是一个非常关键的值,每个人通常都遵循核心开发人员的建议。

区块 gas 限制随时间演变

图 6-1. 区块 gas 限制演变

基础费用不会流向创建区块的验证者(或矿工);相反,它们会立即被销毁,从而减少了 ETH 的总供应量。引入了一种新的费用 - 优先费用 - 您可以将其视为您支付给验证者(或矿工)的小费,以激励他们将您的交易包含在下一个区块中。

提示

理论上,您可以创建仅支付基础费用(这是强制性的)和零优先费用的交易。该协议不要求您向验证者支付小费。但实际上,您应该始终包含它,以便在合理的时间内确认您的交易。请注意,钱包通常会自动为您处理基础费用和优先费用,并将它们设置为正确的值。

EIP-1559 交易是一个序列化的二进制消息,包含以下数据:

链 ID

与传统交易相同

Nonce

与传统交易相同

每 gas 的最高优先费用

发起者愿意直接支付给验证者作为小费的价格(以 wei 为单位),以激励他们将交易包含在区块中

每 gas 的最高费用

发起者愿意支付的总 gas 价格(以 wei 为单位),包括基础费用和优先费用

Gas 限制

与传统交易相同

接收者

与传统交易相同

访问列表

与 EIP-2930 交易相同

Value

与传统交易相同

数据

与传统交易相同

v,r,s

与传统交易相同

与所有交易类型一样,消息结构使用 RLP 编码方案进行序列化。

EIP-4844 交易

EIP-4844 交易是在 2024 年 3 月 13 日的坎昆硬分叉中引入的,交易类型为 0x03。我们在第 4 章的“KZG 承诺”部分中已经提到了它们,我们将在第 16 章中进一步讨论它们。它们也被称为携带 blob 的交易,因为它们带有一个辅助数据 - 一个 blob - 其中包含大量数据(每个 blob ​​大约 131,000 字节),EVM 无法访问这些数据,但可以访问其承诺。

一种新型 gas——blob gas——用于 blob。它与普通 gas 完全分离且独立。它遵循自己的定位规则,即使它仍然深受 EIP-1559 的启发。其想法是,如果使用的 blob gas 大于目标 blob gas 使用量,那么 blob gas 价格就会上涨;否则,它会下降。

序列化的二进制消息与 EIP-1559 共享相同的格式,并添加了两个新内容:

每 blob gas 的最高费用

发起者愿意为 blob 支付的 blob gas 价格(以 wei 为单位)

Blob 版本化哈希

一个 32 字节值的列表,代表与 blob 相关的每个 KZG 承诺的版本化哈希

注意

通过坎昆硬分叉和 EIP-4844 交易的引入,区块头扩展了两个新元素:

Blob gas 已使用

区块中所有 EIP-4844 交易使用的 blob gas 总量

超额 blob gas

区块之前消耗的 blob gas 总量超过目标的累积值

EIP-7702 交易

EIP-7702 交易包含在 2025 年 5 月 7 日的 Pectra 硬分叉中,交易类型为 0x04。它们允许 EOA 设置其帐户中的代码。传统上,EOA 具有空代码;它们只能启动交易,但除非它们与智能合约交互,否则无法真正执行复杂的操作。EIP-7702 改变了这一点,使 EOA 可以执行以下操作。

批处理

允许来自同一用户的多个操作在一个原子交易中完成,例如 ERC-20 授权,然后花费该授权,这在许多去中心化交易所中是一个非常常见的工作流程。

赞助

帐户 X 代表帐户 Y 支付交易费用。

权限降级

用户可以签署子密钥并赋予它们特定的权限,这些权限远弱于对帐户的全局访问权限——例如,每天最多花费总余额的 1% 或仅与特定应用程序交互的权限。

底层细节非常复杂,如果您有兴趣,我们建议阅读 EIP 官方网站。但是,高级概述很简单但非常强大。EIP-7702 允许 EOA 为自己分配一个委托指示符。此委托指示符指向一个智能合约(位于以太坊主网上),并且当交易发送到 EOA 时,它会执行指定地址上的代码,就好像那是 EOA 的实际代码一样,如图 6-2 所示。

EIP-7702 委托指示符

图 6-2. EIP-7702 委托机制

交易 Nonce

Nonce 是交易中最重要的也是最不被理解的组件之一。“黄皮书”中对它的定义如下:

Nonce:一个标量值,等于从此地址发送的交易数量,或者,对于具有关联代码的帐户,等于此帐户创建的合约数量。

严格来说,nonce 是发起地址的一个属性——也就是说,它仅在发送地址的上下文中才有意义。但是,nonce 不会显式地作为帐户状态的一部分存储在区块链上。相反,它是通过计算源自某个地址的已确认交易的数量来动态计算的。

在两种情况下,交易计数 nonce 的存在非常重要:交易按创建顺序包含的可用性功能和交易重复保护的重要功能。让我们看一下每个示例场景:

场景 1

假设您想进行两笔交易。您有一笔重要的 6 以太币付款和另一笔 8 以太币付款。您首先签名并广播 6 以太币交易,因为它更重要,然后签名并广播 8 以太币交易。可悲的是,您忽略了您的帐户仅包含 10 以太币,因此网络无法接受这两项交易:其中一项将失败。因为您首先发送了更重要的 6 以太币交易,所以您理所当然地希望一项交易通过,而 8 以太币交易被拒绝。但是,在像以太坊这样的去中心化系统中,节点可以按任何顺序接收交易;不能保证特定节点会在另一项交易之前收到一项交易。因此,几乎可以肯定的是,某些节点首先收到 6 以太币交易,而其他节点首先收到 8 以太币交易。如果没有 nonce,那么哪一项被接受和哪一项被拒绝将是随机的。但是,通过包含 nonce,您发送的第一项交易将具有一个 nonce,例如 3,而 8 以太币交易具有下一个 nonce 值(即 4)。因此,在处理完 nonce 从 0 到 3 的交易之前,该交易将被忽略,即使它首先被收到。好险!

场景 2

现在假设您有一个包含 100 以太币的帐户。太棒了!您在网上找到一个人,他会接受以太币付款来购买您真正想购买的 mcguffin-widget。您向他们发送 2 以太币,他们向您发送 mcguffin-widget。太好了。为了进行那笔 2 以太币付款,您签署了一项交易,将 2 以太币从您的帐户发送到他们的帐户,然后将其广播到以太坊网络以进行验证并包含在区块链上。现在,如果在交易中没有 nonce 值,那么第二次向同一地址发送 2 以太币的另一项交易看起来与第一项交易完全相同。这意味着任何在以太坊网络上看到您的交易的人(这意味着每个人,包括收件人或您的敌人)都可以通过复制和粘贴您的原始交易并将其重新发送到网络来一遍又一遍地“重放”该交易,直到您的所有以太币都消失为止。但是,由于交易数据中包含 nonce 值,因此每一项交易都是唯一的,即使多次向同一收件人地址发送相同数量的以太币也是如此。因此,由于递增的 nonce 作为交易的一部分,任何人都不可能“复制”您进行的付款。

总而言之,重要的是要注意,与比特币协议的未花费交易输出 (UTXO) 机制相比,使用 nonce 对于基于帐户的协议实际上至关重要。

跟踪 Nonce

在本节和未来的章节中,我们将使用 Foundry 套件——特别是 cast 工具,它对于以非常简单的方式与区块链交互非常有用。如果要复制以下示例,请确保安装它。

首先,我们需要设置我们将在本章中使用的我们的钱包。打开一个终端窗口并键入:

$ cast wallet new
Successfully created new keypair.
Address:     0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0
Private key: 0xd6d2672c6b4489e6bcd4e93b9af620fa0204b639b7d7f93765479c0846be0b58

警告

如果您将资金发送到此处提到的地址,您就是在浪费您的钱,因为私钥是已知的,任何人都可以使用它将所有资金发送给自己。

现在,我们需要将私钥导入到计算机密钥库中,以便我们以后可以轻松地利用它:

$ cast wallet import example \
    --private-key 0xd6d2672c6b4489e6bcd4e93b9af620fa0204b639b7d7f93765479c0846be0b58
Enter password:
`example` keystore was saved successfully. Address: 0x7e41354afe84800680ceb104c5fc99ecb98a25f0

您可以选择(推荐)设置一个密码,在使用该帐户创建交易时需要该密码。现在我们已正确设置,但我们仍然没有任何 ETH。

您可以随时查看您的余额。首先,您需要获取与该帐户关联的地址:

$ cast wallet address --account example
Enter keystore password:
0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0

然后,您可以查询区块链的余额。在本章的所有示例中,我们将使用 Ethereum Sepolia,这是一个测试网区块链:

$ cast balance 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 --rpc-url https://ethereum-sepolia-rpc.publicnode.com
0

注意

请注意最后一个 cast 命令中的 --rpc-url 标志。它应该指向您感兴趣的区块链的 RPC 端点。可靠的 RPC 端点通常需要付费,但如果您只想进行实验(就像我们将在本章中所做的那样),有很多免费选项,例如:

要获得一些免费的 Sepolia ETH 代币,您可以使用其中一个在线水龙头。我们将使用 Google Cloud Web3 水龙头,它提供 0.05 ETH,如图 6-3 所示。转到 Ethereum Sepolia 水龙头。粘贴您的地址,然后单击“接收 0.05 Sepolia ETH”按钮。您应该很快收到 0.05 ETH。

Google Cloud Web3 水龙头界面

图 6-3. Google Cloud Web3 水龙头

您可以检查您的余额现在是否已更改且与 0 不同:

$ cast balance 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 --rpc-url https://ethereum-sepolia-rpc.publicnode.com
50000000000000000

太棒了!现在我们已完成设置,我们可以回到我们对交易 nonce 的实验。

实际上,nonce 是源自帐户的已确认(即链上)交易数量的最新计数。要找出 nonce 是什么,您可以使用 cast 询问区块链。只需打开一个新的终端窗口并键入:

$ cast nonce 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 --rpc-url https://ethereum-sepolia-rpc.publicnode.com
0

提示

Nonce 是一个从零开始的计数器,这意味着第一项交易的 nonce 为 0。实际上,在本示例中,我们尚未发送任何交易。另请注意,RPC 响应始终指向下一个可用的 nonce - 例如,如果一个地址已经发送了 10 项交易,这意味着它已经使用了 nonce 从 0 到 9,则对 nonce 查询的 RPC 响应将为 10。

现在让我们尝试发送一些 ETH。我们将向 vitalik.eth 发送 0.001 以太币,这是以太坊联合创始人 Vitalik Buterin 的 ENS 地址:

$ cast send --account example vitalik.eth --value 0.001ether --rpc-url https://ethereum-sepolia-rpc.publicnode.com
blockHash               0xa1171309fd406e44e86be9695a597d2bf5c728738d140b9958cfb50276c32b1b
blockNumber             6989355
contractAddress
cumulativeGasUsed       18009816
effectiveGasPrice       11163498011
from                    0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0
gasUsed                 21000
logs                    []
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status                  1 (success)
transactionHash         0xeb7bb0322858a4e1ed85271a60d2f8353075dc0bcd0c80448ee1d5ca0bb85def
transactionIndex        60
type                    2
blobGasPrice
blobGasUsed
authorizationList
to                      0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

您的钱包将跟踪它管理的每个地址的 nonce。只要您仅从一个点发起交易,这样做就非常简单。假设您正在编写自己的钱包软件或一些其他发起交易的应用程序。您如何跟踪 nonce?

当您创建新交易时,您将分配序列中的下一个 nonce。但在确认之前,它不会计入 nonce 总数。让我们通过快速连续发送以下命令来看一个例子:

$ cast nonce 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 --rpc-url https://ethereum-sepolia-rpc.publicnode.com
10
$ cast send --account example vitalik.eth --value 0.001ether --async --rpc-url https://ethereum-sepolia-rpc.publicnode.com
0x85f5b0db44407a6e9252590dc809087a2e232e00a951c9cb8853a109da5ddad4
$ cast nonce 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 --rpc-url https://ethereum-sepolia-rpc.publicnode.com
10
$ cast nonce 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 --rpc-url https://ethereum-sepolia-rpc.publicnode.com
11

如您所见,我们发送的交易并没有立即增加 nonce 计数;即使在发送交易后,它仍然等于 10。如果我们等待几秒钟以使网络通信稳定下来并将交易包含在区块中,nonce 调用将返回预期数字 11。

注意

请注意 cast send 命令中使用的 --async 标志:如果不使用它,cast 将阻止终端,直到交易在块中确认。使用该标志,它会将交易发送到网络并立即返回交易哈希,而无需等待将其包含在块中。

现在让我们看一个不同的例子:

$ cast nonce 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 --rpc-url https://ethereum-sepolia-rpc.publicnode.com
11
$ cast send --account example vitalik.eth --value 0.001ether --async --rpc-url https://ethereum-sepolia-rpc.publicnode.com
0x63188aa73247ffe06388a9adf399fa715e42fbc37ca53f77642a7860c80feb9d
$ cast nonce 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 --rpc-url https://ethereum-sepolia-rpc.publicnode.com
11
$ cast rpc eth_getTransactionCount 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 pending --rpc-url https://ethereum-sepolia-rpc.publicnode.com
"0xc"
$ cast nonce 0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0 --rpc-url https://ethereum-sepolia-rpc.publicnode.com
12

在发送交易之前,我们的 nonce 计数为 11;然后我们发送交易并立即询问新的 nonce。正如我们从前面的示例中所期望的那样,由于交易仍在内存池中等待,尚未包含在区块中,因此 nonce 尚未更新。尽管如此,我们使用了一个新的查询,该查询实际上能够获得真实的 nonce 编号,即使交易仍未确认(0xc 是十六进制格式的 12)。几秒钟后,该交易被添加到区块中,并且 cast nonce 调用返回新的正确值。

cast nonceeth_getTransactionCount pending 之间的区别仅仅是,第一个仅考虑已确认的交易——也就是说,包含在 block 中——而后者试图也包括仍在内存池中等待的交易。

警告

使用 eth_getTransactionCount pending 统计待处理交易时要小心。事实上,尽管它试图返回地址的真实 nonce 值,但无法完全确定是否存在其他待处理交易等待在内存池中确认。

公共内存池不是一个普遍的东西。每个节点都有自己的内存池:待处理交易的一种动态存储库,暂时保存它们直到它们在区块链上得到确认。可以通过设置不同的规则来接受或拒绝新交易来进行自定义。虽然 RPC 公司拥有庞大的节点网络并且应该(几乎)完整地了解所有待处理的交易是正确的,但您仍然应该谨慎地将该值视为 100% 正确。

Nonce 中的差距、重复 Nonce 和确认

如果您以编程方式创建交易,尤其是在同时从多个独立进程中执行此操作时,跟踪 nonce 非常重要。

以太坊网络根据 nonce 按顺序处理交易。这意味着如果您发送 nonce 为 0 的交易,然后发送 nonce 为 2 的交易,则第二项交易不会包含在任何区块中。它将存储在内存池中,同时以太坊网络等待缺失的 nonce 出现。所有节点都将假定缺失的 nonce 只是被延迟了,并且使用 nonce 2 的交易是按顺序收到的。

如果您然后发送具有缺失 nonce 1 的交易,则两项交易(nonce 1 和 2)都将被处理并包含(如果有效,当然)。一旦您填补了空白,网络就可以挖掘它保存在内存池中的乱序交易。

这意味着如果您按顺序创建多个交易,并且其中一个交易没有正式包含在任何区块中,则所有后续交易都将“卡住”,等待缺失的 nonce。交易可能会在 nonce 序列中产生无意的“差距”,因为它无效或 gas 不足。要使事情再次开始运行,您必须使用缺失的 nonce 发送有效的交易。您还应同样注意,一旦网络验证了具有“缺失”nonce 的交易,所有广播的具有后续 nonce 的交易都将增量地变为有效;无法“撤回”交易!

另一方面,如果您不小心复制了 nonce——例如,通过发送两项具有相同 nonce 但不同的收件人或值的交易——那么其中一项将被确认,而另一项将被拒绝。确认哪一项将取决于它们到达接收它们的第一个验证节点的顺序——也就是说,它将是相当随机的。

正如您所看到的,跟踪 nonce 是必要的,如果您的应用程序没有正确管理该过程,您将会遇到问题。不幸的是,如果您尝试同时执行此操作,事情会变得更加困难,我们将在下一节中看到。

并发、交易发起和 Nonce

并发是计算机科学的一个复杂方面,有时会出乎意料地出现,尤其是在像以太坊这样的去中心化和分布式实时系统中。

简而言之,并发是指多个独立系统同时进行计算。这些可以在同一程序中(例如,多线程)、在同一 CPU 上(例如,多处理)或在不同的计算机上(例如,分布式系统)。根据定义,以太坊是一个允许操作并发(节点、客户端、DApp)的系统,但通过共识强制执行单例状态。

现在,假设您有多个独立的钱包应用程序,它们从同一地址或多个地址生成交易。这种情况的一个示例是交易所处理来自交易所热钱包的提款(密钥在线存储的钱包,与密钥永不在线的冷钱包相对)。理想情况下,您希望有多个计算机处理提款,这样它不会成为瓶颈或单点故障。但是,这很快就会变得有问题,因为拥有多台计算机来处理提款会导致一些棘手的并发问题,其中最重要的是 nonce 的选择。多个计算机从同一热钱包帐户生成、签名和广播交易如何进行协调?

您可以使用一台计算机来按先到先得的原则将 nonce 分配给签名交易的计算机。但是,现在这台计算机就是单点故障。更糟糕的是,如果分配了多个 nonce 并且其中一个 nonce 永远不会被使用(因为正在处理使用该 nonce 的交易的计算机出现故障),则所有后续交易都将卡住。

另一种方法是生成交易但不为其分配 nonce(因此,让它们未签名——请记住,nonce 是交易数据的组成部分,因此需要包含在对交易进行身份验证的数字签名中)。然后,您可以将它们排队到单个节点,该节点对其进行签名并跟踪 nonce。但是,这再次成为该过程中的一个瓶颈:签名和跟踪 nonce 是您操作中可能在高负载下变得拥塞的部分,而生成未签名的交易是您实际上不需要并行化的部分。您会有一些并发性,但它会在过程的关键部分中缺少。

最后,这些并发问题,加上在独立进程中跟踪帐户余额和交易确认的难度,迫使大多数实现避免并发并创建瓶颈,例如单个进程处理交易所中的所有提款交易或可以完全独立地进行提款并且只需要间歇性地重新平衡的多个热钱包的设置。

交易 Gas

我们在前面的章节中稍微谈到过 gas,我们将在第 14 章中更详细地讨论它。但是,让我们介绍一些关于交易的 gasPricegasLimit 组件的作用的基础知识。

Gas 是以太坊的燃料。Gas 不是以太币:它是一种独立的虚拟货币,具有其自身与以太币的汇率。以太坊使用 gas 来控制交易可以使用的资源量,因为它将在世界各地的数千台计算机上进行处理。开放式(图灵完备)计算模型需要某种形式的计量以避免 DoS 攻击或无意的资源消耗交易。

Gas 与以太币分开以保护系统免受以太币价值快速变化可能带来的波动的影响,并作为管理 gas 支付的各种资源(计算、内存和存储)成本之间重要且敏感的比率的一种方式。

交易中的 gasPrice 字段允许交易发起者设置他们愿意支付的 gas 费用。该价格以每个 gas 单位的 wei 为单位进行衡量。

提示

受欢迎的网站 Etherscan 提供了以太坊主网络当前 gas 价格和其他相关 gas 指标的信息。

钱包可以调整其发起的交易中的 gasPrice,以更快地确认交易。gasPrice 越高,交易被确认的可能性就越大。相反,优先级较低的交易可以携带降低的价格,从而导致确认速度变慢。gasPrice 可以设置成的最小值等于包含它们的区块的基础费用(我们已将其与 EIP-1559 交易一起引入)。

注意

在伦敦硬分叉和 E如果你的交易目标地址是一个合约,那么所需的 gas 量可以被估算,但无法精确确定。这是因为合约可以评估不同的条件,从而导致不同的执行路径,并产生不同的总 gas 成本。合约可能只执行一个简单的计算,也可能执行一个更复杂的计算,这取决于你无法控制和预测的条件。为了演示这一点,让我们看一个例子:我们可以编写一个智能合约,每次被调用时,它都会递增一个计数器,并执行特定循环,循环次数等于调用计数。也许在第 100 次调用时,它会颁发一个特别奖,比如彩票,但这需要额外的计算来计算奖金。如果你调用合约 99 次,会发生一件事,但在第 100 次调用时,会发生非常不同的事情。你需要支付的 gas 量取决于在你的交易被包含在一个区块之前,有多少其他交易调用了该函数。也许你的估计是基于你是第 99 个交易,但在你的交易被确认之前,其他人第 99 次调用了该合约。现在你是第 100 个调用交易,计算量(和 gas 成本)要高得多。

借用以太坊中常用的一个类比,你可以将 gasLimit 视为你汽车油箱的容量(你的汽车是交易)。你往油箱里加满你认为旅程所需的 gas 量(验证你的交易所需的计算量)。你可以在一定程度上估算数量,但你的旅程可能会出现意外变化,例如绕道(更复杂的执行路径),从而增加燃料消耗。

然而,与油箱的类比有些误导。它实际上更像一家加油站公司的信用账户,你在行程结束后根据你实际使用的 gas 量付费。当你传输你的交易时,首批验证步骤之一是检查它发起的账户是否有足够的以太币来支付 maxFeePerGas × gasLimit(或者对于旧式交易,是 gasPrice × gasLimit)的费用。但在交易完成执行之前,这笔金额实际上不会从你的账户中扣除。你只需为你交易实际消耗的 gas 付费,但在你发送交易之前,你必须有足够的余额来支付你愿意支付的最高金额。

交易接收者

交易的接收者在 to 字段中指定。这包含一个 20 字节的以太坊地址。该地址可以是 EOA 或合约地址。

以太坊不会对此字段进行进一步验证。任何 20 字节的值都被认为是有效的。如果 20 字节的值对应于一个没有相应私钥或没有相应合约的地址,则该交易仍然有效。以太坊无法知道一个地址是否正确地从一个现有的公钥(因此从一个私钥)推导出来。

警告

以太坊协议不验证交易中的接收者地址。你可以发送到一个没有相应私钥或合约的地址,从而“销毁”以太币,使其永远无法花费。验证应该在用户界面级别完成。

向错误的地址发送交易可能会销毁发送的以太币,使其永远无法访问(无法花费),因为大多数地址没有已知的私钥,因此无法生成签名来花费它。假设地址的验证发生在用户界面级别(参见“带有大写校验和的十六进制编码(ERC-55)”)。事实上,有很多正当理由可以销毁以太币——例如,作为支付通道和其他智能合约中作弊的抑制因素——而且由于以太币的数量是有限的,销毁以太币实际上将销毁的价值分配给所有以太币持有者(与他们持有的以太币数量成比例)。

交易价值和数据

交易的主要“有效载荷”包含在两个字段中:valuedata。交易可以同时具有价值和数据,只有价值,只有数据,或者既没有价值也没有数据。所有四种组合都是有效的。

只有价值的交易是支付。只有数据的交易是调用。同时具有价值和数据的交易既是支付又是调用。既没有价值也没有数据的交易——嗯,那可能只是浪费 gas!但它仍然是可能的。

让我们尝试所有这些组合。我们将以与之前相同的方式使用 cast 在 Sepolia 测试网上发送交易。

我们的第一个交易只包含一个价值(支付)且没有数据有效载荷:

$ cast send --account example vitalik.eth --value 0.001ether --rpc-url https://ethereum-sepolia-rpc.publicnode.com

在图 6-4 中,你可以看到发送的价值是 0.001 以太币,数据有效载荷(Etherscan 上的输入数据)为空 (0x00)。

只有价值的交易

图 6-4. 只有价值的交易(支付)

下一个示例指定了价值和数据有效载荷(即使这个有效载荷将被忽略,因为我们将向 EOA 发送交易):

$ cast send --account example vitalik.eth 0x0001 --value 0.001ether --rpc-url https://ethereum-sepolia-rpc.publicnode.com

在图 6-5 中,你可以看到输入数据现在包含一些值,特别是 0x0001

具有价值和数据的交易

图 6-5. 具有价值和数据的交易

下一个交易包括数据有效载荷但指定值为零:

$ cast send --account example vitalik.eth 0x0001 --rpc-url https://ethereum-sepolia-rpc.publicnode.com

图 6-6 显示了一个确认屏幕,表明交易中发送的以太币值为零,数据有效载荷等于 0x0001

只有数据的交易

图 6-6. 只有数据的交易(调用)

最后,最后一个交易既没有要发送的值也没有数据有效载荷:

$ cast send --account example vitalik.eth --rpc-url https://ethereum-sepolia-rpc.publicnode.com

图 6-7 显示了我们的交易,它发送了零以太币并包含一个空的有效载荷。

既没有价值也没有数据的交易

图 6-7. 既没有价值也没有数据的交易

向 EOA 和合约传输价值

当你构建包含价值的以太坊交易时,这相当于付款。此类交易的行为方式不同,具体取决于目标地址是否为合约。

对于 EOA 地址——或者更确切地说,对于区块链上未标记为合约的任何地址——以太坊将记录状态更改,将你发送的值添加到该地址的余额中。如果之前没有见过该地址,它将被添加到客户端的内部状态表示中,并且其余额将初始化为你的付款值。

如果目标地址 (to) 是一个合约(或者是一个先前通过 EIP-7702 交易委托了一个合约的 EOA),那么 EVM 将执行该合约,并且将尝试调用你的交易的数据有效载荷中命名的函数。如果您的交易中没有数据,EVM 将调用一个 fallback function,如果该函数是 payable 的,将执行它来确定下一步该做什么。如果没有 fallback function,那么交易的效果将是增加合约的余额,就像支付给钱包一样。

合约可以通过在调用函数时立即抛出异常或由函数中编码的条件确定来拒绝传入的付款。如果该函数成功终止(没有异常),那么合约的状态将被更新以反映合约以太币余额的增加。

将数据有效载荷传输到 EOA 或合约

当您的交易包含数据时,它最有可能被发送到合约地址。这并不意味着您不能发送数据有效载荷到 EOA——这在以太坊协议中是完全有效的。但是,在这种情况下,数据的解释取决于您用来访问 EOA 的钱包。以太坊协议会忽略它。大多数钱包也会忽略在发送到他们控制的 EOA 的交易中收到的任何数据。未来,可能会出现允许钱包以合约的方式解释数据的标准,从而允许交易调用在用户钱包中运行的函数。关键的区别在于,与合约执行不同,EOA 对数据有效载荷的任何解释不受以太坊共识规则的约束。

现在,让我们假设您的交易正在将数据传递到合约地址。在这种情况下,数据将被 EVM 解释为合约调用。大多数合约更具体地使用此数据作为函数调用,调用命名的函数并将任何编码的参数传递给该函数。

发送到与 应用程序二进制接口 (ABI) 兼容的合约的数据有效载荷(您可以假设所有合约都是)是以下内容的十六进制序列化编码:

函数选择器

函数原型的 Keccak-256 哈希的前 4 个字节。这允许合约明确地标识您希望调用的函数。

函数参数

函数的参数,根据 ABI 规范中定义的各种基本类型的规则进行编码。

在示例 2-1 中,我们定义了一个用于提款的函数:

function withdraw(uint256 _withdrawAmount, address payable _to) public {

函数的原型定义为包含函数名称的字符串,后跟每个参数的数据类型,括在括号中并用逗号分隔。这里的函数名是 withdraw,它接受两个参数:

  • _withdrawAmount 是一个 uint256
  • _to 是一个 address

因此,withdraw 的原型将是:

withdraw(uint256,address)

注意

payable 关键字在 Solidity 中用于指示该地址可以接收以太币,但它不是函数选择器计算的一部分。只有基本类型 address 包含在原型中。

让我们计算这个字符串的 Keccak-256 哈希:

$ cast keccak256 "withdraw(uint256,address)"
0x00f714ce93c4a188ecc0c802ca78036f638c1c4b3ee9b98f3ed75364b45f50b1

哈希的前 4 个字节是 0x00f714ce。这是我们的函数选择器值,它将告诉合约我们要调用的函数。

接下来,让我们计算两个值作为参数 withdraw_amount_to 传递。我们想向地址 vitalik.eth 提取 0.000001 以太币,即 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045。让我们将它们与上一步计算的函数选择器一起编码,以获得最终的数据有效载荷(也称为 calldata):

$ cast calldata "withdraw(uint256,address)" 0.000001ether 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
0x00f714ce000000000000000000000000000000000000000000000000000000e8d4a51000000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045

这是我们交易的数据有效载荷,调用 withdraw 函数并请求 0.000001 以太币作为 withdraw_amount,并请求 vitalik.eth 作为 _to 地址。

特殊交易:合约创建

我们应该提到的一个特殊情况是在区块链上创建一个新的合约,将其部署以供将来使用的交易。合约创建交易 被发送到一个特殊的目的地地址,称为 零地址;合约注册交易中的 to 字段包含地址 0x0。这个地址既不代表 EOA(没有相应的私钥-公钥对),也不代表合约。它永远不能花费以太币或发起交易。它仅用作目的地,具有“创建此合约”的特殊含义。

虽然零地址仅用于合约创建,但它有时会收到来自各个地址的付款。对此有两种解释:要么这是偶然的,导致以太币损失,要么这是故意的 以太币销毁(通过将其发送到永远无法花费的地址来故意销毁以太币)。但是,如果你想进行故意的以太币销毁,你应该向网络明确你的意图,并使用专门指定的销毁地址:

0x000000000000000000000000000000000000dEaD

警告

发送到指定销毁地址的任何以太币都将无法花费,并且将永远丢失。

合约创建交易只需要包含一个数据有效载荷,该有效载荷包含将创建合约的已编译字节码。此交易的唯一效果是创建合约。如果你想使用起始余额设置新合约,可以在 value 字段中包含以太币金额,但这是完全可选的。如果你在没有数据有效载荷(没有合约)的情况下向合约创建地址发送一个值(以太币),那么效果与发送到销毁地址相同——没有合约可以记入,因此以太币会丢失。

例如,我们可以通过手动创建一个发送到零地址的交易,并在数据有效载荷中包含合约的方式来创建第 2 章中使用的 Faucet.sol 合约。合约需要被编译成字节码表示形式。这可以使用 Solidity 编译器来完成:

$ solc --bin Faucet.sol
Binary:
6080604052348015600e575f5ffd5…0033

相同的信息可以从 Remix 在线编译器获得。

现在我们可以使用二进制输出创建交易:

$ cast send --account example --rpc-url https://ethereum-sepolia-rpc.publicnode.com --create 6080604052348015600e575f5ffd5…0033

一旦合约发布,我们就可以在 Etherscan 区块浏览器上看到它,如图 6-8 所示。

Etherscan 上的合约创建

图 6-8. Etherscan 上的合约创建交易

我们可以查看交易的回执(使用交易哈希来引用它)以获取有关合约的信息:

$ cast receipt 0xa6b077d7d0ea21ff5f32a5a7243a81f0ab63e3b5e09c8e388c230fb067967cbb \
    --rpc-url https://ethereum-sepolia-rpc.publicnode.com
blockHash               0x6eb071eac79a84793321b086af96b32c1d861f04b0efc7354d0f6b8d5a8fa36a
blockNumber             7135544
contractAddress         0x4658eD241397F08cba8d5F3a69c7774cebE7f67F
cumulativeGasUsed       28390874
effectiveGasPrice       8867964529
from                    0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0
gasUsed                 145123
logs                    []
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status                  1 (success)
transactionHash         0xa6b077d7d0ea21ff5f32a5a7243a81f0ab63e3b5e09c8e388c230fb067967cbb
transactionIndex        136
type                    2
blobGasPrice
blobGasUsed            
authorizationList

这包括合约的地址(参见 contractAddress),我们可以使用它来向合约发送和接收资金,如上一节所示。

让我们首先将新创建的合约地址保存在一个变量中:

$ CONTRACT_ADDRESS=0x4658eD241397F08cba8d5F3a69c7774cebE7f67F

现在我们可以用一些以太币来资助它:

$ cast send --account example $CONTRACT_ADDRESS --value 0.02ether --rpc-url https://ethereum-sepolia-rpc.publicnode.com

最后,让我们使用我们之前计算的数据有效载荷调用 withdraw 函数,将 0.000001 以太币提取到 vitalik.eth 地址:

$ cast send --account example $CONTRACT_ADDRESS \
 0x00f714ce000000000000000000000000000000000000000000000000000000e8d4a51000000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045 \
   --rpc-url https://ethereum-sepolia-rpc.publicnode.com

过一段时间后,这两个交易都可以在 Etherscan 上看到,如图 6-9 所示。

Etherscan 上的合约交互

图 6-9. 合约资助和提款交易

数字签名

到目前为止,我们还没有深入研究有关数字签名的任何细节。在本节中,我们将了解数字签名如何工作,以及如何使用它们来证明私钥的所有权,而无需透露该私钥。

ECDSA

以太坊中使用的数字签名算法是 椭圆曲线数字签名算法 (ECDSA)。它基于椭圆曲线私钥-公钥对,如“椭圆曲线密码学解释”中所述。

数字签名在以太坊中具有三个目的(参见以下侧边栏)。首先,签名证明私钥的所有者,即以太坊账户的所有者,已授权花费以太币或执行合约。其次,它保证了 不可否认性:授权的证明是不可否认的。第三,签名证明了交易数据在签名后未被任何人修改且无法被修改。

数字签名的定义

根据 Wikipedia,数字签名是一种用于呈现数字消息或文档真实性的数学方案。有效的数字签名使接收者有理由相信消息是由已知的发送者创建的(身份验证),发送者不能否认发送了消息(不可否认性),并且消息在传输过程中没有被更改(完整性)。

数字签名如何工作

数字签名是一种由两部分组成的数学方案。第一部分是一种使用私钥(签名密钥)从消息(在我们的例子中是交易)创建签名的算法。第二部分是一种允许任何人仅使用消息和公钥来验证签名的算法。

创建数字签名

以太坊实现的 ECDSA 中,被签名的“消息”是交易,或者更准确地说,是来自交易的 RLP 编码数据的 Keccak-256 哈希。签名密钥是 EOA 的私钥。结果就是签名:

Sig = Fsig (Fkeccak256 (m), k)

其中:

  • k 是签名私钥
  • m 是 RLP 编码的交易
  • Fkeccak256 是 Keccak-256 哈希函数
  • Fsig 是签名算法
  • Sig 是生成的签名

函数 Fsig 生成一个由两个值组成的签名 Sig,通常称为 rs

Sig = (r, s)

验证签名

要验证签名,你必须拥有签名(rs)、序列化的交易和与用于创建签名的私钥对应的公钥。本质上,验证签名意味着只有生成此公钥的私钥的所有者才能在此交易上生成此签名。

签名验证算法接受消息(即,我们使用的交易哈希)、签名者的公钥和签名(rs 值),如果签名对该消息和公钥有效,则返回 true。

ECDSA 数学

如前所述,签名是由一个数学函数 Fsig 创建的,该函数生成一个由两个值 rs 组成的签名。在本节中,我们将更详细地了解函数 Fsig

签名算法首先以加密安全的方式生成一个 临时(临时)私钥。此临时密钥用于计算 rs 值,以确保攻击者无法通过在以太坊网络上观看已签名的交易来计算发送者的实际私钥。

正如我们从第 4 章中了解到的,临时私钥用于派生相应的(临时)公钥,因此我们有:

  • 一个加密安全的随机数 q,用作临时私钥
  • q 和椭圆曲线生成点 G 生成的相应临时公钥 Q

数字签名的 r 值是临时公钥 Q 的 x 坐标。

从那里,该算法计算签名的 s 值,使得:

s ≡ q-1 (Keccak256(m) + r * k) (mod p)

其中:

  • q 是临时私钥
  • r 是临时公钥的 x 坐标
  • k 是签名(EOA 所有者)私钥
  • m 是交易数据
  • p 是椭圆曲线的素数阶

验证是签名生成函数的逆运算,使用 rs 值以及发送者的公钥来计算一个值 Q,该值是椭圆曲线上的一个点(签名创建中使用的临时公钥)。步骤如下:

  1. 检查所有输入是否正确形成。
  2. 计算 w = s-1 mod p
  3. 计算 u1 = Keccak256(m) * w mod p
  4. 计算 u2 = r * w mod p
  5. 最后,计算椭圆曲线上的点 Q ≡ u1 * G + u2 * K (mod p)

其中:

  • rs 是签名值
  • K 是签名者(EOA 所有者)的公钥
  • m 是已签名的交易数据
  • G 是椭圆曲线生成点
  • p 是椭圆曲线的素数阶

如果计算点 Q 的 x 坐标等于 r,那么验证者可以得出结论,签名是有效的。请注意,在验证签名时,既不知道也不透露私钥。

提示

ECDSA 本身必然是一个相当复杂的数学部分;完整的解释超出了本书的范围。在线有很多指南可以逐步引导你完成它:搜索“ECDSA 解释”或尝试 这个

实践中的交易签名

要生成有效的交易,发起者必须使用 ECDSA 对消息进行数字签名。当我们说“签署交易”时,我们实际上是指“签署 RLP 序列化交易数据的 Keccak-256 哈希”。签名应用于交易数据的哈希,而不是交易本身。

要在以太坊中签署交易,发起者必须:

  1. 创建一个交易数据结构,其中包含该特定交易类型所需的所有字段。
  2. 生成交易数据结构的 RLP 编码序列化消息。
  3. 计算此序列化消息的 Keccak-256 哈希。
  4. 计算 ECDSA 签名,使用发起 EOA 的私钥对哈希进行签名。
  5. 将 ECDSA 签名计算出的 vrs 值附加到交易。

特殊的签名变量 v 指示两件事:链 ID 和恢复标识符,以帮助 ECDSArecover 函数检查签名。在非旧式交易中,v 变量不再编码链 ID,因为该 ID 直接包含为构成交易本身的项之一。有关链 ID 的更多信息,请参见“使用 EIP-155 创建原始交易”。恢复标识符用于指示公钥 y 组件的奇偶校验(有关更多详细信息,请参见“签名前缀值 (v) 和公钥恢复”)。

注意

在第 2,675,000 个区块,以太坊实现了 Spurious Dragon 硬分叉,除其他更改外,还引入了一种新的签名方案,其中包括交易重放保护(防止用于一个网络的交易在其他网络上重放)。此新的签名方案在 EIP-155 中指定。此更改会影响交易及其签名的形式,因此必须注意三个签名变量中的第一个(即 v),它采用两种形式之一,并指示包含在正在散列的交易消息中的数据字段。

原始交易创建和签名

在本节中,我们将创建一个原始交易并对其进行签名,使用 ethers.js 库。示例 6-1 演示了通常在钱包或代表用户签署交易的应用程序内部使用的函数。

示例 6-1. 创建和签署原始以太坊交易

// Load requirements first:
//
// npm install ethers
//
// Run with: node eip1559_tx.js
import { ethers } from "ethers";

// Create provider with your RPC endpoint
const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");

// Private key
const privKey = "0xd6d2672c6b4489e6bcd4e93b9af620fa0204b639b7d7f93765479c0846be0b58";

// Create a wallet instance
const wallet = new ethers.Wallet(privKey);

// Get nonce and create transaction data
const txData = {
  nonce: await provider.getTransactionCount(wallet.address), // Get nonce from provider
  to: "0xb0920c523d582040f2bcb1bd7fb1c7c1ecebdb34", // Receiver address
  value: ethers.parseEther("0.0001"), // Amount to send (0.0001 ETH here)
  gasLimit: ethers.toBeHex(0x30000), // Gas limit
  maxFeePerGas: ethers.parseUnits("100", "gwei"), // Max fee per gas
  maxPriorityFeePerGas: ethers.parseUnits("2", "gwei"), // Max priority fee
  data: "0x", // Optional data
  chainId: 11155111, // Sepolia chain ID
};

// Calculate RLP-encoded transaction hash (pre-signed)
const unsignedTx = ethers.Transaction.from(txData).unsignedSerialized;
console.log("RLP-Encoded Tx (Unsigned): " + unsignedTx);
const txHash = ethers.keccak256(unsignedTx);
console.log("Tx Hash (Unsigned): " + txHash);

// Sign the transaction
async function signAndSend() {
  // Sign the transaction with the wallet
  const signedTx = await wallet.signTransaction(txData);
  console.log("Signed Raw Transaction: " + signedTx);

  // Send the signed transaction to the Ethereum network
  const txResponse = await provider.broadcastTransaction(signedTx);
  console.log("Transaction Hash: " + txResponse.hash);

  // Wait for the transaction to be mined
  const receipt = await txResponse.wait();
  console.log("Transaction Receipt: ", receipt);
}

signAndSend().catch(console.error);

运行示例代码会产生以下结果:

$ node eip1559_tx.js 
RLP-Encoded Tx (Unsigned): 0x02f283aa36a714847735940085174876e8008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb34865af3107a400080c0
Tx Hash (Unsigned): 0x31d43a580534a77c71324a8434df6f2df993b3d551b29d4b70d8a889768a53f7
Signed Raw Transaction: 0x02f87583aa36a714847735940085174876e8008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb34865af3107a400080c001a03f8ed18cb03ee0fe3fbc3f0a7477a2f68db6ec84450e77e702b82a3f2c873aa4a0205c4f6a16ea8ad13a148cc3105814cd4a6860cd26a771651199c85ccb7c7f0f
Transaction Hash: 0x07bfbeb337e19763a1f74d989dae2953807dcb06822354cfefb16405a11beb93
Transaction Receipt:  TransactionReceipt {
  provider: JsonRpcProvider {},
  to: '0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34',
  from: '0x7e41354AfE84800680ceB104c5Fc99eCB98A25f0',
  contractAddress: null,
  hash: '0x07bfbeb337e19763a1f74d989dae2953807dcb06822354cfefb16405a11beb93',
  index: 1,
  blockHash: '0x0ac051e8f615805c69eec6e193e39637adeb7cf314a0098d455e7d9ac395a7ee',
  blockNumber: 7135937,
  logsBloom: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000**`0xb0920c523d582040f2bcb1bd7fb1c7c1ecebdb34`**

接收者地址。

**`0x5af3107a4000`**

从发送者发送到接收者的 wei 值:十进制为 1014。 转换为 0.0001 以太币。

**`0x`**

空数据负载。

**`[]`**

空访问列表。

**`0x01`**

签名的 `v` 值;`0x01` 表示椭圆曲线的奇数 y 坐标 ( `0x00` 表示偶数 y 坐标)。

**`0x3f8ed18cb03ee0fe3fbc3f0a7477a2f68db6ec84450e77e702b82a3f2c873aa4`**

签名的 `r` 值。

**`0x205c4f6a16ea8ad13a148cc3105814cd4a6860cd26a771651199c85ccb7c7f0f`**

签名的 `s` 值。

## 使用 EIP-155 创建原始交易

EIP-155 “简单重放攻击保护” 标准规定了一种受重放攻击保护的交易编码,该编码在签名之前在交易数据中包含链 ID。 这确保了为一个区块链(例如,以太坊主网络)创建的交易在另一个区块链(例如,以太坊经典或 Sepolia 测试网络)上无效。 因此,在一个网络上广播的交易不能在另一个网络上重放,因此得名。

通过在签名的数据中包含链 ID,交易签名可以防止任何更改,因为如果修改链 ID,签名将失效。 因此,EIP-155 使得交易不可能在另一个链上重放,因为签名的有效性取决于链 ID。

链 ID 字段根据交易预期的网络取值,如表 6-2 所示。

**表 6-2. 链标识符**

| 链 | 链 ID |
|-------|----------|
| 以太坊主网 | 1 |
| 以太坊 Sepolia | 11155111 |
| 以太坊 Holesky | 17000 |

有关链标识符的完整列表,请参见 [ChainList](https://chainlist.org)。

生成的交易结构经过 RLP 编码、哈希和签名。 有关更多详细信息,请参见 EIP-155 规范。

## 签名前缀值 (v) 和公钥恢复

正如在 “交易的结构” 中提到的,交易消息不包含 “from” 字段。 这是因为发起者的公钥可以直接从 ECDSA 签名中计算出来。 一旦你有了公钥,你就可以很容易地计算出地址。 恢复签名者公钥的过程称为 *公钥恢复* 。

给定在 “ECDSA 数学” 中计算出的值 `r` 和 `s` ,我们可以计算出两个可能的公钥。

首先,我们从签名中的 x 坐标 `r` 值计算出两个椭圆曲线点,`R` 和 `R′`。 之所以有两个点,是因为椭圆曲线关于 x 轴对称,所以对于任何值 x,都有两个可能的值适合曲线,一个在 x 轴的每一侧。

从 `r` 我们还计算 `r–1`,它是 `r` 的乘法逆元。

最后,我们计算 `z`,它是消息散列的 n 个最低有效位,其中 n 是椭圆曲线的阶。

然后,两个可能的公钥是:

K1 = r–1 (sR – zG)


和:

K2 = r–1 (sR′ – zG)



其中:

- `K1` 和 `K2` 是签名者公钥的两种可能性
- `r-1` 是签名的 `r` 值的乘法逆元
- `s` 是签名的 `s` 值
- `R` 和 `R′` 是临时公钥 `Q` 的两种可能性
- `z` 是消息散列的 n 个最低有效位
- `G` 是椭圆曲线生成点

为了提高效率,交易签名包括一个前缀值 `v`,它告诉我们两个可能的 R 值中哪个是临时公钥。 如果 `v` 是偶数,那么 `R` 是正确的值。 如果 `v` 是奇数,那么它是 `R′`。 这样,我们只需要计算 R 的一个值和 K 的一个值。

## 分离签名和传输(离线签名)

交易签名后,就可以传输到以太坊网络了。 创建、签名和广播交易的三个步骤通常作为一个单独的操作发生,例如,使用 `cast send` 命令。 但是,正如你在 “创建和签署原始交易” 中看到的那样,你可以分两个单独的步骤创建和签署交易。 一旦你有了签名的交易,你就可以使用 `ethers.JsonRpcProvider("..."").broadcastTransaction` 发送它,它接受十六进制编码的签名交易并在以太坊网络上发送它。

为什么要分离交易的签名和传输呢? 最常见的原因是安全性。 签署交易的计算机必须在内存中加载解锁的私钥。 进行传输的计算机必须连接到互联网(并且正在运行以太坊客户端)。 如果这两个功能都在一台计算机上,那么你的私钥就会在在线系统上,这非常危险。

> **警告**
>
> 如果你将私钥保存在网上,你将面临多种形式的攻击,例如恶意软件和远程黑客攻击,并且你更容易受到网络钓鱼攻击。

分离签名和传输的功能并在不同的机器上执行这些功能(分别在离线和在线设备上)称为 *离线签名*,是一种常见的安全实践。

图 6-10 显示了该过程,该过程遵循以下步骤:

1. 在在线计算机上创建未签名的交易,在此在线计算机上可以检索到帐户的当前状态,特别是当前的 nonce 和可用资金。
2. 将未签名的交易传输到 “气隙” 离线设备以进行交易签名(例如,通过二维码或 USB 闪存盘)。
3. 将签名的交易(返回)传输到在线设备,以便在以太坊区块链上广播(例如,通过二维码或 USB 闪存盘)。

![离线签名工作流程](https://img.learnblockchain.cn/masterethereumbook/images/ch6/maet_0610.png)

**图 6-10.** 离线签名过程

根据你需要的安全级别,“离线签名” 计算机可以与在线计算机具有不同程度的分离,范围从隔离和防火墙保护的子网(在线但隔离)到称为 *气隙系统* 的完全脱机系统。 在气隙系统中,根本没有网络连接,计算机通过 “气隙” 与在线环境分离。 要签署交易,你需要使用数据存储介质或(更好)网络摄像头和二维码在气隙计算机之间传输交易。 当然,这意味着你必须手动传输每个要签名的交易,而这无法扩展。

虽然没有多少环境可以利用完全气隙的系统,但即使是一小程度的隔离也具有显着的安全优势。 例如,与在线系统上的签名相比,具有仅允许消息队列协议通过的防火墙的隔离子网可以提供大大减少的攻击面和更高的安全性。 许多公司为此目的使用诸如 ZeroMQ (0MQ) 之类的协议。 通过这样的设置,交易被序列化并排队以进行签名。 排队协议以类似于 TCP 套接字的方式将序列化的消息传输到签名计算机。 签名计算机(仔细地)从队列中读取序列化的交易,使用适当的密钥应用签名,然后将它们放在传出队列上。 传出队列将签名的交易传输到具有以太坊客户端的计算机,该客户端将它们出列并传输它们。

## 交易生命周期

在本节中,我们将探讨交易的完整生命周期,从签名的那一刻开始,到包含在区块中以及区块最终确定为止。

### 创建和签署交易

第一步是创建交易,选择交易类型并填写所需的所有字段。 例如,在 “创建和签署原始交易” 部分中,我们创建了一个 EIP-1559 交易。

一旦我们有了交易,我们需要使用正确的私钥对其签名(否则,由于签名无效,交易将无效),以便获得最终和确定的签名交易。 这是我们需要发送到网络并等待其包含在区块中的实际数据。

### 将交易发送到网络

交易需要包含在区块中才能被以太坊协议认为是已确认的,否则,它只是我们知道的签名交易。

> **提示**
>
> 重要的是要理解,以太坊协议仅考虑包含在有效链中的区块中的交易有效。 要更新其状态,你需要将签名的交易发送到网络并等待其将你的交易包含在区块中。 如果你的签名交易未包含在区块中,则它本身不会执行任何操作。

因此,我们需要将签名的交易发送到网络。 为此,我们只需要将我们的交易发送到以太坊节点:这可以是我们的自己的节点或第三方节点,例如 Alchemy、Infura 或 Public Node(这是我们在上一个示例中使用的节点)。

> **注意**
>
> 默认情况下,所有钱包都使用第三方节点,以便用户无需安装任何东西即可开始使用以太坊网络。 尽管如此,如果你想最大程度地提高隐私并且真的不想依赖任何人,你应该使用自己的客户端。 你可以参考第 3 章,获取有关如何安装你的第一个以太坊节点的详细指南。

当我们的签名交易到达第一个节点时,该节点会执行一些验证,以便立即删除垃圾邮件无效交易。 如果验证成功,则该节点会将交易添加到其 *mempool* 中,并将其传播到其所有对等方的子集。 它们每个人都验证它,将其添加到自己的 mempool 中,并进一步传播它。

此过程是以太坊 P2P gossip 协议的一部分,结果是在短短几秒钟内,以太坊交易会传播到全球所有以太坊节点。 从每个节点的角度来看,不可能辨别交易的来源。 将其发送到该节点的邻居可能是交易的发起者,也可能从其邻居之一接收到该交易。 为了能够跟踪交易的来源或干扰传播,攻击者必须控制所有节点的很大一部分。 这是 P2P 网络的安全和隐私设计的一部分,尤其是在应用于区块链网络时。

> **注意**
>
> 你可能想知道为什么节点不将交易泛洪到其所有邻居,而是仅将交易发送到邻居的子集。 答案是效率和带宽保留。 实际上,将所有交易发送到所有节点效率极低:会有大量的重复消息,网络流量会很大,并且随着网络流量随交易数量呈指数增长,可伸缩性会很差。

### 构建区块

现在,我们的交易几乎已经到达了所有以太坊节点,但它仍然没有被确认,因为它没有包含在区块中,直到被选择提议下一个区块的验证者最终从自己的 mempool 中获取所有交易,将它们添加到区块中,并将区块发布到网络。

一旦交易包含在区块中,它们就会通过修改帐户的余额(在简单支付的情况下)或通过调用更改其内部状态的合约来修改以太坊状态。 这些更改与交易一起记录,以 *交易收据* 的形式,其中还可能包含事件。 在 “创建和签署原始交易” 示例中,你可以找到我们交易的最终收据。

### 最终确定交易

我们的交易现在已包含在区块中,并且已经修改了以太坊状态。 尽管如此,包含它的区块仍然可以被还原并被不包含我们的交易的另一个区块替代,即使这极不可能发生。 这称为 *区块重组*,或简称 *区块重组* 。

为了完全确定我们的交易无法还原,我们需要等待包含它的区块被以太坊共识协议最终确定(我们将在第 15 章中更详细地探讨这一点)。 这通常需要大约 12 分钟。

> **注意**
>
> 即使你应该等待最终确定才能完全确定你的交易已在区块链上确认,但通常你只需要等待几个区块即可。 钱包通常会在你的交易包含在区块中后立即将其显示为已确认。

## 另一种生命周期

我们在上一节中探讨的生命周期是交易遵循的旧标准流程,从交易开始到在最终确定的区块中结束。 在过去的三到四年中,甚至在当今,已经为交易建立了新的生命周期。 为了解释这一点,我们需要介绍一个称为 *提议者和构建者分离* 的概念。

### MEV 以及提议者和构建者分离

正如我们将在第 15 章中进一步探讨的那样,每 12 秒需要验证者提议一个新区块以推进以太坊链。验证者从其 mempool 中收集交易,组织它们以填充区块,并将区块发布到网络,以便所有其他节点都可以验证和传播该区块。

传统上,以太坊节点会根据支付给验证者的交易费用(具体而言,是 EIP-1559 交易的优先级费用)来确定交易的优先级,以便最大程度地提高利润。这种方法是多年来优化矿工和验证者收入的标准方法。

然而,随着以太坊在 2020 年 “DeFi 夏季” 期间越来越受欢迎,甚至在 2021 年的牛市期间更是如此,一种新现象出现了:*最大可提取价值* (*MEV*,以前称为矿工可提取价值)。 这一概念极大地改变了以太坊矿工和验证者创建区块的方式。

MEV 指的是区块生产者可以通过就以下方面做出战略决策来从区块中提取的最大价值:

- 要包含在区块中的交易
- 区块内交易的顺序
- 要从区块中排除的交易

虽然这听起来可能无害,但它过去并且现在仍然对区块生产动态产生巨大影响。 例如,考虑一笔将在去中心化交易所(如 Uniswap)上购买大量特定代币 X 的交易。 验证者可以看到此交易并提前知道它会使价格上涨很多。 这意味着验证者可以添加两笔交易来从此情况中获利:

1. 第一笔是购买一些代币 X 的交易,并且已排序使其发生在大型购买交易之前。
2. 第二笔是出售在前一笔交易中购买的代币 X,并且已排序使其发生在大型购买交易之后。

这称为 *三明治攻击*:验证者从大型购买交易产生的滑点中获利。 让我们用一个玩具(和简化的)示例来演示这个概念。

用户提交了一笔购买 1000 个 xyz 代币的交易 (tx1),如图 6-11 所示。 假设代币 xyz 的价格为 1000 美元,因此该用户将购买价值 100 万美元的代币。 这种购买压力将使代币的价格上涨到 1010 美元。

![用户提交大型购买交易](https://img.learnblockchain.cn/masterethereumbook/images/ch6/maet_0611.png)

**图 6-11.** MEV 干预前的用户交易

但是验证者看到了通过抢跑交易 tx1 赚钱的机会。 他们创建了两笔交易:tx0 和 tx2,其中 tx0 包含 10 个 xyz 代币的买单,tx2 包含相同数量的卖单。 验证者将这些交易精确地放置在用户 tx1 之前和之后,如图 6-12 所示。 请记住,验证者可以这样做,因为他们是实际创建区块的角色,因此他们可以自由选择区块中交易的排序。

![验证者夹击用户交易](https://img.learnblockchain.cn/masterethereumbook/images/ch6/maet_0612.png)

**图 6-12.** 验证者的三明治攻击

结果是验证者能够以 1000 美元的价格购买 10 个 xyz 代币,并以 1010 美元的价格出售它们,从而获利 10 美元 × 10 = 100 美元。

这是验证者用来最大化利润的一个非常简单的策略。 还有许多其他更复杂的策略。 竞争非常激烈,以至于出现了一个新的角色:*构建者*。

事实上,运行所有这些策略需要大量的处理能力,比普通验证者拥有的要多得多(请记住,你只需 16 GB 的内存即可运行验证者节点)。 这意味着 MEV 也在威胁验证者的去中心化,有利于那些能够负担得起在基础设施和努力发现新的有利可图策略以构建 “更好” 区块的人身上花费数百万美元的大型实体。

这个问题的解决方案是在 2021 年 1 月随着 Flashbots v0.1 的发布而出现的。 它使区块提议者(首先是矿工,现在是验证者)能够以无需信任的方式将寻找最佳区块构建的任务外包给这些称为构建者的新实体。 由于这种职责分离(构建者用交易填充区块并创建向验证者支付最高费用的区块(构建者也获得一部分费用)和验证者将区块提议到网络)验证者仍然可以在普通供应商机器上运行。

### 私有内存池

MEV 生态系统的发展如此之快,以至于现在,构建者不仅在寻找最大化利润的最佳策略方面竞争,而且还在他们可以用来填充区块的交易方面竞争。 他们拥有的交易越多,他们就越能更好地应用他们的策略。

这导致了 *私有内存池* 的创建。 这些内存池允许用户或实体直接将交易提交给区块生成者,而无需将其暴露给普通网络。 它们在减轻抢跑等风险和支持以隐私为中心的工作流程方面发挥着重要作用。

Flashbots 开发了自己的解决方案,称为 [Flashbots Protect](https://docs.flashbots.net/flashbots-protect/overview)。 最受欢迎的钱包 MetaMask 默认开始为其用户使用私有内存池(称为 [智能交易](https://metamask.io/news/introducing-smart-transactions))。

### 新的交易生命周期

MEV、提议者和构建者分离以及私有内存池已极大地改变了区块生产的环境。 现在,许多交易没有遵循我们在上一节中解释的通常生命周期。 在创建和签名后,它们会通过私有内存池直接发送给构建者。 构建者将处理它们,尝试将它们包含在最佳区块中,最后将整个区块发送给将要提议下一个区块的验证者。 这些交易跳过了公共内存池传播,并直接出现在构建的区块中。

## 多重签名交易

如果您熟悉比特币的脚本功能,您就会知道可以创建一个比特币 *多重签名* 帐户,该帐户只能在多个参与者签署交易时才能花费资金(例如,两个中的两个或四个中的三个签名)。 以太坊的基本 EOA 值交易没有对多重签名进行规定;但是,可以通过智能合约强制执行任意签名限制,该智能合约具有您可以想到的处理以太币和代币转移的任何条件。

为了利用此功能,以太币必须转移到钱包合约,该合约已使用所需的消费规则进行编程,例如多重签名要求或消费限制(或两者的组合)。 然后,一旦消费条件得到满足,钱包合约就会在获得授权 EOA 的提示后发送资金。 例如,要在多重签名条件下保护您的以太币,请将以太币转移到多重签名合约。 无论何时您想将资金发送到另一个帐户,所有必需的用户都需要使用常规钱包应用程序向合约发送交易,从而有效地授权合约执行最终交易。

这些合约还可以设计为在执行本地代码或触发其他合约之前需要多个签名。 该方案的安全性最终由多重签名合约代码决定。

将多重签名交易作为智能合约实施的能力证明了以太坊的灵活性。 目前,Gnosis Safe 已成为创建多重签名帐户的事实标准。 这套经过实战检验的智能合约被主要协议和 DAO 广泛使用,截至 2024 年 11 月,已保护超过 60 亿美元的 ETH 和超过 740 亿美元的 ERC-20 代币,如图 6-13 所示。

![Gnosis Safe 随时间推移保护的价值](https://img.learnblockchain.cn/masterethereumbook/images/ch6/maet_0613.png)

**图 6-13.** Gnosis Safe 保护数十亿价值

> **注意**
>
> 使用 Gnosis Safe 时,执行交易的通常工作流程如下:
>
> 1. Safe 的一个签名者提议一个他们想要签名并发送到以太坊网络的交易。
> 2. 其他签名者看到该交易,如果他们同意其目的,则对其进行签名。
> 3. 当达到法定人数时,该交易最终会发送到网络,并在网络中进行处理和执行。

## 结论

交易是以太坊系统中每个活动的起点。 交易是导致 EVM 评估合约、更新余额以及更普遍地修改以太坊区块链状态的 “输入”。 接下来,我们将更详细地使用智能合约,并学习如何使用 Solidity 这种面向合约的语言进行编程。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ethbook
ethbook
江湖只有他的大名,没有他的介绍。