diff --git a/consensus/replay_stubs.go b/consensus/replay_stubs.go index a39fbbc989..60d587edd4 100644 --- a/consensus/replay_stubs.go +++ b/consensus/replay_stubs.go @@ -47,8 +47,10 @@ func (emptyMempool) TxsBytes() int64 { return 0 } func (emptyMempool) TxsFront() *clist.CElement { return nil } func (emptyMempool) TxsWaitChan() <-chan struct{} { return nil } -func (emptyMempool) InitWAL() error { return nil } -func (emptyMempool) CloseWAL() {} +func (emptyMempool) InitWAL() error { return nil } +func (emptyMempool) CloseWAL() {} +func (emptyMempool) GetTxByKey(types.TxKey) (types.Tx, bool) { return nil, false } +func (emptyMempool) WasRecentlyEvicted(types.TxKey) bool { return false } //----------------------------------------------------------------------------- // mockProxyApp uses ABCIResponses to give the right results. diff --git a/go.mod b/go.mod index 97ff458c46..1a171cfe7b 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/go-logfmt/logfmt v0.5.1 github.com/gofrs/uuid v4.3.0+incompatible github.com/gogo/protobuf v1.3.2 + github.com/golang/mock v1.4.4 github.com/golang/protobuf v1.5.3 github.com/golangci/golangci-lint v1.50.1 github.com/google/orderedcode v0.0.1 @@ -49,8 +50,8 @@ require ( go.opentelemetry.io/otel v1.21.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.18.0 go.opentelemetry.io/otel/sdk v1.21.0 - golang.org/x/crypto v0.21.0 - golang.org/x/net v0.23.0 + golang.org/x/crypto v0.24.0 + golang.org/x/net v0.26.0 gonum.org/v1/gonum v0.8.2 google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 @@ -275,12 +276,12 @@ require ( go.uber.org/zap v1.23.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/exp/typeparams v0.0.0-20220827204233-334a2380cb91 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.13.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index cb28b78e71..d1ef0dce8b 100644 --- a/go.sum +++ b/go.sum @@ -380,6 +380,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -989,8 +990,8 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1038,8 +1039,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1089,8 +1090,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1117,8 +1118,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1200,15 +1201,15 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1220,8 +1221,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1312,8 +1313,8 @@ golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/mempool/cache.go b/mempool/cache.go index 3ed2b27b14..8141f2ae23 100644 --- a/mempool/cache.go +++ b/mempool/cache.go @@ -26,6 +26,9 @@ type TxCache interface { // Has reports whether tx is present in the cache. Checking for presence is // not treated as an access of the value. Has(tx types.Tx) bool + + // HasKey reports whether the given key is present in the cache. + HasKey(key types.TxKey) bool } var _ TxCache = (*LRUTxCache)(nil) @@ -113,12 +116,21 @@ func (c *LRUTxCache) Has(tx types.Tx) bool { return ok } +func (c *LRUTxCache) HasKey(key types.TxKey) bool { + c.mtx.Lock() + defer c.mtx.Unlock() + + _, ok := c.cacheMap[key] + return ok +} + // NopTxCache defines a no-op raw transaction cache. type NopTxCache struct{} var _ TxCache = (*NopTxCache)(nil) -func (NopTxCache) Reset() {} -func (NopTxCache) Push(types.Tx) bool { return true } -func (NopTxCache) Remove(types.Tx) {} -func (NopTxCache) Has(types.Tx) bool { return false } +func (NopTxCache) Reset() {} +func (NopTxCache) Push(types.Tx) bool { return true } +func (NopTxCache) Remove(types.Tx) {} +func (NopTxCache) Has(types.Tx) bool { return false } +func (NopTxCache) HasKey(types.TxKey) bool { return false } diff --git a/mempool/cat/pool.go b/mempool/cat/pool.go index a1a86e1ef4..91caa1d86e 100644 --- a/mempool/cat/pool.go +++ b/mempool/cat/pool.go @@ -64,6 +64,8 @@ type TxPool struct { // Thread-safe cache of rejected transactions for quick look-up rejectedTxCache *LRUTxCache + // Thread-safe cache of evicted transactions for quick look-up + evictedTxCache *LRUTxCache // Thread-safe list of transactions peers have seen that we have not yet seen seenByPeersSet *SeenTxSet @@ -92,6 +94,7 @@ func NewTxPool( proxyAppConn: proxyAppConn, metrics: mempool.NopMetrics(), rejectedTxCache: NewLRUTxCache(cfg.CacheSize), + evictedTxCache: NewLRUTxCache(cfg.CacheSize / 5), seenByPeersSet: NewSeenTxSet(), height: height, preCheckFn: func(_ types.Tx) error { return nil }, @@ -171,9 +174,15 @@ func (txmp *TxPool) Has(txKey types.TxKey) bool { return txmp.store.has(txKey) } -// Get retrieves a transaction based on the key. It returns a bool -// if the transaction exists or not +// Get retrieves a transaction based on the key. +// Deprecated: use GetTxByKey instead. func (txmp *TxPool) Get(txKey types.TxKey) (types.Tx, bool) { + return txmp.GetTxByKey(txKey) +} + +// GetTxByKey retrieves a transaction based on the key. It returns a bool +// indicating whether transaction was found in the cache. +func (txmp *TxPool) GetTxByKey(txKey types.TxKey) (types.Tx, bool) { wtx := txmp.store.get(txKey) if wtx != nil { return wtx.tx, true @@ -181,6 +190,12 @@ func (txmp *TxPool) Get(txKey types.TxKey) (types.Tx, bool) { return types.Tx{}, false } +// WasRecentlyEvicted returns a bool indicating whether the transaction with +// the specified key was recently evicted and is currently within the cache. +func (txmp *TxPool) WasRecentlyEvicted(txKey types.TxKey) bool { + return txmp.evictedTxCache.Has(txKey) +} + // IsRejectedTx returns true if the transaction was recently rejected and is // currently within the cache func (txmp *TxPool) IsRejectedTx(txKey types.TxKey) bool { @@ -195,9 +210,13 @@ func (txmp *TxPool) CheckToPurgeExpiredTxs() { defer txmp.updateMtx.Unlock() if txmp.config.TTLDuration > 0 && time.Since(txmp.lastPurgeTime) > txmp.config.TTLDuration { expirationAge := time.Now().Add(-txmp.config.TTLDuration) - // a height of 0 means no transactions will be removed because of height + // A height of 0 means no transactions will be removed because of height // (in other words, no transaction has a height less than 0) - numExpired := txmp.store.purgeExpiredTxs(0, expirationAge) + purgedTxs, numExpired := txmp.store.purgeExpiredTxs(0, expirationAge) + // Add the purged transactions to the evicted cache + for _, tx := range purgedTxs { + txmp.evictedTxCache.Push(tx.key) + } txmp.metrics.EvictedTxs.Add(float64(numExpired)) txmp.lastPurgeTime = time.Now() } @@ -373,6 +392,7 @@ func (txmp *TxPool) Flush() { txmp.store.reset() txmp.seenByPeersSet.Reset() txmp.rejectedTxCache.Reset() + txmp.evictedTxCache.Reset() txmp.metrics.EvictedTxs.Add(float64(size)) txmp.broadcastMtx.Lock() defer txmp.broadcastMtx.Unlock() @@ -537,6 +557,7 @@ func (txmp *TxPool) addNewTransaction(wtx *wrappedTx, checkTxRes *abci.ResponseC // drop the new one. if len(victims) == 0 || victimBytes < wtx.size() { txmp.metrics.EvictedTxs.Add(1) + txmp.evictedTxCache.Push(wtx.key) checkTxRes.MempoolError = fmt.Sprintf("rejected valid incoming transaction; mempool is full (%X)", wtx.key) return fmt.Errorf("rejected valid incoming transaction; mempool is full (%X). Size: (%d:%d)", @@ -591,6 +612,7 @@ func (txmp *TxPool) addNewTransaction(wtx *wrappedTx, checkTxRes *abci.ResponseC func (txmp *TxPool) evictTx(wtx *wrappedTx) { txmp.store.remove(wtx.key) + txmp.evictedTxCache.Push(wtx.key) txmp.metrics.EvictedTxs.Add(1) txmp.logger.Debug( "evicted valid existing transaction; mempool full", @@ -720,7 +742,11 @@ func (txmp *TxPool) purgeExpiredTxs(blockHeight int64) { expirationAge = time.Time{} } - numExpired := txmp.store.purgeExpiredTxs(expirationHeight, expirationAge) + purgedTxs, numExpired := txmp.store.purgeExpiredTxs(expirationHeight, expirationAge) + // Add the purged transactions to the evicted cache + for _, tx := range purgedTxs { + txmp.evictedTxCache.Push(tx.key) + } txmp.metrics.EvictedTxs.Add(float64(numExpired)) // purge old evicted and seen transactions diff --git a/mempool/cat/pool_test.go b/mempool/cat/pool_test.go index 7785123446..5f27448420 100644 --- a/mempool/cat/pool_test.go +++ b/mempool/cat/pool_test.go @@ -244,6 +244,7 @@ func TestTxPool_Eviction(t *testing.T) { mustCheckTx(t, txmp, "key1=0000=25") require.True(t, txExists("key1=0000=25")) require.False(t, txExists(bigTx)) + require.True(t, txmp.WasRecentlyEvicted(types.Tx(bigTx).Key())) require.Equal(t, int64(len("key1=0000=25")), txmp.SizeBytes()) // Now fill up the rest of the slots with other transactions. @@ -257,23 +258,27 @@ func TestTxPool_Eviction(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "mempool is full") require.False(t, txExists("key6=0005=1")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx("key6=0005=1").Key())) // A new transaction with higher priority should evict key5, which is the // newest of the two transactions with lowest priority. mustCheckTx(t, txmp, "key7=0006=7") require.True(t, txExists("key7=0006=7")) // new transaction added require.False(t, txExists("key5=0004=3")) // newest low-priority tx evicted - require.True(t, txExists("key4=0003=3")) // older low-priority tx retained + require.True(t, txmp.WasRecentlyEvicted(types.Tx("key5=0004=3").Key())) + require.True(t, txExists("key4=0003=3")) // older low-priority tx retained // Another new transaction evicts the other low-priority element. mustCheckTx(t, txmp, "key8=0007=20") require.True(t, txExists("key8=0007=20")) require.False(t, txExists("key4=0003=3")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx("key4=0003=3").Key())) // Now the lowest-priority tx is 5, so that should be the next to go. mustCheckTx(t, txmp, "key9=0008=9") require.True(t, txExists("key9=0008=9")) require.False(t, txExists("key2=0001=5")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx("key2=0001=5").Key())) // Add a transaction that requires eviction of multiple lower-priority // entries, in order to fit the size of the element. @@ -282,8 +287,11 @@ func TestTxPool_Eviction(t *testing.T) { require.True(t, txExists("key8=0007=20")) require.True(t, txExists("key10=0123456789abcdef=11")) require.False(t, txExists("key3=0002=10")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx("key3=0002=10").Key())) require.False(t, txExists("key9=0008=9")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx("key9=0008=9").Key())) require.False(t, txExists("key7=0006=7")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx("key7=0006=7").Key())) // Free up some space so we can add back previously evicted txs err = txmp.Update(1, types.Txs{types.Tx("key10=0123456789abcdef=11")}, []*abci.ResponseDeliverTx{{Code: abci.CodeTypeOK}}, nil, nil) @@ -296,6 +304,7 @@ func TestTxPool_Eviction(t *testing.T) { // space for the previously evicted tx require.NoError(t, txmp.RemoveTxByKey(types.Tx("key8=0007=20").Key())) require.False(t, txExists("key8=0007=20")) + require.False(t, txmp.WasRecentlyEvicted(types.Tx("key8=0007=20").Key())) } func TestTxPool_Flush(t *testing.T) { @@ -567,6 +576,10 @@ func TestTxPool_ExpiredTxs_Timestamp(t *testing.T) { // All the transactions in the original set should have been purged. for _, tx := range added1 { + // Check that it was added to the evictedTxCache + evicted := txmp.WasRecentlyEvicted(tx.tx.Key()) + require.True(t, evicted) + if txmp.store.has(tx.tx.Key()) { t.Errorf("Transaction %X should have been purged for TTL", tx.tx.Key()) } diff --git a/mempool/cat/reactor.go b/mempool/cat/reactor.go index 01d2be4c72..76c918ea23 100644 --- a/mempool/cat/reactor.go +++ b/mempool/cat/reactor.go @@ -318,7 +318,7 @@ func (memR *Reactor) ReceiveEnvelope(e p2p.Envelope) { txKey[:], schema.Download, ) - tx, has := memR.mempool.Get(txKey) + tx, has := memR.mempool.GetTxByKey(txKey) if has && !memR.opts.ListenOnly { peerID := memR.ids.GetIDForPeer(e.Src.ID()) memR.Logger.Debug("sending a tx in response to a want msg", "peer", peerID) diff --git a/mempool/cat/store.go b/mempool/cat/store.go index 8972186797..33ecf1a851 100644 --- a/mempool/cat/store.go +++ b/mempool/cat/store.go @@ -141,19 +141,23 @@ func (s *store) getTxsBelowPriority(priority int64) ([]*wrappedTx, int64) { } // purgeExpiredTxs removes all transactions that are older than the given height -// and time. Returns the amount of transactions that were removed -func (s *store) purgeExpiredTxs(expirationHeight int64, expirationAge time.Time) int { +// and time. Returns the purged txs and amount of transactions that were purged. +func (s *store) purgeExpiredTxs(expirationHeight int64, expirationAge time.Time) ([]*wrappedTx, int) { s.mtx.Lock() defer s.mtx.Unlock() + + var purgedTxs []*wrappedTx counter := 0 + for key, tx := range s.txs { if tx.height < expirationHeight || tx.timestamp.Before(expirationAge) { s.bytes -= tx.size() delete(s.txs, key) + purgedTxs = append(purgedTxs, tx) counter++ } } - return counter + return purgedTxs, counter } func (s *store) reset() { diff --git a/mempool/mempool.go b/mempool/mempool.go index 0f1b32d280..3d7d83d695 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -91,6 +91,15 @@ type Mempool interface { // trigger once every height when transactions are available. EnableTxsAvailable() + // GetTxByKey gets a tx by its key from the mempool. Returns the tx and a bool indicating its presence in the tx cache. + // Used in the RPC endpoint: TxStatus. + GetTxByKey(key types.TxKey) (types.Tx, bool) + + // WasRecentlyEvicted returns true if the tx was evicted from the mempool and exists in the + // evicted cache. + // Used in the RPC endpoint: TxStatus. + WasRecentlyEvicted(key types.TxKey) bool + // Size returns the number of transactions in the mempool. Size() int diff --git a/mempool/mock/mempool.go b/mempool/mock/mempool.go index 986c28ffca..d1adfab008 100644 --- a/mempool/mock/mempool.go +++ b/mempool/mock/mempool.go @@ -30,14 +30,15 @@ func (Mempool) Update( ) error { return nil } -func (Mempool) Flush() {} -func (Mempool) FlushAppConn() error { return nil } -func (Mempool) TxsAvailable() <-chan struct{} { return make(chan struct{}) } -func (Mempool) EnableTxsAvailable() {} -func (Mempool) SizeBytes() int64 { return 0 } - -func (Mempool) TxsFront() *clist.CElement { return nil } -func (Mempool) TxsWaitChan() <-chan struct{} { return nil } +func (Mempool) Flush() {} +func (Mempool) FlushAppConn() error { return nil } +func (Mempool) TxsAvailable() <-chan struct{} { return make(chan struct{}) } +func (Mempool) EnableTxsAvailable() {} +func (Mempool) SizeBytes() int64 { return 0 } +func (m Mempool) GetTxByKey(types.TxKey) (types.Tx, bool) { return nil, false } +func (m Mempool) WasRecentlyEvicted(types.TxKey) bool { return false } +func (Mempool) TxsFront() *clist.CElement { return nil } +func (Mempool) TxsWaitChan() <-chan struct{} { return nil } func (Mempool) InitWAL() error { return nil } func (Mempool) CloseWAL() {} diff --git a/mempool/v0/clist_mempool.go b/mempool/v0/clist_mempool.go index e81083cac8..e783113b0f 100644 --- a/mempool/v0/clist_mempool.go +++ b/mempool/v0/clist_mempool.go @@ -183,6 +183,21 @@ func (mem *CListMempool) TxsFront() *clist.CElement { return mem.txs.Front() } +// GetTxByKey retrieves a transaction from the mempool using its key. +func (mem *CListMempool) GetTxByKey(key types.TxKey) (types.Tx, bool) { + e, ok := mem.txsMap.Load(key) + if !ok { + return nil, false + } + memTx, ok := e.(*clist.CElement).Value.(*mempoolTx) + return memTx.tx, ok +} + +// WasRecentlyEvicted returns false consistently as this implementation does not support transaction eviction. +func (mem *CListMempool) WasRecentlyEvicted(key types.TxKey) bool { + return false +} + // TxsWaitChan returns a channel to wait on transactions. It will be closed // once the mempool is not empty (ie. the internal `mem.txs` has at least one // element) diff --git a/mempool/v0/clist_mempool_test.go b/mempool/v0/clist_mempool_test.go index 0c303714c9..f2e01e5aad 100644 --- a/mempool/v0/clist_mempool_test.go +++ b/mempool/v0/clist_mempool_test.go @@ -549,6 +549,32 @@ func TestMempool_CheckTxChecksTxSize(t *testing.T) { } } +func TestGetTxByKey(t *testing.T) { + app := kvstore.NewApplication() + cc := proxy.NewLocalClientCreator(app) + + mp, cleanup := newMempoolWithApp(cc) + defer cleanup() + + // Create a tx + tx := types.Tx([]byte{0x01}) + // Add it to the mempool + err := mp.CheckTx(tx, nil, mempool.TxInfo{}) + require.NoError(t, err) + + // Query the tx from the mempool + got, ok := mp.GetTxByKey(tx.Key()) + require.True(t, ok) + // Ensure the returned tx is the same as the one we added + require.Equal(t, tx, got) + + // Query a random tx from the mempool + randomTx, ok := mp.GetTxByKey(types.Tx([]byte{0x02}).Key()) + // Ensure the returned tx is nil + require.False(t, ok) + require.Nil(t, randomTx) +} + func TestMempoolTxsBytes(t *testing.T) { app := kvstore.NewApplication() cc := proxy.NewLocalClientCreator(app) diff --git a/mempool/v1/mempool.go b/mempool/v1/mempool.go index 958d229ec3..5a22ab4e24 100644 --- a/mempool/v1/mempool.go +++ b/mempool/v1/mempool.go @@ -57,6 +57,7 @@ type TxMempool struct { txs *clist.CList // valid transactions (passed CheckTx) txByKey map[types.TxKey]*clist.CElement txBySender map[string]*clist.CElement // for sender != "" + evictedTxs mempool.TxCache // for tracking evicted transactions traceClient trace.Tracer } @@ -86,6 +87,7 @@ func NewTxMempool( } if cfg.CacheSize > 0 { txmp.cache = mempool.NewLRUTxCache(cfg.CacheSize) + txmp.evictedTxs = mempool.NewLRUTxCache(cfg.CacheSize / 5) } for _, opt := range options { @@ -259,6 +261,24 @@ func (txmp *TxMempool) RemoveTxByKey(txKey types.TxKey) error { return txmp.removeTxByKey(txKey) } +// GetTxByKey retrieves a transaction based on the key. It returns a bool +// indicating whether transaction was found in the cache. +func (txmp *TxMempool) GetTxByKey(txKey types.TxKey) (types.Tx, bool) { + txmp.mtx.RLock() + defer txmp.mtx.RUnlock() + + if elt, ok := txmp.txByKey[txKey]; ok { + return elt.Value.(*WrappedTx).tx, true + } + return nil, false +} + +// WasRecentlyEvicted returns a bool indicating whether the transaction with +// the specified key was recently evicted and is currently within the evicted cache. +func (txmp *TxMempool) WasRecentlyEvicted(txKey types.TxKey) bool { + return txmp.evictedTxs.HasKey(txKey) +} + // removeTxByKey removes the specified transaction key from the mempool. // The caller must hold txmp.mtx excluxively. func (txmp *TxMempool) removeTxByKey(key types.TxKey) error { @@ -549,6 +569,8 @@ func (txmp *TxMempool) addNewTransaction(wtx *WrappedTx, checkTxRes *abci.Respon fmt.Sprintf("rejected valid incoming transaction; mempool is full (%X)", wtx.tx.Hash()) txmp.metrics.EvictedTxs.With(mempool.TypeLabel, mempool.EvictedNewTxFullMempool).Add(1) + // Add it to evicted transactions cache + txmp.evictedTxs.Push(wtx.tx) return } @@ -581,7 +603,8 @@ func (txmp *TxMempool) addNewTransaction(wtx *WrappedTx, checkTxRes *abci.Respon txmp.removeTxByElement(vic) txmp.cache.Remove(w.tx) txmp.metrics.EvictedTxs.With(mempool.TypeLabel, mempool.EvictedExistingTxFullMempool).Add(1) - + // Add it to evicted transactions cache + txmp.evictedTxs.Push(w.tx) // We may not need to evict all the eligible transactions. Bail out // early if we have made enough room. evictedBytes += w.Size() @@ -772,9 +795,11 @@ func (txmp *TxMempool) purgeExpiredTxs(blockHeight int64) { txmp.removeTxByElement(cur) txmp.cache.Remove(w.tx) txmp.metrics.EvictedTxs.With(mempool.TypeLabel, mempool.EvictedTxExpiredBlocks).Add(1) + txmp.evictedTxs.Push(w.tx) } else if txmp.config.TTLDuration > 0 && now.Sub(w.timestamp) > txmp.config.TTLDuration { txmp.removeTxByElement(cur) txmp.cache.Remove(w.tx) + txmp.evictedTxs.Push(w.tx) txmp.metrics.EvictedTxs.With(mempool.TypeLabel, mempool.EvictedTxExpiredTime).Add(1) } cur = next diff --git a/mempool/v1/mempool_test.go b/mempool/v1/mempool_test.go index 7623ecbc90..88f9de3d0b 100644 --- a/mempool/v1/mempool_test.go +++ b/mempool/v1/mempool_test.go @@ -108,6 +108,8 @@ func mustCheckTx(t *testing.T, txmp *TxMempool, spec string) { <-done } +// checkTxs generates a specified number of txs, checks them into the mempool, +// and returns them. func checkTxs(t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx { txs := make([]testTx, numTxs) txInfo := mempool.TxInfo{SenderID: peerID} @@ -239,7 +241,9 @@ func TestTxMempool_Eviction(t *testing.T) { mustCheckTx(t, txmp, "key1=0000=25") require.True(t, txExists("key1=0000=25")) require.False(t, txExists(bigTx)) - require.False(t, txmp.cache.Has([]byte(bigTx))) + bigTxKey := types.Tx((bigTx)).Key() + require.False(t, txmp.cache.HasKey(bigTxKey)) + require.True(t, txmp.WasRecentlyEvicted(bigTxKey)) // bigTx evicted require.Equal(t, int64(len("key1=0000=25")), txmp.SizeBytes()) // Now fill up the rest of the slots with other transactions. @@ -251,13 +255,15 @@ func TestTxMempool_Eviction(t *testing.T) { // A new transaction with low priority should be discarded. mustCheckTx(t, txmp, "key6=0005=1") require.False(t, txExists("key6=0005=1")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx(("key6=0005=1")).Key())) // key6 evicted // A new transaction with higher priority should evict key5, which is the // newest of the two transactions with lowest priority. mustCheckTx(t, txmp, "key7=0006=7") - require.True(t, txExists("key7=0006=7")) // new transaction added - require.False(t, txExists("key5=0004=3")) // newest low-priority tx evicted - require.True(t, txExists("key4=0003=3")) // older low-priority tx retained + require.True(t, txExists("key7=0006=7")) // new transaction added + require.False(t, txExists("key5=0004=3")) // newest low-priority tx evicted + require.True(t, txmp.WasRecentlyEvicted(types.Tx(("key5=0004=3")).Key())) // key5 evicted + require.True(t, txExists("key4=0003=3")) // older low-priority tx retained // Another new transaction evicts the other low-priority element. mustCheckTx(t, txmp, "key8=0007=20") @@ -268,6 +274,7 @@ func TestTxMempool_Eviction(t *testing.T) { mustCheckTx(t, txmp, "key9=0008=9") require.True(t, txExists("key9=0008=9")) require.False(t, txExists("key2=0001=5")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx(("key2=0001=5")).Key())) // key2 evicted // Add a transaction that requires eviction of multiple lower-priority // entries, in order to fit the size of the element. @@ -276,8 +283,11 @@ func TestTxMempool_Eviction(t *testing.T) { require.True(t, txExists("key8=0007=20")) require.True(t, txExists("key10=0123456789abcdef=11")) require.False(t, txExists("key3=0002=10")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx(("key3=0002=10")).Key())) // key3 evicted require.False(t, txExists("key9=0008=9")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx(("key9=0008=9")).Key())) // key9 evicted require.False(t, txExists("key7=0006=7")) + require.True(t, txmp.WasRecentlyEvicted(types.Tx(("key7=0006=7")).Key())) // key7 evicted } func TestTxMempool_Flush(t *testing.T) { @@ -431,7 +441,7 @@ func TestTxMempool_ReapMaxTxs(t *testing.T) { } func TestTxMempool_CheckTxExceedsMaxSize(t *testing.T) { - txmp := setup(t, 0) + txmp := setup(t, 1) rng := rand.New(rand.NewSource(time.Now().UnixNano())) tx := make([]byte, txmp.config.MaxTxBytes+1) @@ -580,6 +590,10 @@ func TestTxMempool_ExpiredTxs_Timestamp(t *testing.T) { // All the transactions in the original set should have been purged. for _, tx := range added1 { + // Check that they were added to the evicted cache. + evicted := txmp.WasRecentlyEvicted(tx.tx.Key()) + require.True(t, evicted) + if _, ok := txmp.txByKey[tx.tx.Key()]; ok { t.Errorf("Transaction %X should have been purged for TTL", tx.tx.Key()) } @@ -596,6 +610,23 @@ func TestTxMempool_ExpiredTxs_Timestamp(t *testing.T) { } } +func TestGetTxByKey_GetsTx(t *testing.T) { + txmp := setup(t, 500) + txs := checkTxs(t, txmp, 100, 0) + + // Should get all valid txs + for _, tx := range txs { + txKey := tx.tx.Key() + txFromMempool, exists := txmp.GetTxByKey(txKey) + require.Equal(t, tx.tx, txFromMempool) + require.True(t, exists) + } + + // Non-existent tx should return false + _, exists := txmp.GetTxByKey(types.Tx("non-existent-tx").Key()) + require.False(t, exists) +} + func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { txmp := setup(t, 500) txmp.height = 100 @@ -662,7 +693,7 @@ func TestTxMempool_CheckTxPostCheckError(t *testing.T) { postCheckFn := func(_ types.Tx, _ *abci.ResponseCheckTx) error { return testCase.err } - txmp := setup(t, 0, WithPostCheck(postCheckFn)) + txmp := setup(t, 1, WithPostCheck(postCheckFn)) rng := rand.New(rand.NewSource(time.Now().UnixNano())) tx := make([]byte, txmp.config.MaxTxBytes-1) _, err := rng.Read(tx) diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 6d6c96056e..98831bc033 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -538,30 +538,52 @@ func TestBlockSearch(t *testing.T) { func TestTxStatus(t *testing.T) { c := getHTTPClient() + require := require.New(t) + mempool := node.Mempool() - // first we broadcast a few txs - var txHashes [][]byte - var txHeights []int64 - for i := 0; i < 10; i++ { - _, _, tx := MakeTxKV() + // Create a new transaction + _, _, tx := MakeTxKV() - result, err := c.BroadcastTxCommit(context.Background(), tx) - require.NoError(t, err) - txHashes = append(txHashes, result.Hash) - txHeights = append(txHeights, result.Height) - } + // Get the initial size of the mempool + initMempoolSize := mempool.Size() - require.NoError(t, client.WaitForHeight(c, 5, nil)) + // Add the transaction to the mempool + err := mempool.CheckTx(tx, nil, mempl.TxInfo{}) + require.NoError(err) - // check the status of each transaction - for i, hash := range txHashes { - result, err := c.TxStatus(context.Background(), hash) - require.NoError(t, err) + // Check if the size of the mempool has increased + require.Equal(initMempoolSize+1, mempool.Size()) - expectedIndex := int64(0) - require.Equal(t, txHeights[i], result.Height) - require.Equal(t, expectedIndex, result.Index) - } + // Get the tx status from the mempool + result, err := c.TxStatus(context.Background(), types.Tx(tx).Hash()) + require.NoError(err) + require.EqualValues(0, result.Height) + require.EqualValues(0, result.Index) + require.Equal("PENDING", result.Status) + + // Flush the mempool + mempool.Flush() + require.Equal(0, mempool.Size()) + + // Get tx status after flushing it from the mempool + result, err = c.TxStatus(context.Background(), types.Tx(tx).Hash()) + require.NoError(err) + require.EqualValues(0, result.Height) + require.EqualValues(0, result.Index) + require.Equal("UNKNOWN", result.Status) + + // Broadcast the tx again + bres, err := c.BroadcastTxCommit(context.Background(), tx) + require.NoError(err) + require.True(bres.CheckTx.IsOK()) + require.True(bres.DeliverTx.IsOK()) + + // Get the tx status + result, err = c.TxStatus(context.Background(), types.Tx(tx).Hash()) + require.NoError(err) + require.EqualValues(bres.Height, result.Height) + require.EqualValues(0, result.Index) + require.Equal("COMMITTED", result.Status) } func TestTxSearch(t *testing.T) { diff --git a/rpc/core/blocks.go b/rpc/core/blocks.go index 763d2b4ce8..d19757c688 100644 --- a/rpc/core/blocks.go +++ b/rpc/core/blocks.go @@ -178,17 +178,6 @@ func BlockByHash(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultBlock, error return &ctypes.ResultBlock{BlockID: blockMeta.BlockID, Block: block}, nil } -// TxStatus retrieves the status of a transaction given its hash. It returns a ResultTxStatus -// containing the height and index of the transaction within the block. -func TxStatus(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultTxStatus, error) { - env := GetEnvironment() - txInfo := env.BlockStore.LoadTxInfo(hash) - if txInfo == nil { - return &ctypes.ResultTxStatus{}, nil - } - return &ctypes.ResultTxStatus{Height: txInfo.Height, Index: txInfo.Index}, nil -} - // Commit gets block commit at a given height. // If no height is provided, it will fetch the commit for the latest block. // More: https://docs.cometbft.com/v0.34/rpc/#/Info/commit diff --git a/rpc/core/blocks_test.go b/rpc/core/blocks_test.go index 54a2cf4f64..7bddc82a88 100644 --- a/rpc/core/blocks_test.go +++ b/rpc/core/blocks_test.go @@ -126,31 +126,6 @@ func TestBlockResults(t *testing.T) { } } -func TestTxStatus(t *testing.T) { - env := &Environment{} - height := int64(50) - - blocks := randomBlocks(height) - blockStore := mockBlockStore{ - height: height, - blocks: blocks, - } - env.BlockStore = blockStore - - SetEnvironment(env) - - // Iterate over each block - for _, block := range blocks { - // Iterate over each transaction in the block - for i, tx := range block.Data.Txs { - txStatus, _ := TxStatus(&rpctypes.Context{}, tx.Hash()) - assert.Equal(t, block.Height, txStatus.Height) - assert.Equal(t, int64(i), txStatus.Index) - } - } - -} - func TestEncodeDataRootTuple(t *testing.T) { height := uint64(2) dataRoot, err := hex.DecodeString("82dc1607d84557d3579ce602a45f5872e821c36dbda7ec926dfa17ebc8d5c013") diff --git a/rpc/core/mocks/mempool.go b/rpc/core/mocks/mempool.go new file mode 100644 index 0000000000..57d750dad8 --- /dev/null +++ b/rpc/core/mocks/mempool.go @@ -0,0 +1,240 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./mempool/mempool.go + +// Package mock_mempool is a generated GoMock package. +package mock_mempool + +import ( + reflect "reflect" + + types "github.com/cometbft/cometbft/abci/types" + mempool "github.com/cometbft/cometbft/mempool" + types0 "github.com/cometbft/cometbft/types" + gomock "github.com/golang/mock/gomock" +) + +// MockMempool is a mock of Mempool interface. +type MockMempool struct { + ctrl *gomock.Controller + recorder *MockMempoolMockRecorder +} + +// MockMempoolMockRecorder is the mock recorder for MockMempool. +type MockMempoolMockRecorder struct { + mock *MockMempool +} + +// NewMockMempool creates a new mock instance. +func NewMockMempool(ctrl *gomock.Controller) *MockMempool { + mock := &MockMempool{ctrl: ctrl} + mock.recorder = &MockMempoolMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMempool) EXPECT() *MockMempoolMockRecorder { + return m.recorder +} + +// CheckTx mocks base method. +func (m *MockMempool) CheckTx(tx types0.Tx, callback func(*types.Response), txInfo mempool.TxInfo) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckTx", tx, callback, txInfo) + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckTx indicates an expected call of CheckTx. +func (mr *MockMempoolMockRecorder) CheckTx(tx, callback, txInfo interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckTx", reflect.TypeOf((*MockMempool)(nil).CheckTx), tx, callback, txInfo) +} + +// EnableTxsAvailable mocks base method. +func (m *MockMempool) EnableTxsAvailable() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "EnableTxsAvailable") +} + +// EnableTxsAvailable indicates an expected call of EnableTxsAvailable. +func (mr *MockMempoolMockRecorder) EnableTxsAvailable() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableTxsAvailable", reflect.TypeOf((*MockMempool)(nil).EnableTxsAvailable)) +} + +// Flush mocks base method. +func (m *MockMempool) Flush() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Flush") +} + +// Flush indicates an expected call of Flush. +func (mr *MockMempoolMockRecorder) Flush() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flush", reflect.TypeOf((*MockMempool)(nil).Flush)) +} + +// FlushAppConn mocks base method. +func (m *MockMempool) FlushAppConn() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FlushAppConn") + ret0, _ := ret[0].(error) + return ret0 +} + +// FlushAppConn indicates an expected call of FlushAppConn. +func (mr *MockMempoolMockRecorder) FlushAppConn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushAppConn", reflect.TypeOf((*MockMempool)(nil).FlushAppConn)) +} + +// GetTxByKey mocks base method. +func (m *MockMempool) GetTxByKey(key types0.TxKey) (types0.Tx, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTxByKey", key) + ret0, _ := ret[0].(types0.Tx) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetTxByKey indicates an expected call of GetTxByKey. +func (mr *MockMempoolMockRecorder) GetTxByKey(key interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTxByKey", reflect.TypeOf((*MockMempool)(nil).GetTxByKey), key) +} + +// WasRecentlyEvicted mocks base method. +func (m *MockMempool) WasRecentlyEvicted(key types0.TxKey) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WasRecentlyEvicted", key) + ret0, _ := ret[0].(bool) + return ret0 +} + +// WasRecentlyEvicted indicates an expected call of WasRecentlyEvicted. +func (mr *MockMempoolMockRecorder) WasRecentlyEvicted(key interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WasRecentlyEvicted", reflect.TypeOf((*MockMempool)(nil).WasRecentlyEvicted), key) +} + +// Lock mocks base method. +func (m *MockMempool) Lock() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Lock") +} + +// Lock indicates an expected call of Lock. +func (mr *MockMempoolMockRecorder) Lock() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockMempool)(nil).Lock)) +} + +// ReapMaxBytesMaxGas mocks base method. +func (m *MockMempool) ReapMaxBytesMaxGas(maxBytes, maxGas int64) types0.Txs { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReapMaxBytesMaxGas", maxBytes, maxGas) + ret0, _ := ret[0].(types0.Txs) + return ret0 +} + +// ReapMaxBytesMaxGas indicates an expected call of ReapMaxBytesMaxGas. +func (mr *MockMempoolMockRecorder) ReapMaxBytesMaxGas(maxBytes, maxGas interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReapMaxBytesMaxGas", reflect.TypeOf((*MockMempool)(nil).ReapMaxBytesMaxGas), maxBytes, maxGas) +} + +// ReapMaxTxs mocks base method. +func (m *MockMempool) ReapMaxTxs(max int) types0.Txs { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReapMaxTxs", max) + ret0, _ := ret[0].(types0.Txs) + return ret0 +} + +// ReapMaxTxs indicates an expected call of ReapMaxTxs. +func (mr *MockMempoolMockRecorder) ReapMaxTxs(max interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReapMaxTxs", reflect.TypeOf((*MockMempool)(nil).ReapMaxTxs), max) +} + +// RemoveTxByKey mocks base method. +func (m *MockMempool) RemoveTxByKey(txKey types0.TxKey) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveTxByKey", txKey) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveTxByKey indicates an expected call of RemoveTxByKey. +func (mr *MockMempoolMockRecorder) RemoveTxByKey(txKey interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTxByKey", reflect.TypeOf((*MockMempool)(nil).RemoveTxByKey), txKey) +} + +// Size mocks base method. +func (m *MockMempool) Size() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Size") + ret0, _ := ret[0].(int) + return ret0 +} + +// Size indicates an expected call of Size. +func (mr *MockMempoolMockRecorder) Size() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Size", reflect.TypeOf((*MockMempool)(nil).Size)) +} + +// SizeBytes mocks base method. +func (m *MockMempool) SizeBytes() int64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SizeBytes") + ret0, _ := ret[0].(int64) + return ret0 +} + +// SizeBytes indicates an expected call of SizeBytes. +func (mr *MockMempoolMockRecorder) SizeBytes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SizeBytes", reflect.TypeOf((*MockMempool)(nil).SizeBytes)) +} + +// TxsAvailable mocks base method. +func (m *MockMempool) TxsAvailable() <-chan struct{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxsAvailable") + ret0, _ := ret[0].(<-chan struct{}) + return ret0 +} + +// TxsAvailable indicates an expected call of TxsAvailable. +func (mr *MockMempoolMockRecorder) TxsAvailable() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxsAvailable", reflect.TypeOf((*MockMempool)(nil).TxsAvailable)) +} + +// Unlock mocks base method. +func (m *MockMempool) Unlock() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Unlock") +} + +// Unlock indicates an expected call of Unlock. +func (mr *MockMempoolMockRecorder) Unlock() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockMempool)(nil).Unlock)) +} + +// Update mocks base method. +func (m *MockMempool) Update(blockHeight int64, blockTxs types0.Txs, deliverTxResponses []*types.ResponseDeliverTx, newPreFn mempool.PreCheckFunc, newPostFn mempool.PostCheckFunc) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", blockHeight, blockTxs, deliverTxResponses, newPreFn, newPostFn) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockMempoolMockRecorder) Update(blockHeight, blockTxs, deliverTxResponses, newPreFn, newPostFn interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockMempool)(nil).Update), blockHeight, blockTxs, deliverTxResponses, newPreFn, newPostFn) +} diff --git a/rpc/core/tx.go b/rpc/core/tx.go index 01d4cbc0b4..a25cd477cb 100644 --- a/rpc/core/tx.go +++ b/rpc/core/tx.go @@ -17,6 +17,13 @@ import ( "github.com/cometbft/cometbft/types" ) +const ( + txStatusUnknown string = "UNKNOWN" + txStatusPending string = "PENDING" + txStatusEvicted string = "EVICTED" + txStatusCommitted string = "COMMITTED" +) + // Tx allows you to query the transaction results. `nil` could mean the // transaction is in the mempool, invalidated, or was not sent in the first // place. @@ -214,6 +221,40 @@ func ProveShares( return &ctypes.ResultShareProof{Proof: shareProof}, nil } +// TxStatus retrieves the status of a transaction given its hash. It returns a ResultTxStatus +// containing the height and index of the transaction within the block(if committed) +// or whether the transaction is pending, evicted from the mempool, or otherwise unknown. +func TxStatus(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultTxStatus, error) { + env := GetEnvironment() + + // Check if the tx has been committed + txInfo := env.BlockStore.LoadTxInfo(hash) + if txInfo != nil { + return &ctypes.ResultTxStatus{Height: txInfo.Height, Index: txInfo.Index, Status: txStatusCommitted}, nil + } + + // Get the tx key from the hash + txKey, err := types.TxKeyFromBytes(hash) + if err != nil { + return nil, fmt.Errorf("failed to get tx key from hash: %v", err) + } + + // Check if the tx is in the mempool + txInMempool, ok := env.Mempool.GetTxByKey(txKey) + if txInMempool != nil && ok { + return &ctypes.ResultTxStatus{Status: txStatusPending}, nil + } + + // Check if the tx is evicted + isEvicted := env.Mempool.WasRecentlyEvicted(txKey) + if isEvicted { + return &ctypes.ResultTxStatus{Status: txStatusEvicted}, nil + } + + // If the tx is not in the mempool, evicted, or committed, return unknown + return &ctypes.ResultTxStatus{Status: txStatusUnknown}, nil +} + func loadRawBlock(bs state.BlockStore, height int64) ([]byte, error) { var blockMeta = bs.LoadBlockMeta(height) if blockMeta == nil { diff --git a/rpc/core/tx_status_test.go b/rpc/core/tx_status_test.go new file mode 100644 index 0000000000..d91afd468c --- /dev/null +++ b/rpc/core/tx_status_test.go @@ -0,0 +1,133 @@ +package core + +import ( + "testing" + + mock "github.com/cometbft/cometbft/rpc/core/mocks" + rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" + types "github.com/cometbft/cometbft/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +// TestTxStatus tests the TxStatus function in the RPC core +// making sure it fetches the correct status for each transaction. +func TestTxStatus(t *testing.T) { + // Create a controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a new environment + env := &Environment{} + + // Create a new mempool and block store + mempool := mock.NewMockMempool(ctrl) + env.Mempool = mempool + blockStore := mockBlockStore{ + height: 0, + blocks: nil, + } + env.BlockStore = blockStore + SetEnvironment(env) + + tests := []struct { + name string + setup func(*Environment, []types.Tx) + expectedStatus string + }{ + { + name: "Committed", + setup: func(env *Environment, txs []types.Tx) { + height := int64(5) + blocks := randomBlocks(height) + blockStore = mockBlockStore{ + height: height, + blocks: blocks, + } + env.BlockStore = blockStore + }, + expectedStatus: "COMMITTED", + }, + { + name: "Unknown", + setup: func(env *Environment, txs []types.Tx) { + env.BlockStore = mockBlockStore{ + height: 0, + blocks: nil, + } + for _, tx := range txs { + // Set GetTxByKey to return nil and false for all transactions + mempool.EXPECT().GetTxByKey(tx.Key()).Return(nil, false).AnyTimes() + // Set WasRecentlyEvicted to return false for all transactions + mempool.EXPECT().WasRecentlyEvicted(tx.Key()).Return(false).AnyTimes() + } + }, + + expectedStatus: "UNKNOWN", + }, + { + name: "Pending", + setup: func(env *Environment, txs []types.Tx) { + env.BlockStore = mockBlockStore{ + height: 0, + blocks: nil, + } + // Reset the mempool + mempool = mock.NewMockMempool(ctrl) + env.Mempool = mempool + + for _, tx := range txs { + // Set GetTxByKey to return the transaction and true for all transactions + mempool.EXPECT().GetTxByKey(tx.Key()).Return(tx, true).AnyTimes() + } + }, + expectedStatus: "PENDING", + }, + { + name: "Evicted", + setup: func(env *Environment, txs []types.Tx) { + env.BlockStore = mockBlockStore{ + height: 0, + blocks: nil, + } + // Reset the mempool + mempool = mock.NewMockMempool(ctrl) + env.Mempool = mempool + + for _, tx := range txs { + // Set GetTxByKey to return nil and false for all transactions + mempool.EXPECT().GetTxByKey(tx.Key()).Return(nil, false).AnyTimes() + // Set WasRecentlyEvicted to return true for all transactions + mempool.EXPECT().WasRecentlyEvicted(tx.Key()).Return(true).AnyTimes() + } + }, + expectedStatus: "EVICTED", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + height := int64(2) + // Create a set of transactions on the specified height + txs := makeTxs(height) + + tt.setup(env, txs) + + // Check the status of each transaction + for i, tx := range txs { + txStatus, _ := TxStatus(&rpctypes.Context{}, tx.Hash()) + assert.Equal(t, tt.expectedStatus, txStatus.Status) + + // Check the height and index of transactions that are committed + if blockStore.height > 0 && tt.expectedStatus == "COMMITTED" { + txStatus, _ := TxStatus(&rpctypes.Context{}, tx.Hash()) + + assert.Equal(t, txStatus.Status, tt.expectedStatus) + assert.Equal(t, height, txStatus.Height) + assert.Equal(t, int64(i), txStatus.Index) + } + } + + }) + } +} diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index bacf149cd1..c1d7f0900b 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -62,8 +62,9 @@ type ResultCommit struct { // ResultTxStatus contains info to locate a tx in a committed block. type ResultTxStatus struct { - Height int64 `json:"height"` - Index int64 `json:"index"` + Height int64 `json:"height"` + Index int64 `json:"index"` + Status string `json:"status"` } // ABCI results from a block diff --git a/state/mocks/block_store.go b/state/mocks/block_store.go index 98f47dc320..21fe049f5f 100644 --- a/state/mocks/block_store.go +++ b/state/mocks/block_store.go @@ -3,9 +3,9 @@ package mocks import ( - mock "github.com/stretchr/testify/mock" cmtstore "github.com/cometbft/cometbft/proto/tendermint/store" types "github.com/cometbft/cometbft/types" + mock "github.com/stretchr/testify/mock" ) // BlockStore is an autogenerated mock type for the BlockStore type diff --git a/test/maverick/consensus/replay_stubs.go b/test/maverick/consensus/replay_stubs.go index d8ea335c12..9fc3b45013 100644 --- a/test/maverick/consensus/replay_stubs.go +++ b/test/maverick/consensus/replay_stubs.go @@ -40,6 +40,9 @@ func (emptyMempool) TxsAvailable() <-chan struct{} { return make(chan struct{}) func (emptyMempool) EnableTxsAvailable() {} func (emptyMempool) TxsBytes() int64 { return 0 } +func (emptyMempool) GetTxByKey(txKey types.TxKey) (types.Tx, bool) { return nil, false } +func (emptyMempool) WasRecentlyEvicted(txKey types.TxKey) bool { return false } + func (emptyMempool) TxsFront() *clist.CElement { return nil } func (emptyMempool) TxsWaitChan() <-chan struct{} { return nil }