mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-24 19:25:21 +00:00
f3c1283ac6
They're a part of the regular protocol now. Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru> Signed-off-by: Roman Khimov <roman@nspcc.ru>
823 lines
28 KiB
Go
823 lines
28 KiB
Go
package mempool
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type FeerStub struct {
|
|
feePerByte int64
|
|
p2pSigExt bool
|
|
blockHeight uint32
|
|
balance int64
|
|
}
|
|
|
|
func (fs *FeerStub) GetBaseExecFee() int64 {
|
|
return 30
|
|
}
|
|
|
|
func (fs *FeerStub) FeePerByte() int64 {
|
|
return fs.feePerByte
|
|
}
|
|
|
|
func (fs *FeerStub) BlockHeight() uint32 {
|
|
return fs.blockHeight
|
|
}
|
|
|
|
func (fs *FeerStub) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int {
|
|
return big.NewInt(fs.balance)
|
|
}
|
|
|
|
func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) {
|
|
mp := New(10, 0, false, nil)
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.Nonce = 0
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
_, ok := mp.TryGetValue(tx.Hash())
|
|
require.Equal(t, false, ok)
|
|
require.NoError(t, mp.Add(tx, fs))
|
|
// Re-adding should fail.
|
|
require.Error(t, mp.Add(tx, fs))
|
|
tx2, ok := mp.TryGetValue(tx.Hash())
|
|
require.Equal(t, true, ok)
|
|
require.Equal(t, tx, tx2)
|
|
mp.Remove(tx.Hash(), fs)
|
|
_, ok = mp.TryGetValue(tx.Hash())
|
|
require.Equal(t, false, ok)
|
|
// Make sure nothing left in the mempool after removal.
|
|
assert.Equal(t, 0, len(mp.verifiedMap))
|
|
assert.Equal(t, 0, len(mp.verifiedTxes))
|
|
}
|
|
|
|
func TestMemPoolRemoveStale(t *testing.T) {
|
|
mp := New(5, 0, false, nil)
|
|
txs := make([]*transaction.Transaction, 5)
|
|
for i := range txs {
|
|
txs[i] = transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
txs[i].Nonce = uint32(i)
|
|
txs[i].Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
require.NoError(t, mp.Add(txs[i], &FeerStub{blockHeight: uint32(i)}))
|
|
}
|
|
|
|
staleTxs := make(chan *transaction.Transaction, 5)
|
|
f := func(tx *transaction.Transaction, _ any) {
|
|
staleTxs <- tx
|
|
}
|
|
mp.SetResendThreshold(5, f)
|
|
|
|
isValid := func(tx *transaction.Transaction) bool {
|
|
return tx.Nonce%2 == 0
|
|
}
|
|
|
|
mp.RemoveStale(isValid, &FeerStub{blockHeight: 5}) // 0 + 5
|
|
require.Eventually(t, func() bool { return len(staleTxs) == 1 }, time.Second, time.Millisecond*100)
|
|
require.Equal(t, txs[0], <-staleTxs)
|
|
|
|
mp.RemoveStale(isValid, &FeerStub{blockHeight: 7}) // 2 + 5
|
|
require.Eventually(t, func() bool { return len(staleTxs) == 1 }, time.Second, time.Millisecond*100)
|
|
require.Equal(t, txs[2], <-staleTxs)
|
|
|
|
mp.RemoveStale(isValid, &FeerStub{blockHeight: 10}) // 0 + 2 * 5
|
|
require.Eventually(t, func() bool { return len(staleTxs) == 1 }, time.Second, time.Millisecond*100)
|
|
require.Equal(t, txs[0], <-staleTxs)
|
|
|
|
mp.RemoveStale(isValid, &FeerStub{blockHeight: 15}) // 0 + 3 * 5
|
|
|
|
// tx[2] should appear, so it is also checked that tx[0] wasn't sent on height 15.
|
|
mp.RemoveStale(isValid, &FeerStub{blockHeight: 22}) // 2 + 4 * 5
|
|
require.Eventually(t, func() bool { return len(staleTxs) == 1 }, time.Second, time.Millisecond*100)
|
|
require.Equal(t, txs[2], <-staleTxs)
|
|
|
|
// panic if something is sent after this.
|
|
close(staleTxs)
|
|
require.Len(t, staleTxs, 0)
|
|
}
|
|
|
|
func TestMemPoolAddRemove(t *testing.T) {
|
|
var fs = &FeerStub{}
|
|
testMemPoolAddRemoveWithFeer(t, fs)
|
|
}
|
|
|
|
func TestOverCapacity(t *testing.T) {
|
|
var fs = &FeerStub{balance: 10000000}
|
|
const mempoolSize = 10
|
|
mp := New(mempoolSize, 0, false, nil)
|
|
|
|
for i := 0; i < mempoolSize; i++ {
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.Nonce = uint32(i)
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
require.NoError(t, mp.Add(tx, fs))
|
|
}
|
|
txcnt := uint32(mempoolSize)
|
|
require.Equal(t, mempoolSize, mp.Count())
|
|
require.Equal(t, true, sort.IsSorted(sort.Reverse(mp.verifiedTxes)))
|
|
|
|
bigScript := make([]byte, 64)
|
|
bigScript[0] = byte(opcode.PUSH1)
|
|
bigScript[1] = byte(opcode.RET)
|
|
// Fees are also prioritized.
|
|
for i := 0; i < mempoolSize; i++ {
|
|
tx := transaction.New(bigScript, 0)
|
|
tx.NetworkFee = 10000
|
|
tx.Nonce = txcnt
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
txcnt++
|
|
// size is ~90, networkFee is 10000 => feePerByte is 119
|
|
require.NoError(t, mp.Add(tx, fs))
|
|
require.Equal(t, mempoolSize, mp.Count())
|
|
require.Equal(t, true, sort.IsSorted(sort.Reverse(mp.verifiedTxes)))
|
|
}
|
|
// Less prioritized txes are not allowed anymore.
|
|
tx := transaction.New(bigScript, 0)
|
|
tx.NetworkFee = 100
|
|
tx.Nonce = txcnt
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
txcnt++
|
|
require.Error(t, mp.Add(tx, fs))
|
|
require.Equal(t, mempoolSize, mp.Count())
|
|
require.Equal(t, mempoolSize, len(mp.verifiedMap))
|
|
require.Equal(t, mempoolSize, len(mp.verifiedTxes))
|
|
require.False(t, mp.containsKey(tx.Hash()))
|
|
require.Equal(t, true, sort.IsSorted(sort.Reverse(mp.verifiedTxes)))
|
|
|
|
// Low net fee, but higher per-byte fee is still a better combination.
|
|
tx = transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.Nonce = txcnt
|
|
tx.NetworkFee = 7000
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
txcnt++
|
|
// size is ~51 (small script), networkFee is 7000 (<10000)
|
|
// => feePerByte is 137 (>119)
|
|
require.NoError(t, mp.Add(tx, fs))
|
|
require.Equal(t, mempoolSize, mp.Count())
|
|
require.Equal(t, true, sort.IsSorted(sort.Reverse(mp.verifiedTxes)))
|
|
|
|
// High priority always wins over low priority.
|
|
for i := 0; i < mempoolSize; i++ {
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.NetworkFee = 8000
|
|
tx.Nonce = txcnt
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
txcnt++
|
|
require.NoError(t, mp.Add(tx, fs))
|
|
require.Equal(t, mempoolSize, mp.Count())
|
|
require.Equal(t, true, sort.IsSorted(sort.Reverse(mp.verifiedTxes)))
|
|
}
|
|
// Good luck with low priority now.
|
|
tx = transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.Nonce = txcnt
|
|
tx.NetworkFee = 7000
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
require.Error(t, mp.Add(tx, fs))
|
|
require.Equal(t, mempoolSize, mp.Count())
|
|
require.Equal(t, true, sort.IsSorted(sort.Reverse(mp.verifiedTxes)))
|
|
}
|
|
|
|
func TestGetVerified(t *testing.T) {
|
|
var fs = &FeerStub{}
|
|
const mempoolSize = 10
|
|
mp := New(mempoolSize, 0, false, nil)
|
|
|
|
txes := make([]*transaction.Transaction, 0, mempoolSize)
|
|
for i := 0; i < mempoolSize; i++ {
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.Nonce = uint32(i)
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
txes = append(txes, tx)
|
|
require.NoError(t, mp.Add(tx, fs))
|
|
}
|
|
require.Equal(t, mempoolSize, mp.Count())
|
|
verTxes := mp.GetVerifiedTransactions()
|
|
require.Equal(t, mempoolSize, len(verTxes))
|
|
require.ElementsMatch(t, txes, verTxes)
|
|
for _, tx := range txes {
|
|
mp.Remove(tx.Hash(), fs)
|
|
}
|
|
verTxes = mp.GetVerifiedTransactions()
|
|
require.Equal(t, 0, len(verTxes))
|
|
}
|
|
|
|
func TestRemoveStale(t *testing.T) {
|
|
var fs = &FeerStub{}
|
|
const mempoolSize = 10
|
|
mp := New(mempoolSize, 0, false, nil)
|
|
|
|
txes1 := make([]*transaction.Transaction, 0, mempoolSize/2)
|
|
txes2 := make([]*transaction.Transaction, 0, mempoolSize/2)
|
|
for i := 0; i < mempoolSize; i++ {
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.Nonce = uint32(i)
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
if i%2 == 0 {
|
|
txes1 = append(txes1, tx)
|
|
} else {
|
|
txes2 = append(txes2, tx)
|
|
}
|
|
require.NoError(t, mp.Add(tx, fs))
|
|
}
|
|
require.Equal(t, mempoolSize, mp.Count())
|
|
mp.RemoveStale(func(t *transaction.Transaction) bool {
|
|
for _, tx := range txes2 {
|
|
if tx == t {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}, &FeerStub{})
|
|
require.Equal(t, mempoolSize/2, mp.Count())
|
|
verTxes := mp.GetVerifiedTransactions()
|
|
for _, txf := range verTxes {
|
|
require.NotContains(t, txes1, txf)
|
|
require.Contains(t, txes2, txf)
|
|
}
|
|
}
|
|
|
|
func TestMemPoolFees(t *testing.T) {
|
|
mp := New(10, 0, false, nil)
|
|
fs := &FeerStub{balance: 10000000}
|
|
sender0 := util.Uint160{1, 2, 3}
|
|
tx0 := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx0.NetworkFee = fs.balance + 1
|
|
tx0.Signers = []transaction.Signer{{Account: sender0}}
|
|
// insufficient funds to add transaction, and balance shouldn't be stored
|
|
require.Equal(t, false, mp.Verify(tx0, fs))
|
|
require.Error(t, mp.Add(tx0, fs))
|
|
require.Equal(t, 0, len(mp.fees))
|
|
|
|
balancePart := new(big.Int).Div(big.NewInt(fs.balance), big.NewInt(4))
|
|
// no problems with adding another transaction with lower fee
|
|
tx1 := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx1.NetworkFee = balancePart.Int64()
|
|
tx1.Signers = []transaction.Signer{{Account: sender0}}
|
|
require.NoError(t, mp.Add(tx1, fs))
|
|
require.Equal(t, 1, len(mp.fees))
|
|
require.Equal(t, utilityBalanceAndFees{
|
|
balance: *uint256.NewInt(uint64(fs.balance)),
|
|
feeSum: *uint256.NewInt(uint64(tx1.NetworkFee)),
|
|
}, mp.fees[sender0])
|
|
|
|
// balance shouldn't change after adding one more transaction
|
|
tx2 := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx2.NetworkFee = new(big.Int).Sub(big.NewInt(fs.balance), balancePart).Int64()
|
|
tx2.Signers = []transaction.Signer{{Account: sender0}}
|
|
require.NoError(t, mp.Add(tx2, fs))
|
|
require.Equal(t, 2, len(mp.verifiedTxes))
|
|
require.Equal(t, 1, len(mp.fees))
|
|
require.Equal(t, utilityBalanceAndFees{
|
|
balance: *uint256.NewInt(uint64(fs.balance)),
|
|
feeSum: *uint256.NewInt(uint64(fs.balance)),
|
|
}, mp.fees[sender0])
|
|
|
|
// can't add more transactions as we don't have enough GAS
|
|
tx3 := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx3.NetworkFee = 1
|
|
tx3.Signers = []transaction.Signer{{Account: sender0}}
|
|
require.Equal(t, false, mp.Verify(tx3, fs))
|
|
require.Error(t, mp.Add(tx3, fs))
|
|
require.Equal(t, 1, len(mp.fees))
|
|
require.Equal(t, utilityBalanceAndFees{
|
|
balance: *uint256.NewInt(uint64(fs.balance)),
|
|
feeSum: *uint256.NewInt(uint64(fs.balance)),
|
|
}, mp.fees[sender0])
|
|
|
|
// check whether sender's fee updates correctly
|
|
mp.RemoveStale(func(t *transaction.Transaction) bool {
|
|
return t == tx2
|
|
}, fs)
|
|
require.Equal(t, 1, len(mp.fees))
|
|
require.Equal(t, utilityBalanceAndFees{
|
|
balance: *uint256.NewInt(uint64(fs.balance)),
|
|
feeSum: *uint256.NewInt(uint64(tx2.NetworkFee)),
|
|
}, mp.fees[sender0])
|
|
|
|
// there should be nothing left
|
|
mp.RemoveStale(func(t *transaction.Transaction) bool {
|
|
return t == tx3
|
|
}, fs)
|
|
require.Equal(t, 0, len(mp.fees))
|
|
}
|
|
|
|
func TestMempoolItemsOrder(t *testing.T) {
|
|
sender0 := util.Uint160{1, 2, 3}
|
|
balance := big.NewInt(10000000)
|
|
|
|
tx1 := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx1.NetworkFee = new(big.Int).Div(balance, big.NewInt(8)).Int64()
|
|
tx1.Signers = []transaction.Signer{{Account: sender0}}
|
|
tx1.Attributes = []transaction.Attribute{{Type: transaction.HighPriority}}
|
|
item1 := item{txn: tx1}
|
|
|
|
tx2 := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx2.NetworkFee = new(big.Int).Div(balance, big.NewInt(16)).Int64()
|
|
tx2.Signers = []transaction.Signer{{Account: sender0}}
|
|
tx2.Attributes = []transaction.Attribute{{Type: transaction.HighPriority}}
|
|
item2 := item{txn: tx2}
|
|
|
|
tx3 := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx3.NetworkFee = new(big.Int).Div(balance, big.NewInt(2)).Int64()
|
|
tx3.Signers = []transaction.Signer{{Account: sender0}}
|
|
item3 := item{txn: tx3}
|
|
|
|
tx4 := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx4.NetworkFee = new(big.Int).Div(balance, big.NewInt(4)).Int64()
|
|
tx4.Signers = []transaction.Signer{{Account: sender0}}
|
|
item4 := item{txn: tx4}
|
|
|
|
require.True(t, item1.CompareTo(item2) > 0)
|
|
require.True(t, item2.CompareTo(item1) < 0)
|
|
require.True(t, item1.CompareTo(item3) > 0)
|
|
require.True(t, item3.CompareTo(item1) < 0)
|
|
require.True(t, item1.CompareTo(item4) > 0)
|
|
require.True(t, item4.CompareTo(item1) < 0)
|
|
require.True(t, item2.CompareTo(item3) > 0)
|
|
require.True(t, item3.CompareTo(item2) < 0)
|
|
require.True(t, item2.CompareTo(item4) > 0)
|
|
require.True(t, item4.CompareTo(item2) < 0)
|
|
require.True(t, item3.CompareTo(item4) > 0)
|
|
require.True(t, item4.CompareTo(item3) < 0)
|
|
}
|
|
|
|
func TestMempoolAddRemoveOracleResponse(t *testing.T) {
|
|
mp := New(3, 0, false, nil)
|
|
nonce := uint32(0)
|
|
fs := &FeerStub{balance: 10000}
|
|
newTx := func(netFee int64, id uint64) *transaction.Transaction {
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.NetworkFee = netFee
|
|
tx.Nonce = nonce
|
|
nonce++
|
|
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
|
|
tx.Attributes = []transaction.Attribute{{
|
|
Type: transaction.OracleResponseT,
|
|
Value: &transaction.OracleResponse{ID: id},
|
|
}}
|
|
// sanity check
|
|
_, ok := mp.TryGetValue(tx.Hash())
|
|
require.False(t, ok)
|
|
return tx
|
|
}
|
|
|
|
tx1 := newTx(10, 1)
|
|
require.NoError(t, mp.Add(tx1, fs))
|
|
|
|
// smaller network fee
|
|
tx2 := newTx(5, 1)
|
|
err := mp.Add(tx2, fs)
|
|
require.ErrorIs(t, err, ErrOracleResponse)
|
|
|
|
// ok if old tx is removed
|
|
mp.Remove(tx1.Hash(), fs)
|
|
require.NoError(t, mp.Add(tx2, fs))
|
|
|
|
// higher network fee
|
|
tx3 := newTx(6, 1)
|
|
require.NoError(t, mp.Add(tx3, fs))
|
|
_, ok := mp.TryGetValue(tx2.Hash())
|
|
require.False(t, ok)
|
|
_, ok = mp.TryGetValue(tx3.Hash())
|
|
require.True(t, ok)
|
|
|
|
// another oracle response ID
|
|
tx4 := newTx(4, 2)
|
|
require.NoError(t, mp.Add(tx4, fs))
|
|
|
|
mp.RemoveStale(func(tx *transaction.Transaction) bool {
|
|
return tx.Hash() != tx4.Hash()
|
|
}, fs)
|
|
|
|
// check that oracle id was removed.
|
|
tx5 := newTx(3, 2)
|
|
require.NoError(t, mp.Add(tx5, fs))
|
|
|
|
// another oracle response ID with high net fee
|
|
tx6 := newTx(6, 3)
|
|
require.NoError(t, mp.Add(tx6, fs))
|
|
// check respIds
|
|
for _, i := range []uint64{1, 2, 3} {
|
|
_, ok := mp.oracleResp[i]
|
|
require.True(t, ok)
|
|
}
|
|
// reach capacity, check that response ID is removed together with tx5
|
|
tx7 := newTx(6, 4)
|
|
require.NoError(t, mp.Add(tx7, fs))
|
|
for _, i := range []uint64{1, 4, 3} {
|
|
_, ok := mp.oracleResp[i]
|
|
require.True(t, ok)
|
|
}
|
|
}
|
|
|
|
func TestMempoolAddRemoveConflicts(t *testing.T) {
|
|
var (
|
|
capacity = 6
|
|
mp = New(capacity, 0, false, nil)
|
|
sender = transaction.Signer{Account: util.Uint160{1, 2, 3}}
|
|
maliciousSender = transaction.Signer{Account: util.Uint160{4, 5, 6}}
|
|
)
|
|
|
|
var (
|
|
fs = &FeerStub{p2pSigExt: true, balance: 100000}
|
|
nonce uint32 = 1
|
|
)
|
|
getTx := func(netFee int64, sender transaction.Signer, hashes ...util.Uint256) *transaction.Transaction {
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.NetworkFee = netFee
|
|
tx.Nonce = nonce
|
|
nonce++
|
|
tx.Signers = []transaction.Signer{sender}
|
|
tx.Attributes = make([]transaction.Attribute, len(hashes))
|
|
for i, h := range hashes {
|
|
tx.Attributes[i] = transaction.Attribute{
|
|
Type: transaction.ConflictsT,
|
|
Value: &transaction.Conflicts{
|
|
Hash: h,
|
|
},
|
|
}
|
|
}
|
|
_, ok := mp.TryGetValue(tx.Hash())
|
|
require.Equal(t, false, ok)
|
|
return tx
|
|
}
|
|
getConflictsTx := func(netFee int64, hashes ...util.Uint256) *transaction.Transaction {
|
|
return getTx(netFee, sender, hashes...)
|
|
}
|
|
getMaliciousTx := func(netFee int64, hashes ...util.Uint256) *transaction.Transaction {
|
|
return getTx(netFee, maliciousSender, hashes...)
|
|
}
|
|
|
|
// tx1 in mempool and does not conflicts with anyone
|
|
smallNetFee := int64(3)
|
|
tx1 := getConflictsTx(smallNetFee)
|
|
require.NoError(t, mp.Add(tx1, fs))
|
|
|
|
// tx2 conflicts with tx1 and has smaller netfee (Step 2, negative)
|
|
tx2 := getConflictsTx(smallNetFee-1, tx1.Hash())
|
|
require.ErrorIs(t, mp.Add(tx2, fs), ErrConflictsAttribute)
|
|
|
|
// tx3 conflicts with mempooled tx1 and has larger netfee => tx1 should be replaced by tx3 (Step 2, positive)
|
|
tx3 := getConflictsTx(smallNetFee+1, tx1.Hash())
|
|
require.NoError(t, mp.Add(tx3, fs))
|
|
assert.Equal(t, 1, mp.Count())
|
|
assert.Equal(t, 1, len(mp.conflicts))
|
|
assert.Equal(t, []util.Uint256{tx3.Hash()}, mp.conflicts[tx1.Hash()])
|
|
|
|
// tx1 still does not conflicts with anyone, but tx3 is mempooled, conflicts with tx1
|
|
// and has larger netfee => tx1 shouldn't be added again (Step 1, negative)
|
|
require.ErrorIs(t, mp.Add(tx1, fs), ErrConflictsAttribute)
|
|
|
|
// tx2 can now safely be added because conflicting tx1 is not in mempool => we
|
|
// cannot check that tx2 is signed by tx1.Sender
|
|
require.NoError(t, mp.Add(tx2, fs))
|
|
assert.Equal(t, 1, len(mp.conflicts))
|
|
assert.Equal(t, []util.Uint256{tx3.Hash(), tx2.Hash()}, mp.conflicts[tx1.Hash()])
|
|
|
|
// mempooled tx4 conflicts with tx5, but tx4 has smaller netfee => tx4 should be replaced by tx5 (Step 1, positive)
|
|
tx5 := getConflictsTx(smallNetFee + 1)
|
|
tx4 := getConflictsTx(smallNetFee, tx5.Hash())
|
|
require.NoError(t, mp.Add(tx4, fs)) // unverified
|
|
assert.Equal(t, 2, len(mp.conflicts))
|
|
assert.Equal(t, []util.Uint256{tx4.Hash()}, mp.conflicts[tx5.Hash()])
|
|
require.NoError(t, mp.Add(tx5, fs))
|
|
// tx5 does not conflict with anyone
|
|
assert.Equal(t, 1, len(mp.conflicts))
|
|
|
|
// multiple conflicts in attributes of single transaction
|
|
tx6 := getConflictsTx(smallNetFee)
|
|
tx7 := getConflictsTx(smallNetFee)
|
|
tx8 := getConflictsTx(smallNetFee)
|
|
// need small network fee later
|
|
tx9 := getConflictsTx(smallNetFee-2, tx6.Hash(), tx7.Hash(), tx8.Hash())
|
|
require.NoError(t, mp.Add(tx9, fs))
|
|
assert.Equal(t, 4, len(mp.conflicts))
|
|
assert.Equal(t, []util.Uint256{tx9.Hash()}, mp.conflicts[tx6.Hash()])
|
|
assert.Equal(t, []util.Uint256{tx9.Hash()}, mp.conflicts[tx7.Hash()])
|
|
assert.Equal(t, []util.Uint256{tx9.Hash()}, mp.conflicts[tx8.Hash()])
|
|
assert.Equal(t, []util.Uint256{tx3.Hash(), tx2.Hash()}, mp.conflicts[tx1.Hash()])
|
|
|
|
// multiple conflicts in attributes of multiple transactions
|
|
tx10 := getConflictsTx(smallNetFee, tx6.Hash())
|
|
tx11 := getConflictsTx(smallNetFee, tx6.Hash())
|
|
require.NoError(t, mp.Add(tx10, fs)) // unverified, because tx6 is not in the pool
|
|
require.NoError(t, mp.Add(tx11, fs)) // unverified, because tx6 is not in the pool
|
|
assert.Equal(t, 4, len(mp.conflicts))
|
|
assert.Equal(t, []util.Uint256{tx9.Hash(), tx10.Hash(), tx11.Hash()}, mp.conflicts[tx6.Hash()])
|
|
assert.Equal(t, []util.Uint256{tx9.Hash()}, mp.conflicts[tx7.Hash()])
|
|
assert.Equal(t, []util.Uint256{tx9.Hash()}, mp.conflicts[tx8.Hash()])
|
|
assert.Equal(t, []util.Uint256{tx3.Hash(), tx2.Hash()}, mp.conflicts[tx1.Hash()])
|
|
|
|
// reach capacity, remove less prioritised tx9 with its multiple conflicts
|
|
require.Equal(t, capacity, len(mp.verifiedTxes))
|
|
tx12 := getConflictsTx(smallNetFee + 2)
|
|
require.NoError(t, mp.Add(tx12, fs))
|
|
assert.Equal(t, 2, len(mp.conflicts))
|
|
assert.Equal(t, []util.Uint256{tx10.Hash(), tx11.Hash()}, mp.conflicts[tx6.Hash()])
|
|
assert.Equal(t, []util.Uint256{tx3.Hash(), tx2.Hash()}, mp.conflicts[tx1.Hash()])
|
|
|
|
// manually remove tx11 with its single conflict
|
|
mp.Remove(tx11.Hash(), fs)
|
|
assert.Equal(t, 2, len(mp.conflicts))
|
|
assert.Equal(t, []util.Uint256{tx10.Hash()}, mp.conflicts[tx6.Hash()])
|
|
|
|
// manually remove last tx which conflicts with tx6 => mp.conflicts[tx6] should also be deleted
|
|
mp.Remove(tx10.Hash(), fs)
|
|
assert.Equal(t, 1, len(mp.conflicts))
|
|
assert.Equal(t, []util.Uint256{tx3.Hash(), tx2.Hash()}, mp.conflicts[tx1.Hash()])
|
|
|
|
// tx13 conflicts with tx2, but is not signed by tx2.Sender
|
|
tx13 := getMaliciousTx(smallNetFee, tx2.Hash())
|
|
_, ok := mp.TryGetValue(tx13.Hash())
|
|
require.Equal(t, false, ok)
|
|
require.ErrorIs(t, mp.Add(tx13, fs), ErrConflictsAttribute)
|
|
|
|
// tx15 conflicts with tx14, but added firstly and has the same network fee => tx14 must not be added.
|
|
tx14 := getConflictsTx(smallNetFee)
|
|
tx15 := getConflictsTx(smallNetFee, tx14.Hash())
|
|
require.NoError(t, mp.Add(tx15, fs))
|
|
err := mp.Add(tx14, fs)
|
|
require.Error(t, err)
|
|
|
|
require.True(t, strings.Contains(err.Error(), fmt.Sprintf("conflicting transactions have bigger or equal network fee: %d vs %d", smallNetFee, smallNetFee)))
|
|
|
|
check := func(t *testing.T, mainFee int64, fail bool) {
|
|
// Clear mempool.
|
|
mp.RemoveStale(func(t *transaction.Transaction) bool {
|
|
return false
|
|
}, fs)
|
|
|
|
// mempooled tx17, tx18, tx19 conflict with tx16
|
|
tx16 := getConflictsTx(mainFee)
|
|
tx17 := getConflictsTx(smallNetFee, tx16.Hash())
|
|
tx18 := getConflictsTx(smallNetFee, tx16.Hash())
|
|
tx19 := getMaliciousTx(smallNetFee, tx16.Hash()) // malicious, thus, doesn't take into account during fee evaluation
|
|
require.NoError(t, mp.Add(tx17, fs))
|
|
require.NoError(t, mp.Add(tx18, fs))
|
|
require.NoError(t, mp.Add(tx19, fs))
|
|
if fail {
|
|
require.Error(t, mp.Add(tx16, fs))
|
|
_, ok = mp.TryGetValue(tx17.Hash())
|
|
require.True(t, ok)
|
|
_, ok = mp.TryGetValue(tx18.Hash())
|
|
require.True(t, ok)
|
|
_, ok = mp.TryGetValue(tx19.Hash())
|
|
require.True(t, ok)
|
|
} else {
|
|
require.NoError(t, mp.Add(tx16, fs))
|
|
_, ok = mp.TryGetValue(tx17.Hash())
|
|
require.False(t, ok)
|
|
_, ok = mp.TryGetValue(tx18.Hash())
|
|
require.False(t, ok)
|
|
_, ok = mp.TryGetValue(tx19.Hash())
|
|
require.False(t, ok)
|
|
}
|
|
}
|
|
check(t, smallNetFee*2, true)
|
|
check(t, smallNetFee*2+1, false)
|
|
|
|
check = func(t *testing.T, mainFee int64, fail bool) {
|
|
// Clear mempool.
|
|
mp.RemoveStale(func(t *transaction.Transaction) bool {
|
|
return false
|
|
}, fs)
|
|
|
|
// mempooled tx20, tx21, tx22 don't conflict with anyone, but tx23 conflicts with them
|
|
tx20 := getConflictsTx(smallNetFee)
|
|
tx21 := getConflictsTx(smallNetFee)
|
|
tx22 := getConflictsTx(smallNetFee)
|
|
tx23 := getConflictsTx(mainFee, tx20.Hash(), tx21.Hash(), tx22.Hash())
|
|
require.NoError(t, mp.Add(tx20, fs))
|
|
require.NoError(t, mp.Add(tx21, fs))
|
|
require.NoError(t, mp.Add(tx22, fs))
|
|
if fail {
|
|
require.Error(t, mp.Add(tx23, fs))
|
|
_, ok = mp.TryGetData(tx20.Hash())
|
|
require.True(t, ok)
|
|
_, ok = mp.TryGetData(tx21.Hash())
|
|
require.True(t, ok)
|
|
_, ok = mp.TryGetData(tx22.Hash())
|
|
require.True(t, ok)
|
|
} else {
|
|
require.NoError(t, mp.Add(tx23, fs))
|
|
_, ok = mp.TryGetData(tx20.Hash())
|
|
require.False(t, ok)
|
|
_, ok = mp.TryGetData(tx21.Hash())
|
|
require.False(t, ok)
|
|
_, ok = mp.TryGetData(tx22.Hash())
|
|
require.False(t, ok)
|
|
}
|
|
}
|
|
check(t, smallNetFee*3, true)
|
|
check(t, smallNetFee*3+1, false)
|
|
}
|
|
|
|
func TestMempoolAddWithDataGetData(t *testing.T) {
|
|
var (
|
|
smallNetFee int64 = 3
|
|
nonce uint32
|
|
)
|
|
fs := &FeerStub{
|
|
feePerByte: 0,
|
|
p2pSigExt: true,
|
|
blockHeight: 5,
|
|
balance: 100,
|
|
}
|
|
mp := New(10, 1, false, nil)
|
|
|
|
// bad, insufficient deposit
|
|
r1 := &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(fs.balance+1, &nonce),
|
|
}
|
|
require.ErrorIs(t, mp.Add(r1.FallbackTransaction, fs, r1), ErrInsufficientFunds)
|
|
|
|
// good
|
|
r2 := &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r2.FallbackTransaction, fs, r2))
|
|
require.True(t, mp.ContainsKey(r2.FallbackTransaction.Hash()))
|
|
data, ok := mp.TryGetData(r2.FallbackTransaction.Hash())
|
|
require.True(t, ok)
|
|
require.Equal(t, r2, data)
|
|
|
|
// bad, already in pool
|
|
require.ErrorIs(t, mp.Add(r2.FallbackTransaction, fs, r2), ErrDup)
|
|
|
|
// good, higher priority than r2. The resulting mp.verifiedTxes: [r3, r2]
|
|
r3 := &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee+1, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r3.FallbackTransaction, fs, r3))
|
|
require.True(t, mp.ContainsKey(r3.FallbackTransaction.Hash()))
|
|
data, ok = mp.TryGetData(r3.FallbackTransaction.Hash())
|
|
require.True(t, ok)
|
|
require.Equal(t, r3, data)
|
|
|
|
// good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4]
|
|
r4 := &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r4.FallbackTransaction, fs, r4))
|
|
require.True(t, mp.ContainsKey(r4.FallbackTransaction.Hash()))
|
|
data, ok = mp.TryGetData(r4.FallbackTransaction.Hash())
|
|
require.True(t, ok)
|
|
require.Equal(t, r4, data)
|
|
|
|
// good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4, r5]
|
|
r5 := &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r5.FallbackTransaction, fs, r5))
|
|
require.True(t, mp.ContainsKey(r5.FallbackTransaction.Hash()))
|
|
data, ok = mp.TryGetData(r5.FallbackTransaction.Hash())
|
|
require.True(t, ok)
|
|
require.Equal(t, r5, data)
|
|
|
|
// and both r2's and r4's data should still be reachable
|
|
data, ok = mp.TryGetData(r2.FallbackTransaction.Hash())
|
|
require.True(t, ok)
|
|
require.Equal(t, r2, data)
|
|
data, ok = mp.TryGetData(r4.FallbackTransaction.Hash())
|
|
require.True(t, ok)
|
|
require.Equal(t, r4, data)
|
|
|
|
// should fail to get unexisting data
|
|
_, ok = mp.TryGetData(util.Uint256{0, 0, 0})
|
|
require.False(t, ok)
|
|
|
|
// but getting nil data is OK. The resulting mp.verifiedTxes: [r3, r2, r4, r5, r6]
|
|
r6 := mkTwoSignersTx(smallNetFee, &nonce)
|
|
require.NoError(t, mp.Add(r6, fs, nil))
|
|
require.True(t, mp.ContainsKey(r6.Hash()))
|
|
data, ok = mp.TryGetData(r6.Hash())
|
|
require.True(t, ok)
|
|
require.Nil(t, data)
|
|
|
|
// getting data: item is in verifiedMap, but not in verifiedTxes
|
|
r7 := &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r7.FallbackTransaction, fs, r4))
|
|
require.True(t, mp.ContainsKey(r7.FallbackTransaction.Hash()))
|
|
r8 := &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee-1, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r8.FallbackTransaction, fs, r4))
|
|
require.True(t, mp.ContainsKey(r8.FallbackTransaction.Hash()))
|
|
mp.verifiedTxes = append(mp.verifiedTxes[:len(mp.verifiedTxes)-2], mp.verifiedTxes[len(mp.verifiedTxes)-1])
|
|
_, ok = mp.TryGetData(r7.FallbackTransaction.Hash())
|
|
require.False(t, ok)
|
|
}
|
|
|
|
func mkTwoSignersTx(netFee int64, nonce *uint32) *transaction.Transaction {
|
|
tx := transaction.New([]byte{byte(opcode.RET)}, 0)
|
|
tx.Signers = []transaction.Signer{{}, {}}
|
|
tx.NetworkFee = netFee
|
|
*nonce++
|
|
tx.Nonce = *nonce
|
|
return tx
|
|
}
|
|
|
|
func TestMempoolIterateVerifiedTransactions(t *testing.T) {
|
|
var (
|
|
smallNetFee int64 = 3
|
|
nonce uint32
|
|
r1, r2, r3, r4, r5 *payload.P2PNotaryRequest
|
|
)
|
|
fs := &FeerStub{
|
|
feePerByte: 0,
|
|
p2pSigExt: true,
|
|
blockHeight: 5,
|
|
balance: 100,
|
|
}
|
|
mp := New(10, 1, false, nil)
|
|
|
|
checkRequestsOrder := func(orderedRequests []*payload.P2PNotaryRequest) {
|
|
var pooledRequests []*payload.P2PNotaryRequest
|
|
mp.IterateVerifiedTransactions(func(tx *transaction.Transaction, data any) bool {
|
|
d := data.(*payload.P2PNotaryRequest)
|
|
pooledRequests = append(pooledRequests, d)
|
|
return true
|
|
})
|
|
require.Equal(t, orderedRequests, pooledRequests)
|
|
}
|
|
|
|
r1 = &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r1.FallbackTransaction, fs, r1))
|
|
checkRequestsOrder([]*payload.P2PNotaryRequest{r1})
|
|
|
|
// r2 has higher priority than r1. The resulting mp.verifiedTxes: [r2, r1]
|
|
r2 = &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee+1, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r2.FallbackTransaction, fs, r2))
|
|
checkRequestsOrder([]*payload.P2PNotaryRequest{r2, r1})
|
|
|
|
// r3 has the same priority as r1. The resulting mp.verifiedTxes: [r2, r1, r3]
|
|
r3 = &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r3.FallbackTransaction, fs, r3))
|
|
checkRequestsOrder([]*payload.P2PNotaryRequest{r2, r1, r3})
|
|
|
|
// r4 has the same priority as r1. The resulting mp.verifiedTxes: [r2, r1, r3, r4]
|
|
r4 = &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
|
|
}
|
|
require.NoError(t, mp.Add(r4.FallbackTransaction, fs, r4))
|
|
checkRequestsOrder([]*payload.P2PNotaryRequest{r2, r1, r3, r4})
|
|
|
|
checkPooledRequest := func(t *testing.T, r *payload.P2PNotaryRequest, isPooled bool) {
|
|
cont := true
|
|
notaryRequest := &payload.P2PNotaryRequest{}
|
|
mp.IterateVerifiedTransactions(func(tx *transaction.Transaction, data any) bool {
|
|
if data != nil {
|
|
notaryRequest = data.(*payload.P2PNotaryRequest)
|
|
if notaryRequest.MainTransaction.Hash() == r.MainTransaction.Hash() {
|
|
cont = false
|
|
}
|
|
}
|
|
return cont
|
|
})
|
|
|
|
if isPooled {
|
|
require.Equal(t, false, cont)
|
|
require.Equal(t, r, notaryRequest)
|
|
} else {
|
|
require.Equal(t, true, cont)
|
|
}
|
|
}
|
|
checkPooledRequest(t, r1, true)
|
|
checkPooledRequest(t, r2, true)
|
|
checkPooledRequest(t, r3, true)
|
|
checkPooledRequest(t, r4, true)
|
|
|
|
r5 = &payload.P2PNotaryRequest{
|
|
MainTransaction: mkTwoSignersTx(0, &nonce),
|
|
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
|
|
}
|
|
checkPooledRequest(t, r5, false)
|
|
}
|