項目中有使用到以太坊轉賬功能,在有一天以態坊網絡堵塞,轉帳報了replacement transaction underpriced異常,根據這個異常關鍵詞搜索以態坊源碼,發現是這裏報錯的。
func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err error) {
//---------省去前面的代碼------------------------
from, _ := types.Sender(pool.signer, tx) // already validated
if list := pool.pending[from]; list != nil && list.Overlaps(tx) {
// Nonce already pending, check if required price bump is met
inserted, old := list.Add(tx, pool.config.PriceBump)
if !inserted {
pendingDiscardMeter.Mark(1)
return false, ErrReplaceUnderpriced //這裏拋異常了
}
// New transaction is better, replace old one
if old != nil {
pool.all.Remove(old.Hash())
pool.priced.Removed(1)
pendingReplaceMeter.Mark(1)
}
//------------省略後面的代碼-------------------
}
}
func (l *txList) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transaction) {
// If there's an older better transaction, abort
old := l.txs.Get(tx.Nonce())
if old != nil {
threshold := new(big.Int).Div(new(big.Int).Mul(old.GasPrice(), big.NewInt(100+int64(priceBump))), big.NewInt(100))
// Have to ensure that the new gas price is higher than the old gas
// price as well as checking the percentage threshold to ensure that
// this is accurate for low (Wei-level) gas price replacements
//pending隊列已經有相同nonce值的交易,且新舊兩條交易的nonce相同或者新交易的值小於threshold值
if old.GasPrice().Cmp(tx.GasPrice()) >= 0 || threshold.Cmp(tx.GasPrice()) > 0 {
return false, nil
}
}
// Otherwise overwrite the old transaction with the current one
l.txs.Put(tx)
if cost := tx.Cost(); l.costcap.Cmp(cost) < 0 {
l.costcap = cost
}
if gas := tx.Gas(); l.gascap < gas {
l.gascap = gas
}
return true, old
}
pending隊列已經有相同nonce值的交易,且新舊兩條交易的nonce相同或者新交易的值小於threshold值,這是報錯的原因,但由於gas的費用是通過自動計算的,所以原因還是在nonce值上,再看下發送交易的代碼,是沒有傳nonce值進去,結點收到交易消息後,判斷如果傳入參數nonce值爲空就從已有pending隊列裏獲取相同的nonce,nonce值只有等到區塊打包時,把交易消息持久化到stateDB時纔有+1變化。
//以下是獲取默認nonce的代碼
// setDefaults is a helper function that fills in default values for unspecified tx fields.
func (args *SendTxArgs) setDefaults(ctx context.Context, b Backend) error {
//省去前面代碼
if args.Nonce == nil {
nonce, err := b.GetPoolNonce(ctx, args.From)
if err != nil {
return err
}
args.Nonce = (*hexutil.Uint64)(&nonce)
}
//省去後面代碼
}
//以下是執行交易時打包入塊的代碼
// TransitionDb will transition the state by applying the current message and
// returning the result including the used gas. It returns an error if failed.
// An error indicates a consensus issue.
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
//省略前面的代碼
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
// Increment the nonce for the next transaction
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1) //這裏才+1
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
//省略後面的代碼
}
要解決這個問題有兩種方式:
1、 調用方維護nonce值,自己每次創建交易的nonce值都變化並且傳入交易參數
2、 在上條交易費用的基礎上再加多點gas