原文:Transactions
交易是由外部拥有的账户发起的签名消息,由以太坊网络传输,并记录(挖掘)在以太坊区块链上。在这个基本定义的背后,有许多令人惊讶和迷人的细节。查看交易的另一种方法是,它们是唯一可以触发状态更改或导致合约在EVM中执行的事物。以太坊是一个全局单例状态机,交易是唯一可以使状态机“tick”,改变其状态的东西。合约不是自己运行的。以太坊不会“在后台”运行。一切都始于交易。
在本节中,我们将剖析交易,展示它们的工作方式,并了解细节。
交易结构
首先让我们看一下交易的基本结构,因为它是在以太坊网络上序列化和传输的。接收序列化交易的每个客户端和应用程序将使用其自己的内部数据结构将其存储在内存中,可能使用在网络序列化交易本身中不存在的元数据进行修饰。因此,交易的网络序列化是交易结构的唯一通用标准。
交易是包含以下数据的序列化二进制消息:
- nonce:由发起人EOA发出的序列号,用于防止重播消息。
- gas price:发起人愿意支付的gas价格(wei)。
- start gas:发起人愿意支付的最大gas量。
- to:目的地以太坊地址。
- value:要发送到目的地的以太数量。
- data:可变长度二进制数据负载。
- v,r,s:发起人EOA的ECDSA签名的三个组成部分。
交易消息的结构使用递归长度前缀(RLP)编码方案(参见[rlp])进行序列化,该方案专为在以太坊中准确和字节完美的数据序列化而创建。以太坊中的所有数字都被编码为big-endian,长度为8位的倍数。
注意,为了清楚起见,这里示出了字段标签(“to”,“start gas”等),但不是交易序列化数据的一部分,其包含RLP编码的字段值。通常,RLP不包含任何字段分隔符或标签。RLP的长度前缀用于标识每个字段的长度。因此,超出定义长度的任何内容都属于结构中的下一个字段。
虽然这是传输的实际交易结构,但大多数内部表示和用户界面可视化都通过从交易或区块链派生的附加信息来修饰。
例如,您可能会注意到标识发起者EOA的地址中没有“from”数据。EOA的公钥可以很容易地从ECDSA签名的v,r,s组件中获得。反过来,地址可以很容易地从公钥中导出。当您看到显示“from”字段的交易时,该交易由用于可视化交易的软件添加。客户端软件经常添加到交易中的其他元数据包括区块号(一旦被挖掘)和交易ID(计算的哈希)。同样,此数据源自交易,而不是交易消息本身的一部分。
交易中的nonce
nonce是交易中最重要和最不理解的组件之一。黄色纸的定义是:
nonce:一个标量值,等于从这个地址发送的交易数,或者,对于关联code的帐户,这个帐户创建合约的数量。
严格的说,nonce是发送地址的属性(它只在发送地址的上下文中有意义)。但是,nonce不会明确存储为区块链中帐户状态的一部分。相反,它是通过计算发送地址的已确认交易的数量来动态计算的。
nonce值还用于防止错误计算账户余额。例如,假设一个账户的余额为10以太,并签署两个交易,每个交易花费6个以太,分别为nonce 1和nonce 2。这两项交易中哪一项有效?在像以太坊这样的分布式系统中,节点可能不按顺序接收交易。nonce强制来自任何地址的交易按顺序处理,没有间隔,无论节点接收它们的顺序如何。这样,所有节点都计算出相同的余额。使用nonce 1支付6 ether的交易将成功处理,将账户余额减少到4以太。用nonce 2支付6 ether的交易将被所有节点看作无效,无论它何时被接收。
使用nonce确保所有节点计算相同的余额和正确的序列交易,等同于用于防止比特币“双重支付”的机制。但是,由于以太坊跟踪账户余额并且不单独跟踪货币(在比特币中称为UTXO),因此只有在错误地计算账户余额时才会发生“双重支付”。nonce机制可以防止这种情况发生。
追踪nonces
实际上,nonce是源自帐户的已确认(已开采)交易数量的最新计数。要找出nonce是什么,你可以查询区块链,例如通过web3接口:
检索示例地址的交易计数
web3.eth.getTransactionCount( “0x9e713963a92c02317a681b9bb3065a8249de124f”)
40
Tip | nonce是一个从零开始的计数器,这意味着第一个交易具有nonce 0.在检索我们的示例地址的交易计数时,我们的交易计数为40,这意味着已经看到了nonce 0到39。下一笔交易的nonce将是40。 |
---|
您的钱包将跟踪其管理的每个地址的nonce。这样做相当简单,只要您只从一个点发起交易即可。假设您正在编写自己的钱包软件或其他一些发起交易的应用程序。你如何跟踪nonces?
创建新交易时,将在序列中分配下一个nonce。但在确认之前,它不会计入getTransactionCount总计。
不幸的是,如果我们连续发送一些交易,getTransactionCount函数将遇到一些问题。有一个已知的错误,getTransactionCount没有正确计算待处理的交易。我们来看一个例子:
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
40
web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
41
web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
41
web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
41
如您所见,我们发送的第一笔交易将交易计数增加到41,显示待处理的交易。但是当我们快速连续发送3个以上的交易时,getTransactionCount调用没有正确计算它们。它只计算了一个,即使在mempool中有3个待处理的交易。如果我们等待几秒钟,一旦区块被挖掘,getTransactionCount调用将返回正确的数字。但在此期间,虽然有多个待处理的交易,但对我们没有帮助。
实施构建交易的应用程序时,它不能依赖getTransactionCount来处理待处理的交易。只有当待处理和确认相等(所有未完成的交易都已确认)时,您才能信任getTransactionCount的输出以启动您的nonce计数器。此后,在每个交易确认之前跟踪应用程序中的nonce。
Parity的JSON RPC接口提供了parity_nextNonce函数,该函数返回应在交易中使用的下一个nonce。parity_nextNonce函数正确计算nonce,即使您快速连续构造多个交易,也不确认它们。
Parity有一个用于访问JSON RPC接口的Web控制台,但在这里我们使用命令行HTTP客户端来访问它:
curl --data '{"method":"parity_nextNonce","params":["0x9e713963a92c02317a681b9bb3065a8249de124f"],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST localhost:8545
{"jsonrpc":"2.0","result":"0x32","id":1}
nonces,重复nonce和确认间隔
如果以编程方式创建交易,则跟踪nonce非常重要,尤其是如果您同时从多个独立进程执行此操作。
以太坊网络基于nonce顺序处理交易。这意味着如果您使用nonce 0传输交易,然后使用nonce 2传输交易,则不会挖掘第二个交易。它将存储在mempool中,而以太坊网络则等待丢失的nonce出现。所有节点都将假设缺少的nonce已被简单地延迟,并且具有nonce 2的交易是无序接收的。
如果您随后使用缺失的nonce 1传输交易,则将挖掘两个交易(nonce 1和2)。一旦填补了空白,网络就可以挖掘它在mempool中保存的无序交易。
这意味着如果您按顺序创建多个交易并且其中一个交易没有被挖掘,则所有后续交易都将“卡住”,等待缺少的nonce。交易可以在nonce序列中产生无意的“间隔”,因为它无效或者气体不足。为了让事情再次运转起来,您必须使用缺少的nonce传输有效的交易。
另一方面,如果您意外复制了一个nonce,例如通过发送具有相同nonce但具有不同收件人或值的两个交易,则其中一个将被确认,一个将被拒绝。确认哪一个将由它们到达接收它们的第一个验证节点的顺序确定。
正如您所看到的,跟踪nonce是必要的,如果您的应用程序无法正确管理该过程,您将遇到问题。不幸的是,如果您尝试同时执行此操作会变得更加困难,我们将在下一节中看到。
并发,交易发起和nonce
并发性是计算机科学的一个复杂方面,有时会出乎意料地出现,尤其是在像以太坊这样的分散/分布式实时系统中。
简单来说,并发性是指您可以通过多个独立系统同时进行计算。这些可以在相同的程序(例如线程)中,在相同的CPU上(例如,多处理),或在不同的计算机上(即,分布式系统)。根据定义,以太坊是一个允许操作(节点,客户端,DApps)并发的系统,但强制执行单例状态(例如,每个挖掘的区块只有一个系统的公共/共享状态)。
现在,假设我们有多个独立的钱包应用程序,它们使用相同的地址生成交易。这种情况的一个例子是热钱包的交易所处理提款。理想情况下,您希望有多个计算机处理提款,因此它不会成为堵塞或单点故障。然而,这很快就会成为问题,因为有多台计算机产生提款将导致一些棘手的并发问题,其中最重要的是选择nonce。多台计算机如何协调从同一个热钱包帐户生成,签名和广播交易?
您可以使用一台计算机以先到先得的方式将nonce分配给计算机签名交易。但是,这台计算机现在是一个单点故障。更糟糕的是,如果分配了几个nonce并且其中一个永远不会被使用(由于计算机处理与该nonce的交易的失败),所有后续的都会被卡住。
您可以生成交易,但不要对它们进行签名或为它们分配nonce。然后将它们排队到一个标记它们的节点,并跟踪nonce。同样,你有一个单点的失败。对nonce的签名和跟踪是您的操作的一部分,可能会在负载下变得拥挤,而无符号交易的生成是您不需要并行化的部分。您有并发性,但在流程的任何有用部分都没有它。
最后,除了在独立进程中跟踪帐户余额和交易确认的难度之外,这些并发问题迫使大多数实现避免并发和创建瓶颈,例如处理交易所中的所有提款交易的单个流程。
交易中的gas
我们在[gas]中详细讨论了gas。但是,让我们介绍一下有关交易的gasPrice和startGas组件的作用的一些基础知识。
gas是以太坊的燃料。gas不是以太 - 它是一种单独的虚拟货币,汇率与以太成比例。以太坊使用gas来控制交易可以花费的资源量,因为它将在全球数千台计算机上处理。开放式(图灵完成)计算模型需要某种形式的计量,以避免拒绝服务攻击或无意的资源吞噬交易。
gas和ether分离,以保护系统免受随着ether值的快速变化而可能出现的波动。
交易中的gasPrice字段允许交易发起人设置每个单位gas的汇率。gas价格以每单位gas计量。例如,在我们最近为本书中的一个例子创建的交易中,我们的钱包将gasPrice设置为3 Gwei(3 Giga-wei,30亿wei)。
热门网站ethgasstation.info提供有关gas当前价格的信息,以及以太坊主网络的其他相关gas指标:
钱包可以在它们发起的交易中调整gasPrice,以实现更快的交易确认(挖掘)。gasPrice越高,交易可能越快确认。相反,优先级较低的交易可以降低他们愿意为gas支付的价格,从而导致确认速度变慢。可设置的最低gasPrice为零,这意味着免费交易。在区块中对空间的低需求期间,此类交易将被挖掘。
Tip | 最低可接受的gasPrice为零。这意味着钱包可以生成完全免费的交易。根据容量,这些可能永远不会被挖掘,但协议中没有禁止自由交易的内容。您可以在以太坊区块链中找到成功挖掘此类交易的几个示例。 |
---|
web3界面提供了gasPrice建议,通过计算几个区块的中间价格:
truffle(mainnet)> web3.eth.getGasPrice(console.log)
truffle(mainnet)> null BigNumber {s:1,e:10,c:[10000000000]}
与gas有关的第二个重要领域是startGas。这在[gas]中有更详细的解释。简单来说,startGas定义了交易发起人愿意花费多少单位的gas来完成交易。对于简单付款,意味着将ether从一个EOA转移到另一个EOA的交易,所需的燃气量固定为21,000个燃气单位。要计算将花费多少以太,您需要将21,000乘以您愿意支付的gasPrice:
truffle(mainnet)> web3.eth.getGasPrice(function(err, res) {console.log(res*21000)} )
truffle(mainnet)> 210000000000000
如果您的交易的接收地址是合约,那么可以估算所需的gas量,但无法准确确定。这是因为合约可以评估不同的条件,导致不同的执行路径,不同的gas成本。这意味着合约可能只执行一个简单的计算或更复杂的计算,这取决于你无法控制和无法预测的条件。为了证明这一点,让我们使用一个精心设计的例子:每次调用一个合约时,它会递增一个计数器,并且在第100次(仅)计算一些复杂的东西。如果你调用合约99次就会发生一件事,但是在第100次调用时会发生一些完全不同的事情。您要支付的gas量取决于在开采交易之前有多少其他交易已经调用该函数。也许您的估算是基于第99次交易,就在您的交易被开采之前,其他人第99次调用了合约。现在你是第100个要调用的交易,计算工作量(和gas成本)要高得多。
借用以太坊中使用的常用类比,您可以将startGas视为汽车中的油箱(您的汽车就是交易)。您可以使用您认为旅程所需的气体(为验证交易所需的计算)填充油箱。您可以在一定程度上估算金额,但您的旅程可能会出现意外变化,例如转移(更复杂的执行路径),这会增加燃油消耗。
然而,与油箱的类比有些误导。它更像是一家加油站公司的信用账户,根据您实际使用的燃气量,您可以在旅行结束后付款。当您发起交易时,第一个验证步骤之一是检查它发送的帐户是否有足够以支付gasPrice * startGas的费用。但是,在交易执行结束之前,实际上并未从您的帐户中扣除金额。您只需支付最终交易实际消耗的gas费用,但在发送交易之前,您必须有足够的余额支付您愿意支付的最高金额。
交易的接收者
交易接收者在to字段中指定。这包含一个20字节的以太坊地址。地址可以是EOA或合约地址。
以太坊没有进一步验证该领域。任何20字节的值都被认为是有效的。如果20字节值对应于没有相应私钥的地址,或没有相应的合约,则该交易仍然有效。以太坊无法知道地址是否是从公钥(因此来自私钥)正确派生的。
Warning | 以太坊不能也不会验证交易中的接收者地址。您可以发送到没有相应私钥或合约的地址,从而“销毁”以太,使其永远丢失。验证应在用户界面级别完成。 |
---|
将交易发送到无效地址将销毁发送的以太,使其永远无法访问(不可连接),因为无法生成签名来使用它。假设地址验证发生在用户界面级别(参见[eip-55]或[icap])。事实上,销毁以太有很多正当理由,包括作为一种博弈论,用来抑制在支付渠道和其他智能合同中作弊。
交易的Value和Data
交易的主要“有效负载”包含在两个字段中:value和data。交易可以同时具有value和data,仅具有value,仅具有data,或者既不具有value也不具有data。所有四种组合都有效。
仅具有value的交易是付款。仅包含data的交易是调用。既没有value也没有data的交易,这可能只是浪费gas!但它仍然有可能。
让我们尝试以上所有组合:
首先,我们设置钱包中的发送地址和接收地址,只是为了让演示更容易阅读:
设置发送和接收地址
src = web3.eth.accounts[0];
dst = web3.eth.accounts[1];
仅具有value的交易
web3.eth.sendTransaction({from: src, to: dst, value: web3.toWei(0.01, "ether"), data: ""});
图1,Parity钱包显示一笔有value没有data的交易
同时具有value和data的交易
web3.eth.sendTransaction({from: src, to: dst, value: web3.toWei(0.01, "ether"), data: "0x1234"});
图2,Parity钱包显示一笔有value也有data的交易
仅包含data的交易
web3.eth.sendTransaction({from: src, to: dst, value: 0, data: "0x1234"});
图3,Parity钱包显示一笔没有value有data的交易
既没有value也没有data的交易
web3.eth.sendTransaction({from: src, to: dst, value: 0, data: ""}));
图4,Parity钱包显示一笔没有value也没有data的交易
向EOA和合约传递value
当您构建包含value的以太坊交易时,它相当于付款。根据接收地址是否为合约,这些交易的行为会有所不同。
对于EOA地址,或者更确切地说,对于未在区块链中注册为合约的任何地址,以太坊将记录状态更改,并将您发送的value添加到地址的余额中。如果以前没有看到该地址,则会创建该地址并将其余额初始化为您的付款金额。
如果接收地址(to)是合约,则EVM将执行合约并尝试调用交易的数据有效负载中指定的函数(请参阅[invocation])。如果您的交易中没有数据有效负载,则EVM将调用目标合约的fallback功能,如果该功能需要支付,则将执行该功能以确定下一步操作。
合约可以通过在调用payable函数时立即抛出异常,或者由payable函数中编码的条件确定,来拒绝收款。如果payable函数成功终止(没有例外),则更新合约的状态以反映合约的以太余额的增加。
向EOA和合约传递数据有效负载
当您的交易包含数据有效负载时,它很可能发送到合同地址。这并不意味着您无法将数据有效负载发送到EOA。事实上,你可以做到这一点。但是,在这种情况下,数据有效负载的解释取决于您用于访问EOA的钱包。大多数钱包会忽略交易中收到的任``何数据有效负载,而不是他们控制的EOA。将来,有可能出现允许钱包以合约的方式解释数据有效负载编码的标准,从而允许交易调用在用户钱包内运行的功能。关键的区别在于,与合约执行不同,EOA对数据有效负载的任何解释都不受以太坊的共识规则约束。
现在,我们假设您的交易正在向合约地址提供数据有效负载。在这种情况下,数据有效负载将由EVM解释为函数调用,调用命名函数并将任何编码参数传递给函数。
发送给合约的数据有效负载是十六进制序列化编码:
- 函数选择器:函数原型的Keccak256哈希的前4个字节。这允许EVM明确地识别您要调用的函数。
- 函数参数:根据EVM定义的各种基本类型的规则进行编码。
让我们看一个简单的例子,它来自我们的[solidity_faucet_example]。在Faucet.sol中,我们为提款定义了一个函数:
function withdraw(uint withdraw_amount) public {
withdraw函数的原型定义为包含函数名称的字符串,后跟括号中括起的每个参数的数据类型,并用单个逗号分隔。函数名是withdraw,它接受一个uint的参数(这是uint256的别名)。所以withdraw的原型将是:
withdraw(unit256)
让我们计算一下这个字符串的Keccak256哈希值(我们可以使用tuffle控制台或任何JavaScript web3控制台来做到这一点):
web3.sha3("withdraw(uint256)");
'0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f'
哈希的前4个字节是0x2e1a7d4d。这是我们的“函数选择器”值,它将告诉EVM我们想要调用哪个函数。
接下来,让我们计算一个值作为withdraw_amount参数。我们想要提取0.01以太。让我们将其编码为十六进制序列的大端无符号256位整数,以wei命名:
withdraw_amount = web3.toWei(0.01, "ether");
'10000000000000000'
withdraw_amount_hex = web3.toHex(withdraw_amount);
'0x2386f26fc10000'
现在,我们将函数选择器添加到金额(填充到32个字节):
2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000
这是我们交易的数据有效负载,调用withdraw函数并请求0.01 ether作为withdraw_amount。
特殊交易:合约注册
有一个具有数据有效负载且没有value的交易的特例。那就是一个注册新合约的交易。合约注册交易被发送到特殊目的地地址,即零地址。简单来说,合约注册交易中的to字段包含地址0x0。该地址既不代表EOA(没有相应的私钥/公钥对)也不代表合约。它永远不会花费以太或发起交易。它仅用作目的地,具有特殊含义“注册此合约”。
虽然零地址仅用于合同注册,但它有时会收到来自各种地址的付款。对此有两种解释:要么是偶然的,导致失去以太;要么是故意销毁以太。如果您想进行有意的以太销毁,您应该明确网络意图并使用专门指定的地址:
0x000000000000000000000000000000000000dEaD
Warning | 发送到合同合约地址0x0或上面指定的地址0x0 ... dEaD的任何以太将变得不可靠并永远丢失。 |
---|
合约注册交易不应包含以太值,只包含合约的已编译字节码的数据有效负载。此交易的唯一效果是注册合约。
例如,我们可以发布[intro]中使用的Faucet.sol。合约需要编译成二进制十六进制表示。这可以使用Solidity编译器完成。
> solc --bin Faucet.sol
======= Faucet.sol:Faucet =======
Binary:
6060604052341561000f57600080fd5b60e58061001d6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d146041575b005b3415604b57600080fd5b605f60048080359060200190919050506061565b005b67016345785d8a00008111151515607757600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050151560b657600080fd5b505600a165627a7a72305820d276ddd56041f7dc2d2eab69f01dd0a0146446562e25236cf4ba5095d2ee802f0029
也可以从Remix在线编译器获得相同的信息。现在我们可以创建交易了。
> src = web3.eth.accounts[0];
> faucet_code = "0x6060604052341561000f57600080fd5b60e58061001d6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d146041575b005b3415604b57600080fd5b605f60048080359060200190919050506061565b005b67016345785d8a00008111151515607757600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050151560b657600080fd5b505600a165627a7a72305820d276ddd56041f7dc2d2eab69f01dd0a0146446562e25236cf4ba5095d2ee802f0029"
> web3.eth.sendTransaction({from: src, data: faucet_code, gas: 113558, gasPrice: 200000000000})
"0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b"
无需指定to参数,将使用默认的零地址。您可以指定gasPrice和gas limit。合约被注册我们可以在etherscan block explorer上看到它
图5. Etherscan显示合约成功
您可以查看交易收据以获取有关合约的信息。
> eth.getTransactionReceipt("0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b");
{
blockHash: "0x6fa7d8bf982490de6246875deb2c21e5f3665b4422089c060138fc3907a95bb2",
blockNumber: 3105256,
contractAddress: "0xb226270965b43373e98ffc6e2c7693c17e2cf40b",
cumulativeGasUsed: 113558,
from: "0x2a966a87db5913c1b22a59b0d8a11cc51c167a89",
gasUsed: 113558,
logs: [],
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
status: "0x1",
to: null,
transactionHash: "0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b",
transactionIndex: 0
}
在这里我们可以看到合约的地址。我们可以从合约中发送和接收资金,如[将数据有效负载传输到EOA或合约中所示]。
> contract_address = "0xb226270965b43373e98ffc6e2c7693c17e2cf40b"
> web3.eth.sendTransaction({from: src, to: contract_address, value: web3.toWei(0.1, "ether"), data: ""});
"0x6ebf2e1fe95cc9c1fe2e1a0dc45678ccd127d374fdf145c5c8e6cd4ea2e6ca9f"
> web3.eth.sendTransaction({from: src, to: contract_address, value: 0, data: "0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000"});
"0x59836029e7ce43e92daf84313816ca31420a76a9a571b69e31ec4bf4b37cd16e"
过了一会儿,两个交易都在ethescan上可见。
图6. Etherscan显示发送和接收资金的交易
数字签名
到目前为止,我们还没有深入探讨有关“数字签名”的任何细节。在本节中,我们将了解数字签名的工作原理以及如何在不泄露私钥的情况下提供私钥的所有权证明。
椭圆曲线数字签名算法(ECDSA)
以太坊中使用的数字签名算法是椭圆曲线数字签名算法,或ECDSA。ECDSA是用于基于椭圆曲线私钥/公钥对的数字签名的算法,如[elliptic_curve]中所述。
数字签名在以太坊中有三个用途(参见下面的侧栏)。首先,签名证明私钥的所有者(其暗示是以太坊账户的所有者)已授权以太的支出或合约的执行。其次,授权证明是不可否认的(不可否认性)。第三,签名证明交易数据在交易签署后没有也不能被任何人修改。
维基百科对“数字签名”的定义
数字签名是用于证明数字消息或文档的真实性的数学方案。有效的数字签名使收件人有理由相信该邮件是由已知发件人(身份验证)创建的,发件人不能拒绝发送邮件(不可否认),并且邮件在传输过程中未被更改(完整性) 。
资料来源:https://en.wikipedia.org/wiki/Digital_signature
数字签名的工作原理
数字签名是一种由两部分组成的数学方案。第一部分是使用来自消息(交易)的私钥(签名密钥)创建签名的算法。第二部分是一种算法,允许任何人仅使用消息和公钥来验证签名。
创建数字签名
在以太坊的ECDSA实现中,被签名的“消息”是交易,或者更准确地说,是来自交易的RLP编码数据的Keccak256散列。签名密钥是EOA的私钥。结果是签名:
描述:
- k是签名的私钥
- m是RLP编码的交易
- 是Keccak256哈希函数
- 是签名算法
- Sig是由此产生的签名
有关ECDSA数学的更多细节可以在[ECDSA数学中]找到。
函数产生一个由两个值组成的签名Sig,通常称为R和S:
Sig =(R,S)
验证签名
要验证签名,必须具有签名(R和S)、序列化交易和公钥(对应于用于创建签名的私钥)。实质上,验证签名意味着“只有生成此公钥的私钥的所有者才能在此交易中生成此签名”。
签名验证算法接收消息(交易的哈希或其它部分),签名者的公钥和签名(R和S值),如果签名对此消息和公钥有效,则返回TRUE。
ECDSA数学
如前所述,签名由数学函数创建,该函数产生由两个值R和S组成的签名。在本节中,我们将更详细地介绍函数。
签名算法首先生成短暂(临时)私钥/公钥对。在涉及签名私钥和交易哈希的转换之后,该临时密钥对用于计算R和S值。
临时密钥对由两个输入值生成:
- 随机数q,用作临时私钥
- 和椭圆曲线发生器点G.
从q和G,我们生成相应的临时公钥Q(计算为Q = q * G,与导出以太坊公钥的方式相同;参见[pubkey])。然后,数字签名的R值是短暂公钥Q的x坐标。
从那里,算法计算签名的S值,使得:
描述:
- q是临时的私钥
- R是临时公钥的x坐标
- k是签名(EOA所有者)的私钥
- m是交易数据
- p是椭圆曲线的素数阶
验证是签名生成函数的反转,使用R,S值和公钥来计算值Q,它是椭圆曲线上的一个点(签名创建中使用的临时公钥):
描述:
- R和S是签名值
- K是签名(EOA所有者)的公钥
- m是已签名的交易数据
- G是椭圆曲线发生器点
- p是椭圆曲线的素数阶
如果计算的点Q的x坐标等于R,则验证者可以断定签名是有效的。
请注意,在验证签名时,私钥既不知道也不透露。
Tip | ECDSA必然是一个相当复杂的数学; 完整的解释超出了本书的范围。许多优秀的在线指南将逐步引导您完成:搜索“ECDSA解释”或尝试以下内容:http://bit.ly/2r0HhGB。 |
---|
在实践中签署交易
为了产生有效的交易,发起者必须使用椭圆曲线数字签名算法对消息应用数字签名。当我们说“签署交易”时,我们实际上是指“签署RLP序列化交易数据的Keccak256哈希”。签名应用于交易数据的哈希,而不是交易本身。
Tip | 在#2,675,000块,以太坊实施了“Spurious Dragon”硬分叉,除了其他变化之外,还引入了一个包含交易重放保护的新签名方案。这种新的签名方案在EIP-155中规定(见[eip155])。此更改会影响签名过程的第一步,在签名之前向交易添加三个字段(v,r,s)。 |
---|
要在以太坊签署交易,发起人必须:
- 创建一个包含九个字段的交易数据结构:nonce,gasPrice,startGas,to,value,data,v,r,s
- 生成RLP编码的交易序列化消息
- 计算此序列化消息的Keccak256哈希值
- 计算ECDSA签名,使用发件人EOA的私钥对哈希进行签名
- 在交易中插入ECDSA签名的计算r和s值
原始交易创建和签署
让我们使用ethereumjs-tx库创建一个原始交易并对其进行签名。此示例的源代码位于GitHub存储库中的raw_tx_demo.js中:
raw_tx_demo.js:在JavaScript中创建和签署原始交易
link:code/web3js/raw_tx/raw_tx_demo.js[]
在这里下载:https://github.com/ethereumbook/ethereumbook/blob/develop/code/web3js/raw_tx/raw_tx_demo.js
运行示例代码:
$ node raw_tx_demo.js
RLP-Encoded Tx: 0xe6808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb348080
Tx Hash: 0xaa7f03f9f4e52fcf69f836a6d2bbc7706580adce0a068ff6525ba337218e6992
Signed Raw Transaction: 0xf866808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb3480801ca0ae236e42bd8de1be3e62fea2fafac7ec6a0ac3d699c6156ac4f28356a4c034fda0422e3e6466347ef6e9796df8a3b6b05bed913476dc84bbfca90043e3f65d5224
使用EIP-155创建原始交易
EIP-155“简单重放攻击保护”标准规定了重放攻击保护的交易编码,其在签名之前包括交易 数据内的链标识符。这确保了为一个区块链(例如以太坊主网络)创建的交易在另一个区块链(例如以太坊Classic或Ropsten测试网络)上无效。因此,在一个网络上广播的交易不能在另一个网络上重播,因此标准的“重放攻击保护”名称。
EIP-155在交易数据结构中添加了v,r和s三个字段。r和s字段初始化为零。在对交易数据进行编码和哈希之前,会将这三个字段添加到交易数据中。因此,这三个附加字段会更改交易的哈希值,稍后将应用签名。通过在被签名的数据中包含链标识符,交易签名可以防止任何更改,因为如果链标识符被修改,则签名无效。因此,EIP-155使得交易不可能在另一条链上重放,因为签名的有效性取决于链标识符。
v签名前缀字段初始化为链标识符,其值为:
链 | Chain ID |
---|---|
以太坊主网 | 1 |
Morden (obsolete), Expanse | 2 |
Ropsten | 3 |
Rinkeby | 4 |
Rootstock主网 | 30 |
Rootstock测试网络 | 31 |
Kovan | 42 |
以太坊经典主网 | 61 |
以太坊经典测试网络 | 62 |
Geth私有网络 | 1337 |
生成的交易结构经过RLP编码,散列和签名。稍微修改签名算法以对v前缀中的chainID进行编码。
有关更多详细信息,请参阅EIP-155规范:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
签名前缀值(v)和公钥恢复
如[交易结构中]所述,交易消息不包括任何“from”字段。这是因为发起人的公钥可以直接从ECDSA签名中计算出来。获得公钥后,您可以轻松计算地址。恢复签名者公钥的过程称为公钥恢复。
给定在[ECDSA Math]中计算的值r和s,我们可以计算两个可能的公钥。
首先,我们从签名中的x坐标r值计算两个椭圆曲线点R和R'。有两个点,因为椭圆曲线在x轴上是对称的,因此对于任何值x,在x轴的任一侧有两个可能的值匹配曲线。
从r中,我们还要计算,它是r的乘法倒数。
最后,我们计算z,它是消息哈希的n个最低位,其中n是椭圆曲线的阶数。
那么两个可能的公钥是:
和
描述:
- 和是签名者公钥的两种可能性
- 是签名r值的乘法倒数
- s是签名的值
- R和R'是临时公钥Q的两种可能性
- z是消息哈希的n个最低位
- G是椭圆曲线发生器点
为了提高效率,交易签名包括前缀值v,它告诉我们两个可能的R值中的哪一个是临时的公钥。如果v是偶数,则R是正确的值。如果v是奇数,那么R'是正确的值。这样,我们只需要计算R的一个值和K的一个值。
分离签名和传输(离线签名)
交易签署后,即可传输到以太坊网络。创建、签名和广播交易的三个步骤通常发生在单个函数中,例如使用web3.eth.sendTransaction。但是,正如我们在[Raw交易创建和签名]中看到的那样,您可以通过两个单独的步骤创建和签署交易。获得签名交易后,您可以使用web3.eth.sendSignedTransaction传输它,该事件采用十六进制编码和签名的交易消息并在以太坊网络上传输。
您为什么要分开交易的签名和传输?最常见的原因是安全性:签署交易的计算机必须具有加载在内存中的未锁定私钥。传输的计算机必须连接到互联网并运行以太坊客户端。如果这两个功能在一台计算机上,那么您在在线系统上有私钥,这非常危险。分离签名和传输的功能称为离线签名,是一种常见的安全措施。
根据您所需的安全级别,您的“离线签名”计算机可能与在线计算机有不同程度的分离,从孤立和防火墙子网(在线但隔离)到称为气隙系统的完全脱机系统。在气隙系统中根本没有网络连接 - 计算机与在线环境之间存在“空中”差距。要对交易进行签名,您可以使用数据存储介质或(更好)网络摄像头和QR码将其传输到气隙计算机或从气隙计算机传输。当然,这意味着您必须手动传输要签名的每个交易,但这不会扩展。
虽然没有多少环境可以使用完全气隙系统,但即使是很小程度的隔离也具有显着的安全性优势。例如,具有仅允许消息队列协议通过的防火墙的隔离子网可以提供比在线系统上签名大大减少的攻击面和更高的安全性。许多公司使用ZeroMQ(0MQ)等协议,因为它为签名计算机提供了很小的攻击面。通过这样的设置,交易被序列化并排队等待签名。排队协议以类似于TCP套接字的方式将序列化消息发送到签名计算机。签名计算机从队列中读取序列化交易(小心),使用适当的密钥应用签名,并将它们放在传出队列中。
交易广播
以太坊网络使用“flood”路由协议。每个以太坊客户端,充当对等网络(P2P)的节点 ,其(理想地)形成网状网络。没有网络节点是“特殊的”,它们都作为平等的对等体。我们将使用术语“节点”来指代连接并参与P2P网络的以太坊客户端。
交易传播开始于创建以太坊节点(或从离线接收)签署的交易。交易被验证,然后传输到直接连接到始发节点的所有其他以太坊节点。平均而言,每个以太坊节点保持与至少13个称为其邻居的其他节点的连接。每个邻居节点在收到交易后立即验证交易。如果他们同意这是有效的,他们会保存一份副本并将其传播给所有的邻居(除了它的邻居)。结果,交易从源节点向外传播扩散,直到网络中的所有节点都拥有该交易的副本。
几秒钟内,以太坊交易就会传播到全球所有以太坊节点。从每个节点的角度来看,不可能辨别交易的起源。发送给我们节点的邻居可能是交易的发起者,或者可能从其邻居那里收到它。为了能够跟踪交易的起源或干扰传播,攻击者必须控制所有节点的相当大的百分比。这是P2P网络安全和隐私设计的一部分,尤其适用于区块链。
记录在链上
尽管以太坊中的所有节点都是同等的对等节点,但其中一些节点由矿工负责运营,并向矿场提供交易和区块,这些矿场是具有高性能图形处理单元(GPU)的计算机。挖掘计算机将交易添加到候选区块,并尝试找到使得候选区块有效的工作证明。我们将在[共识]中更详细地讨论这一点。
没有太多细节,有效的交易最终将被包含在一个交易区块中,并因此记录在以太坊区块链中。一旦开采成区块,交易还通过修改账户余额(在简单付款的情况下)或通过调用改变其内部状态的合约来修改以太坊singleton的状态。这些变更以交易收据的形式记录在交易旁边,交易收据也可能包括事件。我们将在[evm]中更详细地检查所有这些。
我们的交易已经完成了从创建到签署EOA、传播以及最终采矿的旅程。它改变了singleton的状态,并在区块链上留下了不可磨灭的印记。
多重签名(multisig)交易
如果您熟悉比特币的脚本功能,那么您就知道有可能创建一个比特币多币种账户,该账户只能在多方签署交易时花费资金(例如2of2 或 3of4签名)。以太坊的价值交易没有多重签名的规定,尽管可以部署任意条件的任意合约来处理ether和token的交易。
为了在多重情况下保护你的ether,将它们转移到多签合约中。无论何时您想将资金转入其他账户,所有必需的用户都需要使用常规钱包软件将交易发送至合约,从而有效授权合约执行最终交易。
这些合约还可以设计为在执行本地代码之前需要多个签名或触发其他合约。该方案的安全性最终由multisig合约代码决定。
Discussion 和 Grid +参考实现:https://blog.gridplus.io/toward-an-ethereum-multisig-standard-c566c7b7a3f6