diff --git a/runtime/resolved_tx.go b/runtime/resolved_tx.go index 3c3b797b0..b196f6a82 100644 --- a/runtime/resolved_tx.go +++ b/runtime/resolved_tx.go @@ -96,6 +96,7 @@ func (r *ResolvedTransaction) BuyGas(state *state.State, blockTime uint64) ( baseGasPrice *big.Int, gasPrice *big.Int, payer thor.Address, + prepaid *big.Int, returnGas func(uint64) error, err error, ) { @@ -113,19 +114,20 @@ func (r *ResolvedTransaction) BuyGas(state *state.State, blockTime uint64) ( return returnedEnergy, nil } - prepaid := new(big.Int).Mul(new(big.Int).SetUint64(r.tx.Gas()), gasPrice) + // prepaid is the max total of gas cost available to spend on this transaction + prepaid = new(big.Int).Mul(new(big.Int).SetUint64(r.tx.Gas()), gasPrice) if r.Delegator != nil { var sufficient bool if sufficient, err = energy.Sub(*r.Delegator, prepaid); err != nil { return } if sufficient { - return baseGasPrice, gasPrice, *r.Delegator, func(rgas uint64) error { + return baseGasPrice, gasPrice, *r.Delegator, prepaid, func(rgas uint64) error { _, err := doReturnGas(rgas) return err }, nil } - return nil, nil, thor.Address{}, nil, errors.New("insufficient energy") + return nil, nil, thor.Address{}, nil, nil, errors.New("insufficient energy") } commonTo := r.CommonTo() @@ -164,7 +166,7 @@ func (r *ResolvedTransaction) BuyGas(state *state.State, blockTime uint64) ( return } if ok { - return baseGasPrice, gasPrice, sponsor, doReturnGasAndSetCredit, nil + return baseGasPrice, gasPrice, sponsor, prepaid, doReturnGasAndSetCredit, nil } } // deduct from To @@ -174,7 +176,7 @@ func (r *ResolvedTransaction) BuyGas(state *state.State, blockTime uint64) ( return } if sufficient { - return baseGasPrice, gasPrice, *commonTo, doReturnGasAndSetCredit, nil + return baseGasPrice, gasPrice, *commonTo, prepaid, doReturnGasAndSetCredit, nil } } } @@ -186,9 +188,9 @@ func (r *ResolvedTransaction) BuyGas(state *state.State, blockTime uint64) ( } if sufficient { - return baseGasPrice, gasPrice, r.Origin, func(rgas uint64) error { _, err := doReturnGas(rgas); return err }, nil + return baseGasPrice, gasPrice, r.Origin, prepaid, func(rgas uint64) error { _, err := doReturnGas(rgas); return err }, nil } - return nil, nil, thor.Address{}, nil, errors.New("insufficient energy") + return nil, nil, thor.Address{}, nil, nil, errors.New("insufficient energy") } // ToContext create a tx context object. diff --git a/runtime/resolved_tx_test.go b/runtime/resolved_tx_test.go index 0588f1356..5a2a62f62 100644 --- a/runtime/resolved_tx_test.go +++ b/runtime/resolved_tx_test.go @@ -144,7 +144,7 @@ func (tr *testResolvedTransaction) TestBuyGas() { if err != nil { tr.t.Fatal(err) } - _, _, payer, returnGas, err := resolve.BuyGas(state, targetTime) + _, _, payer, _, returnGas, err := resolve.BuyGas(state, targetTime) tr.assert.Nil(err) returnGas(100) return payer diff --git a/runtime/runtime.go b/runtime/runtime.go index fac4d3096..2ed7b472e 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -382,7 +382,7 @@ func (rt *Runtime) PrepareTransaction(tx *tx.Transaction) (*TransactionExecutor, return nil, err } - baseGasPrice, gasPrice, payer, returnGas, err := resolvedTx.BuyGas(rt.state, rt.ctx.Time) + baseGasPrice, gasPrice, payer, _, returnGas, err := resolvedTx.BuyGas(rt.state, rt.ctx.Time) if err != nil { return nil, err } diff --git a/txpool/tx_object.go b/txpool/tx_object.go index 95f690625..5847c19eb 100644 --- a/txpool/tx_object.go +++ b/txpool/tx_object.go @@ -23,10 +23,13 @@ type txObject struct { *tx.Transaction resolved *runtime.ResolvedTransaction - timeAdded int64 - executable bool + timeAdded int64 + localSubmitted bool // tx is submitted locally on this node, or synced remotely from p2p. + payer *thor.Address // payer of the tx, either origin, delegator, or on-chain delegation payer + cost *big.Int // total tx cost the payer needs to pay before execution(gas price * gas) + + executable bool // don't touch this value, will be updated by the pool overallGasPrice *big.Int // don't touch this value, it's only be used in pool's housekeeping - localSubmitted bool // tx is submitted locally on this node, or synced remotely from p2p. } func resolveTx(tx *tx.Transaction, localSubmitted bool) (*txObject, error) { @@ -51,6 +54,14 @@ func (o *txObject) Delegator() *thor.Address { return o.resolved.Delegator } +func (o *txObject) Cost() *big.Int { + return o.cost +} + +func (o *txObject) Payer() *thor.Address { + return o.payer +} + func (o *txObject) Executable(chain *chain.Chain, state *state.State, headBlock *block.Header) (bool, error) { switch { case o.Gas() > headBlock.GasLimit(): @@ -86,9 +97,18 @@ func (o *txObject) Executable(chain *chain.Chain, state *state.State, headBlock return false, nil } - if _, _, _, _, err := o.resolved.BuyGas(state, headBlock.Timestamp()+thor.BlockInterval); err != nil { + checkpoint := state.NewCheckpoint() + defer state.RevertTo(checkpoint) + + _, _, payer, prepaid, _, err := o.resolved.BuyGas(state, headBlock.Timestamp()+thor.BlockInterval) + if err != nil { return false, err } + + if !o.executable { + o.payer = &payer + o.cost = prepaid + } return true, nil } diff --git a/txpool/tx_object_map.go b/txpool/tx_object_map.go index 2828fd972..4a5ecc4ce 100644 --- a/txpool/tx_object_map.go +++ b/txpool/tx_object_map.go @@ -7,18 +7,20 @@ package txpool import ( "errors" + "math/big" "sync" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/tx" ) -// txObjectMap to maintain mapping of tx hash to tx object, and account quota. +// txObjectMap to maintain mapping of tx hash to tx object, account quota and pending cost. type txObjectMap struct { lock sync.RWMutex mapByHash map[thor.Bytes32]*txObject mapByID map[thor.Bytes32]*txObject quota map[thor.Address]int + cost map[thor.Address]*big.Int } func newTxObjectMap() *txObjectMap { @@ -26,6 +28,7 @@ func newTxObjectMap() *txObjectMap { mapByHash: make(map[thor.Bytes32]*txObject), mapByID: make(map[thor.Bytes32]*txObject), quota: make(map[thor.Address]int), + cost: make(map[thor.Address]*big.Int), } } @@ -36,7 +39,7 @@ func (m *txObjectMap) ContainsHash(txHash thor.Bytes32) bool { return found } -func (m *txObjectMap) Add(txObj *txObject, limitPerAccount int) error { +func (m *txObjectMap) Add(txObj *txObject, limitPerAccount int, validatePayer func(payer thor.Address, needs *big.Int) error) error { m.lock.Lock() defer m.lock.Unlock() @@ -49,22 +52,50 @@ func (m *txObjectMap) Add(txObj *txObject, limitPerAccount int) error { return errors.New("account quota exceeded") } - if d := txObj.Delegator(); d != nil { - if m.quota[*d] >= limitPerAccount { + delegator := txObj.Delegator() + if delegator != nil { + if m.quota[*delegator] >= limitPerAccount { return errors.New("delegator quota exceeded") } - m.quota[*d]++ + } + + var ( + cost *big.Int + payer thor.Address + ) + + if txObj.Cost() != nil { + payer = *txObj.Payer() + pending := m.cost[payer] + + if pending == nil { + cost = new(big.Int).Set(txObj.Cost()) + } else { + cost = new(big.Int).Add(pending, txObj.Cost()) + } + + if err := validatePayer(payer, cost); err != nil { + return err + } } m.quota[txObj.Origin()]++ + if delegator != nil { + m.quota[*delegator]++ + } + + if cost != nil { + m.cost[payer] = cost + } + m.mapByHash[hash] = txObj m.mapByID[txObj.ID()] = txObj return nil } func (m *txObjectMap) GetByID(id thor.Bytes32) *txObject { - m.lock.Lock() - defer m.lock.Unlock() + m.lock.RLock() + defer m.lock.RUnlock() return m.mapByID[id] } @@ -79,11 +110,22 @@ func (m *txObjectMap) RemoveByHash(txHash thor.Bytes32) bool { delete(m.quota, txObj.Origin()) } - if d := txObj.Delegator(); d != nil { - if m.quota[*d] > 1 { - m.quota[*d]-- + if delegator := txObj.Delegator(); delegator != nil { + if m.quota[*delegator] > 1 { + m.quota[*delegator]-- } else { - delete(m.quota, *d) + delete(m.quota, *delegator) + } + } + + // update the pending cost of payers + if payer := txObj.Payer(); payer != nil { + if pending := m.cost[*payer]; pending != nil { + if pending.Cmp(txObj.Cost()) <= 0 { + delete(m.cost, *payer) + } else { + m.cost[*payer] = new(big.Int).Sub(pending, txObj.Cost()) + } } } @@ -94,6 +136,17 @@ func (m *txObjectMap) RemoveByHash(txHash thor.Bytes32) bool { return false } +func (m *txObjectMap) UpdatePendingCost(txObj *txObject) { + m.lock.Lock() + defer m.lock.Unlock() + + if pending := m.cost[*txObj.Payer()]; pending != nil { + m.cost[*txObj.Payer()] = new(big.Int).Add(pending, txObj.Cost()) + } else { + m.cost[*txObj.Payer()] = new(big.Int).Set(txObj.Cost()) + } +} + func (m *txObjectMap) ToTxObjects() []*txObject { m.lock.RLock() defer m.lock.RUnlock() @@ -125,11 +178,12 @@ func (m *txObjectMap) Fill(txObjs []*txObject) { } // skip account limit check m.quota[txObj.Origin()]++ - if d := txObj.Delegator(); d != nil { - m.quota[*d]++ + if delegator := txObj.Delegator(); delegator != nil { + m.quota[*delegator]++ } m.mapByHash[txObj.Hash()] = txObj m.mapByID[txObj.ID()] = txObj + // skip cost check and accumulation } } diff --git a/txpool/tx_object_map_test.go b/txpool/tx_object_map_test.go index 52c4deb5a..9a0b38629 100644 --- a/txpool/tx_object_map_test.go +++ b/txpool/tx_object_map_test.go @@ -7,11 +7,13 @@ package txpool import ( "errors" + "math/big" "testing" "github.com/stretchr/testify/assert" "github.com/vechain/thor/v2/genesis" "github.com/vechain/thor/v2/muxdb" + "github.com/vechain/thor/v2/state" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/tx" ) @@ -30,8 +32,8 @@ func TestGetByID(t *testing.T) { // Creating a new txObjectMap and adding transactions m := newTxObjectMap() - assert.Nil(t, m.Add(txObj1, 1)) - assert.Nil(t, m.Add(txObj2, 1)) + assert.Nil(t, m.Add(txObj1, 1, func(_ thor.Address, _ *big.Int) error { return nil })) + assert.Nil(t, m.Add(txObj2, 1, func(_ thor.Address, _ *big.Int) error { return nil })) // Testing GetByID retrievedTxObj1 := m.GetByID(txObj1.ID()) @@ -52,7 +54,7 @@ func TestFill(t *testing.T) { // Creating transactions tx1 := newTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[0]) - tx2 := newTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[1]) + tx2 := newDelegatedTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, genesis.DevAccounts()[1], genesis.DevAccounts()[2]) // Resolving transactions into txObjects txObj1, _ := resolveTx(tx1, false) @@ -62,7 +64,7 @@ func TestFill(t *testing.T) { m := newTxObjectMap() // Filling the map with transactions - m.Fill([]*txObject{txObj1, txObj2}) + m.Fill([]*txObject{txObj1, txObj2, txObj1}) // Asserting the length of the map assert.Equal(t, 2, m.Len(), "Map should contain only 2 unique transactions") @@ -74,6 +76,10 @@ func TestFill(t *testing.T) { // Asserting duplicate handling assert.Equal(t, m.GetByID(txObj1.ID()), txObj1, "Duplicate tx1 should not be added again") assert.Equal(t, m.GetByID(txObj2.ID()), txObj2, "txObj2 should be retrievable by ID") + + assert.Equal(t, 1, m.quota[genesis.DevAccounts()[0].Address], "Account quota should be 1 for account 0") + assert.Equal(t, 1, m.quota[genesis.DevAccounts()[1].Address], "Account quota should be 1 for account 1") + assert.Equal(t, 1, m.quota[genesis.DevAccounts()[2].Address], "Delegator quota should be 1 for account 2") } func TestTxObjMap(t *testing.T) { @@ -91,14 +97,14 @@ func TestTxObjMap(t *testing.T) { m := newTxObjectMap() assert.Zero(t, m.Len()) - assert.Nil(t, m.Add(txObj1, 1)) - assert.Nil(t, m.Add(txObj1, 1), "should no error if exists") + assert.Nil(t, m.Add(txObj1, 1, func(_ thor.Address, _ *big.Int) error { return nil })) + assert.Nil(t, m.Add(txObj1, 1, func(_ thor.Address, _ *big.Int) error { return nil }), "should no error if exists") assert.Equal(t, 1, m.Len()) - assert.Equal(t, errors.New("account quota exceeded"), m.Add(txObj2, 1)) + assert.Equal(t, errors.New("account quota exceeded"), m.Add(txObj2, 1, func(_ thor.Address, _ *big.Int) error { return nil })) assert.Equal(t, 1, m.Len()) - assert.Nil(t, m.Add(txObj3, 1)) + assert.Nil(t, m.Add(txObj3, 1, func(_ thor.Address, _ *big.Int) error { return nil })) assert.Equal(t, 2, m.Len()) assert.True(t, m.ContainsHash(tx1.Hash())) @@ -126,11 +132,65 @@ func TestLimitByDelegator(t *testing.T) { txObj3, _ := resolveTx(tx3, false) m := newTxObjectMap() - assert.Nil(t, m.Add(txObj1, 1)) - assert.Nil(t, m.Add(txObj3, 1)) + assert.Nil(t, m.Add(txObj1, 1, func(_ thor.Address, _ *big.Int) error { return nil })) + assert.Nil(t, m.Add(txObj3, 1, func(_ thor.Address, _ *big.Int) error { return nil })) m = newTxObjectMap() - assert.Nil(t, m.Add(txObj2, 1)) - assert.Equal(t, errors.New("delegator quota exceeded"), m.Add(txObj3, 1)) - assert.Equal(t, errors.New("account quota exceeded"), m.Add(txObj1, 1)) + assert.Nil(t, m.Add(txObj2, 1, func(_ thor.Address, _ *big.Int) error { return nil })) + assert.Equal(t, errors.New("delegator quota exceeded"), m.Add(txObj3, 1, func(_ thor.Address, _ *big.Int) error { return nil })) + assert.Equal(t, errors.New("account quota exceeded"), m.Add(txObj1, 1, func(_ thor.Address, _ *big.Int) error { return nil })) +} + +func TestPendingCost(t *testing.T) { + db := muxdb.NewMem() + repo := newChainRepo(db) + stater := state.NewStater(db) + + // Creating transactions + tx1 := newTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[0]) + tx2 := newDelegatedTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, genesis.DevAccounts()[1], genesis.DevAccounts()[2]) + tx3 := newDelegatedTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, genesis.DevAccounts()[1], genesis.DevAccounts()[2]) + + // Resolving transactions into txObjects + txObj1, _ := resolveTx(tx1, false) + txObj2, _ := resolveTx(tx2, false) + txObj3, _ := resolveTx(tx3, false) + + chain := repo.NewBestChain() + best := repo.BestBlockSummary() + state := stater.NewState(best.Header.StateRoot(), best.Header.Number(), best.Conflicts, best.SteadyNum) + + var err error + txObj1.executable, err = txObj1.Executable(chain, state, best.Header) + assert.Nil(t, err) + assert.True(t, txObj1.executable) + + txObj2.executable, err = txObj2.Executable(chain, state, best.Header) + assert.Nil(t, err) + assert.True(t, txObj2.executable) + + txObj3.executable, err = txObj3.Executable(chain, state, best.Header) + assert.Nil(t, err) + assert.True(t, txObj3.executable) + + // Creating a new txObjectMap + m := newTxObjectMap() + + m.Add(txObj1, 10, func(_ thor.Address, _ *big.Int) error { return nil }) + m.Add(txObj2, 10, func(_ thor.Address, _ *big.Int) error { return nil }) + m.Add(txObj3, 10, func(_ thor.Address, _ *big.Int) error { return nil }) + + assert.Equal(t, txObj1.Cost(), m.cost[genesis.DevAccounts()[0].Address]) + // No cost for txObj2's origin, should be counted on the delegator + assert.Nil(t, m.cost[genesis.DevAccounts()[1].Address]) + assert.Equal(t, new(big.Int).Add(txObj2.Cost(), txObj3.Cost()), m.cost[genesis.DevAccounts()[2].Address]) + + m.RemoveByHash(txObj1.Hash()) + assert.Nil(t, m.cost[genesis.DevAccounts()[0].Address]) + m.RemoveByHash(txObj2.Hash()) + assert.Equal(t, txObj3.Cost(), m.cost[genesis.DevAccounts()[2].Address]) + m.RemoveByHash(txObj2.Hash()) + assert.Equal(t, txObj3.Cost(), m.cost[genesis.DevAccounts()[2].Address]) + m.RemoveByHash(txObj3.Hash()) + assert.Nil(t, m.cost[genesis.DevAccounts()[2].Address]) } diff --git a/txpool/tx_pool.go b/txpool/tx_pool.go index 6798ae30e..eb3409624 100644 --- a/txpool/tx_pool.go +++ b/txpool/tx_pool.go @@ -7,6 +7,7 @@ package txpool import ( "context" + "math/big" "math/rand" "os" "sync/atomic" @@ -15,6 +16,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/event" + "github.com/pkg/errors" "github.com/vechain/thor/v2/builtin" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/co" @@ -254,7 +256,19 @@ func (p *TxPool) add(newTx *tx.Transaction, rejectNonExecutable bool, localSubmi } txObj.executable = executable - if err := p.all.Add(txObj, p.options.LimitPerAccount); err != nil { + if err := p.all.Add(txObj, p.options.LimitPerAccount, func(payer thor.Address, needs *big.Int) error { + // check payer's balance + balance, err := state.GetEnergy(payer, headSummary.Header.Timestamp()+thor.BlockInterval) + if err != nil { + return err + } + + if balance.Cmp(needs) < 0 { + return errors.New("insufficient energy for overall pending cost") + } + + return nil + }); err != nil { return txRejectedError{err.Error()} } @@ -269,7 +283,8 @@ func (p *TxPool) add(newTx *tx.Transaction, rejectNonExecutable bool, localSubmi return txRejectedError{"pool is full"} } - if err := p.all.Add(txObj, p.options.LimitPerAccount); err != nil { + // skip pending cost check when chain is not synced + if err := p.all.Add(txObj, p.options.LimitPerAccount, func(_ thor.Address, _ *big.Int) error { return nil }); err != nil { return txRejectedError{err.Error()} } logger.Debug("tx added", "id", newTx.ID()) @@ -351,6 +366,7 @@ func (p *TxPool) Dump() tx.Transactions { func (p *TxPool) wash(headSummary *chain.BlockSummary) (executables tx.Transactions, removed int, err error) { all := p.all.ToTxObjects() var toRemove []*txObject + var toUpdateCost []*txObject defer func() { if err != nil { // in case of error, simply cut pool size to limit @@ -367,6 +383,10 @@ func (p *TxPool) wash(headSummary *chain.BlockSummary) (executables tx.Transacti } removed = len(toRemove) } + // update pending cost + for _, txObj := range toUpdateCost { + p.all.UpdatePendingCost(txObj) + } }() // recreate state every time to avoid high RAM usage when the pool at hight water-mark. @@ -460,8 +480,13 @@ func (p *TxPool) wash(headSummary *chain.BlockSummary) (executables tx.Transacti for _, obj := range executableObjs { executables = append(executables, obj.Transaction) - if !obj.executable || obj.localSubmitted { + // the tx is not executable previously + if !obj.executable { obj.executable = true + toUpdateCost = append(toUpdateCost, obj) + toBroadcast = append(toBroadcast, obj.Transaction) + } else if obj.localSubmitted { + // broadcast local submitted even it's already executable toBroadcast = append(toBroadcast, obj.Transaction) } } diff --git a/txpool/tx_pool_test.go b/txpool/tx_pool_test.go index e843a1b7c..73fe7db0a 100644 --- a/txpool/tx_pool_test.go +++ b/txpool/tx_pool_test.go @@ -20,6 +20,7 @@ import ( "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/assert" "github.com/vechain/thor/v2/block" + "github.com/vechain/thor/v2/builtin" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/genesis" "github.com/vechain/thor/v2/muxdb" @@ -278,11 +279,11 @@ func TestWashTxs(t *testing.T) { tx2 := newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), devAccounts[1]) txObj2, _ := resolveTx(tx2, false) - assert.Nil(t, pool.all.Add(txObj2, LIMIT_PER_ACCOUNT)) // this tx will participate in the wash out. + assert.Nil(t, pool.all.Add(txObj2, LIMIT_PER_ACCOUNT, func(_ thor.Address, _ *big.Int) error { return nil })) // this tx will participate in the wash out. tx3 := newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), devAccounts[2]) txObj3, _ := resolveTx(tx3, false) - assert.Nil(t, pool.all.Add(txObj3, LIMIT_PER_ACCOUNT)) // this tx will participate in the wash out. + assert.Nil(t, pool.all.Add(txObj3, LIMIT_PER_ACCOUNT, func(_ thor.Address, _ *big.Int) error { return nil })) // this tx will participate in the wash out. txs, removedCount, err := pool.wash(pool.repo.BestBlockSummary()) assert.Nil(t, err) @@ -455,7 +456,7 @@ func TestBlocked(t *testing.T) { // added into all, will be washed out txObj, err := resolveTx(trx, false) assert.Nil(t, err) - pool.all.Add(txObj, LIMIT_PER_ACCOUNT) + pool.all.Add(txObj, LIMIT_PER_ACCOUNT, func(_ thor.Address, _ *big.Int) error { return nil }) pool.wash(pool.repo.BestBlockSummary()) got := pool.Get(trx.ID()) @@ -499,7 +500,7 @@ func TestWash(t *testing.T) { txObj, err := resolveTx(trx, false) assert.Nil(t, err) - pool.all.Add(txObj, LIMIT_PER_ACCOUNT) + pool.all.Add(txObj, LIMIT_PER_ACCOUNT, func(_ thor.Address, _ *big.Int) error { return nil }) pool.wash(pool.repo.BestBlockSummary()) got := pool.Get(trx.ID()) @@ -526,11 +527,11 @@ func TestWash(t *testing.T) { txObj, err := resolveTx(trx2, false) assert.Nil(t, err) - pool.all.Add(txObj, LIMIT_PER_ACCOUNT) + pool.all.Add(txObj, LIMIT_PER_ACCOUNT, func(_ thor.Address, _ *big.Int) error { return nil }) txObj, err = resolveTx(trx3, false) assert.Nil(t, err) - pool.all.Add(txObj, LIMIT_PER_ACCOUNT) + pool.all.Add(txObj, LIMIT_PER_ACCOUNT, func(_ thor.Address, _ *big.Int) error { return nil }) pool.wash(pool.repo.BestBlockSummary()) got := pool.Get(trx3.ID()) @@ -557,11 +558,11 @@ func TestWash(t *testing.T) { txObj, err := resolveTx(trx2, false) assert.Nil(t, err) - pool.all.Add(txObj, LIMIT_PER_ACCOUNT) + pool.all.Add(txObj, LIMIT_PER_ACCOUNT, func(_ thor.Address, _ *big.Int) error { return nil }) txObj, err = resolveTx(trx3, false) assert.Nil(t, err) - pool.all.Add(txObj, LIMIT_PER_ACCOUNT) + pool.all.Add(txObj, LIMIT_PER_ACCOUNT, func(_ thor.Address, _ *big.Int) error { return nil }) pool.wash(pool.repo.BestBlockSummary()) // all non executable should be washed out @@ -577,3 +578,86 @@ func TestWash(t *testing.T) { t.Run(tt.name, tt.testFunc) } } + +func TestAddOverPendingCost(t *testing.T) { + now := uint64(time.Now().Unix() - time.Now().Unix()%10 - 10) + db := muxdb.NewMem() + builder := new(genesis.Builder). + GasLimit(thor.InitialGasLimit). + Timestamp(now). + State(func(state *state.State) error { + if err := state.SetCode(builtin.Params.Address, builtin.Params.RuntimeBytecodes()); err != nil { + return err + } + if err := state.SetCode(builtin.Prototype.Address, builtin.Prototype.RuntimeBytecodes()); err != nil { + return err + } + bal, _ := new(big.Int).SetString("42000000000000000000", 10) + for _, acc := range devAccounts { + state.SetEnergy(acc.Address, bal, now) + } + return nil + }) + + method, found := builtin.Params.ABI.MethodByName("set") + assert.True(t, found) + + var executor thor.Address + data, err := method.EncodeInput(thor.KeyExecutorAddress, new(big.Int).SetBytes(executor[:])) + assert.Nil(t, err) + builder.Call(tx.NewClause(&builtin.Params.Address).WithData(data), thor.Address{}) + + data, err = method.EncodeInput(thor.KeyBaseGasPrice, thor.InitialBaseGasPrice) + assert.Nil(t, err) + builder.Call(tx.NewClause(&builtin.Params.Address).WithData(data), executor) + + b0, _, _, err := builder.Build(state.NewStater(db)) + assert.Nil(t, err) + + st := state.New(db, b0.Header().StateRoot(), 0, 0, 0) + stage, err := st.Stage(1, 0) + assert.Nil(t, err) + root, err := stage.Commit() + assert.Nil(t, err) + + var feat tx.Features + feat.SetDelegated(true) + b1 := new(block.Builder). + ParentID(b0.Header().ID()). + StateRoot(root). + TotalScore(100). + Timestamp(now + 10). + GasLimit(thor.InitialGasLimit). + TransactionFeatures(feat).Build() + + repo, _ := chain.NewRepository(db, b0) + repo.AddBlock(b1, tx.Receipts{}, 0) + repo.SetBestBlockID(b1.Header().ID()) + pool := New(repo, state.NewStater(db), Options{ + Limit: LIMIT, + LimitPerAccount: LIMIT, + MaxLifetime: time.Hour, + }) + defer pool.Close() + + // first and second tx should be fine + err = pool.Add(newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), devAccounts[0])) + assert.Nil(t, err) + err = pool.Add(newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), devAccounts[0])) + assert.Nil(t, err) + // third tx should be rejected due to insufficient energy + err = pool.Add(newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), devAccounts[0])) + assert.EqualError(t, err, "tx rejected: insufficient energy for overall pending cost") + // delegated fee should also be counted + err = pool.Add(newDelegatedTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, devAccounts[9], devAccounts[0])) + assert.EqualError(t, err, "tx rejected: insufficient energy for overall pending cost") + + // first and second tx should be fine + err = pool.Add(newDelegatedTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, devAccounts[1], devAccounts[2])) + assert.Nil(t, err) + err = pool.Add(newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), devAccounts[2])) + assert.Nil(t, err) + // delegated fee should also be counted + err = pool.Add(newDelegatedTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, devAccounts[8], devAccounts[2])) + assert.EqualError(t, err, "tx rejected: insufficient energy for overall pending cost") +}