以太坊中的nonce有两个意义:
1.工作量证明:为了证明工作量的无意义的值,这是采矿的本质,这个值将决定采矿的难度,
2.账户的随机数:在一个账户中的防止多重交易的用途。例如一个交易从A到B 20个币,可能从A到B发送多次。
为了防止交易的重播攻击,每笔交易必须有一个nonce随机数,针对每一个账户nonce都是从0开始,当nonce为0的交易处理完之后,才会处理nonce为1的交易,并依次加1的交易才会被处理。以下是nonce使用的几条规则:
以太坊在geth的console端使用eth.sendTransaction({from:addr1,to:addr2,value:money})的形式来转账ether的时候,会通过RPC调用到/ethapi/api.go中的函数SendTransaction():
// SendTransaction will create a transaction from the given arguments and
// tries to sign it with the key associated with args.To. If the given passwd isn't
// able to decrypt the key it fails.
func (s *PrivateAccountAPI) SendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) {
if args.Nonce == nil {
// Hold the addresse's mutex around signing to prevent concurrent assignment of
// the same nonce to multiple accounts.
s.nonceLock.LockAddr(args.From)
defer s.nonceLock.UnlockAddr(args.From)
}
signed, err := s.signTransaction(ctx, args, passwd)
if err != nil {
return common.Hash{}, err
}
return submitTransaction(ctx, s.b, signed)
}
sendTransaction()继续调用submitTransaction()函数来执行交易:
// submitTransaction is a helper function that submits tx to txPool and logs a message.
func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) {
if err := b.SendTx(ctx, tx); err != nil {
return common.Hash{}, err
}
if tx.To() == nil {
signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number())
from, err := types.Sender(signer, tx)
if err != nil {
return common.Hash{}, err
}
addr := crypto.CreateAddress(from, tx.Nonce())
log.Info("Submitted contract creation", "fullhash", tx.Hash().Hex(), "contract", addr.Hex())
} else {
log.Info("Submitted transaction", "fullhash", tx.Hash().Hex(), "recipient", tx.To())
}
return tx.Hash(), nil
}
submitTransaction()在sendTx()函数中发送交易。然后在判断交易地址是否为空,为空的则是创建合约的交易。
追踪sendTx()函数到/core/tx_pool.go中的addTx()函数,原来是通过addTx()将交易添加到交易池。
func (pool *TxPool) addTx(tx *types.Transaction, local bool) error {
pool.mu.Lock()
defer pool.mu.Unlock()
// Try to inject the transaction and update any state
replace, err := pool.add(tx, local)
if err != nil {
return err
}
// If we added a new transaction, run promotion checks and return
if !replace {
from, _ := types.Sender(pool.signer, tx) // already validated
pool.promoteExecutables([]common.Address{from})
}
return nil
}
addTx()首先调用add()将交易添加到交易池pool,如果成功,则调用promoteExecutables()函数去执行交易。promoteExecutables()函数关键代码是:
// Gather all executable transactions and promote them
for _, tx := range list.Ready(pool.pendingState.GetNonce(addr)) {
hash := tx.Hash()
log.Trace("Promoting queued transaction", "hash", hash)
pool.promoteTx(addr, hash, tx)
log.Info("lzj log:","hash",hash,"Nonce",pool.pendingState.GetNonce(addr))
}
这段代码收集所有可执行交易并且执行它们。pool.pendingState.GetNonce(addr)得到和地址addr相关的nonce范围N,N等于len(addr.nonces)) + addr.nstart,其实等于该地址已经发送成功的交易个数。list.Ready()函数将返回一个交易集合,这个集合中的所有交易都要求满足nonce不大于N。只有Nonce满足要求的交易才会被处理,就是通过pool.promoteTx(addr, hash, tx)执行交易。
3 Web3j等RPC方式设置Nonce的正确方式
一般的通过Web3j发起以太坊交易的方法是:
Transaction.createEtherTransaction(from,nonce,gasPrice,gasLimit,to,value);
其中的nonce得到方式是:
EthGetTransactionCount ethGetTransactionCount = ethClient.ethGetTransactionCount(
from, DefaultBlockParameterName.LATEST).sendAsync().get();
BigInteger nonce = ethGetTransactionCount.getTransactionCount();
通过取得当前该账户发起过的交易次数,并设其为当前交易的Nonce值。
如果要在for循环里同时发起多笔交易,因为当前交易尚没有被处理成功,所以通过上述方法得到的Nonce将会与上一次交易的Nonce相同,如果这笔交易的Gas比上次交易大,则会替换上次交易,否则将会报如下错误:
transaction failed,info:replacement transaction underpriced
解决这个问题的方法是自己维护Nonce,同一个账户连续发送俩笔交易时,通过递增Nonce来防止Nonce重复。
此种方案也有限制条件。第一,由于nonce统一进行维护,那么这个地址必须是内部地址,而且发起交易必须通过统一维护的nonce作为出口,否则在其他地方发起交易,原有维护的nonce将会出现混乱。第二,一旦已经发出的交易发生异常,异常交易的nonce未被使用,那么异常交易的nonce需要重新被使用之后它后面的nonce才会生效。