项目中有使用到以太坊转账功能,在有一天以态坊网络堵塞,转帐报了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