以太坊C-源码解析(十)以太坊交易中的nonce

以太坊交易中存在一个特殊的值nonce,此nonce并非计算block难度的nonce,此nonce仅仅表示发送账号发送交易的次数,从0开始,每发送一次交易+1,那么第一次发送nonce为0,第二次为1,以此类推。
nonce的存在可以用来防止重放攻击,也就是同一个交易只能被发送一次,下次发送同一个交易时,因为nonce值和最新的nonce不同,会被区块链拒绝。
我们来从代码层面看看这个nonce的生成和检测。

我们可以从一张图来看这个nonce的来龙去脉。
nonce
可以看到这张图上存在一个关键性的三角关系。

  • 三角形上面顶点是区块链blockchain
  • 左边顶点是postseal()中的noncepostseal()是从区块链获取到的最新的区块,那么左边顶点表示当前区块链中最新区块该发送账号的nonce
  • 右边顶点是交易队列中的nonce
    当我们提交一个交易时,交易的nonce取值是左右两个顶点的nonce值中取最大值。然后再与blockchain中最新块的nonce进行比较,如果不同,则区块链拒绝此交易。那么问题来了,postseal()中不就是最新块的nonce吗?为什么还需要再次比较?这是因为postseal()并不是一直与区块链同步的,只有满足某些条件才会同步。另外当交易成功提交后,该交易在正式被区块链确认前,是被存放在交易队列中的,此时右顶点的nonce值为该交易的nonce+1。

我们来一步一步拆开来看:
第一步,假设该发送者从来没有发送过交易,那么他的nonce值应该为0,上面的图会变成:
第一步
此时blockchainpostseal()中均没有该账号的信息,nonce值为初始值0。 交易队列中没有该发送账号的交易,因此nonce值也是0,那么该发送者第一次提交交易时,nonce值会被设置为max(0, 0),也就是0。然后再比较0与0是相等的,那么此交易被正确发送。

第二步,交易被发送到交易队列,这张图变成:
第二步
此时上顶点和左顶点的nonce值不变,右顶点因为交易队列中已有一个该发送者发送的交易,那么nonce+1,变成1。
此时如果该发送者想再发一个交易,那么新交易的nonce会被设置为max(0, 1),也就是1。然后再与上顶点nonce比较,得出不相等的结论,此交易被拒绝,提交失败!

第三步,第一个交易被区块链确认,这张图变成:
第三步
交易被确认后,上顶点blockchainnonce变成1,左顶点因为同步,nonce也变成1,而右顶点交易队列会删除掉已确认的交易,所以没有该发送者的交易了,nonce就为0。此时如果该发送者再发一个交易,新交易的nonce会被设置为max(1, 0),也就是1。然后再与上顶点nonce比较,结果相等,该交易被成功发出!

交易被发出后被发到交易队列,流程就同第二步了。

下面我们来看看具体涉及到的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TransactionSkeleton Client::populateTransactionWithDefaults(TransactionSkeleton const& _t) const
{
TransactionSkeleton ret(_t);

// Default gas value meets the intrinsic gas requirements of both
// send value and create contract transactions and is the same default
// value used by geth and testrpc.
const u256 defaultTransactionGas = 90000;
if (ret.nonce == Invalid256)
ret.nonce = max<u256>(postSeal().transactionsFrom(ret.from), m_tq.maxNonce(ret.from));
if (ret.gasPrice == Invalid256)
ret.gasPrice = gasBidPrice();
if (ret.gas == Invalid256)
ret.gas = defaultTransactionGas;

return ret;
}

这段代码是提交交易时设置交易的一些参数,其中

1
ret.nonce = max<u256>(postSeal().transactionsFrom(ret.from), m_tq.maxNonce(ret.from));

就是设置交易的nonce,是左顶点和右顶点的nonce值的最大值。

再来看交易队列中的nonce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
u256 TransactionQueue::maxNonce(Address const& _a) const
{
ReadGuard l(m_lock);
return maxNonce_WITH_LOCK(_a);
}

u256 TransactionQueue::maxNonce_WITH_LOCK(Address const& _a) const
{
u256 ret = 0;
auto cs = m_currentByAddressAndNonce.find(_a);
if (cs != m_currentByAddressAndNonce.end() && !cs->second.empty())
ret = cs->second.rbegin()->first + 1;
auto fs = m_future.find(_a);
if (fs != m_future.end() && !fs->second.empty())
ret = std::max(ret, fs->second.rbegin()->first + 1);
return ret;
}

注意那个+1,这个表示当前队列中该发送者最大的nonce再+1。

postseal()从区块链同步的代码在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void Client::restartMining()
{
bool preChanged = false;
Block newPreMine(chainParams().accountStartNonce);
DEV_READ_GUARDED(x_preSeal)
newPreMine = m_preSeal;

// TODO: use m_postSeal to avoid re-evaluating our own blocks.
preChanged = newPreMine.sync(bc());

if (preChanged || m_postSeal.author() != m_preSeal.author())
{
DEV_WRITE_GUARDED(x_preSeal)
m_preSeal = newPreMine;
DEV_WRITE_GUARDED(x_working)
m_working = newPreMine;
/// ...
DEV_READ_GUARDED(x_working) DEV_WRITE_GUARDED(x_postSeal)
m_postSeal = m_working;

/// ...
}

/// ...
}

省略了一些次要代码,从这里可以看出,先是从区块链同步最新信息到newPreMine,然后拷贝到m_postSeal,也就是postseal()。 那么Client::restartMining()是从哪里调用的呢?
有两个地方,但是一般都是从Client::resyncStateFromChain()这里调用的:

1
2
3
4
5
6
void Client::resyncStateFromChain()
{
/// ...

restartMining();
}

Client::resyncStateFromChain()函数又是从哪里调用呢?
也有两个地方,一处是Client::onChainChanged()表示区块链同步新块的时候,另一处是Client::syncTransactionQueue()表示同步交易队列中交易的时候。
由此可见,以太坊通过这些机制保证nonce与发送账号的发送次数严格对应,如果发送交易不遵守这条规则,则发送可能会失败,错误码为InvalidNonce

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×