Skip to content

Commit

Permalink
feat: sender mempool impl (cosmos#13888)
Browse files Browse the repository at this point in the history
* draft sender mempool impl

* select

* nit

* random sender update

* nit

* prevent memory leak

* fix nil return

* small fixes

* added tests

* change count

* finish tx order test removed the three address test due to make the test to bloated when including non determinism

* remove unsued variable

* nit

* fix

* temoral commit braking

* nit most

* nit most

* final

* comments

* t

* comments

* test

* add nolint

* Fix comment

* golint comment

* golint

* improve format?

* more gosec disable

* Fix ctr usage

* use #nosec

* Update types/mempool/sender_nonce.go

* Kocubinski/random sender nonce (cosmos#13956)

* refactor

* fix iteration logic

* fix merge err

* import fixes

* derive order randomness from seed only

* gosec fix

* ignore gosec again

* comments

* property based

* minor fixes

* added property test

* comment

* fix imports

* comment

* Update types/mempool/sender_nonce_property_test.go

Co-authored-by: Matt Kocubinski <mkocubinski@gmail.com>

* remove unesessary loop

* improve function name

* Update types/mempool/sender_nonce.go

Co-authored-by: Facundo Medica <14063057+facundomedica@users.noreply.github.com>

* change import name

* change validation to be preemvtive

Co-authored-by: Matt Kocubinski <mkocubinski@gmail.com>
Co-authored-by: Facundo Medica <14063057+facundomedica@users.noreply.github.com>
  • Loading branch information
3 people committed Nov 23, 2022
1 parent 644f906 commit 908fda1
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 222 deletions.
2 changes: 1 addition & 1 deletion baseapp/abci_v1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestABCIv1TestSuite(t *testing.T) {
func (s *ABCIv1TestSuite) SetupTest() {
t := s.T()
anteKey := []byte("ante-key")
pool := mempool.NewNonceMempool()
pool := mempool.NewSenderNonceMempool()
anteOpt := func(bapp *baseapp.BaseApp) {
bapp.SetAnteHandler(anteHandlerTxTest(t, capKey1, anteKey))
}
Expand Down
2 changes: 1 addition & 1 deletion baseapp/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func NewBaseApp(
}

if app.mempool == nil {
app.SetMempool(mempool.NewNonceMempool())
app.SetMempool(mempool.NewSenderNonceMempool())
}

if app.processProposal == nil {
Expand Down
2 changes: 1 addition & 1 deletion baseapp/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func makeTestConfig() depinject.Config {
}

func makeMinimalConfig() depinject.Config {
var mempoolOpt runtime.BaseAppOption = baseapp.SetMempool(mempool.NewNonceMempool())
var mempoolOpt runtime.BaseAppOption = baseapp.SetMempool(mempool.NewSenderNonceMempool())
return depinject.Configs(
depinject.Supply(mempoolOpt),
appconfig.Compose(&appv1alpha1.Config{
Expand Down
3 changes: 2 additions & 1 deletion types/mempool/mempool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func (s *MempoolTestSuite) TestDefaultMempool() {
for i := 0; i < txCount; i++ {
acc := accounts[i%len(accounts)]
tx := testTx{
nonce: 0,
address: acc.Address,
priority: rand.Int63(),
}
Expand Down Expand Up @@ -201,7 +202,7 @@ type MempoolTestSuite struct {

func (s *MempoolTestSuite) resetMempool() {
s.iterations = 0
s.mempool = mempool.NewNonceMempool()
s.mempool = mempool.NewSenderNonceMempool()
}

func (s *MempoolTestSuite) SetupTest() {
Expand Down
125 changes: 0 additions & 125 deletions types/mempool/nonce.go

This file was deleted.

202 changes: 202 additions & 0 deletions types/mempool/sender_nonce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package mempool

import (
crand "crypto/rand" // #nosec // crypto/rand is used for seed generation
"encoding/binary"
"fmt"
"math/rand" // #nosec // math/rand is used for random selection and seeded from crypto/rand

"github.com/huandu/skiplist"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/signing"
)

var (
_ Mempool = (*senderNonceMempool)(nil)
_ Iterator = (*senderNonceMepoolIterator)(nil)
)

// senderNonceMempool is a mempool that prioritizes transactions within a sender by nonce, the lowest first,
// but selects a random sender on each iteration. The mempool is iterated by:
//
// 1) Maintaining a separate list of nonce ordered txs per sender
// 2) For each select iteration, randomly choose a sender and pick the next nonce ordered tx from their list
// 3) Repeat 1,2 until the mempool is exhausted
//
// Note that PrepareProposal could choose to stop iteration before reaching the end if maxBytes is reached.
type senderNonceMempool struct {
senders map[string]*skiplist.SkipList
rnd *rand.Rand
}

// NewSenderNonceMempool creates a new mempool that prioritizes transactions by nonce, the lowest first.
func NewSenderNonceMempool() Mempool {
senderMap := make(map[string]*skiplist.SkipList)
snp := &senderNonceMempool{
senders: senderMap,
}

var seed int64
err := binary.Read(crand.Reader, binary.BigEndian, &seed)
if err != nil {
panic(err)
}
snp.setSeed(seed)

return snp
}

// NewSenderNonceMempoolWithSeed creates a new mempool that prioritizes transactions by nonce, the lowest first and sets the random seed.
func NewSenderNonceMempoolWithSeed(seed int64) Mempool {
senderMap := make(map[string]*skiplist.SkipList)
snp := &senderNonceMempool{
senders: senderMap,
}
snp.setSeed(seed)
return snp
}

func (snm *senderNonceMempool) setSeed(seed int64) {
s1 := rand.NewSource(seed)
snm.rnd = rand.New(s1) //#nosec // math/rand is seeded from crypto/rand by default
}

// Insert adds a tx to the mempool. It returns an error if the tx does not have at least one signer.
// priority is ignored.
func (snm *senderNonceMempool) Insert(_ sdk.Context, tx sdk.Tx) error {
sigs, err := tx.(signing.SigVerifiableTx).GetSignaturesV2()
if err != nil {
return err
}
if len(sigs) == 0 {
return fmt.Errorf("tx must have at least one signer")
}

sig := sigs[0]
sender := sig.PubKey.Address().String()
nonce := sig.Sequence
senderTxs, found := snm.senders[sender]
if !found {
senderTxs = skiplist.New(skiplist.Uint64)
snm.senders[sender] = senderTxs
}
senderTxs.Set(nonce, tx)

return nil
}

// Select returns an iterator ordering transactions the mempool with the lowest nonce of a random selected sender first.
func (snm *senderNonceMempool) Select(_ sdk.Context, _ [][]byte) Iterator {
var senders []string
senderCursors := make(map[string]*skiplist.Element)

orderedSenders := skiplist.New(skiplist.String)

// #nosec
for s := range snm.senders {
orderedSenders.Set(s, s)
}

s := orderedSenders.Front()
for s != nil {
sender := s.Value.(string)
senders = append(senders, sender)
senderCursors[sender] = snm.senders[sender].Front()
s = s.Next()
}

iter := &senderNonceMepoolIterator{
senders: senders,
rnd: snm.rnd,
senderCursors: senderCursors,
}

return iter.Next()
}

// CountTx returns the total count of txs in the mempool.
func (snm *senderNonceMempool) CountTx() int {
count := 0

// Disable gosec here since we need neither strong randomness nor deterministic iteration.
// #nosec
for _, value := range snm.senders {
count += value.Len()
}
return count
}

// Remove removes a tx from the mempool. It returns an error if the tx does not have at least one signer or the tx
// was not found in the pool.
func (snm *senderNonceMempool) Remove(tx sdk.Tx) error {
sigs, err := tx.(signing.SigVerifiableTx).GetSignaturesV2()
if err != nil {
return err
}
if len(sigs) == 0 {
return fmt.Errorf("tx must have at least one signer")
}

sig := sigs[0]
sender := sig.PubKey.Address().String()
nonce := sig.Sequence
senderTxs, found := snm.senders[sender]
if !found {
return ErrTxNotFound
}

res := senderTxs.Remove(nonce)
if res == nil {
return ErrTxNotFound
}

if senderTxs.Len() == 0 {
delete(snm.senders, sender)
}
return nil
}

type senderNonceMepoolIterator struct {
rnd *rand.Rand
currentTx *skiplist.Element
senders []string
senderCursors map[string]*skiplist.Element
}

// Next returns the next iterator state which will contain a tx with the next smallest nonce of a randomly
// selected sender.
func (i *senderNonceMepoolIterator) Next() Iterator {
for len(i.senders) > 0 {
senderIndex := i.rnd.Intn(len(i.senders))
sender := i.senders[senderIndex]
senderCursor, found := i.senderCursors[sender]
if !found {
i.senders = removeAtIndex(i.senders, senderIndex)
continue
}

if nextCursor := senderCursor.Next(); nextCursor != nil {
i.senderCursors[sender] = nextCursor
} else {
i.senders = removeAtIndex(i.senders, senderIndex)
}

return &senderNonceMepoolIterator{
senders: i.senders,
currentTx: senderCursor,
rnd: i.rnd,
senderCursors: i.senderCursors,
}
}

return nil
}

func (i *senderNonceMepoolIterator) Tx() sdk.Tx {
return i.currentTx.Value.(sdk.Tx)
}

func removeAtIndex[T any](slice []T, index int) []T {
return append(slice[:index], slice[index+1:]...)
}
Loading

0 comments on commit 908fda1

Please sign in to comment.