这篇文章总结以太坊上的交易如何被构建和全网广播。

交易是区块链的核心。当你和以太坊交易交互时,本质上就是执行一个交易并更改它的状态。是否好奇在以太坊上交易被执行的过程发生了什么吗?让我们通过一个例子来探讨说明以太坊交易的本质,在这篇文章中我们会覆盖以下知识点:

  1. 全端剖析交易从用户浏览器或者命令行发起交易到区块链全网广播到返回用户终端的过程
  2. 理解使用钱包插件替代全数据节点时交易是如何工作的,譬如 Metamask 或 Myetherwallet 「略」
  3. 当你偏执到不信任其他第三方钱包插件时,你应该如何执行交易 「略」

默认读者具备以太坊的基础知识,譬如账户系统、gas 和合约等。如果你是一名开发者,推荐看文章 Ethereum for web developers简单的投票 Dapp 开发教程

读这篇文章的同时如果一边执行交易的话你应该理解更通透 (有能力的程序员可以搭建私有链节点进行转账),比如把 eth 发给其他普通账户或者合约账户、投票 dapp 交互等操作都是一笔交易。

交易过程

通过调用合约解释交易生命周期的全部流程。投票合约的源码在 这里,整体而言,这是一个初始化一些参与竞选的候选者,任何人都可以对候选者投票,最终投票结果永久不可篡改记录在区块链上。

Voting.deployed().then(function(instance) {
instance.voteForCandidate('Nick', {gas: 140000, from: web3.eth.accounts[0]}).then(function(r) {
  console.log("Voted successfully!")
})
})

ethereum_tx_network.png

假设在你的电脑上已经部署好以太坊客户端 (geth 或 parity) 并连接到以太坊区块链网络 (Testnet 或 Mainnet),通过合约地址和合约 ABI 就能调用合约中的函数。拿到合约对象之后调用 voteForCandidate 函数。

构建原始交易对象

voteForCandidate 函数被调用之后首次生成原始交易:

txnCount = web3.eth.getTransactionCount(web3.eth.accounts[0])
var rawTxn = {
    nonce: web3.toHex(txnCount),
    gasPrice: web3.toHex(100000000000),
    gasLimit: web3.toHex(140000),
    to: '0x633296baebc20f33ac2e1c1b105d7cd1f6a0718b',
    value: web3.toHex(0),
    data: '0xc7ed014952616d6100000000000000000000000000000000000000000000000000000000'
};

逐个解释原始交易中每个字段的意义

  • nonce: 以太坊上每个账户都有一个 nonce 字段,用以标记该账户发生交易的次数。账户中每发生一笔新交易,则 nonce 增加 1 ,与此同时区块链网络也能处理交易的执行顺序。nonce 还被用来重放保护。

    What is a replay attack? Without a replay protection, when you, say send out 1 Bitcoin from the legacy chain, the transaction is also valid on the forked chain with the same amount of new coins and same recipient. Someone else can make use of this and send out your new coins without your agreement. This is the same case for the opposite direction: when you send out the new coins, you are potentially also sending out your Bitcoin!

  • gasPrice:支付该笔交易每个单元 gas 的价格。

  • gasLimit: 支付该笔交易最大数量的 gas 。该字段防止执行交易时出现特殊情况(譬如合约出现无限循环)导致账户余额被耗完,一旦交易完成,剩余的 gas 将会返回到你的账户。

  • to:调用合约时这个字段是合约地址,普通交易时为目标用户地址。在这里是投票合约地址
  • value:转账数量。在这里我们的目的是调用投票合约,故赋值为 0
  • data:交易携带的信息,在这里有个 tip ,普通交易得到交易结果的 input data 通常为 0x, 合约交易结果该字段保护合约交易信息。通过这个字段能够区别普通交易和合约交易。

进一步解释 data 字段值的生成规则。

首先是被调用合约函数的散列之后获取前面四个字节,得到 0xcc9ab267

> web3.sha3('voteForCandidate(bytes32 candidate)')
'0xc7ed014922ff9493a686391b70ca0e8bb7e80f91c98a5cd3d285778ab2e245b3'

然后是被调用合约函数的参数值转为 32 字节,得到 52616d6100000000000000000000000000000000000000000000000000000000*

上述两次得到的组合一起就是 data 字段的值。

签名交易

web3.eth.accounts[0] 执行交易,以太坊网络需校验交易发起者是否有效,通过私钥签名就能证明你有该账户余额的使用权。

const privateKey = Buffer.from('e331b6d69882b4ab4ea581s88e0b6s4039a3de5967d88dfdcffdd2270c0fd109', 'hex')
const txn = new EthereumTx(rawTxn)
txn.sign(privateKey)
const serializedTxn = txn.serialize()

本地节点校验交易合法性

签名后的交易被提交到你搭建的节点,节点会校验被签名的交易是否真的被对应的私钥签名。

交易被全网广播

一旦构建的交易被你搭建的节点广播到区块链网络,本地节点就会返回交易 ID, 通过该散列可追踪交易状态

transactionId = sha3(serializedTxn)

ethereum_tx_signed_propogate_to_network.png

Mainnet 上的交易可在 etherscan.io 上查看详情,如果你的交易被其他节点收到,在区块浏览器上可以看到交易状态为 pending 。本地广播出去的交易并不会被所有的节点接收,这种情况发生在交易的 gas price 低于节点设置的最低 gas price 。

挖矿节点打包交易

矿工节点维护一个交易池,把收集到未被打包的交易按照 gas price 从高到低排列 (当然排列规则是可配置的),然后打包生成一个区块。交易池能够保存的交易有上限,如果网络区块拥堵会导致手续费提高和低手续费的交易迟迟不能得到打包甚至会被矿工丢弃,此时我们就要重新广播交易。还有一个技巧让被矿工从交易池丢弃的交易重新被打包:保持 nonce 不变同时提高 gas price 再重新广播,矿工收到增加手续费的交易后,新交易会覆盖被剔出交易池的交易,旧的交易将会失效。

出块并全网广播

矿工最终把我们构建的交易和其他交易一并打包生成块。以太坊协议通过设置块的 gas limit 限制块中的交易数量,块中所有交易 gas limit 的总和加起来不能超过块设置的 gas limit 。通过 ethstats.net 可查看当前块 gas limit 。

一旦矿工选择把交易打包生成区块,意味着这些交易成功被校验,此时块的状态是 pending block ,接着矿工节点开始工作量证明计算。最终只有一个挖矿节点获得区块权,并把 pending 块追加到链上。广播区块就像我们节点广播交易一样,出块的矿工把块广播到全网。

本地节点接收/同步最新区块

本地节点接收到出块矿工广播的最新块并同步,接收到新块时,本地节点执行块中的所有事务。如果你使用 truffle 执行交易,这个工具会不断地轮训链上的数据判断交易是否被确认,一旦收到确认这段代码就会被执行:

.then(function(r) {
console.log("Voted successfully!")
})

推荐阅读